def get_os_install_device(self): cached_node = get_cached_node() root_device_hints = None if cached_node is not None: root_device_hints = cached_node['properties'].get('root_device') block_devices = self.list_block_devices() if not root_device_hints: return utils.guess_root_disk(block_devices).name else: serialized_devs = [dev.serialize() for dev in block_devices] try: device = il_utils.match_root_device_hints(serialized_devs, root_device_hints) except ValueError as e: # NOTE(lucasagomes): Just playing on the safe side # here, this exception should never be raised because # Ironic should validate the root device hints before the # deployment starts. raise errors.DeviceNotFound( 'No devices could be found using the root device hints ' '%(hints)s because they failed to validate. Error: ' '%(error)s' % {'hints': root_device_hints, 'error': e}) if not device: raise errors.DeviceNotFound( "No suitable device was found for " "deployment using these hints %s" % root_device_hints) return device['name']
def get_os_install_device(self): cached_node = get_cached_node() root_device_hints = None if cached_node is not None: root_device_hints = cached_node['properties'].get('root_device') block_devices = self.list_block_devices() if not root_device_hints: return utils.guess_root_disk(block_devices).name else: def match(hint, current_value, device): hint_value = root_device_hints[hint] if hint_value != current_value: LOG.debug( "Root device hint %(hint)s=%(value)s does not " "match the device %(device)s value of " "%(current)s", { 'hint': hint, 'value': hint_value, 'device': device, 'current': current_value }) return False return True def check_device_attrs(device): for key in ('model', 'wwn', 'serial', 'vendor', 'wwn_with_extension', 'wwn_vendor_extension', 'name'): if key not in root_device_hints: continue value = getattr(device, key) if not value: return False value = utils.normalize(value) if not match(key, value, device.name): return False return True for dev in block_devices: # TODO(lucasagomes): Add support for operators <, >, =, etc... # to better deal with sizes. if 'size' in root_device_hints: # Since we don't support units yet we expect the size # in GiB for now size = dev.size / units.Gi if not match('size', size, dev.name): continue if check_device_attrs(dev): return dev.name else: raise errors.DeviceNotFound("No suitable device was found for " "deployment using these hints %s" % root_device_hints)
def test_evaluate_hw_waits_for_disks(self, mocked_root_dev, mocked_sleep, mocked_block_dev): mocked_root_dev.side_effect = [errors.DeviceNotFound('boom'), None] result = self.hardware.evaluate_hardware_support() self.assertEqual(hardware.HardwareSupport.GENERIC, result) mocked_root_dev.assert_called_with(mocked_block_dev.return_value) self.assertEqual(2, mocked_root_dev.call_count) mocked_sleep.assert_called_once_with(hardware._DISK_WAIT_DELAY)
def _get_partition(device, uuid): """Find the partition of a given device.""" LOG.debug("Find the partition %(uuid)s on device %(dev)s", { 'dev': device, 'uuid': uuid }) try: # Try to tell the kernel to re-read the partition table try: utils.execute('partx', '-u', device, attempts=3, delay_on_retry=True) utils.execute('udevadm', 'settle') except processutils.ProcessExecutionError: LOG.warning("Couldn't re-read the partition table " "on device %s", device) report = utils.execute('lsblk', '-PbioKNAME,UUID,TYPE', device)[0] for line in report.split('\n'): part = {} # Split into KEY=VAL pairs vals = shlex.split(line) for key, val in (v.split('=', 1) for v in vals): part[key] = val.strip() # Ignore non partition if part.get('TYPE') != 'part': continue if part.get('UUID') == uuid: LOG.debug("Partition %(uuid)s found on device " "%(dev)s", { 'uuid': uuid, 'dev': device }) return '/dev/' + part.get('KNAME') else: error_msg = ("No partition with UUID %(uuid)s found on " "device %(dev)s" % { 'uuid': uuid, 'dev': device }) LOG.error(error_msg) raise errors.DeviceNotFound(error_msg) except processutils.ProcessExecutionError as e: error_msg = ('Finding the partition with UUID %(uuid)s on ' 'device %(dev)s failed with %(err)s' % { 'uuid': uuid, 'dev': device, 'err': e }) LOG.error(error_msg) raise errors.CommandExecutionError(error_msg)
def test_evaluate_hw_disks_timeout(self, mocked_root_dev, mocked_sleep, mocked_block_dev): mocked_root_dev.side_effect = errors.DeviceNotFound('boom') result = self.hardware.evaluate_hardware_support() self.assertEqual(hardware.HardwareSupport.GENERIC, result) mocked_root_dev.assert_called_with(mocked_block_dev.return_value) self.assertEqual(hardware._DISK_WAIT_ATTEMPTS, mocked_root_dev.call_count) mocked_sleep.assert_called_with(hardware._DISK_WAIT_DELAY)
def guess_root_disk(block_devices, min_size_required=4 * units.Gi): """Find suitable disk provided that root device hints are not given. If no hints are passed find the first device larger than min_size_required, assume it is the OS disk """ # TODO(russellhaering): This isn't a valid assumption in # all cases, is there a more reasonable default behavior? block_devices.sort(key=lambda device: device.size) if not block_devices or block_devices[-1].size < min_size_required: raise errors.DeviceNotFound( "No suitable device was found " "for deployment - root device hints were not provided " "and all found block devices are smaller than %iB." % min_size_required) for device in block_devices: if device.size >= min_size_required: return device
def find_partition_with_path(path, device=None): """Find a partition with the given path. :param path: Expected path. :param device: Target device. If None, the root device is used. :returns: A context manager that will unmount and delete the temporary mount point on exit. """ if device is None: device = hardware.dispatch_to_managers('get_os_install_device') partitions = disk_utils.list_partitions(device) # Make os.path.join work as expected lookup_path = path.lstrip('/') for part in partitions: if 'lvm' in part['flags']: LOG.debug('Skipping LVM partition %s', part) continue # TODO(dtantsur): switch to ironic-lib instead: # https://review.opendev.org/c/openstack/ironic-lib/+/774502 part_template = '%s%s' if 'nvme' in device: part_template = '%sp%s' part_path = part_template % (device, part['number']) LOG.debug('Inspecting partition %s for path %s', part, path) try: with ironic_utils.mounted(part_path) as local_path: found_path = os.path.join(local_path, lookup_path) if not os.path.isdir(found_path): continue LOG.info('Path %s has been found on partition %s', path, part) yield found_path return except processutils.ProcessExecutionError as exc: LOG.warning('Failure when inspecting partition %s: %s', part, exc) raise errors.DeviceNotFound( "No partition found with path %s, scanned: %s" % (path, partitions))
def guess_root_disk(block_devices, min_size_required=4 * units.Gi): """Find suitable disk provided that root device hints are not given. If no hints are passed, order the devices by size (primary key) and name (secondary key), and return the first device larger than min_size_required as the root disk. """ # NOTE(arne_wiebalck): Order devices by size and name. Secondary # ordering by name is done to increase chances of successful # booting for BIOSes which try only one (the "first") disk. block_devices.sort(key=lambda device: (device.size, device.name)) if not block_devices or block_devices[-1].size < min_size_required: raise errors.DeviceNotFound( "No suitable device was found " "for deployment - root device hints were not provided " "and all found block devices are smaller than %iB." % min_size_required) for device in block_devices: if device.size >= min_size_required: return device
def parse_root_device_hints(): """Parse the root device hints. Parse the root device hints given by Ironic via kernel cmdline or vmedia. :returns: A dict with the hints or an empty dict if no hints are passed. :raises: DeviceNotFound if there are unsupported hints. """ root_device = get_agent_params().get('root_device') if not root_device: return {} hints = dict((item.split('=') for item in root_device.split(','))) # Find invalid hints for logging not_supported = set(hints) - SUPPORTED_ROOT_DEVICE_HINTS if not_supported: error_msg = ('No device can be found because the following hints: ' '"%(not_supported)s" are not supported by this version ' 'of IPA. Supported hints are: "%(supported)s"', { 'not_supported': ', '.join(not_supported), 'supported': ', '.join(SUPPORTED_ROOT_DEVICE_HINTS) }) raise errors.DeviceNotFound(error_msg) # Normalise the values hints = {k: normalize(v) for k, v in hints.items()} if 'size' in hints: # NOTE(lucasagomes): Ironic should validate before passing to # the deploy ramdisk hints['size'] = int(hints['size']) return hints
def _get_partition(device, uuid): """Find the partition of a given device.""" LOG.debug("Find the partition %(uuid)s on device %(dev)s", {'dev': device, 'uuid': uuid}) try: _rescan_device(device) # If the deploy device is an md device, we want to install on # the first partition. We clearly take a shortcut here for now. # TODO(arne_wiebalck): Would it possible to use the partition # UUID and use the "normal" discovery instead? if hardware.is_md_device(device): md_partition = device + 'p1' if (not os.path.exists(md_partition) or not stat.S_ISBLK(os.stat(md_partition).st_mode)): error_msg = ("Could not find partition %(part)s on md " "device %(dev)s" % {'part': md_partition, 'dev': device}) LOG.error(error_msg) raise errors.DeviceNotFound(error_msg) LOG.debug("Found md device with partition %s", md_partition) return md_partition lsblk = utils.execute('lsblk', '-PbioKNAME,UUID,PARTUUID,TYPE', device) report = lsblk[0] for line in report.split('\n'): part = {} # Split into KEY=VAL pairs vals = shlex.split(line) for key, val in (v.split('=', 1) for v in vals): part[key] = val.strip() # Ignore non partition if part.get('TYPE') != 'part': # NOTE(TheJulia): This techincally creates an edge failure # case where a filesystem on a whole block device sans # partitioning would behave differently. continue if part.get('UUID') == uuid: LOG.debug("Partition %(uuid)s found on device " "%(dev)s", {'uuid': uuid, 'dev': device}) return '/dev/' + part.get('KNAME') if part.get('PARTUUID') == uuid: LOG.debug("Partition %(uuid)s found on device " "%(dev)s", {'uuid': uuid, 'dev': device}) return '/dev/' + part.get('KNAME') else: # NOTE(TheJulia): We may want to consider moving towards using # findfs in the future, if we're comfortable with the execution # and interaction. There is value in either way though. try: findfs, stderr = utils.execute('findfs', 'UUID=%s' % uuid) return findfs.strip() except processutils.ProcessExecutionError as e: LOG.debug('First fallback detection attempt for locating ' 'partition via UUID %(uuid)s failed. ' 'Error: %(err)s', {'uuid': uuid, 'err': e}) try: findfs, stderr = utils.execute( 'findfs', 'PARTUUID=%s' % uuid) return findfs.strip() except processutils.ProcessExecutionError as e: LOG.debug('Secondary fallback detection attempt for ' 'locating partition via UUID %(uuid)s failed. ' 'Error: %(err)s', {'uuid': uuid, 'err': e}) error_msg = ("No partition with UUID %(uuid)s found on " "device %(dev)s" % {'uuid': uuid, 'dev': device}) LOG.error(error_msg) raise errors.DeviceNotFound(error_msg) except processutils.ProcessExecutionError as e: error_msg = ('Finding the partition with UUID %(uuid)s on ' 'device %(dev)s failed with %(err)s' % {'uuid': uuid, 'dev': device, 'err': e}) LOG.error(error_msg) raise errors.CommandExecutionError(error_msg)
def _get_partition(device, uuid): """Find the partition of a given device.""" LOG.debug("Find the partition %(uuid)s on device %(dev)s", { 'dev': device, 'uuid': uuid }) try: _rescan_device(device) lsblk = utils.execute('lsblk', '-PbioKNAME,UUID,PARTUUID,TYPE', device) report = lsblk[0] for line in report.split('\n'): part = {} # Split into KEY=VAL pairs vals = shlex.split(line) for key, val in (v.split('=', 1) for v in vals): part[key] = val.strip() # Ignore non partition if part.get('TYPE') not in ['md', 'part']: # NOTE(TheJulia): This technically creates an edge failure # case where a filesystem on a whole block device sans # partitioning would behave differently. continue if part.get('UUID') == uuid: LOG.debug("Partition %(uuid)s found on device " "%(dev)s", { 'uuid': uuid, 'dev': device }) return '/dev/' + part.get('KNAME') if part.get('PARTUUID') == uuid: LOG.debug("Partition %(uuid)s found on device " "%(dev)s", { 'uuid': uuid, 'dev': device }) return '/dev/' + part.get('KNAME') else: # NOTE(TheJulia): We may want to consider moving towards using # findfs in the future, if we're comfortable with the execution # and interaction. There is value in either way though. # NOTE(rg): alternative: blkid -l -t UUID=/PARTUUID= try: findfs, stderr = utils.execute('findfs', 'UUID=%s' % uuid) return findfs.strip() except processutils.ProcessExecutionError as e: LOG.debug( 'First fallback detection attempt for locating ' 'partition via UUID %(uuid)s failed. ' 'Error: %(err)s', { 'uuid': uuid, 'err': e }) try: findfs, stderr = utils.execute('findfs', 'PARTUUID=%s' % uuid) return findfs.strip() except processutils.ProcessExecutionError as e: LOG.debug( 'Secondary fallback detection attempt for ' 'locating partition via UUID %(uuid)s failed. ' 'Error: %(err)s', { 'uuid': uuid, 'err': e }) # Last fallback: In case we cannot find the partition by UUID # and the deploy device is an md device, we check if the md # device has a partition (which we assume to contain the root fs). if hardware.is_md_device(device): md_partition = device + 'p1' if (os.path.exists(md_partition) and stat.S_ISBLK(os.stat(md_partition).st_mode)): LOG.debug("Found md device with partition %s", md_partition) return md_partition else: LOG.debug( 'Could not find partition %(part)s on md ' 'device %(dev)s', { 'part': md_partition, 'dev': device }) # Partition not found, time to escalate. error_msg = ("No partition with UUID %(uuid)s found on " "device %(dev)s" % { 'uuid': uuid, 'dev': device }) LOG.error(error_msg) raise errors.DeviceNotFound(error_msg) except processutils.ProcessExecutionError as e: error_msg = ('Finding the partition with UUID %(uuid)s on ' 'device %(dev)s failed with %(err)s' % { 'uuid': uuid, 'dev': device, 'err': e }) LOG.error(error_msg) raise errors.CommandExecutionError(error_msg)
def get_os_install_device(self): cached_node = get_cached_node() root_device_hints = None if cached_node is not None: root_device_hints = cached_node['properties'].get('root_device') block_devices = self.list_block_devices() if not root_device_hints: return utils.guess_root_disk(block_devices).name else: def match(hint, current_value, device): hint_value = root_device_hints[hint] if hint == 'rotational': hint_value = strutils.bool_from_string(hint_value) elif hint == 'size': try: hint_value = int(hint_value) except (ValueError, TypeError): LOG.warning( 'Root device hint "size" is not an integer. ' 'Current value: "%(value)s"; and type: "%(type)s"', { 'value': hint_value, 'type': type(hint_value) }) return False if hint_value != current_value: LOG.debug( "Root device hint %(hint)s=%(value)s does not " "match the device %(device)s value of " "%(current)s", { 'hint': hint, 'value': hint_value, 'device': device, 'current': current_value }) return False return True def check_device_attrs(device): for key in ('model', 'wwn', 'serial', 'vendor', 'wwn_with_extension', 'wwn_vendor_extension', 'name', 'rotational', 'size'): if key not in root_device_hints: continue value = getattr(device, key) if value is None: return False if isinstance(value, six.string_types): value = utils.normalize(value) if key == 'size': # Since we don't support units yet we expect the size # in GiB for now value = value / units.Gi if not match(key, value, device.name): return False return True for dev in block_devices: if check_device_attrs(dev): return dev.name else: raise errors.DeviceNotFound("No suitable device was found for " "deployment using these hints %s" % root_device_hints)
def _manage_uefi(device, efi_system_part_uuid=None): """Manage the device looking for valid efi bootloaders to update the nvram. This method checks for valid efi bootloaders in the device, if they exists it updates the nvram using the efibootmgr. :param device: the device to be checked. :param efi_system_part_uuid: efi partition uuid. :raises: DeviceNotFound if the efi partition cannot be found. :return: True - if it founds any efi bootloader and the nvram was updated using the efibootmgr. False - if no efi bootloader is found. """ efi_partition_mount_point = None efi_mounted = False try: # Force UEFI to rescan the device. Required if the deployment # was over iscsi. _rescan_device(device) local_path = tempfile.mkdtemp() # Trust the contents on the disk in the event of a whole disk image. efi_partition = utils.get_efi_part_on_device(device) if not efi_partition and efi_system_part_uuid: # _get_partition returns <device>+<partition> and we only need the # partition number partition = _get_partition(device, uuid=efi_system_part_uuid) efi_partition = int(partition.replace(device, "")) if not efi_partition: # NOTE(dtantsur): we cannot have a valid EFI deployment without an # EFI partition at all. This code path is easily hit when using an # image that is not UEFI compatible (which sadly applies to most # cloud images out there, with a nice exception of Ubuntu). raise errors.DeviceNotFound( "No EFI partition could be detected on device %s and " "EFI partition UUID has not been recorded during deployment " "(which is often the case for whole disk images). " "Are you using a UEFI-compatible image?" % device) efi_partition_mount_point = os.path.join(local_path, "boot/efi") if not os.path.exists(efi_partition_mount_point): os.makedirs(efi_partition_mount_point) # The mount needs the device with the partition, in case the # device ends with a digit we add a `p` and the partition number we # found, otherwise we just join the device and the partition number if device[-1].isdigit(): efi_device_part = '{}p{}'.format(device, efi_partition) utils.execute('mount', efi_device_part, efi_partition_mount_point) else: efi_device_part = '{}{}'.format(device, efi_partition) utils.execute('mount', efi_device_part, efi_partition_mount_point) efi_mounted = True valid_efi_bootloaders = _get_efi_bootloaders(efi_partition_mount_point) if valid_efi_bootloaders: _run_efibootmgr(valid_efi_bootloaders, device, efi_partition) return True else: # NOTE(dtantsur): if we have an empty EFI partition, try to use # grub-install to populate it. return False except processutils.ProcessExecutionError as e: error_msg = ('Could not verify uefi on device %(dev)s' 'failed with %(err)s.' % { 'dev': device, 'err': e }) LOG.error(error_msg) raise errors.CommandExecutionError(error_msg) finally: LOG.debug('Executing _manage_uefi clean-up.') umount_warn_msg = "Unable to umount %(local_path)s. Error: %(error)s" try: if efi_mounted: utils.execute('umount', efi_partition_mount_point, attempts=3, delay_on_retry=True) except processutils.ProcessExecutionError as e: error_msg = ('Umounting efi system partition failed. ' 'Attempted 3 times. Error: %s' % e) LOG.error(error_msg) raise errors.CommandExecutionError(error_msg) else: # If umounting the binds succeed then we can try to delete it try: utils.execute('sync') except processutils.ProcessExecutionError as e: LOG.warning(umount_warn_msg, {'path': local_path, 'error': e}) else: # After everything is umounted we can then remove the # temporary directory shutil.rmtree(local_path)
def get_os_install_device(self): block_devices = self.list_block_devices() root_device_hints = utils.parse_root_device_hints() if not root_device_hints: # If no hints are passed find the first device larger than # 4GB, assume it is the OS disk # TODO(russellhaering): This isn't a valid assumption in # all cases, is there a more reasonable default behavior? block_devices.sort(key=lambda device: device.size) for device in block_devices: if device.size >= (4 * pow(1024, 3)): return device.name else: def match(hint, current_value, device): hint_value = root_device_hints[hint] if hint_value != current_value: LOG.debug( "Root device hint %(hint)s=%(value)s does not " "match the device %(device)s value of " "%(current)s", { 'hint': hint, 'value': hint_value, 'device': device, 'current': current_value }) return False return True context = pyudev.Context() for dev in block_devices: try: udev = pyudev.Device.from_device_file(context, dev.name) except (ValueError, EnvironmentError) as e: LOG.warning( "Device %(dev)s is inaccessible, skipping... " "Error: %(error)s", { 'dev': dev, 'error': e }) continue # TODO(lucasagomes): Add support for operators <, >, =, etc... # to better deal with sizes. if 'size' in root_device_hints: # Since we don't support units yet we expect the size # in GiB for now size = dev.size / units.Gi if not match('size', size, dev.name): continue if 'model' in root_device_hints: model = udev.get('ID_MODEL', None) if not model: continue model = utils.normalize(model) if not match('model', model, dev.name): continue if 'wwn' in root_device_hints: wwn = udev.get('ID_WWN', None) if not wwn: continue wwn = utils.normalize(wwn) if not match('wwn', wwn, dev.name): continue if 'serial' in root_device_hints: # TODO(lucasagomes): Since lsblk only supports # returning the short serial we are using # ID_SERIAL_SHORT here to keep compatibility with the # bash deploy ramdisk serial = udev.get('ID_SERIAL_SHORT', None) if not serial: continue serial = utils.normalize(serial) if not match('serial', serial, dev.name): continue if 'vendor' in root_device_hints: vendor = self._get_device_vendor(dev.name) if not vendor: continue vendor = utils.normalize(vendor) if not match('vendor', vendor, dev.name): continue return dev.name else: raise errors.DeviceNotFound("No suitable device was found for " "deployment using these hints %s" % root_device_hints)
def _get_partition(device, uuid): """Find the partition of a given device.""" LOG.debug("Find the partition %(uuid)s on device %(dev)s", {'dev': device, 'uuid': uuid}) try: # Try to tell the kernel to re-read the partition table try: utils.execute('partx', '-u', device, attempts=3, delay_on_retry=True) utils.execute('udevadm', 'settle') except processutils.ProcessExecutionError: LOG.warning("Couldn't re-read the partition table " "on device %s", device) # If the deploy device is an md device, we want to install on # the first partition. We clearly take a shortcut here for now. # TODO(arne_wiebalck): Would it possible to use the partition # UUID and use the "normal" discovery instead? if hardware.is_md_device(device): md_partition = device + 'p1' if (not os.path.exists(md_partition) or not stat.S_ISBLK(os.stat(md_partition).st_mode)): error_msg = ("Could not find partition %(part)s on md " "device %(dev)s" % {'part': md_partition, 'dev': device}) LOG.error(error_msg) raise errors.DeviceNotFound(error_msg) LOG.debug("Found md device with partition %s", md_partition) return md_partition lsblk = utils.execute('lsblk', '-PbioKNAME,UUID,PARTUUID,TYPE', device) report = lsblk[0] for line in report.split('\n'): part = {} # Split into KEY=VAL pairs vals = shlex.split(line) for key, val in (v.split('=', 1) for v in vals): part[key] = val.strip() # Ignore non partition if part.get('TYPE') != 'part': continue if part.get('UUID') == uuid: LOG.debug("Partition %(uuid)s found on device " "%(dev)s", {'uuid': uuid, 'dev': device}) return '/dev/' + part.get('KNAME') if part.get('PARTUUID') == uuid: LOG.debug("Partition %(uuid)s found on device " "%(dev)s", {'uuid': uuid, 'dev': device}) return '/dev/' + part.get('KNAME') else: error_msg = ("No partition with UUID %(uuid)s found on " "device %(dev)s" % {'uuid': uuid, 'dev': device}) LOG.error(error_msg) raise errors.DeviceNotFound(error_msg) except processutils.ProcessExecutionError as e: error_msg = ('Finding the partition with UUID %(uuid)s on ' 'device %(dev)s failed with %(err)s' % {'uuid': uuid, 'dev': device, 'err': e}) LOG.error(error_msg) raise errors.CommandExecutionError(error_msg)