def test_get_node_capability(self): properties = {"capabilities": "cap1:value1, cap2: value2"} self.node.properties = properties expected = "value1" expected2 = "value2" result = driver_utils.get_node_capability(self.node, "cap1") result2 = driver_utils.get_node_capability(self.node, "cap2") self.assertEqual(expected, result) self.assertEqual(expected2, result2)
def test_get_node_capability(self): properties = {'capabilities': 'cap1:value1, cap2: value2'} self.node.properties = properties expected = 'value1' expected2 = 'value2' result = driver_utils.get_node_capability(self.node, 'cap1') result2 = driver_utils.get_node_capability(self.node, 'cap2') self.assertEqual(expected, result) self.assertEqual(expected2, result2)
def dhcp_options_for_instance(task): """Retrieves the DHCP PXE boot options. :param task: A TaskManager instance. """ dhcp_opts = [] if CONF.pxe.ipxe_enabled: script_name = os.path.basename(CONF.pxe.ipxe_boot_script) ipxe_script_url = '/'.join([CONF.pxe.http_url, script_name]) # if the request comes from dumb firmware send them the iPXE # boot image. !175 == non-iPXE. # http://ipxe.org/howto/dhcpd#ipxe-specific_options dhcp_opts.append({'opt_name': '!175,bootfile-name', 'opt_value': CONF.pxe.pxe_bootfile_name}) # If the request comes from iPXE, direct it to boot from the # iPXE script dhcp_opts.append({'opt_name': 'bootfile-name', 'opt_value': ipxe_script_url}) else: if driver_utils.get_node_capability(task.node, 'boot_mode') == 'uefi': boot_file = CONF.pxe.uefi_pxe_bootfile_name else: boot_file = CONF.pxe.pxe_bootfile_name dhcp_opts.append({'opt_name': 'bootfile-name', 'opt_value': boot_file}) dhcp_opts.append({'opt_name': 'server-ip-address', 'opt_value': CONF.pxe.tftp_server}) dhcp_opts.append({'opt_name': 'tftp-server', 'opt_value': CONF.pxe.tftp_server}) return dhcp_opts
def prepare(self, task): """Prepare the deployment environment for this task's node. Generates the TFTP configuration for PXE-booting both the deployment and user images, fetches the TFTP image from Glance and add it to the local cache. :param task: a TaskManager instance containing the node to act on. """ # TODO(deva): optimize this if rerun on existing files if CONF.pxe.ipxe_enabled: # Copy the iPXE boot script to HTTP root directory bootfile_path = os.path.join(CONF.pxe.http_root, os.path.basename(CONF.pxe.ipxe_boot_script)) shutil.copyfile(CONF.pxe.ipxe_boot_script, bootfile_path) pxe_info = _get_image_info(task.node, task.context) pxe_options = _build_pxe_config_options(task.node, pxe_info, task.context) if driver_utils.get_node_capability(task.node, 'boot_mode') == 'uefi': pxe_config_template = CONF.pxe.uefi_pxe_config_template else: pxe_config_template = CONF.pxe.pxe_config_template pxe_utils.create_pxe_config(task, pxe_options, pxe_config_template) _cache_ramdisk_kernel(task.context, task.node, pxe_info)
def get_boot_mode_for_deploy(node): """Returns the boot mode that would be used for deploy. This method returns boot mode to used for deploy using following order: It returns 'uefi' if 'secure_boot' is set to 'true' in 'instance_info/capabilities' of node. It returns value of 'boot_mode' in 'properties/capabilities' of node. It returns boot mode specified in 'instance_info/deploy_boot_mode' of node. It would return None if boot mode is present neither in 'capabilities' of node 'properties' nor in node's 'instance_info'. :param node: an ironic node object. :returns: 'bios', 'uefi' or None """ if is_secure_boot_requested(node): boot_mode = 'uefi' LOG.debug('Deploy boot mode is %(boot_mode)s for %(node)s.', {'boot_mode': boot_mode, 'node': node.uuid}) return boot_mode boot_mode = driver_utils.get_node_capability(node, 'boot_mode') if boot_mode is None: instance_info = node.instance_info boot_mode = instance_info.get('deploy_boot_mode') LOG.debug('Deploy boot mode is %(boot_mode)s for %(node)s.', {'boot_mode': boot_mode, 'node': node.uuid}) return boot_mode
def validate(self, task): """Validate the deployment information for the task's node. :param task: a TaskManager instance containing the node to act on. :raises: InvalidParameterValue. :raises: MissingParameterValue """ # Check the boot_mode capability parameter value. driver_utils.validate_boot_mode_capability(task.node) if CONF.pxe.ipxe_enabled: if not CONF.pxe.http_url or not CONF.pxe.http_root: raise exception.MissingParameterValue(_( "iPXE boot is enabled but no HTTP URL or HTTP " "root was specified.")) # iPXE and UEFI should not be configured together. if driver_utils.get_node_capability(task.node, 'boot_mode') == 'uefi': LOG.error(_LE("UEFI boot mode is not supported with " "iPXE boot enabled.")) raise exception.InvalidParameterValue(_( "Conflict: iPXE is enabled, but cannot be used with node" "%(node_uuid)s configured to use UEFI boot") % {'node_uuid': task.node.uuid}) d_info = _parse_deploy_info(task.node) iscsi_deploy.validate(task) props = ['kernel_id', 'ramdisk_id'] iscsi_deploy.validate_glance_image_properties(task.context, d_info, props)
def get_boot_mode_for_deploy(node): """Returns the boot mode that would be used for deploy. This method returns boot mode to be used for deploy. It returns 'uefi' if 'secure_boot' is set to 'true' or returns 'bios' if 'trusted_boot' is set to 'true' in 'instance_info/capabilities' of node. Otherwise it returns value of 'boot_mode' in 'properties/capabilities' of node if set. If that is not set, it returns boot mode in 'instance_info/deploy_boot_mode' for the node. It would return None if boot mode is present neither in 'capabilities' of node 'properties' nor in node's 'instance_info' (which could also be None). :param node: an ironic node object. :returns: 'bios', 'uefi' or None """ if is_secure_boot_requested(node): LOG.debug("Deploy boot mode is uefi for %s.", node.uuid) return "uefi" if is_trusted_boot_requested(node): # TODO(lintan) Trusted boot also supports uefi, but at the moment, # it should only boot with bios. LOG.debug("Deploy boot mode is bios for %s.", node.uuid) return "bios" boot_mode = driver_utils.get_node_capability(node, "boot_mode") if boot_mode is None: instance_info = node.instance_info boot_mode = instance_info.get("deploy_boot_mode") LOG.debug("Deploy boot mode is %(boot_mode)s for %(node)s.", {"boot_mode": boot_mode, "node": node.uuid}) return boot_mode.lower() if boot_mode else boot_mode
def create_pxe_config(task, pxe_options, template=None): """Generate PXE configuration file and MAC address links for it. This method will generate the PXE configuration file for the task's node under a directory named with the UUID of that node. For each MAC address (port) of that node, a symlink for the configuration file will be created under the PXE configuration directory, so regardless of which port boots first they'll get the same PXE configuration. :param task: A TaskManager instance. :param pxe_options: A dictionary with the PXE configuration parameters. :param template: The PXE configuration template. If no template is given the CONF.pxe.pxe_config_template will be used. """ LOG.debug("Building PXE config for node %s", task.node.uuid) if template is None: template = CONF.pxe.pxe_config_template _ensure_config_dirs_exist(task.node.uuid) pxe_config_file_path = get_pxe_config_file_path(task.node.uuid) pxe_config = _build_pxe_config(pxe_options, template) utils.write_to_file(pxe_config_file_path, pxe_config) if driver_utils.get_node_capability(task.node, 'boot_mode') == 'uefi': _link_ip_address_pxe_configs(task) else: _link_mac_pxe_configs(task)
def clean_up_pxe_config(task): """Clean up the TFTP environment for the task's node. :param task: A TaskManager instance. """ LOG.debug("Cleaning up PXE config for node %s", task.node.uuid) if driver_utils.get_node_capability(task.node, 'boot_mode') == 'uefi': api = dhcp_factory.DHCPFactory().provider ip_addresses = api.get_ip_addresses(task) if not ip_addresses: return for port_ip_address in ip_addresses: try: ip_address_path = _get_pxe_ip_address_path(port_ip_address) except exception.InvalidIPv4Address: continue utils.unlink_without_raise(ip_address_path) else: for mac in driver_utils.get_node_mac_addresses(task): utils.unlink_without_raise(_get_pxe_mac_path(mac)) utils.rmtree_without_raise(os.path.join(get_root_dir(), task.node.uuid))
def get_boot_mode_for_deploy(node): """Returns the boot mode that would be used for deploy. This method returns boot mode to be used for deploy. It returns 'uefi' if 'secure_boot' is set to 'true' in 'instance_info/capabilities' of node. Otherwise it returns value of 'boot_mode' in 'properties/capabilities' of node if set. If that is not set, it returns boot mode in 'instance_info/deploy_boot_mode' for the node. It would return None if boot mode is present neither in 'capabilities' of node 'properties' nor in node's 'instance_info' (which could also be None). :param node: an ironic node object. :returns: 'bios', 'uefi' or None """ if is_secure_boot_requested(node): LOG.debug('Deploy boot mode is uefi for %s.', node.uuid) return 'uefi' boot_mode = driver_utils.get_node_capability(node, 'boot_mode') if boot_mode is None: instance_info = node.instance_info boot_mode = instance_info.get('deploy_boot_mode') LOG.debug('Deploy boot mode is %(boot_mode)s for %(node)s.', {'boot_mode': boot_mode, 'node': node.uuid}) return boot_mode.lower() if boot_mode else boot_mode
def validate(self, task): """Validate the deployment information for the task's node. :param task: a TaskManager instance containing the node to act on. :raises: InvalidParameterValue. :raises: MissingParameterValue """ node = task.node # Check the boot_mode and boot_option capabilities values. driver_utils.validate_boot_mode_capability(node) driver_utils.validate_boot_option_capability(node) boot_mode = driver_utils.get_node_capability(node, 'boot_mode') boot_option = driver_utils.get_node_capability(node, 'boot_option') # NOTE(lucasagomes): We don't support UEFI + localboot because # we do not support creating an EFI boot partition, including the # EFI modules and managing the bootloader variables via efibootmgr. if boot_mode == 'uefi' and boot_option == 'local': error_msg = (_("Local boot is requested, but can't be " "used with node %s because it's configured " "to use UEFI boot") % node.uuid) LOG.error(error_msg) raise exception.InvalidParameterValue(error_msg) if CONF.pxe.ipxe_enabled: if not CONF.pxe.http_url or not CONF.pxe.http_root: raise exception.MissingParameterValue(_( "iPXE boot is enabled but no HTTP URL or HTTP " "root was specified.")) # iPXE and UEFI should not be configured together. if boot_mode == 'uefi': LOG.error(_LE("UEFI boot mode is not supported with " "iPXE boot enabled.")) raise exception.InvalidParameterValue(_( "Conflict: iPXE is enabled, but cannot be used with node" "%(node_uuid)s configured to use UEFI boot") % {'node_uuid': node.uuid}) d_info = _parse_deploy_info(node) iscsi_deploy.validate(task) props = ['kernel_id', 'ramdisk_id'] iscsi_deploy.validate_glance_image_properties(task.context, d_info, props)
def validate(self, task): """Validate storage_interface configuration for Cinder usage. In order to provide fail fast functionality prior to nodes being requested to enter the active state, this method performs basic checks of the volume connectors, volume targets, and operator defined capabilities. These checks are to help ensure that we should have a compatible configuration prior to activating the node. :param task: The task object. :raises: InvalidParameterValue If a misconfiguration or mismatch exists that would prevent storage the cinder storage driver from initializing attachments. """ found_types = self._validate_connectors(task) node = task.node iscsi_boot = strutils.bool_from_string( utils.get_node_capability(node, 'iscsi_boot')) fc_boot = strutils.bool_from_string( utils.get_node_capability(node, 'fibre_channel_boot')) # Validate capability configuration against configured volumes # such that we raise errors for missing connectors if the # boot capability is defined. if iscsi_boot and not found_types['iscsi_found']: valid_types = ', '.join(VALID_ISCSI_TYPES) msg = (_("In order to enable the 'iscsi_boot' capability for " "the node, an associated volume_connector type " "must be valid for iSCSI (%(options)s).") % {'options': valid_types}) self._fail_validation(task, msg) if fc_boot and not found_types['fc_found']: valid_types = ', '.join(VALID_FC_TYPES) msg = (_("In order to enable the 'fibre_channel_boot' capability " "for the node, an associated volume_connector type must " "be valid for Fibre Channel (%(options)s).") % {'options': valid_types}) self._fail_validation(task, msg) self._validate_targets(task, found_types, iscsi_boot, fc_boot)
def prepare(self, task): """Prepare the deployment environment for this task's node. :param task: a TaskManager instance containing the node to act on. :raises: IloOperationError, if some operation on iLO failed. """ boot_mode = driver_utils.get_node_capability(task.node, 'boot_mode') if boot_mode is not None: ilo_common.set_boot_mode(task.node, boot_mode) else: ilo_common.update_boot_mode_capability(task)
def validate_capabilities(node): """Validates that specified supported capabilities have valid value This method checks if the any of the supported capability is present in Node capabilities. For all supported capabilities specified for a Node, it validates that it has a valid value. The node can have capability as part of the 'properties' or 'instance_info' or both. Note that the actual value of a capability does not need to be the same in the node's 'properties' and 'instance_info'. :param node: an ironic node object. :raises: InvalidParameterValue, if the capability is not set to a valid value. """ exp_str = _( "The parameter '%(capability)s' from %(field)s has an " "invalid value: '%(value)s'. Acceptable values are: " "%(valid_values)s." ) for capability_name, valid_values in SUPPORTED_CAPABILITIES.items(): # Validate capability_name in node's properties/capabilities value = driver_utils.get_node_capability(node, capability_name) if value and (value not in valid_values): field = "properties/capabilities" raise exception.InvalidParameterValue( exp_str % { "capability": capability_name, "field": field, "value": value, "valid_values": ", ".join(valid_values), } ) # Validate capability_name in node's instance_info/['capabilities'] capabilities = parse_instance_info_capabilities(node) value = capabilities.get(capability_name) if value and (value not in valid_values): field = "instance_info['capabilities']" raise exception.InvalidParameterValue( exp_str % { "capability": capability_name, "field": field, "value": value, "valid_values": ", ".join(valid_values), } )
def test__prepare_node_for_deploy(self, func_node_power_action, func_disable_secure_boot, func_update_boot_mode): with task_manager.acquire(self.context, self.node.uuid, shared=False) as task: func_disable_secure_boot.return_value = False ilo_deploy._prepare_node_for_deploy(task) func_node_power_action.assert_called_once_with(task, states.POWER_OFF) func_disable_secure_boot.assert_called_once_with(task) func_update_boot_mode.assert_called_once_with(task) bootmode = driver_utils.get_node_capability(task.node, "boot_mode") self.assertIsNone(bootmode)
def _continue_deploy(self, task, **kwargs): """Continues the deployment of baremetal node over iSCSI. This method continues the deployment of the baremetal node over iSCSI from where the deployment ramdisk has left off. :param task: a TaskManager instance containing the node to act on. :param kwargs: kwargs for performing iscsi deployment. :raises: InvalidState """ node = task.node task.process_event('resume') _destroy_token_file(node) root_uuid = iscsi_deploy.continue_deploy(task, **kwargs) if not root_uuid: return # save the node's root disk UUID so that another conductor could # rebuild the PXE config file. Due to a shortcoming in Nova objects, # we have to assign to node.driver_internal_info so the node knows it # has changed. driver_internal_info = node.driver_internal_info driver_internal_info['root_uuid'] = root_uuid node.driver_internal_info = driver_internal_info node.save() try: if iscsi_deploy.get_boot_option(node) == "local": try_set_boot_device(task, boot_devices.DISK) # If it's going to boot from the local disk, get rid of # the PXE configuration files used for the deployment pxe_utils.clean_up_pxe_config(task) else: pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) deploy_utils.switch_pxe_config(pxe_config_path, root_uuid, driver_utils.get_node_capability(node, 'boot_mode')) deploy_utils.notify_deploy_complete(kwargs['address']) LOG.info(_LI('Deployment to node %s done'), node.uuid) task.process_event('done') except Exception as e: LOG.error(_LE('Deploy failed for instance %(instance)s. ' 'Error: %(error)s'), {'instance': node.instance_uuid, 'error': e}) msg = _('Failed to continue iSCSI deployment.') deploy_utils.set_failed_state(task, msg)
def set_boot_device(self, task, device, persistent=False): """Set the boot device for a node. Set the boot device to use on next reboot of the node. :param task: A task from TaskManager. :param device: The boot device, one of the supported devices listed in :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: InvalidParameterValue if an invalid boot device is specified. :raises: MissingParameterValue if a required parameter is missing. :raises: IPMIFailure on an error from ipmitool. """ if device not in self.get_supported_boot_devices(task): raise exception.InvalidParameterValue(_( "Invalid boot device %s specified.") % device) uefi_mode = ( driver_utils.get_node_capability(task.node, 'boot_mode') == 'uefi') # disable 60 secs timer timeout_disable = "0x00 0x08 0x03 0x08" ipmitool.send_raw(task, timeout_disable) # note(naohirot): # Set System Boot Options : ipmi cmd '0x08', bootparam '0x05' # # $ ipmitool raw 0x00 0x08 0x05 data1 data2 0x00 0x00 0x00 # # data1 : '0xe0' persistent + uefi # '0xc0' persistent + bios # '0xa0' next only + uefi # '0x80' next only + bios # data2 : boot device defined in the dict _BOOTPARAM5_DATA2 bootparam5 = '0x00 0x08 0x05 %s %s 0x00 0x00 0x00' if persistent: data1 = '0xe0' if uefi_mode else '0xc0' else: data1 = '0xa0' if uefi_mode else '0x80' data2 = _BOOTPARAM5_DATA2[device] cmd8 = bootparam5 % (data1, data2) ipmitool.send_raw(task, cmd8)
def try_set_boot_device(task, device, persistent=True): # NOTE(faizan): Under UEFI boot mode, setting of boot device may differ # between different machines. IPMI does not work for setting boot # devices in UEFI mode for certain machines. # Expected IPMI failure for uefi boot mode. Logging a message to # set the boot device manually and continue with deploy. try: manager_utils.node_set_boot_device(task, device, persistent=persistent) except exception.IPMIFailure: if driver_utils.get_node_capability(task.node, 'boot_mode') == 'uefi': LOG.warning(_LW("ipmitool is unable to set boot device while " "the node %s is in UEFI boot mode. Please set " "the boot device manually.") % task.node.uuid) else: raise
def test__prepare_node_for_deploy_sec_boot_on_inst_info( self, func_node_power_action, func_disable_secure_boot, func_update_boot_mode): instance_info = {'capabilities': '{"secure_boot": "true"}'} with task_manager.acquire(self.context, self.node.uuid, shared=False) as task: func_disable_secure_boot.return_value = True task.node.instance_info = instance_info ilo_deploy._prepare_node_for_deploy(task) func_node_power_action.assert_called_once_with(task, states.POWER_OFF) func_disable_secure_boot.assert_called_once_with(task) self.assertFalse(func_update_boot_mode.called) bootmode = driver_utils.get_node_capability(task.node, "boot_mode") self.assertIsNone(bootmode) self.assertNotIn('deploy_boot_mode', task.node.instance_info)
def prepare(self, task): """Prepare the deployment environment for this task's node. If the node's 'capabilities' property includes a boot_mode, that boot mode will be applied for the node. Otherwise, the existing boot mode of the node is used in the node's 'capabilities' property. PXEDeploys' prepare method is then called, to prepare the deploy environment for the node :param task: a TaskManager instance containing the node to act on. """ boot_mode = driver_utils.get_node_capability(task.node, 'boot_mode') if boot_mode is None: ilo_common.update_boot_mode_capability(task) else: ilo_common.set_boot_mode(task.node, boot_mode) super(IloPXEDeploy, self).prepare(task)
def set_boot_device(self, task, device, persistent=False): """Set the boot device for a node. Set the boot device to use on next reboot of the node. :param task: A task from TaskManager. :param device: The boot device, one of the supported devices listed in :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: InvalidParameterValue if an invalid boot device is specified. :raises: MissingParameterValue if a required parameter is missing. :raises: IPMIFailure on an error from ipmitool. """ if driver_utils.get_node_capability(task.node, 'boot_mode') == 'uefi': if device not in self.get_supported_boot_devices(): raise exception.InvalidParameterValue(_( "Invalid boot device %s specified.") % device) timeout_disable = "0x00 0x08 0x03 0x08" ipmitool.send_raw(task, timeout_disable) # note(naohirot): As of ipmitool version 1.8.13, # in case of chassis command, the efiboot option doesn't # get set with persistent at the same time. # $ ipmitool chassis bootdev pxe options=efiboot,persistent # In case of raw command, however, both can be set at the # same time. # $ ipmitool raw 0x00 0x08 0x05 0xe0 0x04 0x00 0x00 0x00 # data1^^ ^^data2 # ipmi cmd '0x08' : Set System Boot Options # data1 '0xe0' : persistent and uefi # data1 '0xa0' : next boot only and uefi # data1 = '0xe0' if persistent else '0xa0' bootparam5 = '0x00 0x08 0x05 %s %s 0x00 0x00 0x00' cmd08 = bootparam5 % (data1, _BOOTPARAM5_DATA2[device]) ipmitool.send_raw(task, cmd08) else: super(IRMCManagement, self).set_boot_device( task, device, persistent)
def deploy(self, task): """Start deployment of the task's node'. Fetches instance image, creates a temporary keystone token file, updates the DHCP port options for next boot, and issues a reboot request to the power driver. This causes the node to boot into the deployment ramdisk and triggers the next phase of PXE-based deployment via VendorPassthru._continue_deploy(). :param task: a TaskManager instance containing the node to act on. :returns: deploy state DEPLOYING. """ iscsi_deploy.cache_instance_image(task.context, task.node) iscsi_deploy.check_image_size(task) # TODO(yuriyz): more secure way needed for pass auth token # to deploy ramdisk _create_token_file(task) dhcp_opts = pxe_utils.dhcp_options_for_instance(task) provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) # NOTE(faizan): Under UEFI boot mode, setting of boot device may differ # between different machines. IPMI does not work for setting boot # devices in UEFI mode for certain machines. # Expected IPMI failure for uefi boot mode. Logging a message to # set the boot device manually and continue with deploy. try: manager_utils.node_set_boot_device(task, 'pxe', persistent=True) except exception.IPMIFailure: if driver_utils.get_node_capability(task.node, 'boot_mode') == 'uefi': LOG.warning(_LW("ipmitool is unable to set boot device while " "the node is in UEFI boot mode." "Please set the boot device manually.")) else: raise manager_utils.node_power_action(task, states.REBOOT) return states.DEPLOYWAIT
def set_boot_device(self, task, device, persistent=False): """Set the boot device for the task's node. Set the boot device to use on next boot of the node. :param task: a task from TaskManager. :param device: the boot device. :param persistent: Boolean value. True if the boot device will persist to all future boots, False if not. Default: False. :raises: InvalidParameterValue if an invalid boot device is specified. """ self._check_valid_device(device, task.node) client, blade_id = msftocs_common.get_client_info( task.node.driver_info) boot_mode = drivers_utils.get_node_capability(task.node, 'boot_mode') uefi = (boot_mode == 'uefi') boot_type = DEVICE_TO_BOOT_TYPE_MAP[device] client.set_next_boot(blade_id, boot_type, persistent, uefi)
def dhcp_options_for_instance(task): """Retrieves the DHCP PXE boot options. :param task: A TaskManager instance. """ dhcp_opts = [] if CONF.pxe.ipxe_enabled: script_name = os.path.basename(CONF.pxe.ipxe_boot_script) ipxe_script_url = '/'.join([CONF.pxe.http_url, script_name]) dhcp_provider_name = dhcp_factory.CONF.dhcp.dhcp_provider # if the request comes from dumb firmware send them the iPXE # boot image. if dhcp_provider_name == 'neutron': # Neutron use dnsmasq as default DHCP agent, add extra config # to neutron "dhcp-match=set:ipxe,175" and use below option dhcp_opts.append({'opt_name': 'tag:!ipxe,bootfile-name', 'opt_value': CONF.pxe.pxe_bootfile_name}) else: # !175 == non-iPXE. # http://ipxe.org/howto/dhcpd#ipxe-specific_options dhcp_opts.append({'opt_name': '!175,bootfile-name', 'opt_value': CONF.pxe.pxe_bootfile_name}) # If the request comes from iPXE, direct it to boot from the # iPXE script dhcp_opts.append({'opt_name': 'bootfile-name', 'opt_value': ipxe_script_url}) else: if driver_utils.get_node_capability(task.node, 'boot_mode') == 'uefi': boot_file = CONF.pxe.uefi_pxe_bootfile_name else: boot_file = CONF.pxe.pxe_bootfile_name dhcp_opts.append({'opt_name': 'bootfile-name', 'opt_value': boot_file}) dhcp_opts.append({'opt_name': 'server-ip-address', 'opt_value': CONF.pxe.tftp_server}) dhcp_opts.append({'opt_name': 'tftp-server', 'opt_value': CONF.pxe.tftp_server}) return dhcp_opts
def _continue_deploy(self, task, **kwargs): """Continues the deployment of baremetal node over iSCSI. This method continues the deployment of the baremetal node over iSCSI from where the deployment ramdisk has left off. :param task: a TaskManager instance containing the node to act on. :param kwargs: kwargs for performing iscsi deployment. :raises: InvalidState """ node = task.node task.process_event('resume') _destroy_token_file(node) root_uuid = iscsi_deploy.continue_deploy(task, **kwargs) if not root_uuid: return try: if iscsi_deploy.get_boot_option(node) == "local": try_set_boot_device(task, boot_devices.DISK) # If it's going to boot from the local disk, get rid of # the PXE configuration files used for the deployment pxe_utils.clean_up_pxe_config(task) else: pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) deploy_utils.switch_pxe_config(pxe_config_path, root_uuid, driver_utils.get_node_capability(node, 'boot_mode')) deploy_utils.notify_deploy_complete(kwargs['address']) LOG.info(_LI('Deployment to node %s done'), node.uuid) task.process_event('done') except Exception as e: LOG.error(_LE('Deploy failed for instance %(instance)s. ' 'Error: %(error)s'), {'instance': node.instance_uuid, 'error': e}) msg = _('Failed to continue iSCSI deployment.') deploy_utils.set_failed_state(task, msg)
def _continue_deploy(self, task, **kwargs): """Continues the deployment of baremetal node over iSCSI. This method continues the deployment of the baremetal node over iSCSI from where the deployment ramdisk has left off. :param task: a TaskManager instance containing the node to act on. :param kwargs: kwargs for performing iscsi deployment. """ node = task.node if node.provision_state != states.DEPLOYWAIT: LOG.error(_LE('Node %s is not waiting to be deployed.'), node.uuid) return _destroy_token_file(node) root_uuid = iscsi_deploy.continue_deploy(task, **kwargs) if not root_uuid: return try: pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) deploy_utils.switch_pxe_config(pxe_config_path, root_uuid, driver_utils.get_node_capability(node, 'boot_mode')) deploy_utils.notify_deploy_complete(kwargs['address']) LOG.info(_LI('Deployment to node %s done'), node.uuid) node.provision_state = states.ACTIVE node.target_provision_state = states.NOSTATE node.save() except Exception as e: LOG.error(_LE('Deploy failed for instance %(instance)s. ' 'Error: %(error)s'), {'instance': node.instance_uuid, 'error': e}) msg = _('Failed to continue iSCSI deployment.') iscsi_deploy.set_failed_state(task, msg)
def _continue_deploy(self, task, **kwargs): """Continues the deployment of baremetal node over iSCSI. This method continues the deployment of the baremetal node over iSCSI from where the deployment ramdisk has left off. :param task: a TaskManager instance containing the node to act on. :param kwargs: kwargs for performing iscsi deployment. :raises: InvalidState """ node = task.node task.process_event('resume') _destroy_token_file(node) root_uuid = iscsi_deploy.continue_deploy(task, **kwargs) if not root_uuid: return try: pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) deploy_utils.switch_pxe_config( pxe_config_path, root_uuid, driver_utils.get_node_capability(node, 'boot_mode')) deploy_utils.notify_deploy_complete(kwargs['address']) LOG.info(_LI('Deployment to node %s done'), node.uuid) task.process_event('done') except Exception as e: LOG.error( _LE('Deploy failed for instance %(instance)s. ' 'Error: %(error)s'), { 'instance': node.instance_uuid, 'error': e }) msg = _('Failed to continue iSCSI deployment.') deploy_utils.set_failed_state(task, msg)
def get_boot_mode_for_deploy(node): """Returns the boot mode that would be used for deploy. This method returns boot mode to be used for deploy. It returns 'uefi' if 'secure_boot' is set to 'true' or returns 'bios' if 'trusted_boot' is set to 'true' in 'instance_info/capabilities' of node. Otherwise it returns value of 'boot_mode' in 'properties/capabilities' of node if set. If that is not set, it returns boot mode in 'internal_driver_info/deploy_boot_mode' for the node. If that is not set, it returns boot mode in 'instance_info/deploy_boot_mode' for the node. It would return None if boot mode is present neither in 'capabilities' of node 'properties' nor in node's 'internal_driver_info' nor in node's 'instance_info' (which could also be None). :param node: an ironic node object. :returns: 'bios', 'uefi' or None :raises: InvalidParameterValue, if the node boot mode disagrees with the boot mode set to node properties/capabilities """ if is_secure_boot_requested(node): LOG.debug('Deploy boot mode is uefi for %s.', node.uuid) return 'uefi' if is_trusted_boot_requested(node): # TODO(lintan) Trusted boot also supports uefi, but at the moment, # it should only boot with bios. LOG.debug('Deploy boot mode is bios for %s.', node.uuid) return 'bios' # NOTE(etingof): # The search for a boot mode should be in the priority order: # # 1) instance_info # 2) properties.capabilities # 3) driver_internal_info # # Because: # # (1) can be deleted before teardown # (3) will never be touched if node properties/capabilities # are still present. # (2) becomes operational default as the last resort instance_info = node.instance_info cap_boot_mode = driver_utils.get_node_capability(node, 'boot_mode') boot_mode = instance_info.get('deploy_boot_mode') if boot_mode is None: boot_mode = cap_boot_mode if cap_boot_mode is None: driver_internal_info = node.driver_internal_info boot_mode = driver_internal_info.get('deploy_boot_mode') if not boot_mode: return boot_mode = boot_mode.lower() # NOTE(etingof): # Make sure that the ultimate boot_mode agrees with the one set to # node properties/capabilities. This locks down node to use only # boot mode specified in properties/capabilities. # TODO(etingof): this logic will have to go away when we switch to traits if cap_boot_mode: cap_boot_mode = cap_boot_mode.lower() if cap_boot_mode != boot_mode: msg = (_("Node %(uuid)s boot mode %(boot_mode)s violates " "node properties/capabilities %(caps)s") % {'uuid': node.uuid, 'boot_mode': boot_mode, 'caps': cap_boot_mode}) LOG.error(msg) raise exception.InvalidParameterValue(msg) LOG.debug('Deploy boot mode is %(boot_mode)s for %(node)s.', {'boot_mode': boot_mode, 'node': node.uuid}) return boot_mode
def inspect_hardware(self, task): """Inspect hardware to get the hardware properties. Inspects hardware to get the essential properties. It fails if any of the essential properties are not received from the node. :param task: a TaskManager instance. :raises: HardwareInspectionFailure if essential properties could not be retrieved successfully. :returns: The resulting state of inspection. """ system = redfish_utils.get_system(task.node) # get the essential properties and update the node properties # with it. inspected_properties = task.node.properties if system.memory_summary and system.memory_summary.size_gib: inspected_properties['memory_mb'] = str( system.memory_summary.size_gib * units.Ki) if system.processors and system.processors.summary: cpus, arch = system.processors.summary if cpus: inspected_properties['cpus'] = cpus if arch: try: inspected_properties['cpu_arch'] = CPU_ARCH_MAP[arch] except KeyError: LOG.warning("Unknown CPU arch %(arch)s discovered " "for node %(node)s", {'node': task.node.uuid, 'arch': arch}) simple_storage_size = 0 try: LOG.debug("Attempting to discover system simple storage size for " "node %(node)s", {'node': task.node.uuid}) if (system.simple_storage and system.simple_storage.disks_sizes_bytes): simple_storage_size = [ size for size in system.simple_storage.disks_sizes_bytes if size >= 4 * units.Gi ] or [0] simple_storage_size = simple_storage_size[0] except sushy.exceptions.SushyError as ex: LOG.debug("No simple storage information discovered " "for node %(node)s: %(err)s", {'node': task.node.uuid, 'err': ex}) storage_size = 0 try: LOG.debug("Attempting to discover system storage volume size for " "node %(node)s", {'node': task.node.uuid}) if system.storage and system.storage.volumes_sizes_bytes: storage_size = [ size for size in system.storage.volumes_sizes_bytes if size >= 4 * units.Gi ] or [0] storage_size = storage_size[0] except sushy.exceptions.SushyError as ex: LOG.debug("No storage volume information discovered " "for node %(node)s: %(err)s", {'node': task.node.uuid, 'err': ex}) try: if not storage_size: LOG.debug("Attempting to discover system storage drive size " "for node %(node)s", {'node': task.node.uuid}) if system.storage and system.storage.drives_sizes_bytes: storage_size = [ size for size in system.storage.drives_sizes_bytes if size >= 4 * units.Gi ] or [0] storage_size = storage_size[0] except sushy.exceptions.SushyError as ex: LOG.debug("No storage drive information discovered " "for node %(node)s: %(err)s", {'node': task.node.uuid, 'err': ex}) # NOTE(etingof): pick the smallest disk larger than 4G among available if simple_storage_size and storage_size: local_gb = min(simple_storage_size, storage_size) else: local_gb = max(simple_storage_size, storage_size) # Note(deray): Convert the received size to GiB and reduce the # value by 1 GB as consumers like Ironic requires the ``local_gb`` # to be returned 1 less than actual size. local_gb = max(0, int(local_gb / units.Gi - 1)) # TODO(etingof): should we respect root device hints here? if local_gb: inspected_properties['local_gb'] = str(local_gb) else: LOG.warning("Could not provide a valid storage size configured " "for node %(node)s. Assuming this is a disk-less node", {'node': task.node.uuid}) inspected_properties['local_gb'] = '0' if system.boot.mode: if not drivers_utils.get_node_capability(task.node, 'boot_mode'): capabilities = utils.get_updated_capabilities( inspected_properties.get('capabilities', ''), {'boot_mode': BOOT_MODE_MAP[system.boot.mode]}) inspected_properties['capabilities'] = capabilities valid_keys = self.ESSENTIAL_PROPERTIES missing_keys = valid_keys - set(inspected_properties) if missing_keys: error = (_('Failed to discover the following properties: ' '%(missing_keys)s on node %(node)s'), {'missing_keys': ', '.join(missing_keys), 'node': task.node.uuid}) raise exception.HardwareInspectionFailure(error=error) task.node.properties = inspected_properties task.node.save() LOG.debug("Node properties for %(node)s are updated as " "%(properties)s", {'properties': inspected_properties, 'node': task.node.uuid}) if (system.ethernet_interfaces and system.ethernet_interfaces.summary): macs = system.ethernet_interfaces.summary # Create ports for the discovered NICs being in 'enabled' state enabled_macs = {nic_mac: nic_state for nic_mac, nic_state in macs.items() if nic_state == sushy.STATE_ENABLED} if enabled_macs: inspect_utils.create_ports_if_not_exist( task, enabled_macs, get_mac_address=lambda x: x[0]) else: LOG.warning("Not attempting to create any port as no NICs " "were discovered in 'enabled' state for node " "%(node)s: %(mac_data)s", {'mac_data': macs, 'node': task.node.uuid}) else: LOG.warning("No NIC information discovered " "for node %(node)s", {'node': task.node.uuid}) return states.MANAGEABLE
def _get_boot_iso(task, root_uuid): """This method returns a boot ISO to boot the node. It chooses one of the two options in the order as below: 1. Image deployed has a meta-property 'boot_iso' in Glance. This should refer to the UUID of the boot_iso which exists in Glance. 2. Generates a boot ISO on the fly using kernel and ramdisk mentioned in the image deployed. It uploads the generated boot ISO to Swift. :param task: a TaskManager instance containing the node to act on. :param root_uuid: the uuid of the root partition. :returns: the information about the boot ISO. Returns the information in the format 'glance:<glance-boot-iso-uuid>' or 'swift:<swift-boot_iso-object-name>'. In case of Swift, it is assumed that the object exists in CONF.ilo.swift_ilo_container. On error finding the boot iso, it returns None. :raises: MissingParameterValue, if any of the required parameters are missing in the node's driver_info or instance_info. :raises: InvalidParameterValue, if any of the parameters have invalid value in the node's driver_info or instance_info. :raises: SwiftOperationError, if operation with Swift fails. :raises: ImageCreationFailed, if creation of boot ISO failed. """ # Option 1 - Check if user has provided a boot_iso in Glance. LOG.debug("Trying to get a boot ISO to boot the baremetal node") deploy_info = _parse_deploy_info(task.node) image_href = deploy_info['image_source'] glance_properties = ( images.get_glance_image_properties(task.context, image_href, ['boot_iso', 'kernel_id', 'ramdisk_id'])) boot_iso_uuid = glance_properties.get('boot_iso') kernel_uuid = glance_properties.get('kernel_id') ramdisk_uuid = glance_properties.get('ramdisk_id') if boot_iso_uuid: LOG.debug("Found boot_iso %s in Glance", boot_iso_uuid) return 'glance:%s' % boot_iso_uuid # NOTE(faizan) For uefi boot_mode, operator should provide efi capable # boot-iso in glance if driver_utils.get_node_capability(task.node, 'boot_mode') == 'uefi': LOG.error(_LE("Unable to find boot_iso in Glance, required to deploy " "node %(node)s in UEFI boot mode."), {'node': task.node.uuid}) return if not kernel_uuid or not ramdisk_uuid: LOG.error(_LE("Unable to find 'kernel_id' and 'ramdisk_id' in Glance " "image %(image)s for generating boot ISO for %(node)s"), {'image': image_href, 'node': task.node.uuid}) return # NOTE(rameshg87): Functionality to share the boot ISOs created for # similar instances (instances with same deployed image) is # not implemented as of now. Creation/Deletion of such a shared boot ISO # will require synchronisation across conductor nodes for the shared boot # ISO. Such a synchronisation mechanism doesn't exist in ironic as of now. # Option 2 - Create boot_iso from kernel/ramdisk, upload to Swift # and provide its name. boot_iso_object_name = _get_boot_iso_object_name(task.node) kernel_params = CONF.pxe.pxe_append_params container = CONF.ilo.swift_ilo_container with tempfile.NamedTemporaryFile() as fileobj: boot_iso_tmp_file = fileobj.name images.create_boot_iso(task.context, boot_iso_tmp_file, kernel_uuid, ramdisk_uuid, root_uuid, kernel_params) swift_api = swift.SwiftAPI() swift_api.create_object(container, boot_iso_object_name, boot_iso_tmp_file) LOG.debug("Created boot_iso %s in Swift", boot_iso_object_name) return 'swift:%s' % boot_iso_object_name
def inspect_hardware(self, task): """Inspect hardware to get the hardware properties. Inspects hardware to get the essential properties. It fails if any of the essential properties are not received from the node. :param task: a TaskManager instance. :raises: HardwareInspectionFailure if essential properties could not be retrieved successfully. :returns: The resulting state of inspection. """ system = redfish_utils.get_system(task.node) # get the essential properties and update the node properties # with it. inspected_properties = task.node.properties if system.memory_summary and system.memory_summary.size_gib: inspected_properties['memory_mb'] = str( system.memory_summary.size_gib * units.Ki) if system.processors and system.processors.summary: cpus, arch = system.processors.summary if cpus: inspected_properties['cpus'] = cpus if arch: try: inspected_properties['cpu_arch'] = CPU_ARCH_MAP[arch] except KeyError: LOG.warning( "Unknown CPU arch %(arch)s discovered " "for node %(node)s", { 'node': task.node.uuid, 'arch': arch }) simple_storage_size = 0 try: LOG.debug( "Attempting to discover system simple storage size for " "node %(node)s", {'node': task.node.uuid}) if (system.simple_storage and system.simple_storage.disks_sizes_bytes): simple_storage_size = [ size for size in system.simple_storage.disks_sizes_bytes if size >= 4 * units.Gi ] or [0] simple_storage_size = simple_storage_size[0] except sushy.exceptions.SushyError as ex: LOG.debug( "No simple storage information discovered " "for node %(node)s: %(err)s", { 'node': task.node.uuid, 'err': ex }) storage_size = 0 try: LOG.debug( "Attempting to discover system storage volume size for " "node %(node)s", {'node': task.node.uuid}) if system.storage and system.storage.volumes_sizes_bytes: storage_size = [ size for size in system.storage.volumes_sizes_bytes if size >= 4 * units.Gi ] or [0] storage_size = storage_size[0] except sushy.exceptions.SushyError as ex: LOG.debug( "No storage volume information discovered " "for node %(node)s: %(err)s", { 'node': task.node.uuid, 'err': ex }) try: if not storage_size: LOG.debug( "Attempting to discover system storage drive size " "for node %(node)s", {'node': task.node.uuid}) if system.storage and system.storage.drives_sizes_bytes: storage_size = [ size for size in system.storage.drives_sizes_bytes if size >= 4 * units.Gi ] or [0] storage_size = storage_size[0] except sushy.exceptions.SushyError as ex: LOG.debug( "No storage drive information discovered " "for node %(node)s: %(err)s", { 'node': task.node.uuid, 'err': ex }) # NOTE(etingof): pick the smallest disk larger than 4G among available if simple_storage_size and storage_size: local_gb = min(simple_storage_size, storage_size) else: local_gb = max(simple_storage_size, storage_size) # Note(deray): Convert the received size to GiB and reduce the # value by 1 GB as consumers like Ironic requires the ``local_gb`` # to be returned 1 less than actual size. local_gb = max(0, int(local_gb / units.Gi - 1)) # TODO(etingof): should we respect root device hints here? if local_gb: inspected_properties['local_gb'] = str(local_gb) else: LOG.warning( "Could not provide a valid storage size configured " "for node %(node)s. Assuming this is a disk-less node", {'node': task.node.uuid}) inspected_properties['local_gb'] = '0' if system.boot.mode: if not drivers_utils.get_node_capability(task.node, 'boot_mode'): capabilities = utils.get_updated_capabilities( inspected_properties.get('capabilities', ''), {'boot_mode': BOOT_MODE_MAP[system.boot.mode]}) inspected_properties['capabilities'] = capabilities valid_keys = self.ESSENTIAL_PROPERTIES missing_keys = valid_keys - set(inspected_properties) if missing_keys: error = (_('Failed to discover the following properties: ' '%(missing_keys)s on node %(node)s'), { 'missing_keys': ', '.join(missing_keys), 'node': task.node.uuid }) raise exception.HardwareInspectionFailure(error=error) task.node.properties = inspected_properties task.node.save() LOG.debug( "Node properties for %(node)s are updated as " "%(properties)s", { 'properties': inspected_properties, 'node': task.node.uuid }) if (system.ethernet_interfaces and system.ethernet_interfaces.summary): macs = system.ethernet_interfaces.summary # Create ports for the discovered NICs being in 'enabled' state enabled_macs = { nic_mac: nic_state for nic_mac, nic_state in macs.items() if nic_state == sushy.STATE_ENABLED } if enabled_macs: inspect_utils.create_ports_if_not_exist( task, enabled_macs, get_mac_address=lambda x: x[0]) else: LOG.warning( "Not attempting to create any port as no NICs " "were discovered in 'enabled' state for node " "%(node)s: %(mac_data)s", { 'mac_data': macs, 'node': task.node.uuid }) else: LOG.warning("No NIC information discovered " "for node %(node)s", {'node': task.node.uuid}) return states.MANAGEABLE
def test_get_node_capability_returns_none(self): properties = {'capabilities': 'cap1:value1,cap2:value2'} self.node.properties = properties result = driver_utils.get_node_capability(self.node, 'capX') self.assertIsNone(result)
def inspect_hardware(self, task): """Inspect hardware to get the hardware properties. Inspects hardware to get the essential properties. It fails if any of the essential properties are not received from the node. :param task: a TaskManager instance. :raises: HardwareInspectionFailure if essential properties could not be retrieved successfully. :returns: The resulting state of inspection. """ system = redfish_utils.get_system(task.node) # get the essential properties and update the node properties # with it. inspected_properties = task.node.properties if system.memory_summary and system.memory_summary.size_gib: inspected_properties['memory_mb'] = str( system.memory_summary.size_gib * units.Ki) if system.processors and system.processors.summary: cpus, arch = system.processors.summary if cpus: inspected_properties['cpus'] = cpus if arch: try: inspected_properties['cpu_arch'] = CPU_ARCH_MAP[arch] except KeyError: LOG.warning( "Unknown CPU arch %(arch)s discovered " "for node %(node)s", { 'node': task.node.uuid, 'arch': arch }) # TODO(etingof): should we respect root device hints here? local_gb = self._detect_local_gb(task, system) if local_gb: inspected_properties['local_gb'] = str(local_gb) else: LOG.warning( "Could not provide a valid storage size configured " "for node %(node)s. Assuming this is a disk-less node", {'node': task.node.uuid}) inspected_properties['local_gb'] = '0' if system.boot.mode: if not drivers_utils.get_node_capability(task.node, 'boot_mode'): capabilities = utils.get_updated_capabilities( inspected_properties.get('capabilities', ''), {'boot_mode': BOOT_MODE_MAP[system.boot.mode]}) inspected_properties['capabilities'] = capabilities valid_keys = self.ESSENTIAL_PROPERTIES missing_keys = valid_keys - set(inspected_properties) if missing_keys: error = (_('Failed to discover the following properties: ' '%(missing_keys)s on node %(node)s'), { 'missing_keys': ', '.join(missing_keys), 'node': task.node.uuid }) raise exception.HardwareInspectionFailure(error=error) task.node.properties = inspected_properties task.node.save() LOG.debug( "Node properties for %(node)s are updated as " "%(properties)s", { 'properties': inspected_properties, 'node': task.node.uuid }) self._create_ports(task, system) return states.MANAGEABLE