baremetal: support for allocation API

Change-Id: Ie47a903430d6b54740152676d897b5f8759e36c2
Story: #2004341
Task: #28029
This commit is contained in:
Dmitry Tantsur 2019-02-13 11:45:11 +01:00
parent b7b7353e55
commit e8ccfee5fa
11 changed files with 463 additions and 2 deletions

View File

@ -76,6 +76,16 @@ VIF Operations
.. automethod:: openstack.baremetal.v1._proxy.Proxy.detach_vif_from_node
.. automethod:: openstack.baremetal.v1._proxy.Proxy.list_node_vifs
Allocation Operations
^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: openstack.baremetal.v1._proxy.Proxy
.. automethod:: openstack.baremetal.v1._proxy.Proxy.create_allocation
.. automethod:: openstack.baremetal.v1._proxy.Proxy.delete_allocation
.. automethod:: openstack.baremetal.v1._proxy.Proxy.get_allocation
.. automethod:: openstack.baremetal.v1._proxy.Proxy.allocations
.. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_allocation
Utilities
---------

View File

@ -9,3 +9,4 @@ Baremetal Resources
v1/node
v1/port
v1/port_group
v1/allocation

View File

@ -0,0 +1,12 @@
openstack.baremetal.v1.Allocation
=================================
.. automodule:: openstack.baremetal.v1.allocation
The Allocation Class
--------------------
The ``Allocation`` class inherits from :class:`~openstack.resource.Resource`.
.. autoclass:: openstack.baremetal.v1.allocation.Allocation
:members:

View File

