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: :noindex:
:members: nodes, find_node, get_node, create_node, update_node, patch_node, delete_node, :members: nodes, find_node, get_node, create_node, update_node, patch_node, delete_node,
validate_node, set_node_power_state, set_node_provision_state, validate_node, set_node_power_state, set_node_provision_state,
wait_for_nodes_provision_state, wait_for_node_reservation, wait_for_nodes_provision_state, wait_for_node_power_state,
set_node_maintenance, unset_node_maintenance wait_for_node_reservation, set_node_maintenance, unset_node_maintenance
Port Operations Port Operations
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^

View File

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

View File

@ -49,6 +49,15 @@ EXPECTED_STATES = {
} }
"""Mapping of provisioning actions to expected stable 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 = { STATE_VERSIONS = {
'enroll': '1.11', 'enroll': '1.11',
'manageable': '1.4', 'manageable': '1.4',

View File

@ -442,7 +442,7 @@ class Proxy(proxy.Proxy):
else: else:
return _node.WaitResult(finished, failed, remaining) 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. """Run an action modifying node's power state.
This call is asynchronous, it will return success as soon as the Bare 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 :param node: The value can be the name or ID of a node or a
:class:`~openstack.baremetal.v1.node.Node` instance. :class:`~openstack.baremetal.v1.node.Node` instance.
:param target: Target power state, e.g. "rebooting", "power on". :param target: Target power state, one of
See the Bare Metal service documentation for available actions. :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): def wait_for_node_reservation(self, node, timeout=None):
"""Wait for a lock on the node to be released. """Wait for a lock on the node to be released.

View File

@ -11,6 +11,7 @@
# under the License. # under the License.
import collections import collections
import enum
from openstack.baremetal.v1 import _common from openstack.baremetal.v1 import _common
from openstack import exceptions from openstack import exceptions
@ -32,6 +33,24 @@ class ValidationResult:
self.reason = reason 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', class WaitResult(collections.namedtuple('WaitResult',
['success', 'failure', 'timeout'])): ['success', 'failure', 'timeout'])):
"""A named tuple representing a result of waiting for several nodes. """A named tuple representing a result of waiting for several nodes.
@ -416,6 +435,34 @@ class Node(_common.ListMixin, resource.Resource):
else: else:
return self.fetch(session) 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, def wait_for_provision_state(self, session, expected_state, timeout=None,
abort_on_failed_state=True): abort_on_failed_state=True):
"""Wait for the node to reach the expected state. """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" % "the last error is %(error)s" %
{'node': self.id, 'error': self.last_error}) {'node': self.id, 'error': self.last_error})
# TODO(dtantsur): waiting for power state def set_power_state(self, session, target, wait=False, timeout=None):
def set_power_state(self, session, target):
"""Run an action modifying this node's power state. """Run an action modifying this node's power state.
This call is asynchronous, it will return success as soon as the Bare 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. :param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter` :type session: :class:`~keystoneauth1.adapter.Adapter`
:param target: Target power state, e.g. "rebooting", "power on". :param target: Target power state, as a :class:`PowerAction` or
See the Bare Metal service documentation for available actions. 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) session = self._get_session(session)
if target.startswith("soft "): if target.startswith("soft "):
@ -567,6 +626,9 @@ class Node(_common.ListMixin, resource.Resource):
"to {target}".format(node=self.id, target=target)) "to {target}".format(node=self.id, target=target))
exceptions.raise_from_response(response, error_message=msg) 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): def attach_vif(self, session, vif_id, retry_on_conflict=True):
"""Attach a VIF to the node. """Attach a VIF to the node.

View File

@ -177,12 +177,11 @@ class TestBareMetalNode(base.BaseBaremetalTest):
node = self.create_node() node = self.create_node()
self.assertIsNone(node.power_state) 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) node = self.conn.baremetal.get_node(node.id)
# Fake nodes react immediately to power requests.
self.assertEqual('power on', node.power_state) 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) node = self.conn.baremetal.get_node(node.id)
self.assertEqual('power off', node.power_state) self.assertEqual('power off', node.power_state)

View File

@ -811,3 +811,30 @@ class TestNodePatch(base.TestCase):
self.assertIn('1.45', commit_args) self.assertIn('1.45', commit_args)
self.assertEqual(commit_kwargs['retry_on_conflict'], True) self.assertEqual(commit_kwargs['retry_on_conflict'], True)
mock_patch.assert_not_called() 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.