diff options
-rw-r--r-- | doc/source/admin/hardware_managers.rst | 9 | ||||
-rw-r--r-- | ironic_python_agent/hardware.py | 267 | ||||
-rw-r--r-- | ironic_python_agent/raid_utils.py | 36 | ||||
-rw-r--r-- | ironic_python_agent/tests/unit/extensions/test_image.py | 6 | ||||
-rw-r--r-- | ironic_python_agent/tests/unit/samples/hardware_samples.py | 155 | ||||
-rw-r--r-- | ironic_python_agent/tests/unit/test_hardware.py | 556 | ||||
-rw-r--r-- | ironic_python_agent/tests/unit/test_raid_utils.py | 14 | ||||
-rw-r--r-- | ironic_python_agent/utils.py | 19 | ||||
-rw-r--r-- | releasenotes/notes/enable-skipping-raids-40263cc3a19cfd27.yaml | 6 | ||||
-rw-r--r-- | releasenotes/notes/prioritize-lsblk-device-serials-8cae406ca5164a01.yaml | 8 | ||||
-rw-r--r-- | releasenotes/source/index.rst | 1 | ||||
-rw-r--r-- | releasenotes/source/yoga.rst | 6 | ||||
-rw-r--r-- | releasenotes/source/zed.rst | 6 | ||||
-rw-r--r-- | zuul.d/project.yaml | 3 |
14 files changed, 952 insertions, 140 deletions
diff --git a/doc/source/admin/hardware_managers.rst b/doc/source/admin/hardware_managers.rst index 6c72ba1a..90e6c298 100644 --- a/doc/source/admin/hardware_managers.rst +++ b/doc/source/admin/hardware_managers.rst @@ -121,6 +121,15 @@ containing hints to identify the drives. For example:: 'skip_block_devices': [{'name': '/dev/vda', 'vendor': '0x1af4'}] +To prevent software RAID devices from being deleted, put their volume name +(defined in the ``target_raid_config``) to the list. + +Note: one dictionary with one value for each of the logical disks. +For example:: + + 'skip_block_devices': [{'volume_name': 'large'}, {'volume_name': 'temp'}] + + Shared Disk Cluster Filesystems ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py index eda78f4a..0fbdab3b 100644 --- a/ironic_python_agent/hardware.py +++ b/ironic_python_agent/hardware.py @@ -648,6 +648,9 @@ def list_all_block_devices(block_type='disk', name = os.path.join('/dev', device_raw['kname']) extra = {} + lsblk_serial = device_raw.get('serial') + if lsblk_serial: + extra['serial'] = lsblk_serial try: udev = pyudev.Devices.from_device_file(context, name) except pyudev.DeviceNotFoundByFileError as e: @@ -658,17 +661,21 @@ def list_all_block_devices(block_type='disk', "skipping... Error: %(error)s", {'dev': name, 'error': e}) else: - # TODO(lucasagomes): Since lsblk only supports - # returning the short serial we are using - # ID_SERIAL_SHORT first to keep compatibility with the - # bash deploy ramdisk - for key, udev_key in [ - ('serial', 'SERIAL_SHORT'), - ('serial', 'SERIAL'), + # lsblk serial information is prioritized over + # udev serial information + udev_property_mappings = [ ('wwn', 'WWN'), ('wwn_with_extension', 'WWN_WITH_EXTENSION'), ('wwn_vendor_extension', 'WWN_VENDOR_EXTENSION') - ]: + ] + # Only check device serial information from udev + # when lsblk returned None + if not lsblk_serial: + udev_property_mappings += [ + ('serial', 'SERIAL_SHORT'), + ('serial', 'SERIAL') + ] + for key, udev_key in udev_property_mappings: if key in extra: continue value = (udev.get(f'ID_{udev_key}') @@ -863,6 +870,17 @@ class HardwareManager(object, metaclass=abc.ABCMeta): """ raise errors.IncompatibleHardwareMethodError + def get_skip_list_from_node(self, node, + block_devices=None, just_raids=False): + """Get the skip block devices list from the node + + :param block_devices: a list of BlockDevices + :param just_raids: a boolean to signify that only RAID devices + are important + :return: A set of names of devices on the skip list + """ + raise errors.IncompatibleHardwareMethodError + def list_block_devices_check_skip_list(self, node, include_partitions=False): """List physical block devices without the ones listed in @@ -1391,17 +1409,22 @@ class GenericHardwareManager(HardwareManager): ) return block_devices - def list_block_devices_check_skip_list(self, node, - include_partitions=False): - block_devices = self.list_block_devices( - include_partitions=include_partitions) + def get_skip_list_from_node(self, node, + block_devices=None, just_raids=False): properties = node.get('properties', {}) skip_list_hints = properties.get("skip_block_devices", []) if not skip_list_hints: - return block_devices + return None + if just_raids: + return {d['volume_name'] for d in skip_list_hints + if 'volume_name' in d} + if not block_devices: + return None skip_list = set() serialized_devs = [dev.serialize() for dev in block_devices] for hint in skip_list_hints: + if 'volume_name' in hint: + continue found_devs = il_utils.find_devices_by_hints(serialized_devs, hint) excluded_devs = {dev['name'] for dev in found_devs} skipped_devices = excluded_devs.difference(skip_list) @@ -1409,8 +1432,17 @@ class GenericHardwareManager(HardwareManager): if skipped_devices: LOG.warning("Using hint %(hint)s skipping devices: %(devs)s", {'hint': hint, 'devs': ','.join(skipped_devices)}) - block_devices = [d for d in block_devices - if d.name not in skip_list] + return skip_list + + def list_block_devices_check_skip_list(self, node, + include_partitions=False): + block_devices = self.list_block_devices( + include_partitions=include_partitions) + skip_list = self.get_skip_list_from_node( + node, block_devices) + if skip_list is not None: + block_devices = [d for d in block_devices + if d.name not in skip_list] return block_devices def get_os_install_device(self, permit_refresh=False): @@ -2341,15 +2373,41 @@ class GenericHardwareManager(HardwareManager): return self._do_create_configuration(node, ports, raid_config) def _do_create_configuration(self, node, ports, raid_config): + def _get_volume_names_of_existing_raids(): + list_of_raids = [] + raid_devices = list_all_block_devices(block_type='raid', + ignore_raid=False, + ignore_empty=False) + raid_devices.extend( + list_all_block_devices(block_type='md', + ignore_raid=False, + ignore_empty=False) + ) + for raid_device in raid_devices: + device = raid_device.name + try: + il_utils.execute('mdadm', '--examine', + device, use_standard_locale=True) + except processutils.ProcessExecutionError as e: + if "No md superblock detected" in str(e): + continue + volume_name = raid_utils.get_volume_name_of_raid_device(device) + if volume_name: + list_of_raids.append(volume_name) + else: + list_of_raids.append("unnamed_raid") + return list_of_raids + # No 'software' controller: do nothing. If 'controller' is # set to 'software' on only one of the drives, the validation # code will catch it. software_raid = False logical_disks = raid_config.get('logical_disks') + software_raid_disks = [] for logical_disk in logical_disks: if logical_disk.get('controller') == 'software': software_raid = True - break + software_raid_disks.append(logical_disk) if not software_raid: LOG.debug("No Software RAID config found") return {} @@ -2359,24 +2417,51 @@ class GenericHardwareManager(HardwareManager): # Check if the config is compliant with current limitations. self.validate_configuration(raid_config, node) + # Remove any logical disk from being eligible for inclusion in the + # RAID if it's on the skip list + skip_list = self.get_skip_list_from_node( + node, just_raids=True) + rm_from_list = [] + if skip_list: + present_raids = _get_volume_names_of_existing_raids() + if present_raids: + for ld in logical_disks: + volume_name = ld.get('volume_name', None) + if volume_name in skip_list \ + and volume_name in present_raids: + rm_from_list.append(ld) + LOG.debug("Software RAID device with volume name %s " + "exists and is, therefore, not going to be " + "created", volume_name) + present_raids.remove(volume_name) + # NOTE(kubajj): Raise an error if there is an existing software + # RAID device that either does not have a volume name or does not + # match one on the skip list + if present_raids: + msg = ("Existing Software RAID device detected that should" + " not") + raise errors.SoftwareRAIDError(msg) + logical_disks = [d for d in logical_disks if d not in rm_from_list] + # Log the validated target_raid_configuration. LOG.debug("Target Software RAID configuration: %s", raid_config) block_devices, logical_disks = raid_utils.get_block_devices_for_raid( self.list_block_devices(), logical_disks) - # Make sure there are no partitions yet (or left behind). - with_parts = [] - for dev_name in block_devices: - try: - if disk_utils.list_partitions(dev_name): - with_parts.append(dev_name) - except processutils.ProcessExecutionError: - # Presumably no partitions (or no partition table) - continue - if with_parts: - msg = ("Partitions detected on devices %s during RAID config" % - ', '.join(with_parts)) - raise errors.SoftwareRAIDError(msg) + if not rm_from_list: + # Make sure there are no partitions yet (or left behind). + with_parts = [] + for dev_name in block_devices: + try: + if disk_utils.list_partitions(dev_name): + with_parts.append(dev_name) + except processutils.ProcessExecutionError: + # Presumably no partitions (or no partition table) + continue + if with_parts: + msg = ("Partitions detected on devices %s during RAID config" % + ', '.join(with_parts)) + raise errors.SoftwareRAIDError(msg) partition_table_type = utils.get_partition_table_type_from_specs(node) target_boot_mode = utils.get_node_boot_mode(node) @@ -2484,10 +2569,12 @@ class GenericHardwareManager(HardwareManager): return raid_devices raid_devices = _scan_raids() + skip_list = self.get_skip_list_from_node( + node, just_raids=True) attempts = 0 while attempts < 2: attempts += 1 - self._delete_config_pass(raid_devices) + self._delete_config_pass(raid_devices, skip_list) raid_devices = _scan_raids() if not raid_devices: break @@ -2497,9 +2584,22 @@ class GenericHardwareManager(HardwareManager): LOG.error(msg) raise errors.SoftwareRAIDError(msg) - def _delete_config_pass(self, raid_devices): + def _delete_config_pass(self, raid_devices, skip_list): all_holder_disks = [] + do_not_delete_devices = set() + delete_partitions = {} for raid_device in raid_devices: + do_not_delete = False + volume_name = raid_utils.get_volume_name_of_raid_device( + raid_device.name) + if volume_name: + LOG.info("Software RAID device %(dev)s has volume name" + "%(name)s", {'dev': raid_device.name, + 'name': volume_name}) + if skip_list and volume_name in skip_list: + LOG.warning("RAID device %s will not be deleted", + raid_device.name) + do_not_delete = True component_devices = get_component_devices(raid_device.name) if not component_devices: # A "Software RAID device" without components is usually @@ -2511,52 +2611,73 @@ class GenericHardwareManager(HardwareManager): continue holder_disks = get_holder_disks(raid_device.name) - LOG.info("Deleting Software RAID device %s", raid_device.name) + if do_not_delete: + LOG.warning("Software RAID device %(dev)s is not going to be " + "deleted as its volume name - %(vn)s - is on the " + "skip list", {'dev': raid_device.name, + 'vn': volume_name}) + else: + LOG.info("Deleting Software RAID device %s", raid_device.name) LOG.debug('Found component devices %s', component_devices) LOG.debug('Found holder disks %s', holder_disks) - # Remove md devices. - try: - il_utils.execute('wipefs', '-af', raid_device.name) - except processutils.ProcessExecutionError as e: - LOG.warning('Failed to wipefs %(device)s: %(err)s', - {'device': raid_device.name, 'err': e}) - try: - il_utils.execute('mdadm', '--stop', raid_device.name) - except processutils.ProcessExecutionError as e: - LOG.warning('Failed to stop %(device)s: %(err)s', - {'device': raid_device.name, 'err': e}) - - # Remove md metadata from component devices. - for component_device in component_devices: + if not do_not_delete: + # Remove md devices. try: - il_utils.execute('mdadm', '--examine', component_device, - use_standard_locale=True) + il_utils.execute('wipefs', '-af', raid_device.name) except processutils.ProcessExecutionError as e: - if "No md superblock detected" in str(e): - # actually not a component device - continue - else: - msg = "Failed to examine device {}: {}".format( - component_device, e) - raise errors.SoftwareRAIDError(msg) - - LOG.debug('Deleting md superblock on %s', component_device) + LOG.warning('Failed to wipefs %(device)s: %(err)s', + {'device': raid_device.name, 'err': e}) try: - il_utils.execute('mdadm', '--zero-superblock', - component_device) + il_utils.execute('mdadm', '--stop', raid_device.name) except processutils.ProcessExecutionError as e: - LOG.warning('Failed to remove superblock from' - '%(device)s: %(err)s', + LOG.warning('Failed to stop %(device)s: %(err)s', {'device': raid_device.name, 'err': e}) + # Remove md metadata from component devices. + for component_device in component_devices: + try: + il_utils.execute('mdadm', '--examine', + component_device, + use_standard_locale=True) + except processutils.ProcessExecutionError as e: + if "No md superblock detected" in str(e): + # actually not a component device + continue + else: + msg = "Failed to examine device {}: {}".format( + component_device, e) + raise errors.SoftwareRAIDError(msg) + + LOG.debug('Deleting md superblock on %s', component_device) + try: + il_utils.execute('mdadm', '--zero-superblock', + component_device) + except processutils.ProcessExecutionError as e: + LOG.warning('Failed to remove superblock from' + '%(device)s: %(err)s', + {'device': raid_device.name, 'err': e}) + if skip_list: + dev, part = utils.split_device_and_partition_number( + component_device) + if dev in delete_partitions: + delete_partitions[dev].append(part) + else: + delete_partitions[dev] = [part] + else: + for component_device in component_devices: + do_not_delete_devices.add(component_device) + # NOTE(arne_wiebalck): We cannot delete the partitions right # away since there may be other partitions on the same disks # which are members of other RAID devices. So we remember them # for later. all_holder_disks.extend(holder_disks) - - LOG.info('Deleted Software RAID device %s', raid_device.name) + if do_not_delete: + LOG.warning("Software RAID device %s was not deleted", + raid_device.name) + else: + LOG.info('Deleted Software RAID device %s', raid_device.name) # Remove all remaining raid traces from any drives, in case some # drives or partitions have been member of some raid once @@ -2581,7 +2702,13 @@ class GenericHardwareManager(HardwareManager): # mdadm: Couldn't open /dev/block for write - not zeroing # mdadm -E /dev/block1: still shows superblocks all_blks = reversed(self.list_block_devices(include_partitions=True)) + do_not_delete_disks = set() for blk in all_blks: + if blk.name in do_not_delete_devices: + do_not_delete_disks.add(utils.extract_device(blk.name)) + continue + if blk.name in do_not_delete_disks: + continue try: il_utils.execute('mdadm', '--examine', blk.name, use_standard_locale=True) @@ -2604,6 +2731,20 @@ class GenericHardwareManager(HardwareManager): all_holder_disks_uniq = list( collections.OrderedDict.fromkeys(all_holder_disks)) for holder_disk in all_holder_disks_uniq: + if holder_disk in do_not_delete_disks: + # Remove just partitions not listed in keep_partitions + del_list = delete_partitions[holder_disk] + if del_list: + LOG.warning('Holder disk %(dev)s contains logical disk ' + 'on the skip list. Deleting just partitions: ' + '%(parts)s', {'dev': holder_disk, + 'parts': del_list}) + for part in del_list: + il_utils.execute('parted', holder_disk, 'rm', part) + else: + LOG.warning('Holder disk %(dev)s contains only logical ' + 'disk(s) on the skip list', holder_disk) + continue LOG.info('Removing partitions on holder disk %s', holder_disk) try: il_utils.execute('wipefs', '-af', holder_disk) diff --git a/ironic_python_agent/raid_utils.py b/ironic_python_agent/raid_utils.py index 3d5f260f..84c6941f 100644 --- a/ironic_python_agent/raid_utils.py +++ b/ironic_python_agent/raid_utils.py @@ -267,10 +267,44 @@ def get_next_free_raid_device(): name = f'/dev/md{idx}' if name not in names: return name - raise errors.SoftwareRAIDError("No free md (RAID) devices are left") +def get_volume_name_of_raid_device(raid_device): + """Get the volume name of a RAID device + + :param raid_device: A Software RAID block device name. + :returns: volume name of the device, or None + """ + if not raid_device: + return None + try: + out, _ = utils.execute('mdadm', '--detail', raid_device, + use_standard_locale=True) + except processutils.ProcessExecutionError as e: + LOG.warning('Could not retrieve the volume name of %(dev)s: %(err)s', + {'dev': raid_device, 'err': e}) + return None + lines = out.splitlines() + for line in lines: + if re.search(r'Name', line) is not None: + split_array = line.split(':') + # expecting format: + # Name : <host>:name (optional comment) + if len(split_array) == 3: + candidate = split_array[2] + else: + return None + # if name is followed by some other text + # such as (local to host <domain>) remove + # everything after " " + if " " in candidate: + candidate = candidate.split(" ")[0] + volume_name = candidate + return volume_name + return None + + # TODO(rg): handle PreP boot parts relocation as well def prepare_boot_partitions_for_softraid(device, holders, efi_part, target_boot_mode): diff --git a/ironic_python_agent/tests/unit/extensions/test_image.py b/ironic_python_agent/tests/unit/extensions/test_image.py index 6958036f..46afe0ef 100644 --- a/ironic_python_agent/tests/unit/extensions/test_image.py +++ b/ironic_python_agent/tests/unit/extensions/test_image.py @@ -827,7 +827,8 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640 use_standard_locale=True), mock.call('udevadm', 'settle'), mock.call('lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,' + + 'TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]), mock.call('umount', self.fake_dir + '/boot/efi', attempts=3, delay_on_retry=True), @@ -949,7 +950,8 @@ Boot0004* ironic1 HD(1,GPT,55db8d03-c8f6-4a5b-9155-790dddc348fa,0x800,0x640 use_standard_locale=True), mock.call('udevadm', 'settle'), mock.call('lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,' + + 'TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]), mock.call('umount', self.fake_dir + '/boot/efi', attempts=3, delay_on_retry=True), diff --git a/ironic_python_agent/tests/unit/samples/hardware_samples.py b/ironic_python_agent/tests/unit/samples/hardware_samples.py index f9635e43..c00d637b 100644 --- a/ironic_python_agent/tests/unit/samples/hardware_samples.py +++ b/ironic_python_agent/tests/unit/samples/hardware_samples.py @@ -101,33 +101,36 @@ BLK_DEVICE_TEMPLATE = """ { "blockdevices": [ {"kname":"sda", "model":"TinyUSB Drive", "size":3116853504, - "rota":false, "type":"disk", "serial":123, "uuid":"F531-BDC3", + "rota":false, "type":"disk", "serial":"sda123", "uuid":"F531-BDC3", "partuuid":null}, {"kname":"sdb", "model":"Fastable SD131 7", "size":10737418240, - "rota":false, "type":"disk", + "rota":false, "type":"disk", "serial":"sdb123", "uuid":"9a5e5cca-e03d-4cbd-9054-9e6ca9048222", "partuuid":null}, {"kname":"sdc", "model":"NWD-BLP4-1600", "size":1765517033472, - "rota":false, "type":"disk", "uuid":null, "partuuid":null}, + "rota":false, "type":"disk", "serial":"sdc123", "uuid":null, + "partuuid":null}, {"kname":"sdd", "model":"NWD-BLP4-1600", "size":1765517033472, - "rota":false, "type":"disk", "uuid":null, "partuuid":null}, + "rota":false, "type":"disk", "serial":"sdd123", "uuid":null, + "partuuid":null}, {"kname":"loop0", "model":null, "size":109109248, "rota":true, - "type":"loop", "uuid":null, "partuuid": null}, + "type":"loop", "serial":null, "uuid":null, "partuuid": null}, {"kname":"zram0", "model":null, "size":0, "rota":false, "type":"disk", - "uuid":null, "partuuid":null}, + "serial":null, "uuid":null, "partuuid":null}, {"kname":"ram0", "model":null, "size":8388608, "rota":false, - "type":"disk", "uuid":null, "partuuid":null}, + "type":"disk", "serial":null, "uuid":null, "partuuid":null}, {"kname":"ram1", "model":null, "size":8388608, "rota":false, - "type":"disk", "uuid":null, "partuuid":null}, + "type":"disk", "serial":null, "uuid":null, "partuuid":null}, {"kname":"ram2", "model":null, "size":8388608, "rota":false, - "type":"disk", "uuid":null, "partuuid":null}, + "type":"disk", "serial":null, "uuid":null, "partuuid":null}, {"kname":"ram3", "model":null, "size":8388608, "rota":false, - "type":"disk", "uuid":null, "partuuid":null}, + "type":"disk", "serial":null, "uuid":null, "partuuid":null}, {"kname":"fd1", "model":"magic", "size":4096, "rota":true, - "type":"disk", "uuid":null, "partuuid":null}, + "type":"disk", "serial":null, "uuid":null, "partuuid":null}, {"kname":"sdf", "model":"virtual floppy", "size":0, "rota":true, - "type":"disk", "uuid":null, "partuuid":null}, + "type":"disk", "serial":null, "uuid":null, "partuuid":null}, {"kname":"dm-0", "model":"NWD-BLP4-1600", "size":"1765517033472", - "rota":false, "type":"mpath", "uuid":null, "partuuid":null} + "rota":false, "type":"mpath", "serial":null, "uuid":null, + "partuuid":null} ] } """ @@ -137,9 +140,22 @@ BLK_DEVICE_TEMPLATE_SMALL = """ { "blockdevices": [ {"kname":"sda", "model":"TinyUSB Drive", "size":3116853504, "rota":false, - "type":"disk", "uuid":"F531-BDC", "partuuid":null}, + "type":"disk", "serial":"123", "uuid":"F531-BDC", "partuuid":null}, + {"kname":"sdb", "model":"AlmostBigEnough Drive", "size":"4294967295", + "rota":false, "type":"disk", "serial":"456", "uuid":null, "partuuid":null} + ] +} +""" + + +# NOTE This is intentionally have serials removed +BLK_INCOMPLETE_DEVICE_TEMPLATE_SMALL = """ +{ + "blockdevices": [ + {"kname":"sda", "model":"TinyUSB Drive", "size":3116853504, "rota":false, + "type":"disk", "serial":"", "uuid":"F531-BDC", "partuuid":null}, {"kname":"sdb", "model":"AlmostBigEnough Drive", "size":"4294967295", - "rota":false, "type":"disk", "uuid":null, "partuuid":null} + "rota":false, "type":"disk", "serial":"", "uuid":null, "partuuid":null} ] } """ @@ -155,23 +171,23 @@ RAID_BLK_DEVICE_TEMPLATE = (""" { "blockdevices": [ {"kname":"sda", "model":"DRIVE 0", "size":1765517033472, "rota":true, - "type":"disk", "uuid":null, "partuuid":null}, + "type":"disk", "serial":"sda123", "uuid":null, "partuuid":null}, {"kname":"sda1", "model":"DRIVE 0", "size":107373133824, "rota":true, - "type":"part", "uuid":null, "partuuid":null}, + "type":"part", "serial":"sda1123", "uuid":null, "partuuid":null}, {"kname":"sdb", "model":"DRIVE 1", "size":1765517033472, "rota":true, - "type":"disk", "uuid":null, "partuuid":null}, + "type":"disk", "serial":"sdb123", "uuid":null, "partuuid":null}, {"kname":"sdb", "model":"DRIVE 1", "size":1765517033472, "rota":true, "type":"disk", "uuid":null, "partuuid":null}, {"kname":"sdb1", "model":"DRIVE 1", "size":107373133824, "rota":true, - "type":"part", "uuid":null, "partuuid":null}, + "type":"part", "serial":"sdb1123", "uuid":null, "partuuid":null}, {"kname":"md0p1", "model":"RAID", "size":107236818944, "rota":false, - "type":"md", "uuid":null, "partuuid":null}, + "type":"md", "serial":null, "uuid":null, "partuuid":null}, {"kname":"md0", "model":"RAID", "size":1765517033470, "rota":false, - "type":"raid1", "uuid":null, "partuuid":null}, + "type":"raid1", "serial":null, "uuid":null, "partuuid":null}, {"kname":"md0", "model":"RAID", "size":1765517033470, "rota":false, - "type":"raid1", "uuid":null, "partuuid":null}, + "type":"raid1", "serial":null, "uuid":null, "partuuid":null}, {"kname":"md1", "model":"RAID", "size":0, "rota":false, "type":"raid1", - "uuid":null, "partuuid":null} + "serial":null, "uuid":null, "partuuid":null} ] } """) @@ -180,49 +196,52 @@ MULTIPATH_BLK_DEVICE_TEMPLATE = (""" { "blockdevices": [ {"kname":"sda", "model":"INTEL_SSDSC2CT060A3", "size":"60022480896", - "rota":false, "type":"disk", "uuid":null, "partuuid":null}, + "rota":false, "type":"disk", "serial":"sda123", "uuid":null, + "partuuid":null}, {"kname":"sda2", "model":null, "size":"59162722304", "rota":false, "type":"part", "uuid":"f8b55d59-96c3-3982-b129-1b6b2ee8da86", - "partuuid":"c97c8aac-7796-4433-b1fc-9b5fac43edf3"}, + "partuuid":"c97c8aac-7796-4433-b1fc-9b5fac43edf3", "serial":"sda2123"}, {"kname":"sda3", "model":null, "size":"650002432", "rota":false, "type":"part", "uuid":"b3b03565-5f13-3c93-b2a6-6d90e25be926", - "partuuid":"6c85beff-b2bd-4a1c-91b7-8abb5256459d"}, + "partuuid":"6c85beff-b2bd-4a1c-91b7-8abb5256459d", "serial":"sda3123"}, {"kname":"sda1", "model":null, "size":"209715200", "rota":false, "type":"part", "uuid":"0a83355d-7500-3f5f-9abd-66f6fd03714c", - "partuuid":"eba28b26-b76a-402c-94dd-0b66a523a485"}, + "partuuid":"eba28b26-b76a-402c-94dd-0b66a523a485", "serial":"sda1123"}, {"kname":"dm-0", "model":null, "size":"60022480896", "rota":false, - "type":"mpath", "uuid":null, "partuuid":null}, + "type":"mpath", "serial":null, "uuid":null, "partuuid":null}, {"kname":"dm-4", "model":null, "size":"650002432", "rota":false, "type":"part", "uuid":"b3b03565-5f13-3c93-b2a6-6d90e25be926", - "partuuid":"6c85beff-b2bd-4a1c-91b7-8abb5256459d"}, + "partuuid":"6c85beff-b2bd-4a1c-91b7-8abb5256459d", "serial":null}, {"kname":"dm-2", "model":null, "size":"209715200", "rota":false, "type":"part", "uuid":"0a83355d-7500-3f5f-9abd-66f6fd03714c", - "partuuid":"eba28b26-b76a-402c-94dd-0b66a523a485"}, + "partuuid":"eba28b26-b76a-402c-94dd-0b66a523a485", "serial":null}, {"kname":"dm-3", "model":null, "size":"59162722304", "rota":false, "type":"part", "uuid":"f8b55d59-96c3-3982-b129-1b6b2ee8da86", - "partuuid":"c97c8aac-7796-4433-b1fc-9b5fac43edf3"}, + "partuuid":"c97c8aac-7796-4433-b1fc-9b5fac43edf3", "serial":null}, {"kname":"sdb", "model":"INTEL_SSDSC2CT060A3", "size":"60022480896", - "rota":false, "type":"disk", "uuid":null, "partuuid":null}, + "rota":false, "type":"disk", "serial":"sdb123", "uuid":null, + "partuuid":null}, {"kname":"sdb2", "model":null, "size":"59162722304", - "rota":false, "type":"part", + "rota":false, "type":"part", "serial":"sdb2123", "uuid":"f8b55d59-96c3-3982-b129-1b6b2ee8da86", "partuuid":"c97c8aac-7796-4433-b1fc-9b5fac43edf3"}, {"kname":"sdb3", "model":null, "size":"650002432", - "rota":false, "type":"part", + "rota":false, "type":"part", "serial":"sdv3123", "uuid":"b3b03565-5f13-3c93-b2a6-6d90e25be926", "partuuid":"6c85beff-b2bd-4a1c-91b7-8abb5256459d"}, {"kname":"sdb1", "model":null, "size":"209715200", - "rota":false, "type":"part", + "rota":false, "type":"part", "serial":"sdb1123", "uuid":"0a83355d-7500-3f5f-9abd-66f6fd03714c", "partuuid":"eba28b26-b76a-402c-94dd-0b66a523a485"}, {"kname":"sdc", "model":"ST1000DM003-1CH162", "size":"1000204886016", - "rota":true, "type":"disk", "uuid":null, "partuuid":null}, + "rota":true, "type":"disk", "serial":"sdc123", "uuid":null, + "partuuid":null}, {"kname":"sdc1", "model":null, "size":"899999072256", - "rota":true, "type":"part", + "rota":true, "type":"part", "serial":"sdc1123", "uuid":"457f7d3c-9376-4997-89bd-d1a7c8b04060", "partuuid":"c9433d2e-3bbc-47b4-92bf-43c1d80f06e0"}, {"kname":"dm-1", "model":null, "size":"1000204886016", "rota":false, - "type":"mpath", "uuid":null, "partuuid":null} + "type":"mpath", "serial":null, "uuid":null, "partuuid":null} ] } """) @@ -231,9 +250,10 @@ PARTUUID_DEVICE_TEMPLATE = (""" { "blockdevices": [ {"kname":"sda", "model":"DRIVE 0", "size":1765517033472, "rota":true, - "type":"disk", "uuid":null, "partuuid":null}, + "type":"disk", "serial":"sda123", "uuid":null, "partuuid":null}, {"kname":"sda1", "model":"DRIVE 0", "size":107373133824, "rota":true, - "type":"part", "uuid":"987654-3210", "partuuid":"1234-5678"} + "type":"part", "serial":"sda1123", "uuid":"987654-3210", + "partuuid":"1234-5678"} ] } """) @@ -1031,6 +1051,61 @@ Working Devices : 2 1 259 3 1 active sync /dev/nvme1n1p1 """) +MDADM_DETAIL_OUTPUT_VOLUME_NAME = ("""/dev/md0: + Version : 1.0 + Creation Time : Fri Feb 15 12:37:44 2019 + Raid Level : raid1 + Array Size : 1048512 (1023.94 MiB 1073.68 MB) + Used Dev Size : 1048512 (1023.94 MiB 1073.68 MB) + Raid Devices : 2 + Total Devices : 2 + Persistence : Superblock is persistent + + Update Time : Fri Feb 15 12:38:02 2019 + State : clean + Active Devices : 2 + Working Devices : 2 + Failed Devices : 0 + Spare Devices : 0 + +Consistency Policy : resync + + Name : abc.xyz.com:this_name (local to host abc.xyz.com) + UUID : 83143055:2781ddf5:2c8f44c7:9b45d92e + Events : 17 + + Number Major Minor RaidDevice State + 0 253 64 0 active sync /dev/vde1 + 1 253 80 1 active sync /dev/vdf1 +""") + +MDADM_DETAIL_OUTPUT_VOLUME_NAME_INVALID = ("""/dev/md0: + Version : 1.0 + Creation Time : Fri Feb 15 12:37:44 2019 + Raid Level : raid1 + Array Size : 1048512 (1023.94 MiB 1073.68 MB) + Used Dev Size : 1048512 (1023.94 MiB 1073.68 MB) + Raid Devices : 2 + Total Devices : 2 + Persistence : Superblock is persistent + + Update Time : Fri Feb 15 12:38:02 2019 + State : clean + Active Devices : 2 + Working Devices : 2 + Failed Devices : 0 + Spare Devices : 0 + +Consistency Policy : resync + + UUID : 83143055:2781ddf5:2c8f44c7:9b45d92e + Events : 17 + + Number Major Minor RaidDevice State + 0 253 64 0 active sync /dev/vde1 + 1 253 80 1 active sync /dev/vdf1 +""") + MDADM_DETAIL_OUTPUT_BROKEN_RAID0 = ("""/dev/md126: Version : 1.2 Raid Level : raid0 diff --git a/ironic_python_agent/tests/unit/test_hardware.py b/ironic_python_agent/tests/unit/test_hardware.py index 0d6d28f7..9fcf775c 100644 --- a/ironic_python_agent/tests/unit/test_hardware.py +++ b/ironic_python_agent/tests/unit/test_hardware.py @@ -45,32 +45,38 @@ CONF.import_opt('disk_wait_delay', 'ironic_python_agent.config') BLK_DEVICE_TEMPLATE_SMALL_DEVICES = [ hardware.BlockDevice(name='/dev/sda', model='TinyUSB Drive', size=3116853504, rotational=False, - vendor="FooTastic", uuid="F531-BDC3"), + vendor="FooTastic", uuid="F531-BDC3", + serial="123"), hardware.BlockDevice(name='/dev/sdb', model='AlmostBigEnough Drive', size=4294967295, rotational=False, - vendor="FooTastic", uuid=""), + vendor="FooTastic", uuid="", + serial="456"), ] RAID_BLK_DEVICE_TEMPLATE_DEVICES = [ hardware.BlockDevice(name='/dev/sda', model='DRIVE 0', size=1765517033472, rotational=True, - vendor="FooTastic", uuid=""), + vendor="FooTastic", uuid="", + serial="sda123"), hardware.BlockDevice(name='/dev/sdb', model='DRIVE 1', size=1765517033472, rotational=True, - vendor="FooTastic", uuid=""), + vendor="FooTastic", uuid="", + serial="sdb123"), hardware.BlockDevice(name='/dev/md0', model='RAID', size=1765517033470, rotational=False, - vendor="FooTastic", uuid=""), + vendor="FooTastic", uuid="", + serial=None), hardware.BlockDevice(name='/dev/md1', model='RAID', size=0, rotational=False, - vendor="FooTastic", uuid=""), + vendor="FooTastic", uuid="", + serial=None), ] BLK_DEVICE_TEMPLATE_PARTUUID_DEVICE = [ hardware.BlockDevice(name='/dev/sda1', model='DRIVE 0', size=107373133824, rotational=True, vendor="FooTastic", uuid="987654-3210", - partuuid="1234-5678"), + partuuid="1234-5678", serial="sda1123"), ] @@ -869,7 +875,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): ] expected = [ mock.call('lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]), mock.call('multipath', '-c', '/dev/sda'), mock.call('multipath', '-ll', '/dev/sda'), @@ -949,7 +955,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): ] expected = [ mock.call('lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]), mock.call('multipath', '-c', '/dev/sda'), mock.call('multipath', '-ll', '/dev/sda'), @@ -1004,7 +1010,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): self.assertEqual('/dev/md0', self.hardware.get_os_install_device()) expected = [ mock.call('lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]), ] @@ -1030,14 +1036,14 @@ class TestGenericHardwareManager(base.IronicAgentTest): self.hardware.get_os_install_device) expected = [ mock.call('lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]), ] mocked_execute.assert_has_calls(expected) mocked_execute.assert_called_once_with( 'lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]) self.assertIn(str(4 * units.Gi), ex.details) mock_cached_node.assert_called_once_with() @@ -1542,6 +1548,54 @@ class TestGenericHardwareManager(base.IronicAgentTest): ignore_raid=True)], list_mock.call_args_list) + def test_get_skip_list_from_node_block_devices_with_skip_list(self): + block_devices = [ + hardware.BlockDevice('/dev/sdj', 'big', 1073741824, True), + hardware.BlockDevice('/dev/hdaa', 'small', 65535, False), + ] + expected_skip_list = {'/dev/sdj'} + node = self.node + + node['properties'] = { + 'skip_block_devices': [{ + 'name': '/dev/sdj' + }] + } + + skip_list = self.hardware.get_skip_list_from_node(node, + block_devices) + + self.assertEqual(expected_skip_list, skip_list) + + def test_get_skip_list_from_node_block_devices_just_raids(self): + expected_skip_list = {'large'} + node = self.node + + node['properties'] = { + 'skip_block_devices': [{ + 'name': '/dev/sdj' + }, { + 'volume_name': 'large' + }] + } + + skip_list = self.hardware.get_skip_list_from_node(node, + just_raids=True) + + self.assertEqual(expected_skip_list, skip_list) + + def test_get_skip_list_from_node_block_devices_no_skip_list(self): + block_devices = [ + hardware.BlockDevice('/dev/sdj', 'big', 1073741824, True), + hardware.BlockDevice('/dev/hdaa', 'small', 65535, False), + ] + node = self.node + + skip_list = self.hardware.get_skip_list_from_node(node, + block_devices) + + self.assertIsNone(skip_list) + @mock.patch.object(hardware.GenericHardwareManager, 'list_block_devices', autospec=True) def test_list_block_devices_check_skip_list_with_skip_list(self, @@ -1700,21 +1754,24 @@ class TestGenericHardwareManager(base.IronicAgentTest): rotational=False, vendor='Super Vendor', hctl='1:0:0:0', - by_path='/dev/disk/by-path/1:0:0:0'), + by_path='/dev/disk/by-path/1:0:0:0', + serial='sda123'), hardware.BlockDevice(name='/dev/sdb', model='Fastable SD131 7', size=10737418240, rotational=False, vendor='Super Vendor', hctl='1:0:0:0', - by_path='/dev/disk/by-path/1:0:0:1'), + by_path='/dev/disk/by-path/1:0:0:1', + serial='sdb123'), hardware.BlockDevice(name='/dev/sdc', model='NWD-BLP4-1600', size=1765517033472, rotational=False, vendor='Super Vendor', hctl='1:0:0:0', - by_path='/dev/disk/by-path/1:0:0:2'), + by_path='/dev/disk/by-path/1:0:0:2', + serial='sdc123'), hardware.BlockDevice(name='/dev/dm-0', model='NWD-BLP4-1600', size=1765517033472, @@ -1739,7 +1796,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): mock_readlink.assert_has_calls(expected_calls) expected_calls = [ mock.call('lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]), mock.call('multipath', '-c', '/dev/sda'), mock.call('multipath', '-c', '/dev/sdb'), @@ -1845,7 +1902,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): 'ID_WWN_VENDOR_EXTENSION': 'wwn-vendor-ext%d' % i} for i in range(3) ] + [ - {'DM_WWN': 'wwn3', 'DM_SERIAL': 'serial3'}, + {'DM_WWN': 'wwn3', 'DM_SERIAL': 'serial3'} ] mocked_dev_vendor.return_value = 'Super Vendor' devices = hardware.list_all_block_devices() @@ -1858,7 +1915,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): wwn='wwn0', wwn_with_extension='wwn-ext0', wwn_vendor_extension='wwn-vendor-ext0', - serial='serial0', + serial='sda123', hctl='1:0:0:0'), hardware.BlockDevice(name='/dev/sdb', model='Fastable SD131 7', @@ -1868,7 +1925,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): wwn='wwn1', wwn_with_extension='wwn-ext1', wwn_vendor_extension='wwn-vendor-ext1', - serial='serial1', + serial='sdb123', hctl='1:0:0:0'), hardware.BlockDevice(name='/dev/sdc', model='NWD-BLP4-1600', @@ -1878,7 +1935,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): wwn='wwn2', wwn_with_extension='wwn-ext2', wwn_vendor_extension='wwn-vendor-ext2', - serial='serial2', + serial='sdc123', hctl='1:0:0:0'), hardware.BlockDevice(name='/dev/dm-0', model='NWD-BLP4-1600', @@ -1905,6 +1962,72 @@ class TestGenericHardwareManager(base.IronicAgentTest): mocked_listdir.assert_has_calls(expected_calls) mocked_mpath.assert_called_once_with() + @mock.patch.object(hardware, 'get_multipath_status', autospec=True) + @mock.patch.object(os, 'readlink', autospec=True) + @mock.patch.object(os, 'listdir', autospec=True) + @mock.patch.object(hardware, '_get_device_info', autospec=True) + @mock.patch.object(pyudev.Devices, 'from_device_file', autospec=False) + @mock.patch.object(il_utils, 'execute', autospec=True) + def test_list_all_block_device_with_only_udev(self, + mocked_execute, + mocked_udev, + mocked_dev_vendor, + mocked_listdir, + mocked_readlink, + mocked_mpath): + mocked_readlink.return_value = '../../sda' + mocked_listdir.return_value = ['1:0:0:0'] + mocked_execute.side_effect = [ + (hws.BLK_INCOMPLETE_DEVICE_TEMPLATE_SMALL, ''), + processutils.ProcessExecutionError( + stderr=hws.MULTIPATH_INVALID_PATH % '/dev/sda'), + processutils.ProcessExecutionError( + stderr=hws.MULTIPATH_INVALID_PATH % '/dev/sdb'), + ] + + mocked_mpath.return_value = True + mocked_udev.side_effect = [ + {'ID_WWN': 'wwn%d' % i, 'ID_SERIAL_SHORT': 'serial%d' % i, + 'ID_SERIAL': 'do not use me', + 'ID_WWN_WITH_EXTENSION': 'wwn-ext%d' % i, + 'ID_WWN_VENDOR_EXTENSION': 'wwn-vendor-ext%d' % i} + for i in range(2) + ] + devices = hardware.list_all_block_devices() + expected_devices = [ + hardware.BlockDevice(name='/dev/sda', + model='TinyUSB Drive', + size=3116853504, + rotational=False, + wwn='wwn0', + wwn_with_extension='wwn-ext0', + wwn_vendor_extension='wwn-vendor-ext0', + serial='serial0', + hctl='1:0:0:0'), + hardware.BlockDevice(name='/dev/sdb', + model='AlmostBigEnough Drive', + size=4294967295, + rotational=False, + wwn='wwn1', + wwn_with_extension='wwn-ext1', + wwn_vendor_extension='wwn-vendor-ext1', + serial='serial1', + hctl='1:0:0:0') + ] + + self.assertEqual(2, len(devices)) + for expected, device in zip(expected_devices, devices): + # Compare all attrs of the objects + for attr in ['name', 'model', 'size', 'rotational', + 'wwn', 'serial', 'wwn_with_extension', + 'wwn_vendor_extension', 'hctl']: + self.assertEqual(getattr(expected, attr), + getattr(device, attr)) + expected_calls = [mock.call('/sys/block/%s/device/scsi_device' % dev) + for dev in ('sda', 'sdb')] + mocked_listdir.assert_has_calls(expected_calls) + mocked_mpath.assert_called_once_with() + @mock.patch.object(hardware, 'safety_check_block_device', autospec=True) @mock.patch.object(hardware, 'ThreadPool', autospec=True) @mock.patch.object(hardware, 'dispatch_to_managers', autospec=True) @@ -4369,6 +4492,294 @@ class TestGenericHardwareManager(base.IronicAgentTest): self.hardware.create_configuration, self.node, []) + @mock.patch.object(raid_utils, 'get_volume_name_of_raid_device', + autospec=True) + @mock.patch.object(raid_utils, '_get_actual_component_devices', + autospec=True) + @mock.patch.object(hardware, 'list_all_block_devices', autospec=True) + @mock.patch.object(disk_utils, 'list_partitions', autospec=True) + @mock.patch.object(il_utils, 'execute', autospec=True) + def test_create_configuration_with_skip_list( + self, mocked_execute, mock_list_parts, mocked_list_all_devices, + mocked_actual_comp, mocked_get_volume_name): + node = self.node + + raid_config = { + "logical_disks": [ + { + "size_gb": "10", + "raid_level": "1", + "controller": "software", + "volume_name": "small" + }, + { + "size_gb": "MAX", + "raid_level": "0", + "controller": "software", + "volume_name": "large" + }, + ] + } + node['target_raid_config'] = raid_config + node['properties'] = {'skip_block_devices': [{'volume_name': 'large'}]} + device1 = hardware.BlockDevice('/dev/sda', 'sda', 107374182400, True) + device2 = hardware.BlockDevice('/dev/sdb', 'sdb', 107374182400, True) + raid_device1 = hardware.BlockDevice('/dev/md0', 'RAID-1', + 107374182400, True) + self.hardware.list_block_devices = mock.Mock() + self.hardware.list_block_devices.return_value = [device1, device2] + hardware.list_all_block_devices.side_effect = [ + [raid_device1], # block_type raid + [] # block type md + ] + mocked_get_volume_name.return_value = "large" + + mocked_execute.side_effect = [ + None, # examine md0 + None, # mklabel sda + ('42', None), # sgdisk -F sda + None, # mklabel sda + ('42', None), # sgdisk -F sdb + None, None, None, # parted + partx + udevadm_settle sda + None, None, None, # parted + partx + udevadm_settle sdb + None, None, None, # parted + partx + udevadm_settle sda + None, None, None, # parted + partx + udevadm_settle sdb + None, None # mdadms + ] + + mocked_actual_comp.side_effect = [ + ('/dev/sda1', '/dev/sdb1'), + ('/dev/sda2', '/dev/sdb2'), + ] + + result = self.hardware.create_configuration(node, []) + mocked_execute.assert_has_calls([ + mock.call('mdadm', '--examine', '/dev/md0', + use_standard_locale=True), + mock.call('parted', '/dev/sda', '-s', '--', 'mklabel', 'msdos'), + mock.call('sgdisk', '-F', '/dev/sda'), + mock.call('parted', '/dev/sdb', '-s', '--', 'mklabel', 'msdos'), + mock.call('sgdisk', '-F', '/dev/sdb'), + mock.call('parted', '/dev/sda', '-s', '-a', 'optimal', '--', + 'mkpart', 'primary', '42s', '10GiB'), + mock.call('partx', '-av', '/dev/sda', attempts=3, + delay_on_retry=True), + mock.call('udevadm', 'settle'), + mock.call('parted', '/dev/sdb', '-s', '-a', 'optimal', '--', + 'mkpart', 'primary', '42s', '10GiB'), + mock.call('partx', '-av', '/dev/sdb', attempts=3, + delay_on_retry=True), + mock.call('udevadm', 'settle'), + mock.call('mdadm', '--create', '/dev/md0', '--force', '--run', + '--metadata=1', '--level', '1', '--name', 'small', + '--raid-devices', 2, '/dev/sda1', '/dev/sdb1')]) + self.assertEqual(raid_config, result) + + self.assertEqual(0, mock_list_parts.call_count) + + @mock.patch.object(raid_utils, 'get_volume_name_of_raid_device', + autospec=True) + @mock.patch.object(hardware, 'list_all_block_devices', autospec=True) + @mock.patch.object(il_utils, 'execute', autospec=True) + def test_create_configuration_skip_list_existing_device_does_not_match( + self, mocked_execute, mocked_list_all_devices, + mocked_get_volume_name): + node = self.node + + raid_config = { + "logical_disks": [ + { + "size_gb": "10", + "raid_level": "1", + "controller": "software", + "volume_name": "small" + }, + { + "size_gb": "MAX", + "raid_level": "0", + "controller": "software", + "volume_name": "large" + }, + ] + } + node['target_raid_config'] = raid_config + node['properties'] = {'skip_block_devices': [{'volume_name': 'large'}]} + device1 = hardware.BlockDevice('/dev/sda', 'sda', 107374182400, True) + device2 = hardware.BlockDevice('/dev/sdb', 'sdb', 107374182400, True) + raid_device1 = hardware.BlockDevice('/dev/md0', 'RAID-1', + 107374182400, True) + self.hardware.list_block_devices = mock.Mock() + self.hardware.list_block_devices.return_value = [device1, device2] + hardware.list_all_block_devices.side_effect = [ + [raid_device1], # block_type raid + [] # block type md + ] + mocked_get_volume_name.return_value = "small" + + error_regex = "Existing Software RAID device detected that should not" + mocked_execute.side_effect = [ + processutils.ProcessExecutionError] + self.assertRaisesRegex(errors.SoftwareRAIDError, error_regex, + self.hardware.create_configuration, + self.node, []) + + mocked_execute.assert_called_once_with( + 'mdadm', '--examine', '/dev/md0', use_standard_locale=True) + + @mock.patch.object(raid_utils, '_get_actual_component_devices', + autospec=True) + @mock.patch.object(hardware, 'list_all_block_devices', autospec=True) + @mock.patch.object(disk_utils, 'list_partitions', autospec=True) + @mock.patch.object(il_utils, 'execute', autospec=True) + def test_create_configuration_with_skip_list_no_existing_device( + self, mocked_execute, mock_list_parts, + mocked_list_all_devices, mocked_actual_comp): + node = self.node + + raid_config = { + "logical_disks": [ + { + "size_gb": "10", + "raid_level": "1", + "controller": "software", + "volume_name": "small" + }, + { + "size_gb": "MAX", + "raid_level": "0", + "controller": "software", + "volume_name": "large" + }, + ] + } + node['target_raid_config'] = raid_config + node['properties'] = {'skip_block_devices': [{'volume_name': 'large'}]} + device1 = hardware.BlockDevice('/dev/sda', 'sda', 107374182400, True) + device2 = hardware.BlockDevice('/dev/sdb', 'sdb', 107374182400, True) + self.hardware.list_block_devices = mock.Mock() + self.hardware.list_block_devices.return_value = [device1, device2] + mock_list_parts.side_effect = [ + [], + processutils.ProcessExecutionError + ] + hardware.list_all_block_devices.side_effect = [ + [], # block_type raid + [] # block type md + ] + + mocked_execute.side_effect = [ + None, # mklabel sda + ('42', None), # sgdisk -F sda + None, # mklabel sda + ('42', None), # sgdisk -F sdb + None, None, None, # parted + partx + udevadm_settle sda + None, None, None, # parted + partx + udevadm_settle sdb + None, None, None, # parted + partx + udevadm_settle sda + None, None, None, # parted + partx + udevadm_settle sdb + None, None # mdadms + ] + + mocked_actual_comp.side_effect = [ + ('/dev/sda1', '/dev/sdb1'), + ('/dev/sda2', '/dev/sdb2'), + ] + + result = self.hardware.create_configuration(node, []) + mocked_execute.assert_has_calls([ + mock.call('parted', '/dev/sda', '-s', '--', 'mklabel', 'msdos'), + mock.call('sgdisk', '-F', '/dev/sda'), + mock.call('parted', '/dev/sdb', '-s', '--', 'mklabel', 'msdos'), + mock.call('sgdisk', '-F', '/dev/sdb'), + mock.call('parted', '/dev/sda', '-s', '-a', 'optimal', '--', + 'mkpart', 'primary', '42s', '10GiB'), + mock.call('partx', '-av', '/dev/sda', attempts=3, + delay_on_retry=True), + mock.call('udevadm', 'settle'), + mock.call('parted', '/dev/sdb', '-s', '-a', 'optimal', '--', + 'mkpart', 'primary', '42s', '10GiB'), + mock.call('partx', '-av', '/dev/sdb', attempts=3, + delay_on_retry=True), + mock.call('udevadm', 'settle'), + mock.call('parted', '/dev/sda', '-s', '-a', 'optimal', '--', + 'mkpart', 'primary', '10GiB', '-1'), + mock.call('partx', '-av', '/dev/sda', attempts=3, + delay_on_retry=True), + mock.call('udevadm', 'settle'), + mock.call('parted', '/dev/sdb', '-s', '-a', 'optimal', '--', + 'mkpart', 'primary', '10GiB', '-1'), + mock.call('partx', '-av', '/dev/sdb', attempts=3, + delay_on_retry=True), + mock.call('udevadm', 'settle'), + mock.call('mdadm', '--create', '/dev/md0', '--force', '--run', + '--metadata=1', '--level', '1', '--name', 'small', + '--raid-devices', 2, '/dev/sda1', '/dev/sdb1'), + mock.call('mdadm', '--create', '/dev/md1', '--force', '--run', + '--metadata=1', '--level', '0', '--name', 'large', + '--raid-devices', 2, '/dev/sda2', '/dev/sdb2')]) + + self.assertEqual(raid_config, result) + + self.assertEqual(2, mock_list_parts.call_count) + mock_list_parts.assert_has_calls([ + mock.call(x) for x in ['/dev/sda', '/dev/sdb'] + ]) + + @mock.patch.object(raid_utils, 'get_volume_name_of_raid_device', + autospec=True) + @mock.patch.object(raid_utils, '_get_actual_component_devices', + autospec=True) + @mock.patch.object(hardware, 'list_all_block_devices', autospec=True) + @mock.patch.object(il_utils, 'execute', autospec=True) + def test_create_configuration_with_complete_skip_list( + self, mocked_execute, mocked_ls_all_devs, + mocked_actual_comp, mocked_get_volume_name): + node = self.node + + raid_config = { + "logical_disks": [ + { + "size_gb": "10", + "raid_level": "1", + "controller": "software", + "volume_name": "small" + }, + { + "size_gb": "MAX", + "raid_level": "0", + "controller": "software", + "volume_name": "large" + }, + ] + } + node['target_raid_config'] = raid_config + node['properties'] = {'skip_block_devices': [{'volume_name': 'small'}, + {'volume_name': 'large'}]} + raid_device0 = hardware.BlockDevice('/dev/md0', 'RAID-1', + 2147483648, True) + raid_device1 = hardware.BlockDevice('/dev/md1', 'RAID-0', + 107374182400, True) + device1 = hardware.BlockDevice('/dev/sda', 'sda', 107374182400, True) + device2 = hardware.BlockDevice('/dev/sdb', 'sdb', 107374182400, True) + hardware.list_all_block_devices.side_effect = [ + [raid_device0, raid_device1], # block_type raid + [] # block type md + ] + self.hardware.list_block_devices = mock.Mock() + self.hardware.list_block_devices.return_value = [device1, device2] + mocked_get_volume_name.side_effect = [ + "small", + "large", + ] + + self.hardware.create_configuration(node, []) + mocked_execute.assert_has_calls([ + mock.call('mdadm', '--examine', '/dev/md0', + use_standard_locale=True), + mock.call('mdadm', '--examine', '/dev/md1', + use_standard_locale=True), + ]) + self.assertEqual(2, mocked_execute.call_count) + @mock.patch.object(il_utils, 'execute', autospec=True) def test__get_md_uuid(self, mocked_execute): mocked_execute.side_effect = [(hws.MDADM_DETAIL_OUTPUT, '')] @@ -4462,12 +4873,15 @@ class TestGenericHardwareManager(base.IronicAgentTest): holder_disks = hardware.get_holder_disks('/dev/md0') self.assertEqual(['/dev/vda', '/dev/vdb'], holder_disks) + @mock.patch.object(raid_utils, 'get_volume_name_of_raid_device', + autospec=True) @mock.patch.object(hardware, 'get_holder_disks', autospec=True) @mock.patch.object(hardware, 'get_component_devices', autospec=True) @mock.patch.object(hardware, 'list_all_block_devices', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) def test_delete_configuration(self, mocked_execute, mocked_list, - mocked_get_component, mocked_get_holder): + mocked_get_component, mocked_get_holder, + mocked_get_volume_name): raid_device1 = hardware.BlockDevice('/dev/md0', 'RAID-1', 107374182400, True) raid_device2 = hardware.BlockDevice('/dev/md1', 'RAID-0', @@ -4490,6 +4904,9 @@ class TestGenericHardwareManager(base.IronicAgentTest): mocked_get_holder.side_effect = [ ["/dev/sda", "/dev/sdb"], ["/dev/sda", "/dev/sdb"]] + mocked_get_volume_name.side_effect = [ + "/dev/md0", "/dev/md1" + ] mocked_execute.side_effect = [ None, # mdadm --assemble --scan None, # wipefs md0 @@ -4551,11 +4968,14 @@ class TestGenericHardwareManager(base.IronicAgentTest): mock.call('mdadm', '--assemble', '--scan', check_exit_code=False), ]) + @mock.patch.object(raid_utils, 'get_volume_name_of_raid_device', + autospec=True) @mock.patch.object(hardware, 'get_component_devices', autospec=True) @mock.patch.object(hardware, 'list_all_block_devices', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) def test_delete_configuration_partition(self, mocked_execute, mocked_list, - mocked_get_component): + mocked_get_component, + mocked_get_volume_name): # This test checks that if no components are returned for a given # raid device, then it must be a nested partition and so it gets # skipped @@ -4569,6 +4989,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): [], # list_all_block_devices raid [], # list_all_block_devices raid (md) ] + mocked_get_volume_name.return_value = None mocked_get_component.return_value = [] self.assertIsNone(self.hardware.delete_configuration(self.node, [])) mocked_execute.assert_has_calls([ @@ -4576,11 +4997,14 @@ class TestGenericHardwareManager(base.IronicAgentTest): mock.call('mdadm', '--assemble', '--scan', check_exit_code=False), ]) + @mock.patch.object(raid_utils, 'get_volume_name_of_raid_device', + autospec=True) @mock.patch.object(hardware, 'get_component_devices', autospec=True) @mock.patch.object(hardware, 'list_all_block_devices', autospec=True) @mock.patch.object(il_utils, 'execute', autospec=True) def test_delete_configuration_failure_blocks_remaining( - self, mocked_execute, mocked_list, mocked_get_component): + self, mocked_execute, mocked_list, mocked_get_component, + mocked_get_volume_name): # This test checks that, if after two raid clean passes there still # remain softraid hints on drives, then the delete_configuration call @@ -4601,6 +5025,7 @@ class TestGenericHardwareManager(base.IronicAgentTest): [], # list_all_block_devices raid (type md) ] mocked_get_component.return_value = [] + mocked_get_volume_name.return_value = "/dev/md0" self.assertRaisesRegex( errors.SoftwareRAIDError, @@ -4614,6 +5039,79 @@ class TestGenericHardwareManager(base.IronicAgentTest): mock.call('mdadm', '--assemble', '--scan', check_exit_code=False), ]) + @mock.patch.object(raid_utils, 'get_volume_name_of_raid_device', + autospec=True) + @mock.patch.object(hardware.GenericHardwareManager, + 'get_skip_list_from_node', autospec=True) + @mock.patch.object(hardware, 'get_holder_disks', autospec=True) + @mock.patch.object(hardware, 'get_component_devices', autospec=True) + @mock.patch.object(hardware, 'list_all_block_devices', autospec=True) + @mock.patch.object(il_utils, 'execute', autospec=True) + def test_delete_configuration_skip_list(self, mocked_execute, mocked_list, + mocked_get_component, + mocked_get_holder, + mocked_get_skip_list, + mocked_get_volume_name): + raid_device1 = hardware.BlockDevice('/dev/md0', 'RAID-1', + 107374182400, True) + raid_device2 = hardware.BlockDevice('/dev/md1', 'RAID-0', + 2147483648, True) + sda = hardware.BlockDevice('/dev/sda', 'model12', 21, True) + sdb = hardware.BlockDevice('/dev/sdb', 'model12', 21, True) + sdc = hardware.BlockDevice('/dev/sdc', 'model12', 21, True) + + partitions = [ + hardware.BlockDevice('/dev/sdb1', 'raid-member', 32767, False), + hardware.BlockDevice('/dev/sdb2', 'raid-member', 32767, False), + hardware.BlockDevice('/dev/sda1', 'raid_member', 32767, False), + hardware.BlockDevice('/dev/sda2', 'raid-member', 32767, False), + ] + + hardware.list_all_block_devices.side_effect = [ + [raid_device1, raid_device2], # list_all_block_devices raid + [], # list_all_block_devices raid (md) + [sda, sdb, sdc], # list_all_block_devices disks + partitions, # list_all_block_devices parts + [], # list_all_block_devices raid + [], # list_all_block_devices raid (md) + ] + mocked_get_component.side_effect = [ + ["/dev/sda1", "/dev/sdb1"], + ["/dev/sda2", "/dev/sdb2"]] + mocked_get_holder.side_effect = [ + ["/dev/sda", "/dev/sdb"], + ["/dev/sda", "/dev/sdb"]] + mocked_get_volume_name.side_effect = [ + "/dev/md0", "small" + ] + mocked_get_skip_list.return_value = ["small"] + + self.hardware.delete_configuration(self.node, []) + + mocked_execute.assert_has_calls([ + mock.call('mdadm', '--assemble', '--scan', check_exit_code=False), + mock.call('wipefs', '-af', '/dev/md0'), + mock.call('mdadm', '--stop', '/dev/md0'), + mock.call('mdadm', '--examine', '/dev/sda1', + use_standard_locale=True), + mock.call('mdadm', '--zero-superblock', '/dev/sda1'), + mock.call('mdadm', '--examine', '/dev/sdb1', + use_standard_locale=True), + mock.call('mdadm', '--zero-superblock', '/dev/sdb1'), + mock.call('mdadm', '--examine', '/dev/sda1', + use_standard_locale=True), + mock.call('mdadm', '--zero-superblock', '/dev/sda1'), + mock.call('mdadm', '--examine', '/dev/sdb1', + use_standard_locale=True), + mock.call('mdadm', '--zero-superblock', '/dev/sdb1'), + mock.call('mdadm', '--examine', '/dev/sdc', + use_standard_locale=True), + mock.call('mdadm', '--zero-superblock', '/dev/sdc'), + mock.call('parted', '/dev/sda', 'rm', '1'), + mock.call('parted', '/dev/sdb', 'rm', '1'), + mock.call('mdadm', '--assemble', '--scan', check_exit_code=False), + ]) + @mock.patch.object(il_utils, 'execute', autospec=True) def test_validate_configuration_valid_raid1(self, mocked_execute): raid_config = { @@ -5034,7 +5532,7 @@ class TestModuleFunctions(base.IronicAgentTest): result = hardware.list_all_block_devices() expected_calls = [ mock.call('lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]), mock.call('multipath', '-c', '/dev/sda'), mock.call('multipath', '-c', '/dev/sdb') @@ -5081,7 +5579,7 @@ class TestModuleFunctions(base.IronicAgentTest): ] expected_calls = [ mock.call('lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]), mock.call('multipath', '-c', '/dev/sda'), mock.call('multipath', '-c', '/dev/sda1'), @@ -5120,7 +5618,7 @@ class TestModuleFunctions(base.IronicAgentTest): result = hardware.list_all_block_devices(block_type='part') expected_calls = [ mock.call('lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]), mock.call('multipath', '-c', '/dev/sda'), mock.call('multipath', '-c', '/dev/sda1'), @@ -5143,7 +5641,7 @@ class TestModuleFunctions(base.IronicAgentTest): result = hardware.list_all_block_devices() mocked_execute.assert_called_once_with( 'lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]) self.assertEqual([], result) mocked_udev.assert_called_once_with() @@ -5157,7 +5655,7 @@ class TestModuleFunctions(base.IronicAgentTest): mocked_mpath.return_value = False expected_calls = [ mock.call('lsblk', '-bia', '--json', - '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID', + '-oKNAME,MODEL,SIZE,ROTA,TYPE,UUID,PARTUUID,SERIAL', check_exit_code=[0]), ] mocked_execute.return_value = ( @@ -5165,7 +5663,7 @@ class TestModuleFunctions(base.IronicAgentTest): self.assertRaisesRegex( errors.BlockDeviceError, r'^Block device caused unknown error: kname, partuuid, rota, ' - r'size, uuid must be returned by lsblk.$', + r'serial, size, uuid must be returned by lsblk.$', hardware.list_all_block_devices) mocked_udev.assert_called_once_with() mocked_execute.assert_has_calls(expected_calls) diff --git a/ironic_python_agent/tests/unit/test_raid_utils.py b/ironic_python_agent/tests/unit/test_raid_utils.py index 6624304b..a82027c8 100644 --- a/ironic_python_agent/tests/unit/test_raid_utils.py +++ b/ironic_python_agent/tests/unit/test_raid_utils.py @@ -139,6 +139,20 @@ class TestRaidUtils(base.IronicAgentTest): raid_utils.create_raid_device, 0, logical_disk) + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_volume_name_of_raid_device(self, mock_execute): + mock_execute.side_effect = [(hws.MDADM_DETAIL_OUTPUT_VOLUME_NAME, '')] + volume_name = raid_utils.get_volume_name_of_raid_device('/dev/md0') + self.assertEqual("this_name", volume_name) + + @mock.patch.object(utils, 'execute', autospec=True) + def test_get_volume_name_of_raid_device_invalid(self, mock_execute): + mock_execute.side_effect = [( + hws.MDADM_DETAIL_OUTPUT_VOLUME_NAME_INVALID, '' + )] + volume_name = raid_utils.get_volume_name_of_raid_device('/dev/md0') + self.assertIsNone(volume_name) + @mock.patch.object(disk_utils, 'trigger_device_rescan', autospec=True) @mock.patch.object(raid_utils, 'get_next_free_raid_device', autospec=True, return_value='/dev/md42') diff --git a/ironic_python_agent/utils.py b/ironic_python_agent/utils.py index 09d726cf..41daaf97 100644 --- a/ironic_python_agent/utils.py +++ b/ironic_python_agent/utils.py @@ -59,7 +59,8 @@ CONF = cfg.CONF AGENT_PARAMS_CACHED = dict() -LSBLK_COLUMNS = ['KNAME', 'MODEL', 'SIZE', 'ROTA', 'TYPE', 'UUID', 'PARTUUID'] +LSBLK_COLUMNS = ['KNAME', 'MODEL', 'SIZE', 'ROTA', + 'TYPE', 'UUID', 'PARTUUID', 'SERIAL'] COLLECT_LOGS_COMMANDS = { @@ -651,6 +652,22 @@ def extract_device(part): return (m.group(1) or m.group(2)) +def split_device_and_partition_number(part): + """Extract the partition number from a partition name or path. + + :param part: the partition + :return: device and partition number if success, None otherwise + """ + + device = extract_device(part) + if not device: + return None + partition_number = part.replace(device, '') + if 'nvme' in device and partition_number[0] == 'p': + partition_number = partition_number[1:] + return (device, partition_number) + + # See ironic.drivers.utils.get_node_capability def _parse_capabilities_str(cap_str): """Extract capabilities from string. diff --git a/releasenotes/notes/enable-skipping-raids-40263cc3a19cfd27.yaml b/releasenotes/notes/enable-skipping-raids-40263cc3a19cfd27.yaml new file mode 100644 index 00000000..999437cb --- /dev/null +++ b/releasenotes/notes/enable-skipping-raids-40263cc3a19cfd27.yaml @@ -0,0 +1,6 @@ +--- +features: + - The node property ``skip_block_devices`` supports + specifying volume names of software RAID devices. + These devices are not cleaned during cleaning and + are not created provided they already exist. diff --git a/releasenotes/notes/prioritize-lsblk-device-serials-8cae406ca5164a01.yaml b/releasenotes/notes/prioritize-lsblk-device-serials-8cae406ca5164a01.yaml new file mode 100644 index 00000000..3c0b26b1 --- /dev/null +++ b/releasenotes/notes/prioritize-lsblk-device-serials-8cae406ca5164a01.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + When detecting a serial number of a block device, the agent now tries + to use lsblk first and only falls back to udev if lsblk does not return + a serial number. Based on experience it looks like lsblk might be a better + source of truth than udev in regerard to serial number information. + diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index d3d198cb..21b2de88 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ Ironic Python Agent Release Notes :maxdepth: 1 unreleased + zed yoga xena wallaby diff --git a/releasenotes/source/yoga.rst b/releasenotes/source/yoga.rst index 7cd5e908..9b0242d5 100644 --- a/releasenotes/source/yoga.rst +++ b/releasenotes/source/yoga.rst @@ -1,6 +1,6 @@ -========================= -Yoga Series Release Notes -========================= +========================================= +Yoga Series (8.3.0 - 8.5.x) Release Notes +========================================= .. release-notes:: :branch: stable/yoga diff --git a/releasenotes/source/zed.rst b/releasenotes/source/zed.rst new file mode 100644 index 00000000..9608c05e --- /dev/null +++ b/releasenotes/source/zed.rst @@ -0,0 +1,6 @@ +======================== +Zed Series Release Notes +======================== + +.. release-notes:: + :branch: stable/zed diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 0c4e16fb..ceff4cfb 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -2,7 +2,8 @@ templates: - check-requirements - openstack-cover-jobs - - openstack-python3-zed-jobs + - openstack-python3-jobs + - openstack-python3-jobs-arm64 - publish-openstack-docs-pti - release-notes-jobs-python3 check: |