image: Add support for metadef property operations

Like placement's resource provider inventory API and keystone's old
extensions API, this is yet another example of an API that returns an
object with property names as keys and all other attributes as the
values, e.g. we see:

  {
    "os_admin_user": { ... },
    ...
  }

rather than:

  [
    {
      "name": "os_admin_user",
      ...
    },
    ,,,
  ]

Change-Id: I8e2ae8545cfaf32ced6d086a0921732f16282216
Co-authored-by: KIM SOJUNG <thwjd2717@gmail.com>
Co-authored-by: GA EUM KIM <rkdms7220@naver.com>
Co-authored-by: EunYoung Kim <lilac94.kim@gmail.com>
Co-authored-by: hyemin Choi <dropmoon3523@gmail.com>
Co-authored-by: Antonia Gaete <antoniagaete@osuosl.org>
Co-authored-by: YeJun, Jung <yejun614@naver.com>
This commit is contained in:
jihyun huh 2022-09-15 00:30:56 +09:00 committed by Stephen Finucane
parent 8b92fed92a
commit c2600c35b7
8 changed files with 545 additions and 0 deletions

View File

@ -84,6 +84,16 @@ Metadef Resource Type Operations
create_metadef_resource_type_association,
delete_metadef_resource_type_association
Metadef Property Operations
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: openstack.image.v2._proxy.Proxy
:noindex:
:members: create_metadef_property, update_metadef_property,
delete_metadef_property, get_metadef_property
Helpers
^^^^^^^

View File

@ -20,6 +20,7 @@ Image v2 Resources
v2/metadef_namespace
v2/metadef_object
v2/metadef_resource_type
v2/metadef_property
v2/metadef_schema
v2/task
v2/service_info

View File

@ -0,0 +1,13 @@
openstack.image.v2.metadef_property
===================================
.. automodule:: openstack.image.v2.metadef_property
The MetadefProperty Class
-------------------------
The ``MetadefProperty`` class inherits from
:class:`~openstack.resource.Resource`.
.. autoclass:: openstack.image.v2.metadef_property.MetadefProperty
:members:

View File

