API extensions framework for v3 API
This is the initial patch for the new extension framework to be used by the Nova v3 API. It will only be used by v3 API extensions and not v2 API extensions. v3 API extensions will only use this plugin framework and will not be compatible with the old one. - Sets up a /v3 url prefix - Looks in an entry point namespace of nova.api.extensions - The fixed_ips extensions is ported to /v3 as an example of a resource extensions. Required changes are very minor. - All extensions must derive from the V3APIExtensionBase class - Drops updated field from extensions, replaced with version field - Ports tests for fixed_ips extension - None of the core has been ported in this patch, though the example extension works without it. The intent is to port the core code over as plugins as well. There will still be a conceptual core however I don't think we need a separate directory for core. This is the first of a series of patches to add support for the new extension framework. Future direction including support for controller extensions, removal of extension code in core code etc can be seen here: https://github.com/cyeoh/nova/tree/v3_api_extension_framework Partially implements blueprint v3-api-extension-framework Change-Id: I88aa6353ad1d74cac51abbb6aac7274b1567485a
This commit is contained in:
@@ -61,6 +61,7 @@ use = call:nova.api.openstack.urlmap:urlmap_factory
|
||||
/: oscomputeversions
|
||||
/v1.1: openstack_compute_api_v2
|
||||
/v2: openstack_compute_api_v2
|
||||
/v3: openstack_compute_api_v3
|
||||
|
||||
[composite:openstack_compute_api_v2]
|
||||
use = call:nova.api.auth:pipeline_factory
|
||||
@@ -68,6 +69,12 @@ noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v2
|
||||
keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2
|
||||
keystone_nolimit = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2
|
||||
|
||||
[composite:openstack_compute_api_v3]
|
||||
use = call:nova.api.auth:pipeline_factory
|
||||
noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v3
|
||||
keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v3
|
||||
keystone_nolimit = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v3
|
||||
|
||||
[filter:faultwrap]
|
||||
paste.filter_factory = nova.api.openstack:FaultWrapper.factory
|
||||
|
||||
@@ -83,6 +90,9 @@ paste.filter_factory = nova.api.sizelimit:RequestBodySizeLimiter.factory
|
||||
[app:osapi_compute_app_v2]
|
||||
paste.app_factory = nova.api.openstack.compute:APIRouter.factory
|
||||
|
||||
[app:osapi_compute_app_v3]
|
||||
paste.app_factory = nova.api.openstack.compute:APIRouterV3.factory
|
||||
|
||||
[pipeline:oscomputeversions]
|
||||
pipeline = faultwrap oscomputeversionapp
|
||||
|
||||
|
@@ -21,9 +21,11 @@ WSGI middleware for OpenStack API controllers.
|
||||
"""
|
||||
|
||||
import routes
|
||||
import stevedore
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import wsgi
|
||||
from nova import notifications
|
||||
from nova.openstack.common import log as logging
|
||||
@@ -191,3 +193,111 @@ class APIRouter(base_wsgi.Router):
|
||||
|
||||
def _setup_routes(self, mapper, ext_mgr, init_only):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class APIRouterV3(base_wsgi.Router):
|
||||
"""
|
||||
Routes requests on the OpenStack v3 API to the appropriate controller
|
||||
and method.
|
||||
"""
|
||||
|
||||
API_EXTENSION_NAMESPACE = 'nova.api.v3.extensions'
|
||||
|
||||
@classmethod
|
||||
def factory(cls, global_config, **local_config):
|
||||
"""Simple paste factory, :class:`nova.wsgi.Router` doesn't have one."""
|
||||
return cls()
|
||||
|
||||
def __init__(self):
|
||||
# TODO(cyeoh): bp v3-api-extension-framework. Currently load
|
||||
# all extensions but eventually should be able to exclude
|
||||
# based on a config file
|
||||
def _check_load_extension(ext):
|
||||
return isinstance(ext.obj, extensions.V3APIExtensionBase)
|
||||
|
||||
self.api_extension_manager = stevedore.enabled.EnabledExtensionManager(
|
||||
namespace=self.API_EXTENSION_NAMESPACE,
|
||||
check_func=_check_load_extension,
|
||||
invoke_on_load=True)
|
||||
|
||||
mapper = ProjectMapper()
|
||||
self.resources = {}
|
||||
|
||||
# NOTE(cyeoh) Core API support is rewritten as extensions
|
||||
# but conceptually still have core
|
||||
if list(self.api_extension_manager):
|
||||
# NOTE(cyeoh): Stevedore raises an exception if there are
|
||||
# no plugins detected. I wonder if this is a bug.
|
||||
self.api_extension_manager.map(self._register_extensions)
|
||||
self.api_extension_manager.map(self._register_resources,
|
||||
mapper=mapper)
|
||||
self.api_extension_manager.map(self._register_controllers)
|
||||
|
||||
super(APIRouterV3, self).__init__(mapper)
|
||||
|
||||
def _register_extensions(self, ext):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _register_resources(self, ext, mapper):
|
||||
"""Register resources defined by the extensions
|
||||
|
||||
Extensions define what resources they want to add through a
|
||||
get_resources function
|
||||
"""
|
||||
|
||||
handler = ext.obj
|
||||
LOG.debug("Running _register_resources on %s", ext.obj)
|
||||
|
||||
for resource in handler.get_resources():
|
||||
LOG.debug(_('Extended resource: %s'), resource.collection)
|
||||
|
||||
inherits = None
|
||||
if resource.inherits:
|
||||
inherits = self.resources.get(resource.inherits)
|
||||
if not resource.controller:
|
||||
resource.controller = inherits.controller
|
||||
wsgi_resource = wsgi.Resource(resource.controller,
|
||||
inherits=inherits)
|
||||
self.resources[resource.collection] = wsgi_resource
|
||||
kargs = dict(
|
||||
controller=wsgi_resource,
|
||||
collection=resource.collection_actions,
|
||||
member=resource.member_actions)
|
||||
|
||||
if resource.parent:
|
||||
kargs['parent_resource'] = resource.parent
|
||||
|
||||
mapper.resource(resource.collection, resource.collection,
|
||||
**kargs)
|
||||
|
||||
if resource.custom_routes_fn:
|
||||
resource.custom_routes_fn(mapper, wsgi_resource)
|
||||
|
||||
def _register_controllers(self, ext):
|
||||
"""Register controllers defined by the extensions
|
||||
|
||||
Extensions define what resources they want to add through
|
||||
a get_controller_extensions function
|
||||
"""
|
||||
|
||||
handler = ext.obj
|
||||
LOG.debug("Running _register_controllers on %s", ext.obj)
|
||||
|
||||
for extension in handler.get_controller_extensions():
|
||||
ext_name = extension.extension.name
|
||||
collection = extension.collection
|
||||
controller = extension.controller
|
||||
|
||||
if collection not in self.resources:
|
||||
LOG.warning(_('Extension %(ext_name)s: Cannot extend '
|
||||
'resource %(collection)s: No such resource'),
|
||||
{'ext_name': ext_name, 'collection': collection})
|
||||
continue
|
||||
|
||||
LOG.debug(_('Extension %(ext_name)s extending resource: '
|
||||
'%(collection)s'),
|
||||
{'ext_name': ext_name, 'collection': collection})
|
||||
|
||||
resource = self.resources[collection]
|
||||
resource.register_actions(controller)
|
||||
resource.register_extensions(controller)
|
||||
|
@@ -128,3 +128,15 @@ class APIRouter(nova.api.openstack.APIRouter):
|
||||
controller=server_metadata_controller,
|
||||
action='update_all',
|
||||
conditions={"method": ['PUT']})
|
||||
|
||||
|
||||
class APIRouterV3(nova.api.openstack.APIRouterV3):
|
||||
"""
|
||||
Routes requests on the OpenStack API to the appropriate controller
|
||||
and method.
|
||||
"""
|
||||
|
||||
def _register_extensions(self, ext):
|
||||
pass
|
||||
# TODO(cyeoh): bp v3-api-extension-framework - Register extension
|
||||
# information
|
||||
|
0
nova/api/openstack/compute/plugins/__init__.py
Normal file
0
nova/api/openstack/compute/plugins/__init__.py
Normal file
0
nova/api/openstack/compute/plugins/v3/__init__.py
Normal file
0
nova/api/openstack/compute/plugins/v3/__init__.py
Normal file
98
nova/api/openstack/compute/plugins/v3/fixed_ips.py
Normal file
98
nova/api/openstack/compute/plugins/v3/fixed_ips.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012, 2013 IBM Corp.
|
||||
#
|
||||
# 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 webob.exc
|
||||
|
||||
from nova.api.openstack import extensions
|
||||
from nova import db
|
||||
from nova import exception
|
||||
from nova.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
authorize = extensions.extension_authorizer('compute', 'fixed_ips')
|
||||
|
||||
|
||||
class FixedIPController(object):
|
||||
def show(self, req, id):
|
||||
"""Return data about the given fixed ip."""
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
|
||||
try:
|
||||
fixed_ip = db.fixed_ip_get_by_address_detailed(context, id)
|
||||
except exception.FixedIpNotFoundForAddress as ex:
|
||||
raise webob.exc.HTTPNotFound(explanation=ex.format_message())
|
||||
|
||||
fixed_ip_info = {"fixed_ip": {}}
|
||||
if not fixed_ip[1]:
|
||||
msg = _("Fixed IP %s has been deleted") % id
|
||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
fixed_ip_info['fixed_ip']['cidr'] = fixed_ip[1]['cidr']
|
||||
fixed_ip_info['fixed_ip']['address'] = fixed_ip[0]['address']
|
||||
|
||||
if fixed_ip[2]:
|
||||
fixed_ip_info['fixed_ip']['hostname'] = fixed_ip[2]['hostname']
|
||||
fixed_ip_info['fixed_ip']['host'] = fixed_ip[2]['host']
|
||||
else:
|
||||
fixed_ip_info['fixed_ip']['hostname'] = None
|
||||
fixed_ip_info['fixed_ip']['host'] = None
|
||||
|
||||
return fixed_ip_info
|
||||
|
||||
def action(self, req, id, body):
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
if 'reserve' in body:
|
||||
LOG.debug(_("Reserving IP address %s") % id)
|
||||
return self._set_reserved(context, id, True)
|
||||
elif 'unreserve' in body:
|
||||
LOG.debug(_("Unreserving IP address %s") % id)
|
||||
return self._set_reserved(context, id, False)
|
||||
else:
|
||||
raise webob.exc.HTTPBadRequest(
|
||||
explanation="No valid action specified")
|
||||
|
||||
def _set_reserved(self, context, address, reserved):
|
||||
try:
|
||||
fixed_ip = db.fixed_ip_get_by_address(context, address)
|
||||
db.fixed_ip_update(context, fixed_ip['address'],
|
||||
{'reserved': reserved})
|
||||
except exception.FixedIpNotFoundForAddress:
|
||||
msg = _("Fixed IP %s not found") % address
|
||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
return webob.exc.HTTPAccepted()
|
||||
|
||||
|
||||
class FixedIPs(extensions.V3APIExtensionBase):
|
||||
"""Fixed IPs support."""
|
||||
|
||||
name = "FixedIPs"
|
||||
alias = "os-fixed-ips"
|
||||
namespace = "http://docs.openstack.org/compute/ext/fixed_ips/api/v3"
|
||||
version = 1
|
||||
|
||||
def get_resources(self):
|
||||
member_actions = {'action': 'POST'}
|
||||
resources = [
|
||||
extensions.ResourceExtension('os-fixed-ips',
|
||||
FixedIPController(),
|
||||
member_actions=member_actions)]
|
||||
return resources
|
||||
|
||||
def get_controller_extensions(self):
|
||||
return []
|
@@ -16,6 +16,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import abc
|
||||
import os
|
||||
|
||||
import webob.dec
|
||||
@@ -396,3 +397,52 @@ def soft_extension_authorizer(api_name, extension_name):
|
||||
except exception.NotAuthorized:
|
||||
return False
|
||||
return authorize
|
||||
|
||||
|
||||
class V3APIExtensionBase(object):
|
||||
"""Abstract base class for all V3 API extensions.
|
||||
|
||||
All V3 API extensions must derive from this class and implement
|
||||
the abstract methods get_resources and get_controller_extensions
|
||||
even if they just return an empty list. The extensions must also
|
||||
define the abstract properties.
|
||||
"""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_resources(self):
|
||||
"""Return a list of resources extensions.
|
||||
|
||||
The extensions should return a list of ResourceExtension
|
||||
objects. This list may be empty.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_controller_extensions(self):
|
||||
"""Return a list of controller extensions.
|
||||
|
||||
The extensions should return a list of ControllerExtension
|
||||
objects. This list may be empty.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractproperty
|
||||
def name(self):
|
||||
"""Name of the extension."""
|
||||
pass
|
||||
|
||||
@abc.abstractproperty
|
||||
def alias(self):
|
||||
"""Alias for the extension."""
|
||||
pass
|
||||
|
||||
@abc.abstractproperty
|
||||
def namespace(self):
|
||||
"""Namespace for the extension."""
|
||||
pass
|
||||
|
||||
@abc.abstractproperty
|
||||
def version(self):
|
||||
"""Version of the extension."""
|
||||
pass
|
||||
|
188
nova/tests/api/openstack/compute/plugins/v3/test_fixed_ips.py
Normal file
188
nova/tests/api/openstack/compute/plugins/v3/test_fixed_ips.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 IBM Corp.
|
||||
#
|
||||
# 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 webob
|
||||
|
||||
from nova.api.openstack.compute.plugins.v3 import fixed_ips
|
||||
from nova import context
|
||||
from nova import db
|
||||
from nova import exception
|
||||
from nova import test
|
||||
from nova.tests.api.openstack import fakes
|
||||
|
||||
|
||||
fake_fixed_ips = [{'id': 1,
|
||||
'address': '192.168.1.1',
|
||||
'network_id': 1,
|
||||
'virtual_interface_id': 1,
|
||||
'instance_uuid': '1',
|
||||
'allocated': False,
|
||||
'leased': False,
|
||||
'reserved': False,
|
||||
'host': None,
|
||||
'deleted': False},
|
||||
{'id': 2,
|
||||
'address': '192.168.1.2',
|
||||
'network_id': 1,
|
||||
'virtual_interface_id': 2,
|
||||
'instance_uuid': '2',
|
||||
'allocated': False,
|
||||
'leased': False,
|
||||
'reserved': False,
|
||||
'host': None,
|
||||
'deleted': False},
|
||||
{'id': 3,
|
||||
'address': '10.0.0.2',
|
||||
'network_id': 1,
|
||||
'virtual_interface_id': 3,
|
||||
'instance_uuid': '3',
|
||||
'allocated': False,
|
||||
'leased': False,
|
||||
'reserved': False,
|
||||
'host': None,
|
||||
'deleted': True},
|
||||
]
|
||||
|
||||
|
||||
def fake_fixed_ip_get_by_address(context, address):
|
||||
for fixed_ip in fake_fixed_ips:
|
||||
if fixed_ip['address'] == address and not fixed_ip['deleted']:
|
||||
return fixed_ip
|
||||
raise exception.FixedIpNotFoundForAddress(address=address)
|
||||
|
||||
|
||||
def fake_fixed_ip_get_by_address_detailed(context, address):
|
||||
network = {'id': 1,
|
||||
'cidr': "192.168.1.0/24"}
|
||||
for fixed_ip in fake_fixed_ips:
|
||||
if fixed_ip['address'] == address and not fixed_ip['deleted']:
|
||||
return (fixed_ip, FakeModel(network), None)
|
||||
raise exception.FixedIpNotFoundForAddress(address=address)
|
||||
|
||||
|
||||
def fake_fixed_ip_update(context, address, values):
|
||||
fixed_ip = fake_fixed_ip_get_by_address(context, address)
|
||||
if fixed_ip is None:
|
||||
raise exception.FixedIpNotFoundForAddress(address=address)
|
||||
else:
|
||||
for key in values:
|
||||
fixed_ip[key] = values[key]
|
||||
|
||||
|
||||
class FakeModel(object):
|
||||
"""Stubs out for model."""
|
||||
def __init__(self, values):
|
||||
self.values = values
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self.values[name]
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self.values:
|
||||
return self.values[key]
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
def __repr__(self):
|
||||
return '<FakeModel: %s>' % self.values
|
||||
|
||||
|
||||
def fake_network_get_all(context):
|
||||
network = {'id': 1,
|
||||
'cidr': "192.168.1.0/24"}
|
||||
return [FakeModel(network)]
|
||||
|
||||
|
||||
class FixedIpTest(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(FixedIpTest, self).setUp()
|
||||
|
||||
self.stubs.Set(db, "fixed_ip_get_by_address",
|
||||
fake_fixed_ip_get_by_address)
|
||||
self.stubs.Set(db, "fixed_ip_get_by_address_detailed",
|
||||
fake_fixed_ip_get_by_address_detailed)
|
||||
self.stubs.Set(db, "fixed_ip_update", fake_fixed_ip_update)
|
||||
|
||||
self.context = context.get_admin_context()
|
||||
self.controller = fixed_ips.FixedIPController()
|
||||
|
||||
def test_fixed_ips_get(self):
|
||||
req = fakes.HTTPRequest.blank('/v3/fake/os-fixed-ips/192.168.1.1')
|
||||
res_dict = self.controller.show(req, '192.168.1.1')
|
||||
response = {'fixed_ip': {'cidr': '192.168.1.0/24',
|
||||
'hostname': None,
|
||||
'host': None,
|
||||
'address': '192.168.1.1'}}
|
||||
self.assertEqual(response, res_dict)
|
||||
|
||||
def test_fixed_ips_get_bad_ip_fail(self):
|
||||
req = fakes.HTTPRequest.blank('/v3/fake/os-fixed-ips/10.0.0.1')
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, req,
|
||||
'10.0.0.1')
|
||||
|
||||
def test_fixed_ips_get_deleted_ip_fail(self):
|
||||
req = fakes.HTTPRequest.blank('/v3/fake/os-fixed-ips/10.0.0.2')
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, req,
|
||||
'10.0.0.2')
|
||||
|
||||
def test_fixed_ip_reserve(self):
|
||||
fake_fixed_ips[0]['reserved'] = False
|
||||
body = {'reserve': None}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v3/fake/os-fixed-ips/192.168.1.1/action')
|
||||
result = self.controller.action(req, "192.168.1.1", body)
|
||||
|
||||
self.assertEqual('202 Accepted', result.status)
|
||||
self.assertEqual(fake_fixed_ips[0]['reserved'], True)
|
||||
|
||||
def test_fixed_ip_reserve_bad_ip(self):
|
||||
body = {'reserve': None}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v3/fake/os-fixed-ips/10.0.0.1/action')
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.action, req,
|
||||
'10.0.0.1', body)
|
||||
|
||||
def test_fixed_ip_reserve_deleted_ip(self):
|
||||
body = {'reserve': None}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v3/fake/os-fixed-ips/10.0.0.2/action')
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.action, req,
|
||||
'10.0.0.2', body)
|
||||
|
||||
def test_fixed_ip_unreserve(self):
|
||||
fake_fixed_ips[0]['reserved'] = True
|
||||
body = {'unreserve': None}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v3/fake/os-fixed-ips/192.168.1.1/action')
|
||||
result = self.controller.action(req, "192.168.1.1", body)
|
||||
|
||||
self.assertEqual('202 Accepted', result.status)
|
||||
self.assertEqual(fake_fixed_ips[0]['reserved'], False)
|
||||
|
||||
def test_fixed_ip_unreserve_bad_ip(self):
|
||||
body = {'unreserve': None}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v3/fake/os-fixed-ips/10.0.0.1/action')
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.action, req,
|
||||
'10.0.0.1', body)
|
||||
|
||||
def test_fixed_ip_unreserve_deleted_ip(self):
|
||||
body = {'unreserve': None}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v3/fake/os-fixed-ips/10.0.0.2/action')
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.action, req,
|
||||
'10.0.0.2', body)
|
@@ -53,6 +53,9 @@ console_scripts =
|
||||
nova-spicehtml5proxy = nova.cmd.spicehtml5proxy:main
|
||||
nova-xvpvncproxy = nova.cmd.xvpvncproxy:main
|
||||
|
||||
nova.api.v3.extensions =
|
||||
fixed_ips = nova.api.openstack.compute.plugins.v3.fixed_ips:FixedIPs
|
||||
|
||||
[build_sphinx]
|
||||
all_files = 1
|
||||
build-dir = doc/build
|
||||
|
Reference in New Issue
Block a user