ironic/ironic/drivers/modules/irmc/power.py

339 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 Power Driver using the Base Server Profile
"""
from ironic_lib import metrics_utils
from oslo_log import log as logging
from oslo_service import loopingcall
from oslo_utils import importutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import states
from ironic.conductor import task_manager
from ironic.conf import CONF
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')
LOG = logging.getLogger(__name__)
METRICS = metrics_utils.get_metrics_logger(__name__)
"""
SC2.mib: sc2srvCurrentBootStatus returns status of the current boot
"""
BOOT_STATUS_OID = "1.3.6.1.4.1.231.2.10.2.2.10.4.1.1.4.1"
BOOT_STATUS_VALUE = {
'error': 0,
'unknown': 1,
'off': 2,
'no-boot-cpu': 3,
'self-test': 4,
'setup': 5,
'os-boot': 6,
'diagnostic-boot': 7,
'os-running': 8,
'diagnostic-running': 9,
'os-shutdown': 10,
'diagnostic-shutdown': 11,
'reset': 12
}
BOOT_STATUS = {v: k for k, v in BOOT_STATUS_VALUE.items()}
if scci:
STATES_MAP = {states.POWER_OFF: scci.POWER_OFF,
states.POWER_ON: scci.POWER_ON,
states.REBOOT: scci.POWER_RESET,
states.SOFT_REBOOT: scci.POWER_SOFT_CYCLE,
states.SOFT_POWER_OFF: scci.POWER_SOFT_OFF}
def _is_expected_power_state(target_state, boot_status_value):
"""Predicate if target power state and boot status values match.
:param target_state: Target power state.
:param boot_status_value: SNMP BOOT_STATUS_VALUE.
:returns: True if expected power state, otherwise Flase.
"""
if (target_state == states.SOFT_POWER_OFF
and boot_status_value in (BOOT_STATUS_VALUE['unknown'],
BOOT_STATUS_VALUE['off'])):
return True
elif (target_state == states.SOFT_REBOOT
and boot_status_value == BOOT_STATUS_VALUE['os-running']):
return True
return False
def _wait_power_state(task, target_state, timeout=None):
"""Wait for having changed to the target power state.
:param task: A TaskManager instance containing the node to act on.
:raises: IRMCOperationError if the target state acknowledge failed.
:raises: SNMPFailure if SNMP request failed.
"""
node = task.node
d_info = irmc_common.parse_driver_info(node)
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'))
interval = CONF.irmc.snmp_polling_interval
retry_timeout_soft = timeout or CONF.conductor.soft_power_off_timeout
max_retry = int(retry_timeout_soft / interval)
def _wait(mutable):
mutable['boot_status_value'] = snmp_client.get(BOOT_STATUS_OID)
LOG.debug("iRMC SNMP agent of %(node_id)s returned "
"boot status value %(bootstatus)s on attempt %(times)s.",
{'node_id': node.uuid,
'bootstatus': BOOT_STATUS[mutable['boot_status_value']],
'times': mutable['times']})
if _is_expected_power_state(target_state,
mutable['boot_status_value']):
mutable['state'] = target_state
raise loopingcall.LoopingCallDone()
mutable['times'] += 1
if mutable['times'] > max_retry:
mutable['state'] = states.ERROR
raise loopingcall.LoopingCallDone()
store = {'state': None, 'times': 0, 'boot_status_value': None}
timer = loopingcall.FixedIntervalLoopingCall(_wait, store)
timer.start(interval=interval).wait()
if store['state'] == target_state:
# iRMC acknowledged the target state
node.last_error = None
node.power_state = (states.POWER_OFF
if target_state == states.SOFT_POWER_OFF
else states.POWER_ON)
node.target_power_state = states.NOSTATE
node.save()
LOG.info('iRMC successfully set node %(node_id)s '
'power state to %(bootstatus)s.',
{'node_id': node.uuid,
'bootstatus': BOOT_STATUS[store['boot_status_value']]})
else:
# iRMC failed to acknowledge the target state
last_error = (_('iRMC returned unexpected boot status value %s') %
BOOT_STATUS[store['boot_status_value']])
node.last_error = last_error
node.power_state = states.ERROR
node.target_power_state = states.NOSTATE
node.save()
LOG.error('iRMC failed to acknowledge the target state for node '
'%(node_id)s. Error: %(last_error)s',
{'node_id': node.uuid, 'last_error': last_error})
error = _('unexpected boot status value')
raise exception.IRMCOperationError(operation=target_state,
error=error)
def _set_power_state(task, target_state, timeout=None):
"""Turn the server power on/off or do a reboot.
:param task: a TaskManager instance containing the node to act on.
:param target_state: target state of the node.
:param timeout: timeout (in seconds) positive integer (> 0) for any
power state. ``None`` indicates default timeout.
:raises: InvalidParameterValue if an invalid power state was specified.
:raises: MissingParameterValue if some mandatory information
is missing on the node
:raises: IRMCOperationError on an error from SCCI or SNMP
"""
node = task.node
irmc_client = irmc_common.get_irmc_client(node)
if target_state in (states.POWER_ON, states.REBOOT, states.SOFT_REBOOT):
irmc_boot.attach_boot_iso_if_needed(task)
try:
irmc_client(STATES_MAP[target_state])
except KeyError:
msg = _("_set_power_state called with invalid power state "
"'%s'") % target_state
raise exception.InvalidParameterValue(msg)
except scci.SCCIClientError as irmc_exception:
LOG.error("iRMC set_power_state failed to set state to %(tstate)s "
" for node %(node_id)s with error: %(error)s",
{'tstate': target_state, 'node_id': node.uuid,
'error': irmc_exception})
operation = _('iRMC set_power_state')
raise exception.IRMCOperationError(operation=operation,
error=irmc_exception)
try:
if target_state in (states.SOFT_REBOOT, states.SOFT_POWER_OFF):
# note (naohirot):
# The following call covers both cases since SOFT_REBOOT matches
# 'unknown' and SOFT_POWER_OFF matches 'off' or 'unknown'.
_wait_power_state(task, states.SOFT_POWER_OFF, timeout=timeout)
if target_state == states.SOFT_REBOOT:
_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. %(advice)s",
{'node_id': node.uuid, 'error': snmp_exception,
'advice': advice})
raise exception.IRMCOperationError(operation=target_state,
error=snmp_exception)
class IRMCPower(redfish_power.RedfishPower, base.PowerInterface):
"""Interface for power-related actions."""
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('IRMCPower.validate')
def validate(self, task):
"""Validate the driver-specific Node power info.
This method validates whether the 'driver_info' property of the
supplied node contains the required information for this driver to
manage the power state of the node.
:param task: a TaskManager instance containing the node to act on.
:raises: InvalidParameterValue if required driver_info attribute
is missing or invalid on the node.
:raises: MissingParameterValue if a required parameter is missing.
"""
# 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):
"""Return the power state of the task's node.
: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 parameters are incorrect.
:raises: MissingParameterValue if required parameters are missing.
:raises: IRMCOperationError If IPMI or Redfish operation fails
"""
# 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
def set_power_state(self, task, power_state, timeout=None):
"""Set the power state of the task's node.
:param task: a TaskManager instance containing the node to act on.
:param power_state: Any power state from :mod:`ironic.common.states`.
:param timeout: timeout (in seconds) positive integer (> 0) for any
power state. ``None`` indicates default timeout.
:raises: InvalidParameterValue if an invalid power state was specified.
:raises: MissingParameterValue if some mandatory information
is missing on the node
:raises: IRMCOperationError if failed to set the power state.
"""
_set_power_state(task, power_state, timeout=timeout)
@METRICS.timer('IRMCPower.reboot')
@task_manager.require_exclusive_lock
def reboot(self, task, timeout=None):
"""Perform a hard reboot of the task's node.
:param task: a TaskManager instance containing the node to act on.
:param timeout: timeout (in seconds) positive integer (> 0) for any
power state. ``None`` indicates default timeout.
:raises: InvalidParameterValue if an invalid power state was specified.
:raises: IRMCOperationError if failed to set the power state.
"""
current_pstate = self.get_power_state(task)
if current_pstate == states.POWER_ON:
_set_power_state(task, states.REBOOT, timeout=timeout)
elif current_pstate == states.POWER_OFF:
_set_power_state(task, states.POWER_ON, timeout=timeout)
@METRICS.timer('IRMCPower.get_supported_power_states')
def get_supported_power_states(self, task):
"""Get a list of the supported power states.
:param task: A TaskManager instance containing the node to act on.
currently not used.
:returns: A list with the supported power states defined
in :mod:`ironic.common.states`.
"""
return [states.POWER_ON, states.POWER_OFF, states.REBOOT,
states.SOFT_REBOOT, states.SOFT_POWER_OFF]