339 lines
14 KiB
Python
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]
|