placement: Add support for traits
Another weird one. There are two oddities here. Firstly, creation of a trait is a PUT request and doesn't return a body, only a 'Location'. It also doesn't error out if the trait already exists but rather returns a HTTP 204. This is mostly handled by setting the 'create_method' attribute. Secondly, the list response returns a list of strings rather than a list of objects. This is less easily worked around and once again requires a custom implementation of the 'list' class method. We extend the 'QueryParameter' class to accept a new kwarg argument, 'include_pagination_defaults', which allows us to disable adding the default 'limit' and 'marker' query string parameters: Placement doesn't use these. Change-Id: Idafa6c5c356d215224711b73c56a87ed7a690b94 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
parent
ebd4d75418
commit
d54f4e30af
@ -40,3 +40,10 @@ Resource Provider Inventories
|
||||
delete_resource_provider_inventory,
|
||||
get_resource_provider_inventory,
|
||||
resource_provider_inventories
|
||||
|
||||
Traits
|
||||
^^^^^^
|
||||
|
||||
.. autoclass:: openstack.placement.v1._proxy.Proxy
|
||||
:noindex:
|
||||
:members: create_trait, delete_trait, get_trait, traits
|
||||
|
@ -7,3 +7,4 @@ Placement v1 Resources
|
||||
v1/resource_class
|
||||
v1/resource_provider
|
||||
v1/resource_provider_inventory
|
||||
v1/trait
|
||||
|
12
doc/source/user/resources/placement/v1/trait.rst
Normal file
12
doc/source/user/resources/placement/v1/trait.rst
Normal file
@ -0,0 +1,12 @@
|
||||
openstack.placement.v1.trait
|
||||
============================
|
||||
|
||||
.. automodule:: openstack.placement.v1.trait
|
||||
|
||||
The Trait Class
|
||||
---------------
|
||||
|
||||
The ``Trait`` class inherits from :class:`~openstack.resource.Resource`.
|
||||
|
||||
.. autoclass:: openstack.placement.v1.trait.Trait
|
||||
:members:
|
@ -15,6 +15,7 @@ from openstack.placement.v1 import resource_provider as _resource_provider
|
||||
from openstack.placement.v1 import (
|
||||
resource_provider_inventory as _resource_provider_inventory,
|
||||
)
|
||||
from openstack.placement.v1 import trait as _trait
|
||||
from openstack import proxy
|
||||
from openstack import resource
|
||||
|
||||
@ -409,3 +410,53 @@ class Proxy(proxy.Proxy):
|
||||
resource_provider_id=resource_provider_id,
|
||||
**query,
|
||||
)
|
||||
|
||||
# ========== Traits ==========
|
||||
|
||||
def create_trait(self, name):
|
||||
"""Create a new trait
|
||||
|
||||
:param name: The name of the new trait
|
||||
|
||||
:returns: The results of trait creation
|
||||
:rtype: :class:`~openstack.placement.v1.trait.Trait`
|
||||
"""
|
||||
return self._create(_trait.Trait, name=name)
|
||||
|
||||
def delete_trait(self, trait, ignore_missing=True):
|
||||
"""Delete a trait
|
||||
|
||||
:param trait: The value can be either the ID of a trait or an
|
||||
:class:`~openstack.placement.v1.trait.Trait`, instance.
|
||||
: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``
|
||||
"""
|
||||
self._delete(_trait.Trait, trait, ignore_missing=ignore_missing)
|
||||
|
||||
def get_trait(self, trait):
|
||||
"""Get a single trait
|
||||
|
||||
:param trait: The value can be either the ID of a trait or an
|
||||
:class:`~openstack.placement.v1.trait.Trait`, instance.
|
||||
|
||||
:returns: An instance of
|
||||
:class:`~openstack.placement.v1.resource_provider_inventory.ResourceProviderInventory`
|
||||
:raises: :class:`~openstack.exceptions.ResourceNotFound` when no
|
||||
trait matching the criteria could be found.
|
||||
"""
|
||||
return self._get(_trait.Trait, trait)
|
||||
|
||||
def traits(self, **query):
|
||||
"""Retrieve a generator of traits
|
||||
|
||||
:param query: Optional query parameters to be sent to limit
|
||||
the resources being returned.
|
||||
|
||||
:returns: A generator of trait objects
|
||||
"""
|
||||
return self._list(_trait.Trait, **query)
|
||||
|
142
openstack/placement/v1/trait.py
Normal file
142
openstack/placement/v1/trait.py
Normal file
@ -0,0 +1,142 @@
|
||||
# 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 Trait(resource.Resource):
|
||||
resource_key = None
|
||||
resources_key = None
|
||||
base_path = '/traits'
|
||||
|
||||
# Capabilities
|
||||
|
||||
allow_create = True
|
||||
allow_fetch = True
|
||||
allow_delete = True
|
||||
allow_list = True
|
||||
|
||||
create_method = 'PUT'
|
||||
|
||||
# Added in 1.6
|
||||
_max_microversion = '1.6'
|
||||
|
||||
_query_mapping = resource.QueryParameters(
|
||||
'name',
|
||||
'associated',
|
||||
include_pagination_defaults=False,
|
||||
)
|
||||
|
||||
name = resource.Body('name', alternate_id=True)
|
||||
|
||||
@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 the list of strings (as opposed to a list of objects) that this
|
||||
call returns.
|
||||
|
||||
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 trait_name in data['traits']:
|
||||
trait = {
|
||||
'name': trait_name,
|
||||
**uri_params,
|
||||
}
|
||||
value = cls.existing(
|
||||
microversion=microversion,
|
||||
connection=session._get_connection(),
|
||||
**trait,
|
||||
)
|
||||
|
||||
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
|
@ -321,25 +321,32 @@ class _Request:
|
||||
|
||||
|
||||
class QueryParameters:
|
||||
def __init__(self, *names, **mappings):
|
||||
def __init__(
|
||||
self,
|
||||
*names,
|
||||
include_pagination_defaults=True,
|
||||
**mappings,
|
||||
):
|
||||
"""Create a dict of accepted query parameters
|
||||
|
||||
:param names: List of strings containing client-side query parameter
|
||||
names. Each name in the list maps directly to the name
|
||||
expected by the server.
|
||||
|
||||
names. Each name in the list maps directly to the name
|
||||
expected by the server.
|
||||
:param mappings: Key-value pairs where the key is the client-side
|
||||
name we'll accept here and the value is the name
|
||||
the server expects, e.g, changes_since=changes-since.
|
||||
Additionally, a value can be a dict with optional keys
|
||||
name - server-side name,
|
||||
type - callable to convert from client to server
|
||||
representation.
|
||||
name we'll accept here and the value is the name
|
||||
the server expects, e.g, ``changes_since=changes-since``.
|
||||
Additionally, a value can be a dict with optional keys:
|
||||
|
||||
By default, both limit and marker are included in the initial mapping
|
||||
as they're the most common query parameters used for listing resources.
|
||||
- ``name`` - server-side name,
|
||||
- ``type`` - callable to convert from client to server
|
||||
representation
|
||||
:param include_pagination_defaults: If true, include default pagination
|
||||
parameters, ``limit`` and ``marker``. These are the most common
|
||||
query parameters used for listing resources in OpenStack APIs.
|
||||
"""
|
||||
self._mapping = {"limit": "limit", "marker": "marker"}
|
||||
self._mapping = {}
|
||||
if include_pagination_defaults:
|
||||
self._mapping.update({"limit": "limit", "marker": "marker"})
|
||||
self._mapping.update({name: name for name in names})
|
||||
self._mapping.update(mappings)
|
||||
|
||||
|
61
openstack/tests/functional/placement/v1/test_trait.py
Normal file
61
openstack/tests/functional/placement/v1/test_trait.py
Normal file
@ -0,0 +1,61 @@
|
||||
# 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 trait as _trait
|
||||
from openstack.tests.functional import base
|
||||
|
||||
|
||||
class TestTrait(base.BaseFunctionalTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
if not self.operator_cloud.has_service('placement'):
|
||||
self.skipTest('placement service not supported by cloud')
|
||||
|
||||
self.trait_name = f'CUSTOM_{uuid.uuid4().hex.upper()}'
|
||||
|
||||
trait = self.operator_cloud.placement.create_trait(
|
||||
name=self.trait_name,
|
||||
)
|
||||
self.assertIsInstance(trait, _trait.Trait)
|
||||
self.assertEqual(self.trait_name, trait.name)
|
||||
|
||||
self.trait = trait
|
||||
|
||||
def tearDown(self):
|
||||
self.operator_cloud.placement.delete_trait(self.trait)
|
||||
super().tearDown()
|
||||
|
||||
def test_resource_provider_inventory(self):
|
||||
# list all traits
|
||||
|
||||
traits = list(self.operator_cloud.placement.traits())
|
||||
self.assertIsInstance(traits[0], _trait.Trait)
|
||||
self.assertIn(self.trait.name, {x.id for x in traits})
|
||||
|
||||
# (no update_trait method)
|
||||
|
||||
# retrieve details of the trait
|
||||
|
||||
trait = self.operator_cloud.placement.get_trait(self.trait)
|
||||
self.assertIsInstance(trait, _trait.Trait)
|
||||
self.assertEqual(self.trait_name, trait.id)
|
||||
|
||||
# retrieve details of the trait using IDs
|
||||
|
||||
trait = self.operator_cloud.placement.get_trait(self.trait_name)
|
||||
self.assertIsInstance(trait, _trait.Trait)
|
||||
self.assertEqual(self.trait_name, trait.id)
|
||||
|
||||
# (no find_trait method)
|
42
openstack/tests/unit/placement/v1/test_trait.py
Normal file
42
openstack/tests/unit/placement/v1/test_trait.py
Normal file
@ -0,0 +1,42 @@
|
||||
# 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 trait as _trait
|
||||
from openstack.tests.unit import base
|
||||
|
||||
FAKE = {
|
||||
'name': 'CUSTOM_FOO',
|
||||
}
|
||||
|
||||
|
||||
class TestResourceClass(base.TestCase):
|
||||
def test_basic(self):
|
||||
sot = _trait.Trait()
|
||||
self.assertEqual(None, sot.resource_key)
|
||||
self.assertEqual(None, sot.resources_key)
|
||||
self.assertEqual('/traits', 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)
|
||||
self.assertFalse(sot.allow_patch)
|
||||
|
||||
self.assertDictEqual(
|
||||
{'name': 'name', 'associated': 'associated'},
|
||||
sot._query_mapping._mapping,
|
||||
)
|
||||
|
||||
def test_make_it(self):
|
||||
sot = _trait.Trait(**FAKE)
|
||||
self.assertEqual(FAKE['name'], sot.id)
|
||||
self.assertEqual(FAKE['name'], sot.name)
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add support for the ``Trait`` Placement resource.
|
Loading…
Reference in New Issue
Block a user