Support waiting for bare metal power states

Use the new functionality to fix a race in the functional job.

Depends-On: https://review.opendev.org/757293
Change-Id: Icc881acebd72ed75e654677d2d5dbff63c969298
This commit is contained in:
Dmitry Tantsur 2020-10-12 11:49:46 +02:00
parent 8c44ff176c
commit 97f7095abd
8 changed files with 142 additions and 13 deletions

View File

@ -18,8 +18,8 @@ Node Operations
:noindex:
:members: nodes, find_node, get_node, create_node, update_node, patch_node, delete_node,
validate_node, set_node_power_state, set_node_provision_state,
wait_for_nodes_provision_state, wait_for_node_reservation,
set_node_maintenance, unset_node_maintenance
wait_for_nodes_provision_state, wait_for_node_power_state,
wait_for_node_reservation, set_node_maintenance, unset_node_maintenance
Port Operations
^^^^^^^^^^^^^^^

View File

@ -11,6 +11,14 @@ The ``Node`` class inherits from :class:`~openstack.resource.Resource`.
.. autoclass:: openstack.baremetal.v1.node.Node
:members:
The PowerAction Class
^^^^^^^^^^^^^^^^^^^^^
The ``PowerAction`` enumeration represents known power actions.
.. autoclass:: openstack.baremetal.v1.node.PowerAction
:members:
The ValidationResult Class
^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -49,6 +49,15 @@ EXPECTED_STATES = {
}
"""Mapping of provisioning actions to expected stable states."""
EXPECTED_POWER_STATES = {
'power on': 'power on',
'power off': 'power off',
'rebooting': 'power on',
'soft power off': 'power off',
'soft rebooting': 'power on',
}
"""Mapping of target power states to expected power states."""
STATE_VERSIONS = {
'enroll': '1.11',
'manageable': '1.4',

View File

@ -442,7 +442,7 @@ class Proxy(proxy.Proxy):
else:
return _node.WaitResult(finished, failed, remaining)
def set_node_power_state(self, node, target):
def set_node_power_state(self, node, target, wait=False, timeout=None):
"""Run an action modifying node's power state.
This call is asynchronous, it will return success as soon as the Bare
@ -450,10 +450,30 @@ class Proxy(proxy.Proxy):
:param node: The value can be the name or ID of a node or a
:class:`~openstack.baremetal.v1.node.Node` instance.
:param target: Target power state, e.g. "rebooting", "power on".
See the Bare Metal service documentation for available actions.
:param target: Target power state, one of
:class:`~openstack.baremetal.v1.node.PowerAction` or a string.
:param wait: Whether to wait for the node to get into the expected
state.
:param timeout: If ``wait`` is set to ``True``, specifies how much (in
seconds) to wait for the expected state to be reached. The value of
``None`` (the default) means no client-side timeout.
"""
self._get_resource(_node.Node, node).set_power_state(self, target)
self._get_resource(_node.Node, node).set_power_state(
self, target, wait=wait, timeout=timeout)
def wait_for_node_power_state(self, node, expected_state, timeout=None):
"""Wait for the node to reach the power state.
:param node: The value can be the name or ID of a node or a
:class:`~openstack.baremetal.v1.node.Node` instance.
:param timeout: How much (in seconds) to wait for the target state
to be reached. The value of ``None`` (the default) means
no timeout.
:returns: The updated :class:`~openstack.baremetal.v1.node.Node`
"""
res = self._get_resource(_node.Node, node)
return res.wait_for_power_state(self, expected_state, timeout=timeout)
def wait_for_node_reservation(self, node, timeout=None):
"""Wait for a lock on the node to be released.

View File

@ -11,6 +11,7 @@
# under the License.
import collections
import enum
from openstack.baremetal.v1 import _common
from openstack import exceptions
@ -32,6 +33,24 @@ class ValidationResult:
self.reason = reason
class PowerAction(enum.Enum):
"""Mapping from an action to a target power state."""
POWER_ON = 'power on'
"""Power on the node."""
POWER_OFF = 'power off'
"""Power off the node (using hard power off)."""
REBOOT = 'rebooting'
"""Reboot the node (using hard power off)."""
SOFT_POWER_OFF = 'soft power off'
"""Power off the node using soft power off."""
SOFT_REBOOT = 'soft rebooting'
"""Reboot the node using soft power off."""
class WaitResult(collections.namedtuple('WaitResult',
['success', 'failure', 'timeout'])):
"""A named tuple representing a result of waiting for several nodes.
@ -416,6 +435,34 @@ class Node(_common.ListMixin, resource.Resource):
else:
return self.fetch(session)
def wait_for_power_state(self, session, expected_state, timeout=None):
"""Wait for the node to reach the expected power state.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param expected_state: The expected power state to reach.
:param timeout: If ``wait`` is set to ``True``, specifies how much (in
seconds) to wait for the expected state to be reached. The value of
``None`` (the default) means no client-side timeout.
:return: This :class:`Node` instance.
:raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout.
"""
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for node %(node)s to reach "
"power state '%(state)s'" % {'node': self.id,
'state': expected_state}):
self.fetch(session)
if self.power_state == expected_state:
return self
session.log.debug(
'Still waiting for node %(node)s to reach power state '
'"%(target)s", the current state is "%(state)s"',
{'node': self.id, 'target': expected_state,
'state': self.power_state})
def wait_for_provision_state(self, session, expected_state, timeout=None,
abort_on_failed_state=True):
"""Wait for the node to reach the expected state.
@ -532,8 +579,7 @@ class Node(_common.ListMixin, resource.Resource):
"the last error is %(error)s" %
{'node': self.id, 'error': self.last_error})
# TODO(dtantsur): waiting for power state
def set_power_state(self, session, target):
def set_power_state(self, session, target, wait=False, timeout=None):
"""Run an action modifying this node's power state.
This call is asynchronous, it will return success as soon as the Bare
@ -541,9 +587,22 @@ class Node(_common.ListMixin, resource.Resource):
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param target: Target power state, e.g. "rebooting", "power on".
See the Bare Metal service documentation for available actions.
:param target: Target power state, as a :class:`PowerAction` or
a string.
:param wait: Whether to wait for the expected power state to be
reached.
:param timeout: Timeout (in seconds) to wait for the target state to be
reached. If ``None``, wait without timeout.
"""
if isinstance(target, PowerAction):
target = target.value
if wait:
try:
expected = _common.EXPECTED_POWER_STATES[target]
except KeyError:
raise ValueError("Cannot use target power state %s with wait, "
"the expected state is not known" % target)
session = self._get_session(session)
if target.startswith("soft "):
@ -567,6 +626,9 @@ class Node(_common.ListMixin, resource.Resource):
"to {target}".format(node=self.id, target=target))
exceptions.raise_from_response(response, error_message=msg)
if wait:
self.wait_for_power_state(session, expected, timeout=timeout)
def attach_vif(self, session, vif_id, retry_on_conflict=True):
"""Attach a VIF to the node.

