1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
|
Developing deploy and clean steps
=================================
Deploy steps basics
-------------------
To support customized deployment step, implement a new method in an interface
class and use the decorator ``deploy_step`` defined in
``ironic/drivers/base.py``. For example, we will implement a ``do_nothing``
deploy step in the ``AgentDeploy`` class.
.. code-block:: python
from ironic.drivers.modules import agent
class AgentDeploy(agent.AgentDeploy):
@base.deploy_step(priority=200, argsinfo={
'test_arg': {
'description': (
"This is a test argument."
),
'required': True
}
})
def do_nothing(self, task, **kwargs):
return None
If you want to completely replace the deployment procedure, but still have the
agent up and running, inherit ``CustomAgentDeploy``:
.. code-block:: python
from ironic.drivers.modules import agent
class AgentDeploy(agent.CustomAgentDeploy):
def validate(self, task):
super().validate(task)
# ... custom validation
@base.deploy_step(priority=80)
def my_write_image(self, task, **kwargs):
pass # ... custom image writing
@base.deploy_step(priority=70)
def my_configure_bootloader(self, task, **kwargs):
pass # ... custom bootloader configuration
After deployment of the baremetal node, check the updated deploy steps::
baremetal node show $node_ident -f json -c driver_internal_info
The above command outputs the ``driver_internal_info`` as following::
{
"driver_internal_info": {
...
"deploy_steps": [
{
"priority": 200,
"interface": "deploy",
"step": "do_nothing",
"argsinfo":
{
"test_arg":
{
"required": True,
"description": "This is a test argument."
}
}
},
{
"priority": 100,
"interface": "deploy",
"step": "deploy",
"argsinfo": null
}
],
"deploy_step_index": 1
}
}
In-band deploy steps (deploy steps that are run inside the ramdisk) have to be
implemented in a custom :ironic-python-agent-doc:`IPA hardware manager
<contributor/hardware_managers.html#custom-hardwaremanagers-and-deploying>`.
All in-band deploy steps must have priorities between 41 and 99, see
:ref:`node-deployment-core-steps` for details.
Clean steps basics
------------------
Clean steps are written similarly to deploy steps, but are executed during
:doc:`cleaning </admin/cleaning>`. Steps with priority > 0 are executed during
automated cleaning, all steps can be executed explicitly during manual
cleaning. Unlike deploy steps, clean steps are commonly found in these
interfaces:
``bios``
Steps that apply BIOS settings, see `Implementing BIOS settings`_.
``deploy``
Steps that undo the effect of deployment (e.g. erase disks).
``management``
Additional steps that use the node's BMC, such as out-of-band firmware
update or BMC reset.
``raid``
Steps that build or tear down RAID, see `Implementing RAID`_.
.. note::
When designing a new step for your driver, try to make it consistent with
existing steps on other drivers.
Just as deploy steps, in-band clean steps have to be
implemented in a custom :ironic-python-agent-doc:`IPA hardware manager
<contributor/hardware_managers.html#custom-hardwaremanagers-and-cleaning>`.
Asynchronous steps
------------------
If the step returns ``None``, ironic assumes its execution is finished and
proceeds to the next step. Many steps are executed asynchronously; in this case
you need to inform ironic that the step is not finished. There are several
possibilities:
Combined in-band and out-of-band step
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If your step starts as out-of-band and then proceeds as in-band (i.e. inside
the agent), you only need to return ``CLEANWAIT``/``DEPLOYWAIT`` from
the step.
.. code-block:: python
from ironic.drivers import base
from ironic.drivers.modules import agent
from ironic.drivers.modules import agent_base
from ironic.drivers.modules import agent_client
from ironic.drivers.modules import deploy_utils
class MyDeploy(agent.CustomAgentDeploy):
...
@base.deploy_step(priority=80)
def my_deploy(self, task):
...
return deploy_utils.get_async_step_return_state(task.node)
# Usually you can use a more high-level pattern:
@base.deploy_step(priority=60)
def my_deploy2(self, task):
new_step = {'interface': 'deploy',
'step': 'my_deploy2',
'args': {...}}
client = agent_client.get_client(task)
return agent_base.execute_step(task, new_step, 'deploy',
client=client)
.. warning::
This approach only works for steps implemented on a ``deploy``
interface that inherits agent deploy.
Execution on reboot
~~~~~~~~~~~~~~~~~~~
Some steps are executed out-of-band, but require a reboot to complete. Use the
following pattern:
.. code-block:: python
from ironic.drivers import base
from ironic.drivers.modules import deploy_utils
class MyManagement(base.ManagementInterface):
...
@base.clean_step(priority=0)
def my_action(self, task):
...
# Tell ironic that...
deploy_utils.set_async_step_flags(
node,
# ... we're waiting for IPA to come back after reboot
reboot=True,
# ... the current step is done
skip_current_step=True)
return deploy_utils.reboot_to_finish_step(task)
.. _deploy-steps-polling:
Polling for completion
~~~~~~~~~~~~~~~~~~~~~~~
Finally, you may want to poll the BMC until the operation is complete. Often
enough, this also involves a reboot. In this case you can use the
:py:func:`ironic.conductor.periodics.node_periodic` decorator to create a
periodic task that operates on relevant nodes:
.. code-block:: python
from ironic.common import states
from ironic.common import utils
from ironic.conductor import periodics
from ironic.drivers import base
from ironic.drivers.modules import deploy_utils
_STATUS_CHECK_INTERVAL = ... # better use a configuration option
class MyManagement(base.ManagementInterface):
...
@base.clean_step(priority=0)
def my_action(self, task):
...
reboot_required = ... # your step may or may not need rebooting
# Make this node as running my_action. Often enough you will store
# some useful data rather than a boolean flag.
utils.set_node_nested_field(task.node, 'driver_internal_info',
'in_my_action', True)
# Tell ironic that...
deploy_utils.set_async_step_flags(
node,
# ... we're waiting for IPA to come back after reboot
reboot=reboot_required,
# ... the current step shouldn't be entered again
skip_current_step=True,
# ... we'll be polling until the step is done
polling=True)
if reboot_required:
return deploy_utils.reboot_to_finish_step(task)
@periodics.node_periodic(
purpose='checking my action status',
spacing=_STATUS_CHECK_INTERVAL,
filters={
# Skip nodes that already have a lock
'reserved': False,
# Only consider nodes that are waiting for cleaning or failed
# on timeout.
'provision_state_in': [states.CLEANWAIT, states.CLEANFAIL],
},
# Load driver_internal_info from the database on listing
predicate_extra_fields=['driver_internal_info'],
# Only consider nodes with in_my_action
predicate=lambda n: n.driver_internal_info.get('in_my_action'),
)
def check_my_action(self, task, manager, context):
if not needs_actions(): # insert your checks here
return
task.upgrade_lock()
... # do any required updates
# Drop the flag so that this node is no longer considered
utils.pop_node_nested_field(task.node, 'driver_internal_info',
'in_my_action')
Note that creating a ``task`` involves an additional database query, so you
want to avoid creating them for too many nodes in your periodic tasks. Instead:
* Try to use precise ``filters`` to filter out nodes on the database level.
Using ``reserved`` and ``provision_state``/``provision_state_in`` are
recommended in most cases. See
:py:meth:`ironic.db.api.Connection.get_nodeinfo_list` for a list of possible
filters.
* Use ``predicate`` to filter on complex fields such as
``driver_internal_info``. Predicates are checked before tasks are created.
Implementing RAID
-----------------
RAID is implemented via deploy and clean steps in the ``raid`` interfaces.
By convention they have the following signatures:
.. code-block:: python
from ironic.drivers import base
class MyRAID(base.RAIDInterface):
@base.clean_step(priority=0, abortable=False, argsinfo={
'create_root_volume': {
'description': (
'This specifies whether to create the root volume. '
'Defaults to `True`.'
),
'required': False
},
'create_nonroot_volumes': {
'description': (
'This specifies whether to create the non-root volumes. '
'Defaults to `True`.'
),
'required': False
},
'delete_existing': {
'description': (
'Setting this to `True` indicates to delete existing RAID '
'configuration prior to creating the new configuration. '
'Default value is `False`.'
),
'required': False,
}
})
def create_configuration(self, task, create_root_volume=True,
create_nonroot_volumes=True,
delete_existing=False):
pass
@base.clean_step(priority=0)
@base.deploy_step(priority=0)
def delete_configuration(self, task):
pass
@base.deploy_step(priority=0,
argsinfo=base.RAID_APPLY_CONFIGURATION_ARGSINFO)
def apply_configuration(self, task, raid_config,
create_root_volume=True,
create_nonroot_volumes=False,
delete_existing=False):
pass
Notes:
* ``create_configuration`` only works as a clean step, during deployment
``apply_configuration`` is used instead.
* ``apply_configuration`` accepts the target RAID configuration explicitly,
while ``create_configuration`` uses the node's ``target_raid_config`` field.
* Priorities default to 0 since RAID should not be built by default.
Implementing BIOS settings
--------------------------
BIOS is implemented via deploy and clean steps in the ``raid`` interfaces.
By convention they have the following signatures:
.. code-block:: python
from ironic.drivers import base
_APPLY_CONFIGURATION_ARGSINFO = {
'settings': {
'description': (
'A list of BIOS settings to be applied'
),
'required': True
}
}
class MyBIOS(base.BIOSInterface):
@base.clean_step(priority=0)
@base.deploy_step(priority=0)
@base.cache_bios_settings
def factory_reset(self, task):
pass
@base.clean_step(priority=0, argsinfo=_APPLY_CONFIGURATION_ARGSINFO)
@base.deploy_step(priority=0, argsinfo=_APPLY_CONFIGURATION_ARGSINFO)
@base.cache_bios_settings
def apply_configuration(self, task, settings):
pass
Notes:
* Both ``factory_reset`` and ``apply_configuration`` can be used as deploy
and clean steps.
* The ``cache_bios_settings`` decorator is used to ensure that the settings
cached in the ironic database is updated.
* Priorities default to 0 since BIOS settings should not be modified
by default.
|