From d2a4dca36d236f29298ef6f00ee36cf0b3d6f976 Mon Sep 17 00:00:00 2001 From: Naohiro Tamura Date: Tue, 25 Aug 2015 18:54:51 +0900 Subject: [PATCH] iRMC power driver for soft reboot and soft power off This patch enhances iRMC power driver to support SOFT_REBOOT_SOFT and SOFT_POWER_OFF. Partial-Bug: #1526226 Change-Id: I8c69904063ac0a9e042f54158a20347f0c2325e1 --- etc/ironic/ironic.conf.sample | 3 + ironic/conf/irmc.py | 3 + ironic/drivers/modules/irmc/power.py | 172 +++++++++++-- .../unit/drivers/modules/irmc/test_power.py | 243 +++++++++++++++++- .../drivers/third_party_driver_mock_specs.py | 2 + .../irmc-soft-power-281a83647baa4b23.yaml | 4 + 6 files changed, 399 insertions(+), 28 deletions(-) create mode 100644 releasenotes/notes/irmc-soft-power-281a83647baa4b23.yaml diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index 28b18349cd..2d6218d751 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -1831,6 +1831,9 @@ # SNMP security name. Required for version "v3" (string value) #snmp_security = +# SNMP polling interval in seconds (integer value) +#snmp_polling_interval = 10 + [ironic_lib] diff --git a/ironic/conf/irmc.py b/ironic/conf/irmc.py index 59f8c91445..ac590339b8 100644 --- a/ironic/conf/irmc.py +++ b/ironic/conf/irmc.py @@ -66,6 +66,9 @@ opts = [ help=_('SNMP community. Required for versions "v1" and "v2c"')), cfg.StrOpt('snmp_security', help=_('SNMP security name. Required for version "v3"')), + cfg.IntOpt('snmp_polling_interval', + default=10, + help='SNMP polling interval in seconds'), ] diff --git a/ironic/drivers/modules/irmc/power.py b/ironic/drivers/modules/irmc/power.py index 5dae6e5969..368a5eede1 100644 --- a/ironic/drivers/modules/irmc/power.py +++ b/ironic/drivers/modules/irmc/power.py @@ -15,20 +15,23 @@ """ 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 _, _LE +from ironic.common.i18n import _ +from ironic.common.i18n import _LE +from ironic.common.i18n import _LI 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 import snmp scci = importutils.try_import('scciclient.irmc.scci') @@ -36,37 +39,158 @@ 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.REBOOT: scci.POWER_RESET, + states.SOFT_REBOOT: scci.POWER_SOFT_CYCLE, + states.SOFT_POWER_OFF: scci.POWER_SOFT_OFF} -def _set_power_state(task, target_state): - """Turns the server power on/off or do a reboot. +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 failure + or SNMP failure. + """ + node = task.node + d_info = irmc_common.parse_driver_info(node) + snmp_client = snmp.SNMPClient(d_info['irmc_address'], + d_info['irmc_snmp_port'], + d_info['irmc_snmp_version'], + d_info['irmc_snmp_community'], + d_info['irmc_snmp_security']) + + 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(_LI('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(_LE('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 + 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): + 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]) + if target_state in (states.SOFT_REBOOT, states.SOFT_POWER_OFF): + _wait_power_state(task, target_state, timeout=timeout) + except KeyError: msg = _("_set_power_state called with invalid power state " "'%s'") % target_state raise exception.InvalidParameterValue(msg) + except exception.SNMPFailure as snmp_exception: + LOG.error(_LE("iRMC failed to acknowledge the target state " + "for node %(node_id)s. Error: %(error)s"), + {'node_id': node.uuid, 'error': snmp_exception}) + raise exception.IRMCOperationError(operation=target_state, + error=snmp_exception) + except scci.SCCIClientError as irmc_exception: LOG.error(_LE("iRMC set_power_state failed to set state to %(tstate)s " " for node %(node_id)s with error: %(error)s"), @@ -119,29 +243,45 @@ class IRMCPower(base.PowerInterface): @METRICS.timer('IRMCPower.set_power_state') @task_manager.require_exclusive_lock - def set_power_state(self, task, power_state): + 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 + is missing on the node :raises: IRMCOperationError if failed to set the power state. """ - _set_power_state(task, power_state) + _set_power_state(task, power_state, timeout=timeout) @METRICS.timer('IRMCPower.reboot') @task_manager.require_exclusive_lock - def reboot(self, task): + 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) + _set_power_state(task, states.REBOOT, timeout=timeout) elif current_pstate == states.POWER_OFF: - _set_power_state(task, states.POWER_ON) + _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] diff --git a/ironic/tests/unit/drivers/modules/irmc/test_power.py b/ironic/tests/unit/drivers/modules/irmc/test_power.py index c1f22bffe9..72ee1d8ed5 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_power.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_power.py @@ -16,6 +16,8 @@ Test class for iRMC Power Driver """ +import time + import mock from oslo_utils import uuidutils @@ -34,8 +36,6 @@ from ironic.tests.unit.objects import utils as obj_utils INFO_DICT = db_utils.get_test_irmc_info() -@mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, - autospec=True) class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase): def setUp(self): @@ -47,11 +47,98 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase): driver_info=driver_info, instance_uuid=uuidutils.generate_uuid()) + def test__is_expected_power_state(self): + target_state = states.SOFT_POWER_OFF + boot_status_value = irmc_power.BOOT_STATUS_VALUE['unknown'] + self.assertTrue(irmc_power._is_expected_power_state( + target_state, boot_status_value)) + + target_state = states.SOFT_POWER_OFF + boot_status_value = irmc_power.BOOT_STATUS_VALUE['off'] + self.assertTrue(irmc_power._is_expected_power_state( + target_state, boot_status_value)) + + target_state = states.SOFT_REBOOT + boot_status_value = irmc_power.BOOT_STATUS_VALUE['os-running'] + self.assertTrue(irmc_power._is_expected_power_state( + target_state, boot_status_value)) + + target_state = states.SOFT_POWER_OFF + boot_status_value = irmc_power.BOOT_STATUS_VALUE['os-running'] + self.assertFalse(irmc_power._is_expected_power_state( + target_state, boot_status_value)) + + @mock.patch.object(time, 'sleep', lambda seconds: None) + @mock.patch('ironic.drivers.modules.irmc.power.snmp.SNMPClient', + spec_set=True, autospec=True) + def test__wait_power_state_soft_power_off(self, snmpclient_mock): + target_state = states.SOFT_POWER_OFF + self.config(snmp_polling_interval=1, group='irmc') + self.config(soft_power_off_timeout=3, group='conductor') + snmpclient_mock.return_value = mock.Mock( + **{'get.side_effect': [8, 8, 2]}) + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + irmc_power._wait_power_state(task, target_state) + + task.node.refresh() + self.assertIsNone(task.node.last_error) + self.assertEqual(states.POWER_OFF, task.node.power_state) + self.assertEqual(states.NOSTATE, task.node.target_power_state) + + @mock.patch.object(time, 'sleep', lambda seconds: None) + @mock.patch('ironic.drivers.modules.irmc.power.snmp.SNMPClient', + spec_set=True, autospec=True) + def test__wait_power_state_soft_reboot(self, snmpclient_mock): + target_state = states.SOFT_REBOOT + self.config(snmp_polling_interval=1, group='irmc') + self.config(soft_power_off_timeout=3, group='conductor') + snmpclient_mock.return_value = mock.Mock( + **{'get.side_effect': [10, 6, 8]}) + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + irmc_power._wait_power_state(task, target_state) + + task.node.refresh() + self.assertIsNone(task.node.last_error) + self.assertEqual(states.POWER_ON, task.node.power_state) + self.assertEqual(states.NOSTATE, task.node.target_power_state) + + @mock.patch.object(time, 'sleep', lambda seconds: None) + @mock.patch('ironic.drivers.modules.irmc.power.snmp.SNMPClient', + spec_set=True, autospec=True) + def test__wait_power_state_timeout(self, snmpclient_mock): + target_state = states.SOFT_POWER_OFF + self.config(snmp_polling_interval=1, group='irmc') + self.config(soft_power_off_timeout=2, group='conductor') + snmpclient_mock.return_value = mock.Mock( + **{'get.side_effect': [8, 8, 8]}) + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + irmc_power._wait_power_state, + task, + target_state, + timeout=None) + + task.node.refresh() + self.assertIsNotNone(task.node.last_error) + self.assertEqual(states.ERROR, task.node.power_state) + self.assertEqual(states.NOSTATE, task.node.target_power_state) + + @mock.patch.object(irmc_power, '_wait_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) @mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed') def test__set_power_state_power_on_ok( self, attach_boot_iso_if_needed_mock, - get_irmc_client_mock): + get_irmc_client_mock, + _wait_power_state_mock): irmc_client = get_irmc_client_mock.return_value target_state = states.POWER_ON with task_manager.acquire(self.context, self.node.uuid, @@ -59,21 +146,33 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase): irmc_power._set_power_state(task, target_state) attach_boot_iso_if_needed_mock.assert_called_once_with(task) irmc_client.assert_called_once_with(irmc_power.scci.POWER_ON) + self.assertFalse(_wait_power_state_mock.called) + @mock.patch.object(irmc_power, '_wait_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) def test__set_power_state_power_off_ok(self, - get_irmc_client_mock): + get_irmc_client_mock, + _wait_power_state_mock): irmc_client = get_irmc_client_mock.return_value target_state = states.POWER_OFF with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: irmc_power._set_power_state(task, target_state) irmc_client.assert_called_once_with(irmc_power.scci.POWER_OFF) + self.assertFalse(_wait_power_state_mock.called) + @mock.patch.object(irmc_power, '_wait_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) @mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed') - def test__set_power_state_power_reboot_ok( + def test__set_power_state_reboot_ok( self, attach_boot_iso_if_needed_mock, - get_irmc_client_mock): + get_irmc_client_mock, + _wait_power_state_mock): irmc_client = get_irmc_client_mock.return_value target_state = states.REBOOT with task_manager.acquire(self.context, self.node.uuid, @@ -81,18 +180,72 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase): irmc_power._set_power_state(task, target_state) attach_boot_iso_if_needed_mock.assert_called_once_with(task) irmc_client.assert_called_once_with(irmc_power.scci.POWER_RESET) + self.assertFalse(_wait_power_state_mock.called) - def test__set_power_state_invalid_target_state(self, - get_irmc_client_mock): + @mock.patch.object(irmc_power, '_wait_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + @mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed') + def test__set_power_state_soft_reboot_ok( + self, + attach_boot_iso_if_needed_mock, + get_irmc_client_mock, + _wait_power_state_mock): + irmc_client = get_irmc_client_mock.return_value + target_state = states.SOFT_REBOOT + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + irmc_power._set_power_state(task, target_state) + attach_boot_iso_if_needed_mock.assert_called_once_with(task) + irmc_client.assert_called_once_with(irmc_power.scci.POWER_SOFT_CYCLE) + _wait_power_state_mock.assert_called_once_with(task, target_state, + timeout=None) + + @mock.patch.object(irmc_power, '_wait_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + @mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed') + def test__set_power_state_soft_power_off_ok(self, + attach_boot_iso_if_needed_mock, + get_irmc_client_mock, + _wait_power_state_mock): + irmc_client = get_irmc_client_mock.return_value + target_state = states.SOFT_POWER_OFF + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + irmc_power._set_power_state(task, target_state) + self.assertFalse(attach_boot_iso_if_needed_mock.called) + irmc_client.assert_called_once_with(irmc_power.scci.POWER_SOFT_OFF) + _wait_power_state_mock.assert_called_once_with(task, target_state, + timeout=None) + + @mock.patch.object(irmc_power, '_wait_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed') + def test__set_power_state_invalid_target_state( + self, + attach_boot_iso_if_needed_mock, + _wait_power_state_mock): with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: self.assertRaises(exception.InvalidParameterValue, irmc_power._set_power_state, task, states.ERROR) + self.assertFalse(attach_boot_iso_if_needed_mock.called) + self.assertFalse(_wait_power_state_mock.called) + @mock.patch.object(irmc_power, '_wait_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + @mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed') def test__set_power_state_scci_exception(self, - get_irmc_client_mock): + attach_boot_iso_if_needed_mock, + get_irmc_client_mock, + _wait_power_state_mock): irmc_client = get_irmc_client_mock.return_value irmc_client.side_effect = Exception() irmc_power.scci.SCCIClientError = Exception @@ -103,6 +256,30 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase): irmc_power._set_power_state, task, states.POWER_ON) + attach_boot_iso_if_needed_mock.assert_called_once_with( + task) + self.assertFalse(_wait_power_state_mock.called) + + @mock.patch.object(irmc_power, '_wait_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed') + def test__set_power_state_snmp_exception(self, + attach_boot_iso_if_needed_mock, + _wait_power_state_mock): + target_state = states.SOFT_REBOOT + _wait_power_state_mock.side_effect = exception.SNMPFailure( + "fake exception") + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + irmc_power._set_power_state, + task, + target_state) + attach_boot_iso_if_needed_mock.assert_called_once_with( + task) + _wait_power_state_mock.assert_called_once_with( + task, target_state, timeout=None) class IRMCPowerTestCase(db_base.DbTestCase): @@ -158,7 +335,19 @@ class IRMCPowerTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid, shared=False) as task: task.driver.power.set_power_state(task, states.POWER_ON) - mock_set_power.assert_called_once_with(task, states.POWER_ON) + mock_set_power.assert_called_once_with(task, states.POWER_ON, + timeout=None) + + @mock.patch.object(irmc_power, '_set_power_state', spec_set=True, + autospec=True) + def test_set_power_state_timeout(self, mock_set_power): + mock_set_power.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.set_power_state(task, states.POWER_ON, + timeout=2) + mock_set_power.assert_called_once_with(task, states.POWER_ON, + timeout=2) @mock.patch.object(irmc_power, '_set_power_state', spec_set=True, autospec=True) @@ -171,7 +360,22 @@ class IRMCPowerTestCase(db_base.DbTestCase): task.driver.power.reboot(task) mock_get_power.assert_called_once_with( task.driver.power, task) - mock_set_power.assert_called_once_with(task, states.REBOOT) + mock_set_power.assert_called_once_with(task, states.REBOOT, + timeout=None) + + @mock.patch.object(irmc_power, '_set_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_power.IRMCPower, 'get_power_state', spec_set=True, + autospec=True) + def test_reboot_reboot_timeout(self, mock_get_power, mock_set_power): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_get_power.return_value = states.POWER_ON + task.driver.power.reboot(task, timeout=2) + mock_get_power.assert_called_once_with( + task.driver.power, task) + mock_set_power.assert_called_once_with(task, states.REBOOT, + timeout=2) @mock.patch.object(irmc_power, '_set_power_state', spec_set=True, autospec=True) @@ -184,4 +388,19 @@ class IRMCPowerTestCase(db_base.DbTestCase): task.driver.power.reboot(task) mock_get_power.assert_called_once_with( task.driver.power, task) - mock_set_power.assert_called_once_with(task, states.POWER_ON) + mock_set_power.assert_called_once_with(task, states.POWER_ON, + timeout=None) + + @mock.patch.object(irmc_power, '_set_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_power.IRMCPower, 'get_power_state', spec_set=True, + autospec=True) + def test_reboot_power_on_timeout(self, mock_get_power, mock_set_power): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_get_power.return_value = states.POWER_OFF + task.driver.power.reboot(task, timeout=2) + mock_get_power.assert_called_once_with( + task.driver.power, task) + mock_set_power.assert_called_once_with(task, states.POWER_ON, + timeout=2) diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py index 0519e8c8ab..0dad0257c9 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -113,6 +113,8 @@ SCCICLIENT_IRMC_SCCI_SPEC = ( 'POWER_OFF', 'POWER_ON', 'POWER_RESET', + 'POWER_SOFT_CYCLE', + 'POWER_SOFT_OFF', 'MOUNT_CD', 'UNMOUNT_CD', 'MOUNT_FD', diff --git a/releasenotes/notes/irmc-soft-power-281a83647baa4b23.yaml b/releasenotes/notes/irmc-soft-power-281a83647baa4b23.yaml new file mode 100644 index 0000000000..9a3253d246 --- /dev/null +++ b/releasenotes/notes/irmc-soft-power-281a83647baa4b23.yaml @@ -0,0 +1,4 @@ +--- +features: + - Adds support for ``soft reboot`` and ``soft power off`` to + iRMC driver.