View File

@ -177,12 +177,11 @@ class TestBareMetalNode(base.BaseBaremetalTest):
node = self.create_node()
self.assertIsNone(node.power_state)
self.conn.baremetal.set_node_power_state(node, 'power on')
self.conn.baremetal.set_node_power_state(node, 'power on', wait=True)
node = self.conn.baremetal.get_node(node.id)
# Fake nodes react immediately to power requests.
self.assertEqual('power on', node.power_state)
self.conn.baremetal.set_node_power_state(node, 'power off')
self.conn.baremetal.set_node_power_state(node, 'power off', wait=True)
node = self.conn.baremetal.get_node(node.id)
self.assertEqual('power off', node.power_state)

View File

@ -811,3 +811,30 @@ class TestNodePatch(base.TestCase):
self.assertIn('1.45', commit_args)
self.assertEqual(commit_kwargs['retry_on_conflict'], True)
mock_patch.assert_not_called()
@mock.patch('time.sleep', lambda _t: None)
@mock.patch.object(node.Node, 'fetch', autospec=True)
class TestNodeWaitForPowerState(base.TestCase):
def setUp(self):
super(TestNodeWaitForPowerState, self).setUp()
self.node = node.Node(**FAKE)
self.session = mock.Mock()
def test_success(self, mock_fetch):
self.node.power_state = 'power on'
def _get_side_effect(_self, session):
self.node.power_state = 'power off'
self.assertIs(session, self.session)
mock_fetch.side_effect = _get_side_effect
node = self.node.wait_for_power_state(self.session, 'power off')
self.assertIs(node, self.node)
def test_timeout(self, mock_fetch):
self.node.power_state = 'power on'
self.assertRaises(exceptions.ResourceTimeout,
self.node.wait_for_power_state,
self.session, 'power off', timeout=0.001)

View File

@ -0,0 +1,4 @@
---
features:
- |
Support waiting for bare metal power states.