summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPavlo Shchelokovskyy <shchelokovskyy@gmail.com>2017-06-12 11:16:36 +0000
committerPavlo Shchelokovskyy <shchelokovskyy@gmail.com>2017-06-12 13:47:18 +0000
commitb963a18c63bdf0eb682faf4b81708d02830f65e5 (patch)
treeea2d327ded6b21478b0c48370dbb871b21ea7ce9
parentc67e89c1f1abc0fc4352ccff8d6536c0d29645e5 (diff)
[ansible] Major changes in playbooks "API"
Possibly existing out-of-tree playbooks will be imcompatible with this version and must be rewritten! Changes include: - all info passed into ansible playbooks from ironic is now available in the playbooks as elements of 'ironic' dictionary to better differentiate those from other vars possibly created/set inside playbooks. - any field of node's instance_info having a form of "image_<field>" is now available in playbooks as "ironic.image.<field>" var. - 'parted' tag in playbooks is removed and instead differentiation between partition and whole-disk imaged is being done based on ironic.image.type var value. - 'shutdown' tag is removed, and soft power-off is moved to a separate playbook, defined by new driver_info field 'ansible_shutdown_playbook' ('shutdown.yaml' by default) - default 'deploy' role is split into smaller roles, each targeting a separate stage of deployment process to faciliate customiation and re-use - discover - e.g. set root device and image target - prepare - if needed, prepare system, e.g. create partitions - deploy - download/convert/write user image and configdrive - configure - post-deployment steps, e.g. installing the bootloader Documentation is updated. Change-Id: I158a96d26dc9a114b6b607267c13e3ee1939cac9
Notes
Notes (review): Code-Review+2: Dmitry Tantsur <divius.inside@gmail.com> Code-Review+2: Vladyslav Drok <vdrok@mirantis.com> Workflow+1: Vladyslav Drok <vdrok@mirantis.com> Verified+2: Jenkins Submitted-by: Jenkins Submitted-at: Thu, 15 Jun 2017 09:58:00 +0000 Reviewed-on: https://review.openstack.org/410352 Project: openstack/ironic-staging-drivers Branch: refs/heads/master
-rw-r--r--doc/source/drivers/ansible.rst196
-rw-r--r--ironic_staging_drivers/ansible/deploy.py45
-rw-r--r--ironic_staging_drivers/ansible/playbooks/add-ironic-nodes.yaml2
-rw-r--r--ironic_staging_drivers/ansible/playbooks/deploy.yaml10
-rwxr-xr-xironic_staging_drivers/ansible/playbooks/roles/configure/files/install_grub.sh (renamed from ironic_staging_drivers/ansible/playbooks/roles/deploy/files/install_grub.sh)5
-rw-r--r--ironic_staging_drivers/ansible/playbooks/roles/configure/tasks/grub.yaml (renamed from ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/grub.yaml)2
-rw-r--r--ironic_staging_drivers/ansible/playbooks/roles/configure/tasks/main.yaml2
-rwxr-xr-xironic_staging_drivers/ansible/playbooks/roles/deploy/files/partition_configdrive.sh33
-rw-r--r--ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/configdrive.yaml36
-rw-r--r--ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/download.yaml11
-rw-r--r--ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/main.yaml17
-rw-r--r--ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/write.yaml8
-rw-r--r--ironic_staging_drivers/ansible/playbooks/roles/discover/tasks/main.yaml (renamed from ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/root-device.yaml)0
-rw-r--r--ironic_staging_drivers/ansible/playbooks/roles/prepare/tasks/main.yaml2
-rw-r--r--ironic_staging_drivers/ansible/playbooks/roles/prepare/tasks/parted.yaml (renamed from ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/parted.yaml)16
-rw-r--r--ironic_staging_drivers/ansible/playbooks/shutdown.yaml6
-rw-r--r--ironic_staging_drivers/tests/unit/ansible/test_deploy.py95
-rw-r--r--releasenotes/notes/ansible-change-api-510961a1132a2ced.yaml39
18 files changed, 296 insertions, 229 deletions
diff --git a/doc/source/drivers/ansible.rst b/doc/source/drivers/ansible.rst
index a33e7b1..1865f3d 100644
--- a/doc/source/drivers/ansible.rst
+++ b/doc/source/drivers/ansible.rst
@@ -9,7 +9,7 @@ and requiring no agents running on the node being configured.
9All communications with the node are by default performed over secure SSH 9All communications with the node are by default performed over secure SSH
10transport. 10transport.
11 11
12This deployment driver is using Ansible playbooks to define the 12The Ansible-deploy deployment driver is using Ansible playbooks to define the
13deployment logic. It is not based on `Ironic Python Agent`_ (IPA) 13deployment logic. It is not based on `Ironic Python Agent`_ (IPA)
14and does not generally need it to be running in the deploy ramdisk. 14and does not generally need it to be running in the deploy ramdisk.
15 15
@@ -44,8 +44,7 @@ CLI command via Python's ``subprocess`` library.
44 44
45Each action (deploy, clean) is described by single playbook with roles, 45Each action (deploy, clean) is described by single playbook with roles,
46which is run whole during deployment, or tag-wise during cleaning. 46which is run whole during deployment, or tag-wise during cleaning.
47Control of deployment types and cleaning steps is through tags and 47Control of cleaning steps is through tags and auxiliary clean steps file.
48auxiliary steps file for cleaning.
49The playbooks for actions can be set per-node, as is cleaning steps 48The playbooks for actions can be set per-node, as is cleaning steps
50file. 49file.
51 50
@@ -76,7 +75,7 @@ Configdrive partition
76~~~~~~~~~~~~~~~~~~~~~ 75~~~~~~~~~~~~~~~~~~~~~
77 76
78Creating a configdrive partition is supported for both whole disk 77Creating a configdrive partition is supported for both whole disk
79and partition images, on both ``msdos`` and ``GPT`` labeled disks. 78and partition images.
80 79
81Root device hints 80Root device hints
82~~~~~~~~~~~~~~~~~ 81~~~~~~~~~~~~~~~~~
@@ -107,9 +106,9 @@ Logging
107 106
108Logging is implemented as custom Ansible callback module, 107Logging is implemented as custom Ansible callback module,
109that makes use of ``oslo.log`` and ``oslo.config`` libraries 108that makes use of ``oslo.log`` and ``oslo.config`` libraries
110and can interleave Ansible event log into the log file configured in 109and can re-use logging configuration defined in the main ironic configuration
111main ironic configuration file (``/etc/ironic/ironic.conf`` by default), 110file (``/etc/ironic/ironic.conf`` by default) to set logging for Ansible
112or use a separate file to log Ansible events into. 111events, or use a separate file for this purpose.
113 112
114.. note:: 113.. note::
115 Currently this has some quirks in DevStack - due to default 114 Currently this has some quirks in DevStack - due to default
@@ -118,13 +117,11 @@ or use a separate file to log Ansible events into.
118 DevStack in 'developer' mode using ``screen``. 117 DevStack in 'developer' mode using ``screen``.
119 118
120 119
121
122Requirements 120Requirements
123============ 121============
124 122
125ironic 123ironic
126 Requires ironic API ≥ 1.22 when using callback functionality. 124 Requires ironic of Newton release or newer.
127 For better logging, ironic should be > 6.1.0 release.
128 125
129Ansible 126Ansible
130 Tested with and targets Ansible ≥ 2.1 127 Tested with and targets Ansible ≥ 2.1
@@ -144,7 +141,7 @@ Bootstrap image requirements
144- python-netifaces (for ironic callback) 141- python-netifaces (for ironic callback)
145 142
146Set of scripts to build a suitable deploy ramdisk based on TinyCore Linux, 143Set of scripts to build a suitable deploy ramdisk based on TinyCore Linux,
147and an element for ``diskimage-builder`` will be provided. 144and an element for ``diskimage-builder`` is provided.
148 145
149Setting up your environment 146Setting up your environment
150=========================== 147===========================
@@ -280,6 +277,11 @@ ansible_deploy_playbook
280 to use when deploying this node. 277 to use when deploying this node.
281 Default is ``deploy.yaml``. 278 Default is ``deploy.yaml``.
282 279
280ansible_shutdown_playbook
281 Name of the playbook file inside the ``playbooks_path`` folder
282 to use to gracefully shutdown the node in-band.
283 Default is ``shutdown.yaml``.
284
283ansible_clean_playbook 285ansible_clean_playbook
284 Name of the playbook file inside the ``playbooks_path`` folder 286 Name of the playbook file inside the ``playbooks_path`` folder
285 to use when cleaning the node. 287 to use when cleaning the node.
@@ -336,6 +338,23 @@ add-ironic-nodes.yaml
336 as well as some per-node variables. 338 as well as some per-node variables.
337 Include it in all your custom playbooks as the first play. 339 Include it in all your custom playbooks as the first play.
338 340
341The default ``deploy.yaml`` playbook is using several smaller roles that
342correspond to particular stages of deployment process:
343
344 - ``discover`` - e.g. set root device and image target
345 - ``prepare`` - if needed, prepare system, for example create partitions
346 - ``deploy`` - download/convert/write user image and configdrive
347 - ``configure`` - post-deployment steps, e.g. installing the bootloader
348
349Some more included roles are:
350
351 - ``wait`` - used when the driver is configured to not use callback from
352 node to start the deployment. This role waits for OpenSSH server to
353 become available on the node to connect to.
354 - ``shutdown`` - used to gracefully power the node off in-band
355 - ``clean`` - defines cleaning procedure, with each clean step defined
356 as separate playbook tag.
357
339Extending playbooks 358Extending playbooks
340------------------- 359-------------------
341 360
@@ -344,14 +363,19 @@ Most probably you'd start experimenting like this:
344#. Create a copy of ``deploy.yaml`` playbook, name it distinctively. 363#. Create a copy of ``deploy.yaml`` playbook, name it distinctively.
345#. Create Ansible roles with your customized logic in ``roles`` folder. 364#. Create Ansible roles with your customized logic in ``roles`` folder.
346 365
347 A. Add the role with logic to be run *before* image download/writing 366 A. In your custom deploy playbook, replace the ``prepare`` role
348 as the first role in your playbook. This is a good place to 367 with your own one that defines steps to be run
349 set facts overriding those provided/omitted by the driver, 368 *before* image download/writing.
350 like ``ironic_partitions`` or ``ironic_root_device``. 369 This is a good place to set facts overriding those provided/omitted
351 B. Add the role with logic to be run *after* image is written to disk 370 by the driver, like ``ironic_partitions`` or ``ironic_root_device``,
352 as second-to-last role in the playbook (right before ``shutdown`` role). 371 and create custom partitions or (software) RAIDs.
353 372 B. In your custom deploy playbook, replace the ``configure`` role
354#. Assign the playbook you've created to the node's 373 with your own one that defines steps to be run
374 *after* image is written to disk.
375 This is a good place for example to configure the bootloader and
376 add kernel options to avoid additional reboots.
377
378#. Assign the custom deploy playbook you've created to the node's
355 ``driver_info/ansible_deploy_playbook`` field. 379 ``driver_info/ansible_deploy_playbook`` field.
356#. Run deployment. 380#. Run deployment.
357 381
@@ -364,93 +388,82 @@ Most probably you'd start experimenting like this:
364Variables you have access to 388Variables you have access to
365---------------------------- 389----------------------------
366 390
367This driver will pass the following extra arguments to ``ansible-playbook`` 391This driver will pass the single JSON-ified extra var argument to
368invocation which you can use in your plays as well 392Ansible (as ``ansible-playbook -e ..``).
393Those values are then accessible in your plays as well
369(some of them are optional and might not be defined): 394(some of them are optional and might not be defined):
370 395
371``image`` 396.. code-block:: yaml
372 Dictionary of the following structure: 397
373 398
374 .. code-block:: json 399 ironic:
375 400 nodes:
376 {"image": { 401 - ip: <IPADDRESS>
377 "url": "<url-to-user-image>", 402 name: <NODE_UUID>
378 "disk_format": "<qcow|raw|..>", 403 user: <USER ANSIBLE WILL USE>
379 "checksum": "<hash-algo:hash>", 404 extra: <COPY OF NODE's EXTRA FIELD>
380 "mem_req": 12345 405 image:
381 } 406 url: <URL TO FETCH THE USER IMAGE FROM>
382 } 407 disk_format: <qcow2|raw|...>
383 408 container_format: <bare|...>
384 where 409 checksum: <hash-algo:hashstring>
385 410 mem_req: <REQUIRED FREE MEMORY TO DOWNLOAD IMAGE TO RAM>
386 - ``url`` - URL to download the target image from as set in 411 tags: <LIST OF IMAGE TAGS AS DEFINED IN GLANCE>
387 ``instance_info/image_url``. 412 properties: <DICT OF IMAGE PROPERTIES AS DEFINED IN GLANCE>
388 - ``disk_format`` - fetched from Glance or set in 413 configdrive:
389 ``instance_info/image_disk_format``. 414 type: <url|file>
390 Mainly used to distinguish ``raw`` images that can be streamed directly 415 location: <URL OR PATH ON CONDUCTOR>
391 to disk. 416 partition_info:
392 - ``checksum`` - (optional) image checksum as fetched from Glance or set 417 preserve_ephemeral: <bool>
393 in ``instance_info/image_checksum``. Used to verify downloaded image. 418 ephemeral_format: <FILESYSTEM TO CREATE ON EPHEMERAL PARTITION>
394 When deploying from Glance, this will always be ``md5`` checksum. 419 partitions: <LIST OF PARTITIONS IN FORMAT EXPECTED BY PARTED MODULE>
395 When deploying standalone, can also be set in the form ``<algo>:<hash>`` 420
396 to specify another hashing algorithm, which must be supported by 421
397 Python ``hashlib`` package from standard library. 422Some more explanations:
398 - ``mem_req`` - (optional) required available memory on the node to fit 423
399 the target image when not streamed to disk directly. 424``ironic.nodes``
400 Calculated from the image size and ``[ansible]extra_memory`` 425 List of dictionaries (currently of only one element) that will be used by
401 config option. 426 ``add-ironic-nodes.yaml`` play to populate in-memory inventory.
402 427 It also contains a copy of node's ``extra`` field so you can access it in
403``configdrive`` 428 the playbooks. The Ansible's host is set to node's UUID.
404 Optional. When defined in ``instance_info`` is a dictionary 429
405 of the following structure: 430``ironic.image``
406 431 All fields of node's ``instance_info`` that start with ``image_`` are
407 .. code-block:: json 432 passed inside this variable. Some extra notes and fields:
408 433
409 {"configdrive": { 434 - ``mem_req`` is calculated from image size (if available) and config
410 "type": "<url|file>", 435 option ``[ansible]extra_memory``.
411 "location": "<local-path-or-url>" 436 - if ``checksum`` initially does not start with ``hash-algo:``, hashing
412 } 437 algorithm is assumed to be ``md5`` (default in Glance).
413 } 438
414 439``ironic.partiton_info.partitions``
415 where
416
417 - ``type`` - either ``url`` or ``file``
418 - ``location`` - depending on ``type``, either a URL or path to file
419 stored on ironic-conductor node to fetch the content
420 of configdrive partition from.
421
422``ironic_partitions``
423 Optional. List of dictionaries defining partitions to create on the node 440 Optional. List of dictionaries defining partitions to create on the node
424 in the form: 441 in the form:
425 442
426 .. code-block:: json 443 .. code-block:: yaml
427 444
428 {"ironic_partitions": [ 445 partitions:
429 { 446 - name: <NAME OF PARTITION>
430 "name": "<partition name>", 447 size_mib: <SIZE OF THE PARTITION>
431 "size_mib": 12345, 448 boot: <bool>
432 "boot": "yes|no|..", 449 swap: <bool>
433 "swap": "yes|no|.."
434 }
435 ]}
436 450
437 The driver will populate this list from ``root_gb``, ``swap_mb`` and 451 The driver will populate this list from ``root_gb``, ``swap_mb`` and
438 ``ephemeral_gb`` fields of ``instance_info``. 452 ``ephemeral_gb`` fields of ``instance_info``.
439 453
440``ephemeral_format`` 454 Please read the documentation included in the ``parted`` module's source
455 for more info on the module and its arguments.
456
457``ironic.partiton_info.ephemeral_format``
441 Optional. Taken from ``instance_info``, it defines file system to be 458 Optional. Taken from ``instance_info``, it defines file system to be
442 created on the ephemeral partition. 459 created on the ephemeral partition.
443 Defaults to the value of ``[pxe]default_ephemeral_format`` option 460 Defaults to the value of ``[pxe]default_ephemeral_format`` option
444 in ironic configuration file. 461 in ironic configuration file.
445 462
446``preserve_ephemeral`` 463``ironic.partiton_info.preserve_ephemeral``
447 Optional. Taken from the ``instance_info``, it specifies if the ephemeral 464 Optional. Taken from the ``instance_info``, it specifies if the ephemeral
448 partition must be preserved or rebuilt. Defaults to ``no``. 465 partition must be preserved or rebuilt. Defaults to ``no``.
449 466
450``ironic_extra``
451 Dictionary holding a copy of ``extra`` field of ironic node,
452 with any per-node information.
453
454As usual for Ansible playbooks, you also have access to standard 467As usual for Ansible playbooks, you also have access to standard
455Ansible facts discovered by ``setup`` module. 468Ansible facts discovered by ``setup`` module.
456 469
@@ -458,17 +471,20 @@ Included custom Ansible modules
458------------------------------- 471-------------------------------
459 472
460The provided ``playbooks_path/library`` folder includes several custom 473The provided ``playbooks_path/library`` folder includes several custom
461Ansible modules used by default implementation of ``deploy`` role. 474Ansible modules used by default implementation of ``deploy`` and
475``prepare`` roles.
462You can use these modules in your playbooks as well. 476You can use these modules in your playbooks as well.
463 477
464``stream_url`` 478``stream_url``
465 Streaming download from HTTP(S) source to the disk device directly, 479 Streaming download from HTTP(S) source to the disk device directly,
466 tries to be compatible with Ansible-core ``get_url`` module in terms of 480 tries to be compatible with Ansible's ``get_url`` module in terms of
467 module arguments. 481 module arguments.
468 Due to the low level of such operation it is not idempotent. 482 Due to the low level of such operation it is not idempotent.
469 483
470``parted`` 484``parted``
471 creates partition tables and partitions with ``parted`` utility. 485 creates partition tables and partitions with ``parted`` utility.
472 Due to the low level of such operation it is not idempotent. 486 Due to the low level of such operation it is not idempotent.
487 Please read the documentation included in the module's source
488 for more information about this module and its arguments.
473 489
474.. _Ironic Python Agent: http://docs.openstack.org/developer/ironic-python-agent 490.. _Ironic Python Agent: http://docs.openstack.org/developer/ironic-python-agent
diff --git a/ironic_staging_drivers/ansible/deploy.py b/ironic_staging_drivers/ansible/deploy.py
index b89edbd..7b0da0b 100644
--- a/ironic_staging_drivers/ansible/deploy.py
+++ b/ironic_staging_drivers/ansible/deploy.py
@@ -108,6 +108,7 @@ METRICS = metrics_utils.get_metrics_logger(__name__)
108 108
109DEFAULT_PLAYBOOKS = { 109DEFAULT_PLAYBOOKS = {
110 'deploy': 'deploy.yaml', 110 'deploy': 'deploy.yaml',
111 'shutdown': 'shutdown.yaml',
111 'clean': 'clean.yaml' 112 'clean': 'clean.yaml'
112} 113}
113DEFAULT_CLEAN_STEPS = 'clean_steps.yaml' 114DEFAULT_CLEAN_STEPS = 'clean_steps.yaml'
@@ -126,6 +127,10 @@ OPTIONAL_PROPERTIES = {
126 'ansible_deploy_playbook': _('Name of the Ansible playbook used for ' 127 'ansible_deploy_playbook': _('Name of the Ansible playbook used for '
127 'deployment. Default is %s. Optional.' 128 'deployment. Default is %s. Optional.'
128 ) % DEFAULT_PLAYBOOKS['deploy'], 129 ) % DEFAULT_PLAYBOOKS['deploy'],
130 'ansible_shutdown_playbook': _('Name of the Ansible playbook used to '
131 'power off the node in-band. '
132 'Default is %s. Optional.'
133 ) % DEFAULT_PLAYBOOKS['shutdown'],
129 'ansible_clean_playbook': _('Name of the Ansible playbook used for ' 134 'ansible_clean_playbook': _('Name of the Ansible playbook used for '
130 'cleaning. Default is %s. Optional.' 135 'cleaning. Default is %s. Optional.'
131 ) % DEFAULT_PLAYBOOKS['clean'], 136 ) % DEFAULT_PLAYBOOKS['clean'],
@@ -189,7 +194,7 @@ def _prepare_extra_vars(host_list, variables=None):
189 nodes_var = [] 194 nodes_var = []
190 for node_uuid, ip, user, extra in host_list: 195 for node_uuid, ip, user, extra in host_list:
191 nodes_var.append(dict(name=node_uuid, ip=ip, user=user, extra=extra)) 196 nodes_var.append(dict(name=node_uuid, ip=ip, user=user, extra=extra))
192 extra_vars = dict(ironic_nodes=nodes_var) 197 extra_vars = dict(nodes=nodes_var)
193 if variables: 198 if variables:
194 extra_vars.update(variables) 199 extra_vars.update(variables)
195 return extra_vars 200 return extra_vars
@@ -198,9 +203,10 @@ def _prepare_extra_vars(host_list, variables=None):
198def _run_playbook(name, extra_vars, key, tags=None, notags=None): 203def _run_playbook(name, extra_vars, key, tags=None, notags=None):
199 """Execute ansible-playbook.""" 204 """Execute ansible-playbook."""
200 playbook = os.path.join(CONF.ansible.playbooks_path, name) 205 playbook = os.path.join(CONF.ansible.playbooks_path, name)
206 ironic_vars = {'ironic': extra_vars}
201 args = [CONF.ansible.ansible_playbook_script, playbook, 207 args = [CONF.ansible.ansible_playbook_script, playbook,
202 '-i', INVENTORY_FILE, 208 '-i', INVENTORY_FILE,
203 '-e', json.dumps(extra_vars), 209 '-e', json.dumps(ironic_vars),
204 ] 210 ]
205 211
206 if CONF.ansible.config_file_path: 212 if CONF.ansible.config_file_path:
@@ -242,7 +248,6 @@ def _parse_partitioning_info(node):
242 248
243 info = node.instance_info 249 info = node.instance_info
244 i_info = {} 250 i_info = {}
245
246 partitions = [] 251 partitions = []
247 root_partition = {'name': 'root', 252 root_partition = {'name': 'root',
248 'size_mib': info['root_mb'], 253 'size_mib': info['root_mb'],
@@ -270,19 +275,20 @@ def _parse_partitioning_info(node):
270 i_info['preserve_ephemeral'] = ( 275 i_info['preserve_ephemeral'] = (
271 'yes' if info['preserve_ephemeral'] else 'no') 276 'yes' if info['preserve_ephemeral'] else 'no')
272 277
273 i_info['ironic_partitions'] = partitions 278 i_info['partitions'] = partitions
274 return i_info 279 return {'partition_info': i_info}
275 280
276 281
277def _prepare_variables(task): 282def _prepare_variables(task):
278 node = task.node 283 node = task.node
279 i_info = node.instance_info 284 i_info = node.instance_info
280 image = { 285 image = {}
281 'url': i_info['image_url'], 286 for i_key, i_value in i_info.items():
282 'mem_req': _calculate_memory_req(task), 287 if i_key.startswith('image_'):
283 'disk_format': i_info.get('image_disk_format'), 288 image[i_key[6:]] = i_value
284 } 289 image['mem_req'] = _calculate_memory_req(task)
285 checksum = i_info.get('image_checksum') 290
291 checksum = image.get('checksum')
286 if checksum: 292 if checksum:
287 # NOTE(pas-ha) checksum can be in <algo>:<checksum> format 293 # NOTE(pas-ha) checksum can be in <algo>:<checksum> format
288 # as supported by various Ansible modules, mostly good for 294 # as supported by various Ansible modules, mostly good for
@@ -290,8 +296,7 @@ def _prepare_variables(task):
290 # With no <algo> we take that instance_info is populated from Glance, 296 # With no <algo> we take that instance_info is populated from Glance,
291 # where API reports checksum as MD5 always. 297 # where API reports checksum as MD5 always.
292 if ':' not in checksum: 298 if ':' not in checksum:
293 checksum = 'md5:%s' % checksum 299 image['checksum'] = 'md5:%s' % checksum
294 image['checksum'] = checksum
295 variables = {'image': image} 300 variables = {'image': image}
296 configdrive = i_info.get('configdrive') 301 configdrive = i_info.get('configdrive')
297 if configdrive: 302 if configdrive:
@@ -416,17 +421,12 @@ class AnsibleDeploy(agent_base.HeartbeatMixin, base.DeployInterface):
416 421
417 def _ansible_deploy(self, task, node_address): 422 def _ansible_deploy(self, task, node_address):
418 """Internal function for deployment to a node.""" 423 """Internal function for deployment to a node."""
419 notags = ['shutdown'] 424 notags = ['wait'] if CONF.ansible.use_ramdisk_callback else []
420 if CONF.ansible.use_ramdisk_callback:
421 notags.append('wait')
422 node = task.node 425 node = task.node
423 LOG.debug('IP of node %(node)s is %(ip)s', 426 LOG.debug('IP of node %(node)s is %(ip)s',
424 {'node': node.uuid, 'ip': node_address}) 427 {'node': node.uuid, 'ip': node_address})
425 variables = _prepare_variables(task) 428 variables = _prepare_variables(task)
426 iwdi = node.driver_internal_info.get('is_whole_disk_image') 429 if not node.driver_internal_info.get('is_whole_disk_image'):
427 if iwdi:
428 notags.append('parted')
429 else:
430 variables.update(_parse_partitioning_info(task.node)) 430 variables.update(_parse_partitioning_info(task.node))
431 playbook, user, key = _parse_ansible_driver_info(task.node) 431 playbook, user, key = _parse_ansible_driver_info(task.node)
432 node_list = [(node.uuid, node_address, user, node.extra)] 432 node_list = [(node.uuid, node_address, user, node.extra)]
@@ -648,11 +648,10 @@ class AnsibleDeploy(agent_base.HeartbeatMixin, base.DeployInterface):
648 try: 648 try:
649 node_address = _get_node_ip(task) 649 node_address = _get_node_ip(task)
650 playbook, user, key = _parse_ansible_driver_info( 650 playbook, user, key = _parse_ansible_driver_info(
651 node) 651 node, action='shutdown')
652 node_list = [(node.uuid, node_address, user, node.extra)] 652 node_list = [(node.uuid, node_address, user, node.extra)]
653 extra_vars = _prepare_extra_vars(node_list) 653 extra_vars = _prepare_extra_vars(node_list)
654 _run_playbook(playbook, extra_vars, key, 654 _run_playbook(playbook, extra_vars, key)
655 tags=['shutdown'])
656 _wait_until_powered_off(task) 655 _wait_until_powered_off(task)
657 except Exception as e: 656 except Exception as e:
658 LOG.warning( 657 LOG.warning(
diff --git a/ironic_staging_drivers/ansible/playbooks/add-ironic-nodes.yaml b/ironic_staging_drivers/ansible/playbooks/add-ironic-nodes.yaml
index c39c51d..568ff28 100644
--- a/ironic_staging_drivers/ansible/playbooks/add-ironic-nodes.yaml
+++ b/ironic_staging_drivers/ansible/playbooks/add-ironic-nodes.yaml
@@ -7,5 +7,5 @@
7 ansible_host: "{{ item.ip }}" 7 ansible_host: "{{ item.ip }}"
8 ansible_user: "{{ item.user }}" 8 ansible_user: "{{ item.user }}"
9 ironic_extra: "{{ item.extra | default({}) }}" 9 ironic_extra: "{{ item.extra | default({}) }}"
10 with_items: "{{ ironic_nodes }}" 10 with_items: "{{ ironic.nodes }}"
11 tags: always 11 tags: always
diff --git a/ironic_staging_drivers/ansible/playbooks/deploy.yaml b/ironic_staging_drivers/ansible/playbooks/deploy.yaml
index 9f75ad4..1022769 100644
--- a/ironic_staging_drivers/ansible/playbooks/deploy.yaml
+++ b/ironic_staging_drivers/ansible/playbooks/deploy.yaml
@@ -9,6 +9,10 @@
9 9
10- hosts: ironic 10- hosts: ironic
11 roles: 11 roles:
12 - role: deploy 12 - discover
13 - role: shutdown 13 - prepare
14 tags: shutdown 14 - deploy
15 - configure
16 post_tasks:
17 - name: flush disk state
18 command: sync
diff --git a/ironic_staging_drivers/ansible/playbooks/roles/deploy/files/install_grub.sh b/ironic_staging_drivers/ansible/playbooks/roles/configure/files/install_grub.sh
index d19044a..8a2af96 100755
--- a/ironic_staging_drivers/ansible/playbooks/roles/deploy/files/install_grub.sh
+++ b/ironic_staging_drivers/ansible/playbooks/roles/configure/files/install_grub.sh
@@ -5,8 +5,11 @@ readonly target_disk=$1
5readonly root_part=$2 5readonly root_part=$2
6readonly root_part_mount=/mnt/rootfs 6readonly root_part_mount=/mnt/rootfs
7 7
8# We need to run partprobe to ensure all partitions are visible 8# We need to run partprobe to ensure all partitions are visible.
9# On some test environments this is too fast
10# and kernel does not have time to react to changes
9partprobe $target_disk 11partprobe $target_disk
12sleep 5
10 13
11mkdir -p $root_part_mount 14mkdir -p $root_part_mount
12 15
diff --git a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/grub.yaml b/ironic_staging_drivers/ansible/playbooks/roles/configure/tasks/grub.yaml
index ce6308b..91691f1 100644
--- a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/grub.yaml
+++ b/ironic_staging_drivers/ansible/playbooks/roles/configure/tasks/grub.yaml
@@ -1,3 +1,3 @@
1- name: configure bootloader 1- name: install grub
2 become: yes 2 become: yes
3 script: install_grub.sh {{ ironic_root_device }} {{ ironic_image_target }} 3 script: install_grub.sh {{ ironic_root_device }} {{ ironic_image_target }}
diff --git a/ironic_staging_drivers/ansible/playbooks/roles/configure/tasks/main.yaml b/ironic_staging_drivers/ansible/playbooks/roles/configure/tasks/main.yaml
new file mode 100644
index 0000000..b93f055
--- /dev/null
+++ b/ironic_staging_drivers/ansible/playbooks/roles/configure/tasks/main.yaml
@@ -0,0 +1,2 @@
1- include: grub.yaml
2 when: "{{ ironic.image.type | default('whole-disk-image') == 'partition' }}"
diff --git a/ironic_staging_drivers/ansible/playbooks/roles/deploy/files/partition_configdrive.sh b/ironic_staging_drivers/ansible/playbooks/roles/deploy/files/partition_configdrive.sh
index 00fa742..acfe504 100755
--- a/ironic_staging_drivers/ansible/playbooks/roles/deploy/files/partition_configdrive.sh
+++ b/ironic_staging_drivers/ansible/playbooks/roles/deploy/files/partition_configdrive.sh
@@ -16,9 +16,6 @@
16 16
17# NOTE(pas-ha) this is mostly copied over from Ironic Python Agent 17# NOTE(pas-ha) this is mostly copied over from Ironic Python Agent
18# compared to the original file in IPA, 18# compared to the original file in IPA,
19# all logging is disabled to let Ansible output the full trace.
20# The places that log to fail are commented out to be replaced later
21# with different handler when making this script a real Ansible module
22 19
23# TODO(pas-ha) rewrite this shell script to be a proper Ansible module 20# TODO(pas-ha) rewrite this shell script to be a proper Ansible module
24 21
@@ -46,7 +43,7 @@ DEVICE="$1"
46 43
47# We need to run partx -u to ensure all partitions are visible so the 44# We need to run partx -u to ensure all partitions are visible so the
48# following blkid command returns partitions just imaged to the device 45# following blkid command returns partitions just imaged to the device
49partx -u $DEVICE # || fail "running partx -u $DEVICE" 46partx -u $DEVICE || fail "running partx -u $DEVICE"
50 47
51# todo(jayf): partx -u doesn't work in all cases, but partprobe fails in 48# todo(jayf): partx -u doesn't work in all cases, but partprobe fails in
52# devstack. We run both commands now as a temporary workaround for bug 1433812 49# devstack. We run both commands now as a temporary workaround for bug 1433812
@@ -56,16 +53,11 @@ partprobe $DEVICE || true
56 53
57# Check for preexisting partition for configdrive 54# Check for preexisting partition for configdrive
58EXISTING_PARTITION=`/sbin/blkid -l -o device $DEVICE -t LABEL=config-2` 55EXISTING_PARTITION=`/sbin/blkid -l -o device $DEVICE -t LABEL=config-2`
59if [ $? = 0 ]; then 56if [ -z $EXISTING_PARTITION ]; then
60 #log "Existing configdrive found on ${DEVICE} at ${EXISTING_PARTITION}"
61 ISO_PARTITION=$EXISTING_PARTITION
62else
63
64 # Check if it is GPT partition and needs to be re-sized 57 # Check if it is GPT partition and needs to be re-sized
65 partprobe $DEVICE print 2>&1 | grep "fix the GPT to use all of the space" 58 if [ `partprobe $DEVICE print 2>&1 | grep "fix the GPT to use all of the space"` ]; then
66 if [ $? = 0 ]; then 59 log "Fixing GPT to use all of the space on device $DEVICE"
67 #log "Fixing GPT to use all of the space on device $DEVICE" 60 sgdisk -e $DEVICE || fail "move backup GPT data structures to the end of ${DEVICE}"
68 sgdisk -e $DEVICE #|| fail "move backup GPT data structures to the end of ${DEVICE}"
69 61
70 # Need to create new partition for config drive 62 # Need to create new partition for config drive
71 # Not all images have partion numbers in a sequential numbers. There are holes. 63 # Not all images have partion numbers in a sequential numbers. There are holes.
@@ -77,15 +69,15 @@ else
77 gdisk -l $DEVICE | grep -A$MAX_DISK_PARTITIONS "Number Start" | grep -v "Number Start" > $EXISTING_PARTITION_LIST 69 gdisk -l $DEVICE | grep -A$MAX_DISK_PARTITIONS "Number Start" | grep -v "Number Start" > $EXISTING_PARTITION_LIST
78 70
79 # Create small partition at the end of the device 71 # Create small partition at the end of the device
80 #log "Adding configdrive partition to $DEVICE" 72 log "Adding configdrive partition to $DEVICE"
81 sgdisk -n 0:-64MB:0 $DEVICE #|| fail "creating configdrive on ${DEVICE}" 73 sgdisk -n 0:-64MB:0 $DEVICE || fail "creating configdrive on ${DEVICE}"
82 74
83 gdisk -l $DEVICE | grep -A$MAX_DISK_PARTITIONS "Number Start" | grep -v "Number Start" > $UPDATED_PARTITION_LIST 75 gdisk -l $DEVICE | grep -A$MAX_DISK_PARTITIONS "Number Start" | grep -v "Number Start" > $UPDATED_PARTITION_LIST
84 76
85 CONFIG_PARTITION_ID=`diff $EXISTING_PARTITION_LIST $UPDATED_PARTITION_LIST | tail -n1 |awk '{print $2}'` 77 CONFIG_PARTITION_ID=`diff $EXISTING_PARTITION_LIST $UPDATED_PARTITION_LIST | tail -n1 |awk '{print $2}'`
86 ISO_PARTITION="${DEVICE}${CONFIG_PARTITION_ID}" 78 ISO_PARTITION="${DEVICE}${CONFIG_PARTITION_ID}"
87 else 79 else
88 #log "Working on MBR only device $DEVICE" 80 log "Working on MBR only device $DEVICE"
89 81
90 # get total disk size, to detect if that exceeds 2TB msdos limit 82 # get total disk size, to detect if that exceeds 2TB msdos limit
91 disksize_bytes=$(blockdev --getsize64 $DEVICE) 83 disksize_bytes=$(blockdev --getsize64 $DEVICE)
@@ -99,16 +91,19 @@ else
99 endlimit=$(($MAX_MBR_SIZE_MB - 1)) 91 endlimit=$(($MAX_MBR_SIZE_MB - 1))
100 fi 92 fi
101 93
102 #log "Adding configdrive partition to $DEVICE" 94 log "Adding configdrive partition to $DEVICE"
103 parted -a optimal -s -- $DEVICE mkpart primary ext2 $startlimit $endlimit #|| fail "creating configdrive on ${DEVICE}" 95 parted -a optimal -s -- $DEVICE mkpart primary fat32 $startlimit $endlimit || fail "creating configdrive on ${DEVICE}"
104 96
105 # Find partition we just created 97 # Find partition we just created
106 # Dump all partitions, ignore empty ones, then get the last partition ID 98 # Dump all partitions, ignore empty ones, then get the last partition ID
107 ISO_PARTITION=`sfdisk --dump $DEVICE | grep -v ' 0,' | tail -n1 | awk -F ':' '{print $1}' | sed -e 's/\s*$//'` #|| fail "finding ISO partition created on ${DEVICE}" 99 ISO_PARTITION=`sfdisk --dump $DEVICE | grep -v ' 0,' | tail -n1 | awk -F ':' '{print $1}' | sed -e 's/\s*$//'` || fail "finding ISO partition created on ${DEVICE}"
108 100
109 # Wait for udev to pick up the partition 101 # Wait for udev to pick up the partition
110 udevadm settle --exit-if-exists=$ISO_PARTITION 102 udevadm settle --exit-if-exists=$ISO_PARTITION
111 fi 103 fi
104else
105 log "Existing configdrive found on ${DEVICE} at ${EXISTING_PARTITION}"
106 ISO_PARTITION=$EXISTING_PARTITION
112fi 107fi
113 108
114# Output the created/discovered partition for configdrive 109# Output the created/discovered partition for configdrive
diff --git a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/configdrive.yaml b/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/configdrive.yaml
index ed77610..13d4fc5 100644
--- a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/configdrive.yaml
+++ b/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/configdrive.yaml
@@ -1,37 +1,43 @@
1- name: download configdrive data 1- name: download configdrive data
2 get_url: 2 get_url:
3 url: "{{ configdrive.location }}" 3 url: "{{ ironic.configdrive.location }}"
4 dest: /tmp/{{ inventory_hostname }}.gz.base64 4 dest: /tmp/{{ inventory_hostname }}.gz.base64
5 async: 600 5 async: 600
6 poll: 15 6 poll: 15
7 when: "{{ configdrive.type|default('') == 'url' }}" 7 when: "{{ ironic.configdrive.type|default('') == 'url' }}"
8 8
9- block: 9- block:
10 - name: copy configdrive file to node 10 - name: copy configdrive file to node
11 copy: 11 copy:
12 src: "{{ configdrive.location }}" 12 src: "{{ ironic.configdrive.location }}"
13 dest: /tmp/{{ inventory_hostname }}.gz.base64 13 dest: /tmp/{{ inventory_hostname }}.gz.base64
14 - name: remove configdrive from conductor 14 - name: remove configdrive from conductor
15 delegate_to: conductor 15 delegate_to: conductor
16 file: 16 file:
17 path: "{{ configdrive.location }}" 17 path: "{{ ironic.configdrive.location }}"
18 state: absent 18 state: absent
19 when: "{{ configdrive.type|default('') == 'file' }}" 19 when: "{{ ironic.configdrive.type|default('') == 'file' }}"
20 20
21- name: unpack configdrive 21- name: unpack configdrive
22 shell: cat /tmp/{{ inventory_hostname }}.gz.base64 | base64 --decode | gunzip > /tmp/{{ inventory_hostname }}.cndrive 22 shell: cat /tmp/{{ inventory_hostname }}.gz.base64 | base64 --decode | gunzip > /tmp/{{ inventory_hostname }}.cndrive
23 23
24- name: prepare config drive partition 24- block:
25 become: yes 25 - name: prepare config drive partition
26 script: partition_configdrive.sh {{ ironic_root_device }} 26 become: yes
27 register: configdrive_partition_output 27 script: partition_configdrive.sh {{ ironic_root_device }}
28 register: configdrive_partition_output
29
30 - name: test the output of configdrive partitioner
31 assert:
32 that:
33 - "{{ (configdrive_partition_output.stdout_lines | last).split() | length == 2 }}"
34 - "{{ (configdrive_partition_output.stdout_lines | last).split() | first == 'configdrive' }}"
28 35
29- name: test the output of configdrive partitioner 36 - name: store configdrive partition
30 assert: 37 set_fact:
31 that: 38 ironic_configdrive_target: "{{ (configdrive_partition_output.stdout_lines | last).split() | last }}"
32 - "{{ (configdrive_partition_output.stdout_lines | last).split() | length == 2 }}" 39 when: "{{ ironic_configdrive_target is undefined }}"
33 - "{{ (configdrive_partition_output.stdout_lines | last).split() | first == 'configdrive' }}"
34 40
35- name: write configdrive 41- name: write configdrive
36 become: yes 42 become: yes
37 command: dd if=/tmp/{{ inventory_hostname }}.cndrive of={{ (configdrive_partition_output.stdout_lines | last).split() | last }} bs=64K oflag=direct 43 command: dd if=/tmp/{{ inventory_hostname }}.cndrive of={{ ironic_configdrive_target }} bs=64K oflag=direct
diff --git a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/download.yaml b/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/download.yaml
index f979679..00d7c9f 100644
--- a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/download.yaml
+++ b/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/download.yaml
@@ -1,11 +1,12 @@
1- name: fail if not enough memory to store downloaded image 1- name: check that downloaded image will fit into memory
2 fail: 2 assert:
3 that: "{{ ansible_memfree_mb }} >= {{ ironic.image.mem_req }}"
3 msg: "The image size is too big, no free memory available" 4 msg: "The image size is too big, no free memory available"
4 when: "{{ ansible_memfree_mb }} < {{ image.mem_req }}" 5
5- name: download image with checksum validation 6- name: download image with checksum validation
6 get_url: 7 get_url:
7 url: "{{ image.url }}" 8 url: "{{ ironic.image.url }}"
8 dest: /tmp/{{ inventory_hostname }}.img 9 dest: /tmp/{{ inventory_hostname }}.img
9 checksum: "{{ image.checksum|default(omit) }}" 10 checksum: "{{ ironic.image.checksum|default(omit) }}"
10 async: 600 11 async: 600
11 poll: 15 12 poll: 15
diff --git a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/main.yaml b/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/main.yaml
index e099b79..16efa90 100644
--- a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/main.yaml
+++ b/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/main.yaml
@@ -1,20 +1,7 @@
1- include: root-device.yaml
2
3- include: parted.yaml
4 tags:
5 - parted
6
7- include: download.yaml 1- include: download.yaml
8 when: "{{ image.disk_format != 'raw' }}" 2 when: "{{ ironic.image.disk_format != 'raw' }}"
9 3
10- include: write.yaml 4- include: write.yaml
11 5
12- include: configdrive.yaml 6- include: configdrive.yaml
13 when: configdrive is defined 7 when: "{{ ironic.configdrive is defined }}"
14
15- include: grub.yaml
16 tags:
17 - parted
18
19- name: flush
20 command: sync
diff --git a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/write.yaml b/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/write.yaml
index 1a8eadc..f470fce 100644
--- a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/write.yaml
+++ b/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/write.yaml
@@ -3,17 +3,17 @@
3 command: qemu-img convert -t directsync -O host_device /tmp/{{ inventory_hostname }}.img {{ ironic_image_target }} 3 command: qemu-img convert -t directsync -O host_device /tmp/{{ inventory_hostname }}.img {{ ironic_image_target }}
4 async: 400 4 async: 400
5 poll: 10 5 poll: 10
6 when: "{{ image.disk_format != 'raw' }}" 6 when: "{{ ironic.image.disk_format != 'raw' }}"
7 7
8- name: stream to target 8- name: stream to target
9 become: yes 9 become: yes
10 stream_url: 10 stream_url:
11 url: "{{ image.url }}" 11 url: "{{ ironic.image.url }}"
12 dest: "{{ ironic_image_target }}" 12 dest: "{{ ironic_image_target }}"
13 checksum: "{{ image.checksum }}" 13 checksum: "{{ ironic.image.checksum|default(omit) }}"
14 async: 600 14 async: 600
15 poll: 15 15 poll: 15
16 when: "{{ image.disk_format == 'raw' }}" 16 when: "{{ ironic.image.disk_format == 'raw' }}"
17 17
18- name: flush 18- name: flush
19 command: sync 19 command: sync
diff --git a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/root-device.yaml b/ironic_staging_drivers/ansible/playbooks/roles/discover/tasks/main.yaml
index 6f88623..6f88623 100644
--- a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/root-device.yaml
+++ b/ironic_staging_drivers/ansible/playbooks/roles/discover/tasks/main.yaml
diff --git a/ironic_staging_drivers/ansible/playbooks/roles/prepare/tasks/main.yaml b/ironic_staging_drivers/ansible/playbooks/roles/prepare/tasks/main.yaml
new file mode 100644
index 0000000..679da81
--- /dev/null
+++ b/ironic_staging_drivers/ansible/playbooks/roles/prepare/tasks/main.yaml
@@ -0,0 +1,2 @@
1- include: parted.yaml
2 when: "{{ ironic.image.type | default('whole-disk-image') == 'partition' }}"
diff --git a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/parted.yaml b/ironic_staging_drivers/ansible/playbooks/roles/prepare/tasks/parted.yaml
index 2b908e5..f5f0f4a 100644
--- a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/parted.yaml
+++ b/ironic_staging_drivers/ansible/playbooks/roles/prepare/tasks/parted.yaml
@@ -1,16 +1,16 @@
1- name: erase partition table 1- name: erase partition table
2 become: yes 2 become: yes
3 command: dd if=/dev/zero of={{ ironic_root_device }} bs=512 count=36 3 command: dd if=/dev/zero of={{ ironic_root_device }} bs=512 count=36
4 when: "{{ not preserve_ephemeral|default('no')|bool }}" 4 when: "{{ not ironic.partition_info.preserve_ephemeral|default('no')|bool }}"
5 5
6- name: run parted 6- name: run parted
7 become: yes 7 become: yes
8 parted: 8 parted:
9 device: "{{ ironic_root_device }}" 9 device: "{{ ironic_root_device }}"
10 dryrun: "{{ preserve_ephemeral|default('no')|bool }}"
11 new_label: yes
12 label: msdos 10 label: msdos
13 partitions: "{{ ironic_partitions }}" 11 new_label: yes
12 dryrun: "{{ ironic.partition_info.preserve_ephemeral|default('no')|bool }}"
13 partitions: "{{ ironic.partition_info.partitions }}"
14 register: parts 14 register: parts
15 15
16- name: reset image target to root partition 16- name: reset image target to root partition
@@ -24,5 +24,9 @@
24 24
25- name: format ephemeral partition 25- name: format ephemeral partition
26 become: yes 26 become: yes
27 command: mkfs -F -t {{ ephemeral_format }} -L ephemeral0 {{ parts.created.ephemeral }} 27 filesystem:
28 when: "{{ parts.created.ephemeral is defined and not preserve_ephemeral|default('no')|bool }}" 28 dev: "{{ parts.created.ephemeral }}"
29 fstype: "{{ ironic.partition_info.ephemeral_format }}"
30 force: yes
31 opts: "-L ephemeral0"
32 when: "{{ parts.created.ephemeral is defined and not ironic.partition_info.preserve_ephemeral|default('no')|bool }}"
diff --git a/ironic_staging_drivers/ansible/playbooks/shutdown.yaml b/ironic_staging_drivers/ansible/playbooks/shutdown.yaml
new file mode 100644
index 0000000..2f3db32
--- /dev/null
+++ b/ironic_staging_drivers/ansible/playbooks/shutdown.yaml
@@ -0,0 +1,6 @@
1---
2- include: add-ironic-nodes.yaml
3
4- hosts: ironic
5 roles:
6 - shutdown
diff --git a/ironic_staging_drivers/tests/unit/ansible/test_deploy.py b/ironic_staging_drivers/tests/unit/ansible/test_deploy.py
index b84d8c7..822a637 100644
--- a/ironic_staging_drivers/tests/unit/ansible/test_deploy.py
+++ b/ironic_staging_drivers/tests/unit/ansible/test_deploy.py
@@ -156,7 +156,7 @@ class TestAnsibleMethods(db_base.DbTestCase):
156 execute_mock.assert_called_once_with( 156 execute_mock.assert_called_once_with(
157 'env', 'ANSIBLE_CONFIG=/path/to/config', 157 'env', 'ANSIBLE_CONFIG=/path/to/config',
158 'ansible-playbook', '/path/to/playbooks/deploy', '-i', 158 'ansible-playbook', '/path/to/playbooks/deploy', '-i',
159 ansible_deploy.INVENTORY_FILE, '-e', '{"foo": "bar"}', 159 ansible_deploy.INVENTORY_FILE, '-e', '{"ironic": {"foo": "bar"}}',
160 '--tags=spam', '--skip-tags=ham', 160 '--tags=spam', '--skip-tags=ham',
161 '--private-key=/path/to/key', '-vvv', '--timeout=100') 161 '--private-key=/path/to/key', '-vvv', '--timeout=100')
162 162
@@ -173,7 +173,7 @@ class TestAnsibleMethods(db_base.DbTestCase):
173 execute_mock.assert_called_once_with( 173 execute_mock.assert_called_once_with(
174 'env', 'ANSIBLE_CONFIG=/path/to/config', 174 'env', 'ANSIBLE_CONFIG=/path/to/config',
175 'ansible-playbook', '/path/to/playbooks/deploy', '-i', 175 'ansible-playbook', '/path/to/playbooks/deploy', '-i',
176 ansible_deploy.INVENTORY_FILE, '-e', '{"foo": "bar"}', 176 ansible_deploy.INVENTORY_FILE, '-e', '{"ironic": {"foo": "bar"}}',
177 '--private-key=/path/to/key') 177 '--private-key=/path/to/key')
178 178
179 @mock.patch.object(com_utils, 'execute', return_value=('out', 'err'), 179 @mock.patch.object(com_utils, 'execute', return_value=('out', 'err'),
@@ -189,7 +189,7 @@ class TestAnsibleMethods(db_base.DbTestCase):
189 execute_mock.assert_called_once_with( 189 execute_mock.assert_called_once_with(
190 'env', 'ANSIBLE_CONFIG=/path/to/config', 190 'env', 'ANSIBLE_CONFIG=/path/to/config',
191 'ansible-playbook', '/path/to/playbooks/deploy', '-i', 191 'ansible-playbook', '/path/to/playbooks/deploy', '-i',
192 ansible_deploy.INVENTORY_FILE, '-e', '{"foo": "bar"}', 192 ansible_deploy.INVENTORY_FILE, '-e', '{"ironic": {"foo": "bar"}}',
193 '--private-key=/path/to/key', '-vvvv') 193 '--private-key=/path/to/key', '-vvvv')
194 194
195 @mock.patch.object(com_utils, 'execute', 195 @mock.patch.object(com_utils, 'execute',
@@ -209,56 +209,51 @@ class TestAnsibleMethods(db_base.DbTestCase):
209 execute_mock.assert_called_once_with( 209 execute_mock.assert_called_once_with(
210 'env', 'ANSIBLE_CONFIG=/path/to/config', 210 'env', 'ANSIBLE_CONFIG=/path/to/config',
211 'ansible-playbook', '/path/to/playbooks/deploy', '-i', 211 'ansible-playbook', '/path/to/playbooks/deploy', '-i',
212 ansible_deploy.INVENTORY_FILE, '-e', '{"foo": "bar"}', 212 ansible_deploy.INVENTORY_FILE, '-e', '{"ironic": {"foo": "bar"}}',
213 '--private-key=/path/to/key') 213 '--private-key=/path/to/key')
214 214
215 def test__parse_partitioning_info(self): 215 def test__parse_partitioning_info_root_only(self):
216 expected_info = { 216 expected_info = {
217 'ironic_partitions': 217 'partition_info': {
218 [{'boot': 'yes', 'swap': 'no', 218 'partitions': [
219 'size_mib': INSTANCE_INFO['root_mb'], 219 {'name': 'root',
220 'name': 'root'}]} 220 'size_mib': INSTANCE_INFO['root_mb'],
221 'boot': 'yes',
222 'swap': 'no'}
223 ]}}
221 224
222 i_info = ansible_deploy._parse_partitioning_info(self.node) 225 i_info = ansible_deploy._parse_partitioning_info(self.node)
223 226
224 self.assertEqual(expected_info, i_info) 227 self.assertEqual(expected_info, i_info)
225 228
226 def test__parse_partitioning_info_swap(self): 229 def test__parse_partitioning_info_all(self):
227 in_info = dict(INSTANCE_INFO) 230 in_info = dict(INSTANCE_INFO)
228 in_info['swap_mb'] = 128 231 in_info['swap_mb'] = 128
229 self.node.instance_info = in_info 232 in_info['ephemeral_mb'] = 256
230 self.node.save()
231
232 expected_info = {
233 'ironic_partitions':
234 [{'boot': 'yes', 'swap': 'no',
235 'size_mib': INSTANCE_INFO['root_mb'],
236 'name': 'root'},
237 {'boot': 'no', 'swap': 'yes',
238 'size_mib': 128, 'name': 'swap'}]}
239
240 i_info = ansible_deploy._parse_partitioning_info(self.node)
241
242 self.assertEqual(expected_info, i_info)
243
244 def test__parse_partitioning_info_ephemeral(self):
245 in_info = dict(INSTANCE_INFO)
246 in_info['ephemeral_mb'] = 128
247 in_info['ephemeral_format'] = 'ext4' 233 in_info['ephemeral_format'] = 'ext4'
248 in_info['preserve_ephemeral'] = True 234 in_info['preserve_ephemeral'] = True
249 self.node.instance_info = in_info 235 self.node.instance_info = in_info
250 self.node.save() 236 self.node.save()
251 237
252 expected_info = { 238 expected_info = {
253 'ironic_partitions': 239 'partition_info': {
254 [{'boot': 'yes', 'swap': 'no', 240 'ephemeral_format': 'ext4',
255 'size_mib': INSTANCE_INFO['root_mb'], 241 'preserve_ephemeral': 'yes',
256 'name': 'root'}, 242 'partitions': [
257 {'boot': 'no', 'swap': 'no', 243 {'name': 'root',
258 'size_mib': 128, 'name': 'ephemeral'}], 244 'size_mib': INSTANCE_INFO['root_mb'],
259 'ephemeral_format': 'ext4', 245 'boot': 'yes',
260 'preserve_ephemeral': 'yes' 246 'swap': 'no'},
261 } 247 {'name': 'swap',
248 'size_mib': 128,
249 'boot': 'no',
250 'swap': 'yes'},
251 {'name': 'ephemeral',
252 'size_mib': 256,
253 'boot': 'no',
254 'swap': 'no'},
255 ]}}
256
262 i_info = ansible_deploy._parse_partitioning_info(self.node) 257 i_info = ansible_deploy._parse_partitioning_info(self.node)
263 258
264 self.assertEqual(expected_info, i_info) 259 self.assertEqual(expected_info, i_info)
@@ -282,7 +277,7 @@ class TestAnsibleMethods(db_base.DbTestCase):
282 ('other-uuid', '5.6.7.8', 'eggs', 'vikings')] 277 ('other-uuid', '5.6.7.8', 'eggs', 'vikings')]
283 ansible_vars = {"foo": "bar"} 278 ansible_vars = {"foo": "bar"}
284 self.assertEqual( 279 self.assertEqual(
285 {"ironic_nodes": [ 280 {"nodes": [
286 {"name": "fake-uuid", "ip": '1.2.3.4', 281 {"name": "fake-uuid", "ip": '1.2.3.4',
287 "user": "spam", "extra": "ham"}, 282 "user": "spam", "extra": "ham"},
288 {"name": "other-uuid", "ip": '5.6.7.8', 283 {"name": "other-uuid", "ip": '5.6.7.8',
@@ -293,7 +288,9 @@ class TestAnsibleMethods(db_base.DbTestCase):
293 @mock.patch.object(ansible_deploy, '_calculate_memory_req', autospec=True, 288 @mock.patch.object(ansible_deploy, '_calculate_memory_req', autospec=True,
294 return_value=2000) 289 return_value=2000)
295 def test__prepare_variables(self, mem_req_mock): 290 def test__prepare_variables(self, mem_req_mock):
296 expected = {"image": {"url": "http://image", "mem_req": 2000, 291 expected = {"image": {"url": "http://image",
292 "source": "fake-image",
293 "mem_req": 2000,
297 "disk_format": "qcow2", 294 "disk_format": "qcow2",
298 "checksum": "md5:checksum"}} 295 "checksum": "md5:checksum"}}
299 with task_manager.acquire(self.context, self.node.uuid) as task: 296 with task_manager.acquire(self.context, self.node.uuid) as task:
@@ -307,7 +304,9 @@ class TestAnsibleMethods(db_base.DbTestCase):
307 i_info['image_checksum'] = 'sha256:checksum' 304 i_info['image_checksum'] = 'sha256:checksum'
308 self.node.instance_info = i_info 305 self.node.instance_info = i_info
309 self.node.save() 306 self.node.save()
310 expected = {"image": {"url": "http://image", "mem_req": 2000, 307 expected = {"image": {"url": "http://image",
308 "source": "fake-image",
309 "mem_req": 2000,
311 "disk_format": "qcow2", 310 "disk_format": "qcow2",
312 "checksum": "sha256:checksum"}} 311 "checksum": "sha256:checksum"}}
313 with task_manager.acquire(self.context, self.node.uuid) as task: 312 with task_manager.acquire(self.context, self.node.uuid) as task:
@@ -321,7 +320,9 @@ class TestAnsibleMethods(db_base.DbTestCase):
321 i_info['configdrive'] = 'http://configdrive_url' 320 i_info['configdrive'] = 'http://configdrive_url'
322 self.node.instance_info = i_info 321 self.node.instance_info = i_info
323 self.node.save() 322 self.node.save()
324 expected = {"image": {"url": "http://image", "mem_req": 2000, 323 expected = {"image": {"url": "http://image",
324 "source": "fake-image",
325 "mem_req": 2000,
325 "disk_format": "qcow2", 326 "disk_format": "qcow2",
326 "checksum": "md5:checksum"}, 327 "checksum": "md5:checksum"},
327 'configdrive': {'type': 'url', 328 'configdrive': {'type': 'url',
@@ -338,7 +339,9 @@ class TestAnsibleMethods(db_base.DbTestCase):
338 self.node.instance_info = i_info 339 self.node.instance_info = i_info
339 self.node.save() 340 self.node.save()
340 self.config(tempdir='/path/to/tmpfiles') 341 self.config(tempdir='/path/to/tmpfiles')
341 expected = {"image": {"url": "http://image", "mem_req": 2000, 342 expected = {"image": {"url": "http://image",
343 "source": "fake-image",
344 "mem_req": 2000,
342 "disk_format": "qcow2", 345 "disk_format": "qcow2",
343 "checksum": "md5:checksum"}, 346 "checksum": "md5:checksum"},
344 'configdrive': {'type': 'file', 347 'configdrive': {'type': 'file',
@@ -793,7 +796,7 @@ class TestAnsibleDeploy(db_base.DbTestCase):
793 (self.node['uuid'], 796 (self.node['uuid'],
794 DRIVER_INTERNAL_INFO['ansible_cleaning_ip'], 797 DRIVER_INTERNAL_INFO['ansible_cleaning_ip'],
795 'test_u')]}, 'test_k', 798 'test_u')]}, 'test_k',
796 notags=['shutdown', 'wait']) 799 notags=['wait'])
797 800
798 @mock.patch.object(ansible_deploy, '_run_playbook', autospec=True) 801 @mock.patch.object(ansible_deploy, '_run_playbook', autospec=True)
799 @mock.patch.object(ansible_deploy, '_prepare_extra_vars', autospec=True) 802 @mock.patch.object(ansible_deploy, '_prepare_extra_vars', autospec=True)
@@ -835,7 +838,7 @@ class TestAnsibleDeploy(db_base.DbTestCase):
835 (self.node['uuid'], 838 (self.node['uuid'],
836 DRIVER_INTERNAL_INFO['ansible_cleaning_ip'], 839 DRIVER_INTERNAL_INFO['ansible_cleaning_ip'],
837 'test_u')]}, 'test_k', 840 'test_u')]}, 'test_k',
838 notags=['shutdown', 'wait', 'parted']) 841 notags=['wait'])
839 842
840 @mock.patch.object(fake.FakePower, 'get_power_state', 843 @mock.patch.object(fake.FakePower, 'get_power_state',
841 return_value=states.POWER_OFF) 844 return_value=states.POWER_OFF)
@@ -898,8 +901,8 @@ class TestAnsibleDeploy(db_base.DbTestCase):
898 ((task, states.POWER_ON),)] 901 ((task, states.POWER_ON),)]
899 self.assertEqual(expected_power_calls, 902 self.assertEqual(expected_power_calls,
900 power_action_mock.call_args_list) 903 power_action_mock.call_args_list)
901 ansible_mock.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, 904 ansible_mock.assert_called_once_with('shutdown.yaml',
902 tags=['shutdown']) 905 mock.ANY, mock.ANY)
903 906
904 @mock.patch.object(ansible_deploy, '_get_node_ip_heartbeat', autospec=True, 907 @mock.patch.object(ansible_deploy, '_get_node_ip_heartbeat', autospec=True,
905 return_value='1.2.3.4') 908 return_value='1.2.3.4')
diff --git a/releasenotes/notes/ansible-change-api-510961a1132a2ced.yaml b/releasenotes/notes/ansible-change-api-510961a1132a2ced.yaml
new file mode 100644
index 0000000..feb2a9e
--- /dev/null
+++ b/releasenotes/notes/ansible-change-api-510961a1132a2ced.yaml
@@ -0,0 +1,39 @@
1---
2features:
3 - |
4 Ansible-deploy driver has considerably changed in terms of playbook
5 structure and accepted incoming variables.
6
7 + all info passed into Ansible playbooks from ironic is now available in
8 the playbooks as elements of ``ironic`` dictionary to better
9 differentiate those from other vars possibly created/set
10 inside playbooks.
11
12 + any field of node's instance_info having a form of ``image_<field>``
13 is now available in playbooks as ``ironic.image.<field>`` variable.
14
15 + ``parted`` tag in playbooks is removed and instead differentiation
16 between partition and whole-disk imaged is being done based on
17 ``ironic.image.type`` variable value.
18
19 + ``shutdown`` tag is removed, and soft power-off is moved to a separate
20 playbook, defined by new optional ``driver_info`` field
21 ``ansible_shutdown_playbook`` (the default ``shutdown.yaml``
22 is provided in the code tree).
23
24 + default ``deploy`` role is split into smaller roles,
25 each targeting a separate stage of deployment process
26 to faciliate customiation and re-use
27
28 - ``discover`` - e.g. set root device and image target
29 - ``prepare`` - if needed, prepare system, e.g. create partitions
30 - ``deploy`` - download/convert/write user image and configdrive
31 - ``configure`` - post-deployment steps, e.g. installing the bootloader
32
33upgrade:
34 - |
35 Ansible-deploy driver has considerably changed in terms of playbook
36 structure and accepted incoming variables.
37
38 **Any out-of-tree playbooks written for previous versions are incompatible
39 with this release and must be changed at least to accept new variables!**