Account for power interfaces that cannot power on
The future agent power interface will only be capable of rebooting, so: 1) Adjust agent_base to be able to use reboot instead power off+on 2) Adjust power sync to avoid trying to force power state for such nodes Change-Id: Ia95a68729f684a06c722539816eadea5ebb80d1a Story: #2007771 Task: #40381
This commit is contained in:
parent
3b6163afd2
commit
e804f6c56b
ironic
conductor
drivers
tests/unit
releasenotes/notes
@ -3669,7 +3669,8 @@ def do_sync_power_state(task, count):
|
|||||||
handle_sync_power_state_max_retries_exceeded(task, power_state)
|
handle_sync_power_state_max_retries_exceeded(task, power_state)
|
||||||
return count
|
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 "
|
LOG.warning("During sync_power_state, node %(node)s state "
|
||||||
"'%(actual)s' does not match expected state. "
|
"'%(actual)s' does not match expected state. "
|
||||||
"Changing hardware state to '%(state)s'.",
|
"Changing hardware state to '%(state)s'.",
|
||||||
|
@ -616,6 +616,18 @@ class PowerInterface(BaseInterface):
|
|||||||
"""
|
"""
|
||||||
return [states.POWER_ON, states.POWER_OFF, states.REBOOT]
|
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):
|
class ConsoleInterface(BaseInterface):
|
||||||
"""Interface for console-related actions."""
|
"""Interface for console-related actions."""
|
||||||
|
@ -800,8 +800,15 @@ class AgentOobStepsMixin(object):
|
|||||||
|
|
||||||
:param task: a TaskManager object containing the node
|
:param task: a TaskManager object containing the node
|
||||||
"""
|
"""
|
||||||
|
can_power_on = (states.POWER_ON in
|
||||||
|
task.driver.power.get_supported_power_states(task))
|
||||||
try:
|
try:
|
||||||
|
if can_power_on:
|
||||||
manager_utils.node_power_action(task, states.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:
|
except Exception as e:
|
||||||
msg = (_('Error booting node %(node)s after deploy. '
|
msg = (_('Error booting node %(node)s after deploy. '
|
||||||
'%(cls)s: %(error)s') %
|
'%(cls)s: %(error)s') %
|
||||||
@ -1168,9 +1175,17 @@ class AgentDeployMixin(HeartbeatMixin, AgentOobStepsMixin):
|
|||||||
# in-band methods
|
# in-band methods
|
||||||
oob_power_off = strutils.bool_from_string(
|
oob_power_off = strutils.bool_from_string(
|
||||||
node.driver_info.get('deploy_forces_oob_reboot', False))
|
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:
|
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:
|
try:
|
||||||
self._client.power_off(node)
|
self._client.power_off(node)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -4877,6 +4877,21 @@ class ManagerDoSyncPowerStateTestCase(db_base.DbTestCase):
|
|||||||
self.assertEqual(1,
|
self.assertEqual(1,
|
||||||
self.service.power_state_sync_count[self.node.uuid])
|
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)
|
@mock.patch.object(nova, 'power_update', autospec=True)
|
||||||
def test_max_retries_exceeded(self, mock_power_update, node_power_action):
|
def test_max_retries_exceeded(self, mock_power_update, node_power_action):
|
||||||
self.config(force_power_state_during_sync=True, group='conductor')
|
self.config(force_power_state_during_sync=True, group='conductor')
|
||||||
|
@ -1100,6 +1100,29 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
|
|||||||
{'node': task.node.uuid, 'error': log_error})
|
{'node': task.node.uuid, 'error': log_error})
|
||||||
self.assertFalse(mock_collect.called)
|
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',
|
@mock.patch.object(manager_utils, 'restore_power_state_if_needed',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(manager_utils, 'power_on_node_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)
|
configure_tenant_net_mock.assert_called_once_with(mock.ANY, task)
|
||||||
self.assertFalse(mock_collect.called)
|
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',
|
@mock.patch.object(agent_client.AgentClient, 'install_bootloader',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(deploy_utils, 'try_set_boot_device', autospec=True)
|
@mock.patch.object(deploy_utils, 'try_set_boot_device', autospec=True)
|
||||||
|
9
releasenotes/notes/no-power-on-842b21d55b07a632.yaml
Normal file
9
releasenotes/notes/no-power-on-842b21d55b07a632.yaml
Normal file
@ -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``.
|
Loading…
x
Reference in New Issue
Block a user