Add set_provision_state and wait_for_provision_state for baremetal Node

This change adds set_provision_state and wait_for_provision_state
to openstack.baremetal.v1.Node, as well as set_node_provision_state
to the bare metal Proxy. Also adds wait_for_nodes_provision_state,
which is similar to Node.wait_for_provision_state but handles several
nodes at the same time, which is important for bulk operations.

The cloud's node_set_provision_state was updated to use the new calls.
As a nice side effect, it now supports all provision states and actions
up to the Queens release, as well as does proper microversioning.

Some documentation was written for the bare metal proxy.

Change-Id: I22a76c3623f4dd2cca0b2103cbd8b853d5cebb71
This commit is contained in:
Dmitry Tantsur 2018-05-29 12:46:47 +02:00
parent 2fa2720eeb
commit 5e4420763a
16 changed files with 611 additions and 51 deletions

View File

@ -1,9 +1,64 @@
Using OpenStack Baremetal
=========================
Before working with the Baremetal service, you'll need to create a
Before working with the Bare Metal service, you'll need to create a
connection to your OpenStack cloud by following the :doc:`connect` user
guide. This will provide you with the ``conn`` variable used in the examples
below.
.. TODO(Qiming): Implement this guide
.. contents:: Table of Contents
:local:
The primary resource of the Bare Metal service is the **node**.
CRUD operations
~~~~~~~~~~~~~~~
List Nodes
----------
A **node** is a bare metal machine.
.. literalinclude:: ../examples/baremetal/list.py
:pyobject: list_nodes
Full example: `baremetal resource list`_
Provisioning operations
~~~~~~~~~~~~~~~~~~~~~~~
Provisioning actions are the main way to manipulate the nodes. See `Bare Metal
service states documentation`_ for details.
Manage and inspect Node
-----------------------
*Managing* a node in the ``enroll`` provision state validates the management
(IPMI, Redfish, etc) credentials and moves the node to the ``manageable``
state. *Managing* a node in the ``available`` state moves it to the
``manageable`` state. In this state additional actions, such as configuring
RAID or inspecting, are available.
*Inspecting* a node detects its properties by either talking to its BMC or by
booting a special ramdisk.
.. literalinclude:: ../examples/baremetal/provisioning.py
:pyobject: manage_and_inspect_node
Full example: `baremetal provisioning`_
Provide Node
------------
*Providing* a node in the ``manageable`` provision state makes it available
for deployment.
.. literalinclude:: ../examples/baremetal/provisioning.py
:pyobject: provide_node
Full example: `baremetal provisioning`_
.. _baremetal resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/baremetal/list.py
.. _baremetal provisioning: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/baremetal/provisioning.py
.. _Bare Metal service states documentation: https://docs.openstack.org/ironic/latest/contributor/states.html

View File

@ -22,6 +22,8 @@ Node Operations
.. automethod:: openstack.baremetal.v1._proxy.Proxy.get_node
.. automethod:: openstack.baremetal.v1._proxy.Proxy.find_node
.. automethod:: openstack.baremetal.v1._proxy.Proxy.nodes
.. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_provision_state
.. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_nodes_provision_state
Port Operations
^^^^^^^^^^^^^^^

View File

@ -0,0 +1,25 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
List resources from the Bare Metal service.
"""
def list_nodes(conn):
print("List Nodes:")
for node in conn.baremetal.nodes():
print(node)
# TODO(dtantsur): other resources

View File

@ -0,0 +1,35 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Operations with the provision state in the Bare Metal service.
"""
from __future__ import print_function
def manage_and_inspect_node(conn, uuid):
node = conn.baremetal.find_node(uuid)
print('Before:', node.provision_state)
conn.baremetal.set_node_provision_state(node, 'manage')
conn.baremetal.wait_for_nodes_provision_state([node], 'manageable')
conn.baremetal.set_node_provision_state(node, 'inspect')
res = conn.baremetal.wait_for_nodes_provision_state([node], 'manageable')
print('After:', res[0].provision_state)
def provide_node(conn, uuid):
node = conn.baremetal.find_node(uuid)
print('Before:', node.provision_state)
conn.baremetal.set_node_provision_state(node, 'provide')
res = conn.baremetal.wait_for_nodes_provision_state([node], 'available')
print('After:', res[0].provision_state)

