~ubuntu-branches/ubuntu/vivid/ironic/vivid-updates

« back to all changes in this revision

Viewing changes to ironic/drivers/modules/deploy_utils.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2015-03-30 11:14:57 UTC
  • mfrom: (1.2.6)
  • Revision ID: package-import@ubuntu.com-20150330111457-kr4ju3guf22m4vbz
Tags: 2015.1~b3-0ubuntu1
* New upstream release.
  + d/control: 
    - Align with upstream dependencies.
    - Add dh-python to build-dependencies.
    - Add psmisc as a dependency. (LP: #1358820)
  + d/p/fix-requirements.patch: Rediffed.
  + d/ironic-conductor.init.in: Fixed typos in LSB headers,
    thanks to JJ Asghar. (LP: #1429962)

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
 
16
16
 
17
17
import base64
 
18
import contextlib
18
19
import gzip
19
20
import math
20
21
import os
25
26
import tempfile
26
27
import time
27
28
 
28
 
from oslo.utils import excutils
29
 
from oslo.utils import units
30
29
from oslo_concurrency import processutils
31
30
from oslo_config import cfg
 
31
from oslo_serialization import jsonutils
 
32
from oslo_utils import excutils
 
33
from oslo_utils import units
32
34
import requests
33
35
import six
 
36
from six.moves.urllib import parse
34
37
 
35
38
from ironic.common import disk_partitioner
36
39
from ironic.common import exception
37
40
from ironic.common.i18n import _
38
41
from ironic.common.i18n import _LE
 
42
from ironic.common.i18n import _LW
39
43
from ironic.common import images
40
44
from ironic.common import states
41
45
from ironic.common import utils
42
46
from ironic.conductor import utils as manager_utils
 
47
from ironic.drivers.modules import agent_client
43
48
from ironic.drivers.modules import image_cache
 
49
from ironic.drivers import utils as driver_utils
 
50
from ironic import objects
44
51
from ironic.openstack.common import log as logging
45
52
 
46
53
 
47
54
deploy_opts = [
 
55
    cfg.IntOpt('efi_system_partition_size',
 
56
               default=200,
 
57
               help='Size of EFI system partition in MiB when configuring '
 
58
                    'UEFI systems for local boot.'),
48
59
    cfg.StrOpt('dd_block_size',
49
60
               default='1M',
50
61
               help='Block size to use when writing to the nodes disk.'),
59
70
 
60
71
LOG = logging.getLogger(__name__)
61
72
 
 
73
VALID_ROOT_DEVICE_HINTS = set(('size', 'model', 'wwn', 'serial', 'vendor'))
 
74
 
 
75
 
 
76
def _get_agent_client():
 
77
    return agent_client.AgentClient()
 
78
 
62
79
 
63
80
# All functions are called from deploy() directly or indirectly.
64
81
# They are split for stub-out.
86
103
                  check_exit_code=[0],
87
104
                  attempts=5,
88
105
                  delay_on_retry=True)
89
 
    # NOTE(dprince): partial revert of 4606716 until we debug further
90
 
    time.sleep(3)
91
106
    # Ensure the login complete
92
107
    verify_iscsi_connection(target_iqn)
93
108
    # force iSCSI initiator to re-read luns
94
109
    force_iscsi_lun_update(target_iqn)
 
110
    # ensure file system sees the block device
 
111
    check_file_system_for_iscsi_device(portal_address,
 
112
                                       portal_port,
 
113
                                       target_iqn)
 
114
 
 
115
 
 
116
def check_file_system_for_iscsi_device(portal_address,
 
117
                                       portal_port,
 
118
                                       target_iqn):
 
119
    """Ensure the file system sees the iSCSI block device."""
 
120
    check_dir = "/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-1" % (portal_address,
 
121
                                                               portal_port,
 
122
                                                               target_iqn)
 
123
    total_checks = CONF.deploy.iscsi_verify_attempts
 
124
    for attempt in range(total_checks):
 
125
        if os.path.exists(check_dir):
 
126
            break
 
127
        time.sleep(1)
 
128
        LOG.debug("iSCSI connection not seen by file system. Rechecking. "
 
129
                  "Attempt %(attempt)d out of %(total)d",
 
130
                  {"attempt": attempt + 1,
 
131
                   "total": total_checks})
 
132
    else:
 
133
        msg = _("iSCSI connection was not seen by the file system after "
 
134
                "attempting to verify %d times.") % total_checks
 
135
        LOG.error(msg)
 
136
        raise exception.InstanceDeployFailure(msg)
95
137
 
96
138
 
97
139
def verify_iscsi_connection(target_iqn):
157
199
                  delay_on_retry=True)
158
200
 
159
201
 
 
202
def get_disk_identifier(dev):
 
203
    """Get the disk identifier from the disk being exposed by the ramdisk.
 
204
 
 
205
    This disk identifier is appended to the pxe config which will then be
 
206
    used by chain.c32 to detect the correct disk to chainload. This is helpful
 
207
    in deployments to nodes with multiple disks.
 
208
 
 
209
    http://www.syslinux.org/wiki/index.php/Comboot/chain.c32#mbr:
 
210
 
 
211
    :param dev: Path for the already populated disk device.
 
212
    :returns The Disk Identifier.
 
213
    """
 
214
    disk_identifier = utils.execute('hexdump', '-s', '440', '-n', '4',
 
215
                                     '-e', '''\"0x%08x\"''',
 
216
                                     dev,
 
217
                                     run_as_root=True,
 
218
                                     check_exit_code=[0],
 
219
                                     attempts=5,
 
220
                                     delay_on_retry=True)
 
221
    return disk_identifier[0]
 
222
 
 
223
 
160
224
def make_partitions(dev, root_mb, swap_mb, ephemeral_mb,
161
 
                    configdrive_mb, commit=True):
 
225
                    configdrive_mb, commit=True, boot_option="netboot",
 
226
                    boot_mode="bios"):
