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:
parent
2fa2720eeb
commit
5e4420763a
@ -1,9 +1,64 @@
|
|||||||
Using OpenStack Baremetal
|
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
|
connection to your OpenStack cloud by following the :doc:`connect` user
|
||||||
guide. This will provide you with the ``conn`` variable used in the examples
|
guide. This will provide you with the ``conn`` variable used in the examples
|
||||||
below.
|
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
|
||||||
|
@ -22,6 +22,8 @@ Node Operations
|
|||||||
.. automethod:: openstack.baremetal.v1._proxy.Proxy.get_node
|
.. automethod:: openstack.baremetal.v1._proxy.Proxy.get_node
|
||||||
.. automethod:: openstack.baremetal.v1._proxy.Proxy.find_node
|
.. automethod:: openstack.baremetal.v1._proxy.Proxy.find_node
|
||||||
.. automethod:: openstack.baremetal.v1._proxy.Proxy.nodes
|
.. 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
|
Port Operations
|
||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
|
25
examples/baremetal/list.py
Normal file
25
examples/baremetal/list.py
Normal 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
|
35
examples/baremetal/provisioning.py
Normal file
35
examples/baremetal/provisioning.py
Normal 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)
|
48
openstack/baremetal/v1/_common.py
Normal file
48
openstack/baremetal/v1/_common.py
Normal 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."""
|
@ -10,6 +10,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from openstack import _log
|
||||||
from openstack.baremetal.v1 import chassis as _chassis
|
from openstack.baremetal.v1 import chassis as _chassis
|
||||||
from openstack.baremetal.v1 import driver as _driver
|
from openstack.baremetal.v1 import driver as _driver
|
||||||
from openstack.baremetal.v1 import node as _node
|
from openstack.baremetal.v1 import node as _node
|
||||||
@ -19,6 +20,9 @@ from openstack import proxy
|
|||||||
from openstack import utils
|
from openstack import utils
|
||||||
|
|
||||||
|
|
||||||
|
_logger = _log.setup_logging('openstack')
|
||||||
|
|
||||||
|
|
||||||
class Proxy(proxy.Proxy):
|
class Proxy(proxy.Proxy):
|
||||||
|
|
||||||
def chassis(self, details=False, **query):
|
def chassis(self, details=False, **query):
|
||||||
@ -240,6 +244,87 @@ class Proxy(proxy.Proxy):
|
|||||||
"""
|
"""
|
||||||
return self._update(_node.Node, node, **attrs)
|
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):
|
def delete_node(self, node, ignore_missing=True):
|
||||||
"""Delete a node.
|
"""Delete a node.
|
||||||
|
|
||||||
|
@ -10,8 +10,15 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from openstack import _log
|
||||||
from openstack.baremetal import baremetal_service
|
from openstack.baremetal import baremetal_service
|
||||||
|
from openstack.baremetal.v1 import _common
|
||||||
|
from openstack import exceptions
|
||||||
from openstack import resource
|
from openstack import resource
|
||||||
|
from openstack import utils
|
||||||
|
|
||||||
|
|
||||||
|
_logger = _log.setup_logging('openstack')
|
||||||
|
|
||||||
|
|
||||||
class Node(resource.Resource):
|
class Node(resource.Resource):
|
||||||
@ -113,6 +120,160 @@ class Node(resource.Resource):
|
|||||||
#: Timestamp at which the node was last updated.
|
#: Timestamp at which the node was last updated.
|
||||||
updated_at = resource.Body("updated_at")
|
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):
|
class NodeDetail(Node):
|
||||||
|
|
||||||
|
@ -9607,7 +9607,7 @@ class OpenStackCloud(_normalize.Normalizer):
|
|||||||
config drive to be utilized.
|
config drive to be utilized.
|
||||||
|
|
||||||
:param string name_or_id: The Name or UUID value representing the
|
: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
|
:param string state: The desired provision state for the
|
||||||
baremetal node.
|
baremetal node.
|
||||||
:param string configdrive: An optional URL or file or path
|
: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
|
:returns: ``munch.Munch`` representing the current state of the machine
|
||||||
upon exit of the method.
|
upon exit of the method.
|
||||||
"""
|
"""
|
||||||
# NOTE(TheJulia): Default microversion for this call is 1.6.
|
node = self.baremetal.set_node_provision_state(
|
||||||
# Setting locally until we have determined our master plan regarding
|
name_or_id, target=state, config_drive=configdrive,
|
||||||
# microversion handling.
|
wait=wait, timeout=timeout)
|
||||||
version = "1.6"
|
return node._to_munch()
|
||||||
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
|
|
||||||
|
|
||||||
def set_machine_maintenance_state(
|
def set_machine_maintenance_state(
|
||||||
self,
|
self,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"application_catalog_api_version": "1",
|
"application_catalog_api_version": "1",
|
||||||
"auth_type": "password",
|
"auth_type": "password",
|
||||||
"baremetal_api_version": "1",
|
"baremetal_api_version": "1",
|
||||||
|
"baremetal_status_code_retries": 5,
|
||||||
"block_storage_api_version": "2",
|
"block_storage_api_version": "2",
|
||||||
"clustering_api_version": "1",
|
"clustering_api_version": "1",
|
||||||
"container_api_version": "1",
|
"container_api_version": "1",
|
||||||
|
@ -35,6 +35,7 @@ import collections
|
|||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from keystoneauth1 import adapter
|
from keystoneauth1 import adapter
|
||||||
|
import munch
|
||||||
from requests import structures
|
from requests import structures
|
||||||
|
|
||||||
from openstack import _log
|
from openstack import _log
|
||||||
@ -573,7 +574,8 @@ class Resource(object):
|
|||||||
"""
|
"""
|
||||||
return cls(_synchronized=synchronized, **obj)
|
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
|
"""Return a dictionary of this resource's contents
|
||||||
|
|
||||||
:param bool body: Include the :class:`~openstack.resource.Body`
|
: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
|
:param bool ignore_none: When True, exclude key/value pairs where
|
||||||
the value is None. This will exclude
|
the value is None. This will exclude
|
||||||
attributes that the server hasn't returned.
|
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
|
:return: A dictionary of key/value pairs where keys are named
|
||||||
as they exist as attributes of this class.
|
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
|
# Since we're looking at class definitions we need to include
|
||||||
# subclasses, so check the whole MRO.
|
# subclasses, so check the whole MRO.
|
||||||
for klass in self.__class__.__mro__:
|
for klass in self.__class__.__mro__:
|
||||||
for key, value in klass.__dict__.items():
|
for attr, component in klass.__dict__.items():
|
||||||
if isinstance(value, components):
|
if isinstance(component, components):
|
||||||
|
if original_names:
|
||||||
|
key = component.name
|
||||||
|
else:
|
||||||
|
key = attr
|
||||||
# Make sure base classes don't end up overwriting
|
# Make sure base classes don't end up overwriting
|
||||||
# mappings we've found previously in subclasses.
|
# mappings we've found previously in subclasses.
|
||||||
if key not in mapping:
|
if key not in mapping:
|
||||||
value = getattr(self, key, None)
|
value = getattr(self, attr, None)
|
||||||
if ignore_none and value is None:
|
if ignore_none and value is None:
|
||||||
continue
|
continue
|
||||||
if isinstance(value, Resource):
|
if isinstance(value, Resource):
|
||||||
@ -629,6 +637,11 @@ class Resource(object):
|
|||||||
|
|
||||||
return mapping
|
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):
|
def _prepare_request(self, requires_id=None, prepend_key=False):
|
||||||
"""Prepare a request to be sent to the server
|
"""Prepare a request to be sent to the server
|
||||||
|
|
||||||
|
@ -107,3 +107,15 @@ class TestCase(base.BaseTestCase):
|
|||||||
self.addDetail(name, testtools.content.text_content(
|
self.addDetail(name, testtools.content.text_content(
|
||||||
pprint.pformat(text)))
|
pprint.pformat(text)))
|
||||||
self.addOnException(add_content)
|
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))
|
||||||
|
@ -10,9 +10,12 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from openstack.tests.unit import base
|
from keystoneauth1 import adapter
|
||||||
|
import mock
|
||||||
|
|
||||||
from openstack.baremetal.v1 import node
|
from openstack.baremetal.v1 import node
|
||||||
|
from openstack import exceptions
|
||||||
|
from openstack.tests.unit import base
|
||||||
|
|
||||||
# NOTE: Sample data from api-ref doc
|
# NOTE: Sample data from api-ref doc
|
||||||
FAKE = {
|
FAKE = {
|
||||||
@ -196,3 +199,78 @@ class TestNodeDetail(base.TestCase):
|
|||||||
self.assertEqual(FAKE['target_power_state'], sot.target_power_state)
|
self.assertEqual(FAKE['target_power_state'], sot.target_power_state)
|
||||||
self.assertEqual(FAKE['target_raid_config'], sot.target_raid_config)
|
self.assertEqual(FAKE['target_raid_config'], sot.target_raid_config)
|
||||||
self.assertEqual(FAKE['updated_at'], sot.updated_at)
|
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')
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import deprecation
|
import deprecation
|
||||||
|
import mock
|
||||||
|
|
||||||
from openstack.baremetal.v1 import _proxy
|
from openstack.baremetal.v1 import _proxy
|
||||||
from openstack.baremetal.v1 import chassis
|
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 node
|
||||||
from openstack.baremetal.v1 import port
|
from openstack.baremetal.v1 import port
|
||||||
from openstack.baremetal.v1 import port_group
|
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
|
from openstack.tests.unit import test_proxy_base
|
||||||
|
|
||||||
|
|
||||||
@ -162,3 +165,41 @@ class TestBaremetalProxy(test_proxy_base.TestProxyBase):
|
|||||||
def test_delete_portgroup_ignore(self):
|
def test_delete_portgroup_ignore(self):
|
||||||
self.verify_delete(self.proxy.delete_portgroup, port_group.PortGroup,
|
self.verify_delete(self.proxy.delete_portgroup, port_group.PortGroup,
|
||||||
True)
|
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)
|
||||||
|
@ -764,7 +764,7 @@ class TestBaremetalNode(base.IronicTestCase):
|
|||||||
'active',
|
'active',
|
||||||
wait=True)
|
wait=True)
|
||||||
|
|
||||||
self.assertEqual(active_node, return_value)
|
self.assertSubdict(active_node, return_value)
|
||||||
self.assert_calls()
|
self.assert_calls()
|
||||||
|
|
||||||
def test_node_set_provision_state_wait_timeout_fails(self):
|
def test_node_set_provision_state_wait_timeout_fails(self):
|
||||||
@ -817,7 +817,7 @@ class TestBaremetalNode(base.IronicTestCase):
|
|||||||
'active',
|
'active',
|
||||||
wait=True)
|
wait=True)
|
||||||
|
|
||||||
self.assertEqual(self.fake_baremetal_node, return_value)
|
self.assertSubdict(self.fake_baremetal_node, return_value)
|
||||||
self.assert_calls()
|
self.assert_calls()
|
||||||
|
|
||||||
def test_node_set_provision_state_wait_failure_cases(self):
|
def test_node_set_provision_state_wait_failure_cases(self):
|
||||||
@ -875,7 +875,7 @@ class TestBaremetalNode(base.IronicTestCase):
|
|||||||
'provide',
|
'provide',
|
||||||
wait=True)
|
wait=True)
|
||||||
|
|
||||||
self.assertEqual(available_node, return_value)
|
self.assertSubdict(available_node, return_value)
|
||||||
self.assert_calls()
|
self.assert_calls()
|
||||||
|
|
||||||
def test_wait_for_baremetal_node_lock_locked(self):
|
def test_wait_for_baremetal_node_lock_locked(self):
|
||||||
|
@ -154,3 +154,32 @@ def supports_microversion(adapter, microversion):
|
|||||||
microversion)):
|
microversion)):
|
||||||
return True
|
return True
|
||||||
return False
|
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)
|
||||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user