Rehome neutron.api.v2.attributes
This patch rehomes a bulk of neutron.api.v2.attributes into lib with the following notable aspects: - A new AttributeInfo class is available and wraps a resource's attribute dict to provide operations with the attributes. The class can be used as a operational wrapper on a dict of attrs, or it an be used as a transport of attrs based on the consumer's need. - A global singleton attribute map RESOURCES is available and is initially populated with core resources. - Some unit tests are beefed a little and make them more modular. - Some huge comments into the devref. - The fixture for API defs is updated to support backing up the global RESOURCES dict. Change-Id: Ic455e1af2796e58025381160dc5c3b83217413fa
This commit is contained in:
		
							
								
								
									
										90
									
								
								doc/source/devref/api_attributes.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								doc/source/devref/api_attributes.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| .. | ||||
|       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. | ||||
|  | ||||
|  | ||||
|       Convention for heading levels in Neutron devref: | ||||
|       =======  Heading 0 (reserved for the title in a document) | ||||
|       -------  Heading 1 | ||||
|       ~~~~~~~  Heading 2 | ||||
|       +++++++  Heading 3 | ||||
|       '''''''  Heading 4 | ||||
|       (Avoid deeper levels because they do not render well.) | ||||
|  | ||||
|  | ||||
| API Attributes | ||||
| ============== | ||||
|  | ||||
| Neutron's resource attributes are defined in dictionaries | ||||
| in ``api/definitions``. | ||||
|  | ||||
| The map containing all installed resources (for core and active extensions) | ||||
| is in ``api/attributes.py``. | ||||
|  | ||||
|  | ||||
| Attribute map structure | ||||
| ----------------------- | ||||
|  | ||||
| Example attribute definitions for ``dns_name``: | ||||
|  | ||||
| .. code-block:: python | ||||
|  | ||||
|         'dns_name': { | ||||
|             'allow_post': True, | ||||
|             'allow_put': True, | ||||
|             'default': '', | ||||
|             'convert_to': convert_to_lowercase, | ||||
|             'validate': {'type:dns_name': FQDN_MAX_LEN}, | ||||
|             'is_visible': True | ||||
|         }, | ||||
|  | ||||
|  | ||||
| The ``validate`` item specifies rules for `validating <api_validators.html>`_ | ||||
| the attribute. | ||||
|  | ||||
| The ``convert_to`` item specifies rules for `converting <api_converters.html>`_ | ||||
| the attribute. | ||||
|  | ||||
| Example attribute definitions for ``gateway_ip``: | ||||
|  | ||||
| .. code-block:: python | ||||
|  | ||||
|         'gateway_ip': { | ||||
|             'allow_post': True, | ||||
|             'allow_put': True, | ||||
|             'default': constants.ATTR_NOT_SPECIFIED, | ||||
|             'validate': {'type:ip_address_or_none': None}, | ||||
|             'is_visible': True | ||||
|         } | ||||
|  | ||||
| Note: a default of ``ATTR_NOT_SPECIFIED`` indicates that an attribute is not | ||||
| required, but will be generated by the plugin if it is not specified. | ||||
| Particularly, a value of ``ATTR_NOT_SPECIFIED`` is different from an | ||||
| attribute that has been specified with a value of ``None``.  For example, | ||||
| if ``gateway_ip`` is omitted in a request to create a subnet, the plugin | ||||
| will receive ``ATTR_NOT_SPECIFIED`` and the default gateway IP will be | ||||
| generated.  However, if ``gateway_ip`` is specified as ``None``, this means | ||||
| that the subnet does not have a gateway IP. | ||||
|  | ||||
| The following are the defined keys for attribute maps: | ||||
|  | ||||
| ======================  ====== | ||||
| ``default``             default value of the attribute (if missing, the attribute becomes mandatory) | ||||
| ``allow_post``          the attribute can be used on ``POST`` requests | ||||
| ``allow_put``           the attribute can be used on ``PUT`` requests | ||||
| ``validate``            specifies rules for validating data in the attribute | ||||
| ``convert_to``          transformation to apply to the value before it is returned | ||||
| ``convert_list_to``     if the value is a list, apply this transformation to the value before it is returned | ||||
| ``is_visible``          the attribute is returned in ``GET`` responses | ||||
| ``required_by_policy``  the attribute is required by the policy engine and should therefore be filled by the API layer even if not present in request body | ||||
| ``enforce_policy``      the attribute is actively part of the policy enforcing mechanism, ie: there might be rules which refer to this attribute | ||||
| ======================  ====== | ||||
| @@ -64,7 +64,6 @@ dictionary key for the validator. For example: | ||||
|  | ||||
| :: | ||||
|  | ||||
|   RESOURCE_ATTRIBUTE_MAP = { | ||||
|       NETWORKS: { | ||||
|           'id': {'allow_post': False, 'allow_put': False, | ||||
|                  'validate': {'type:uuid': None}, | ||||
| @@ -72,7 +71,7 @@ dictionary key for the validator. For example: | ||||
|                  'primary_key': True}, | ||||
|           'name': {'allow_post': True, 'allow_put': True, | ||||
|                    'validate': {'type:string': NAME_MAX_LEN}, | ||||
|                    'default': '', 'is_visible': True}, | ||||
|                    'default': '', 'is_visible': True}} | ||||
|  | ||||
| Here, the networks resource has an 'id' attribute with a UUID validator, | ||||
| as seen by the 'validate' key containing a dictionary with a key of | ||||
| @@ -88,7 +87,7 @@ could have a validator defined as follows: | ||||
|    'ip_version': {'allow_post': True, 'allow_put': False, | ||||
|                   'convert_to': conversions.convert_to_int, | ||||
|                   'validate': {'type:values': [4, 6]}, | ||||
|                   'is_visible': True}, | ||||
|                   'is_visible': True}} | ||||
|  | ||||
| Here, the validate_values() method will take the list of values as the | ||||
| allowable values that can be specified for this attribute. | ||||
| @@ -99,4 +98,3 @@ Test The Validator | ||||
| Do the right thing, and make sure you've created a unit test for any | ||||
| validator that you add to verify that it works as expected, even for | ||||
| simple validators. | ||||
|  | ||||
|   | ||||
| @@ -32,6 +32,7 @@ Neutron Lib Internals | ||||
| .. toctree:: | ||||
|    :maxdepth: 3 | ||||
|  | ||||
|    api_attributes | ||||
|    api_extensions | ||||
|    api_converters | ||||
|    api_validators | ||||
|   | ||||
| @@ -13,6 +13,22 @@ | ||||
| from webob import exc | ||||
|  | ||||
| from neutron_lib._i18n import _ | ||||
| from neutron_lib.api.definitions import network | ||||
| from neutron_lib.api.definitions import port | ||||
| from neutron_lib.api.definitions import subnet | ||||
| from neutron_lib.api.definitions import subnetpool | ||||
| from neutron_lib.api import validators | ||||
| from neutron_lib import constants | ||||
| from neutron_lib import exceptions | ||||
|  | ||||
|  | ||||
| def _validate_privileges(context, res_dict): | ||||
|     if ('project_id' in res_dict and | ||||
|             res_dict['project_id'] != context.project_id and | ||||
|             not context.is_admin): | ||||
|         msg = _("Specifying 'project_id' or 'tenant_id' other than the " | ||||
|                 "authenticated project in request requires admin privileges") | ||||
|         raise exc.HTTPBadRequest(msg) | ||||
|  | ||||
|  | ||||
| def populate_project_info(attributes): | ||||
| @@ -23,12 +39,11 @@ def populate_project_info(attributes): | ||||
|  | ||||
|     If neither are present then attributes is not updated. | ||||
|  | ||||
|     :param attributes: a dictionary of resource/API attributes | ||||
|     :type attributes: dict | ||||
|  | ||||
|     :return: the updated attributes dictionary | ||||
|     :rtype: dict | ||||
|  | ||||
|     :param attributes: A dictionary of resource/API attributes | ||||
|     or API request/response dict. | ||||
|     :returns: attributes (updated with project_id if applicable). | ||||
|     :raises: HTTPBadRequest if the attributes project_id and tenant_id | ||||
|     don't match. | ||||
|     """ | ||||
|     if 'tenant_id' in attributes and 'project_id' not in attributes: | ||||
|         attributes['project_id'] = attributes['tenant_id'] | ||||
| @@ -41,3 +56,159 @@ def populate_project_info(attributes): | ||||
|         raise exc.HTTPBadRequest(msg) | ||||
|  | ||||
|     return attributes | ||||
|  | ||||
|  | ||||
| class AttributeInfo(object): | ||||
|     """Provides operations on a resource's attribute map. | ||||
|  | ||||
|     AttributeInfo wraps an API resource's attribute dict and provides methods | ||||
|     for filling defaults, validating, converting, etc. based on the | ||||
|     underlying attributes. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, resource_attrs): | ||||
|         """Create a new instance that wraps the given resource attributes. | ||||
|  | ||||
|         :param resource_attrs: The resource's attributes that can be any | ||||
|         of the following types: an instance of AttributeInfo, an API definition | ||||
|         that contains a RESOURCE_ATTRIBUTE_MAP attribute or a dict of | ||||
|         attributes for the resource. | ||||
|         """ | ||||
|         if isinstance(resource_attrs, AttributeInfo): | ||||
|             resource_attrs = resource_attrs.attributes | ||||
|         elif getattr(resource_attrs, | ||||
|                      'RESOURCE_ATTRIBUTE_MAP', None) is not None: | ||||
|             # handle neutron_lib API definitions | ||||
|             resource_attrs = resource_attrs.RESOURCE_ATTRIBUTE_MAP | ||||
|  | ||||
|         self.attributes = resource_attrs | ||||
|  | ||||
|     def fill_post_defaults( | ||||
|             self, res_dict, | ||||
|             exc_cls=lambda m: exceptions.InvalidInput(error_message=m), | ||||
|             check_allow_post=True): | ||||
|         """Fill in default values for attributes in a POST request. | ||||
|  | ||||
|         When a POST request is made, the attributes with default values do not | ||||
|         need to be specified by the user. This function fills in the values of | ||||
|         any unspecified attributes if they have a default value. | ||||
|  | ||||
|         If an attribute is not specified and it does not have a default value, | ||||
|         an exception is raised. | ||||
|  | ||||
|         If an attribute is specified and it is not allowed in POST requests, an | ||||
|         exception is raised. The caller can override this behavior by setting | ||||
|         check_allow_post=False (used by some internal admin operations). | ||||
|  | ||||
|         :param res_dict: The resource attributes from the request. | ||||
|         :param exc_cls: Exception to be raised on error that must take | ||||
|         a single error message as it's only constructor arg. | ||||
|         :param check_allow_post: Raises an exception if a non-POST-able | ||||
|         attribute is specified. | ||||
|         :raises: exc_cls If check_allow_post is True and this instance of | ||||
|         ResourceAttributes doesn't support POST. | ||||
|         """ | ||||
|         for attr, attr_vals in self.attributes.items(): | ||||
|             if attr_vals['allow_post']: | ||||
|                 if 'default' not in attr_vals and attr not in res_dict: | ||||
|                     msg = _("Failed to parse request. Required " | ||||
|                             "attribute '%s' not specified") % attr | ||||
|                     raise exc_cls(msg) | ||||
|                 res_dict[attr] = res_dict.get(attr, | ||||
|                                               attr_vals.get('default')) | ||||
|             elif check_allow_post: | ||||
|                 if attr in res_dict: | ||||
|                     msg = _("Attribute '%s' not allowed in POST") % attr | ||||
|                     raise exc_cls(msg) | ||||
|  | ||||
|     def convert_values( | ||||
|             self, res_dict, | ||||
|             exc_cls=lambda m: exceptions.InvalidInput(error_message=m)): | ||||
|         """Convert and validate attribute values for a request. | ||||
|  | ||||
|         :param res_dict: The resource attributes from the request. | ||||
|         :param exc_cls: Exception to be raised on error that must take | ||||
|         a single error message as it's only constructor arg. | ||||
|         :raises: exc_cls If any errors occur converting/validating the | ||||
|         res_dict. | ||||
|         """ | ||||
|         for attr, attr_vals in self.attributes.items(): | ||||
|             if (attr not in res_dict or | ||||
|                     res_dict[attr] is constants.ATTR_NOT_SPECIFIED): | ||||
|                 continue | ||||
|             # Convert values if necessary | ||||
|             if 'convert_to' in attr_vals: | ||||
|                 res_dict[attr] = attr_vals['convert_to'](res_dict[attr]) | ||||
|             # Check that configured values are correct | ||||
|             if 'validate' not in attr_vals: | ||||
|                 continue | ||||
|             for rule in attr_vals['validate']: | ||||
|                 validator = validators.get_validator(rule) | ||||
|                 res = validator(res_dict[attr], attr_vals['validate'][rule]) | ||||
|  | ||||
|                 if res: | ||||
|                     msg_dict = dict(attr=attr, reason=res) | ||||
|                     msg = _("Invalid input for %(attr)s. " | ||||
|                             "Reason: %(reason)s.") % msg_dict | ||||
|                     raise exc_cls(msg) | ||||
|  | ||||
|     def populate_project_id(self, context, res_dict, is_create): | ||||
|         """Populate the owner information in a request body. | ||||
|  | ||||
|         Ensure both project_id and tenant_id attributes are present. | ||||
|         Validate that the requestor has the required privileges. | ||||
|         For a create request, copy owner info from context to request body | ||||
|         if needed and verify that owner is specified if required. | ||||
|  | ||||
|         :param context: The request context. | ||||
|         :param res_dict: The resource attributes from the request. | ||||
|         :param attr_info: The attribute map for the resource. | ||||
|         :param is_create: Is this a create request? | ||||
|         :raises: HTTPBadRequest If neither the project_id nor tenant_id | ||||
|         are specified in the res_dict. | ||||
|  | ||||
|         """ | ||||
|         populate_project_info(res_dict) | ||||
|         _validate_privileges(context, res_dict) | ||||
|  | ||||
|         if is_create and 'project_id' not in res_dict: | ||||
|             if context.project_id: | ||||
|                 res_dict['project_id'] = context.project_id | ||||
|  | ||||
|                 # For backward compatibility | ||||
|                 res_dict['tenant_id'] = context.project_id | ||||
|  | ||||
|             elif 'tenant_id' in self.attributes: | ||||
|                 msg = _("Running without keystone AuthN requires " | ||||
|                         "that tenant_id is specified") | ||||
|                 raise exc.HTTPBadRequest(msg) | ||||
|  | ||||
|     def verify_attributes(self, attrs_to_verify): | ||||
|         """Reject unknown attributes. | ||||
|  | ||||
|         Consumers should ensure the project info is populated in the | ||||
|         attrs_to_verify before calling this method. | ||||
|  | ||||
|         :param attrs_to_verify: The attributes to verify against this | ||||
|         resource attributes. | ||||
|         :raises: HTTPBadRequest: If attrs_to_verify contains any unrecognized | ||||
|         for this resource attributes instance. | ||||
|         """ | ||||
|         extra_keys = set(attrs_to_verify.keys()) - set(self.attributes.keys()) | ||||
|         if extra_keys: | ||||
|             msg = _("Unrecognized attribute(s) '%s'") % ', '.join(extra_keys) | ||||
|             raise exc.HTTPBadRequest(msg) | ||||
|  | ||||
|  | ||||
| def _core_resource_attributes(): | ||||
|     resources = {} | ||||
|     for core_def in [network.RESOURCE_ATTRIBUTE_MAP, | ||||
|                      port.RESOURCE_ATTRIBUTE_MAP, | ||||
|                      subnet.RESOURCE_ATTRIBUTE_MAP, | ||||
|                      subnetpool.RESOURCE_ATTRIBUTE_MAP]: | ||||
|         resources.update(core_def) | ||||
|     return resources | ||||
|  | ||||
|  | ||||
| # populate core resources into singleton global | ||||
| RESOURCES = _core_resource_attributes() | ||||
|   | ||||
| @@ -123,19 +123,6 @@ KNOWN_EXTENSIONS = ( | ||||
|     'bgpvpn',  # https://git.openstack.org/cgit/openstack/networking-bgpvpn | ||||
| ) | ||||
|  | ||||
| # The following is a short reference for understanding attribute info: | ||||
| # allow_post: the attribute can be used on POST requests. | ||||
| # allow_put: the attribute can be used on PUT requests. | ||||
| # convert_to: transformation to apply to the value before it is returned | ||||
| # default: default value of the attribute (if missing, the attribute | ||||
| # becomes mandatory. | ||||
| # enforce_policy: the attribute is actively part of the policy enforcing | ||||
| # mechanism, ie: there might be rules which refer to this attribute. | ||||
| # is_visible: the attribute is returned in GET responses. | ||||
| # required_by_policy: the attribute is required by the policy engine and | ||||
| # should therefore be filled by the API layer even if not present in | ||||
| # request body. | ||||
| # validate: specifies rules for validating data in the attribute. | ||||
| KNOWN_KEYWORDS = ( | ||||
|     'allow_post', | ||||
|     'allow_put', | ||||
|   | ||||
| @@ -13,6 +13,7 @@ | ||||
| import copy | ||||
| import fixtures | ||||
|  | ||||
| from neutron_lib.api import attributes | ||||
| from neutron_lib.api import definitions | ||||
| from neutron_lib.callbacks import manager | ||||
| from neutron_lib.callbacks import registry | ||||
| @@ -98,11 +99,29 @@ class APIDefinitionFixture(fixtures.Fixture): | ||||
|     This fixture saves and restores 1 or more neutron-lib API definitions | ||||
|     attribute maps. It should be used anywhere multiple tests can be run | ||||
|     that might update an extension attribute map. | ||||
|  | ||||
|     In addition the fixture backs up and restores the global attribute | ||||
|     RESOURCES base on the boolean value of its backup_global_resources | ||||
|     attribute. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, *api_definitions): | ||||
|         self.definitions = api_definitions | ||||
|         """Create a new instance. | ||||
|  | ||||
|         Consumers can also control if the fixture should handle the global | ||||
|         attribute RESOURCE map using the backup_global_resources of the | ||||
|         fixture instance. If True the fixture will also handle | ||||
|         neutron_lib.api.attributes.RESOURCES. | ||||
|  | ||||
|         :param api_definitions: Zero or more API definitions the fixture | ||||
|         should handle. If no api_definitions are passed, the default is | ||||
|         to handle all neutron_lib API definitions as well as the global | ||||
|         RESOURCES attribute map. | ||||
|         """ | ||||
|         self.definitions = api_definitions or definitions._ALL_API_DEFINITIONS | ||||
|         self._orig_attr_maps = {} | ||||
|         self._orig_resources = {} | ||||
|         self.backup_global_resources = True | ||||
|  | ||||
|     def _setUp(self): | ||||
|         for api_def in self.definitions: | ||||
| @@ -110,12 +129,17 @@ class APIDefinitionFixture(fixtures.Fixture): | ||||
|                 api_def, api_def.RESOURCE_ATTRIBUTE_MAP) | ||||
|             api_def.RESOURCE_ATTRIBUTE_MAP = copy.deepcopy( | ||||
|                 api_def.RESOURCE_ATTRIBUTE_MAP) | ||||
|         if self.backup_global_resources: | ||||
|             for resource, attrs in attributes.RESOURCES.items(): | ||||
|                 self._orig_resources[resource] = copy.deepcopy(attrs) | ||||
|         self.addCleanup(self._restore) | ||||
|  | ||||
|     def _restore(self): | ||||
|         for alias, def_and_map in self._orig_attr_maps.items(): | ||||
|             api_def, attr_map = def_and_map[0], def_and_map[1] | ||||
|             api_def.RESOURCE_ATTRIBUTE_MAP = attr_map | ||||
|         if self.backup_global_resources: | ||||
|             attributes.RESOURCES = self._orig_resources | ||||
|  | ||||
|     @classmethod | ||||
|     def all_api_definitions_fixture(cls): | ||||
|   | ||||
| @@ -10,15 +10,24 @@ | ||||
| #    License for the specific language governing permissions and limitations | ||||
| #    under the License. | ||||
|  | ||||
| from oslo_utils import uuidutils | ||||
| import testtools | ||||
| from webob import exc | ||||
|  | ||||
| from neutron_lib.api import attributes | ||||
| from oslo_utils import uuidutils | ||||
| from neutron_lib.api import converters | ||||
| from neutron_lib.api.definitions import network | ||||
| from neutron_lib.api.definitions import port | ||||
| from neutron_lib.api.definitions import subnet | ||||
| from neutron_lib.api.definitions import subnetpool | ||||
| from neutron_lib import constants | ||||
| from neutron_lib import context | ||||
| from neutron_lib import exceptions | ||||
|  | ||||
| from neutron_lib.tests import _base as base | ||||
|  | ||||
|  | ||||
| class TestApiUtils(base.BaseTestCase): | ||||
| class TestPopulateProjectInfo(base.BaseTestCase): | ||||
|  | ||||
|     def test_populate_project_info_add_project(self): | ||||
|         attrs_in = {'tenant_id': uuidutils.generate_uuid()} | ||||
| @@ -47,3 +56,216 @@ class TestApiUtils(base.BaseTestCase): | ||||
|         } | ||||
|         self.assertRaises(exc.HTTPBadRequest, | ||||
|                           attributes.populate_project_info, attrs) | ||||
|  | ||||
|  | ||||
| class TestAttributeInfo(base.BaseTestCase): | ||||
|  | ||||
|     class _MyException(Exception): | ||||
|         pass | ||||
|  | ||||
|     _EXC_CLS = _MyException | ||||
|     _RESOURCE_NAME = 'thing' | ||||
|     _RESOURCE_ATTRS = {'name': {}, 'type': {}} | ||||
|     _RESOURCE_MAP = {_RESOURCE_NAME: _RESOURCE_ATTRS} | ||||
|     _ATTRS_INSTANCE = attributes.AttributeInfo(_RESOURCE_MAP) | ||||
|  | ||||
|     def test_create_from_attribute_info_instance(self): | ||||
|         cloned_attrs = attributes.AttributeInfo( | ||||
|             TestAttributeInfo._ATTRS_INSTANCE) | ||||
|  | ||||
|         self.assertEqual(TestAttributeInfo._ATTRS_INSTANCE.attributes, | ||||
|                          cloned_attrs.attributes) | ||||
|  | ||||
|     def test_create_from_api_def(self): | ||||
|         self.assertEqual( | ||||
|             port.RESOURCE_ATTRIBUTE_MAP, | ||||
|             attributes.AttributeInfo(port.RESOURCE_ATTRIBUTE_MAP).attributes) | ||||
|  | ||||
|     def _test_fill_default_value(self, attr_inst, expected, res_dict, | ||||
|                                  check_allow_post=True): | ||||
|         attr_inst.fill_post_defaults( | ||||
|             res_dict, check_allow_post=check_allow_post) | ||||
|         self.assertEqual(expected, res_dict) | ||||
|  | ||||
|     def test_fill_default_value_ok(self): | ||||
|         attr_info = { | ||||
|             'key': { | ||||
|                 'allow_post': True, | ||||
|                 'default': constants.ATTR_NOT_SPECIFIED, | ||||
|             }, | ||||
|         } | ||||
|         attr_inst = attributes.AttributeInfo(attr_info) | ||||
|         self._test_fill_default_value(attr_inst, {'key': 'X'}, {'key': 'X'}) | ||||
|         self._test_fill_default_value( | ||||
|             attr_inst, {'key': constants.ATTR_NOT_SPECIFIED}, {}) | ||||
|  | ||||
|     def test_override_no_allow_post(self): | ||||
|         attr_info = { | ||||
|             'key': { | ||||
|                 'allow_post': False, | ||||
|                 'default': constants.ATTR_NOT_SPECIFIED, | ||||
|             }, | ||||
|         } | ||||
|         attr_inst = attributes.AttributeInfo(attr_info) | ||||
|         self._test_fill_default_value(attr_inst, {'key': 'X'}, {'key': 'X'}, | ||||
|                                       check_allow_post=False) | ||||
|  | ||||
|     def test_fill_no_default_value_allow_post(self): | ||||
|         attr_info = { | ||||
|             'key': { | ||||
|                 'allow_post': True, | ||||
|             }, | ||||
|         } | ||||
|         attr_inst = attributes.AttributeInfo(attr_info) | ||||
|         self._test_fill_default_value(attr_inst, {'key': 'X'}, {'key': 'X'}) | ||||
|         self.assertRaises(exceptions.InvalidInput, | ||||
|                           self._test_fill_default_value, | ||||
|                           attr_inst, {'key': 'X'}, {}) | ||||
|         self.assertRaises(self._EXC_CLS, attr_inst.fill_post_defaults, | ||||
|                           {}, self._EXC_CLS) | ||||
|  | ||||
|     def test_fill_no_default_value_no_allow_post(self): | ||||
|         attr_info = { | ||||
|             'key': { | ||||
|                 'allow_post': False, | ||||
|             }, | ||||
|         } | ||||
|         attr_inst = attributes.AttributeInfo(attr_info) | ||||
|         self.assertRaises(exceptions.InvalidInput, | ||||
|                           self._test_fill_default_value, | ||||
|                           attr_inst, {'key': 'X'}, {'key': 'X'}) | ||||
|         self._test_fill_default_value(attr_inst, {}, {}) | ||||
|         self.assertRaises(self._EXC_CLS, attr_inst.fill_post_defaults, | ||||
|                           {'key': 'X'}, self._EXC_CLS) | ||||
|  | ||||
|     def _test_convert_value(self, attr_inst, expected, res_dict): | ||||
|         attr_inst.convert_values(res_dict) | ||||
|         self.assertEqual(expected, res_dict) | ||||
|  | ||||
|     def test_convert_value(self): | ||||
|         attr_info = { | ||||
|             'key': { | ||||
|             }, | ||||
|         } | ||||
|         attr_inst = attributes.AttributeInfo(attr_info) | ||||
|         self._test_convert_value(attr_inst, | ||||
|                                  {'key': constants.ATTR_NOT_SPECIFIED}, | ||||
|                                  {'key': constants.ATTR_NOT_SPECIFIED}) | ||||
|         self._test_convert_value(attr_inst, {'key': 'X'}, {'key': 'X'}) | ||||
|         self._test_convert_value(attr_inst, | ||||
|                                  {'other_key': 'X'}, {'other_key': 'X'}) | ||||
|  | ||||
|         attr_info = { | ||||
|             'key': { | ||||
|                 'convert_to': converters.convert_to_int, | ||||
|             }, | ||||
|         } | ||||
|         attr_inst = attributes.AttributeInfo(attr_info) | ||||
|         self._test_convert_value(attr_inst, | ||||
|                                  {'key': constants.ATTR_NOT_SPECIFIED}, | ||||
|                                  {'key': constants.ATTR_NOT_SPECIFIED}) | ||||
|         self._test_convert_value(attr_inst, {'key': 1}, {'key': '1'}) | ||||
|         self._test_convert_value(attr_inst, {'key': 1}, {'key': 1}) | ||||
|         self.assertRaises(exceptions.InvalidInput, self._test_convert_value, | ||||
|                           attr_inst, {'key': 1}, {'key': 'a'}) | ||||
|  | ||||
|         attr_info = { | ||||
|             'key': { | ||||
|                 'validate': {'type:uuid': None}, | ||||
|             }, | ||||
|         } | ||||
|         attr_inst = attributes.AttributeInfo(attr_info) | ||||
|         self._test_convert_value(attr_inst, | ||||
|                                  {'key': constants.ATTR_NOT_SPECIFIED}, | ||||
|                                  {'key': constants.ATTR_NOT_SPECIFIED}) | ||||
|         uuid_str = '01234567-1234-1234-1234-1234567890ab' | ||||
|         self._test_convert_value(attr_inst, | ||||
|                                  {'key': uuid_str}, {'key': uuid_str}) | ||||
|         self.assertRaises(exceptions.InvalidInput, self._test_convert_value, | ||||
|                           attr_inst, {'key': 1}, {'key': 1}) | ||||
|         self.assertRaises(self._EXC_CLS, attr_inst.convert_values, | ||||
|                           {'key': 1}, self._EXC_CLS) | ||||
|  | ||||
|     def test_populate_project_id_admin_req(self): | ||||
|         tenant_id_1 = uuidutils.generate_uuid() | ||||
|         tenant_id_2 = uuidutils.generate_uuid() | ||||
|         # non-admin users can't create a res on behalf of another project | ||||
|         ctx = context.Context(user_id=None, tenant_id=tenant_id_1) | ||||
|         res_dict = {'tenant_id': tenant_id_2} | ||||
|         attr_inst = attributes.AttributeInfo({}) | ||||
|         self.assertRaises(exc.HTTPBadRequest, | ||||
|                           attr_inst.populate_project_id, | ||||
|                           ctx, res_dict, None) | ||||
|         # but admin users can | ||||
|         ctx.is_admin = True | ||||
|         attr_inst.populate_project_id(ctx, res_dict, is_create=False) | ||||
|  | ||||
|     def test_populate_project_id_from_context(self): | ||||
|         tenant_id = uuidutils.generate_uuid() | ||||
|         ctx = context.Context(user_id=None, tenant_id=tenant_id) | ||||
|         # for each create request, the tenant_id should be added to the | ||||
|         # req body | ||||
|         res_dict = {} | ||||
|         attr_inst = attributes.AttributeInfo({}) | ||||
|         attr_inst.populate_project_id(ctx, res_dict, is_create=True) | ||||
|         self.assertEqual( | ||||
|             {'tenant_id': ctx.tenant_id, 'project_id': ctx.tenant_id}, | ||||
|             res_dict) | ||||
|  | ||||
|     def test_populate_project_id_mandatory_not_specified(self): | ||||
|         tenant_id = uuidutils.generate_uuid() | ||||
|         ctx = context.Context(user_id=None, tenant_id=tenant_id) | ||||
|         # if the tenant_id is mandatory for the resource and not specified | ||||
|         # in the request nor in the context, an exception should be raised | ||||
|         res_dict = {} | ||||
|         attr_info = {'tenant_id': {'allow_post': True}} | ||||
|         ctx.tenant_id = None | ||||
|         attr_inst = attributes.AttributeInfo(attr_info) | ||||
|         self.assertRaises(exc.HTTPBadRequest, | ||||
|                           attr_inst.populate_project_id, | ||||
|                           ctx, res_dict, True) | ||||
|  | ||||
|     def test_populate_project_id_not_mandatory(self): | ||||
|         ctx = context.Context(user_id=None) | ||||
|         # if the tenant_id is not mandatory for the resource it should be | ||||
|         # OK if it is not in the request. | ||||
|         res_dict = {'name': 'test_port'} | ||||
|         attr_inst = attributes.AttributeInfo({}) | ||||
|         ctx.tenant_id = None | ||||
|         attr_inst.populate_project_id(ctx, res_dict, True) | ||||
|  | ||||
|     def test_verify_attributes_null(self): | ||||
|         attributes.AttributeInfo({}).verify_attributes({}) | ||||
|  | ||||
|     def test_verify_attributes_ok_with_project_id(self): | ||||
|         attributes.AttributeInfo( | ||||
|             {'tenant_id': 'foo', 'project_id': 'foo'}).verify_attributes( | ||||
|             {'tenant_id': 'foo'}) | ||||
|  | ||||
|     def test_verify_attributes_ok_subset(self): | ||||
|         attributes.AttributeInfo( | ||||
|             {'attr1': 'foo', 'attr2': 'bar'}).verify_attributes( | ||||
|             {'attr1': 'foo'}) | ||||
|  | ||||
|     def test_verify_attributes_unrecognized(self): | ||||
|         with testtools.ExpectedException(exc.HTTPBadRequest) as bad_req: | ||||
|             attributes.AttributeInfo( | ||||
|                 {'attr1': 'foo'}).verify_attributes( | ||||
|                 {'attr1': 'foo', 'attr2': 'bar'}) | ||||
|             self.assertEqual(bad_req.message, | ||||
|                              "Unrecognized attribute(s) 'attr2'") | ||||
|  | ||||
|  | ||||
| class TestCoreResources(base.BaseTestCase): | ||||
|  | ||||
|     CORE_DEFS = [network, port, subnet, subnetpool] | ||||
|  | ||||
|     def test_core_resource_names(self): | ||||
|         self.assertEqual( | ||||
|             sorted([r.COLLECTION_NAME for r in TestCoreResources.CORE_DEFS]), | ||||
|             sorted(attributes.RESOURCES.keys())) | ||||
|  | ||||
|     def test_core_resource_attrs(self): | ||||
|         for r in TestCoreResources.CORE_DEFS: | ||||
|             self.assertIs(r.RESOURCE_ATTRIBUTE_MAP[r.COLLECTION_NAME], | ||||
|                           attributes.RESOURCES[r.COLLECTION_NAME]) | ||||
|   | ||||
| @@ -16,10 +16,12 @@ from oslo_config import cfg | ||||
| from oslo_db import options | ||||
| from oslotest import base | ||||
|  | ||||
| from neutron_lib.api import attributes | ||||
| from neutron_lib.callbacks import registry | ||||
| from neutron_lib.db import model_base | ||||
| from neutron_lib import fixture | ||||
| from neutron_lib.plugins import directory | ||||
| from neutron_lib.tests.unit.api import test_attributes | ||||
|  | ||||
|  | ||||
| class PluginDirectoryFixtureTestCase(base.BaseTestCase): | ||||
| @@ -63,27 +65,30 @@ class SqlFixtureTestCase(base.BaseTestCase): | ||||
|  | ||||
| class APIDefinitionFixtureTestCase(base.BaseTestCase): | ||||
|  | ||||
|     _ATTR_MAP_1 = {'routers': {'name': 'a'}} | ||||
|     _ATTR_MAP_2 = {'ports': {'description': 'a'}} | ||||
|     def _test_all_api_definitions_fixture(self, global_cleanup=True): | ||||
|         apis = fixture.APIDefinitionFixture.all_api_definitions_fixture() | ||||
|         apis.backup_global_resources = global_cleanup | ||||
|         apis.setUp() | ||||
|  | ||||
|     def setUp(self): | ||||
|         super(APIDefinitionFixtureTestCase, self).setUp() | ||||
|         self.routers_def = mock.Mock() | ||||
|         self.routers_def.RESOURCE_ATTRIBUTE_MAP = ( | ||||
|             APIDefinitionFixtureTestCase._ATTR_MAP_1) | ||||
|         self.ports_def = mock.Mock() | ||||
|         self.ports_def.RESOURCE_ATTRIBUTE_MAP = ( | ||||
|             APIDefinitionFixtureTestCase._ATTR_MAP_2) | ||||
|         self.useFixture(fixture.APIDefinitionFixture( | ||||
|             self.routers_def, self.ports_def)) | ||||
|         asserteq = self.assertNotEqual if global_cleanup else self.assertEqual | ||||
|         asserteq({}, apis._orig_resources) | ||||
|  | ||||
|     def test_fixture(self): | ||||
|         # assert same contents, but different instances | ||||
|         self.assertEqual(APIDefinitionFixtureTestCase._ATTR_MAP_1, | ||||
|                          self.routers_def.RESOURCE_ATTRIBUTE_MAP) | ||||
|         self.assertEqual(APIDefinitionFixtureTestCase._ATTR_MAP_2, | ||||
|                          self.ports_def.RESOURCE_ATTRIBUTE_MAP) | ||||
|         self.assertIsNot(APIDefinitionFixtureTestCase._ATTR_MAP_1, | ||||
|                          self.routers_def.RESOURCE_ATTRIBUTE_MAP) | ||||
|         self.assertIsNot(APIDefinitionFixtureTestCase._ATTR_MAP_2, | ||||
|                          self.ports_def.RESOURCE_ATTRIBUTE_MAP) | ||||
|         for r in test_attributes.TestCoreResources.CORE_DEFS: | ||||
|             attributes.RESOURCES[r.COLLECTION_NAME]['_test_'] = {} | ||||
|             r.RESOURCE_ATTRIBUTE_MAP['_test_'] = {} | ||||
|  | ||||
|         apis.cleanUp() | ||||
|         for r in test_attributes.TestCoreResources.CORE_DEFS: | ||||
|             self.assertNotIn('_test_', r.RESOURCE_ATTRIBUTE_MAP) | ||||
|             global_assert = (self.assertNotIn | ||||
|                              if global_cleanup else self.assertIn) | ||||
|             global_assert('_test_', attributes.RESOURCES[r.COLLECTION_NAME]) | ||||
|             # cleanup | ||||
|             if not global_cleanup: | ||||
|                 del attributes.RESOURCES[r.COLLECTION_NAME]['_test_'] | ||||
|  | ||||
|     def test_all_api_definitions_fixture_no_global_backup(self): | ||||
|         self._test_all_api_definitions_fixture(global_cleanup=False) | ||||
|  | ||||
|     def test_all_api_definitions_fixture_with_global_backup(self): | ||||
|         self._test_all_api_definitions_fixture(global_cleanup=True) | ||||
|   | ||||
							
								
								
									
										14
									
								
								releasenotes/notes/core-attributes-43e6969f1b187e5c.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								releasenotes/notes/core-attributes-43e6969f1b187e5c.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| --- | ||||
| features: | ||||
|   - A bulk of the ``neutron.api.v2.attributes`` functionality is now available | ||||
|     in ``neutron_lib.api.attributes``. A new ``AttributeInfo`` class is | ||||
|     available in that acts as a wrapper for an API resource's attribute dict | ||||
|     and allows consumers to perform operations with the underlying attribute | ||||
|     dict. The ``populate_project_info`` function is now available. The global | ||||
|     attribute map ``RESOURCES`` is now available and will take the place of | ||||
|     neutron's global ``RESOURCE_ATTRIBUTE_MAP``. | ||||
|   - The ``neutron_lib.fixture.APIDefinitionFixture`` has been updated to handle | ||||
|     backing-up and restoring the global ``RESOURCES`` dict. By default the | ||||
|     constructor now also uses all API definitions if none are passed to it's | ||||
|     constructor. This is the default behavior almost all consumers need and is | ||||
|     thus a convenience change. | ||||
		Reference in New Issue
	
	Block a user
	 Boden R
					Boden R