def validate_job_queue(node): """Validates the job queue on the node. It raises an exception if an unfinished configuration job exists. :param node: an ironic node object. :raises: DracOperationError on an error from python-dracclient. """ client = drac_common.get_drac_client(node) try: unfinished_jobs = client.list_jobs(only_unfinished=True) except drac_exceptions.BaseClientException as exc: LOG.error( _LE('DRAC driver failed to get the list of unfinished jobs ' 'for node %(node_uuid)s. Reason: %(error)s.'), { 'node_uuid': node.uuid, 'error': exc }) raise exception.DracOperationError(error=exc) if unfinished_jobs: msg = _('Unfinished config jobs found: %(jobs)r. Make sure they are ' 'completed before retrying.') % { 'jobs': unfinished_jobs } raise exception.DracOperationError(error=msg)
def _get_next_persistent_boot_mode(node): client = drac_common.get_drac_client(node) try: boot_modes = client.list_boot_modes() except drac_exceptions.BaseClientException as exc: LOG.error( 'DRAC driver failed to get next persistent boot mode for ' 'node %(node_uuid)s. Reason: %(error)s', { 'node_uuid': node.uuid, 'error': exc }) raise exception.DracOperationError(error=exc) next_persistent_boot_mode = None for mode in boot_modes: if mode.is_next and mode.id != _NON_PERSISTENT_BOOT_MODE: next_persistent_boot_mode = mode.id break if not next_persistent_boot_mode: message = _('List of boot modes, %(list_boot_modes)s, does not ' 'contain a persistent mode') % { 'list_boot_modes': boot_modes } LOG.error( 'DRAC driver failed to get next persistent boot mode for ' 'node %(node_uuid)s. Reason: %(message)s', { 'node_uuid': node.uuid, 'message': message }) raise exception.DracOperationError(error=message) return next_persistent_boot_mode
def _calculate_volume_props(logical_disk, physical_disks, free_space_mb): selected_disks = [ disk for disk in physical_disks if disk.id in logical_disk['physical_disks'] ] spans_count = _calculate_spans(logical_disk['raid_level'], len(selected_disks)) if len(selected_disks) % spans_count != 0: error_msg = _('invalid number of physical disks was provided') raise exception.DracOperationError(error=error_msg) disks_per_span = len(selected_disks) / spans_count # Best practice is to not pass span_length and span_depth when creating a # RAID10. The iDRAC will dynamically calculate these values using maximum # values obtained from the RAID controller. logical_disk['span_depth'] = None logical_disk['span_length'] = None if logical_disk['raid_level'] != '1+0': logical_disk['span_depth'] = spans_count logical_disk['span_length'] = disks_per_span max_volume_size_mb = _max_volume_size_mb(logical_disk['raid_level'], selected_disks, free_space_mb, spans_count=spans_count) if logical_disk['size_mb'] == 'MAX': if max_volume_size_mb == 0: error_msg = _("size set to 'MAX' but could not allocate physical " "disk space") raise exception.DracOperationError(error=error_msg) logical_disk['size_mb'] = max_volume_size_mb elif max_volume_size_mb < logical_disk['size_mb']: if max_volume_size_mb == 0: error_msg = _('not enough physical disk space for the logical ' 'disk') raise exception.DracOperationError(error=error_msg) disk_usage = _volume_usage_per_disk_mb(logical_disk, selected_disks, spans_count=spans_count) for disk in selected_disks: if free_space_mb[disk] < disk_usage: error_msg = _('not enough free space on physical disks for the ' 'logical disk') raise exception.DracOperationError(error=error_msg) else: free_space_mb[disk] -= disk_usage if 'controller' not in logical_disk: logical_disk['controller'] = selected_disks[0].controller
def _calculate_volume_props(logical_disk, physical_disks, free_space_mb): selected_disks = [ disk for disk in physical_disks if disk.id in logical_disk['physical_disks'] ] spans_count = _calculate_spans(logical_disk['raid_level'], len(selected_disks)) if len(selected_disks) % spans_count != 0: error_msg = _('invalid number of physical disks was provided') raise exception.DracOperationError(error=error_msg) disks_per_span = len(selected_disks) / spans_count logical_disk['span_depth'] = spans_count logical_disk['span_length'] = disks_per_span max_volume_size_mb = _max_volume_size_mb(logical_disk['raid_level'], selected_disks, free_space_mb, spans_count=spans_count) if logical_disk['size_mb'] == 'MAX': if max_volume_size_mb == 0: error_msg = _("size set to 'MAX' but could not allocate physical " "disk space") raise exception.DracOperationError(error=error_msg) logical_disk['size_mb'] = max_volume_size_mb elif max_volume_size_mb < logical_disk['size_mb']: if max_volume_size_mb == 0: error_msg = _('not enough physical disk space for the logical ' 'disk') raise exception.DracOperationError(error=error_msg) disk_usage = _volume_usage_per_disk_mb(logical_disk, selected_disks, spans_count=spans_count) for disk in selected_disks: if free_space_mb[disk] < disk_usage: error_msg = _('not enough free space on physical disks for the ' 'logical disk') raise exception.DracOperationError(error=error_msg) else: free_space_mb[disk] -= disk_usage if 'controller' not in logical_disk: logical_disk['controller'] = selected_disks[0].controller
def _get_boot_device(node, drac_boot_devices=None): client = drac_common.get_drac_client(node) try: boot_modes = client.list_boot_modes() next_boot_modes = [mode.id for mode in boot_modes if mode.is_next] if NON_PERSISTENT_BOOT_MODE in next_boot_modes: next_boot_mode = NON_PERSISTENT_BOOT_MODE else: next_boot_mode = next_boot_modes[0] if drac_boot_devices is None: drac_boot_devices = client.list_boot_devices() drac_boot_device = drac_boot_devices[next_boot_mode][0] boot_device = next(key for (key, value) in _BOOT_DEVICES_MAP.items() if value in drac_boot_device.id) return { 'boot_device': boot_device, 'persistent': next_boot_mode == PERSISTENT_BOOT_MODE } except (drac_exceptions.BaseClientException, IndexError) as exc: LOG.error( 'DRAC driver failed to get next boot mode for ' 'node %(node_uuid)s. Reason: %(error)s.', { 'node_uuid': node.uuid, 'error': exc }) raise exception.DracOperationError(error=exc)
def set_config(task, **kwargs): """Sets the pending_value parameter for each of the values passed in. :param task: a TaskManager instance containing the node to act on. :param kwargs: a dictionary of {'AttributeName': 'NewValue'} :raises: DracOperationError on an error from python-dracclient. :returns: A dictionary containing the commit_required key with a boolean value indicating whether commit_bios_config() needs to be called to make the changes. """ node = task.node drac_job.validate_job_queue(node) client = drac_common.get_drac_client(node) if 'http_method' in kwargs: del kwargs['http_method'] try: return client.set_bios_settings(kwargs) except drac_exceptions.BaseClientException as exc: LOG.error( _LE('DRAC driver failed to set the BIOS settings for node ' '%(node_uuid)s. Reason: %(error)s.'), { 'node_uuid': node.uuid, 'error': exc }) raise exception.DracOperationError(error=exc)
def commit_config(node, raid_controller, reboot=False, realtime=False): """Apply all pending changes on a RAID controller. :param node: an ironic node object. :param raid_controller: id of the RAID controller. :param reboot: indicates whether a reboot job should be automatically created with the config job. (optional, defaults to False) :param realtime: indicates RAID controller supports realtime. (optional, defaults to False) :returns: id of the created job :raises: DracOperationError on an error from python-dracclient. """ client = drac_common.get_drac_client(node) try: return client.commit_pending_raid_changes( raid_controller=raid_controller, reboot=reboot, realtime=realtime) except drac_exceptions.BaseClientException as exc: LOG.error( 'DRAC driver failed to commit pending RAID config for' ' controller %(raid_controller_fqdd)s on node ' '%(node_uuid)s. Reason: %(error)s.', { 'raid_controller_fqdd': raid_controller, 'node_uuid': node.uuid, 'error': exc }) raise exception.DracOperationError(error=exc)
def _set_power_state(node, power_state): """Turns the server power on/off or do a reboot. :param node: an ironic node object. :param power_state: a power state from :mod:`ironic.common.states`. :raises: InvalidParameterValue if required DRAC credentials are missing. :raises: DracOperationError on an error from python-dracclient """ # NOTE(ifarkas): DRAC interface doesn't allow changing the boot device # multiple times in a row without a reboot. This is # because a change need to be committed via a # configuration job, and further configuration jobs # cannot be created until the previous one is processed # at the next boot. As a workaround, it is saved to # driver_internal_info during set_boot_device and committing # it here. _commit_boot_list_change(node) client = drac_common.get_drac_client(node) target_power_state = REVERSE_POWER_STATES[power_state] try: client.set_power_state(target_power_state) except drac_exceptions.BaseClientException as exc: LOG.error( 'DRAC driver failed to set power state for node ' '%(node_uuid)s to %(power_state)s. ' 'Reason: %(error)s.', { 'node_uuid': node.uuid, 'power_state': power_state, 'error': exc }) raise exception.DracOperationError(error=exc)
def clear_foreign_config(node, raid_controller): """Free up the foreign drives. :param node: an ironic node object. :param raid_controller: id of the RAID controller. :returns: a dictionary containing - The is_commit_required needed key with a boolean value indicating whether a config job must be created for the values to be applied. - The is_reboot_required key with a RebootRequired enumerated value indicating whether the server must be rebooted to clear foreign configuration. :raises: DracOperationError on an error from python-dracclient. """ try: drac_job.validate_job_queue(node) client = drac_common.get_drac_client(node) return client.clear_foreign_config(raid_controller) except drac_exceptions.BaseClientException as exc: LOG.error('DRAC driver failed to free foreign driver ' 'on %(raid_controller_fqdd)s ' 'for node %(node_uuid)s. ' 'Reason: %(error)s.', {'raid_controller_fqdd': raid_controller, 'node_uuid': node.uuid, 'error': exc}) raise exception.DracOperationError(error=exc)
def _is_raid_controller(node, raid_controller_fqdd, raid_controllers=None): """Find out if object's fqdd is for a raid controller or not :param node: an ironic node object :param raid_controller_fqdd: The object's fqdd we are testing to see if it is a raid controller or not. :param raid_controllers: A list of RAIDControllers used to check for the presence of BOSS cards. If None, the iDRAC will be queried for the list of controllers. :returns: boolean, True if the device is a RAID controller, False if not. """ client = drac_common.get_drac_client(node) try: return client.is_raid_controller(raid_controller_fqdd, raid_controllers) except drac_exceptions.BaseClientException as exc: LOG.error('Unable to determine if controller %(raid_controller_fqdd)s ' 'on node %(node_uuid)s is a RAID controller. ' 'Reason: %(error)s. ', {'raid_controller_fqdd': raid_controller_fqdd, 'node_uuid': node.uuid, 'error': exc}) raise exception.DracOperationError(error=exc)
def delete_virtual_disk(node, virtual_disk): """Delete a single virtual disk on a RAID controller. The deleted virtual disk will be in pending state. The DRAC card will do the actual configuration once the changes are applied by calling the ``commit_config`` method. :param node: an ironic node object. :param virtual_disk: id of the virtual disk. :returns: a dictionary containing the commit_needed key with a boolean value indicating whether a config job must be created for the values to be applied. :raises: DracOperationError on an error from python-dracclient. """ drac_job.validate_job_queue(node) client = drac_common.get_drac_client(node) try: return client.delete_virtual_disk(virtual_disk) except drac_exceptions.BaseClientException as exc: LOG.error( _LE('DRAC driver failed to delete virtual disk ' '%(virtual_disk_fqdd)s for node %(node_uuid)s. ' 'Reason: %(error)s.'), { 'virtual_disk_fqdd': virtual_disk, 'node_uuid': node.uuid, 'error': exc }) raise exception.DracOperationError(error=exc)
def test_list_unfinished_jobs_fail(self, mock_get_drac_client): mock_client = mock.Mock() mock_get_drac_client.return_value = mock_client exc = exception.DracOperationError('boom') mock_client.list_jobs.side_effect = exc self.assertRaises(exception.DracOperationError, drac_job.list_unfinished_jobs, self.node)
def test_get_job_fail(self, mock_get_drac_client): mock_client = mock.Mock() mock_get_drac_client.return_value = mock_client exc = exception.DracOperationError('boom') mock_client.get_job.side_effect = exc self.assertRaises(exception.DracOperationError, drac_job.get_job, self.node, 'foo')
def _raid_level_overhead(raid_level, spans_count=1): try: raid_level_info = RAID_LEVELS[raid_level] except KeyError: reason = (_('RAID level %(raid_level)s is not supported by the ' 'driver. Supported RAID levels: %(supported_raid_levels)s') % {'raid_level': raid_level, 'supported_raid_levels': list(RAID_LEVELS)}) raise exception.DracOperationError(error=reason) if raid_level_info['type'] == 'spanned': if spans_count <= 1: reason = _('Spanned RAID volumes cannot contain a single span') raise exception.DracOperationError(error=reason) span_type = raid_level_info['span_type'] raid_level_info = RAID_LEVELS[span_type] return raid_level_info['overhead'] * spans_count
def test_list_unfinished_jobs_fail(self, mock_get_drac_client): mock_client = mock.Mock() mock_get_drac_client.return_value = mock_client exc = exception.DracOperationError('boom') mock_client.list_jobs.side_effect = exc with task_manager.acquire(self.context, self.node.uuid, shared=False) as task: self.assertRaises(exception.DracOperationError, task.driver.vendor.list_unfinished_jobs, task)
def set_boot_device(node, device, persistent=False): """Set the boot device for a node. Set the boot device to use on next boot of the node. :param node: an ironic node object. :param device: the boot device, one of :mod:`ironic.common.boot_devices`. :param persistent: Boolean value. True if the boot device will persist to all future boots, False if not. Default: False. :raises: DracOperationError on an error from python-dracclient. """ drac_job.validate_job_queue(node) client = drac_common.get_drac_client(node) try: drac_boot_devices = client.list_boot_devices() current_boot_device = _get_boot_device(node, drac_boot_devices) # If we are already booting from the right device, do nothing. if current_boot_device == { 'boot_device': device, 'persistent': persistent }: LOG.debug('DRAC already set to boot from %s', device) return drac_boot_device = next( drac_device.id for drac_device in drac_boot_devices[PERSISTENT_BOOT_MODE] if _BOOT_DEVICES_MAP[device] in drac_device.id) if persistent: boot_list = PERSISTENT_BOOT_MODE else: boot_list = NON_PERSISTENT_BOOT_MODE client.change_boot_device_order(boot_list, drac_boot_device) client.commit_pending_bios_changes() except drac_exceptions.BaseClientException as exc: LOG.error( 'DRAC driver failed to change boot device order for ' 'node %(node_uuid)s. Reason: %(error)s.', { 'node_uuid': node.uuid, 'error': exc }) raise exception.DracOperationError(error=exc)
def validate_job_queue(node): """Validates the job queue on the node. It raises an exception if an unfinished configuration job exists. :param node: an ironic node object. :raises: DracOperationError on an error from python-dracclient. """ unfinished_jobs = list_unfinished_jobs(node) if unfinished_jobs: msg = _('Unfinished config jobs found: %(jobs)r. Make sure they are ' 'completed before retrying.') % {'jobs': unfinished_jobs} raise exception.DracOperationError(error=msg)
def test_set_boot_device_with_invalid_job_queue( self, mock_validate_job_queue, mock_get_drac_client): mock_client = mock.Mock() mock_get_drac_client.return_value = mock_client mock_validate_job_queue.side_effect = exception.DracOperationError( 'boom') self.assertRaises(exception.DracOperationError, drac_mgmt.set_boot_device, self.node, ironic.common.boot_devices.PXE, persistent=True) self.assertEqual(0, mock_client.change_boot_device_order.call_count) self.assertEqual(0, mock_client.set_bios_settings.call_count) self.assertEqual(0, mock_client.commit_pending_bios_changes.call_count)
def list_raid_controllers(node): """List the RAID controllers of the node. :param node: an ironic node object. :returns: a list of RAIDController objects from dracclient. :raises: DracOperationError on an error from python-dracclient. """ client = drac_common.get_drac_client(node) try: return client.list_raid_controllers() except drac_exceptions.BaseClientException as exc: LOG.error('DRAC driver failed to get the list of RAID controllers ' 'for node %(node_uuid)s. Reason: %(error)s.', {'node_uuid': node.uuid, 'error': exc}) raise exception.DracOperationError(error=exc)
def _calculate_spans(raid_level, disks_count): """Calculates number of spans for a RAID level given a physical disk count :param raid_level: RAID level of the virtual disk. :param disk_count: number of physical disks used for the virtual disk. :returns: number of spans. """ if raid_level in ['0', '1', '5', '6']: return 1 elif raid_level in ['5+0', '6+0']: return 2 elif raid_level in ['1+0']: return disks_count >> 1 else: reason = (_('Cannot calculate spans for RAID level "%s"') % raid_level) raise exception.DracOperationError(error=reason)
def clear_job_queue(self, task): """Clear the job queue. :param task: a TaskManager instance containing the node to act on. :returns: None if it is completed. :raises: DracOperationError on an error from python-dracclient. """ try: node = task.node client = drac_common.get_drac_client(node) client.delete_jobs(job_ids=[_CLEAR_JOB_IDS]) except drac_exceptions.BaseClientException as exc: LOG.error('DRAC driver failed to clear the job queue for node ' '%(node_uuid)s. Reason: %(error)s.', {'node_uuid': node.uuid, 'error': exc}) raise exception.DracOperationError(error=exc)
def abandon_config(task): """Abandons uncommitted changes added by set_config :param task: a TaskManager instance containing the node to act on. :raises: DracOperationError on an error from python-dracclient. """ node = task.node client = drac_common.get_drac_client(node) try: client.abandon_pending_bios_changes() except drac_exceptions.BaseClientException as exc: LOG.error('DRAC driver failed to delete the pending BIOS ' 'settings for node %(node_uuid)s. Reason: %(error)s.', {'node_uuid': node.uuid, 'error': exc}) raise exception.DracOperationError(error=exc)
def cache_bios_settings(self, task): """Store or update the current BIOS settings for the node. Get the current BIOS settings and store them in the bios_settings database table. :param task: a TaskManager instance containing the node to act on. :raises: DracOperationError on an error from python-dracclient """ node = task.node node_id = node.id node_uuid = node.uuid client = drac_common.get_drac_client(node) try: kwsettings = client.list_bios_settings() except drac_exceptions.BaseClientException as exc: LOG.error( 'DRAC driver failed to get the BIOS settings for node ' '%(node_uuid)s. Reason: %(error)s.', { 'node_uuid': node.uuid, 'error': exc }) raise exception.DracOperationError(error=exc) # convert dracclient BIOS settings into ironic settings list settings = [{ "name": name, "value": attrib.current_value } for name, attrib in kwsettings.items()] # Store them in the database table LOG.debug('Caching BIOS settings for node %(node_uuid)s', {'node_uuid': node_uuid}) create_list, update_list, delete_list, nochange_list = ( objects.BIOSSettingList.sync_node_setting(task.context, node_id, settings)) if create_list: objects.BIOSSettingList.create(task.context, node_id, create_list) if update_list: objects.BIOSSettingList.save(task.context, node_id, update_list) if delete_list: delete_names = [d['name'] for d in delete_list] objects.BIOSSettingList.delete(task.context, node_id, delete_names)
def list_unfinished_jobs(node): """List unfinished config jobs of the node. :param node: an ironic node object. :returns: a list of Job objects from dracclient. :raises: DracOperationError on an error from python-dracclient. """ client = drac_common.get_drac_client(node) try: return client.list_jobs(only_unfinished=True) except drac_exceptions.BaseClientException as exc: LOG.error('DRAC driver failed to get the list of unfinished jobs ' 'for node %(node_uuid)s. Reason: %(error)s.', {'node_uuid': node.uuid, 'error': exc}) raise exception.DracOperationError(error=exc)
def get_job(node, job_id): """Get the details of a Lifecycle job of the node. :param node: an ironic node object. :param job_id: ID of the Lifecycle job. :returns: a Job object from dracclient. :raises: DracOperationError on an error from python-dracclient. """ client = drac_common.get_drac_client(node) try: return client.get_job(job_id) except drac_exceptions.BaseClientException as exc: LOG.error('DRAC driver failed to get the job %(job_id)s ' 'for node %(node_uuid)s. Reason: %(error)s.', {'node_uuid': node.uuid, 'error': exc}) raise exception.DracOperationError(error=exc)
def wait_for_job_completion(node, retries=CONF.drac.config_job_max_retries): """Wait for job to complete It will wait for the job to complete for 20 minutes and raises timeout if job never complete within given interval of time. :param node: an ironic node object. :param retries: no of retries to make conductor wait. :raises: DracOperationError on exception raised from python-dracclient or a timeout while waiting for job completion. """ if not list_unfinished_jobs(node): return err_msg = _('There are unfinished jobs in the job ' 'queue on node %(node_uuid)s.') % { 'node_uuid': node.uuid } LOG.warning(err_msg) raise exception.DracOperationError(error=err_msg)
def _set_power_state(node, target_state): """Turns the server power on/off or do a reboot. :param node: an ironic node object. :param target_state: target state of the node. :raises: DracClientError if the client received unexpected response. :raises: InvalidParameterValue if an invalid power state was specified. """ client = drac_common.get_wsman_client(node) options = pywsman.ClientOptions() options.add_selector('CreationClassName', 'DCIM_ComputerSystem') options.add_selector('Name', 'srv:system') options.add_property('RequestedState', REVERSE_POWER_STATES[target_state]) try: root = client.wsman_invoke(resource_uris.DCIM_ComputerSystem, options, 'RequestStateChange') except exception.DracClientError as exc: with excutils.save_and_reraise_exception(): LOG.error( _LE('DRAC driver failed to set power state for node ' '%(node_uuid)s to %(target_power_state)s. ' 'Reason: %(error)s.'), { 'node_uuid': node.uuid, 'target_power_state': target_state, 'error': exc }) return_value = drac_common.find_xml(root, 'ReturnValue', resource_uris.DCIM_ComputerSystem).text if return_value != drac_common.RET_SUCCESS: message = drac_common.find_xml(root, 'Message', resource_uris.DCIM_ComputerSystem).text LOG.error( _LE('DRAC driver failed to set power state for node ' '%(node_uuid)s to %(target_power_state)s. ' 'Reason: %(error)s.'), { 'node_uuid': node.uuid, 'target_power_state': target_state, 'error': message }) raise exception.DracOperationError(operation='set_power_state', error=message)
def abandon_config(node, raid_controller): """Deletes all pending changes on a RAID controller. :param node: an ironic node object. :param raid_controller: id of the RAID controller. :raises: DracOperationError on an error from python-dracclient. """ client = drac_common.get_drac_client(node) try: client.abandon_pending_raid_changes(raid_controller) except drac_exceptions.BaseClientException as exc: LOG.error('DRAC driver failed to delete pending RAID config ' 'for controller %(raid_controller_fqdd)s on node ' '%(node_uuid)s. Reason: %(error)s.', {'raid_controller_fqdd': raid_controller, 'node_uuid': node.uuid, 'error': exc}) raise exception.DracOperationError(error=exc)
def list_physical_disks(node): """List the physical disks of the node. :param node: an ironic node object. :returns: a list of PhysicalDisk objects from dracclient. :raises: DracOperationError on an error from python-dracclient. """ client = drac_common.get_drac_client(node) try: return client.list_physical_disks() except drac_exceptions.BaseClientException as exc: LOG.error( _LE('DRAC driver failed to get the list of physical disks ' 'for node %(node_uuid)s. Reason: %(error)s.'), { 'node_uuid': node.uuid, 'error': exc }) raise exception.DracOperationError(error=exc)
def create_virtual_disk(node, raid_controller, physical_disks, raid_level, size_mb, disk_name=None, span_length=None, span_depth=None): """Create a single virtual disk on a RAID controller. The created virtual disk will be in pending state. The DRAC card will do the actual configuration once the changes are applied by calling the ``commit_config`` method. :param node: an ironic node object. :param raid_controller: id of the RAID controller. :param physical_disks: ids of the physical disks. :param raid_level: RAID level of the virtual disk. :param size_mb: size of the virtual disk. :param disk_name: name of the virtual disk. (optional) :param span_depth: Number of spans in virtual disk. (optional) :param span_length: Number of disks per span. (optional) :returns: a dictionary containing the commit_needed key with a boolean value indicating whether a config job must be created for the values to be applied. :raises: DracOperationError on an error from python-dracclient. """ drac_job.validate_job_queue(node) client = drac_common.get_drac_client(node) try: return client.create_virtual_disk(raid_controller, physical_disks, raid_level, size_mb, disk_name, span_length, span_depth) except drac_exceptions.BaseClientException as exc: LOG.error( _LE('DRAC driver failed to create virtual disk for node ' '%(node_uuid)s. Reason: %(error)s.'), { 'node_uuid': node.uuid, 'error': exc }) raise exception.DracOperationError(error=exc)