diff options
Diffstat (limited to 'ironic/drivers/modules/irmc')
-rw-r--r-- | ironic/drivers/modules/irmc/common.py | 226 | ||||
-rw-r--r-- | ironic/drivers/modules/irmc/inspect.py | 98 | ||||
-rw-r--r-- | ironic/drivers/modules/irmc/management.py | 289 | ||||
-rw-r--r-- | ironic/drivers/modules/irmc/power.py | 64 | ||||
-rw-r--r-- | ironic/drivers/modules/irmc/vendor.py | 75 |
5 files changed, 677 insertions, 75 deletions
diff --git a/ironic/drivers/modules/irmc/common.py b/ironic/drivers/modules/irmc/common.py index 7a8fc0f1d..4341a82f4 100644 --- a/ironic/drivers/modules/irmc/common.py +++ b/ironic/drivers/modules/irmc/common.py @@ -15,9 +15,12 @@ """ Common functionalities shared between different iRMC modules. """ +import json import os +import re from oslo_log import log as logging +from oslo_serialization import jsonutils from oslo_utils import importutils from oslo_utils import strutils @@ -31,6 +34,23 @@ scci = importutils.try_import('scciclient.irmc.scci') elcm = importutils.try_import('scciclient.irmc.elcm') LOG = logging.getLogger(__name__) + + +IRMC_OS_NAME_R = re.compile(r'iRMC\s+S\d+') +IRMC_OS_NAME_NUM_R = re.compile(r'\d+$') +IRMC_FW_VER_R = re.compile(r'\d(\.\d+)*\w*') +IRMC_FW_VER_NUM_R = re.compile(r'\d(\.\d+)*') + +IPMI_ENABLED_BY_DEFAULT_RANGES = { + # iRMC S4 enables IPMI over LAN by default + '4': None, + # iRMC S5 enables IPMI over LAN by default + '5': None, + # iRMC S6 disables IPMI over LAN by default from version 2.00 + '6': {'upper': '2.00'}} + +ELCM_STATUS_PATH = '/rest/v1/Oem/eLCM/eLCMStatus' + REQUIRED_PROPERTIES = { 'irmc_address': _("IP address or hostname of the iRMC. Required."), 'irmc_username': _("Username for the iRMC with administrator privileges. " @@ -83,7 +103,9 @@ SNMP_V3_REQUIRED_PROPERTIES = { SNMP_V3_OPTIONAL_PROPERTIES = { 'irmc_snmp_auth_proto': _("SNMPv3 message authentication protocol ID. " "Required for version 'v3'. " - "'sha' is supported."), + "If using iRMC S4/S5, only 'sha' is supported." + "If using iRMC S6, the valid options are " + "'sha256', 'sha384', 'sha512'."), 'irmc_snmp_priv_proto': _("SNMPv3 message privacy (encryption) protocol " "ID. Required for version 'v3'. " "'aes' is supported."), @@ -243,7 +265,8 @@ def _parse_snmp_driver_info(node, info): def _parse_snmp_v3_info(node, info): snmp_info = {} missing_info = [] - valid_values = {'irmc_snmp_auth_proto': ['sha'], + valid_values = {'irmc_snmp_auth_proto': ['sha', 'sha256', 'sha384', + 'sha512'], 'irmc_snmp_priv_proto': ['aes']} valid_protocols = {'irmc_snmp_auth_proto': snmp.snmp_auth_protocols, 'irmc_snmp_priv_proto': snmp.snmp_priv_protocols} @@ -433,3 +456,202 @@ def set_secure_boot_mode(node, enable): raise exception.IRMCOperationError( operation=_("setting secure boot mode"), error=irmc_exception) + + +def check_elcm_license(node): + """Connect to iRMC and return status of eLCM license + + This function connects to iRMC REST API and check whether eLCM + license is active. This function can be used to check connection to + iRMC REST API. + + :param node: An ironic node object + :returns: dictionary whose keys are 'active' and 'status_code'. + value of 'active' is boolean showing if eLCM license is active + and value of 'status_code' is int which is HTTP return code + from iRMC REST API access + :raises: InvalidParameterValue if invalid value is contained + in the 'driver_info' property. + :raises: MissingParameterValue if some mandatory key is missing + in the 'driver_info' property. + :raises: IRMCOperationError if the operation fails. + """ + try: + d_info = parse_driver_info(node) + # GET to /rest/v1/Oem/eLCM/eLCMStatus returns + # JSON data like this: + # + # { + # "eLCMStatus":{ + # "EnabledAndLicenced":"true", + # "SDCardMounted":"false" + # } + # } + # + # EnabledAndLicenced tells whether eLCM license is valid + # + r = elcm.elcm_request(d_info, 'GET', ELCM_STATUS_PATH) + + # If r.status_code is 200, it means success and r.text is JSON. + # If it is 500, it means there is problem at iRMC side + # and iRMC cannot return eLCM status. + # If it was 401, elcm_request raises SCCIClientError. + # Otherwise, r.text may not be JSON. + if r.status_code == 200: + license_active = strutils.bool_from_string( + jsonutils.loads(r.text)['eLCMStatus']['EnabledAndLicenced'], + strict=True) + else: + license_active = False + + return {'active': license_active, 'status_code': r.status_code} + except (scci.SCCIError, + json.JSONDecodeError, + TypeError, + KeyError, + ValueError) as irmc_exception: + LOG.error("Failed to check eLCM license status for node $(node)s", + {'node': node.uuid}) + raise exception.IRMCOperationError( + operation='checking eLCM license status', + error=irmc_exception) + + +def set_irmc_version(task): + """Fetch and save iRMC firmware version. + + This function should be called before calling any other functions which + need to check node's iRMC firmware version. + + Set `<iRMC OS>/<fw version>` to driver_internal_info['irmc_fw_version'] + + :param node: An ironic node object + :raises: InvalidParameterValue if invalid value is contained + in the 'driver_info' property. + :raises: MissingParameterValue if some mandatory key is missing + in the 'driver_info' property. + :raises: IRMCOperationError if the operation fails. + :raises: NodeLocked if the target node is already locked. + """ + + node = task.node + try: + report = get_irmc_report(node) + irmc_os, fw_version = scci.get_irmc_version_str(report) + + fw_ver = node.driver_internal_info.get('irmc_fw_version') + if fw_ver != '/'.join([irmc_os, fw_version]): + task.upgrade_lock(purpose='saving firmware version') + node.set_driver_internal_info('irmc_fw_version', + f"{irmc_os}/{fw_version}") + node.save() + except scci.SCCIError as irmc_exception: + LOG.error("Failed to fetch iRMC FW version for node %s", + node.uuid) + raise exception.IRMCOperationError( + operation=_("fetching irmc fw version "), + error=irmc_exception) + + +def _version_lt(v1, v2): + v1_l = v1.split('.') + v2_l = v2.split('.') + if len(v1_l) <= len(v2_l): + v1_l.extend(['0'] * (len(v2_l) - len(v1_l))) + else: + v2_l.extend(['0'] * (len(v1_l) - len(v2_l))) + + for i in range(len(v1_l)): + if int(v1_l[i]) < int(v2_l[i]): + return True + elif int(v1_l[i]) > int(v2_l[i]): + return False + else: + return False + + +def _version_le(v1, v2): + v1_l = v1.split('.') + v2_l = v2.split('.') + if len(v1_l) <= len(v2_l): + v1_l.extend(['0'] * (len(v2_l) - len(v1_l))) + else: + v2_l.extend(['0'] * (len(v1_l) - len(v2_l))) + + for i in range(len(v1_l)): + if int(v1_l[i]) < int(v2_l[i]): + return True + elif int(v1_l[i]) > int(v2_l[i]): + return False + else: + return True + + +def within_version_ranges(node, version_ranges): + """Read saved iRMC FW version and check if it is within the passed ranges. + + :param node: An ironic node object + :param version_ranges: A Python dictionary containing version ranges in the + next format: <os_n>: <ranges>, where <os_n> is a string representing + iRMC OS number (e.g. '4') and <ranges> is a dictionaries indicating + the specific firmware version ranges under the iRMC OS number <os_n>. + + The dictionary used in <ranges> only has two keys: 'min' and 'upper', + and value of each key is a string representing iRMC firmware version + number or None. Both keys can be absent and their value can be None. + + It is acceptable to not set ranges for a <os_n> (for example set + <ranges> to None, {}, etc...), in this case, this function only + checks if the node's iRMC OS number matches the <os_n>. + + Valid <version_ranges> example: + {'3': None, # all version of iRMC S3 matches + '4': {}, # all version of iRMC S4 matches + # all version of iRMC S5 matches + '5': {'min': None, 'upper': None}, + # iRMC S6 whose version is >=1.20 matches + '6': {'min': '1.20', 'upper': None}, + # iRMC S7 whose version is + # 5.51<= (version) <8.23 matches + '7': {'min': '5.51', 'upper': '8.23'}} + + :returns: True if node's iRMC FW is in range, False if not or + fails to parse firmware version + """ + + try: + fw_version = node.driver_internal_info.get('irmc_fw_version', '') + irmc_os, irmc_ver = fw_version.split('/') + + if IRMC_OS_NAME_R.match(irmc_os) and IRMC_FW_VER_R.match(irmc_ver): + os_num = IRMC_OS_NAME_NUM_R.search(irmc_os).group(0) + fw_num = IRMC_FW_VER_NUM_R.search(irmc_ver).group(0) + + if os_num not in version_ranges: + return False + + v_range = version_ranges[os_num] + + # An OS number with no ranges setted means no need to check + # specific version, all the version under this OS number is valid. + if not v_range: + return True + + # Specific range is setted, check if the node's + # firmware version is within it. + min_ver = v_range.get('min') + upper_ver = v_range.get('upper') + flag = True + if min_ver: + flag = _version_le(min_ver, fw_num) + if flag and upper_ver: + flag = _version_lt(fw_num, upper_ver) + return flag + + except Exception: + # All exceptions are ignored + pass + + LOG.warning('Failed to parse iRMC firmware version on node %(uuid)s: ' + '%(fw_ver)s', {'uuid': node.uuid, 'fw_ver': fw_version}) + return False diff --git a/ironic/drivers/modules/irmc/inspect.py b/ironic/drivers/modules/irmc/inspect.py index 9b6bff5bc..f7c2ad7ba 100644 --- a/ironic/drivers/modules/irmc/inspect.py +++ b/ironic/drivers/modules/irmc/inspect.py @@ -32,7 +32,7 @@ from ironic.drivers.modules.irmc import common as irmc_common from ironic.drivers.modules import snmp from ironic import objects -scci = importutils.try_import('scciclient.irmc.scci') +irmc = importutils.try_import('scciclient.irmc') LOG = logging.getLogger(__name__) @@ -122,6 +122,39 @@ def _get_mac_addresses(node): if c == NODE_CLASS_OID_VALUE['primary']] +def _get_capabilities_properties_without_ipmi(d_info, cap_props, + current_cap, props): + capabilities = {} + snmp_client = snmp.SNMPClient( + address=d_info['irmc_address'], + port=d_info['irmc_snmp_port'], + version=d_info['irmc_snmp_version'], + read_community=d_info['irmc_snmp_community'], + user=d_info.get('irmc_snmp_user'), + auth_proto=d_info.get('irmc_snmp_auth_proto'), + auth_key=d_info.get('irmc_snmp_auth_password'), + priv_proto=d_info.get('irmc_snmp_priv_proto'), + priv_key=d_info.get('irmc_snmp_priv_password')) + + if 'rom_firmware_version' in cap_props: + capabilities['rom_firmware_version'] = \ + irmc.snmp.get_bios_firmware_version(snmp_client) + + if 'irmc_firmware_version' in cap_props: + capabilities['irmc_firmware_version'] = \ + irmc.snmp.get_irmc_firmware_version(snmp_client) + + if 'server_model' in cap_props: + capabilities['server_model'] = irmc.snmp.get_server_model( + snmp_client) + + capabilities = utils.get_updated_capabilities(current_cap, capabilities) + if capabilities: + props['capabilities'] = capabilities + + return props + + def _inspect_hardware(node, existing_traits=None, **kwargs): """Inspect the node and get hardware information. @@ -161,39 +194,50 @@ def _inspect_hardware(node, existing_traits=None, **kwargs): try: report = irmc_common.get_irmc_report(node) - props = scci.get_essential_properties( + props = irmc.scci.get_essential_properties( report, IRMCInspect.ESSENTIAL_PROPERTIES) d_info = irmc_common.parse_driver_info(node) - capabilities = scci.get_capabilities_properties( - d_info, - capabilities_props, - gpu_ids, - fpga_ids=fpga_ids, - **kwargs) - if capabilities: - if capabilities.get('pci_gpu_devices') == 0: - capabilities.pop('pci_gpu_devices') - - cpu_fpga = capabilities.pop('cpu_fpga', 0) - if cpu_fpga == 0 and 'CUSTOM_CPU_FPGA' in new_traits: - new_traits.remove('CUSTOM_CPU_FPGA') - elif cpu_fpga != 0 and 'CUSTOM_CPU_FPGA' not in new_traits: - new_traits.append('CUSTOM_CPU_FPGA') - - # Ironic no longer supports trusted boot - capabilities.pop('trusted_boot', None) - capabilities = utils.get_updated_capabilities( - node.properties.get('capabilities'), capabilities) + if node.driver_internal_info.get('irmc_ipmi_succeed'): + capabilities = irmc.scci.get_capabilities_properties( + d_info, + capabilities_props, + gpu_ids, + fpga_ids=fpga_ids, + **kwargs) if capabilities: - props['capabilities'] = capabilities + if capabilities.get('pci_gpu_devices') == 0: + capabilities.pop('pci_gpu_devices') + + cpu_fpga = capabilities.pop('cpu_fpga', 0) + if cpu_fpga == 0 and 'CUSTOM_CPU_FPGA' in new_traits: + new_traits.remove('CUSTOM_CPU_FPGA') + elif cpu_fpga != 0 and 'CUSTOM_CPU_FPGA' not in new_traits: + new_traits.append('CUSTOM_CPU_FPGA') + + # Ironic no longer supports trusted boot + capabilities.pop('trusted_boot', None) + capabilities = utils.get_updated_capabilities( + node.properties.get('capabilities', ''), capabilities) + if capabilities: + props['capabilities'] = capabilities + + else: + props = _get_capabilities_properties_without_ipmi( + d_info, capabilities_props, + node.properties.get('capabilities', ''), props) macs = _get_mac_addresses(node) - except (scci.SCCIInvalidInputError, - scci.SCCIClientError, + except (irmc.scci.SCCIInvalidInputError, + irmc.scci.SCCIClientError, exception.SNMPFailure) as e: + advice = "" + if ("SNMP operation" in str(e)): + advice = ("The SNMP related parameters' value may be different " + "with the server, please check if you have set them " + "correctly.") error = (_("Inspection failed for node %(node_id)s " - "with the following error: %(error)s") % - {'node_id': node.uuid, 'error': e}) + "with the following error: %(error)s. (advice)s") % + {'node_id': node.uuid, 'error': e, 'advice': advice}) raise exception.HardwareInspectionFailure(error=error) return props, macs, new_traits diff --git a/ironic/drivers/modules/irmc/management.py b/ironic/drivers/modules/irmc/management.py index 079ae9e44..cf146f2cd 100644 --- a/ironic/drivers/modules/irmc/management.py +++ b/ironic/drivers/modules/irmc/management.py @@ -27,9 +27,10 @@ from ironic.conductor import task_manager from ironic.conductor import utils as manager_utils from ironic import conf from ironic.drivers import base +from ironic.drivers.modules import boot_mode_utils from ironic.drivers.modules import ipmitool from ironic.drivers.modules.irmc import common as irmc_common -from ironic.drivers import utils as driver_utils +from ironic.drivers.modules.redfish import management as redfish_management irmc = importutils.try_import('scciclient.irmc') @@ -204,7 +205,8 @@ def _restore_bios_config(task): manager_utils.node_power_action(task, states.POWER_ON) -class IRMCManagement(ipmitool.IPMIManagement): +class IRMCManagement(ipmitool.IPMIManagement, + redfish_management.RedfishManagement): def get_properties(self): """Return the properties of the interface. @@ -224,9 +226,30 @@ class IRMCManagement(ipmitool.IPMIManagement): :raises: InvalidParameterValue if required parameters are invalid. :raises: MissingParameterValue if a required parameter is missing. """ - irmc_common.parse_driver_info(task.node) - irmc_common.update_ipmi_properties(task) - super(IRMCManagement, self).validate(task) + if task.node.driver_internal_info.get('irmc_ipmi_succeed'): + irmc_common.parse_driver_info(task.node) + irmc_common.update_ipmi_properties(task) + super(IRMCManagement, self).validate(task) + else: + irmc_common.parse_driver_info(task.node) + super(ipmitool.IPMIManagement, self).validate(task) + + def get_supported_boot_devices(self, task): + """Get list of supported boot devices + + Actual code is delegated to IPMIManagement or RedfishManagement + based on iRMC firmware version. + + :param task: A TaskManager instance + :returns: A list with the supported boot devices defined + in :mod:`ironic.common.boot_devices`. + + """ + if task.node.driver_internal_info.get('irmc_ipmi_succeed'): + return super(IRMCManagement, self).get_supported_boot_devices(task) + else: + return super(ipmitool.IPMIManagement, + self).get_supported_boot_devices(task) @METRICS.timer('IRMCManagement.set_boot_device') @task_manager.require_exclusive_lock @@ -245,39 +268,112 @@ class IRMCManagement(ipmitool.IPMIManagement): specified. :raises: MissingParameterValue if a required parameter is missing. :raises: IPMIFailure on an error from ipmitool. + :raises: RedfishConnectionError on Redfish operation failure. + :raises: RedfishError on Redfish operation failure. + """ + if task.node.driver_internal_info.get('irmc_ipmi_succeed'): + if device not in self.get_supported_boot_devices(task): + raise exception.InvalidParameterValue(_( + "Invalid boot device %s specified.") % device) + + uefi_mode = ( + boot_mode_utils.get_boot_mode(task.node) == '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) + else: + if device not in self.get_supported_boot_devices(task): + raise exception.InvalidParameterValue(_( + "Invalid boot device %s specified. " + "Current iRMC firmware condition doesn't support IPMI " + "but Redfish.") % device) + super(ipmitool.IPMIManagement, self).set_boot_device( + task, device, persistent) + + def get_boot_device(self, task): + """Get the current boot device for the task's node. + Returns the current boot device of the node. + + :param task: a task from TaskManager. + :raises: InvalidParameterValue if an invalid boot device is + specified. + :raises: MissingParameterValue if a required parameter is missing. + :raises: IPMIFailure on an error from ipmitool. + :raises: RedfishConnectionError on Redfish operation failure. + :raises: RedfishError on Redfish operation failure. + :returns: a dictionary containing: + + :boot_device: the boot device, one of + :mod:`ironic.common.boot_devices` or None if it is unknown. + :persistent: Whether the boot device will persist to all + future boots or not, None if it is unknown. """ - 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' + if task.node.driver_internal_info.get('irmc_ipmi_succeed'): + return super(IRMCManagement, self).get_boot_device(task) else: - data1 = '0xa0' if uefi_mode else '0x80' - data2 = _BOOTPARAM5_DATA2[device] + return super( + ipmitool.IPMIManagement, self).get_boot_device(task) - cmd8 = bootparam5 % (data1, data2) - ipmitool.send_raw(task, cmd8) + def get_supported_boot_modes(self, task): + """Get a list of the supported boot modes. + + IRMCManagement class doesn't support this method + + :param task: a task from TaskManager. + :raises: UnsupportedDriverExtension if requested operation is + not supported by the driver + """ + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='get_supported_boot_modes') + + def set_boot_mode(self, task, mode): + """Set the boot mode for a node. + + IRMCManagement class doesn't support this method + + :param task: a task from TaskManager. + :param mode: The boot mode, one of + :mod:`ironic.common.boot_modes`. + :raises: UnsupportedDriverExtension if requested operation is + not supported by the driver + """ + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='set_boot_mode') + + def get_boot_mode(self, task): + """Get the current boot mode for a node. + + IRMCManagement class doesn't support this method + + :param task: a task from TaskManager. + :raises: UnsupportedDriverExtension if requested operation is + not supported by the driver + """ + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='get_boot_mode') @METRICS.timer('IRMCManagement.get_sensors_data') def get_sensors_data(self, task): @@ -330,7 +426,13 @@ class IRMCManagement(ipmitool.IPMIManagement): if sensor_method == 'scci': return _get_sensors_data(task) elif sensor_method == 'ipmitool': - return super(IRMCManagement, self).get_sensors_data(task) + if task.node.driver_internal_info.get('irmc_ipmi_succeed'): + return super(IRMCManagement, self).get_sensors_data(task) + else: + raise exception.InvalidParameterValue(_( + "Invalid sensor method %s specified. " + "IPMI operation doesn't work on current iRMC " + "condition.") % sensor_method) @METRICS.timer('IRMCManagement.inject_nmi') @task_manager.require_exclusive_lock @@ -401,3 +503,120 @@ class IRMCManagement(ipmitool.IPMIManagement): not supported by the driver or the hardware """ return irmc_common.set_secure_boot_mode(task.node, state) + + def get_supported_indicators(self, task, component=None): + """Get a map of the supported indicators (e.g. LEDs). + + IRMCManagement class doesn't support this method + + :param task: a task from TaskManager. + :param component: If not `None`, return indicator information + for just this component, otherwise return indicators for + all existing components. + :raises: UnsupportedDriverExtension if requested operation is + not supported by the driver + + """ + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='get_supported_indicators') + + def set_indicator_state(self, task, component, indicator, state): + """Set indicator on the hardware component to the desired state. + + IRMCManagement class doesn't support this method + + :param task: A task from TaskManager. + :param component: The hardware component, one of + :mod:`ironic.common.components`. + :param indicator: Indicator ID (as reported by + `get_supported_indicators`). + :state: Desired state of the indicator, one of + :mod:`ironic.common.indicator_states`. + :raises: UnsupportedDriverExtension if requested operation is + not supported by the driver + """ + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='set_indicator_state') + + def get_indicator_state(self, task, component, indicator): + """Get current state of the indicator of the hardware component. + + IRMCManagement class doesn't support this method + + :param task: A task from TaskManager. + :param component: The hardware component, one of + :mod:`ironic.common.components`. + :param indicator: Indicator ID (as reported by + `get_supported_indicators`). + :raises: UnsupportedDriverExtension if requested operation is + not supported by the driver + """ + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='get_indicator_state') + + def detect_vendor(self, task): + """Detects and returns the hardware vendor. + + :param task: A task from TaskManager. + :raises: InvalidParameterValue if a required parameter is missing + :raises: MissingParameterValue if a required parameter is missing + :raises: RedfishError on Redfish operation error. + :raises: PasswordFileFailedToCreate from creating or writing to the + temporary file during IPMI operation. + :raises: processutils.ProcessExecutionError from executing ipmi command + :returns: String representing the BMC reported Vendor or + Manufacturer, otherwise returns None. + """ + if task.node.driver_internal_info.get('irmc_ipmi_succeed'): + return super(IRMCManagement, self).detect_vendor(task) + else: + return super(ipmitool.IPMIManagement, self).detect_vendor(task) + + def get_mac_addresses(self, task): + """Get MAC address information for the node. + + IRMCManagement class doesn't support this method + + :param task: A TaskManager instance containing the node to act on. + :raises: UnsupportedDriverExtension + """ + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='get_mac_addresses') + + @base.verify_step(priority=10) + def verify_http_https_connection_and_fw_version(self, task): + """Check http(s) connection to iRMC and save fw version + + :param task' A task from TaskManager + 'raises: IRMCOperationError + """ + error_msg_https = ('Access to REST API returns unexpected ' + 'status code. Check driver_info parameter ' + 'related to iRMC driver') + error_msg_http = ('Access to REST API returns unexpected ' + 'status code. Check driver_info parameter ' + 'or version of iRMC because iRMC does not ' + 'support HTTP connection to iRMC REST API ' + 'since iRMC S6 2.00.') + try: + # Check connection to iRMC + elcm_license = irmc_common.check_elcm_license(task.node) + + # On iRMC S6 2.00, access to REST API through HTTP returns 404 + if elcm_license.get('status_code') not in (200, 500): + port = task.node.driver_info.get( + 'irmc_port', CONF.irmc.get('port')) + if port == 80: + e_msg = error_msg_http + else: + e_msg = error_msg_https + raise exception.IRMCOperationError( + operation='establishing connection to REST API', + error=e_msg) + + irmc_common.set_irmc_version(task) + except (exception.InvalidParameterValue, + exception.MissingParameterValue) as irmc_exception: + raise exception.IRMCOperationError( + operation='configuration validation', + error=irmc_exception) diff --git a/ironic/drivers/modules/irmc/power.py b/ironic/drivers/modules/irmc/power.py index 28041d835..48f7ea321 100644 --- a/ironic/drivers/modules/irmc/power.py +++ b/ironic/drivers/modules/irmc/power.py @@ -29,6 +29,7 @@ from ironic.drivers import base from ironic.drivers.modules import ipmitool from ironic.drivers.modules.irmc import boot as irmc_boot from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules.redfish import power as redfish_power from ironic.drivers.modules import snmp scci = importutils.try_import('scciclient.irmc.scci') @@ -203,14 +204,17 @@ def _set_power_state(task, target_state, timeout=None): _wait_power_state(task, states.SOFT_REBOOT, timeout=timeout) except exception.SNMPFailure as snmp_exception: + advice = ("The SNMP related parameters' value may be different with " + "the server, please check if you have set them correctly.") LOG.error("iRMC failed to acknowledge the target state " - "for node %(node_id)s. Error: %(error)s", - {'node_id': node.uuid, 'error': snmp_exception}) + "for node %(node_id)s. Error: %(error)s. %(advice)s", + {'node_id': node.uuid, 'error': snmp_exception, + 'advice': advice}) raise exception.IRMCOperationError(operation=target_state, error=snmp_exception) -class IRMCPower(base.PowerInterface): +class IRMCPower(redfish_power.RedfishPower, base.PowerInterface): """Interface for power-related actions.""" def get_properties(self): @@ -233,7 +237,19 @@ class IRMCPower(base.PowerInterface): is missing or invalid on the node. :raises: MissingParameterValue if a required parameter is missing. """ - irmc_common.parse_driver_info(task.node) + # validate method of power interface is called at very first point + # in verifying. + # We take try-fallback approach against iRMC S6 2.00 and later + # incompatibility in which iRMC firmware disables IPMI by default. + # get_power_state method first try IPMI and if fails try Redfish + # along with setting irmc_ipmi_succeed flag to indicate if IPMI works. + if (task.node.driver_internal_info.get('irmc_ipmi_succeed') + or (task.node.driver_internal_info.get('irmc_ipmi_succeed') + is None)): + irmc_common.parse_driver_info(task.node) + else: + irmc_common.parse_driver_info(task.node) + super(IRMCPower, self).validate(task) @METRICS.timer('IRMCPower.get_power_state') def get_power_state(self, task): @@ -241,14 +257,40 @@ class IRMCPower(base.PowerInterface): :param task: a TaskManager instance containing the node to act on. :returns: a power state. One of :mod:`ironic.common.states`. - :raises: InvalidParameterValue if required ipmi parameters are missing. - :raises: MissingParameterValue if a required parameter is missing. - :raises: IPMIFailure on an error from ipmitool (from _power_status - call). + :raises: InvalidParameterValue if required parameters are incorrect. + :raises: MissingParameterValue if required parameters are missing. + :raises: IRMCOperationError If IPMI or Redfish operation fails """ - irmc_common.update_ipmi_properties(task) - ipmi_power = ipmitool.IPMIPower() - return ipmi_power.get_power_state(task) + # If IPMI operation failed, iRMC may not enable/support IPMI, + # so fallback to Redfish. + # get_power_state is called at verifying and is called periodically + # so this method is good choice to determine IPMI enablement. + try: + irmc_common.update_ipmi_properties(task) + ipmi_power = ipmitool.IPMIPower() + pw_state = ipmi_power.get_power_state(task) + if (task.node.driver_internal_info.get('irmc_ipmi_succeed') + is not True): + task.upgrade_lock(purpose='update irmc_ipmi_succeed flag', + retry=True) + task.node.set_driver_internal_info('irmc_ipmi_succeed', True) + task.node.save() + task.downgrade_lock() + return pw_state + except exception.IPMIFailure: + if (task.node.driver_internal_info.get('irmc_ipmi_succeed') + is not False): + task.upgrade_lock(purpose='update irmc_ipmi_succeed flag', + retry=True) + task.node.set_driver_internal_info('irmc_ipmi_succeed', False) + task.node.save() + task.downgrade_lock() + try: + return super(IRMCPower, self).get_power_state(task) + except (exception.RedfishConnectionError, + exception.RedfishError): + raise exception.IRMCOperationError( + operation='IPMI try and Redfish fallback operation') @METRICS.timer('IRMCPower.set_power_state') @task_manager.require_exclusive_lock diff --git a/ironic/drivers/modules/irmc/vendor.py b/ironic/drivers/modules/irmc/vendor.py new file mode 100644 index 000000000..35535f69d --- /dev/null +++ b/ironic/drivers/modules/irmc/vendor.py @@ -0,0 +1,75 @@ +# Copyright 2022 FUJITSU LIMITED +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Vendor interface of iRMC driver +""" + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.drivers import base +from ironic.drivers.modules.irmc import common as irmc_common + + +class IRMCVendorPassthru(base.VendorInterface): + def get_properties(self): + """Return the properties of the interface. + + :returns: Dictionary of <property name>:<property description> entries. + """ + return irmc_common.COMMON_PROPERTIES + + def validate(self, task, method=None, **kwargs): + """Validate vendor-specific actions. + + This method validates whether the 'driver_info' property of the + supplied node contains the required information for this driver. + + :param task: An instance of TaskManager. + :param method: Name of vendor passthru method + :raises: InvalidParameterValue if invalid value is contained + in the 'driver_info' property. + :raises: MissingParameterValue if some mandatory key is missing + in the 'driver_info' property. + """ + irmc_common.parse_driver_info(task.node) + + @base.passthru(['POST'], + async_call=True, + description='Connect to iRMC and fetch iRMC firmware ' + 'version and, if firmware version has not been cached ' + 'in or actual firmware version is different from one in ' + 'driver_internal_info/irmc_fw_version, store firmware ' + 'version in driver_internal_info/irmc_fw_version.', + attach=False, + require_exclusive_lock=False) + def cache_irmc_firmware_version(self, task, **kwargs): + """Fetch and save iRMC firmware version. + + This method connects to iRMC and fetch iRMC firmware verison. + If fetched firmware version is not cached in or is different from + one in driver_internal_info/irmc_fw_version, store fetched version + in driver_internal_info/irmc_fw_version. + + :param task: An instance of TaskManager. + :raises: IRMCOperationError if some error occurs + """ + try: + irmc_common.set_irmc_version(task) + except (exception.IRMCOperationError, + exception.InvalidParameterValue, + exception.MissingParameterValue, + exception.NodeLocked) as e: + raise exception.IRMCOperationError( + operation=_('caching firmware version'), error=e) |