162
227
    """Partition the disk device.
163
228
 
164
229
    Create partitions for root, swap, ephemeral and configdrive on a
173
238
        mebibytes (MiB). If 0, no partition will be created.
174
239
    :param commit: True/False. Default for this setting is True. If False
175
240
        partitions will not be written to disk.
 
241
    :param boot_option: Can be "local" or "netboot". "netboot" by default.
 
242
    :param boot_mode: Can be "bios" or "uefi". "bios" by default.
176
243
    :returns: A dictionary containing the partition type as Key and partition
177
244
        path as Value for the partitions created by this method.
178
245
 
181
248
              {'dev': dev})
182
249
    part_template = dev + '-part%d'
183
250
    part_dict = {}
184
 
    dp = disk_partitioner.DiskPartitioner(dev)
 
251
 
 
252
    # For uefi localboot, switch partition table to gpt and create the efi
 
253
    # system partition as the first partition.
 
254
    if boot_mode == "uefi" and boot_option == "local":
 
255
        dp = disk_partitioner.DiskPartitioner(dev, disk_label="gpt")
 
256
        part_num = dp.add_partition(CONF.deploy.efi_system_partition_size,
 
257
                                    fs_type='fat32',
 
258
                                    bootable=True)
 
259
        part_dict['efi system partition'] = part_template % part_num
 
260
    else:
 
261
        dp = disk_partitioner.DiskPartitioner(dev)
 
262
 
185
263
    if ephemeral_mb:
186
264
        LOG.debug("Add ephemeral partition (%(size)d MB) to device: %(dev)s",
187
265
                 {'dev': dev, 'size': ephemeral_mb})
203
281
    # partition until the end of the disk.
204
282
    LOG.debug("Add root partition (%(size)d MB) to device: %(dev)s",
205
283
             {'dev': dev, 'size': root_mb})
206
 
    part_num = dp.add_partition(root_mb)
 
284
    part_num = dp.add_partition(root_mb, bootable=(boot_option == "local" and
 
285
                                                   boot_mode == "bios"))
207
286
    part_dict['root'] = part_template % part_num
208
287
 
209
288
    if commit:
214
293
 
215
294
def is_block_device(dev):
216
295
    """Check whether a device is block or not."""
217
 
    s = os.stat(dev)
218
 
    return stat.S_ISBLK(s.st_mode)
 
296
    attempts = CONF.deploy.iscsi_verify_attempts
 
297
    for attempt in range(attempts):
 
298
        try:
 
299
            s = os.stat(dev)
 
300
        except OSError as e:
 
301
            LOG.debug("Unable to stat device %(dev)s. Attempt %(attempt)d "
 
302
                      "out of %(total)d. Error: %(err)s", {"dev": dev,
 
303
                      "attempt": attempt + 1, "total": attempts, "err": e})
 
304
            time.sleep(1)
 
305
        else:
 
306
            return stat.S_ISBLK(s.st_mode)
 
307
    msg = _("Unable to stat device %(dev)s after attempting to verify "
 
308
            "%(attempts)d times.") % {'dev': dev, 'attempts': attempts}
 
309
    LOG.error(msg)
 
310
    raise exception.InstanceDeployFailure(msg)
219
311
 
220
312
 
221
313
def dd(src, dst):
231
323
        images.convert_image(src, dst, 'raw', True)
232
324
 
233
325
 
234
 
def mkswap(dev, label='swap1'):
235
 
    """Execute mkswap on a device."""
236
 
    utils.mkfs('swap', dev, label)
237
 
 
238
 
 
239
 
def mkfs_ephemeral(dev, ephemeral_format, label="ephemeral0"):
240
 
    utils.mkfs(ephemeral_format, dev, label)
 
326
# TODO(rameshg87): Remove this one-line method and use utils.mkfs
 
327
# directly.
 
328
def mkfs(fs, dev, label=None):
 
329
    """Execute mkfs on a device."""
 
330
    utils.mkfs(fs, dev, label)
241
331
 
242
332
 
243
333
def block_uuid(dev):
248
338
    return out.strip()
249
339
 
250
340
 
251
 
def switch_pxe_config(path, root_uuid, boot_mode):
252
 
    """Switch a pxe config from deployment mode to service mode."""
 
341
def _replace_lines_in_file(path, regex_pattern, replacement):
253
342
    with open(path) as f:
254
343
        lines = f.readlines()
 
344
 
 
345
    compiled_pattern = re.compile(regex_pattern)
 
346
    with open(path, 'w') as f:
 
347
        for line in lines:
 
348
            line = compiled_pattern.sub(replacement, line)
 
349
            f.write(line)
 
350
 
 
351
 
 
352
def _replace_root_uuid(path, root_uuid):
255
353
    root = 'UUID=%s' % root_uuid
256
 
    rre = re.compile(r'\{\{ ROOT \}\}')
 
354
    pattern = r'\{\{ ROOT \}\}'
 
355
    _replace_lines_in_file(path, pattern, root)
 
356
 
 
357
 
 
358
def _replace_boot_line(path, boot_mode, is_whole_disk_image):
 
359
    if is_whole_disk_image:
 
360
        boot_disk_type = 'boot_whole_disk'
 
361
    else:
 
362
        boot_disk_type = 'boot_partition'
257
363
 
258
364
    if boot_mode == 'uefi':
259
 
        dre = re.compile('^default=.*$')
260
 
        boot_line = 'default=boot'
 
365
        pattern = '^default=.*$'
 
366
        boot_line = 'default=%s' % boot_disk_type
261
367
    else:
262
368
        pxe_cmd = 'goto' if CONF.pxe.ipxe_enabled else 'default'
263
 
        dre = re.compile('^%s .*$' % pxe_cmd)
264
 
        boot_line = '%s boot' % pxe_cmd
265
 
 
266
 
    with open(path, 'w') as f:
267
 
        for line in lines:
268
 
            line = rre.sub(root, line)
269
 
            line = dre.sub(boot_line, line)
270
 
            f.write(line)
 
369
        pattern = '^%s .*$' % pxe_cmd
 
370
        boot_line = '%s %s' % (pxe_cmd, boot_disk_type)
 
371
 
 
372
    _replace_lines_in_file(path, pattern, boot_line)
 
373
 
 
374
 
 
375
def _replace_disk_identifier(path, disk_identifier):
 
376
    pattern = r'\{\{ DISK_IDENTIFIER \}\}'
 
377
    _replace_lines_in_file(path, pattern, disk_identifier)
 
378
 
 
379
 
 
380
def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode,
 
381
                      is_whole_disk_image):
 
382
    """Switch a pxe config from deployment mode to service mode.
 