View File

@ -0,0 +1,48 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
RETRIABLE_STATUS_CODES = [
# HTTP Conflict - happens if a node is locked
409,
# HTTP Service Unavailable happens if there's no free conductor
503
]
"""HTTP status codes that should be retried."""
PROVISIONING_VERSIONS = {
'abort': 13,
'adopt': 17,
'clean': 15,
'inspect': 6,
'manage': 4,
'provide': 4,
'rescue': 38,
'unrescue': 38,
}
"""API microversions introducing provisioning verbs."""
# Based on https://docs.openstack.org/ironic/latest/contributor/states.html
EXPECTED_STATES = {
'active': 'active',
'adopt': 'available',
'clean': 'manageable',
'deleted': 'available',
'inspect': 'manageable',
'manage': 'manageable',
'provide': 'available',
'rebuild': 'active',
'rescue': 'rescue',
}
"""Mapping of provisioning actions to expected stable states."""

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack import _log
from openstack.baremetal.v1 import chassis as _chassis
from openstack.baremetal.v1 import driver as _driver
from openstack.baremetal.v1 import node as _node
@ -19,6 +20,9 @@ from openstack import proxy
from openstack import utils
_logger = _log.setup_logging('openstack')
class Proxy(proxy.Proxy):
def chassis(self, details=False, **query):
@ -240,6 +244,87 @@ class Proxy(proxy.Proxy):
"""
return self._update(_node.Node, node, **attrs)
def set_node_provision_state(self, node, target, config_drive=None,
clean_steps=None, rescue_password=None,
wait=False, timeout=None):
"""Run an action modifying node's provision state.
This call is asynchronous, it will return success as soon as the Bare
Metal service acknowledges the request.
:param node: The value can be the name or ID of a node or a
:class:`~openstack.baremetal.v1.node.Node` instance.
:param target: Provisioning action, e.g. ``active``, ``provide``.
See the Bare Metal service documentation for available actions.
:param config_drive: Config drive to pass to the node, only valid
for ``active` and ``rebuild`` targets.
:param clean_steps: Clean steps to execute, only valid for ``clean``
target.
:param rescue_password: Password for the rescue operation, only valid
for ``rescue`` target.
:param wait: Whether to wait for the node to get into the expected
state. The expected state is determined from a combination of
the current provision state and ``target``.
: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.
:returns: The updated :class:`~openstack.baremetal.v1.node.Node`
:raises: ValueError if ``config_drive``, ``clean_steps`` or
``rescue_password`` are provided with an invalid ``target``.
"""
res = self._get_resource(_node.Node, node)
return res.set_provision_state(self, target, config_drive=config_drive,
clean_steps=clean_steps,
rescue_password=rescue_password,
wait=wait, timeout=timeout)
def wait_for_nodes_provision_state(self, nodes, expected_state,
timeout=None,
abort_on_failed_state=True):
"""Wait for the nodes to reach the expected state.
:param nodes: List of nodes - name, ID or
:class:`~openstack.baremetal.v1.node.Node` instance.
:param expected_state: The expected provisioning 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.
:param abort_on_failed_state: If ``True`` (the default), abort waiting
if any node reaches a failure state which does not match the
expected one. Note that the failure state for ``enroll`` ->
``manageable`` transition is ``enroll`` again.
:return: The list of :class:`~openstack.baremetal.v1.node.Node`
instances that reached the requested state.
"""
log_nodes = ', '.join(n.id if isinstance(n, _node.Node) else n
for n in nodes)
finished = []
remaining = nodes
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for nodes %(nodes)s to reach "
"target state '%(state)s'" % {'nodes': log_nodes,
'state': expected_state}):
nodes = [self.get_node(n) for n in remaining]
remaining = []
for n in nodes:
if n._check_state_reached(self, expected_state,
abort_on_failed_state):
finished.append(n)
else:
remaining.append(n)
if not remaining:
return finished
_logger.debug('Still waiting for nodes %(nodes)s to reach state '
'"%(target)s"',
{'nodes': ', '.join(n.id for n in remaining),
'target': expected_state})
def delete_node(self, node, ignore_missing=True):
"""Delete a node.

View File

@ -10,8 +10,15 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack import _log
from openstack.baremetal import baremetal_service
from openstack.baremetal.v1 import _common
from openstack import exceptions
from openstack import resource
from openstack import utils
_logger = _log.setup_logging('openstack')
class Node(resource.Resource):
@ -113,6 +120,160 @@ class Node(resource.Resource):
#: Timestamp at which the node was last updated.
updated_at = resource.Body("updated_at")
def set_provision_state(self, session, target, config_drive=None,
clean_steps=None, rescue_password=None,
wait=False, timeout=True):
"""Run an action modifying this node's provision state.
This call is asynchronous, it will return success as soon as the Bare
Metal service acknowledges the request.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param target: Provisioning action, e.g. ``active``, ``provide``.
See the Bare Metal service documentation for available actions.
:param config_drive: Config drive to pass to the node, only valid
for ``active` and ``rebuild`` targets.
:param clean_steps: Clean steps to execute, only valid for ``clean``
target.
:param rescue_password: Password for the rescue operation, only valid
for ``rescue`` target.
:param wait: Whether to wait for the target state to be reached.
:param timeout: Timeout (in seconds) to wait for the target state to be
reached. If ``None``, wait without timeout.
:return: This :class:`Node` instance.
:raises: ValueError if ``config_drive``, ``clean_steps`` or
``rescue_password`` are provided with an invalid ``target``.
"""
session = self._get_session(session)
if target in _common.PROVISIONING_VERSIONS:
version = '1.%d' % _common.PROVISIONING_VERSIONS[target]
else:
if config_drive and target == 'rebuild':
version = '1.35'
else:
version = None
version = utils.pick_microversion(session, version)
body = {'target': target}
if config_drive:
if target not in ('active', 'rebuild'):
raise ValueError('Config drive can only be provided with '
'"active" and "rebuild" targets')
# Not a typo - ironic accepts "configdrive" (without underscore)
body['configdrive'] = config_drive
if clean_steps is not None:
if target != 'clean':
raise ValueError('Clean steps can only be provided with '
'"clean" target')
body['clean_steps'] = clean_steps
if rescue_password is not None:
if target != 'rescue':
raise ValueError('Rescue password can only be provided with '
'"rescue" target')
body['rescue_password'] = rescue_password
if wait:
try:
expected_state = _common.EXPECTED_STATES[target]
except KeyError:
raise ValueError('For target %s the expected state is not '
'known, cannot wait for it' % target)
request = self._prepare_request(requires_id=True)
request.url = utils.urljoin(request.url, 'states', 'provision')
response = session.put(
request.url, json=body,
headers=request.headers, microversion=version,
retriable_status_codes=_common.RETRIABLE_STATUS_CODES)
msg = ("Failed to set provision state for bare metal node {node} "
"to {target}".format(node=self.id, target=target))
exceptions.raise_from_response(response, error_message=msg)
if wait:
return self.wait_for_provision_state(session,
expected_state,
timeout=timeout)
else:
return self.get(session)
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.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param expected_state: The expected provisioning 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.
:param abort_on_failed_state: If ``True`` (the default), abort waiting
if the node reaches a failure state which does not match the
expected one. Note that the failure state for ``enroll`` ->
``manageable`` transition is ``enroll`` again.
:return: This :class:`Node` instance.
"""
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for node %(node)s to reach "
"target state '%(state)s'" % {'node': self.id,
'state': expected_state}):
self.get(session)
if self._check_state_reached(session, expected_state,
abort_on_failed_state):
return self
_logger.debug('Still waiting for node %(node)s to reach state '
'"%(target)s", the current state is "%(state)s"',
{'node': self.id, 'target': expected_state,
'state': self.provision_state})
def _check_state_reached(self, session, expected_state,
abort_on_failed_state=True):
"""Wait for the node to reach the expected state.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param expected_state: The expected provisioning state to reach.
:param abort_on_failed_state: If ``True`` (the default), abort waiting
if the node reaches a failure state which does not match the
expected one. Note that the failure state for ``enroll`` ->
``manageable`` transition is ``enroll`` again.
:return: ``True`` if the target state is reached
:raises: SDKException if ``abort_on_failed_state`` is ``True`` and
a failure state is reached.
"""
# NOTE(dtantsur): microversion 1.2 changed None to available
if (self.provision_state == expected_state or
(expected_state == 'available' and
self.provision_state is None)):
return True
elif not abort_on_failed_state:
return False
if self.provision_state.endswith(' failed'):
raise exceptions.SDKException(
"Node %(node)s reached failure state \"%(state)s\"; "
"the last error is %(error)s" %
{'node': self.id, 'state': self.provision_state,
'error': self.last_error})
# Special case: a failure state for "manage" transition can be
# "enroll"
elif (expected_state == 'manageable' and
self.provision_state == 'enroll' and self.last_error):
raise exceptions.SDKException(
"Node %(node)s could not reach state manageable: "
"failed to verify management credentials; "
"the last error is %(error)s" %
{'node': self.id, 'error': self.last_error})
class NodeDetail(Node):

