370 lines
14 KiB
Python
370 lines
14 KiB
Python
# 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):
|
|
"""Remove backup bios config from the node."""
|
|
internal_info = task.node.driver_internal_info
|
|
internal_info.pop('irmc_bios_config', None)
|
|
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)
|
|
|
|
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)
|