383
 
 
384
    :param path: path to the pxe config file in tftpboot.
 
385
    :param root_uuid_or_disk_id: root uuid in case of partition image or
 
386
                                 disk_id in case of whole disk image.
 
387
    :param boot_mode: if boot mode is uefi or bios.
 
388
    :param is_whole_disk_image: if the image is a whole disk image or not.
 
389
    """
 
390
    if not is_whole_disk_image:
 
391
        _replace_root_uuid(path, root_uuid_or_disk_id)
 
392
    else:
 
393
        _replace_disk_identifier(path, root_uuid_or_disk_id)
 
394
 
 
395
    _replace_boot_line(path, boot_mode, is_whole_disk_image)
271
396
 
272
397
 
273
398
def notify(address, port):
418
543
 
419
544
def work_on_disk(dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format,
420
545
                 image_path, node_uuid, preserve_ephemeral=False,
421
 
                 configdrive=None):
 
546
                 configdrive=None, boot_option="netboot",
 
547
                 boot_mode="bios"):
422
548
    """Create partitions and copy an image to the root partition.
423
549
 
424
550
    :param dev: Path for the device to work on.
435
561
        partition table has not changed).
436
562
    :param configdrive: Optional. Base64 encoded Gzipped configdrive content
437
563
                        or configdrive HTTP URL.