View File

@ -9607,7 +9607,7 @@ class OpenStackCloud(_normalize.Normalizer):
config drive to be utilized.
:param string name_or_id: The Name or UUID value representing the
baremetal node.
baremetal node.
:param string state: The desired provision state for the
baremetal node.
:param string configdrive: An optional URL or file or path
@ -9629,46 +9629,10 @@ class OpenStackCloud(_normalize.Normalizer):
:returns: ``munch.Munch`` representing the current state of the machine
upon exit of the method.
"""
# NOTE(TheJulia): Default microversion for this call is 1.6.
# Setting locally until we have determined our master plan regarding
# microversion handling.
version = "1.6"
msg = ("Baremetal machine node failed change provision state to "
"{state}".format(state=state))
url = '/nodes/{node_id}/states/provision'.format(
node_id=name_or_id)
payload = {'target': state}
if configdrive:
payload['configdrive'] = configdrive
machine = _utils._call_client_and_retry(self._baremetal_client.put,
url,
retry_on=[409, 503],
json=payload,
error_message=msg,
microversion=version)
if wait:
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for node transition to "
"target state of '%s'" % state):
machine = self.get_machine(name_or_id)
if 'failed' in machine['provision_state']:
raise exc.OpenStackCloudException(
"Machine encountered a failure.")
# NOTE(TheJulia): This performs matching if the requested
# end state matches the state the node has reached.
if state in machine['provision_state']:
break
# NOTE(TheJulia): This performs matching for cases where
# the reqeusted state action ends in available state.
if ("available" in machine['provision_state'] and
state in ["provide", "deleted"]):
break
else:
machine = self.get_machine(name_or_id)
return machine
node = self.baremetal.set_node_provision_state(
name_or_id, target=state, config_drive=configdrive,
wait=wait, timeout=timeout)
return node._to_munch()
def set_machine_maintenance_state(
self,

View File

@ -2,6 +2,7 @@
"application_catalog_api_version": "1",
"auth_type": "password",
"baremetal_api_version": "1",
"baremetal_status_code_retries": 5,
"block_storage_api_version": "2",
"clustering_api_version": "1",
"container_api_version": "1",

View File

@ -35,6 +35,7 @@ import collections
import itertools
from keystoneauth1 import adapter
import munch
from requests import structures
from openstack import _log
@ -573,7 +574,8 @@ class Resource(object):
"""
return cls(_synchronized=synchronized, **obj)
def to_dict(self, body=True, headers=True, ignore_none=False):
def to_dict(self, body=True, headers=True, ignore_none=False,
original_names=False):
"""Return a dictionary of this resource's contents
:param bool body: Include the :class:`~openstack.resource.Body`
@ -583,6 +585,8 @@ class Resource(object):
:param bool ignore_none: When True, exclude key/value pairs where
the value is None. This will exclude
attributes that the server hasn't returned.
:param bool original_names: When True, use attribute names as they
were received from the server.
:return: A dictionary of key/value pairs where keys are named
as they exist as attributes of this class.
@ -608,12 +612,16 @@ class Resource(object):
# Since we're looking at class definitions we need to include
# subclasses, so check the whole MRO.
for klass in self.__class__.__mro__:
for key, value in klass.__dict__.items():
if isinstance(value, components):
for attr, component in klass.__dict__.items():
if isinstance(component, components):
if original_names:
key = component.name
else:
key = attr
# Make sure base classes don't end up overwriting
# mappings we've found previously in subclasses.
if key not in mapping:
value = getattr(self, key, None)
value = getattr(self, attr, None)
if ignore_none and value is None:
continue
if isinstance(value, Resource):
@ -629,6 +637,11 @@ class Resource(object):
return mapping
def _to_munch(self):
"""Convert this resource into a Munch compatible with shade."""
return munch.Munch(self.to_dict(body=True, headers=False,
original_names=True))
def _prepare_request(self, requires_id=None, prepend_key=False):
"""Prepare a request to be sent to the server

