summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--etc/nova/api-paste.ini10
-rw-r--r--nova/api/openstack/__init__.py110
-rw-r--r--nova/api/openstack/compute/__init__.py12
-rw-r--r--nova/api/openstack/compute/plugins/__init__.py0
-rw-r--r--nova/api/openstack/compute/plugins/v3/__init__.py0
-rw-r--r--nova/api/openstack/compute/plugins/v3/fixed_ips.py98
-rw-r--r--nova/api/openstack/extensions.py50
-rw-r--r--nova/tests/api/openstack/compute/plugins/__init__.py0
-rw-r--r--nova/tests/api/openstack/compute/plugins/v3/__init__.py0
-rw-r--r--nova/tests/api/openstack/compute/plugins/v3/test_fixed_ips.py188
-rw-r--r--setup.cfg3
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)
diff --git a/setup.cfg b/setup.cfg
index aa9371ecb..54127dcfb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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