438
 
    :returns: the UUID of the root partition.
 
564
    :param boot_option: Can be "local" or "netboot". "netboot" by default.
 
565
    :param boot_mode: Can be "bios" or "uefi". "bios" by default.
 
566
    :returns: a dictionary containing the UUID of root partition and efi system
 
567
        partition (if boot mode is uefi).
439
568
    """
440
 
    if not is_block_device(dev):
441
 
        raise exception.InstanceDeployFailure(
442
 
            _("Parent device '%s' not found") % dev)
443
 
 
444
569
    # the only way for preserve_ephemeral to be set to true is if we are
445
570
    # rebuilding an instance with --preserve_ephemeral.
446
571
    commit = not preserve_ephemeral
458
583
                                                                node_uuid)
459
584
 
460
585
        part_dict = make_partitions(dev, root_mb, swap_mb, ephemeral_mb,
461
 
                                    configdrive_mb, commit=commit)
 
586
                                    configdrive_mb, commit=commit,
 
587
                                    boot_option=boot_option,
 
588
                                    boot_mode=boot_mode)
462
589
 
463
590
        ephemeral_part = part_dict.get('ephemeral')
464
591
        swap_part = part_dict.get('swap')
469
596
            raise exception.InstanceDeployFailure(
470
597
                _("Root device '%s' not found") % root_part)
471
598
 
472
 
        for part in ('swap', 'ephemeral', 'configdrive'):
 
599
        for part in ('swap', 'ephemeral', 'configdrive',
 
600
                     'efi system partition'):
473
601
            part_device = part_dict.get(part)
474
602
            LOG.debug("Checking for %(part)s device (%(dev)s) on node "
475
603
                      "%(node)s.", {'part': part, 'dev': part_device,
479
607
                    _("'%(partition)s' device '%(part_device)s' not found") %
480
608
                    {'partition': part, 'part_device': part_device})
481
609
 
 
610
        # If it's a uefi localboot, then we have created the efi system
 
611
        # partition.  Create a fat filesystem on it.
 
612
        if boot_mode == "uefi" and boot_option == "local":
 
613
            efi_system_part = part_dict.get('efi system partition')
 
614
            mkfs(dev=efi_system_part, fs='vfat', label='efi-part')
 
615
 
482
616
        if configdrive_part:
483
617
            # Copy the configdrive content to the configdrive partition
484
618
            dd(configdrive_file, configdrive_part)
492
626
    populate_image(image_path, root_part)
493
627
 
494
628
    if swap_part:
495
 
        mkswap(swap_part)
 
629
        mkfs(dev=swap_part, fs='swap', label='swap1')
496
630
 
497
631
    if ephemeral_part and not preserve_ephemeral:
498
 
        mkfs_ephemeral(ephemeral_part, ephemeral_format)
 
632
        mkfs(dev=ephemeral_part, fs=ephemeral_format, label="ephemeral0")
 
633
 
 
634
    uuids_to_return = {
 
635
        'root uuid': root_part,
 
636
        'efi system partition uuid': part_dict.get('efi system partition')
 
637
    }
499
638
 
500
639
    try:
501
 
        root_uuid = block_uuid(root_part)
 
640
        for part, part_dev in six.iteritems(uuids_to_return):
 
641
            if part_dev:
 
642
                uuids_to_return[part] = block_uuid(part_dev)
 
643
 
502
644
    except processutils.ProcessExecutionError:
503
645
        with excutils.save_and_reraise_exception():
504
 
            LOG.error(_LE("Failed to detect root device UUID."))
505
 
 
506
 
    return root_uuid
507
 
 
508
 
 
509
 
def deploy(address, port, iqn, lun, image_path,
 
646
            LOG.error(_LE("Failed to detect %s"), part)
 
647
 
 
648
    return uuids_to_return
 
649
 
 
650
 
 
651
def deploy_partition_image(address, port, iqn, lun, image_path,
510
652
           root_mb, swap_mb, ephemeral_mb, ephemeral_format, node_uuid,
511
 
           preserve_ephemeral=False, configdrive=None):
512
 
    """All-in-one function to deploy a node.
 
