diff options
-rw-r--r-- | etc/nova/api-paste.ini | 10 | ||||
-rw-r--r-- | nova/api/openstack/__init__.py | 110 | ||||
-rw-r--r-- | nova/api/openstack/compute/__init__.py | 12 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/__init__.py | 0 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/__init__.py | 0 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/fixed_ips.py | 98 | ||||
-rw-r--r-- | nova/api/openstack/extensions.py | 50 | ||||
-rw-r--r-- | nova/tests/api/openstack/compute/plugins/__init__.py | 0 | ||||
-rw-r--r-- | nova/tests/api/openstack/compute/plugins/v3/__init__.py | 0 | ||||
-rw-r--r-- | nova/tests/api/openstack/compute/plugins/v3/test_fixed_ips.py | 188 | ||||
-rw-r--r-- | setup.cfg | 3 |
11 files changed, 471 insertions, 0 deletions
diff --git a/etc/nova/api-paste.ini b/etc/nova/api-paste.ini index 34c87b92d..1bd26143f 100644 --- a/etc/nova/api-paste.ini +++ b/etc/nova/api-paste.ini @@ -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 diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index a76b74324..cc276234b 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -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) diff --git a/nova/api/openstack/compute/__init__.py b/nova/api/openstack/compute/__init__.py index a46d51eaf..80247705f 100644 --- a/nova/api/openstack/compute/__init__.py +++ b/nova/api/openstack/compute/__init__.py @@ -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 diff --git a/nova/api/openstack/compute/plugins/__init__.py b/nova/api/openstack/compute/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/nova/api/openstack/compute/plugins/__init__.py diff --git a/nova/api/openstack/compute/plugins/v3/__init__.py b/nova/api/openstack/compute/plugins/v3/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/__init__.py diff --git a/nova/api/openstack/compute/plugins/v3/fixed_ips.py b/nova/api/openstack/compute/plugins/v3/fixed_ips.py new file mode 100644 index 000000000..dc22a83ea --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/fixed_ips.py @@ -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 [] diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index e7c806388..dcf6149e5 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -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 diff --git a/nova/tests/api/openstack/compute/plugins/__init__.py b/nova/tests/api/openstack/compute/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/__init__.py diff --git a/nova/tests/api/openstack/compute/plugins/v3/__init__.py b/nova/tests/api/openstack/compute/plugins/v3/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/__init__.py diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_fixed_ips.py b/nova/tests/api/openstack/compute/plugins/v3/test_fixed_ips.py new file mode 100644 index 000000000..2a1387d15 --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_fixed_ips.py @@ -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 |