# Copyright 2014 Red Hat, Inc. # All Rights Reserved. # # 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. """ Ironic iBoot PDU power manager. """ import time from ironic.common import exception as ironic_exception from ironic.common import states from ironic.conductor import task_manager from ironic.drivers import base from oslo_config import cfg from oslo_log import log as logging from oslo_service import loopingcall from oslo_utils import importutils import six from ironic_staging_drivers.common.i18n import _ from ironic_staging_drivers.common import utils iboot = importutils.try_import('iboot') LOG = logging.getLogger(__name__) opts = [ cfg.IntOpt('max_retry', default=3, min=0, help=_('Maximum retries for iBoot operations')), cfg.IntOpt('retry_interval', default=1, min=0, help=_('Time (in seconds) between retry attempts for iBoot ' 'operations')), cfg.IntOpt('reboot_delay', default=5, min=0, help=_('Time (in seconds) to sleep between when rebooting ' '(powering off and on again).')) ] CONF = cfg.CONF opt_group = cfg.OptGroup(name='iboot', title='Options for the iBoot power driver') CONF.register_group(opt_group) try: CONF.register_opts(opts, opt_group) except cfg.DuplicateOptError: LOG.warning('The iboot configuration options have been registered ' 'already. Is the iboot driver included in both ironic ' 'and ironic staging drivers?') REQUIRED_PROPERTIES = { 'iboot_address': _("IP address of the PDU. Required."), 'iboot_username': _("username. Required."), 'iboot_password': _("password. Required."), } OPTIONAL_PROPERTIES = { 'iboot_relay_id': _("iBoot PDU relay id; default is 1. Optional."), 'iboot_port': _("iBoot PDU port; default is 9100. Optional."), } COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) def _parse_driver_info(node): info = node.driver_info or {} missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)] if missing_info: raise ironic_exception.MissingParameterValue( _("Missing the following iBoot credentials in node's" " driver_info: %s.") % missing_info) address = info['iboot_address'] username = info['iboot_username'] password = info['iboot_password'] relay_id = info.get('iboot_relay_id', 1) try: relay_id = int(relay_id) except ValueError: raise ironic_exception.InvalidParameterValue( _("iBoot PDU relay id must be an integer.")) port = info.get('iboot_port', 9100) port = utils.validate_network_port(port, 'iboot_port') return { 'address': address, 'username': username, 'password': password, 'port': port, 'relay_id': relay_id, 'uuid': node.uuid, } def _get_connection(driver_info): # NOTE: python-iboot wants username and password as strings (not unicode) return iboot.iBootInterface(driver_info['address'], six.binary_type(driver_info['username']), six.binary_type(driver_info['password']), port=driver_info['port'], num_relays=driver_info['relay_id']) def _switch(driver_info, enabled): conn = _get_connection(driver_info) relay_id = driver_info['relay_id'] def _wait_for_switch(mutable): if mutable['retries'] > CONF.iboot.max_retry: LOG.warning( 'Reached maximum number of attempts (%(attempts)d) to set ' 'power state for node %(node)s to "%(op)s"', {'attempts': mutable['retries'], 'node': driver_info['uuid'], 'op': states.POWER_ON if enabled else states.POWER_OFF}) raise loopingcall.LoopingCallDone() try: mutable['retries'] += 1 mutable['response'] = conn.switch(relay_id, enabled) if mutable['response']: raise loopingcall.LoopingCallDone() except (TypeError, IndexError): LOG.warning("Cannot call set power state for node '%(node)s' " "at relay '%(relay)s'. iBoot switch() failed.", {'node': driver_info['uuid'], 'relay': relay_id}) mutable = {'response': False, 'retries': 0} timer = loopingcall.FixedIntervalLoopingCall(_wait_for_switch, mutable) timer.start(interval=CONF.iboot.retry_interval).wait() return mutable['response'] def _sleep_switch(seconds): """Function broken out for testing purpose.""" time.sleep(seconds) def _check_power_state(driver_info, pstate): """Function to check power state is correct. Up to max retries.""" # always try once + number of retries for num in range(0, 1 + CONF.iboot.max_retry): state = _power_status(driver_info) if state == pstate: return if num < CONF.iboot.max_retry: time.sleep(CONF.iboot.retry_interval) raise ironic_exception.PowerStateFailure(pstate=pstate) def _power_status(driver_info): conn = _get_connection(driver_info) relay_id = driver_info['relay_id'] def _wait_for_power_status(mutable): if mutable['retries'] > CONF.iboot.max_retry: LOG.warning( 'Reached maximum number of attempts (%(attempts)d) to get ' 'power state for node %(node)s', {'attempts': mutable['retries'], 'node': driver_info['uuid']}) raise loopingcall.LoopingCallDone() try: mutable['retries'] += 1 response = conn.get_relays() status = response[relay_id - 1] if status: mutable['state'] = states.POWER_ON else: mutable['state'] = states.POWER_OFF raise loopingcall.LoopingCallDone() except (TypeError, IndexError): LOG.warning("Cannot get power state for node '%(node)s' at " "relay '%(relay)s'. iBoot get_relays() failed.", {'node': driver_info['uuid'], 'relay': relay_id}) mutable = {'state': states.ERROR, 'retries': 0} timer = loopingcall.FixedIntervalLoopingCall(_wait_for_power_status, mutable) timer.start(interval=CONF.iboot.retry_interval).wait() return mutable['state'] class IBootPower(base.PowerInterface): """iBoot PDU Power Driver for Ironic This PowerManager class provides a mechanism for controlling power state via an iBoot capable device. Requires installation of python-iboot: https://github.com/darkip/python-iboot """ def get_properties(self): return COMMON_PROPERTIES def validate(self, task): """Validate driver_info for iboot driver. :param task: a TaskManager instance containing the node to act on. :raises: InvalidParameterValue if iboot parameters are invalid. :raises: MissingParameterValue if required iboot parameters are missing. """ _parse_driver_info(task.node) def get_power_state(self, task): """Get the current power state of the task's node. :param task: a TaskManager instance containing the node to act on. :returns: one of ironic.common.states POWER_OFF, POWER_ON or ERROR. :raises: InvalidParameterValue if iboot parameters are invalid. :raises: MissingParameterValue if required iboot parameters are missing. """ driver_info = _parse_driver_info(task.node) return _power_status(driver_info) @task_manager.require_exclusive_lock def set_power_state(self, task, pstate): """Turn the power on or off. :param task: a TaskManager instance containing the node to act on. :param pstate: The desired power state, one of ironic.common.states POWER_ON, POWER_OFF. :raises: InvalidParameterValue if iboot parameters are invalid or if an invalid power state was specified. :raises: MissingParameterValue if required iboot parameters are missing. :raises: PowerStateFailure if the power couldn't be set to pstate. """ driver_info = _parse_driver_info(task.node) if pstate == states.POWER_ON: _switch(driver_info, True) elif pstate == states.POWER_OFF: _switch(driver_info, False) else: raise ironic_exception.InvalidParameterValue( _("set_power_state called with invalid " "power state %s.") % pstate) _check_power_state(driver_info, pstate) @task_manager.require_exclusive_lock def reboot(self, task): """Cycles the power to the task's node. :param task: a TaskManager instance containing the node to act on. :raises: InvalidParameterValue if iboot parameters are invalid. :raises: MissingParameterValue if required iboot parameters are missing. :raises: PowerStateFailure if the final state of the node is not POWER_ON. """ driver_info = _parse_driver_info(task.node) _switch(driver_info, False) _sleep_switch(CONF.iboot.reboot_delay) _switch(driver_info, True) _check_power_state(driver_info, states.POWER_ON)