diff --git a/doc/source/devref/api_extensions.rst b/doc/source/devref/api_extensions.rst
new file mode 100644
index 000000000..95fe1a4bb
--- /dev/null
+++ b/doc/source/devref/api_extensions.rst
@@ -0,0 +1,77 @@
+..
+ 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 Extensions
+==============
+
+API extensions provide a standardized way of introducing new API functionality.
+While the ``neutron-lib`` project itself does not serve an API, the ``neutron``
+project does and leverages the API extension framework from ``neutron-lib``.
+
+API extensions consist of the following high-level constructs:
+
+- API definitions that specify the extension's static metadata. This metadata
+ includes basic details about the extension such as its name, description,
+ alias, etc. as well as its extended resources/sub-resources and
+ required/optional extensions. These definitions live in the
+ ``neutron_lib.api.definitions`` package.
+- API reference documenting the APIs/resources added/modified by the extension.
+ This documentation is in ``rst`` format and is used to generate the
+ `openstack API reference `_. The API reference lives under the ``api-ref/source/v2``
+ directory of the ``neutron-lib`` project repository.
+- An extension descriptor class that must be defined in an extension directory
+ for ``neutron`` or other sub-project that supports extensions. This concrete
+ class provides the extension's metadata to the API server. These extension
+ classes reside outside of ``neutron-lib``, but leverage the base classes
+ from ``neutron_lib.api.extensions``. For more details see the section below
+ on using neutron-lib's extension classes.
+- The API extension plugin implementation itself. This is the code that
+ implements the extension's behavior and should carry out the operations
+ defined by the extension. This code resides under its respective project
+ repository, not in ``neutron-lib``. For more details see the `neutron api
+ extension dev-ref `_.
+
+
+Using neutron-lib's base extension classes
+------------------------------------------
+
+The ``neutron_lib.api.extensions`` module provides a set of base extension
+descriptor classes consumers can use to define their extension descriptor(s).
+For those extensions that have an API definition in
+``neutron_lib.api.definitions``, the ``APIExtensionDescriptor`` class can
+be used. For example::
+
+ from neutron_lib.api.definitions import provider_net
+ from neutron_lib.api import extensions
+
+
+ class Providernet(extensions.APIExtensionDescriptor):
+ api_definition = provider_net
+ # nothing else needed if default behavior is acceptable
+
+
+For extensions that do not yet have a definition in
+``neutron_lib.api.definitions``, they can continue to use the
+``ExtensionDescriptor`` as has been done historically.
diff --git a/doc/source/devref/index.rst b/doc/source/devref/index.rst
index a798afd70..ddbdcaf99 100644
--- a/doc/source/devref/index.rst
+++ b/doc/source/devref/index.rst
@@ -32,6 +32,7 @@ Neutron Lib Internals
.. toctree::
:maxdepth: 3
+ api_extensions
api_converters
api_validators
callbacks
diff --git a/neutron_lib/api/extensions.py b/neutron_lib/api/extensions.py
index 834467406..ccd06762f 100644
--- a/neutron_lib/api/extensions.py
+++ b/neutron_lib/api/extensions.py
@@ -16,6 +16,12 @@
import abc
import six
+from neutron_lib._i18n import _
+from neutron_lib import constants
+
+
+_UNSET = constants.Sentinel()
+
def is_extension_supported(plugin, alias):
"""Validate that the extension is supported.
@@ -92,7 +98,6 @@ class ExtensionDescriptor(object):
map[][][]
specifying the extended resource attribute properties required
by that API version.
-
Extension can add resources and their attr definitions too.
The returned map can be integrated into RESOURCE_ATTRIBUTE_MAP.
"""
@@ -128,7 +133,6 @@ class ExtensionDescriptor(object):
An extension can use this method and supplying its own resource
attribute map in extension_attrs_map argument to extend all its
attributes that needs to be extended.
-
If an extension does not implement update_attributes_map, the method
does nothing and just return.
"""
@@ -147,8 +151,83 @@ class ExtensionDescriptor(object):
The controllers associated with each instance of
extensions.ResourceExtension should be a subclass of
neutron.pecan_wsgi.controllers.utils.NeutronPecanController.
-
If a resource is defined in both get_resources and get_pecan_resources,
the resource defined in get_pecan_resources will take precedence.
"""
return []
+
+
+class APIExtensionDescriptor(ExtensionDescriptor):
+ """Base class that defines the contract for extensions.
+
+ Concrete implementations of API extensions should first provide
+ an API definition in neutron_lib.api.definitions. The API
+ definition module (object reference) can then be specified as a
+ class level attribute on the concrete extension.
+
+ For example::
+
+ from neutron_lib.api.definitions import provider_net
+ from neutron_lib.api import extensions
+
+
+ class Providernet(extensions.APIExtensionDescriptor):
+ api_definition = provider_net
+ # nothing else needed if default behavior is acceptable
+
+
+ If extension implementations need to override the default behavior of
+ this class they can override the respective method directly.
+ """
+ api_definition = _UNSET
+
+ @classmethod
+ def _assert_api_definition(cls, attr=None):
+ if cls.api_definition == _UNSET:
+ raise NotImplementedError(
+ _("Extension module API definition not set."))
+ if attr and getattr(cls.api_definition, attr, _UNSET) == _UNSET:
+ raise NotImplementedError(_("Extension module API definition "
+ "does not define '%s'") % attr)
+
+ @classmethod
+ def get_name(cls):
+ """The name of the API definition."""
+ cls._assert_api_definition('NAME')
+ return cls.api_definition.NAME
+
+ @classmethod
+ def get_alias(cls):
+ """The alias for the API definition."""
+ cls._assert_api_definition('ALIAS')
+ return cls.api_definition.ALIAS
+
+ @classmethod
+ def get_description(cls):
+ """Friendly description for the API definition."""
+ cls._assert_api_definition('DESCRIPTION')
+ return cls.api_definition.DESCRIPTION
+
+ @classmethod
+ def get_updated(cls):
+ """The timestamp when the API definition was last updated."""
+ cls._assert_api_definition('UPDATED_TIMESTAMP')
+ return cls.api_definition.UPDATED_TIMESTAMP
+
+ def get_extended_resources(self, version):
+ """Retrieve the resource attribute map for the API definition."""
+ if version == "2.0":
+ self._assert_api_definition('RESOURCE_ATTRIBUTE_MAP')
+ return self.api_definition.RESOURCE_ATTRIBUTE_MAP
+ else:
+ return {}
+
+ def get_required_extensions(self):
+ """Returns the API definition's required extensions."""
+ self._assert_api_definition('REQUIRED_EXTENSIONS')
+ return self.api_definition.REQUIRED_EXTENSIONS
+
+ def get_optional_extensions(self):
+ """Returns the API definition's optional extensions."""
+ self._assert_api_definition('OPTIONAL_EXTENSIONS')
+ return self.api_definition.OPTIONAL_EXTENSIONS
diff --git a/neutron_lib/tests/unit/api/test_extensions.py b/neutron_lib/tests/unit/api/test_extensions.py
index 5153b68a8..7b3f9f3e5 100644
--- a/neutron_lib/tests/unit/api/test_extensions.py
+++ b/neutron_lib/tests/unit/api/test_extensions.py
@@ -108,3 +108,95 @@ class TestExtensionIsSupported(base.BaseTestCase):
def test_extension_does_not_exist(self):
self.assertFalse(extensions.is_extension_supported(self._plugin,
"gordon"))
+
+
+class TestAPIExtensionDescriptor(base.BaseTestCase):
+
+ # API definition attributes; acts as an API definition module
+ NAME = 'Test API'
+ ALIAS = 'test-api'
+ DESCRIPTION = 'A test API definition'
+ UPDATED_TIMESTAMP = '2017-02-01T10:00:00-00:00'
+ RESOURCE_ATTRIBUTE_MAP = {'ports': {}}
+ REQUIRED_EXTENSIONS = ['l3']
+ OPTIONAL_EXTENSIONS = ['fw']
+
+ def setUp(self):
+ super(TestAPIExtensionDescriptor, self).setUp()
+ self.extn = _APIDefinition()
+ self.empty_extn = _EmptyAPIDefinition()
+
+ def test__assert_api_definition_no_defn(self):
+ self.assertRaises(NotImplementedError,
+ _NoAPIDefinition._assert_api_definition)
+
+ def test__assert_api_definition_no_attr(self):
+ self.assertRaises(
+ NotImplementedError, self.extn._assert_api_definition, attr='NOPE')
+
+ def test_get_name(self):
+ self.assertEqual(self.NAME, self.extn.get_name())
+
+ def test_get_name_unset(self):
+ self.assertRaises(NotImplementedError, _EmptyAPIDefinition.get_name)
+
+ def test_get_alias(self):
+ self.assertEqual(self.ALIAS, self.extn.get_alias())
+
+ def test_get_alias_unset(self):
+ self.assertRaises(NotImplementedError, _EmptyAPIDefinition.get_alias)
+
+ def test_get_description(self):
+ self.assertEqual(self.DESCRIPTION, self.extn.get_description())
+
+ def test_get_description_unset(self):
+ self.assertRaises(NotImplementedError,
+ _EmptyAPIDefinition.get_description)
+
+ def test_get_updated(self):
+ self.assertEqual(self.UPDATED_TIMESTAMP, self.extn.get_updated())
+
+ def test_get_updated_unset(self):
+ self.assertRaises(NotImplementedError, _EmptyAPIDefinition.get_updated)
+
+ def test_get_extended_resources_v2(self):
+ self.assertEqual(self.RESOURCE_ATTRIBUTE_MAP,
+ self.extn.get_extended_resources('2.0'))
+
+ def test_get_extended_resources_v2_unset(self):
+ self.assertRaises(NotImplementedError,
+ self.empty_extn.get_extended_resources, '2.0')
+
+ def test_get_extended_resources_v1(self):
+ self.assertEqual({}, self.extn.get_extended_resources('1.0'))
+
+ def test_get_extended_resources_v1_unset(self):
+ self.assertEqual({}, self.empty_extn.get_extended_resources('1.0'))
+
+ def test_get_required_extensions(self):
+ self.assertEqual(self.REQUIRED_EXTENSIONS,
+ self.extn.get_required_extensions())
+
+ def test_get_required_extensions_unset(self):
+ self.assertRaises(NotImplementedError,
+ self.empty_extn.get_required_extensions)
+
+ def test_get_optional_extensions(self):
+ self.assertEqual(self.OPTIONAL_EXTENSIONS,
+ self.extn.get_optional_extensions())
+
+ def test_get_optional_extensions_unset(self):
+ self.assertRaises(NotImplementedError,
+ self.empty_extn.get_optional_extensions)
+
+
+class _APIDefinition(extensions.APIExtensionDescriptor):
+ api_definition = TestAPIExtensionDescriptor
+
+
+class _NoAPIDefinition(extensions.APIExtensionDescriptor):
+ pass
+
+
+class _EmptyAPIDefinition(extensions.APIExtensionDescriptor):
+ api_definition = {}
diff --git a/releasenotes/notes/boilerplate-ext-descriptor-a5cec8b9b900cbfd.yaml b/releasenotes/notes/boilerplate-ext-descriptor-a5cec8b9b900cbfd.yaml
new file mode 100644
index 000000000..2000c9ff2
--- /dev/null
+++ b/releasenotes/notes/boilerplate-ext-descriptor-a5cec8b9b900cbfd.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - The ``APIExtensionDescriptor`` was added to ``neutron_lib.api.extensions``
+ and can be used with extensions that have an API definition in neutron-lib
+ to minimize the boilplate code needed in the extension definition class.
+ For more details, see the dev-ref.