653
           preserve_ephemeral=False, configdrive=None,
 
654
           boot_option="netboot", boot_mode="bios"):
 
655
    """All-in-one function to deploy a partition image to a node.
513
656
 
514
657
    :param address: The iSCSI IP address.
515
658
    :param port: The iSCSI port number.
528
671
        partition table has not changed).
529
672
    :param configdrive: Optional. Base64 encoded Gzipped configdrive content
530
673
                        or configdrive HTTP URL.
531
 
    :returns: the UUID of the root partition.
 
674
    :param boot_option: Can be "local" or "netboot". "netboot" by default.
 
675
    :param boot_mode: Can be "bios" or "uefi". "bios" by default.
 
676
    :returns: a dictionary containing the UUID of root partition and efi system
 
677
        partition (if boot mode is uefi).
 
678
    """
 
679
    with _iscsi_setup_and_handle_errors(address, port, iqn,
 
680
                                        lun, image_path) as dev:
 
681
        image_mb = get_image_mb(image_path)
 
682
        if image_mb > root_mb:
 
683
            root_mb = image_mb
 
684
 
 
685
        uuid_dict_returned = work_on_disk(
 
686
            dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format, image_path,
 
687
            node_uuid, preserve_ephemeral=preserve_ephemeral,
 
688
            configdrive=configdrive, boot_option=boot_option,
 
689
            boot_mode=boot_mode)
 
690
 
 
691
    return uuid_dict_returned
 
692
 
 
693
 
 
694
def deploy_disk_image(address, port, iqn, lun,
 
695
                      image_path, node_uuid):
 
696
    """All-in-one function to deploy a whole disk image to a node.
 
697
 
 
698
    :param address: The iSCSI IP address.
 
699
    :param port: The iSCSI port number.
 
700
    :param iqn: The iSCSI qualified name.
 
701
    :param lun: The iSCSI logical unit number.
 
702
    :param image_path: Path for the instance's disk image.
 
703
    :param node_uuid: node's uuid. Used for logging. Currently not in use
 
704
        by this function but could be used in the future.
 
705
    :returns: a dictionary containing the disk identifier for the disk.
 
706
    """
 
707
    with _iscsi_setup_and_handle_errors(address, port, iqn,
 
708
                                        lun, image_path) as dev:
 
709
        populate_image(image_path, dev)
 
710
        disk_identifier = get_disk_identifier(dev)
 
711
 
 
712
    return {'disk identifier': disk_identifier}
 
713
 
 
714
 
 
715
@contextlib.contextmanager
 
716
def _iscsi_setup_and_handle_errors(address, port, iqn, lun,
 
717
                                   image_path):
 
718
    """Function that yields an iSCSI target device to work on.
 
719
 
 
720
    :param address: The iSCSI IP address.
 
721
    :param port: The iSCSI port number.
 
722
    :param iqn: The iSCSI qualified name.
 
723
    :param lun: The iSCSI logical unit number.
 
724
    :param image_path: Path for the instance's disk image.
532
725
    """
533
726
    dev = get_dev(address, port, iqn, lun)
534
 
    image_mb = get_image_mb(image_path)
535
 
    if image_mb > root_mb:
536
 
        root_mb = image_mb
537
727
    discovery(address, port)
538
728
    login_iscsi(address, port, iqn)
 
729
    if not is_block_device(dev):
 
730
        raise exception.InstanceDeployFailure(_("Parent device '%s' not found")
 
731
                                                % dev)
539
732
    try:
