diff --git a/ironic/conductor/utils.py b/ironic/conductor/utils.py index 4316dcdb71..2667862e10 100644 --- a/ironic/conductor/utils.py +++ b/ironic/conductor/utils.py @@ -124,6 +124,12 @@ def _can_skip_state_change(task, new_state): :returns: True if should ignore the requested power state change. False otherwise """ + # We only ignore certain state changes. So if the desired new_state is not + # one of them, then we can return early and not do an un-needed + # get_power_state() call + if new_state not in (states.POWER_ON, states.POWER_OFF, + states.SOFT_POWER_OFF): + return False node = task.node diff --git a/ironic/drivers/modules/ipmitool.py b/ironic/drivers/modules/ipmitool.py index 18c2e281bd..b6c505c3c9 100644 --- a/ironic/drivers/modules/ipmitool.py +++ b/ironic/drivers/modules/ipmitool.py @@ -833,7 +833,11 @@ class IPMIPower(base.PowerInterface): """ driver_info = _parse_driver_info(task.node) - _power_off(task, driver_info, timeout=timeout) + # NOTE(jlvillal): Some BMCs will error if setting power state to off if + # the node is already turned off. + current_status = _power_status(driver_info) + if current_status != states.POWER_OFF: + _power_off(task, driver_info, timeout=timeout) driver_utils.ensure_next_boot_device(task, driver_info) _power_on(task, driver_info, timeout=timeout) diff --git a/ironic/tests/unit/conductor/test_utils.py b/ironic/tests/unit/conductor/test_utils.py index 9496eeca54..7c3649ef13 100644 --- a/ironic/tests/unit/conductor/test_utils.py +++ b/ironic/tests/unit/conductor/test_utils.py @@ -179,7 +179,10 @@ class NodePowerActionTestCase(db_base.DbTestCase): task = task_manager.TaskManager(self.context, node.uuid) with mock.patch.object(self.driver.power, 'reboot') as reboot_mock: - conductor_utils.node_power_action(task, states.REBOOT) + with mock.patch.object(self.driver.power, + 'get_power_state') as get_power_mock: + conductor_utils.node_power_action(task, states.REBOOT) + self.assertFalse(get_power_mock.called) node.refresh() reboot_mock.assert_called_once_with(mock.ANY) @@ -205,7 +208,7 @@ class NodePowerActionTestCase(db_base.DbTestCase): "INVALID_POWER_STATE") node.refresh() - get_power_mock.assert_called_once_with(mock.ANY) + self.assertFalse(get_power_mock.called) self.assertEqual(states.POWER_ON, node['power_state']) self.assertIsNone(node['target_power_state']) self.assertIsNotNone(node['last_error']) @@ -240,7 +243,7 @@ class NodePowerActionTestCase(db_base.DbTestCase): "INVALID_POWER_STATE") node.refresh() - get_power_mock.assert_called_once_with(mock.ANY) + self.assertFalse(get_power_mock.called) self.assertEqual(states.POWER_ON, node.power_state) self.assertIsNone(node.target_power_state) self.assertIsNotNone(node.last_error) @@ -720,7 +723,7 @@ class NodeSoftPowerActionTestCase(db_base.DbTestCase): conductor_utils.node_power_action(task, states.SOFT_REBOOT) node.refresh() - get_power_mock.assert_called_once_with(mock.ANY) + self.assertFalse(get_power_mock.called) self.assertEqual(states.POWER_ON, node['power_state']) self.assertIsNone(node['target_power_state']) self.assertIsNone(node['last_error']) @@ -741,7 +744,7 @@ class NodeSoftPowerActionTestCase(db_base.DbTestCase): timeout=2) node.refresh() - get_power_mock.assert_called_once_with(mock.ANY) + self.assertFalse(get_power_mock.called) self.assertEqual(states.POWER_ON, node['power_state']) self.assertIsNone(node['target_power_state']) self.assertIsNone(node['last_error']) diff --git a/ironic/tests/unit/drivers/modules/test_ipmitool.py b/ironic/tests/unit/drivers/modules/test_ipmitool.py index 9baee0f1d2..37b8b25981 100644 --- a/ironic/tests/unit/drivers/modules/test_ipmitool.py +++ b/ironic/tests/unit/drivers/modules/test_ipmitool.py @@ -1680,6 +1680,8 @@ class IPMIToolDriverTestCase(db_base.DbTestCase): @mock.patch.object(driver_utils, 'ensure_next_boot_device', autospec=True) @mock.patch.object(ipmi, '_power_off', spec_set=types.FunctionType) @mock.patch.object(ipmi, '_power_on', spec_set=types.FunctionType) + @mock.patch.object(ipmi, '_power_status', + lambda driver_info: states.POWER_ON) def test_reboot_ok(self, mock_on, mock_off, mock_next_boot): manager = mock.MagicMock() # NOTE(rloo): if autospec is True, then manager.mock_calls is empty @@ -1695,11 +1697,34 @@ class IPMIToolDriverTestCase(db_base.DbTestCase): self.driver.power.reboot(task) mock_next_boot.assert_called_once_with(task, self.info) - self.assertEqual(manager.mock_calls, expected) + self.assertEqual(expected, manager.mock_calls) @mock.patch.object(driver_utils, 'ensure_next_boot_device', autospec=True) @mock.patch.object(ipmi, '_power_off', spec_set=types.FunctionType) @mock.patch.object(ipmi, '_power_on', spec_set=types.FunctionType) + @mock.patch.object(ipmi, '_power_status', + lambda driver_info: states.POWER_OFF) + def test_reboot_already_off(self, mock_on, mock_off, mock_next_boot): + manager = mock.MagicMock() + # NOTE(rloo): if autospec is True, then manager.mock_calls is empty + mock_off.return_value = states.POWER_OFF + mock_on.return_value = states.POWER_ON + manager.attach_mock(mock_off, 'power_off') + manager.attach_mock(mock_on, 'power_on') + + with task_manager.acquire(self.context, + self.node.uuid) as task: + expected = [mock.call.power_on(task, self.info, timeout=None)] + self.driver.power.reboot(task) + mock_next_boot.assert_called_once_with(task, self.info) + + self.assertEqual(expected, manager.mock_calls) + + @mock.patch.object(driver_utils, 'ensure_next_boot_device', autospec=True) + @mock.patch.object(ipmi, '_power_off', spec_set=types.FunctionType) + @mock.patch.object(ipmi, '_power_on', spec_set=types.FunctionType) + @mock.patch.object(ipmi, '_power_status', + lambda driver_info: states.POWER_ON) def test_reboot_timeout_ok(self, mock_on, mock_off, mock_next_boot): manager = mock.MagicMock() # NOTE(rloo): if autospec is True, then manager.mock_calls is empty @@ -1714,10 +1739,12 @@ class IPMIToolDriverTestCase(db_base.DbTestCase): self.driver.power.reboot(task, timeout=2) mock_next_boot.assert_called_once_with(task, self.info) - self.assertEqual(manager.mock_calls, expected) + self.assertEqual(expected, manager.mock_calls) @mock.patch.object(ipmi, '_power_off', spec_set=types.FunctionType) @mock.patch.object(ipmi, '_power_on', spec_set=types.FunctionType) + @mock.patch.object(ipmi, '_power_status', + lambda driver_info: states.POWER_ON) def test_reboot_fail_power_off(self, mock_on, mock_off): manager = mock.MagicMock() # NOTE(rloo): if autospec is True, then manager.mock_calls is empty @@ -1733,10 +1760,12 @@ class IPMIToolDriverTestCase(db_base.DbTestCase): self.driver.power.reboot, task) - self.assertEqual(manager.mock_calls, expected) + self.assertEqual(expected, manager.mock_calls) @mock.patch.object(ipmi, '_power_off', spec_set=types.FunctionType) @mock.patch.object(ipmi, '_power_on', spec_set=types.FunctionType) + @mock.patch.object(ipmi, '_power_status', + lambda driver_info: states.POWER_ON) def test_reboot_fail_power_on(self, mock_on, mock_off): manager = mock.MagicMock() # NOTE(rloo): if autospec is True, then manager.mock_calls is empty @@ -1754,10 +1783,12 @@ class IPMIToolDriverTestCase(db_base.DbTestCase): self.driver.power.reboot, task) - self.assertEqual(manager.mock_calls, expected) + self.assertEqual(expected, manager.mock_calls) @mock.patch.object(ipmi, '_power_off', spec_set=types.FunctionType) @mock.patch.object(ipmi, '_power_on', spec_set=types.FunctionType) + @mock.patch.object(ipmi, '_power_status', + lambda driver_info: states.POWER_ON) def test_reboot_timeout_fail(self, mock_on, mock_off): manager = mock.MagicMock() # NOTE(rloo): if autospec is True, then manager.mock_calls is empty @@ -1774,7 +1805,7 @@ class IPMIToolDriverTestCase(db_base.DbTestCase): self.driver.power.reboot, task, timeout=2) - self.assertEqual(manager.mock_calls, expected) + self.assertEqual(expected, manager.mock_calls) @mock.patch.object(ipmi, '_parse_driver_info', autospec=True) def test_vendor_passthru_validate__parse_driver_info_fail(self, info_mock): diff --git a/releasenotes/notes/reboot-do-not-power-off-if-already-1452256167d40009.yaml b/releasenotes/notes/reboot-do-not-power-off-if-already-1452256167d40009.yaml new file mode 100644 index 0000000000..2b6f60d45d --- /dev/null +++ b/releasenotes/notes/reboot-do-not-power-off-if-already-1452256167d40009.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixes a problem when using ipmitool and rebooting a node which would cause + a deploy to fail. Now when rebooting a node we check if the node is already + powered off, if it is we don't attempt to power off the node. This is + because some BMCs will error if the node is already powered off and an + ipmitool request is made to power it off. See + https://bugs.launchpad.net/ironic/+bug/1718794 for details.