View File

@ -107,3 +107,15 @@ class TestCase(base.BaseTestCase):
self.addDetail(name, testtools.content.text_content(
pprint.pformat(text)))
self.addOnException(add_content)
def assertSubdict(self, part, whole):
missing_keys = set(part) - set(whole)
if missing_keys:
self.fail("Keys %s are in %s but not in %s" %
(missing_keys, part, whole))
wrong_values = [(key, part[key], whole[key])
for key in part if part[key] != whole[key]]
if wrong_values:
self.fail("Mismatched values: %s" %
", ".join("for %s got %s and %s" % tpl
for tpl in wrong_values))

View File

@ -10,9 +10,12 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack.tests.unit import base
from keystoneauth1 import adapter
import mock
from openstack.baremetal.v1 import node
from openstack import exceptions
from openstack.tests.unit import base
# NOTE: Sample data from api-ref doc
FAKE = {
@ -196,3 +199,78 @@ class TestNodeDetail(base.TestCase):
self.assertEqual(FAKE['target_power_state'], sot.target_power_state)
self.assertEqual(FAKE['target_raid_config'], sot.target_raid_config)
self.assertEqual(FAKE['updated_at'], sot.updated_at)
@mock.patch('time.sleep', lambda _t: None)
@mock.patch.object(node.Node, 'get', autospec=True)
class TestNodeWaitForProvisionState(base.TestCase):
def setUp(self):
super(TestNodeWaitForProvisionState, self).setUp()
self.node = node.Node(**FAKE)
self.session = mock.Mock()
def test_success(self, mock_get):
def _get_side_effect(_self, session):
self.node.provision_state = 'manageable'
self.assertIs(session, self.session)
mock_get.side_effect = _get_side_effect
node = self.node.wait_for_provision_state(self.session, 'manageable')
self.assertIs(node, self.node)
def test_failure(self, mock_get):
def _get_side_effect(_self, session):
self.node.provision_state = 'deploy failed'
self.assertIs(session, self.session)
mock_get.side_effect = _get_side_effect
self.assertRaisesRegex(exceptions.SDKException,
'failure state "deploy failed"',
self.node.wait_for_provision_state,
self.session, 'manageable')
def test_enroll_as_failure(self, mock_get):
def _get_side_effect(_self, session):
self.node.provision_state = 'enroll'
self.node.last_error = 'power failure'
self.assertIs(session, self.session)
mock_get.side_effect = _get_side_effect
self.assertRaisesRegex(exceptions.SDKException,
'failed to verify management credentials',
self.node.wait_for_provision_state,
self.session, 'manageable')
def test_timeout(self, mock_get):
self.assertRaises(exceptions.ResourceTimeout,
self.node.wait_for_provision_state,
self.session, 'manageable', timeout=0.001)
def test_not_abort_on_failed_state(self, mock_get):
def _get_side_effect(_self, session):
self.node.provision_state = 'deploy failed'
self.assertIs(session, self.session)
mock_get.side_effect = _get_side_effect
self.assertRaises(exceptions.ResourceTimeout,
self.node.wait_for_provision_state,
self.session, 'manageable', timeout=0.001,
abort_on_failed_state=False)
@mock.patch.object(node.Node, 'get', lambda self, session: self)
@mock.patch.object(exceptions, 'raise_from_response', mock.Mock())
class TestNodeSetProvisionState(base.TestCase):
def setUp(self):
super(TestNodeSetProvisionState, self).setUp()
self.node = node.Node(**FAKE)
self.session = mock.Mock(spec=adapter.Adapter,
default_microversion=None)
def test_no_arguments(self):
self.node.set_provision_state(self.session, 'manage')

View File

@ -11,6 +11,7 @@
# under the License.
import deprecation
import mock
from openstack.baremetal.v1 import _proxy
from openstack.baremetal.v1 import chassis
@ -18,6 +19,8 @@ from openstack.baremetal.v1 import driver
from openstack.baremetal.v1 import node
from openstack.baremetal.v1 import port
from openstack.baremetal.v1 import port_group
from openstack import exceptions
from openstack.tests.unit import base
from openstack.tests.unit import test_proxy_base
@ -162,3 +165,41 @@ class TestBaremetalProxy(test_proxy_base.TestProxyBase):
def test_delete_portgroup_ignore(self):
self.verify_delete(self.proxy.delete_portgroup, port_group.PortGroup,
True)
@mock.patch('time.sleep', lambda _sec: None)
@mock.patch.object(_proxy.Proxy, 'get_node', autospec=True)
class TestWaitForNodesProvisionState(base.TestCase):
def setUp(self):
super(TestWaitForNodesProvisionState, self).setUp()
self.session = mock.Mock()
self.proxy = _proxy.Proxy(self.session)
def test_success(self, mock_get):
# two attempts, one node succeeds after the 1st
nodes = [mock.Mock(spec=node.Node, id=str(i))
for i in range(3)]
for i, n in enumerate(nodes):
# 1st attempt on 1st node, 2nd attempt on 2nd node
n._check_state_reached.return_value = not (i % 2)
mock_get.side_effect = nodes
result = self.proxy.wait_for_nodes_provision_state(
['abcd', node.Node(id='1234')], 'fake state')
self.assertEqual([nodes[0], nodes[2]], result)
for n in nodes:
n._check_state_reached.assert_called_once_with(
self.proxy, 'fake state', True)
def test_timeout(self, mock_get):
mock_get.return_value._check_state_reached.return_value = False
mock_get.return_value.id = '1234'
self.assertRaises(exceptions.ResourceTimeout,
self.proxy.wait_for_nodes_provision_state,
['abcd', node.Node(id='1234')], 'fake state',
timeout=0.001)
mock_get.return_value._check_state_reached.assert_called_with(
self.proxy, 'fake state', True)

View File

@ -764,7 +764,7 @@ class TestBaremetalNode(base.IronicTestCase):
'active',
wait=True)
self.assertEqual(active_node, return_value)
self.assertSubdict(active_node, return_value)
self.assert_calls()
def test_node_set_provision_state_wait_timeout_fails(self):
@ -817,7 +817,7 @@ class TestBaremetalNode(base.IronicTestCase):
'active',
wait=True)
self.assertEqual(self.fake_baremetal_node, return_value)
self.assertSubdict(self.fake_baremetal_node, return_value)
self.assert_calls()
def test_node_set_provision_state_wait_failure_cases(self):
@ -875,7 +875,7 @@ class TestBaremetalNode(base.IronicTestCase):
'provide',
wait=True)
self.assertEqual(available_node, return_value)
self.assertSubdict(available_node, return_value)
self.assert_calls()
def test_wait_for_baremetal_node_lock_locked(self):

View File

@ -154,3 +154,32 @@ def supports_microversion(adapter, microversion):
microversion)):
return True
return False
def pick_microversion(session, required):
"""Get a new microversion if it is higher than session's default.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param required: Version that is required for an action.
:type required: String or tuple or None.
:return: ``required`` as a string if the ``session``'s default is too low,
the ``session``'s default otherwise. Returns ``None`` of both
are ``None``.
:raises: TypeError if ``required`` is invalid.
"""
if required is not None:
required = discover.normalize_version_number(required)
if session.default_microversion is not None:
default = discover.normalize_version_number(
session.default_microversion)
if required is None:
required = default
else:
required = (default if discover.version_match(required, default)
else required)
if required is not None:
return discover.version_to_string(required)

View File

@ -0,0 +1,11 @@
---
features:
- |
Adds ``set_provision_state`` and ``wait_for_provision_state`` to
``openstack.baremetal.v1.Node``.
- |
Adds ``node_set_provision_state`` and ``wait_for_nodes_provision_state``
to the baremetal Proxy.
- |
The ``node_set_provision_state`` call now supports provision states
up to the Queens release.