From f4fc39de7c2def4580398beeceb8e25fcef90a5a Mon Sep 17 00:00:00 2001 From: Shivanand Tendulker Date: Tue, 11 Sep 2018 23:32:16 -0400 Subject: [PATCH] Adds support for soft power operations to 'ilo' power interface Adds support for soft power-off and soft reboot operations to 'ilo' power interface. Change-Id: I38571f49d21007c00ddda28651569333e0823cad Story: #2003727 Task: #26394 --- ironic/conf/ilo.py | 4 + ironic/drivers/modules/ilo/common.py | 44 +++ ironic/drivers/modules/ilo/power.py | 182 +++++++++--- .../unit/drivers/modules/ilo/test_common.py | 42 +++ .../unit/drivers/modules/ilo/test_power.py | 258 +++++++++++++++--- ...oft-power-operations-eaef33a3ff56b047.yaml | 10 + 6 files changed, 469 insertions(+), 71 deletions(-) create mode 100644 releasenotes/notes/ilo-soft-power-operations-eaef33a3ff56b047.yaml diff --git a/ironic/conf/ilo.py b/ironic/conf/ilo.py index 884d88d17f..467b185739 100644 --- a/ironic/conf/ilo.py +++ b/ironic/conf/ilo.py @@ -64,6 +64,10 @@ opts = [ 'password.')), cfg.IntOpt('power_retry', default=6, + deprecated_for_removal=True, + deprecated_reason=_('This configuration option is replaced ' + 'by [conductor] soft_power_off_timeout, ' + 'please use that instead.'), help=_('Number of times a power operation needs to be ' 'retried')), cfg.IntOpt('power_wait', diff --git a/ironic/drivers/modules/ilo/common.py b/ironic/drivers/modules/ilo/common.py index a851cf2bb2..80ac80847d 100644 --- a/ironic/drivers/modules/ilo/common.py +++ b/ironic/drivers/modules/ilo/common.py @@ -96,6 +96,27 @@ BOOT_MODE_GENERIC_TO_ILO = {'bios': 'legacy', 'uefi': 'uefi'} BOOT_MODE_ILO_TO_GENERIC = dict( (v, k) for (k, v) in BOOT_MODE_GENERIC_TO_ILO.items()) +POST_NULL_STATE = 'Null' +""" Node is in Null post state.""" + +POST_UNKNOWN_STATE = "Unknown" +""" Node is in Unknown post state.""" + +POST_RESET_STATE = "Reset" +""" Node is in Reset post state.""" + +POST_POWEROFF_STATE = "PowerOff" +""" Node is in PowerOff post state.""" + +POST_INPOST_STATE = "InPost" +""" Node is in InPost post state.""" + +POST_INPOSTDISCOVERY_STATE = "InPostDiscoveryComplete" +""" Node is in InPostDiscoveryComplete post state.""" + +POST_FINISHEDPOST_STATE = "FinishedPost" +""" Node is in FinishedPost post state.""" + def copy_image_to_web_server(source_file_path, destination): """Copies the given image to the http web server. @@ -785,3 +806,26 @@ def verify_image_checksum(image_location, expected_checksum): LOG.error(msg) raise exception.ImageRefValidationFailed(image_href=image_location, reason=msg) + + +def get_server_post_state(node): + """Get the current state of system POST. + + :param node: an ironic node object. + :returns: POST state of the server. The valida states are:- + null, Unknown, Reset, PowerOff, InPost, InPostDiscoveryComplete + and FinishedPost. + :raises: IloOperationError on an error from IloClient library. + :raises: IloOperationNotSupported if retrieving post state is not + supported on the server. + """ + ilo_object = get_ilo_object(node) + operation = _("Get server post state for node %s.") % node.uuid + try: + return ilo_object.get_host_post_state() + except ilo_error.IloCommandNotSupportedError as ilo_exception: + raise exception.IloOperationNotSupported(operation=operation, + error=ilo_exception) + except ilo_error.IloError as ilo_exception: + raise exception.IloOperationError(operation=operation, + error=ilo_exception) diff --git a/ironic/drivers/modules/ilo/power.py b/ironic/drivers/modules/ilo/power.py index a7edc4d514..94861f7b6a 100644 --- a/ironic/drivers/modules/ilo/power.py +++ b/ironic/drivers/modules/ilo/power.py @@ -94,46 +94,113 @@ def _get_power_state(node): return states.ERROR -def _wait_for_state_change(node, target_state): - """Wait for the power state change to get reflected.""" +def _wait_for_state_change(node, target_state, + is_final_state=True, timeout=None): + """Wait for the power state change to get reflected. + + :param node: The node. + :param target_state: target 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] + force_legacy_behavior = False + interval = CONF.ilo.power_wait + if timeout: + max_retry = int(timeout / interval) + elif CONF.ilo.power_retry: + # Do not use post state to track power state as its too small + # timeout value to track post state when server is powered on. + # The total timeout value would be 12 secs with default values + # of configs ilo.power_retry and ilo.power_wait. + # Use older ways of get_power_state() to track the power state + # changes, instead. + force_legacy_behavior = True + max_retry = CONF.ilo.power_retry + 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 not force_legacy_behavior and _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: + state_to_check = ilo_common.POST_FINISHEDPOST_STATE def _wait(state): - - state[0] = _get_power_state(node) + 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] == target_state: + if retries[0] != 0 and state[0] == state_to_check: raise loopingcall.LoopingCallDone() - if retries[0] > CONF.ilo.power_retry: + 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=CONF.ilo.power_wait).wait() - - return state[0] + 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): +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] + + 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: @@ -145,6 +212,8 @@ def _set_power_state(task, target_state): _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 @@ -159,15 +228,58 @@ def _set_power_state(task, target_state): raise exception.IloOperationError(operation=operation, error=ilo_exception) - # Wait till the state change gets reflected. - state = _wait_for_state_change(node, target_state) + # 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, 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, is_final_state=True, + timeout=rem_timeout) + else: + time_consumed = _wait_for_state_change( + node, target_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}) - if state != target_state: - timeout = (CONF.ilo.power_wait) * (CONF.ilo.power_retry) - LOG.error("iLO failed to change state to %(tstate)s " - "within %(timeout)s sec", - {'tstate': target_state, 'timeout': timeout}) - raise exception.PowerStateFailure(pstate=target_state) + +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): @@ -211,14 +323,7 @@ class IloPower(base.PowerInterface): :raises: IloOperationError on an error from IloClient library. :raises: PowerStateFailure if the power couldn't be set to power_state. """ - # TODO(rloo): Support timeouts! - if timeout is not None: - LOG.warning("The 'ilo' Power Interface's 'set_power_state' method " - "doesn't support the 'timeout' parameter. Ignoring " - "timeout=%(timeout)s", - {'timeout': timeout}) - - _set_power_state(task, power_state) + _set_power_state(task, power_state, timeout=timeout) @METRICS.timer('IloPower.reboot') @task_manager.require_exclusive_lock @@ -231,16 +336,21 @@ class IloPower(base.PowerInterface): POWER_ON. :raises: IloOperationError on an error from IloClient library. """ - # TODO(rloo): Support timeouts! - if timeout is not None: - LOG.warning("The 'ilo' Power Interface's 'reboot' method " - "doesn't support the 'timeout' parameter. Ignoring " - "timeout=%(timeout)s", - {'timeout': timeout}) - node = task.node current_pstate = _get_power_state(node) 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('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] diff --git a/ironic/tests/unit/drivers/modules/ilo/test_common.py b/ironic/tests/unit/drivers/modules/ilo/test_common.py index 5299b5a95f..64a270ad2d 100644 --- a/ironic/tests/unit/drivers/modules/ilo/test_common.py +++ b/ironic/tests/unit/drivers/modules/ilo/test_common.py @@ -1071,3 +1071,45 @@ class IloCommonMethodsTestCase(BaseIloTest): ilo_common.verify_image_checksum, file_like_object, invalid_hash) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_server_post_state(self, + get_ilo_object_mock): + ilo_object_mock = get_ilo_object_mock.return_value + post_state = 'FinishedPost' + ilo_object_mock.get_host_post_state.return_value = post_state + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ret = ilo_common.get_server_post_state(task.node) + ilo_object_mock.get_host_post_state.assert_called_once_with() + self.assertEqual(post_state, ret) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_server_post_state_fail(self, + get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + exc = ilo_error.IloError('error') + ilo_mock_object.get_host_post_state.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + ilo_common.get_server_post_state, task.node) + ilo_mock_object.get_host_post_state.assert_called_once_with() + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_server_post_state_not_supported(self, + ilo_object_mock): + ilo_mock_object = ilo_object_mock.return_value + exc = ilo_error.IloCommandNotSupportedError('error') + ilo_mock_object.get_host_post_state.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationNotSupported, + ilo_common.get_server_post_state, + task.node) + ilo_mock_object.get_host_post_state.assert_called_once_with() diff --git a/ironic/tests/unit/drivers/modules/ilo/test_power.py b/ironic/tests/unit/drivers/modules/ilo/test_power.py index 48783d0518..4c2905cc72 100644 --- a/ironic/tests/unit/drivers/modules/ilo/test_power.py +++ b/ironic/tests/unit/drivers/modules/ilo/test_power.py @@ -128,6 +128,199 @@ class IloPowerInternalMethodsTestCase(test_common.BaseIloTest): ilo_mock_object.get_host_power_status.assert_called_with() ilo_mock_object.set_host_power.assert_called_once_with('ON') + @mock.patch.object(ilo_power.LOG, 'info') + @mock.patch.object(ilo_power, '_attach_boot_iso_if_needed', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'get_server_post_state', spec_set=True, + autospec=True) + def test__set_power_state_soft_reboot_ok( + self, get_post_mock, attach_boot_iso_mock, + log_mock, get_ilo_object_mock): + CONF.set_override('power_wait', 1, 'ilo') + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_host_power_status.return_value = 'ON' + get_post_mock.side_effect = ( + ['FinishedPost', 'FinishedPost', 'PowerOff', 'PowerOff', 'InPost', + 'FinishedPost']) + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_power._set_power_state(task, states.SOFT_REBOOT, timeout=3) + get_post_mock.assert_called_with(task.node) + ilo_mock_object.press_pwr_btn.assert_called_once_with() + attach_boot_iso_mock.assert_called_once_with(task) + ilo_mock_object.set_host_power.assert_called_once_with('ON') + log_mock.assert_called_once_with( + "The node %(node_id)s operation of '%(state)s' " + "is completed in %(time_consumed)s seconds.", + {'state': 'soft rebooting', 'node_id': task.node.uuid, + 'time_consumed': 2}) + + @mock.patch.object(ilo_power.LOG, 'info') + @mock.patch.object(ilo_power, '_attach_boot_iso_if_needed', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'get_server_post_state', spec_set=True, + autospec=True) + def test__set_power_state_soft_reboot_ok_initial_power_off( + self, get_post_mock, attach_boot_iso_mock, + log_mock, get_ilo_object_mock): + CONF.set_override('power_wait', 1, 'ilo') + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_host_power_status.return_value = 'OFF' + get_post_mock.side_effect = ['FinishedPost', 'PowerOff', + 'FinishedPost'] + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_power._set_power_state(task, states.SOFT_REBOOT, timeout=3) + get_post_mock.assert_called_with(task.node) + attach_boot_iso_mock.assert_called_once_with(task) + ilo_mock_object.set_host_power.assert_called_once_with('ON') + log_mock.assert_called_once_with( + "The node %(node_id)s operation of '%(state)s' " + "is completed in %(time_consumed)s seconds.", + {'state': 'power on', 'node_id': task.node.uuid, + 'time_consumed': 1}) + + @mock.patch.object(ilo_power.LOG, 'info') + @mock.patch.object(ilo_power, '_attach_boot_iso_if_needed', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'get_server_post_state', spec_set=True, + autospec=True) + def test__set_power_state_soft_reboot_fail_to_off( + self, get_post_mock, attach_boot_iso_mock, + log_mock, get_ilo_object_mock): + CONF.set_override('power_wait', 1, 'ilo') + exc = ilo_error.IloError('error') + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_host_power_status.return_value = 'ON' + ilo_mock_object.press_pwr_btn.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IloOperationError, + ilo_power._set_power_state, + task, states.SOFT_REBOOT, timeout=3) + ilo_mock_object.press_pwr_btn.assert_called_once_with() + self.assertFalse(get_post_mock.called) + self.assertFalse(attach_boot_iso_mock.called) + self.assertFalse(log_mock.called) + + @mock.patch.object(ilo_power.LOG, 'info') + @mock.patch.object(ilo_power, '_attach_boot_iso_if_needed', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'get_server_post_state', spec_set=True, + autospec=True) + def test__set_power_state_soft_reboot_fail_to_on( + self, get_post_mock, attach_boot_iso_mock, + log_mock, get_ilo_object_mock): + CONF.set_override('power_wait', 1, 'ilo') + exc = ilo_error.IloError('error') + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_host_power_status.return_value = 'ON' + get_post_mock.side_effect = ( + ['FinishedPost', 'PowerOff', 'PowerOff', 'InPost', + 'InPost', 'InPost', 'InPost', 'InPost']) + ilo_mock_object.press_pwr_btn.side_effect = [None, exc] + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.PowerStateFailure, + ilo_power._set_power_state, + task, states.SOFT_REBOOT, timeout=3) + get_post_mock.assert_called_with(task.node) + ilo_mock_object.press_pwr_btn.assert_called_once_with() + ilo_mock_object.set_host_power.assert_called_once_with('ON') + attach_boot_iso_mock.assert_called_once_with(task) + self.assertFalse(log_mock.called) + + @mock.patch.object(ilo_power.LOG, 'info') + @mock.patch.object(ilo_power, '_attach_boot_iso_if_needed', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'get_server_post_state', spec_set=True, + autospec=True) + def test__set_power_state_soft_reboot_timeout( + self, get_post_mock, attach_boot_iso_mock, + log_mock, get_ilo_object_mock): + CONF.set_override('power_wait', 1, 'ilo') + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_host_power_status.return_value = 'ON' + get_post_mock.side_effect = ['FinishedPost', 'FinishedPost', + 'PowerOff', 'InPost', 'InPost', 'InPost' + 'InPost', 'InPost'] + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.PowerStateFailure, + ilo_power._set_power_state, + task, states.SOFT_REBOOT, timeout=2) + get_post_mock.assert_called_with(task.node) + ilo_mock_object.press_pwr_btn.assert_called_once_with() + ilo_mock_object.set_host_power.assert_called_once_with('ON') + attach_boot_iso_mock.assert_called_once_with(task) + self.assertFalse(log_mock.called) + + @mock.patch.object(ilo_power.LOG, 'info') + @mock.patch.object(ilo_common, 'get_server_post_state', spec_set=True, + autospec=True) + def test__set_power_state_soft_power_off_ok( + self, get_post_mock, log_mock, get_ilo_object_mock): + CONF.set_override('power_wait', 1, 'ilo') + ilo_mock_object = get_ilo_object_mock.return_value + get_post_mock.side_effect = ['FinishedPost', 'FinishedPost', 'PowerOff' + 'PowerOff', 'PowerOff'] + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_power._set_power_state(task, states.SOFT_POWER_OFF, timeout=3) + get_post_mock.assert_called_with(task.node) + ilo_mock_object.press_pwr_btn.assert_called_once_with() + log_mock.assert_called_once_with( + "The node %(node_id)s operation of '%(state)s' " + "is completed in %(time_consumed)s seconds.", + {'state': 'soft power off', 'node_id': task.node.uuid, + 'time_consumed': 2}) + + @mock.patch.object(ilo_power.LOG, 'info') + @mock.patch.object(ilo_common, 'get_server_post_state', spec_set=True, + autospec=True) + def test__set_power_state_soft_power_off_fail( + self, get_post_mock, log_mock, get_ilo_object_mock): + CONF.set_override('power_wait', 1, 'ilo') + exc = ilo_error.IloError('error') + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_host_power_status.return_value = 'ON' + ilo_mock_object.press_pwr_btn.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IloOperationError, + ilo_power._set_power_state, + task, states.SOFT_POWER_OFF, timeout=2) + ilo_mock_object.press_pwr_btn.assert_called_once_with() + self.assertFalse(get_post_mock.called) + self.assertFalse(log_mock.called) + + @mock.patch.object(ilo_power.LOG, 'info') + @mock.patch.object(ilo_common, 'get_server_post_state', spec_set=True, + autospec=True) + def test__set_power_state_soft_power_off_timeout( + self, get_post_mock, log_mock, get_ilo_object_mock): + CONF.set_override('power_wait', 1, 'ilo') + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_host_power_status.return_value = 'ON' + get_post_mock.side_effect = ['FinishedPost', 'InPost', 'InPost', + 'InPost', 'InPost'] + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.PowerStateFailure, + ilo_power._set_power_state, + task, states.SOFT_POWER_OFF, timeout=2) + get_post_mock.assert_called_with(task.node) + ilo_mock_object.press_pwr_btn.assert_called_with() + self.assertFalse(log_mock.called) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, autospec=True) @mock.patch.object(ilo_common, 'setup_vmedia_for_boot', spec_set=True, @@ -197,55 +390,50 @@ class IloPowerTestCase(test_common.BaseIloTest): task.driver.power.get_power_state(task)) mock_get_power.assert_called_once_with(task.node) - @mock.patch.object(ilo_power.LOG, 'warning') @mock.patch.object(ilo_power, '_set_power_state', spec_set=True, autospec=True) - def test_set_power_state(self, mock_set_power, mock_log): - 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) - mock_set_power.assert_called_once_with(task, states.POWER_ON) - self.assertFalse(mock_log.called) - - @mock.patch.object(ilo_power.LOG, 'warning') - @mock.patch.object(ilo_power, '_set_power_state', spec_set=True, - autospec=True) - def test_set_power_state_timeout(self, mock_set_power, mock_log): + def _test_set_power_state(self, mock_set_power, timeout=None): 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=13) - mock_set_power.assert_called_once_with(task, states.POWER_ON) - self.assertTrue(mock_log.called) + timeout=timeout) + mock_set_power.assert_called_once_with(task, states.POWER_ON, + timeout=timeout) + + def test_set_power_state_no_timeout(self): + self._test_set_power_state(timeout=None) + + def test_set_power_state_timeout(self): + self._test_set_power_state(timeout=13) - @mock.patch.object(ilo_power.LOG, 'warning') @mock.patch.object(ilo_power, '_set_power_state', spec_set=True, autospec=True) @mock.patch.object(ilo_power, '_get_power_state', spec_set=True, autospec=True) - def test_reboot(self, mock_get_power, mock_set_power, mock_log): + def _test_reboot( + self, mock_get_power, mock_set_power, + timeout=None): + mock_get_power.return_value = states.POWER_ON with task_manager.acquire(self.context, self.node.uuid, shared=False) as task: - mock_get_power.return_value = states.POWER_ON - mock_set_power.return_value = states.POWER_ON - task.driver.power.reboot(task) + task.driver.power.reboot(task, timeout=timeout) mock_get_power.assert_called_once_with(task.node) - mock_set_power.assert_called_once_with(task, states.REBOOT) - self.assertFalse(mock_log.called) + mock_set_power.assert_called_once_with( + task, states.REBOOT, timeout=timeout) - @mock.patch.object(ilo_power.LOG, 'warning') - @mock.patch.object(ilo_power, '_set_power_state', spec_set=True, - autospec=True) - @mock.patch.object(ilo_power, '_get_power_state', spec_set=True, - autospec=True) - def test_reboot_timeout(self, mock_get_power, mock_set_power, mock_log): + def test_reboot_no_timeout(self): + self._test_reboot(timeout=None) + + def test_reboot_with_timeout(self): + self._test_reboot(timeout=100) + + def test_get_supported_power_states(self): with task_manager.acquire(self.context, self.node.uuid, shared=False) as task: - mock_get_power.return_value = states.POWER_ON - mock_set_power.return_value = states.POWER_ON - task.driver.power.reboot(task, timeout=123) - mock_get_power.assert_called_once_with(task.node) - mock_set_power.assert_called_once_with(task, states.REBOOT) - self.assertTrue(mock_log.called) + expected = [states.POWER_OFF, states.POWER_ON, states.REBOOT, + states.SOFT_POWER_OFF, states.SOFT_REBOOT] + self.assertEqual( + sorted(expected), + sorted(task.driver.power. + get_supported_power_states(task))) diff --git a/releasenotes/notes/ilo-soft-power-operations-eaef33a3ff56b047.yaml b/releasenotes/notes/ilo-soft-power-operations-eaef33a3ff56b047.yaml new file mode 100644 index 0000000000..06d828d01b --- /dev/null +++ b/releasenotes/notes/ilo-soft-power-operations-eaef33a3ff56b047.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Adds support for ``soft power off`` and ``soft reboot`` operations to + ``ilo`` power interface. +deprecations: + - | + The ``[ilo]/power_retry`` config is deprecated and will be removed in + the future release. Please use ``[conductor]/soft_power_off_timeout`` + instead.