diff options
| author | Vishvananda Ishaya <vishvananda@yahoo.com> | 2010-08-31 23:21:30 -0700 |
|---|---|---|
| committer | Vishvananda Ishaya <vishvananda@yahoo.com> | 2010-08-31 23:21:30 -0700 |
| commit | e7eb9443bc07a173b5885f634c80c616d9d59e9b (patch) | |
| tree | 579aef2e8566adbf7d94ef82aa121120f3fdb37d /nova/api | |
| parent | ad7a20231a8fb11bf7c75f2e180735e2de450102 (diff) | |
| parent | 975861fd0b8fe7c89ccb6a31b0d0c89948c18252 (diff) | |
| download | nova-e7eb9443bc07a173b5885f634c80c616d9d59e9b.tar.gz nova-e7eb9443bc07a173b5885f634c80c616d9d59e9b.tar.xz nova-e7eb9443bc07a173b5885f634c80c616d9d59e9b.zip | |
merged orm branch
Diffstat (limited to 'nova/api')
| -rw-r--r-- | nova/api/__init__.py | 37 | ||||
| -rw-r--r-- | nova/api/ec2/__init__.py | 42 | ||||
| -rw-r--r-- | nova/api/rackspace/__init__.py | 83 | ||||
| -rw-r--r-- | nova/api/rackspace/_id_translator.py | 42 | ||||
| -rw-r--r-- | nova/api/rackspace/base.py | 30 | ||||
| -rw-r--r-- | nova/api/rackspace/flavors.py | 54 | ||||
| -rw-r--r-- | nova/api/rackspace/images.py | 70 | ||||
| -rw-r--r-- | nova/api/rackspace/notes.txt | 23 | ||||
| -rw-r--r-- | nova/api/rackspace/servers.py | 83 | ||||
| -rw-r--r-- | nova/api/rackspace/sharedipgroups.py | 18 | ||||
| -rw-r--r-- | nova/api/test.py | 61 |
11 files changed, 543 insertions, 0 deletions
diff --git a/nova/api/__init__.py b/nova/api/__init__.py new file mode 100644 index 000000000..b9b9e3988 --- /dev/null +++ b/nova/api/__init__.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. + +""" +Root WSGI middleware for all API controllers. +""" + +import routes + +from nova import wsgi +from nova.api import ec2 +from nova.api import rackspace + + +class API(wsgi.Router): + """Routes top-level requests to the appropriate controller.""" + + def __init__(self): + mapper = routes.Mapper() + mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API()) + mapper.connect("/ec2/{path_info:.*}", controller=ec2.API()) + super(API, self).__init__(mapper) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py new file mode 100644 index 000000000..6eec0abf7 --- /dev/null +++ b/nova/api/ec2/__init__.py @@ -0,0 +1,42 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +""" +WSGI middleware for EC2 API controllers. +""" + +import routes +import webob.dec + +from nova import wsgi + + +class API(wsgi.Router): + """Routes EC2 requests to the appropriate controller.""" + + def __init__(self): + mapper = routes.Mapper() + mapper.connect(None, "{all:.*}", controller=self.dummy) + super(API, self).__init__(mapper) + + @staticmethod + @webob.dec.wsgify + def dummy(req): + """Temporary dummy controller.""" + msg = "dummy response -- please hook up __init__() to cloud.py instead" + return repr({'dummy': msg, + 'kwargs': repr(req.environ['wsgiorg.routing_args'][1])}) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py new file mode 100644 index 000000000..b4d666d63 --- /dev/null +++ b/nova/api/rackspace/__init__.py @@ -0,0 +1,83 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. + +""" +WSGI middleware for Rackspace API controllers. +""" + +import json +import time + +import routes +import webob.dec +import webob.exc + +from nova import flags +from nova import wsgi +from nova.api.rackspace import flavors +from nova.api.rackspace import images +from nova.api.rackspace import servers +from nova.api.rackspace import sharedipgroups +from nova.auth import manager + + +class API(wsgi.Middleware): + """WSGI entry point for all Rackspace API requests.""" + + def __init__(self): + app = AuthMiddleware(APIRouter()) + super(API, self).__init__(app) + + +class AuthMiddleware(wsgi.Middleware): + """Authorize the rackspace API request or return an HTTP Forbidden.""" + + #TODO(gundlach): isn't this the old Nova API's auth? Should it be replaced + #with correct RS API auth? + + @webob.dec.wsgify + def __call__(self, req): + context = {} + if "HTTP_X_AUTH_TOKEN" in req.environ: + context['user'] = manager.AuthManager().get_user_from_access_key( + req.environ['HTTP_X_AUTH_TOKEN']) + if context['user']: + context['project'] = manager.AuthManager().get_project( + context['user'].name) + if "user" not in context: + return webob.exc.HTTPForbidden() + req.environ['nova.context'] = context + return self.application + + +class APIRouter(wsgi.Router): + """ + Routes requests on the Rackspace API to the appropriate controller + and method. + """ + + def __init__(self): + mapper = routes.Mapper() + mapper.resource("server", "servers", controller=servers.Controller()) + mapper.resource("image", "images", controller=images.Controller(), + collection={'detail': 'GET'}) + mapper.resource("flavor", "flavors", controller=flavors.Controller(), + collection={'detail': 'GET'}) + mapper.resource("sharedipgroup", "sharedipgroups", + controller=sharedipgroups.Controller()) + super(APIRouter, self).__init__(mapper) diff --git a/nova/api/rackspace/_id_translator.py b/nova/api/rackspace/_id_translator.py new file mode 100644 index 000000000..aec5fb6a5 --- /dev/null +++ b/nova/api/rackspace/_id_translator.py @@ -0,0 +1,42 @@ +from nova import datastore + +class RackspaceAPIIdTranslator(object): + """ + Converts Rackspace API ids to and from the id format for a given + strategy. + """ + + def __init__(self, id_type, service_name): + """ + Creates a translator for ids of the given type (e.g. 'flavor'), for the + given storage service backend class name (e.g. 'LocalFlavorService'). + """ + + self._store = datastore.Redis.instance() + key_prefix = "rsapi.idtranslator.%s.%s" % (id_type, service_name) + # Forward (strategy format -> RS format) and reverse translation keys + self._fwd_key = "%s.fwd" % key_prefix + self._rev_key = "%s.rev" % key_prefix + + def to_rs_id(self, opaque_id): + """Convert an id from a strategy-specific one to a Rackspace one.""" + result = self._store.hget(self._fwd_key, str(opaque_id)) + if result: # we have a mapping from opaque to RS for this strategy + return int(result) + else: + # Store the mapping. + nextid = self._store.incr("%s.lastid" % self._fwd_key) + if self._store.hsetnx(self._fwd_key, str(opaque_id), nextid): + # If someone else didn't beat us to it, store the reverse + # mapping as well. + self._store.hset(self._rev_key, nextid, str(opaque_id)) + return nextid + else: + # Someone beat us to it; use their number instead, and + # discard nextid (which is OK -- we don't require that + # every int id be used.) + return int(self._store.hget(self._fwd_key, str(opaque_id))) + + def from_rs_id(self, strategy_name, rs_id): + """Convert a Rackspace id to a strategy-specific one.""" + return self._store.hget(self._rev_key, rs_id) diff --git a/nova/api/rackspace/base.py b/nova/api/rackspace/base.py new file mode 100644 index 000000000..dd2c6543c --- /dev/null +++ b/nova/api/rackspace/base.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +from nova import wsgi + + +class Controller(wsgi.Controller): + """TODO(eday): Base controller for all rackspace controllers. What is this + for? Is this just Rackspace specific? """ + + @classmethod + def render(cls, instance): + if isinstance(instance, list): + return {cls.entity_name: cls.render(instance)} + else: + return {"TODO": "TODO"} diff --git a/nova/api/rackspace/flavors.py b/nova/api/rackspace/flavors.py new file mode 100644 index 000000000..60b35c939 --- /dev/null +++ b/nova/api/rackspace/flavors.py @@ -0,0 +1,54 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +from nova.api.rackspace import base +from nova.compute import instance_types +from webob import exc + +class Controller(base.Controller): + """Flavor controller for the Rackspace API.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "flavor": [ "id", "name", "ram", "disk" ] + } + } + } + + def index(self, req): + """Return all flavors in brief.""" + return dict(flavors=[dict(id=flavor['id'], name=flavor['name']) + for flavor in self.detail(req)['flavors']]) + + def detail(self, req): + """Return all flavors in detail.""" + items = [self.show(req, id)['flavor'] for id in self._all_ids()] + return dict(flavors=items) + + def show(self, req, id): + """Return data about the given flavor id.""" + for name, val in instance_types.INSTANCE_TYPES.iteritems(): + if val['flavorid'] == int(id): + item = dict(ram=val['memory_mb'], disk=val['local_gb'], + id=val['flavorid'], name=name) + return dict(flavor=item) + raise exc.HTTPNotFound() + + def _all_ids(self): + """Return the list of all flavorids.""" + return [i['flavorid'] for i in instance_types.INSTANCE_TYPES.values()] diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py new file mode 100644 index 000000000..2f3e928b9 --- /dev/null +++ b/nova/api/rackspace/images.py @@ -0,0 +1,70 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# 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 nova.image.service +from nova.api.rackspace import base +from nova.api.rackspace import _id_translator +from webob import exc + +class Controller(base.Controller): + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "image": [ "id", "name", "updated", "created", "status", + "serverId", "progress" ] + } + } + } + + def __init__(self): + self._service = nova.image.service.ImageService.load() + self._id_translator = _id_translator.RackspaceAPIIdTranslator( + "image", self._service.__class__.__name__) + + def index(self, req): + """Return all public images in brief.""" + return dict(images=[dict(id=img['id'], name=img['name']) + for img in self.detail(req)['images']]) + + def detail(self, req): + """Return all public images in detail.""" + data = self._service.index() + for img in data: + img['id'] = self._id_translator.to_rs_id(img['id']) + return dict(images=data) + + def show(self, req, id): + """Return data about the given image id.""" + opaque_id = self._id_translator.from_rs_id(id) + img = self._service.show(opaque_id) + img['id'] = id + return dict(image=img) + + def delete(self, req, id): + # Only public images are supported for now. + raise exc.HTTPNotFound() + + def create(self, req): + # Only public images are supported for now, so a request to + # make a backup of a server cannot be supproted. + raise exc.HTTPNotFound() + + def update(self, req, id): + # Users may not modify public images, and that's all that + # we support for now. + raise exc.HTTPNotFound() diff --git a/nova/api/rackspace/notes.txt b/nova/api/rackspace/notes.txt new file mode 100644 index 000000000..e133bf5ea --- /dev/null +++ b/nova/api/rackspace/notes.txt @@ -0,0 +1,23 @@ +We will need: + +ImageService +a service that can do crud on image information. not user-specific. opaque +image ids. + +GlanceImageService(ImageService): +image ids are URIs. + +LocalImageService(ImageService): +image ids are random strings. + +RackspaceAPITranslationStore: +translates RS server/images/flavor/etc ids into formats required +by a given ImageService strategy. + +api.rackspace.images.Controller: +uses an ImageService strategy behind the scenes to do its fetching; it just +converts int image id into a strategy-specific image id. + +who maintains the mapping from user to [images he owns]? nobody, because +we have no way of enforcing access to his images, without kryptex which +won't be in Austin. diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py new file mode 100644 index 000000000..25d1fe9c8 --- /dev/null +++ b/nova/api/rackspace/servers.py @@ -0,0 +1,83 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +from nova import rpc +from nova.compute import model as compute +from nova.api.rackspace import base + + +class Controller(base.Controller): + entity_name = 'servers' + + def index(self, **kwargs): + instances = [] + for inst in compute.InstanceDirectory().all: + instances.append(instance_details(inst)) + + def show(self, **kwargs): + instance_id = kwargs['id'] + return compute.InstanceDirectory().get(instance_id) + + def delete(self, **kwargs): + instance_id = kwargs['id'] + instance = compute.InstanceDirectory().get(instance_id) + if not instance: + raise ServerNotFound("The requested server was not found") + instance.destroy() + return True + + def create(self, **kwargs): + inst = self.build_server_instance(kwargs['server']) + rpc.cast( + FLAGS.compute_topic, { + "method": "run_instance", + "args": {"instance_id": inst.instance_id}}) + + def update(self, **kwargs): + instance_id = kwargs['id'] + instance = compute.InstanceDirectory().get(instance_id) + if not instance: + raise ServerNotFound("The requested server was not found") + instance.update(kwargs['server']) + instance.save() + + def build_server_instance(self, env): + """Build instance data structure and save it to the data store.""" + reservation = utils.generate_uid('r') + ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) + inst = self.instdir.new() + inst['name'] = env['server']['name'] + inst['image_id'] = env['server']['imageId'] + inst['instance_type'] = env['server']['flavorId'] + inst['user_id'] = env['user']['id'] + inst['project_id'] = env['project']['id'] + inst['reservation_id'] = reservation + inst['launch_time'] = ltime + inst['mac_address'] = utils.generate_mac() + address = self.network.allocate_ip( + inst['user_id'], + inst['project_id'], + mac=inst['mac_address']) + inst['private_dns_name'] = str(address) + inst['bridge_name'] = network.BridgedNetwork.get_network_for_project( + inst['user_id'], + inst['project_id'], + 'default')['bridge_name'] + # key_data, key_name, ami_launch_index + # TODO(todd): key data or root password + inst.save() + return inst diff --git a/nova/api/rackspace/sharedipgroups.py b/nova/api/rackspace/sharedipgroups.py new file mode 100644 index 000000000..986f11434 --- /dev/null +++ b/nova/api/rackspace/sharedipgroups.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +class Controller(object): pass diff --git a/nova/api/test.py b/nova/api/test.py new file mode 100644 index 000000000..51b114b8e --- /dev/null +++ b/nova/api/test.py @@ -0,0 +1,61 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +""" +Test for the root WSGI middleware for all API controllers. +""" + +import unittest + +import stubout +import webob +import webob.dec + +from nova import api + + +class Test(unittest.TestCase): + + def setUp(self): # pylint: disable-msg=C0103 + self.stubs = stubout.StubOutForTesting() + + def tearDown(self): # pylint: disable-msg=C0103 + self.stubs.UnsetAll() + + def test_rackspace(self): + self.stubs.Set(api.rackspace, 'API', APIStub) + result = webob.Request.blank('/v1.0/cloud').get_response(api.API()) + self.assertEqual(result.body, "/cloud") + + def test_ec2(self): + self.stubs.Set(api.ec2, 'API', APIStub) + result = webob.Request.blank('/ec2/cloud').get_response(api.API()) + self.assertEqual(result.body, "/cloud") + + def test_not_found(self): + self.stubs.Set(api.ec2, 'API', APIStub) + self.stubs.Set(api.rackspace, 'API', APIStub) + result = webob.Request.blank('/test/cloud').get_response(api.API()) + self.assertNotEqual(result.body, "/cloud") + + +class APIStub(object): + """Class to verify request and mark it was called.""" + + @webob.dec.wsgify + def __call__(self, req): + return req.path_info |
