diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index 1281602117..d27c990a44 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -3669,7 +3669,8 @@ def do_sync_power_state(task, count): handle_sync_power_state_max_retries_exceeded(task, power_state) return count - if CONF.conductor.force_power_state_during_sync: + if (CONF.conductor.force_power_state_during_sync + and task.driver.power.supports_power_sync(task)): LOG.warning("During sync_power_state, node %(node)s state " "'%(actual)s' does not match expected state. " "Changing hardware state to '%(state)s'.", diff --git a/ironic/drivers/base.py b/ironic/drivers/base.py index 3fbecba492..c33ad05b49 100644 --- a/ironic/drivers/base.py +++ b/ironic/drivers/base.py @@ -616,6 +616,18 @@ class PowerInterface(BaseInterface): """ return [states.POWER_ON, states.POWER_OFF, states.REBOOT] + def supports_power_sync(self, task): + """Check if power sync is supported for the given node. + + If ``False``, the conductor will simply store whatever + ``get_power_state`` returns in the database instead of trying + to force the expected power state. + + :param task: A TaskManager instance containing the node to act on. + :returns: boolean, whether power sync is supported. + """ + return True + class ConsoleInterface(BaseInterface): """Interface for console-related actions.""" diff --git a/ironic/drivers/modules/agent_base.py b/ironic/drivers/modules/agent_base.py index 267214073f..49debc5a90 100644 --- a/ironic/drivers/modules/agent_base.py +++ b/ironic/drivers/modules/agent_base.py @@ -800,8 +800,15 @@ class AgentOobStepsMixin(object): :param task: a TaskManager object containing the node """ + can_power_on = (states.POWER_ON in + task.driver.power.get_supported_power_states(task)) try: - manager_utils.node_power_action(task, states.POWER_ON) + if can_power_on: + manager_utils.node_power_action(task, states.POWER_ON) + else: + LOG.debug('Not trying to power on node %s that does not ' + 'support powering on, assuming already running', + task.node.uuid) except Exception as e: msg = (_('Error booting node %(node)s after deploy. ' '%(cls)s: %(error)s') % @@ -1168,9 +1175,17 @@ class AgentDeployMixin(HeartbeatMixin, AgentOobStepsMixin): # in-band methods oob_power_off = strutils.bool_from_string( node.driver_info.get('deploy_forces_oob_reboot', False)) + can_power_on = (states.POWER_ON in + task.driver.power.get_supported_power_states(task)) try: - if not oob_power_off: + if not can_power_on: + LOG.info('Power interface of node %(node)s does not support ' + 'power on, using reboot to switch to the instance', + node.uuid) + self._client.sync(node) + manager_utils.node_power_action(task, states.REBOOT) + elif not oob_power_off: try: self._client.power_off(node) except Exception as e: diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index 67ee3c570e..0d43fc98f2 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -4877,6 +4877,21 @@ class ManagerDoSyncPowerStateTestCase(db_base.DbTestCase): self.assertEqual(1, self.service.power_state_sync_count[self.node.uuid]) + @mock.patch.object(nova, 'power_update', autospec=True) + def test_no_power_sync_support(self, mock_power_update, node_power_action): + self.config(force_power_state_during_sync=True, group='conductor') + self.power.supports_power_sync.return_value = False + + self._do_sync_power_state(states.POWER_ON, states.POWER_OFF) + + self.assertFalse(self.power.validate.called) + self.power.get_power_state.assert_called_once_with(self.task) + self.assertFalse(node_power_action.called) + self.assertEqual(states.POWER_OFF, self.node.power_state) + self.task.upgrade_lock.assert_called_once_with() + mock_power_update.assert_called_once_with( + self.task.context, self.node.instance_uuid, states.POWER_OFF) + @mock.patch.object(nova, 'power_update', autospec=True) def test_max_retries_exceeded(self, mock_power_update, node_power_action): self.config(force_power_state_during_sync=True, group='conductor') diff --git a/ironic/tests/unit/drivers/modules/test_agent_base.py b/ironic/tests/unit/drivers/modules/test_agent_base.py index 25fa4dc230..dac8e2fca2 100644 --- a/ironic/tests/unit/drivers/modules/test_agent_base.py +++ b/ironic/tests/unit/drivers/modules/test_agent_base.py @@ -1100,6 +1100,29 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest): {'node': task.node.uuid, 'error': log_error}) self.assertFalse(mock_collect.called) + @mock.patch.object(manager_utils, 'power_on_node_if_needed', autospec=True) + @mock.patch.object(driver_utils, 'collect_ramdisk_logs', autospec=True) + @mock.patch.object(time, 'sleep', lambda seconds: None) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(fake.FakePower, 'get_supported_power_states', + lambda self, task: [states.REBOOT]) + @mock.patch.object(agent_client.AgentClient, 'sync', autospec=True) + def test_tear_down_agent_no_power_on_support( + self, sync_mock, node_power_action_mock, collect_mock, + power_on_node_if_needed_mock): + cfg.CONF.set_override('deploy_logs_collect', 'always', 'agent') + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.deploy.tear_down_agent(task) + node_power_action_mock.assert_called_once_with(task, states.REBOOT) + self.assertEqual(states.DEPLOYING, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + collect_mock.assert_called_once_with(task.node) + self.assertFalse(power_on_node_if_needed_mock.called) + sync_mock.assert_called_once_with(self.deploy._client, task.node) + @mock.patch.object(manager_utils, 'restore_power_state_if_needed', autospec=True) @mock.patch.object(manager_utils, 'power_on_node_if_needed', autospec=True) @@ -1145,6 +1168,27 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest): configure_tenant_net_mock.assert_called_once_with(mock.ANY, task) self.assertFalse(mock_collect.called) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + def test_boot_instance(self, node_power_action_mock): + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.deploy.boot_instance(task) + node_power_action_mock.assert_called_once_with(task, + states.POWER_ON) + + @mock.patch.object(fake.FakePower, 'get_supported_power_states', + lambda self, task: [states.REBOOT]) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + def test_boot_instance_no_power_on(self, node_power_action_mock): + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.deploy.boot_instance(task) + self.assertFalse(node_power_action_mock.called) + @mock.patch.object(agent_client.AgentClient, 'install_bootloader', autospec=True) @mock.patch.object(deploy_utils, 'try_set_boot_device', autospec=True) diff --git a/releasenotes/notes/no-power-on-842b21d55b07a632.yaml b/releasenotes/notes/no-power-on-842b21d55b07a632.yaml new file mode 100644 index 0000000000..71f4dc9eb6 --- /dev/null +++ b/releasenotes/notes/no-power-on-842b21d55b07a632.yaml @@ -0,0 +1,9 @@ +--- +other: + - | + A new method ``supports_power_sync`` has been added to ``PowerInterface``. + If it returns ``False``, the conductor will not try to assert power state + for the node, merely recording the returned state instead. + - | + The base agent deploy interface code now correctly handles power interfaces + that do not support the ``power on`` action but support ``reboot``.