"Fossies" - the Fresh Open Source Software Archive

Member "ironic-16.0.3/ironic/drivers/modules/redfish/management.py" (18 Jan 2021, 41303 Bytes) of package /linux/misc/openstack/ironic-16.0.3.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "management.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 16.0.2_vs_16.0.3.

    1 # Copyright 2017 Red Hat, Inc.
    2 # All Rights Reserved.
    3 #
    4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    5 #    not use this file except in compliance with the License. You may obtain
    6 #    a copy of the License at
    7 #
    8 #         http://www.apache.org/licenses/LICENSE-2.0
    9 #
   10 #    Unless required by applicable law or agreed to in writing, software
   11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   13 #    License for the specific language governing permissions and limitations
   14 #    under the License.
   15 
   16 import collections
   17 
   18 from futurist import periodics
   19 from ironic_lib import metrics_utils
   20 from oslo_log import log
   21 from oslo_utils import importutils
   22 from oslo_utils import timeutils
   23 
   24 from ironic.common import boot_devices
   25 from ironic.common import boot_modes
   26 from ironic.common import components
   27 from ironic.common import exception
   28 from ironic.common.i18n import _
   29 from ironic.common import indicator_states
   30 from ironic.common import states
   31 from ironic.common import utils
   32 from ironic.conductor import task_manager
   33 from ironic.conductor import utils as manager_utils
   34 from ironic.conf import CONF
   35 from ironic.drivers import base
   36 from ironic.drivers.modules import boot_mode_utils
   37 from ironic.drivers.modules import deploy_utils
   38 from ironic.drivers.modules.redfish import utils as redfish_utils
   39 
   40 LOG = log.getLogger(__name__)
   41 METRICS = metrics_utils.get_metrics_logger(__name__)
   42 
   43 sushy = importutils.try_import('sushy')
   44 
   45 if sushy:
   46     BOOT_DEVICE_MAP = {
   47         sushy.BOOT_SOURCE_TARGET_PXE: boot_devices.PXE,
   48         sushy.BOOT_SOURCE_TARGET_HDD: boot_devices.DISK,
   49         sushy.BOOT_SOURCE_TARGET_CD: boot_devices.CDROM,
   50         sushy.BOOT_SOURCE_TARGET_BIOS_SETUP: boot_devices.BIOS
   51     }
   52 
   53     BOOT_DEVICE_MAP_REV = {v: k for k, v in BOOT_DEVICE_MAP.items()}
   54 
   55     BOOT_MODE_MAP = {
   56         sushy.BOOT_SOURCE_MODE_UEFI: boot_modes.UEFI,
   57         sushy.BOOT_SOURCE_MODE_BIOS: boot_modes.LEGACY_BIOS
   58     }
   59 
   60     BOOT_MODE_MAP_REV = {v: k for k, v in BOOT_MODE_MAP.items()}
   61 
   62     BOOT_DEVICE_PERSISTENT_MAP = {
   63         sushy.BOOT_SOURCE_ENABLED_CONTINUOUS: True,
   64         sushy.BOOT_SOURCE_ENABLED_ONCE: False
   65     }
   66 
   67     BOOT_DEVICE_PERSISTENT_MAP_REV = {v: k for k, v in
   68                                       BOOT_DEVICE_PERSISTENT_MAP.items()}
   69 
   70     INDICATOR_MAP = {
   71         sushy.INDICATOR_LED_LIT: indicator_states.ON,
   72         sushy.INDICATOR_LED_OFF: indicator_states.OFF,
   73         sushy.INDICATOR_LED_BLINKING: indicator_states.BLINKING,
   74         sushy.INDICATOR_LED_UNKNOWN: indicator_states.UNKNOWN
   75     }
   76 
   77     INDICATOR_MAP_REV = {
   78         v: k for k, v in INDICATOR_MAP.items()}
   79 
   80 
   81 def _set_boot_device(task, system, device, persistent=False):
   82     """An internal routine to set the boot device.
   83 
   84     :param task: a task from TaskManager.
   85     :param system: a Redfish System object.
   86     :param device: the Redfish boot device.
   87     :param persistent: Boolean value. True if the boot device will
   88                        persist to all future boots, False if not.
   89                        Default: False.
   90     :raises: SushyError on an error from the Sushy library
   91     """
   92     desired_enabled = BOOT_DEVICE_PERSISTENT_MAP_REV[persistent]
   93     current_enabled = system.boot.get('enabled')
   94 
   95     # NOTE(etingof): this can be racy, esp if BMC is not RESTful
   96     enabled = (desired_enabled
   97                if desired_enabled != current_enabled else None)
   98 
   99     try:
  100         system.set_system_boot_options(device, enabled=enabled)
  101     except sushy.exceptions.SushyError as e:
  102         if enabled == sushy.BOOT_SOURCE_ENABLED_CONTINUOUS:
  103             # NOTE(dtantsur): continuous boot device settings have been
  104             # removed from Redfish, and some vendors stopped supporting
  105             # it before an alternative was provided. As a work around,
  106             # use one-time boot and restore the boot device on every
  107             # reboot via RedfishPower.
  108             LOG.debug('Error %(error)s when trying to set a '
  109                       'persistent boot device on node %(node)s, '
  110                       'falling back to one-time boot settings',
  111                       {'error': e, 'node': task.node.uuid})
  112             system.set_system_boot_options(
  113                 device, enabled=sushy.BOOT_SOURCE_ENABLED_ONCE)
  114             LOG.warning('Could not set persistent boot device to '
  115                         '%(dev)s for node %(node)s, using one-time '
  116                         'boot device instead',
  117                         {'dev': device, 'node': task.node.uuid})
  118             utils.set_node_nested_field(
  119                 task.node, 'driver_internal_info',
  120                 'redfish_boot_device', device)
  121             task.node.save()
  122         else:
  123             raise
  124 
  125 
  126 class RedfishManagement(base.ManagementInterface):
  127 
  128     def __init__(self):
  129         """Initialize the Redfish management interface.
  130 
  131         :raises: DriverLoadError if the driver can't be loaded due to
  132             missing dependencies
  133         """
  134         super(RedfishManagement, self).__init__()
  135         if not sushy:
  136             raise exception.DriverLoadError(
  137                 driver='redfish',
  138                 reason=_('Unable to import the sushy library'))
  139 
  140     def get_properties(self):
  141         """Return the properties of the interface.
  142 
  143         :returns: dictionary of <property name>:<property description> entries.
  144         """
  145         return redfish_utils.COMMON_PROPERTIES.copy()
  146 
  147     def validate(self, task):
  148         """Validates the driver information needed by the redfish driver.
  149 
  150         :param task: a TaskManager instance containing the node to act on.
  151         :raises: InvalidParameterValue on malformed parameter(s)
  152         :raises: MissingParameterValue on missing parameter(s)
  153         """
  154         redfish_utils.parse_driver_info(task.node)
  155 
  156     def get_supported_boot_devices(self, task):
  157         """Get a list of the supported boot devices.
  158 
  159         :param task: a task from TaskManager.
  160         :returns: A list with the supported boot devices defined
  161                   in :mod:`ironic.common.boot_devices`.
  162         """
  163         return list(BOOT_DEVICE_MAP_REV)
  164 
  165     @task_manager.require_exclusive_lock
  166     def restore_boot_device(self, task, system):
  167         """Restore boot device if needed.
  168 
  169         Checks the redfish_boot_device internal flag and sets the one-time
  170         boot device accordingly. A warning is issued if it fails.
  171 
  172         This method is supposed to be called from the Redfish power interface
  173         and should be considered private to the Redfish hardware type.
  174 
  175         :param task: a task from TaskManager.
  176         :param system: a Redfish System object.
  177         """
  178         device = task.node.driver_internal_info.get('redfish_boot_device')
  179         if not device:
  180             return
  181 
  182         LOG.debug('Restoring boot device %(dev)s on node %(node)s',
  183                   {'dev': device, 'node': task.node.uuid})
  184         try:
  185             _set_boot_device(task, system, device)
  186         except sushy.exceptions.SushyError as e:
  187             LOG.warning('Unable to recover boot device %(dev)s for node '
  188                         '%(node)s, relying on the pre-configured boot order. '
  189                         'Error: %(error)s',
  190                         {'dev': device, 'node': task.node.uuid, 'error': e})
  191 
  192     @task_manager.require_exclusive_lock
  193     def set_boot_device(self, task, device, persistent=False):
  194         """Set the boot device for a node.
  195 
  196         Set the boot device to use on next reboot of the node.
  197 
  198         :param task: a task from TaskManager.
  199         :param device: the boot device, one of
  200                        :mod:`ironic.common.boot_devices`.
  201         :param persistent: Boolean value. True if the boot device will
  202                            persist to all future boots, False if not.
  203                            Default: False.
  204         :raises: InvalidParameterValue on malformed parameter(s)
  205         :raises: MissingParameterValue on missing parameter(s)
  206         :raises: RedfishConnectionError when it fails to connect to Redfish
  207         :raises: RedfishError on an error from the Sushy library
  208         """
  209         utils.pop_node_nested_field(
  210             task.node, 'driver_internal_info', 'redfish_boot_device')
  211         task.node.save()
  212 
  213         system = redfish_utils.get_system(task.node)
  214 
  215         try:
  216             _set_boot_device(
  217                 task, system, BOOT_DEVICE_MAP_REV[device],
  218                 persistent=persistent)
  219         except sushy.exceptions.SushyError as e:
  220             error_msg = (_('Redfish set boot device failed for node '
  221                            '%(node)s. Error: %(error)s') %
  222                          {'node': task.node.uuid, 'error': e})
  223             LOG.error(error_msg)
  224             raise exception.RedfishError(error=error_msg)
  225 
  226         # Ensure that boot mode is synced with what is set.
  227         # Some BMCs reset it to default (BIOS) when changing the boot device.
  228         boot_mode_utils.sync_boot_mode(task)
  229 
  230     def get_boot_device(self, task):
  231         """Get the current boot device for a node.
  232 
  233         :param task: a task from TaskManager.
  234         :raises: InvalidParameterValue on malformed parameter(s)
  235         :raises: MissingParameterValue on missing parameter(s)
  236         :raises: RedfishConnectionError when it fails to connect to Redfish
  237         :raises: RedfishError on an error from the Sushy library
  238         :returns: a dictionary containing:
  239 
  240             :boot_device:
  241                 the boot device, one of :mod:`ironic.common.boot_devices` or
  242                 None if it is unknown.
  243             :persistent:
  244                 Boolean value or None, True if the boot device persists,
  245                 False otherwise. None if it's unknown.
  246 
  247 
  248         """
  249         system = redfish_utils.get_system(task.node)
  250         return {'boot_device': BOOT_DEVICE_MAP.get(system.boot.get('target')),
  251                 'persistent': BOOT_DEVICE_PERSISTENT_MAP.get(
  252                     system.boot.get('enabled'))}
  253 
  254     def get_supported_boot_modes(self, task):
  255         """Get a list of the supported boot modes.
  256 
  257         :param task: A task from TaskManager.
  258         :returns: A list with the supported boot modes defined
  259                   in :mod:`ironic.common.boot_modes`. If boot
  260                   mode support can't be determined, empty list
  261                   is returned.
  262         """
  263         return list(BOOT_MODE_MAP_REV)
  264 
  265     @task_manager.require_exclusive_lock
  266     def set_boot_mode(self, task, mode):
  267         """Set the boot mode for a node.
  268 
  269         Set the boot mode to use on next reboot of the node.
  270 
  271         :param task: A task from TaskManager.
  272         :param mode: The boot mode, one of
  273                      :mod:`ironic.common.boot_modes`.
  274         :raises: InvalidParameterValue if an invalid boot mode is
  275                  specified.
  276         :raises: MissingParameterValue if a required parameter is missing
  277         :raises: RedfishConnectionError when it fails to connect to Redfish
  278         :raises: RedfishError on an error from the Sushy library
  279         """
  280         system = redfish_utils.get_system(task.node)
  281 
  282         try:
  283             system.set_system_boot_options(mode=BOOT_MODE_MAP_REV[mode])
  284 
  285         except sushy.exceptions.SushyError as e:
  286             error_msg = (_('Setting boot mode to %(mode)s '
  287                            'failed for node %(node)s. '
  288                            'Error: %(error)s') %
  289                          {'node': task.node.uuid, 'mode': mode,
  290                           'error': e})
  291             LOG.error(error_msg)
  292             raise exception.RedfishError(error=error_msg)
  293 
  294     def get_boot_mode(self, task):
  295         """Get the current boot mode for a node.
  296 
  297         Provides the current boot mode of the node.
  298 
  299         :param task: A task from TaskManager.
  300         :raises: MissingParameterValue if a required parameter is missing
  301         :raises: DriverOperationError or its  derivative in case
  302                  of driver runtime error.
  303         :returns: The boot mode, one of :mod:`ironic.common.boot_mode` or
  304                   None if it is unknown.
  305         """
  306         system = redfish_utils.get_system(task.node)
  307 
  308         return BOOT_MODE_MAP.get(system.boot.get('mode'))
  309 
  310     @staticmethod
  311     def _sensor2dict(resource, *fields):
  312         return {field: getattr(resource, field)
  313                 for field in fields
  314                 if hasattr(resource, field)}
  315 
  316     @classmethod
  317     def _get_sensors_fan(cls, chassis):
  318         """Get fan sensors reading.
  319 
  320         :param chassis: Redfish `chassis` object
  321         :returns: returns a dict of sensor data.
  322         """
  323         sensors = {}
  324 
  325         for fan in chassis.thermal.fans:
  326             sensor = cls._sensor2dict(
  327                 fan, 'identity', 'max_reading_range',
  328                 'min_reading_range', 'reading', 'reading_units',
  329                 'serial_number', 'physical_context')
  330             sensor.update(cls._sensor2dict(fan.status, 'state', 'health'))
  331             unique_name = '%s@%s' % (fan.identity, chassis.identity)
  332             sensors[unique_name] = sensor
  333 
  334         return sensors
  335 
  336     @classmethod
  337     def _get_sensors_temperatures(cls, chassis):
  338         """Get temperature sensors reading.
  339 
  340         :param chassis: Redfish `chassis` object
  341         :returns: returns a dict of sensor data.
  342         """
  343         sensors = {}
  344 
  345         for temps in chassis.thermal.temperatures:
  346             sensor = cls._sensor2dict(
  347                 temps, 'identity', 'max_reading_range_temp',
  348                 'min_reading_range_temp', 'reading_celsius',
  349                 'physical_context', 'sensor_number')
  350             sensor.update(cls._sensor2dict(temps.status, 'state', 'health'))
  351             unique_name = '%s@%s' % (temps.identity, chassis.identity)
  352             sensors[unique_name] = sensor
  353 
  354         return sensors
  355 
  356     @classmethod
  357     def _get_sensors_power(cls, chassis):
  358         """Get power supply sensors reading.
  359 
  360         :param chassis: Redfish `chassis` object
  361         :returns: returns a dict of sensor data.
  362         """
  363         sensors = {}
  364 
  365         for power in chassis.power.power_supplies:
  366             sensor = cls._sensor2dict(
  367                 power, 'power_capacity_watts',
  368                 'line_input_voltage', 'last_power_output_watts',
  369                 'serial_number')
  370             sensor.update(cls._sensor2dict(power.status, 'state', 'health'))
  371             sensor.update(cls._sensor2dict(
  372                 power.input_ranges, 'minimum_voltage',
  373                 'maximum_voltage', 'minimum_frequency_hz',
  374                 'maximum_frequency_hz', 'output_wattage'))
  375             unique_name = '%s:%s@%s' % (
  376                 power.identity, chassis.power.identity,
  377                 chassis.identity)
  378             sensors[unique_name] = sensor
  379 
  380         return sensors
  381 
  382     @classmethod
  383     def _get_sensors_drive(cls, system):
  384         """Get storage drive sensors reading.
  385 
  386         :param chassis: Redfish `system` object
  387         :returns: returns a dict of sensor data.
  388         """
  389         sensors = {}
  390 
  391         for storage in system.simple_storage.get_members():
  392             for drive in storage.devices:
  393                 sensor = cls._sensor2dict(
  394                     drive, 'name', 'model', 'capacity_bytes')
  395                 sensor.update(
  396                     cls._sensor2dict(drive.status, 'state', 'health'))
  397                 unique_name = '%s:%s@%s' % (
  398                     drive.name, storage.identity, system.identity)
  399                 sensors[unique_name] = sensor
  400 
  401         return sensors
  402 
  403     def get_sensors_data(self, task):
  404         """Get sensors data.
  405 
  406         :param task: a TaskManager instance.
  407         :raises: FailedToGetSensorData when getting the sensor data fails.
  408         :raises: FailedToParseSensorData when parsing sensor data fails.
  409         :raises: InvalidParameterValue if required parameters
  410                  are missing.
  411         :raises: MissingParameterValue if a required parameter is missing.
  412         :returns: returns a dict of sensor data grouped by sensor type.
  413         """
  414         node = task.node
  415 
  416         sensors = collections.defaultdict(dict)
  417 
  418         system = redfish_utils.get_system(node)
  419 
  420         for chassis in system.chassis:
  421             try:
  422                 sensors['Fan'].update(self._get_sensors_fan(chassis))
  423 
  424             except sushy.exceptions.SushyError as exc:
  425                 LOG.debug("Failed reading fan information for node "
  426                           "%(node)s: %(error)s", {'node': node.uuid,
  427                                                   'error': exc})
  428 
  429             try:
  430                 sensors['Temperature'].update(
  431                     self._get_sensors_temperatures(chassis))
  432 
  433             except sushy.exceptions.SushyError as exc:
  434                 LOG.debug("Failed reading temperature information for node "
  435                           "%(node)s: %(error)s", {'node': node.uuid,
  436                                                   'error': exc})
  437 
  438             try:
  439                 sensors['Power'].update(self._get_sensors_power(chassis))
  440 
  441             except sushy.exceptions.SushyError as exc:
  442                 LOG.debug("Failed reading power information for node "
  443                           "%(node)s: %(error)s", {'node': node.uuid,
  444                                                   'error': exc})
  445 
  446         try:
  447             sensors['Drive'].update(self._get_sensors_drive(system))
  448 
  449         except sushy.exceptions.SushyError as exc:
  450             LOG.debug("Failed reading drive information for node "
  451                       "%(node)s: %(error)s", {'node': node.uuid,
  452                                               'error': exc})
  453 
  454         LOG.debug("Gathered sensor data: %(sensors)s", {'sensors': sensors})
  455 
  456         return sensors
  457 
  458     @task_manager.require_exclusive_lock
  459     def inject_nmi(self, task):
  460         """Inject NMI, Non Maskable Interrupt.
  461 
  462         Inject NMI (Non Maskable Interrupt) for a node immediately.
  463 
  464         :param task: A TaskManager instance containing the node to act on.
  465         :raises: InvalidParameterValue on malformed parameter(s)
  466         :raises: MissingParameterValue on missing parameter(s)
  467         :raises: RedfishConnectionError when it fails to connect to Redfish
  468         :raises: RedfishError on an error from the Sushy library
  469         """
  470         system = redfish_utils.get_system(task.node)
  471         try:
  472             system.reset_system(sushy.RESET_NMI)
  473         except sushy.exceptions.SushyError as e:
  474             error_msg = (_('Redfish inject NMI failed for node %(node)s. '
  475                            'Error: %(error)s') % {'node': task.node.uuid,
  476                                                   'error': e})
  477             LOG.error(error_msg)
  478             raise exception.RedfishError(error=error_msg)
  479 
  480     def get_supported_indicators(self, task, component=None):
  481         """Get a map of the supported indicators (e.g. LEDs).
  482 
  483         :param task: A task from TaskManager.
  484         :param component: If not `None`, return indicator information
  485             for just this component, otherwise return indicators for
  486             all existing components.
  487         :returns: A dictionary of hardware components
  488             (:mod:`ironic.common.components`) as keys with values
  489             being dictionaries having indicator IDs as keys and indicator
  490             properties as values.
  491 
  492             ::
  493 
  494              {
  495                  'chassis': {
  496                      'enclosure-0': {
  497                          "readonly": true,
  498                          "states": [
  499                              "OFF",
  500                              "ON"
  501                          ]
  502                      }
  503                  },
  504                  'system':
  505                      'blade-A': {
  506                          "readonly": true,
  507                          "states": [
  508                              "OFF",
  509                              "ON"
  510                          ]
  511                      }
  512                  },
  513                  'drive':
  514                      'ssd0': {
  515                          "readonly": true,
  516                          "states": [
  517                              "OFF",
  518                              "ON"
  519                          ]
  520                      }
  521                  }
  522              }
  523         """
  524         properties = {
  525             "readonly": False,
  526             "states": [
  527                 indicator_states.BLINKING,
  528                 indicator_states.OFF,
  529                 indicator_states.ON
  530             ]
  531         }
  532 
  533         indicators = {}
  534 
  535         system = redfish_utils.get_system(task.node)
  536 
  537         try:
  538             if component in (None, components.CHASSIS) and system.chassis:
  539                 indicators[components.CHASSIS] = {
  540                     chassis.uuid: properties for chassis in system.chassis
  541                     if chassis.indicator_led
  542                 }
  543 
  544         except sushy.exceptions.SushyError as e:
  545             LOG.debug('Chassis indicator not available for node %(node)s: '
  546                       '%(error)s', {'node': task.node.uuid, 'error': e})
  547 
  548         try:
  549             if component in (None, components.SYSTEM) and system.indicator_led:
  550                 indicators[components.SYSTEM] = {
  551                     system.uuid: properties
  552                 }
  553 
  554         except sushy.exceptions.SushyError as e:
  555             LOG.debug('System indicator not available for node %(node)s: '
  556                       '%(error)s', {'node': task.node.uuid, 'error': e})
  557 
  558         try:
  559             if (component in (None, components.DISK)
  560                     and system.simple_storage
  561                     and system.simple_storage.drives):
  562                 indicators[components.DISK] = {
  563                     drive.uuid: properties
  564                     for drive in system.simple_storage.drives
  565                     if drive.indicator_led
  566                 }
  567 
  568         except sushy.exceptions.SushyError as e:
  569             LOG.debug('Drive indicator not available for node %(node)s: '
  570                       '%(error)s', {'node': task.node.uuid, 'error': e})
  571 
  572         return indicators
  573 
  574     def set_indicator_state(self, task, component, indicator, state):
  575         """Set indicator on the hardware component to the desired state.
  576 
  577         :param task: A task from TaskManager.
  578         :param component: The hardware component, one of
  579             :mod:`ironic.common.components`.
  580         :param indicator: Indicator ID (as reported by
  581             `get_supported_indicators`).
  582         :param state: Desired state of the indicator, one of
  583             :mod:`ironic.common.indicator_states`.
  584         :raises: InvalidParameterValue if an invalid component, indicator
  585                  or state is specified.
  586         :raises: MissingParameterValue if a required parameter is missing
  587         :raises: RedfishError on an error from the Sushy library
  588         """
  589         system = redfish_utils.get_system(task.node)
  590 
  591         try:
  592             if (component == components.SYSTEM
  593                     and indicator == system.uuid):
  594                 system.set_indicator_led(INDICATOR_MAP_REV[state])
  595                 return
  596 
  597             elif (component == components.CHASSIS
  598                     and system.chassis):
  599                 for chassis in system.chassis:
  600                     if chassis.uuid == indicator:
  601                         chassis.set_indicator_led(
  602                             INDICATOR_MAP_REV[state])
  603                         return
  604 
  605             elif (component == components.DISK
  606                   and system.simple_storage
  607                   and system.simple_storage.drives):
  608                 for drive in system.simple_storage.drives:
  609                     if drive.uuid == indicator:
  610                         drive.set_indicator_led(
  611                             INDICATOR_MAP_REV[state])
  612                         return
  613 
  614         except sushy.exceptions.SushyError as e:
  615             error_msg = (_('Redfish set %(component)s indicator %(indicator)s '
  616                            'state %(state)s failed for node %(node)s. Error: '
  617                            '%(error)s') % {'component': component,
  618                                            'indicator': indicator,
  619                                            'state': state,
  620                                            'node': task.node.uuid,
  621                                            'error': e})
  622             LOG.error(error_msg)
  623             raise exception.RedfishError(error=error_msg)
  624 
  625         raise exception.MissingParameterValue(_(
  626             "Unknown indicator %(indicator)s for component %(component)s of "
  627             "node %(uuid)s") % {'indicator': indicator,
  628                                 'component': component,
  629                                 'uuid': task.node.uuid})
  630 
  631     def get_indicator_state(self, task, component, indicator):
  632         """Get current state of the indicator of the hardware component.
  633 
  634         :param task: A task from TaskManager.
  635         :param component: The hardware component, one of
  636             :mod:`ironic.common.components`.
  637         :param indicator: Indicator ID (as reported by
  638             `get_supported_indicators`).
  639         :raises: MissingParameterValue if a required parameter is missing
  640         :raises: RedfishError on an error from the Sushy library
  641         :returns: Current state of the indicator, one of
  642             :mod:`ironic.common.indicator_states`.
  643         """
  644         system = redfish_utils.get_system(task.node)
  645 
  646         try:
  647             if (component == components.SYSTEM
  648                     and indicator == system.uuid):
  649                 return INDICATOR_MAP[system.indicator_led]
  650 
  651             if (component == components.CHASSIS
  652                     and system.chassis):
  653                 for chassis in system.chassis:
  654                     if chassis.uuid == indicator:
  655                         return INDICATOR_MAP[chassis.indicator_led]
  656 
  657             if (component == components.DISK
  658                     and system.simple_storage
  659                     and system.simple_storage.drives):
  660                 for drive in system.simple_storage.drives:
  661                     if drive.uuid == indicator:
  662                         return INDICATOR_MAP[drive.indicator_led]
  663 
  664         except sushy.exceptions.SushyError as e:
  665             error_msg = (_('Redfish get %(component)s indicator %(indicator)s '
  666                            'state failed for node %(node)s. Error: '
  667                            '%(error)s') % {'component': component,
  668                                            'indicator': indicator,
  669                                            'node': task.node.uuid,
  670                                            'error': e})
  671             LOG.error(error_msg)
  672             raise exception.RedfishError(error=error_msg)
  673 
  674         raise exception.MissingParameterValue(_(
  675             "Unknown indicator %(indicator)s for component %(component)s of "
  676             "node %(uuid)s") % {'indicator': indicator,
  677                                 'component': component,
  678                                 'uuid': task.node.uuid})
  679 
  680     @METRICS.timer('RedfishManagement.update_firmware')
  681     @base.clean_step(priority=0, abortable=False, argsinfo={
  682         'firmware_images': {
  683             'description': (
  684                 'A list of firmware images to apply.'
  685             ),
  686             'required': True
  687         }})
  688     def update_firmware(self, task, firmware_images):
  689         """Updates the firmware on the node.
  690 
  691         :param task: a TaskManager instance containing the node to act on.
  692         :param firmware_images: A list of firmware images are to apply.
  693         :returns: None if it is completed.
  694         :raises: RedfishError on an error from the Sushy library.
  695         """
  696         node = task.node
  697 
  698         LOG.debug('Updating firmware on node %(node_uuid)s with firmware '
  699                   '%(firmware_images)s',
  700                   {'node_uuid': node.uuid,
  701                    'firmware_images': firmware_images})
  702 
  703         update_service = redfish_utils.get_update_service(task.node)
  704 
  705         # The cleaning infrastructure has an exclusive lock on the node, so
  706         # there is no need to get one here.
  707         self._apply_firmware_update(node, update_service, firmware_images)
  708 
  709         # set_async_step_flags calls node.save()
  710         deploy_utils.set_async_step_flags(
  711             node,
  712             reboot=True,
  713             skip_current_step=True,
  714             polling=True)
  715 
  716         manager_utils.node_power_action(task, states.REBOOT)
  717 
  718         return deploy_utils.get_async_step_return_state(task.node)
  719 
  720     def _apply_firmware_update(self, node, update_service, firmware_updates):
  721         """Applies the next firmware update to the node
  722 
  723         Applies the first firmware update in the firmware_updates list to
  724         the node.
  725 
  726         Note that the caller must have an exclusive lock on the node and
  727         the caller must ensure node.save() is called after making this
  728         call.
  729 
  730         :param node: the node to apply the next update to
  731         :param update_service: the sushy firmware update service
  732         :param firmware_updates: the remaining firmware updates to apply
  733         """
  734 
  735         firmware_update = firmware_updates[0]
  736         firmware_url = firmware_update['url']
  737 
  738         LOG.debug('Applying firmware %(firmware_image)s to node '
  739                   '%(node_uuid)s',
  740                   {'firmware_image': firmware_url,
  741                    'node_uuid': node.uuid})
  742 
  743         task_monitor = update_service.simple_update(firmware_url)
  744 
  745         driver_internal_info = node.driver_internal_info
  746         firmware_update['task_monitor'] = task_monitor.task_monitor
  747         driver_internal_info['firmware_updates'] = firmware_updates
  748         node.driver_internal_info = driver_internal_info
  749 
  750     def _continue_firmware_updates(self, task, update_service,
  751                                    firmware_updates):
  752         """Continues processing the firmware updates
  753 
  754         Continues to process the firmware updates on the node.
  755 
  756         Note that the caller must have an exclusive lock on the node.
  757 
  758         :param task: a TaskManager instance containing the node to act on.
  759         :param update_service: the sushy firmware update service
  760         :param firmware_updates: the remaining firmware updates to apply
  761         """
  762 
  763         node = task.node
  764         firmware_update = firmware_updates[0]
  765         wait_interval = firmware_update.get('wait')
  766         if wait_interval:
  767             time_now = str(timeutils.utcnow().isoformat())
  768             firmware_update['wait_start_time'] = time_now
  769 
  770             LOG.debug('Waiting at %(time)s for %(seconds)s seconds after '
  771                       'firmware update %(firmware_image)s on node %(node)s',
  772                       {'time': time_now,
  773                        'seconds': wait_interval,
  774                        'firmware_image': firmware_update['url'],
  775                        'node': node.uuid})
  776 
  777             driver_internal_info = node.driver_internal_info
  778             driver_internal_info['firmware_updates'] = firmware_updates
  779             node.driver_internal_info = driver_internal_info
  780             node.save()
  781             return
  782 
  783         if len(firmware_updates) == 1:
  784             self._clear_firmware_updates(node)
  785 
  786             LOG.info('Firmware updates completed for node %(node)s',
  787                      {'node': node.uuid})
  788 
  789             manager_utils.notify_conductor_resume_clean(task)
  790         else:
  791             firmware_updates.pop(0)
  792             self._apply_firmware_update(node,
  793                                         update_service,
  794                                         firmware_updates)
  795             node.save()
  796             manager_utils.node_power_action(task, states.REBOOT)
  797 
  798     def _clear_firmware_updates(self, node):
  799         """Clears firmware updates from driver_internal_info
  800 
  801         Note that the caller must have an exclusive lock on the node.
  802 
  803         :param node: the node to clear the firmware updates from
  804         """
  805         driver_internal_info = node.driver_internal_info
  806         driver_internal_info.pop('firmware_updates', None)
  807         node.driver_internal_info = driver_internal_info
  808         node.save()
  809 
  810     @METRICS.timer('RedfishManagement._query_firmware_update_failed')
  811     @periodics.periodic(
  812         spacing=CONF.redfish.firmware_update_fail_interval,
  813         enabled=CONF.redfish.firmware_update_fail_interval > 0)
  814     def _query_firmware_update_failed(self, manager, context):
  815         """Periodic job to check for failed firmware updates."""
  816 
  817         filters = {'reserved': False, 'provision_state': states.CLEANFAIL,
  818                    'maintenance': True}
  819 
  820         fields = ['driver_internal_info']
  821 
  822         node_list = manager.iter_nodes(fields=fields, filters=filters)
  823         for (node_uuid, driver, conductor_group,
  824              driver_internal_info) in node_list:
  825             try:
  826                 lock_purpose = 'checking async firmware update failed.'
  827                 with task_manager.acquire(context, node_uuid,
  828                                           purpose=lock_purpose,
  829                                           shared=True) as task:
  830                     if not isinstance(task.driver.management,
  831                                       RedfishManagement):
  832                         continue
  833 
  834                     firmware_updates = driver_internal_info.get(
  835                         'firmware_updates')
  836                     if not firmware_updates:
  837                         continue
  838 
  839                     node = task.node
  840 
  841                     # A firmware update failed. Discard any remaining firmware
  842                     # updates so when the user takes the node out of
  843                     # maintenance mode, pending firmware updates do not
  844                     # automatically continue.
  845                     LOG.warning('Firmware update failed for node %(node)s. '
  846                                 'Discarding remaining firmware updates.',
  847                                 {'node': node.uuid})
  848 
  849                     task.upgrade_lock()
  850                     self._clear_firmware_updates(node)
  851 
  852             except exception.NodeNotFound:
  853                 LOG.info('During _query_firmware_update_failed, node '
  854                          '%(node)s was not found and presumed deleted by '
  855                          'another process.', {'node': node_uuid})
  856             except exception.NodeLocked:
  857                 LOG.info('During _query_firmware_update_failed, node '
  858                          '%(node)s was already locked by another process. '
  859                          'Skip.', {'node': node_uuid})
  860 
  861     @METRICS.timer('RedfishManagement._query_firmware_update_status')
  862     @periodics.periodic(
  863         spacing=CONF.redfish.firmware_update_status_interval,
  864         enabled=CONF.redfish.firmware_update_status_interval > 0)
  865     def _query_firmware_update_status(self, manager, context):
  866         """Periodic job to check firmware update tasks."""
  867 
  868         filters = {'reserved': False, 'provision_state': states.CLEANWAIT}
  869         fields = ['driver_internal_info']
  870 
  871         node_list = manager.iter_nodes(fields=fields, filters=filters)
  872         for (node_uuid, driver, conductor_group,
  873              driver_internal_info) in node_list:
  874             try:
  875                 lock_purpose = 'checking async firmware update tasks.'
  876                 with task_manager.acquire(context, node_uuid,
  877                                           purpose=lock_purpose,
  878                                           shared=True) as task:
  879                     if not isinstance(task.driver.management,
  880                                       RedfishManagement):
  881                         continue
  882 
  883                     firmware_updates = driver_internal_info.get(
  884                         'firmware_updates')
  885                     if not firmware_updates:
  886                         continue
  887 
  888                     self._check_node_firmware_update(task)
  889 
  890             except exception.NodeNotFound:
  891                 LOG.info('During _query_firmware_update_status, node '
  892                          '%(node)s was not found and presumed deleted by '
  893                          'another process.', {'node': node_uuid})
  894             except exception.NodeLocked:
  895                 LOG.info('During _query_firmware_update_status, node '
  896                          '%(node)s was already locked by another process. '
  897                          'Skip.', {'node': node_uuid})
  898 
  899     @METRICS.timer('RedfishManagement._check_node_firmware_update')
  900     def _check_node_firmware_update(self, task):
  901         """Check the progress of running firmware update on a node."""
  902 
  903         node = task.node
  904 
  905         firmware_updates = node.driver_internal_info['firmware_updates']
  906         current_update = firmware_updates[0]
  907 
  908         try:
  909             update_service = redfish_utils.get_update_service(node)
  910         except exception.RedfishConnectionError as e:
  911             # If the BMC firmware is being updated, the BMC will be
  912             # unavailable for some amount of time.
  913             LOG.warning('Unable to communicate with firmware update service '
  914                         'on node %(node)s. Will try again on the next poll. '
  915                         'Error: %(error)s',
  916                         {'node': node.uuid,
  917                          'error': e})
  918             return
  919 
  920         wait_start_time = current_update.get('wait_start_time')
  921         if wait_start_time:
  922             wait_start = timeutils.parse_isotime(wait_start_time)
  923 
  924             elapsed_time = timeutils.utcnow(True) - wait_start
  925             if elapsed_time.seconds >= current_update['wait']:
  926                 LOG.debug('Finished waiting after firmware update '
  927                           '%(firmware_image)s on node %(node)s. '
  928                           'Elapsed time: %(seconds)s seconds',
  929                           {'firmware_image': current_update['url'],
  930                            'node': node.uuid,
  931                            'seconds': elapsed_time.seconds})
  932                 current_update.pop('wait', None)
  933                 current_update.pop('wait_start_time', None)
  934 
  935                 task.upgrade_lock()
  936                 self._continue_firmware_updates(task,
  937                                                 update_service,
  938                                                 firmware_updates)
  939             else:
  940                 LOG.debug('Continuing to wait after firmware update '
  941                           '%(firmware_image)s on node %(node)s. '
  942                           'Elapsed time: %(seconds)s seconds',
  943                           {'firmware_image': current_update['url'],
  944                            'node': node.uuid,
  945                            'seconds': elapsed_time.seconds})
  946 
  947             return
  948 
  949         try:
  950             task_monitor = update_service.get_task_monitor(
  951                 current_update['task_monitor'])
  952         except sushy.exceptions.ResourceNotFoundError:
  953             # The BMC deleted the Task before we could query it
  954             LOG.warning('Firmware update completed for node %(node)s, '
  955                         'firmware %(firmware_image)s, but success of the '
  956                         'update is unknown.  Assuming update was successful.',
  957                         {'node': node.uuid,
  958                          'firmware_image': current_update['url']})
  959             task.upgrade_lock()
  960             self._continue_firmware_updates(task,
  961                                             update_service,
  962                                             firmware_updates)
  963             return
  964 
  965         if not task_monitor.is_processing:
  966             # The last response does not necessarily contain a Task,
  967             # so get it
  968             sushy_task = task_monitor.get_task()
  969 
  970             # Only parse the messages if the BMC did not return parsed
  971             # messages
  972             messages = []
  973             if not sushy_task.messages[0].message:
  974                 sushy_task.parse_messages()
  975 
  976             messages = [m.message for m in sushy_task.messages]
  977 
  978             if (sushy_task.task_state == sushy.TASK_STATE_COMPLETED
  979                     and sushy_task.task_status in
  980                     [sushy.HEALTH_OK, sushy.HEALTH_WARNING]):
  981                 LOG.info('Firmware update succeeded for node %(node)s, '
  982                          'firmware %(firmware_image)s: %(messages)s',
  983                          {'node': node.uuid,
  984                           'firmware_image': current_update['url'],
  985                           'messages': ", ".join(messages)})
  986 
  987                 task.upgrade_lock()
  988                 self._continue_firmware_updates(task,
  989                                                 update_service,
  990                                                 firmware_updates)
  991             else:
  992                 error_msg = (_('Firmware update failed for node %(node)s, '
  993                                'firmware %(firmware_image)s. '
  994                                'Error: %(errors)s') %
  995                              {'node': node.uuid,
  996                               'firmware_image': current_update['url'],
  997                               'errors': ",  ".join(messages)})
  998 
  999                 task.upgrade_lock()
 1000                 self._clear_firmware_updates(node)
 1001                 manager_utils.cleaning_error_handler(task, error_msg)
 1002         else:
 1003             LOG.debug('Firmware update in progress for node %(node)s, '
 1004                       'firmware %(firmware_image)s.',
 1005                       {'node': node.uuid,
 1006                        'firmware_image': current_update['url']})