diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 04538c39e..55043c433 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -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 ^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/baremetal/v1/node.rst b/doc/source/user/resources/baremetal/v1/node.rst index ebc5fb327..14e691ed2 100644 --- a/doc/source/user/resources/baremetal/v1/node.rst +++ b/doc/source/user/resources/baremetal/v1/node.rst @@ -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 ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/openstack/baremetal/v1/_common.py b/openstack/baremetal/v1/_common.py index e37c7b73a..7459842ac 100644 --- a/openstack/baremetal/v1/_common.py +++ b/openstack/baremetal/v1/_common.py @@ -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', diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 903e584c3..76d144f2a 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -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. diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 8ef4edff4..72c50e19a 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -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. diff --git a/openstack/tests/functional/baremetal/test_baremetal_node.py b/openstack/tests/functional/baremetal/test_baremetal_node.py index 23f5ed5d8..aa44c2265 100644 --- a/openstack/tests/functional/baremetal/test_baremetal_node.py +++ b/openstack/tests/functional/baremetal/test_baremetal_node.py @@ -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) diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index dbc874f4c..4b0802ca9 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -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) diff --git a/releasenotes/notes/power-wait-751083852f958cb4.yaml b/releasenotes/notes/power-wait-751083852f958cb4.yaml new file mode 100644 index 000000000..359f1d35b --- /dev/null +++ b/releasenotes/notes/power-wait-751083852f958cb4.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Support waiting for bare metal power states.