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:
Chris Yeoh
2013-04-23 02:15:27 +09:30
parent 5dba32a23e
commit 0c7237efd9
11 changed files with 471 additions and 0 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View 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 []

View File

@@ -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

View 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)

View File

@@ -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