placement: Add support for resource provider inventories

Unlike resource classes, this one is not easy. The list operation is
particularly tricky: unlike most OpenStack APIs that returns a list of
objects, the resource provider inventory list operation returns an
object with resource class as the keys and the inventory details as the
values. Put another way, we see:

  {
    "MEMORY_MB": { ... },
    "VCPU": { ... },
    ...
  }

instead of the more typical:

  [
    {
      "resource_class": "MEMORY_MB",
      ...
    },
    {
      "resource_class": "VCPU",
      ...
    },
    ...
  ]

This need special handling code, namely a reimplementation of the 'list'
class method.

In addition, updating inventory requires providing
'resource_provider_generation'. This should match the current server
value of this field, but by setting this SDK assumes the field is
unchanged and doesn't include it in the list of "dirty" attributes. This
requires manually adding the 'resource_provider_generation' value to the
list of 'dirty' fields before the 'commit' operation.

Change-Id: I5ea5d0a477147e1a4e30b428f17b44807253deaa
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane 2021-04-22 17:29:44 +01:00
parent 69fea357b5
commit 15207b2070
10 changed files with 661 additions and 6 deletions

View File

@ -27,3 +27,14 @@ Resource Providers
:members: create_resource_provider, update_resource_provider,
delete_resource_provider, get_resource_provider,
find_resource_provider, resource_providers
Resource Provider Inventories
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: openstack.placement.v1._proxy.Proxy
:noindex:
:members: create_resource_provider_inventory,
update_resource_provider_inventory,
delete_resource_provider_inventory,
get_resource_provider_inventory,
resource_provider_inventories

View File

@ -6,3 +6,4 @@ Placement v1 Resources
v1/resource_class
v1/resource_provider
v1/resource_provider_inventory

View File

@ -0,0 +1,13 @@
openstack.placement.v1.resource_provider_inventory
==================================================
.. automodule:: openstack.placement.v1.resource_provider_inventory
The ResourceProviderInventory Class
-----------------------------------
The ``ResourceProviderInventory`` class inherits from
:class:`~openstack.resource.Resource`.
.. autoclass:: openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory
:members:

View File

