diff --git a/doc/source/user/proxies/baremetal.rst b/doc/source/user/proxies/baremetal.rst index 1a6c212af..cace96a7a 100644 --- a/doc/source/user/proxies/baremetal.rst +++ b/doc/source/user/proxies/baremetal.rst @@ -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 --------- diff --git a/doc/source/user/resources/baremetal/index.rst b/doc/source/user/resources/baremetal/index.rst index 123fc5e4a..3f7c56fa0 100644 --- a/doc/source/user/resources/baremetal/index.rst +++ b/doc/source/user/resources/baremetal/index.rst @@ -9,3 +9,4 @@ Baremetal Resources v1/node v1/port v1/port_group + v1/allocation diff --git a/doc/source/user/resources/baremetal/v1/allocation.rst b/doc/source/user/resources/baremetal/v1/allocation.rst new file mode 100644 index 000000000..013518083 --- /dev/null +++ b/doc/source/user/resources/baremetal/v1/allocation.rst @@ -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: diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index 7280c6a5d..dd9d65aed 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -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) diff --git a/openstack/baremetal/v1/allocation.py b/openstack/baremetal/v1/allocation.py new file mode 100644 index 000000000..27ac55181 --- /dev/null +++ b/openstack/baremetal/v1/allocation.py @@ -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}) diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 6ab81c360..59d3b8785 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -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. diff --git a/openstack/tests/functional/baremetal/base.py b/openstack/tests/functional/baremetal/base.py index 4faf913a0..77796096a 100644 --- a/openstack/tests/functional/baremetal/base.py +++ b/openstack/tests/functional/baremetal/base.py @@ -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( diff --git a/openstack/tests/functional/baremetal/test_baremetal_allocation.py b/openstack/tests/functional/baremetal/test_baremetal_allocation.py new file mode 100644 index 000000000..1f36985bc --- /dev/null +++ b/openstack/tests/functional/baremetal/test_baremetal_allocation.py @@ -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) diff --git a/openstack/tests/unit/baremetal/v1/test_allocation.py b/openstack/tests/unit/baremetal/v1/test_allocation.py new file mode 100644 index 000000000..b2a82ec99 --- /dev/null +++ b/openstack/tests/unit/baremetal/v1/test_allocation.py @@ -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/", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/allocations/", + "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) diff --git a/openstack/tests/unit/baremetal/v1/test_proxy.py b/openstack/tests/unit/baremetal/v1/test_proxy.py index 0ab402785..cd6c65d53 100644 --- a/openstack/tests/unit/baremetal/v1/test_proxy.py +++ b/openstack/tests/unit/baremetal/v1/test_proxy.py @@ -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) diff --git a/releasenotes/notes/allocation-api-04f6b3b7a0ccc850.yaml b/releasenotes/notes/allocation-api-04f6b3b7a0ccc850.yaml new file mode 100644 index 000000000..8ca573f13 --- /dev/null +++ b/releasenotes/notes/allocation-api-04f6b3b7a0ccc850.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds support for the baremetal allocation API.