@ -20,6 +20,7 @@ from openstack.image.v2 import image as _image
from openstack.image.v2 import member as _member
from openstack.image.v2 import metadef_namespace as _metadef_namespace
from openstack.image.v2 import metadef_object as _metadef_object
from openstack.image.v2 import metadef_property as _metadef_property
from openstack.image.v2 import metadef_resource_type as _metadef_resource_type
from openstack.image.v2 import metadef_schema as _metadef_schema
from openstack.image.v2 import schema as _schema
@ -1397,6 +1398,132 @@ class Proxy(proxy.Proxy):
**query,
)
# ====== METADEF PROPERTY ======
def create_metadef_property(self, metadef_namespace, **attrs):
"""Create a metadef property
:param metadef_namespace: The value can be either the name of metadef
namespace or an
:class:`~openstack.image.v2.metadef_property.MetadefNamespace`
instance
:param attrs: The attributes to create on the metadef property
represented by ``metadef_property``.
:returns: The created metadef property
:rtype: :class:`~openstack.image.v2.metadef_property.MetadefProperty`
"""
namespace_name = resource.Resource._get_id(metadef_namespace)
return self._create(
_metadef_property.MetadefProperty,
namespace_name=namespace_name,
**attrs,
)
def update_metadef_property(
self, metadef_property, metadef_namespace, **attrs
):
"""Update a metadef property
:param metadef_property: The value can be either the name of metadef
property or an
:class:`~openstack.image.v2.metadef_property.MetadefProperty`
instance.
:param metadef_namespace: The value can be either the name of metadef
namespace or an
:class:`~openstack.image.v2.metadef_namespace.MetadefNamespace`
instance
:param attrs: The attributes to update on the metadef property
represented by ``metadef_property``.
:returns: The updated metadef property
:rtype: :class:`~openstack.image.v2.metadef_property.MetadefProperty`
"""
namespace_name = resource.Resource._get_id(metadef_namespace)
metadef_property = resource.Resource._get_id(metadef_property)
return self._update(
_metadef_property.MetadefProperty,
metadef_property,
namespace_name=namespace_name,
**attrs,
)
def delete_metadef_property(
self, metadef_property, metadef_namespace, ignore_missing=True
):
"""Delete a metadef property
:param metadef_property: The value can be either the name of metadef
property or an
:class:`~openstack.image.v2.metadef_property.MetadefProperty`
instance
:param metadef_namespace: The value can be either the name of metadef
namespace or an
:class:`~openstack.image.v2.metadef_namespace.MetadefNamespace`
instance
:param bool ignore_missing: When set to
``False`` :class:`~openstack.exceptions.ResourceNotFound` will be
raised when the instance does not exist. When set to ``True``,
no exception will be set when attempting to delete a nonexistent
instance.
:returns: ``None``
"""
namespace_name = resource.Resource._get_id(metadef_namespace)
metadef_property = resource.Resource._get_id(metadef_property)
return self._delete(
_metadef_property.MetadefProperty,
metadef_property,
namespace_name=namespace_name,
ignore_missing=ignore_missing,
)
def metadef_properties(self, metadef_namespace, **query):
"""Return a generator of metadef properties
:param metadef_namespace: The value can be either the name of metadef
namespace or an
:class:`~openstack.image.v2.metadef_namespace.MetadefNamespace`
instance
:param kwargs query: Optional query parameters to be sent to limit
the resources being returned.
:returns: A generator of property objects
"""
namespace_name = resource.Resource._get_id(metadef_namespace)
return self._list(
_metadef_property.MetadefProperty,
requires_id=False,
namespace_name=namespace_name,
**query,
)
def get_metadef_property(
self, metadef_property, metadef_namespace, **query
):
"""Get a single metadef property
:param metadef_property: The value can be either the name of metadef
property or an
:class:`~openstack.image.v2.metadef_property.MetadefProperty`
instance.
:param metadef_namespace: The value can be either the name of metadef
namespace or an
:class:`~openstack.image.v2.metadef_namespace.MetadefNamespace`
instance
:returns: One
:class:`~~openstack.image.v2.metadef_property.MetadefProperty`
:raises: :class:`~openstack.exceptions.ResourceNotFound` when no
resource can be found.
"""
namespace_name = resource.Resource._get_id(metadef_namespace)
return self._get(
_metadef_property.MetadefProperty,
metadef_property,
namespace_name=namespace_name,
**query,
)
# ====== SCHEMAS ======
def get_images_schema(self):
"""Get images schema

View File

@ -0,0 +1,179 @@
# 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 MetadefProperty(resource.Resource):
base_path = '/metadefs/namespaces/%(namespace_name)s/properties'
# capabilities
allow_create = True
allow_fetch = True
allow_commit = True
allow_delete = True
allow_list = True
#: An identifier (a name) for the namespace.
namespace_name = resource.URI('namespace_name')
#: The name of the property
name = resource.Body('name', alternate_id=True)
#: The property type.
type = resource.Body('type')
#: The title of the property.
title = resource.Body('title')
#: Detailed description of the property.
description = resource.Body('description')
#: A list of operator
operators = resource.Body('operators', type=list)
#: Default property description.
default = resource.Body('default')
#: Indicates whether this is a read-only property.
is_readonly = resource.Body('readonly', type=bool)
#: Minimum allowed numerical value.
minimum = resource.Body('minimum', type=int)
#: Maximum allowed numerical value.
maximum = resource.Body('maximum', type=int)
#: Enumerated list of property values.
enum = resource.Body('enum', type=list)
#: A regular expression
#: (`ECMA 262 <http://www.ecma-international.org/publications/standards/Ecma-262.htm>`_)
#: that a string value must match.
pattern = resource.Body('pattern')
#: Minimum allowed string length.
min_length = resource.Body('minLength', type=int, minimum=0, default=0)
#: Maximum allowed string length.
max_length = resource.Body('maxLength', type=int, minimum=0)
#: Schema for the items in an array.
items = resource.Body('items', type=dict)
#: Indicates whether all values in the array must be distinct.
require_unique_items = resource.Body(
'uniqueItems', type=bool, default=False
)
#: Minimum length of an array.
min_items = resource.Body('minItems', type=int, minimum=0, default=0)
#: Maximum length of an array.
max_items = resource.Body('maxItems', type=int, minimum=0)
#: Describes extra items, if you use tuple typing. If the value of
#: ``items`` is an array (tuple typing) and the instance is longer than
#: the list of schemas in ``items``, the additional items are described by
#: the schema in this property. If this value is ``false``, the instance
#: cannot be longer than the list of schemas in ``items``. If this value
#: is ``true``, that is equivalent to the empty schema (anything goes).
allow_additional_items = resource.Body('additionalItems', type=bool)
# 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 glance'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 name, property_data in data['properties'].items():
property = {
'name': name,
**property_data,
**uri_params,
}
value = cls.existing(
microversion=microversion,
connection=session._get_connection(),
**property,
)
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

@ -0,0 +1,129 @@
# 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
import string
from openstack.image.v2 import metadef_namespace as _metadef_namespace
from openstack.image.v2 import metadef_property as _metadef_property
from openstack.tests.functional.image.v2 import base
class TestMetadefProperty(base.BaseImageTest):
def setUp(self):
super().setUp()
# there's a limit on namespace length
namespace = 'test_' + ''.join(
random.choice(string.ascii_lowercase) for _ in range(75)
)
self.metadef_namespace = self.conn.image.create_metadef_namespace(
namespace=namespace,
)
self.assertIsInstance(
self.metadef_namespace,
_metadef_namespace.MetadefNamespace,
)
self.assertEqual(namespace, self.metadef_namespace.namespace)
# there's a limit on property length
property_name = 'test_' + ''.join(
random.choice(string.ascii_lowercase) for _ in range(75)
)
self.attrs = {
'name': property_name,
'title': property_name,
'type': 'string',
'description': 'Web Server port',
'enum': ["80", "443"],
}
self.metadef_property = self.conn.image.create_metadef_property(
self.metadef_namespace.namespace, **self.attrs
)
self.assertIsInstance(
self.metadef_property, _metadef_property.MetadefProperty
)
self.assertEqual(self.attrs['name'], self.metadef_property.name)
self.assertEqual(self.attrs['title'], self.metadef_property.title)
self.assertEqual(self.attrs['type'], self.metadef_property.type)
self.assertEqual(
self.attrs['description'], self.metadef_property.description
)
self.assertEqual(self.attrs['enum'], self.metadef_property.enum)
def tearDown(self):
# we do this in tearDown rather than via 'addCleanup' since we want to
# wait for the deletion of the resource to ensure it completes
self.conn.image.delete_metadef_property(
self.metadef_property, self.metadef_namespace
)
self.conn.image.delete_metadef_namespace(self.metadef_namespace)
self.conn.image.wait_for_delete(self.metadef_namespace)
super().tearDown()
def test_metadef_property(self):
# get metadef property
metadef_property = self.conn.image.get_metadef_property(
self.metadef_property, self.metadef_namespace
)
self.assertIsNotNone(metadef_property)
self.assertIsInstance(
metadef_property, _metadef_property.MetadefProperty
)
self.assertEqual(self.attrs['name'], metadef_property.name)
self.assertEqual(self.attrs['title'], metadef_property.title)
self.assertEqual(self.attrs['type'], metadef_property.type)
self.assertEqual(
self.attrs['description'], metadef_property.description
)
self.assertEqual(self.attrs['enum'], metadef_property.enum)
# (no find_metadef_property method)
# list
metadef_properties = list(
self.conn.image.metadef_properties(self.metadef_namespace)
)
self.assertIsNotNone(metadef_properties)
self.assertIsInstance(
metadef_properties[0], _metadef_property.MetadefProperty
)
# update
self.attrs['title'] = ''.join(
random.choice(string.ascii_lowercase) for _ in range(10)
)
self.attrs['description'] = ''.join(
random.choice(string.ascii_lowercase) for _ in range(10)
)
metadef_property = self.conn.image.update_metadef_property(
self.metadef_property,
self.metadef_namespace.namespace,
**self.attrs
)
self.assertIsNotNone(metadef_property)
self.assertIsInstance(
metadef_property,
_metadef_property.MetadefProperty,
)
metadef_property = self.conn.image.get_metadef_property(
self.metadef_property.name, self.metadef_namespace
)
self.assertEqual(
self.attrs['title'],
metadef_property.title,
)
self.assertEqual(
self.attrs['description'],
metadef_property.description,
)

View File

@ -0,0 +1,82 @@
# 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.image.v2 import metadef_property
from openstack.tests.unit import base
EXAMPLE = {
'namespace_name': 'CIM::StorageAllocationSettingData',
'name': 'Access',
'type': 'string',
'title': 'Access',
'description': (
'Access describes whether the allocated storage extent is '
'1 (readable), 2 (writeable), or 3 (both).'
),
'operators': ['<or>'],
'default': None,
'readonly': None,
'minimum': None,
'maximum': None,
'enum': [
'Unknown',
'Readable',
'Writeable',
'Read/Write Supported',
'DMTF Reserved',
],
'pattern': None,
'min_length': 0,
'max_length': None,
'items': None,
'unique_items': False,
'min_items': 0,
'max_items': None,
'additional_items': None,
}
class TestMetadefProperty(base.TestCase):
def test_basic(self):
sot = metadef_property.MetadefProperty()
self.assertEqual(
'/metadefs/namespaces/%(namespace_name)s/properties', 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)
def test_make_it(self):
sot = metadef_property.MetadefProperty(**EXAMPLE)
self.assertEqual(EXAMPLE['namespace_name'], sot.namespace_name)
self.assertEqual(EXAMPLE['name'], sot.name)
self.assertEqual(EXAMPLE['type'], sot.type)
self.assertEqual(EXAMPLE['title'], sot.title)
self.assertEqual(EXAMPLE['description'], sot.description)
self.assertListEqual(EXAMPLE['operators'], sot.operators)
self.assertEqual(EXAMPLE['default'], sot.default)
self.assertEqual(EXAMPLE['readonly'], sot.is_readonly)
self.assertEqual(EXAMPLE['minimum'], sot.minimum)
self.assertEqual(EXAMPLE['maximum'], sot.maximum)
self.assertListEqual(EXAMPLE['enum'], sot.enum)
self.assertEqual(EXAMPLE['pattern'], sot.pattern)
self.assertEqual(EXAMPLE['min_length'], sot.min_length)
self.assertEqual(EXAMPLE['max_length'], sot.max_length)
self.assertEqual(EXAMPLE['items'], sot.items)
self.assertEqual(EXAMPLE['unique_items'], sot.require_unique_items)
self.assertEqual(EXAMPLE['min_items'], sot.min_items)
self.assertEqual(EXAMPLE['max_items'], sot.max_items)
self.assertEqual(
EXAMPLE['additional_items'], sot.allow_additional_items
)

View File

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