@ -12,7 +12,11 @@
from openstack.placement.v1 import resource_class as _resource_class
from openstack.placement.v1 import resource_provider as _resource_provider
from openstack.placement.v1 import (
resource_provider_inventory as _resource_provider_inventory,
)
from openstack import proxy
from openstack import resource
class Proxy(proxy.Proxy):
@ -204,3 +208,160 @@ class Proxy(proxy.Proxy):
:returns: A generator of resource provider instances.
"""
return self._list(_resource_provider.ResourceProvider, **query)
# resource provider inventories
def create_resource_provider_inventory(
self,
resource_provider,
resource_class,
*,
total,
**attrs,
):
"""Create a new resource provider inventory from attributes
:param resource_provider: Either the ID of a resource provider or a
:class:`~openstack.placement.v1.resource_provider.ResourceProvider`
instance.
:param total: The actual amount of the resource that the provider can
accommodate.
:param attrs: Keyword arguments which will be used to create a
:class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`,
comprised of the properties on the ResourceProviderInventory class.
:returns: The results of resource provider inventory creation
:rtype: :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`
""" # noqa: E501
resource_provider_id = resource.Resource._get_id(resource_provider)
resource_class_name = resource.Resource._get_id(resource_class)
return self._create(
_resource_provider_inventory.ResourceProviderInventory,
resource_provider_id=resource_provider_id,
resource_class=resource_class_name,
total=total,
**attrs,
)
def delete_resource_provider_inventory(
self,
resource_provider_inventory,
resource_provider=None,
ignore_missing=True,
):
"""Delete a resource provider inventory
:param resource_provider_inventory: The value can be either the ID of a
resource provider or an
:class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`,
instance.
:param resource_provider: Either the ID of a resource provider or a
:class:`~openstack.placement.v1.resource_provider.ResourceProvider`
instance. This value must be specified when
``resource_provider_inventory`` is an ID.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be raised when
the resource provider inventory does not exist. When set to
``True``, no exception will be set when attempting to delete a
nonexistent resource provider inventory.
:returns: ``None``
"""
resource_provider_id = self._get_uri_attribute(
resource_provider_inventory,
resource_provider,
'resource_provider_id',
)
self._delete(
_resource_provider_inventory.ResourceProviderInventory,
resource_provider_inventory,
resource_provider_id=resource_provider_id,
ignore_missing=ignore_missing,
)
def update_resource_provider_inventory(
self,
resource_provider_inventory,
resource_provider=None,
*,
resource_provider_generation=None,
**attrs,
):
"""Update a resource provider's inventory
:param resource_provider_inventory: The value can be either the ID of a resource
provider inventory or an
:class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`,
instance.
:param resource_provider: Either the ID of a resource provider or a
:class:`~openstack.placement.v1.resource_provider.ResourceProvider`
instance. This value must be specified when
``resource_provider_inventory`` is an ID.
:attrs kwargs: The attributes to update on the resource provider inventory
represented by ``resource_provider_inventory``.
:returns: The updated resource provider inventory
:rtype: :class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`
""" # noqa: E501
resource_provider_id = self._get_uri_attribute(
resource_provider_inventory,
resource_provider,
'resource_provider_id',
)
return self._update(
_resource_provider_inventory.ResourceProviderInventory,
resource_provider_inventory,
resource_provider_id=resource_provider_id,
resource_provider_generation=resource_provider_generation,
**attrs,
)
def get_resource_provider_inventory(
self,
resource_provider_inventory,
resource_provider=None,
):
"""Get a single resource_provider_inventory
:param resource_provider_inventory: The value can be either the ID of a
resource provider inventory or an
:class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`,
instance.
:param resource_provider: Either the ID of a resource provider or a
:class:`~openstack.placement.v1.resource_provider.ResourceProvider`
instance. This value must be specified when
``resource_provider_inventory`` is an ID.
:returns: An instance of
:class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`
:raises: :class:`~openstack.exceptions.ResourceNotFound` when no
resource provider inventory matching the criteria could be found.
"""
resource_provider_id = self._get_uri_attribute(
resource_provider_inventory,
resource_provider,
'resource_provider_id',
)
return self._get(
_resource_provider_inventory.ResourceProviderInventory,
resource_provider_inventory,
resource_provider_id=resource_provider_id,
)
def resource_provider_inventories(self, resource_provider, **query):
"""Retrieve a generator of resource provider inventories
:param resource_provider: Either the ID of a resource provider or a
:class:`~openstack.placement.v1.resource_provider.ResourceProvider`
instance.
:param query: Optional query parameters to be sent to limit
the resources being returned.
:returns: A generator of resource provider inventory instances.
"""
resource_provider_id = resource.Resource._get_id(resource_provider)
return self._list(
_resource_provider_inventory.ResourceProviderInventory,
resource_provider_id=resource_provider_id,
**query,
)

View File

@ -0,0 +1,193 @@
# 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 exceptions
from openstack import resource
class ResourceProviderInventory(resource.Resource):
resource_key = None
resources_key = None
base_path = '/resource_providers/%(resource_provider_id)s/inventories'
_query_mapping = {}
# Capabilities
allow_create = True
allow_fetch = True
allow_commit = True
allow_delete = True
allow_list = True
# Properties
#: The UUID of a resource provider.
resource_provider_id = resource.URI('resource_provider_id')
#: The name of the resource class.
resource_class = resource.Body('resource_class', alternate_id=True)
#: A consistent view marker that assists with the management of concurrent
#: resource provider updates.
resource_provider_generation = resource.Body(
'resource_provider_generation',
type=int,
)
#: It is used in determining whether consumption of the resource of the
#: provider can exceed physical constraints.
allocation_ratio = resource.Body('allocation_ratio', type=float)
#: A maximum amount any single allocation against an inventory can have.
max_unit = resource.Body('max_unit', type=int)
#: A minimum amount any single allocation against an inventory can have.
min_unit = resource.Body('min_unit', type=int)
#: The amount of the resource a provider has reserved for its own use.
reserved = resource.Body('reserved', type=int)
#: A representation of the divisible amount of the resource that may be
#: requested. For example, step_size = 5 means that only values divisible
#: by 5 (5, 10, 15, etc.) can be requested.
step_size = resource.Body('step_size', type=int)
#: The actual amount of the resource that the provider can accommodate.
total = resource.Body('total', type=int)
def commit(
self,
session,
prepend_key=True,
has_body=True,
retry_on_conflict=None,
base_path=None,
*,
microversion=None,
**kwargs,
):
# resource_provider_generation must always be provided on update, but
# it will appear to be identical (by design) so we strip it. Prevent
# tihs happening.
self._body._dirty.add('resource_provider_generation')
return super().commit(
session,
prepend_key=prepend_key,
has_body=has_body,
retry_on_conflict=retry_on_conflict,
base_path=base_path,
microversion=microversion,
**kwargs,
)
# TODO(stephenfin): It would be nicer if we could do this in Resource
# itself since the logic is also found elsewhere (e.g.
# openstack.identity.v2.extension.Extension) but that code is a bit of a
# rat's nest right now and needs a spring clean
@classmethod
def list(
cls,
session,
paginated=True,
base_path=None,
allow_unknown_params=False,
*,
microversion=None,
**params,
):
"""This method is a generator which yields resource objects.
A re-implementation of :meth:`~openstack.resource.Resource.list` that
handles placement's single, unpaginated list implementation.
Refer to :meth:`~openstack.resource.Resource.list` for full
documentation including parameter, exception and return type
documentation.
"""
session = cls._get_session(session)
if microversion is None:
microversion = cls._get_microversion(session, action='list')
if base_path is None:
base_path = cls.base_path
# There is no server-side filtering, only client-side
client_filters = {}
# Gather query parameters which are not supported by the server
for k, v in params.items():
if (
# Known attr
hasattr(cls, k)
# Is real attr property
and isinstance(getattr(cls, k), resource.Body)
# not included in the query_params
and k not in cls._query_mapping._mapping.keys()
):
client_filters[k] = v
uri = base_path % params
uri_params = {}
for k, v in params.items():
# We need to gather URI parts to set them on the resource later
if hasattr(cls, k) and isinstance(getattr(cls, k), resource.URI):
uri_params[k] = v
def _dict_filter(f, d):
"""Dict param based filtering"""
if not d:
return False
for key in f.keys():
if isinstance(f[key], dict):
if not _dict_filter(f[key], d.get(key, None)):
return False
elif d.get(key, None) != f[key]:
return False
return True
response = session.get(
uri,
headers={"Accept": "application/json"},
params={},
microversion=microversion,
)
exceptions.raise_from_response(response)
data = response.json()
for resource_class, resource_data in data['inventories'].items():
resource_inventory = {
'resource_class': resource_class,
'resource_provider_generation': data[
'resource_provider_generation'
], # noqa: E501
**resource_data,
**uri_params,
}
value = cls.existing(
microversion=microversion,
connection=session._get_connection(),
**resource_inventory,
)
filters_matched = True
# Iterate over client filters and return only if matching
for key in client_filters.keys():
if isinstance(client_filters[key], dict):
if not _dict_filter(
client_filters[key],
value.get(key, None),
):
filters_matched = False
break
elif value.get(key, None) != client_filters[key]:
filters_matched = False
break
if filters_matched:
yield value
return None

View File

@ -350,7 +350,7 @@ class QueryParameters:
client-side parameter name or server side name.
:param base_path: Formatted python string of the base url path for
the resource.
: param allow_unknown_params: Exclude query params not known by the
:param allow_unknown_params: Exclude query params not known by the
resource.
:returns: Filtered collection of the supported QueryParameters
@ -2018,17 +2018,19 @@ class Resource(dict):
raise exceptions.MethodNotSupported(cls, 'list')
session = cls._get_session(session)
if microversion is None:
microversion = cls._get_microversion(session, action='list')
if base_path is None:
base_path = cls.base_path
api_filters = cls._query_mapping._validate(
params,
base_path=base_path,
allow_unknown_params=True,
)
client_filters = dict()
client_filters = {}
# Gather query parameters which are not supported by the server
for k, v in params.items():
if (

View File

@ -0,0 +1,163 @@
# 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 uuid
from openstack.placement.v1 import resource_class as _resource_class
from openstack.placement.v1 import resource_provider as _resource_provider
from openstack.placement.v1 import (
resource_provider_inventory as _resource_provider_inventory,
)
from openstack.tests.functional import base
class TestResourceProviderInventory(base.BaseFunctionalTest):
def setUp(self):
super().setUp()
if not self.operator_cloud.has_service('placement'):
self.skipTest('placement service not supported by cloud')
self.resource_provider_name = self.getUniqueString()
self.resource_class_name = f'CUSTOM_{uuid.uuid4().hex.upper()}'
resource_class = self.operator_cloud.placement.create_resource_class(
name=self.resource_class_name,
)
self.assertIsInstance(resource_class, _resource_class.ResourceClass)
self.assertEqual(self.resource_class_name, resource_class.name)
resource_provider = (
self.operator_cloud.placement.create_resource_provider(
name=self.resource_provider_name,
)
)
self.assertIsInstance(
resource_provider,
_resource_provider.ResourceProvider,
)
self.assertEqual(self.resource_provider_name, resource_provider.name)
self.resource_provider = resource_provider
self.resource_class = resource_class
def tearDown(self):
self.operator_cloud.placement.delete_resource_provider(
self.resource_provider,
)
self.operator_cloud.placement.delete_resource_class(
self.resource_class,
)
super().tearDown()
def test_resource_provider_inventory(self):
# create the resource provider inventory
resource_provider_inventory = (
self.operator_cloud.placement.create_resource_provider_inventory(
self.resource_provider,
resource_class=self.resource_class,
total=10,
step_size=1,
)
)
self.assertIsInstance(
resource_provider_inventory,
_resource_provider_inventory.ResourceProviderInventory,
)
self.assertEqual(
self.resource_class.name,
resource_provider_inventory.resource_class,
)
self.assertEqual(10, resource_provider_inventory.total)
# list all resource provider inventories (there should only be one)
resource_provider_inventories = list(
self.operator_cloud.placement.resource_provider_inventories(
self.resource_provider
)
)
self.assertIsInstance(
resource_provider_inventories[0],
_resource_provider_inventory.ResourceProviderInventory,
)
self.assertIn(
self.resource_class.name,
{rpi.id for rpi in resource_provider_inventories},
)
# update the resource provider inventory
resource_provider_inventory = self.operator_cloud.placement.update_resource_provider_inventory(
resource_provider_inventory,
total=20,
resource_provider_generation=resource_provider_inventory.resource_provider_generation,
)
self.assertIsInstance(
resource_provider_inventory,
_resource_provider_inventory.ResourceProviderInventory,
)
self.assertEqual(
self.resource_class.name,
resource_provider_inventory.id,
)
self.assertEqual(20, resource_provider_inventory.total)
# retrieve details of the (updated) resource provider inventory
resource_provider_inventory = (
self.operator_cloud.placement.get_resource_provider_inventory(
resource_provider_inventory,
)
)
self.assertIsInstance(
resource_provider_inventory,
_resource_provider_inventory.ResourceProviderInventory,
)
self.assertEqual(
self.resource_class.name,
resource_provider_inventory.id,
)
self.assertEqual(20, resource_provider_inventory.total)
# retrieve details of the resource provider inventory using IDs
# (requires us to provide the resource provider also)
resource_provider_inventory = (
self.operator_cloud.placement.get_resource_provider_inventory(
resource_provider_inventory.id,
self.resource_provider,
)
)
self.assertIsInstance(
resource_provider_inventory,
_resource_provider_inventory.ResourceProviderInventory,
)
self.assertEqual(
self.resource_class.name,
resource_provider_inventory.id,
)
self.assertEqual(20, resource_provider_inventory.total)
# (no find_resource_provider_inventory method)
# delete the resource provider inventory
result = (
self.operator_cloud.placement.delete_resource_provider_inventory(
resource_provider_inventory,
self.resource_provider,
ignore_missing=False,
)
)
self.assertIsNone(result)

View File

@ -13,6 +13,7 @@
from openstack.placement.v1 import _proxy
from openstack.placement.v1 import resource_class
from openstack.placement.v1 import resource_provider
from openstack.placement.v1 import resource_provider_inventory
from openstack.tests.unit import test_proxy_base as test_proxy_base
@ -22,8 +23,7 @@ class TestPlacementProxy(test_proxy_base.TestProxyBase):
self.proxy = _proxy.Proxy(self.session)
# resource classes
class TestPlacementResourceClass:
class TestPlacementResourceClass(TestPlacementProxy):
def test_resource_class_create(self):
self.verify_create(
self.proxy.create_resource_class,
@ -57,8 +57,7 @@ class TestPlacementResourceClass:
)
# resource providers
class TestPlacementResourceProvider:
class TestPlacementResourceProvider(TestPlacementProxy):
def test_resource_provider_create(self):
self.verify_create(
self.proxy.create_resource_provider,
@ -90,3 +89,60 @@ class TestPlacementResourceProvider:
self.proxy.resource_providers,
resource_provider.ResourceProvider,
)
class TestPlacementResourceProviderInventory(TestPlacementProxy):
def test_resource_provider_inventory_create(self):
self.verify_create(
self.proxy.create_resource_provider_inventory,
resource_provider_inventory.ResourceProviderInventory,
method_kwargs={
'resource_provider': 'test_id',
'resource_class': 'CUSTOM_FOO',
'total': 20,
},
expected_kwargs={
'resource_provider_id': 'test_id',
'resource_class': 'CUSTOM_FOO',
'total': 20,
},
)
def test_resource_provider_inventory_delete(self):
self.verify_delete(
self.proxy.delete_resource_provider_inventory,
resource_provider_inventory.ResourceProviderInventory,
ignore_missing=False,
method_kwargs={'resource_provider': 'test_id'},
expected_kwargs={'resource_provider_id': 'test_id'},
)
def test_resource_provider_inventory_update(self):
self.verify_update(
self.proxy.update_resource_provider_inventory,
resource_provider_inventory.ResourceProviderInventory,
method_kwargs={
'resource_provider': 'test_id',
'resource_provider_generation': 1,
},
expected_kwargs={
'resource_provider_id': 'test_id',
'resource_provider_generation': 1,
},
)
def test_resource_provider_inventory_get(self):
self.verify_get(
self.proxy.get_resource_provider_inventory,
resource_provider_inventory.ResourceProviderInventory,
method_kwargs={'resource_provider': 'test_id'},
expected_kwargs={'resource_provider_id': 'test_id'},
)
def test_resource_provider_inventories(self):
self.verify_list(
self.proxy.resource_provider_inventories,
resource_provider_inventory.ResourceProviderInventory,
method_kwargs={'resource_provider': 'test_id'},
expected_kwargs={'resource_provider_id': 'test_id'},
)

View File

@ -0,0 +1,51 @@
# 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.placement.v1 import resource_provider_inventory
from openstack.tests.unit import base
FAKE = {
'allocation_ratio': 1.0,
'max_unit': 35,
'min_unit': 1,
'reserved': 0,
'step_size': 1,
'total': 35,
}
class TestResourceProviderInventory(base.TestCase):
def test_basic(self):
sot = resource_provider_inventory.ResourceProviderInventory()
self.assertIsNone(sot.resource_key)
self.assertIsNone(sot.resources_key)
self.assertEqual(
'/resource_providers/%(resource_provider_id)s/inventories',
sot.base_path,
)
self.assertTrue(sot.allow_create)
self.assertTrue(sot.allow_fetch)
self.assertTrue(sot.allow_commit)
self.assertTrue(sot.allow_delete)
self.assertTrue(sot.allow_list)
self.assertFalse(sot.allow_patch)
self.assertDictEqual({}, sot._query_mapping)
def test_make_it(self):
sot = resource_provider_inventory.ResourceProviderInventory(**FAKE)
self.assertEqual(FAKE['allocation_ratio'], sot.allocation_ratio)
self.assertEqual(FAKE['max_unit'], sot.max_unit)
self.assertEqual(FAKE['min_unit'], sot.min_unit)
self.assertEqual(FAKE['reserved'], sot.reserved)
self.assertEqual(FAKE['step_size'], sot.step_size)
self.assertEqual(FAKE['total'], sot.total)

View File

@ -0,0 +1,4 @@
---
features:
- |
Added support for the ``ResourceProviderInventory`` Placement resource.