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:
parent
8c44ff176c
commit
97f7095abd
@ -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
|
||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
|
@ -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
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
4
releasenotes/notes/power-wait-751083852f958cb4.yaml
Normal file
4
releasenotes/notes/power-wait-751083852f958cb4.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Support waiting for bare metal power states.
|
Loading…
x
Reference in New Issue
Block a user