540
 
        root_uuid = work_on_disk(dev, root_mb, swap_mb, ephemeral_mb,
541
 
                                 ephemeral_format, image_path, node_uuid,
542
 
                                 preserve_ephemeral=preserve_ephemeral,
543
 
                                 configdrive=configdrive)
 
733
        yield dev
544
734
    except processutils.ProcessExecutionError as err:
545
735
        with excutils.save_and_reraise_exception():
546
736
            LOG.error(_LE("Deploy to address %s failed."), address)
555
745
        logout_iscsi(address, port, iqn)
556
746
        delete_iscsi(address, port, iqn)
557
747
 
558
 
    return root_uuid
559
 
 
560
748
 
561
749
def notify_deploy_complete(address):
562
750
    """Notifies the completion of deployment to the baremetal node.
652
840
    for port in task.ports:
653
841
        if port.extra.get('vif_port_id'):
654
842
            return port.address
 
843
 
 
844
 
 
845
def parse_instance_info_capabilities(node):
 
846
    """Parse the instance_info capabilities.
 
847
 
 
848
    One way of having these capabilities set is via Nova, where the
 
849
    capabilities are defined in the Flavor extra_spec and passed to
 
850
    Ironic by the Nova Ironic driver.
 
851
 
 
852
    NOTE: Although our API fully supports JSON fields, to maintain the
 
853
    backward compatibility with Juno the Nova Ironic driver is sending
 
854
    it as a string.
 
855
 
 
856
    :param node: a single Node.
 
857
    :raises: InvalidParameterValue if the capabilities string is not a
 
858
             dictionary or is malformed.
 
859
    :returns: A dictionary with the capabilities if found, otherwise an
 
860
              empty dictionary.
 
861
    """
 
862
 
 
863
    def parse_error():
 
864
        error_msg = (_('Error parsing capabilities from Node %s instance_info '
 
865
                       'field. A dictionary or a "jsonified" dictionary is '
 
866
                       'expected.') % node.uuid)
 
867
        raise exception.InvalidParameterValue(error_msg)
 
868
 
 
869
    capabilities = node.instance_info.get('capabilities', {})
 
870
    if isinstance(capabilities, six.string_types):
 
871
        try:
 
872
            capabilities = jsonutils.loads(capabilities)
 
873
        except (ValueError, TypeError):
 
874
            parse_error()
 
875
 
 
876
    if not isinstance(capabilities, dict):
 
877
        parse_error()
 
878
 
 
879
    return capabilities
 
880
 
 
881
 
 
882
def agent_get_clean_steps(task):
 
883
    """Get the list of clean steps from the agent.
 
884
 
 
885
    #TODO(JoshNang) move to BootInterface
 
886
 
 
887
    :param task: a TaskManager object containing the node
 
888
    :raises: NodeCleaningFailure if the agent returns invalid results
 
889
    :returns: A list of clean step dictionaries
 
890
    """
 
891
    client = _get_agent_client()
 
892
    ports = objects.Port.list_by_node_id(
 
893
        task.context, task.node.id)
 
894
    result = client.get_clean_steps(task.node, ports).get('command_result')
 
895
 
 
896
    if ('clean_steps' not in result or
 
897
            'hardware_manager_version' not in result):
 
898
        raise exception.NodeCleaningFailure(_(
 
899
            'get_clean_steps for node %(node)s returned invalid result:'
 
900
            ' %(result)s') % ({'node': task.node.uuid, 'result': result}))
 
901
 
 
902
    driver_info = task.node.driver_internal_info
 
903
    driver_info['hardware_manager_version'] = result[
 
904
        'hardware_manager_version']
 
905
    task.node.driver_internal_info = driver_info
 
906
    task.node.save()
 
907
 
 
908
    # Clean steps looks like {'HardwareManager': [{step1},{steps2}..]..}
 
909
    # Flatten clean steps into one list
 
910
    steps_list = [step for step_list in
 
911
                  result['clean_steps'].values()
 
912
                  for step in step_list]
 
913
    # Filter steps to only return deploy steps
 
914
    steps = [step for step in steps_list
 
915
             if step.get('interface') == 'deploy']
 
916
    return steps
 
917
 
 
918
 
 
919
def agent_execute_clean_step(task, step):
 
920
    """Execute a clean step asynchronously on the agent.
 
