text: # Copyright 2015 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.
  """
  iRMC Management Driver
  """
  
  from ironic_lib import metrics_utils
  from oslo_log import log as logging
  from oslo_utils import importutils
  
  from ironic.common import boot_devices
  from ironic.common import exception
  from ironic.common.i18n import _
  from ironic.common import states
  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 ipmitool
  from ironic.drivers.modules.irmc import common as irmc_common
  from ironic.drivers import utils as driver_utils
  
  irmc = importutils.try_import('scciclient.irmc')
  
  LOG = logging.getLogger(__name__)
  CONF = conf.CONF
  
  METRICS = metrics_utils.get_metrics_logger(__name__)
  
  # Boot Option Parameters #5 Data2 defined in
  # Set/Get System Boot Options Command, IPMI spec v2.0.
  _BOOTPARAM5_DATA2 = {boot_devices.PXE: '0x04',
                       boot_devices.DISK: '0x08',
                       # note (naohirot)
                       # boot_devices.CDROM is tentatively set to '0x20' rather
                       # than '0x14' as a work-around to force iRMC vmedia boot.
                       #   0x14 = Force boot from default CD/DVD
                       #   0x20 = Force boot from remotely connected CD/DVD
                       boot_devices.CDROM: '0x20',
                       boot_devices.BIOS: '0x18',
                       boot_devices.SAFE: '0x0c',
                       }
  
  
  def _get_sensors_data(task):
      """Get sensors data method.
  
      It gets sensor data from the task's node via SCCI, and convert the data
      from XML to the dict format.
  
      :param task: A TaskManager instance.
      :raises: FailedToGetSensorData when getting the sensor data fails.
      :returns: Returns a consistent formatted dict of sensor data grouped
                by sensor type, which can be processed by Ceilometer.
      """
  
      try:
          report = irmc_common.get_irmc_report(task.node)
          sensor = irmc.scci.get_sensor_data(report)
  
      except (exception.InvalidParameterValue,
              exception.MissingParameterValue,
              irmc.scci.SCCIInvalidInputError,
              irmc.scci.SCCIClientError) as e:
          LOG.error("SCCI get sensor data failed for node %(node_id)s "
                    "with the following error: %(error)s",
                    {'node_id': task.node.uuid, 'error': e})
          raise exception.FailedToGetSensorData(
              node=task.node.uuid, error=e)
  
      sensors_data = {}
      for sdr in sensor:
          sensor_type_name = sdr.find('./Data/Decoded/Sensor/TypeName')
          sensor_type_number = sdr.find('./Data/Decoded/Sensor/Type')
          entity_name = sdr.find('./Data/Decoded/Entity/Name')
          entity_id = sdr.find('./Data/Decoded/Entity/ID')
  
          if None in (sensor_type_name, sensor_type_number,
                      entity_name, entity_id):
              continue
  
          sensor_type = ('%s (%s)' %
                         (sensor_type_name.text, sensor_type_number.text))
          sensor_id = ('%s (%s)' %
                       (entity_name.text, entity_id.text))
          reading_value = sdr.find(
              './Data/Decoded/Sensor/Thresholds/*/Normalized')
          reading_value_text = "None" if (
              reading_value is None) else str(reading_value.text)
          reading_units = sdr.find('./Data/Decoded/Sensor/BaseUnitName')
          reading_units_text = "None" if (
              reading_units is None) else str(reading_units.text)
          sensor_reading = '%s %s' % (reading_value_text, reading_units_text)
  
          sensors_data.setdefault(sensor_type, {})[sensor_id] = {
              'Sensor Reading': sensor_reading,
              'Sensor ID': sensor_id,
              'Units': reading_units_text,
          }
  
      return sensors_data
  
  
  def backup_bios_config(task):
      """Backup BIOS config from a node.
  
      :param task: a TaskManager instance containing the node to act on.
      :raises: IRMCOperationError on failure.
      """
      node_uuid = task.node.uuid
  
      # Skip this operation if the clean step 'restore' is disabled
      if CONF.irmc.clean_priority_restore_irmc_bios_config == 0:
          LOG.debug('Skipped the operation backup_BIOS_config for node %s '
                    'as the clean step restore_BIOS_config is disabled.',
                    node_uuid)
          return
  
      irmc_info = irmc_common.parse_driver_info(task.node)
  
      try:
          # Backup bios config
          result = irmc.elcm.backup_bios_config(irmc_info)
      except irmc.scci.SCCIError as e:
          LOG.error('Failed to backup BIOS config for node %(node)s. '
                    'Error: %(error)s', {'node': node_uuid, 'error': e})
          raise exception.IRMCOperationError(operation='backup BIOS config',
                                             error=e)
  
      # Save bios config into the driver_internal_info
      internal_info = task.node.driver_internal_info
      internal_info['irmc_bios_config'] = result['bios_config']
      task.node.driver_internal_info = internal_info
      task.node.save()
  
      LOG.info('BIOS config is backed up successfully for node %s',
               node_uuid)
  
      # NOTE(tiendc): When the backup operation done, server is automatically
      # shutdown. However, this function is called right before the method
      # task.driver.deploy() that will trigger a reboot. So, we don't need
      # to power on the server at this point.
  
  
  def _restore_bios_config(task):
      """Restore BIOS config to a node.
  
      :param task: a TaskManager instance containing the node to act on.
      :raises: IRMCOperationError if the operation fails.
      """
      node_uuid = task.node.uuid
  
      # Get bios config stored in the node object
      bios_config = task.node.driver_internal_info.get('irmc_bios_config')
      if not bios_config:
          LOG.info('Skipped operation "restore BIOS config" on node %s '
                   'as the backup data not found.', node_uuid)
          return
  
      def _remove_bios_config(task, reboot_flag=False):
          """Remove backup bios config from the node."""
          internal_info = task.node.driver_internal_info
          internal_info.pop('irmc_bios_config', None)
          # NOTE(tiendc): If reboot flag is raised, then the BM will
          # reboot and cause a bug if the next clean step is in-band.
          # See https://storyboard.openstack.org/#!/story/2002731
          if reboot_flag:
              internal_info['cleaning_reboot'] = True
          task.node.driver_internal_info = internal_info
          task.node.save()
  
      irmc_info = irmc_common.parse_driver_info(task.node)
  
      try:
          # Restore bios config
          irmc.elcm.restore_bios_config(irmc_info, bios_config)
      except irmc.scci.SCCIError as e:
          # If the input bios config is not correct or corrupted, then
          # we should remove it from the node object.
          if isinstance(e, irmc.scci.SCCIInvalidInputError):
              _remove_bios_config(task)
  
          LOG.error('Failed to restore BIOS config on node %(node)s. '
                    'Error: %(error)s', {'node': node_uuid, 'error': e})
          raise exception.IRMCOperationError(operation='restore BIOS config',
                                             error=e)
  
      # Remove the backup data after restoring
      _remove_bios_config(task, reboot_flag=True)
  
      LOG.info('BIOS config is restored successfully on node %s',
               node_uuid)
  
      # Change power state to ON as server is automatically
      # shutdown after the operation.
      manager_utils.node_power_action(task, states.POWER_ON)
  
  
  class IRMCManagement(ipmitool.IPMIManagement):
  
      def get_properties(self):
          """Return the properties of the interface.
  
          :returns: Dictionary of <property name>:<property description> entries.
          """
          return irmc_common.COMMON_PROPERTIES
  
      @METRICS.timer('IRMCManagement.validate')
      def validate(self, task):
          """Validate the driver-specific management information.
  
          This method validates whether the 'driver_info' property of the
          supplied node contains the required information for this driver.
  
          :param task: A TaskManager instance containing the node to act on.
          :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)
  
      @METRICS.timer('IRMCManagement.set_boot_device')
      @task_manager.require_exclusive_lock
      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)
  
      @METRICS.timer('IRMCManagement.get_sensors_data')
      def get_sensors_data(self, task):
          """Get sensors data method.
  
          It gets sensor data from the task's node via SCCI, and convert the data
          from XML to the dict format.
  
          :param task: A TaskManager instance.
          :raises: FailedToGetSensorData when getting the sensor data fails.
          :raises: FailedToParseSensorData when parsing sensor data fails.
          :raises: InvalidParameterValue if required parameters are invalid.
          :raises: MissingParameterValue if a required parameter is missing.
          :returns: Returns a consistent formatted dict of sensor data grouped
                    by sensor type, which can be processed by Ceilometer.
                    Example::
  
                        {
                          'Sensor Type 1': {
                            'Sensor ID 1': {
                              'Sensor Reading': 'Value1 Units1',
                              'Sensor ID': 'Sensor ID 1',
                              'Units': 'Units1'
                            },
                            'Sensor ID 2': {
                              'Sensor Reading': 'Value2 Units2',
                              'Sensor ID': 'Sensor ID 2',
                              'Units': 'Units2'
                            }
                          },
                          'Sensor Type 2': {
                            'Sensor ID 3': {
                              'Sensor Reading': 'Value3 Units3',
                              'Sensor ID': 'Sensor ID 3',
                              'Units': 'Units3'
                            },
                            'Sensor ID 4': {
                              'Sensor Reading': 'Value4 Units4',
                              'Sensor ID': 'Sensor ID 4',
                              'Units': 'Units4'
                            }
                          }
                        }
  
          """
          # irmc_common.parse_driver_info() makes sure that
          # d_info['irmc_sensor_method'] is either 'scci' or 'ipmitool'.
          d_info = irmc_common.parse_driver_info(task.node)
          sensor_method = d_info['irmc_sensor_method']
          if sensor_method == 'scci':
              return _get_sensors_data(task)
          elif sensor_method == 'ipmitool':
              return super(IRMCManagement, self).get_sensors_data(task)
  
      @METRICS.timer('IRMCManagement.inject_nmi')
      @task_manager.require_exclusive_lock
      def inject_nmi(self, task):
          """Inject NMI, Non Maskable Interrupt.
  
          Inject NMI (Non Maskable Interrupt) for a node immediately.
  
          :param task: A TaskManager instance containing the node to act on.
          :raises: IRMCOperationError on an error from SCCI
          :returns: None
  
          """
          node = task.node
          irmc_client = irmc_common.get_irmc_client(node)
          try:
              irmc_client(irmc.scci.POWER_RAISE_NMI)
          except irmc.scci.SCCIClientError as err:
              LOG.error('iRMC Inject NMI failed for node %(node)s: %(err)s.',
                        {'node': node.uuid, 'err': err})
              raise exception.IRMCOperationError(
                  operation=irmc.scci.POWER_RAISE_NMI, error=err)
  
      @METRICS.timer('IRMCManagement.restore_irmc_bios_config')
      @base.clean_step(
          priority=CONF.irmc.clean_priority_restore_irmc_bios_config)
      def restore_irmc_bios_config(self, task):
          """Restore BIOS config for a node.
  
          :param task: a task from TaskManager.
          :raises: NodeCleaningFailure, on failure to execute step.
          :returns: None.
          """
          try:
              _restore_bios_config(task)
          except exception.IRMCOperationError as e:
              raise exception.NodeCleaningFailure(node=task.node.uuid,
                                                  reason=e)
  
      def get_secure_boot_state(self, task):
          """Get the current secure boot state for the node.
  
          NOTE: Not all drivers support this method. Older hardware
                may not implement that.
  
          :param task: A task from TaskManager.
          :raises: MissingParameterValue if a required parameter is missing
          :raises: DriverOperationError or its derivative in case
                   of driver runtime error.
          :raises: UnsupportedDriverExtension if secure boot is
                   not supported by the driver or the hardware
          :returns: Boolean
          """
          return irmc_common.get_secure_boot_mode(task.node)
  
      def set_secure_boot_state(self, task, state):
          """Set the current secure boot state for the node.
  
          NOTE: Not all drivers support this method. Older hardware
                may not implement that.
  
          :param task: A task from TaskManager.
          :param state: A new state as a boolean.
          :raises: MissingParameterValue if a required parameter is missing
          :raises: DriverOperationError or its derivative in case
                   of driver runtime error.
          :raises: UnsupportedDriverExtension if secure boot is
                   not supported by the driver or the hardware
          """
          return irmc_common.set_secure_boot_mode(task.node, state)
  