@ -12,6 +12,7 @@
from openstack import _log
from openstack.baremetal.v1 import _common
from openstack.baremetal.v1 import allocation as _allocation
from openstack.baremetal.v1 import chassis as _chassis
from openstack.baremetal.v1 import driver as _driver
from openstack.baremetal.v1 import node as _node
@ -703,3 +704,94 @@ class Proxy(proxy.Proxy):
"""
res = self._get_resource(_node.Node, node)
return res.list_vifs(self)
def allocations(self, **query):
"""Retrieve a generator of allocations.
:param dict query: Optional query parameters to be sent to restrict
the allocation to be returned. Available parameters include:
* ``fields``: A list containing one or more fields to be returned
in the response. This may lead to some performance gain
because other fields of the resource are not refreshed.
* ``limit``: Requests at most the specified number of items be
returned from the query.
* ``marker``: Specifies the ID of the last-seen allocation. Use the
``limit`` parameter to make an initial limited request and
use the ID of the last-seen allocation from the response as
the ``marker`` value in a subsequent limited request.
* ``sort_dir``: Sorts the response by the requested sort direction.
A valid value is ``asc`` (ascending) or ``desc``
(descending). Default is ``asc``. You can specify multiple
pairs of sort key and sort direction query parameters. If
you omit the sort direction in a pair, the API uses the
natural sorting direction of the server attribute that is
provided as the ``sort_key``.
* ``sort_key``: Sorts the response by the this attribute value.
Default is ``id``. You can specify multiple pairs of sort
key and sort direction query parameters. If you omit the
sort direction in a pair, the API uses the natural sorting
direction of the server attribute that is provided as the
``sort_key``.
:returns: A generator of allocation instances.
"""
return _allocation.Allocation.list(self, **query)
def create_allocation(self, **attrs):
"""Create a new allocation from attributes.
:param dict attrs: Keyword arguments that will be used to create a
:class:`~openstack.baremetal.v1.allocation.Allocation`.
:returns: The results of allocation creation.
:rtype: :class:`~openstack.baremetal.v1.allocation.Allocation`.
"""
return self._create(_allocation.Allocation, **attrs)
def get_allocation(self, allocation):
"""Get a specific allocation.
:param allocation: The value can be the name or ID of an allocation or
a :class:`~openstack.baremetal.v1.allocation.Allocation` instance.
:returns: One :class:`~openstack.baremetal.v1.allocation.Allocation`
:raises: :class:`~openstack.exceptions.ResourceNotFound` when no
allocation matching the name or ID could be found.
"""
return self._get(_allocation.Allocation, allocation)
def delete_allocation(self, allocation, ignore_missing=True):
"""Delete an allocation.
:param allocation: The value can be the name or ID of an allocation or
a :class:`~openstack.baremetal.v1.allocation.Allocation` instance.
:param bool ignore_missing: When set to ``False``, an exception
:class:`~openstack.exceptions.ResourceNotFound` will be raised
when the allocation could not be found. When set to ``True``, no
exception will be raised when attempting to delete a non-existent
allocation.
:returns: The instance of the allocation which was deleted.
:rtype: :class:`~openstack.baremetal.v1.allocation.Allocation`.
"""
return self._delete(_allocation.Allocation, allocation,
ignore_missing=ignore_missing)
def wait_for_allocation(self, allocation, timeout=None,
ignore_error=False):
"""Wait for the allocation to become active.
:param allocation: The value can be the name or ID of an allocation or
a :class:`~openstack.baremetal.v1.allocation.Allocation` instance.
:param timeout: How much (in seconds) to wait for the allocation.
The value of ``None`` (the default) means no client-side timeout.
:param ignore_error: If ``True``, this call will raise an exception
if the allocation reaches the ``error`` state. Otherwise the error
state is considered successful and the call returns.
:returns: The instance of the allocation.
:rtype: :class:`~openstack.baremetal.v1.allocation.Allocation`.
"""
res = self._get_resource(_allocation.Allocation, allocation)
return res.wait(self, timeout=timeout, ignore_error=ignore_error)

View File

@ -0,0 +1,98 @@
# 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.
from openstack import _log
from openstack.baremetal.v1 import _common
from openstack import exceptions
from openstack import resource
from openstack import utils
_logger = _log.setup_logging('openstack')
class Allocation(_common.ListMixin, resource.Resource):
resources_key = 'allocations'
base_path = '/allocations'
# capabilities
allow_create = True
allow_fetch = True
allow_commit = False
allow_delete = True
allow_list = True
_query_mapping = resource.QueryParameters(
'node', 'resource_class', 'state',
fields={'name': 'fields', 'type': _common.comma_separated_list},
)
# The allocation API introduced in 1.52.
_max_microversion = '1.52'
#: The candidate nodes for this allocation.
candidate_nodes = resource.Body('candidate_nodes', type=list)
#: Timestamp at which the allocation was created.
created_at = resource.Body('created_at')
#: A set of one or more arbitrary metadata key and value pairs.
extra = resource.Body('extra', type=dict)
#: The UUID for the allocation.
id = resource.Body('uuid', alternate_id=True)
#: The last error for the allocation.
last_error = resource.Body("last_error")
#: A list of relative links, including the self and bookmark links.
links = resource.Body('links', type=list)
#: The name of the allocation.
name = resource.Body('name')
#: UUID of the node this allocation belongs to.
node_id = resource.Body('node_uuid')
#: The requested resource class.
resource_class = resource.Body('resource_class')
#: The state of the allocation.
state = resource.Body('state')
#: The requested traits.
traits = resource.Body('traits', type=list)
#: Timestamp at which the allocation was last updated.
updated_at = resource.Body('updated_at')
def wait(self, session, timeout=None, ignore_error=False):
"""Wait for the allocation to become active.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param timeout: How much (in seconds) to wait for the allocation.
The value of ``None`` (the default) means no client-side timeout.
:param ignore_error: If ``True``, this call will raise an exception
if the allocation reaches the ``error`` state. Otherwise the error
state is considered successful and the call returns.
:return: This :class:`Allocation` instance.
"""
if self.state == 'active':
return self
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for the allocation %s" % self.id):
self.fetch(session)
if self.state == 'error' and not ignore_error:
raise exceptions.SDKException(
"Allocation %(allocation)s failed: %(error)s" %
{'allocation': self.id, 'error': self.last_error})
elif self.state != 'allocating':
return self
_logger.debug('Still waiting for the allocation %(allocation)s '
'to become active, the current state is %(state)s',
{'allocation': self.id, 'state': self.state})

View File

@ -56,10 +56,13 @@ class Node(_common.ListMixin, resource.Resource):
is_maintenance='maintenance',
)
# The conductor field introduced in 1.49 (Stein).
_max_microversion = '1.49'
# The allocation_uuid field introduced in 1.52 (Stein).
_max_microversion = '1.52'
# Properties
#: The UUID of the allocation associated with this node. Added in API
#: microversion 1.52.
allocation_id = resource.Body("allocation_uuid")
#: The UUID of the chassis associated wit this node. Can be empty or None.
chassis_id = resource.Body("chassis_uuid")
#: The current clean step.

View File

@ -23,6 +23,13 @@ class BaseBaremetalTest(base.BaseFunctionalTest):
self.require_service('baremetal',
min_microversion=self.min_microversion)
def create_allocation(self, **kwargs):
allocation = self.conn.baremetal.create_allocation(**kwargs)
self.addCleanup(
lambda: self.conn.baremetal.delete_allocation(allocation.id,
ignore_missing=True))
return allocation
def create_chassis(self, **kwargs):
chassis = self.conn.baremetal.create_chassis(**kwargs)
self.addCleanup(

View File

@ -0,0 +1,110 @@
# 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.
import random
from openstack import exceptions
from openstack.tests.functional.baremetal import base
class TestBareMetalAllocation(base.BaseBaremetalTest):
min_microversion = '1.52'
def setUp(self):
super(TestBareMetalAllocation, self).setUp()
# NOTE(dtantsur): generate a unique resource class to prevent parallel
# tests from clashing.
self.resource_class = 'baremetal-%d' % random.randrange(1024)
self.node = self._create_available_node()
def _create_available_node(self):
node = self.create_node(resource_class=self.resource_class)
self.conn.baremetal.set_node_provision_state(node, 'manage',
wait=True)
self.conn.baremetal.set_node_provision_state(node, 'provide',
wait=True)
# Make sure the node has non-empty power state by forcing power off.
self.conn.baremetal.set_node_power_state(node, 'power off')
self.addCleanup(
lambda: self.conn.baremetal.update_node(node.id,
instance_id=None))
return node
def test_allocation_create_get_delete(self):
allocation = self.create_allocation(resource_class=self.resource_class)
self.assertEqual('allocating', allocation.state)
self.assertIsNone(allocation.node_id)
self.assertIsNone(allocation.last_error)
loaded = self.conn.baremetal.wait_for_allocation(allocation)
self.assertEqual(loaded.id, allocation.id)
self.assertEqual('active', allocation.state)
self.assertEqual(self.node.id, allocation.node_id)
self.assertIsNone(allocation.last_error)
node = self.conn.baremetal.get_node(self.node.id)
self.assertEqual(allocation.id, node.allocation_id)
self.conn.baremetal.delete_allocation(allocation, ignore_missing=False)
self.assertRaises(exceptions.ResourceNotFound,
self.conn.baremetal.get_allocation, allocation.id)
def test_allocation_list(self):
allocation1 = self.create_allocation(
resource_class=self.resource_class)
allocation2 = self.create_allocation(
resource_class=self.resource_class + '-fail')
self.conn.baremetal.wait_for_allocation(allocation1)
self.conn.baremetal.wait_for_allocation(allocation2, ignore_error=True)
allocations = self.conn.baremetal.allocations()
self.assertEqual({p.id for p in allocations},
{allocation1.id, allocation2.id})
allocations = self.conn.baremetal.allocations(state='active')
self.assertEqual([p.id for p in allocations], [allocation1.id])
allocations = self.conn.baremetal.allocations(node=self.node.id)
self.assertEqual([p.id for p in allocations], [allocation1.id])
allocations = self.conn.baremetal.allocations(
resource_class=self.resource_class + '-fail')
self.assertEqual([p.id for p in allocations], [allocation2.id])
def test_allocation_negative_failure(self):
allocation = self.create_allocation(
resource_class=self.resource_class + '-fail')
self.assertRaises(exceptions.SDKException,
self.conn.baremetal.wait_for_allocation,
allocation)
allocation = self.conn.baremetal.get_allocation(allocation.id)
self.assertEqual('error', allocation.state)
self.assertIn(self.resource_class + '-fail', allocation.last_error)
def test_allocation_negative_non_existing(self):
uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971"
self.assertRaises(exceptions.ResourceNotFound,
self.conn.baremetal.get_allocation, uuid)
self.assertRaises(exceptions.ResourceNotFound,
self.conn.baremetal.delete_allocation, uuid,
ignore_missing=False)
self.assertIsNone(self.conn.baremetal.delete_allocation(uuid))
def test_allocation_fields(self):
self.create_allocation(resource_class=self.resource_class)
result = self.conn.baremetal.allocations(fields=['uuid'])
for item in result:
self.assertIsNotNone(item.id)
self.assertIsNone(item.resource_class)

View File

@ -0,0 +1,109 @@
# 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.
from keystoneauth1 import adapter
import mock
from openstack.baremetal.v1 import allocation
from openstack import exceptions
from openstack.tests.unit import base
FAKE = {
"candidate_nodes": [],
"created_at": "2016-08-18T22:28:48.165105+00:00",
"extra": {},
"last_error": None,
"links": [
{
"href": "http://127.0.0.1:6385/v1/allocations/<PG_ID>",
"rel": "self"
},
{
"href": "http://127.0.0.1:6385/allocations/<PG_ID>",
"rel": "bookmark"
}
],
"name": "test_allocation",
"node_uuid": "6d85703a-565d-469a-96ce-30b6de53079d",
"resource_class": "baremetal",
"state": "active",
"traits": [],
"updated_at": None,
"uuid": "e43c722c-248e-4c6e-8ce8-0d8ff129387a",
}
class TestAllocation(base.TestCase):
def test_basic(self):
sot = allocation.Allocation()
self.assertIsNone(sot.resource_key)
self.assertEqual('allocations', sot.resources_key)
self.assertEqual('/allocations', sot.base_path)
self.assertTrue(sot.allow_create)
self.assertTrue(sot.allow_fetch)
self.assertFalse(sot.allow_commit)
self.assertTrue(sot.allow_delete)
self.assertTrue(sot.allow_list)
def test_instantiate(self):
sot = allocation.Allocation(**FAKE)
self.assertEqual(FAKE['candidate_nodes'], sot.candidate_nodes)
self.assertEqual(FAKE['created_at'], sot.created_at)
self.assertEqual(FAKE['extra'], sot.extra)
self.assertEqual(FAKE['last_error'], sot.last_error)
self.assertEqual(FAKE['links'], sot.links)
self.assertEqual(FAKE['name'], sot.name)
self.assertEqual(FAKE['node_uuid'], sot.node_id)
self.assertEqual(FAKE['resource_class'], sot.resource_class)
self.assertEqual(FAKE['state'], sot.state)
self.assertEqual(FAKE['traits'], sot.traits)
self.assertEqual(FAKE['updated_at'], sot.updated_at)
self.assertEqual(FAKE['uuid'], sot.id)
@mock.patch('time.sleep', lambda _t: None)
@mock.patch.object(allocation.Allocation, 'fetch', autospec=True)
class TestWaitForAllocation(base.TestCase):
def setUp(self):
super(TestWaitForAllocation, self).setUp()
self.session = mock.Mock(spec=adapter.Adapter)
self.session.default_microversion = '1.52'
self.fake = dict(FAKE, state='allocating', node_uuid=None)
self.allocation = allocation.Allocation(**self.fake)
def test_already_active(self, mock_fetch):
self.allocation.state = 'active'
allocation = self.allocation.wait(None)
self.assertIs(allocation, self.allocation)
self.assertFalse(mock_fetch.called)
def test_wait(self, mock_fetch):
marker = [False] # mutable object to modify in the closure
def _side_effect(allocation, session):
if marker[0]:
self.allocation.state = 'active'
self.allocation.node_id = FAKE['node_uuid']
else:
marker[0] = True
mock_fetch.side_effect = _side_effect
allocation = self.allocation.wait(self.session)
self.assertIs(allocation, self.allocation)
self.assertEqual(2, mock_fetch.call_count)
def test_timeout(self, mock_fetch):
self.assertRaises(exceptions.ResourceTimeout,
self.allocation.wait, self.session, timeout=0.001)
mock_fetch.assert_called_with(self.allocation, self.session)

View File

@ -13,6 +13,7 @@
import mock
from openstack.baremetal.v1 import _proxy
from openstack.baremetal.v1 import allocation
from openstack.baremetal.v1 import chassis
from openstack.baremetal.v1 import driver
from openstack.baremetal.v1 import node
@ -149,6 +150,20 @@ class TestBaremetalProxy(test_proxy_base.TestProxyBase):
self.assertIs(result, mock_list.return_value)
mock_list.assert_called_once_with(self.proxy, details=False, query=1)
def test_create_allocation(self):
self.verify_create(self.proxy.create_allocation, allocation.Allocation)
def test_get_allocation(self):
self.verify_get(self.proxy.get_allocation, allocation.Allocation)
def test_delete_allocation(self):
self.verify_delete(self.proxy.delete_allocation, allocation.Allocation,
False)
def test_delete_allocation_ignore(self):
self.verify_delete(self.proxy.delete_allocation, allocation.Allocation,
True)
@mock.patch('time.sleep', lambda _sec: None)
@mock.patch.object(_proxy.Proxy, 'get_node', autospec=True)

View File

@ -0,0 +1,4 @@
---
features:
- |
Adds support for the baremetal allocation API.