# Copyright 2014 Hewlett-Packard Development Company, L.P. # # 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. """ iLO Power Driver """ 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 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.conf import CONF from ironic.drivers import base from ironic.drivers.modules.ilo import common as ilo_common from ironic.drivers import utils as driver_utils ilo_error = importutils.try_import('proliantutils.exception') LOG = logging.getLogger(__name__) METRICS = metrics_utils.get_metrics_logger(__name__) def _attach_boot_iso_if_needed(task): """Attaches boot ISO for a deployed node. This method checks the instance info of the baremetal node for a boot iso. If the instance info has a value of key 'boot_iso', it indicates ramdisk deploy. Therefore it attaches the boot ISO on the baremetal node and then sets the node to boot from virtual media cdrom. :param task: a TaskManager instance containing the node to act on. """ node_state = task.node.provision_state # NOTE: On instance rebuild, boot_iso will be present in # instance_info but the node will be in DEPLOYING state. # In such a scenario, the boot_iso shouldn't be # attached to the node while powering on the node (the node # should boot from deploy ramdisk instead, which will already # be attached by the deploy driver). boot_iso = driver_utils.get_field(task.node, 'boot_iso', deprecated_prefix='ilo', collection='instance_info') if boot_iso and node_state == states.ACTIVE: ilo_common.setup_vmedia_for_boot(task, boot_iso) manager_utils.node_set_boot_device(task, boot_devices.CDROM) def _get_power_state(node): """Returns the current power state of the node. :param node: The node. :returns: power state, one of :mod: `ironic.common.states`. :raises: InvalidParameterValue if required iLO credentials are missing. :raises: IloOperationError on an error from IloClient library. """ ilo_object = ilo_common.get_ilo_object(node) # Check the current power state. try: power_status = ilo_object.get_host_power_status() except ilo_error.IloError as ilo_exception: LOG.error("iLO get_power_state failed for node %(node_id)s with " "error: %(error)s.", {'node_id': node.uuid, 'error': ilo_exception}) operation = _('iLO get_power_status') raise exception.IloOperationError(operation=operation, error=ilo_exception) if power_status == "ON": return states.POWER_ON elif power_status == "OFF": return states.POWER_OFF else: return states.ERROR def _wait_for_state_change(node, target_state, requested_state, is_final_state=True, timeout=None): """Wait for the power state change to get reflected. :param node: The node. :param target_state: calculated target power state of the node. :param requested_state: actual requested power state of the node. :param is_final_state: True, if the given target state is the final expected power state of the node. Default is True. :param timeout: timeout (in seconds) positive integer (> 0) for any power state. ``None`` indicates default timeout. :returns: time consumed to achieve the power state change. :raises: IloOperationError on an error from IloClient library. :raises: PowerStateFailure if power state failed to change within timeout. """ state = [None] retries = [0] interval = CONF.ilo.power_wait if timeout: max_retry = int(timeout / interval) else: # Since we are going to track server post state, we are not using # CONF.conductor.power_state_change_timeout as its default value # is too short for bare metal to reach 'finished post' state # during 'power on' operation. It could lead to deploy failures # with default ironic configuration. # Use conductor.soft_power_off_timeout, instead. max_retry = int(CONF.conductor.soft_power_off_timeout / interval) state_to_check = target_state use_post_state = False if _can_get_server_post_state(node): use_post_state = True if (target_state in [states.POWER_OFF, states.SOFT_POWER_OFF] or target_state == states.SOFT_REBOOT and not is_final_state): state_to_check = ilo_common.POST_POWEROFF_STATE else: # It may not be able to finish POST if no bootable device is # found. Track (POST_FINISHEDPOST_STATE) only for soft reboot. # For other power-on cases track for beginning of POST operation # (POST_INPOST_STATE) to return. state_to_check = ( ilo_common.POST_FINISHEDPOST_STATE if requested_state == states.SOFT_REBOOT else ilo_common.POST_INPOST_STATE) def _wait(state): if use_post_state: state[0] = ilo_common.get_server_post_state(node) else: state[0] = _get_power_state(node) # NOTE(rameshg87): For reboot operations, initially the state # will be same as the final state. So defer the check for one retry. if retries[0] != 0 and state[0] == state_to_check: raise loopingcall.LoopingCallDone() if retries[0] > max_retry: state[0] = states.ERROR raise loopingcall.LoopingCallDone() LOG.debug("%(tim)s secs elapsed while waiting for power state " "of '%(target_state)s', current state of server %(node)s " "is '%(cur_state)s'.", {'tim': int(retries[0] * interval), 'target_state': state_to_check, 'node': node.uuid, 'cur_state': state[0]}) retries[0] += 1 # Start a timer and wait for the operation to complete. timer = loopingcall.FixedIntervalLoopingCall(_wait, state) timer.start(interval=interval).wait() if state[0] == state_to_check: return int(retries[0] * interval) else: timeout = int(max_retry * interval) LOG.error("iLO failed to change state to %(tstate)s " "within %(timeout)s sec for node %(node)s", {'tstate': target_state, 'node': node.uuid, 'timeout': int(max_retry * interval)}) raise exception.PowerStateFailure(pstate=target_state) def _set_power_state(task, target_state, timeout=None): """Turns 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: IloOperationError on an error from IloClient library. :raises: PowerStateFailure if the power couldn't be set to target_state. """ node = task.node ilo_object = ilo_common.get_ilo_object(node) # Check if its soft power operation soft_power_op = target_state in [states.SOFT_POWER_OFF, states.SOFT_REBOOT] requested_state = target_state if target_state == states.SOFT_REBOOT: if _get_power_state(node) == states.POWER_OFF: target_state = states.POWER_ON # Trigger the operation based on the target state. try: if target_state == states.POWER_OFF: ilo_object.hold_pwr_btn() elif target_state == states.POWER_ON: _attach_boot_iso_if_needed(task) ilo_object.set_host_power('ON') elif target_state == states.REBOOT: _attach_boot_iso_if_needed(task) ilo_object.reset_server() target_state = states.POWER_ON elif target_state in (states.SOFT_POWER_OFF, states.SOFT_REBOOT): ilo_object.press_pwr_btn() else: msg = _("_set_power_state called with invalid power state " "'%s'") % target_state raise exception.InvalidParameterValue(msg) except ilo_error.IloError as ilo_exception: LOG.error("iLO 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': ilo_exception}) operation = _('iLO set_power_state') raise exception.IloOperationError(operation=operation, error=ilo_exception) # Wait till the soft power state change gets reflected. time_consumed = 0 if soft_power_op: # For soft power-off, bare metal reaches final state with one # power operation. In case of soft reboot it takes two; soft # power-off followed by power-on. Also, for soft reboot we # need to ensure timeout does not expire during power-off # and power-on operation. is_final_state = target_state in (states.SOFT_POWER_OFF, states.POWER_ON) time_consumed = _wait_for_state_change( node, target_state, requested_state, is_final_state=is_final_state, timeout=timeout) if target_state == states.SOFT_REBOOT: _attach_boot_iso_if_needed(task) try: ilo_object.set_host_power('ON') except ilo_error.IloError as ilo_exception: operation = (_('Powering on failed after soft power off for ' 'node %s') % node.uuid) raise exception.IloOperationError(operation=operation, error=ilo_exception) # Re-calculate timeout available for power-on operation rem_timeout = timeout - time_consumed time_consumed += _wait_for_state_change( node, states.SOFT_REBOOT, requested_state, is_final_state=True, timeout=rem_timeout) else: time_consumed = _wait_for_state_change( node, target_state, requested_state, is_final_state=True, timeout=timeout) LOG.info("The node %(node_id)s operation of '%(state)s' " "is completed in %(time_consumed)s seconds.", {'node_id': node.uuid, 'state': target_state, 'time_consumed': time_consumed}) def _can_get_server_post_state(node): """Checks if POST state can be retrieved. Returns True if the POST state of the server can be retrieved. It cannot be retrieved for older ProLiant models. :param node: The node. :returns: True if POST state can be retrieved, else Flase. :raises: IloOperationError on an error from IloClient library. """ try: ilo_common.get_server_post_state(node) return True except exception.IloOperationNotSupported as exc: LOG.debug("Node %(node)s does not support retrieval of " "boot post state. Reason: %(reason)s", {'node': node.uuid, 'reason': exc}) return False class IloPower(base.PowerInterface): def get_properties(self): return ilo_common.COMMON_PROPERTIES @METRICS.timer('IloPower.validate') def validate(self, task): """Check if node.driver_info contains the required iLO credentials. :param task: a TaskManager instance. :param node: Single node object. :raises: InvalidParameterValue if required iLO credentials are missing. """ ilo_common.parse_driver_info(task.node) @METRICS.timer('IloPower.get_power_state') def get_power_state(self, task): """Gets the current power state. :param task: a TaskManager instance. :param node: The Node. :returns: one of :mod:`ironic.common.states` POWER_OFF, POWER_ON or ERROR. :raises: InvalidParameterValue if required iLO credentials are missing. :raises: IloOperationError on an error from IloClient library. """ return _get_power_state(task.node) @METRICS.timer('IloPower.set_power_state') @task_manager.require_exclusive_lock def set_power_state(self, task, power_state, timeout=None): """Turn the current power state on or off. :param task: a TaskManager instance. :param power_state: The desired power state POWER_ON,POWER_OFF or REBOOT from :mod:`ironic.common.states`. :param timeout: timeout (in seconds). Unsupported by this interface. :raises: InvalidParameterValue if an invalid power state was specified. :raises: IloOperationError on an error from IloClient library. :raises: PowerStateFailure if the power couldn't be set to power_state. """ _set_power_state(task, power_state, timeout=timeout) @METRICS.timer('IloPower.reboot') @task_manager.require_exclusive_lock def reboot(self, task, timeout=None): """Reboot the node :param task: a TaskManager instance. :param timeout: timeout (in seconds). Unsupported by this interface. :raises: PowerStateFailure if the final state of the node is not POWER_ON. :raises: IloOperationError on an error from IloClient library. """ node = task.node current_pstate = _get_power_state(node) 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('IloPower.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_OFF, states.POWER_ON, states.REBOOT, states.SOFT_POWER_OFF, states.SOFT_REBOOT]