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,
|
delete_resource_provider_inventory,
|
||||||
get_resource_provider_inventory,
|
get_resource_provider_inventory,
|
||||||
resource_provider_inventories
|
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_class
|
||||||
v1/resource_provider
|
v1/resource_provider
|
||||||
v1/resource_provider_inventory
|
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 (
|
from openstack.placement.v1 import (
|
||||||
resource_provider_inventory as _resource_provider_inventory,
|
resource_provider_inventory as _resource_provider_inventory,
|
||||||
)
|
)
|
||||||
|
from openstack.placement.v1 import trait as _trait
|
||||||
from openstack import proxy
|
from openstack import proxy
|
||||||
from openstack import resource
|
from openstack import resource
|
||||||
|
|
||||||
@ -409,3 +410,53 @@ class Proxy(proxy.Proxy):
|
|||||||
resource_provider_id=resource_provider_id,
|
resource_provider_id=resource_provider_id,
|
||||||
**query,
|
**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:
|
class QueryParameters:
|
||||||
def __init__(self, *names, **mappings):
|
def __init__(
|
||||||
|
self,
|
||||||
|
*names,
|
||||||
|
include_pagination_defaults=True,
|
||||||
|
**mappings,
|
||||||
|
):
|
||||||
"""Create a dict of accepted query parameters
|
"""Create a dict of accepted query parameters
|
||||||
|
|
||||||
:param names: List of strings containing client-side query parameter
|
:param names: List of strings containing client-side query parameter
|
||||||
names. Each name in the list maps directly to the name
|
names. Each name in the list maps directly to the name
|
||||||
expected by the server.
|
expected by the server.
|
||||||
|
|
||||||
:param mappings: Key-value pairs where the key is the client-side
|
:param mappings: Key-value pairs where the key is the client-side
|
||||||
name we'll accept here and the value is the name
|
name we'll accept here and the value is the name
|
||||||
the server expects, e.g, changes_since=changes-since.
|
the server expects, e.g, ``changes_since=changes-since``.
|
||||||
Additionally, a value can be a dict with optional keys
|
Additionally, a value can be a dict with optional keys:
|
||||||
name - server-side name,
|
|
||||||
type - callable to convert from client to server
|
|
||||||
representation.
|
|
||||||
|
|
||||||
By default, both limit and marker are included in the initial mapping
|
- ``name`` - server-side name,
|
||||||
as they're the most common query parameters used for listing resources.
|
- ``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({name: name for name in names})
|
||||||
self._mapping.update(mappings)
|
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…
x
Reference in New Issue
Block a user