921
 
 
922
    #TODO(JoshNang) move to BootInterface
 
923
 
 
924
    :param task: a TaskManager object containing the node
 
925
    :param step: a clean step dictionary to execute
 
926
    :raises: NodeCleaningFailure if the agent does not return a command status
 
927
    :returns: states.CLEANING to signify the step will be completed async
 
928
    """
 
929
    client = _get_agent_client()
 
930
    ports = objects.Port.list_by_node_id(
 
931
        task.context, task.node.id)
 
932
    result = client.execute_clean_step(step, task.node, ports)
 
933
    if not result.get('command_status'):
 
934
        raise exception.NodeCleaningFailure(_(
 
935
            'Agent on node %(node)s returned bad command result: '
 
936
            '%(result)s') % {'node': task.node.uuid,
 
937
                             'result': result.get('command_error')})
 
938
    return states.CLEANING
 
939
 
 
940
 
 
941
def try_set_boot_device(task, device, persistent=True):
 
942
    """Tries to set the boot device on the node.
 
943
 
 
944
    This method tries to set the boot device on the node to the given
 
945
    boot device.  Under uefi boot mode, setting of boot device may differ
 
946
    between different machines. IPMI does not work for setting boot
 
947
    devices in uefi mode for certain machines.  This method ignores the
 
948
    expected IPMI failure for uefi boot mode and just logs a message.
 
949
    In error cases, it is expected the operator has to manually set the
 
950
    node to boot from the correct device.
 
951
 
 
952
    :param task: a TaskManager object containing the node
 
953
    :param device: the boot device
 
954
    :param persistent: Whether to set the boot device persistently
 
955
    :raises: Any exception from set_boot_device except IPMIFailure
 
956
        (setting of boot device using ipmi is expected to fail).
 
957
    """
 
958
    try:
 
959
        manager_utils.node_set_boot_device(task, device,
 
960
                                           persistent=persistent)
 
961
    except exception.IPMIFailure:
 
962
        if driver_utils.get_node_capability(task.node,
 
963
                                            'boot_mode') == 'uefi':
 
964
            LOG.warning(_LW("ipmitool is unable to set boot device while "
 
965
                            "the node %s is in UEFI boot mode. Please set "
 
966
                            "the boot device manually.") % task.node.uuid)
 
967
        else:
 
968
            raise
 
969
 
 
970
 
 
971
def parse_root_device_hints(node):
 
972
    """Parse the root_device property of a node.
 
973
 
 
974
    Parse the root_device property of a node and make it a flat string
 
975
    to be passed via the PXE config.
 
976
 
 
977
    :param node: a single Node.
 
978
    :returns: A flat string with the following format
 
979
              opt1=value1,opt2=value2. Or None if the
 
980
              Node contains no hints.
 
981
    :raises: InvalidParameterValue, if some information is invalid.
 
982
 
 
983
    """
 
984
    root_device = node.properties.get('root_device')
 
985
    if not root_device:
 
986
        return
 
987
 
 
988
    # Find invalid hints for logging
 
989
    invalid_hints = set(root_device) - VALID_ROOT_DEVICE_HINTS
 
990
    if invalid_hints:
 
991
        raise exception.InvalidParameterValue(
 
992
            _('The hints "%(invalid_hints)s" are invalid. '
 
993
              'Valid hints are: "%(valid_hints)s"') %
 
994
            {'invalid_hints': ', '.join(invalid_hints),
 
995
             'valid_hints': ', '.join(VALID_ROOT_DEVICE_HINTS)})
 
996
 
 
997
    if 'size' in root_device:
 
998
        try:
 
999
            int(root_device['size'])
 
1000
        except ValueError:
 
1001
            raise exception.InvalidParameterValue(
 
1002
                _('Root device hint "size" is not an integer value.'))
 
1003
 
 
1004
    hints = []
 
1005
    for key, value in root_device.items():
 
1006
        # NOTE(lucasagomes): We can't have spaces in the PXE config
 
1007
        # file, so we are going to url/percent encode the value here
 
1008
        # and decode on the other end.
 
1009
        if isinstance(value, six.string_types):
 
1010
            value = value.strip()
 
1011
            value = parse.quote(value)
 
1012
 
 
1013
        hints.append("%s=%s" % (key, value))
 
1014
 
 
1015
    return ','.join(hints)