From 4fa2258af9fb130be1650372cf48be39e83451e5 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 3 Oct 2010 13:12:32 +0100 Subject: Bug #654025: nova-manage project zip and nova-manage vpn list broken by change in DB semantics when networks are missing Catch exception.NotFound when getting project VPN data. This is in two places: nova-manage as part of its vpn list command, and auth.manager.AuthManager.get_credentials. Also, document the behaviour of db.api.project_get_network. --- nova/auth/manager.py | 5 ++++- nova/db/api.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 0bc12c80f..c30192e20 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -653,7 +653,10 @@ class AuthManager(object): zippy.writestr(FLAGS.credential_key_file, private_key) zippy.writestr(FLAGS.credential_cert_file, signed_cert) - (vpn_ip, vpn_port) = self.get_project_vpn_data(project) + try: + (vpn_ip, vpn_port) = self.get_project_vpn_data(project) + except exception.NotFound: + vpn_ip = None if vpn_ip: configfile = open(FLAGS.vpn_client_template, "r") s = string.Template(configfile.read()) diff --git a/nova/db/api.py b/nova/db/api.py index 5c935b561..d34f1b2cb 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -432,7 +432,11 @@ def network_update(context, network_id, values): def project_get_network(context, project_id): - """Return the network associated with the project.""" + """Return the network associated with the project. + + Raises NotFound if no such network can be found. + + """ return IMPL.project_get_network(context, project_id) -- cgit From 8fb9f78a313a43f333d20c7cc600a5085eb68915 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 4 Oct 2010 21:53:22 +0200 Subject: Replace the embarrasingly crude string based tests for to_xml with some more sensible ElementTree based stuff. --- nova/tests/virt_unittest.py | 63 ++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 21 deletions(-) (limited to 'nova') diff --git a/nova/tests/virt_unittest.py b/nova/tests/virt_unittest.py index 2aab16809..998cc07db 100644 --- a/nova/tests/virt_unittest.py +++ b/nova/tests/virt_unittest.py @@ -14,36 +14,49 @@ # License for the specific language governing permissions and limitations # under the License. +from xml.etree.ElementTree import fromstring as parseXml + from nova import flags from nova import test from nova.virt import libvirt_conn FLAGS = flags.FLAGS - class LibvirtConnTestCase(test.TrialTestCase): def test_get_uri_and_template(self): - class MockDataModel(object): - def __init__(self): - self.datamodel = { 'name' : 'i-cafebabe', - 'memory_kb' : '1024000', - 'basepath' : '/some/path', - 'bridge_name' : 'br100', - 'mac_address' : '02:12:34:46:56:67', - 'vcpus' : 2 } + instance = { 'name' : 'i-cafebabe', + 'id' : 'i-cafebabe', + 'memory_kb' : '1024000', + 'basepath' : '/some/path', + 'bridge_name' : 'br100', + 'mac_address' : '02:12:34:46:56:67', + 'vcpus' : 2, + 'project_id' : 'fake', + 'ip_address' : '10.11.12.13', + 'bridge' : 'br101', + 'instance_type' : 'm1.small'} type_uri_map = { 'qemu' : ('qemu:///system', - [lambda s: '' in s, - lambda s: 'type>hvm/usr/bin/kvm' not in s]), + [(lambda t: t.find('.').tag, 'domain'), + (lambda t: t.find('.').get('type'), 'qemu'), + (lambda t: t.find('./os/type').text, 'hvm'), + (lambda t: t.find('./devices/emulator'), None)]), 'kvm' : ('qemu:///system', - [lambda s: '' in s, - lambda s: 'type>hvm/usr/bin/qemu<' not in s]), + [(lambda t: t.find('.').tag, 'domain'), + (lambda t: t.find('.').get('type'), 'kvm'), + (lambda t: t.find('./os/type').text, 'hvm'), + (lambda t: t.find('./devices/emulator'), None)]), 'uml' : ('uml:///system', - [lambda s: '' in s, - lambda s: 'type>uml Date: Tue, 5 Oct 2010 10:07:37 +0200 Subject: Create and destroy user appropriately. Remove security group related tests (since they haven't been merged yet). --- nova/tests/virt_unittest.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) (limited to 'nova') diff --git a/nova/tests/virt_unittest.py b/nova/tests/virt_unittest.py index 998cc07db..730928f39 100644 --- a/nova/tests/virt_unittest.py +++ b/nova/tests/virt_unittest.py @@ -18,11 +18,20 @@ from xml.etree.ElementTree import fromstring as parseXml from nova import flags from nova import test +from nova.auth import manager +# Needed to get FLAGS.instances_path defined: +from nova.compute import manager as compute_manager from nova.virt import libvirt_conn FLAGS = flags.FLAGS class LibvirtConnTestCase(test.TrialTestCase): + def setUp(self): + self.manager = manager.AuthManager() + self.user = self.manager.create_user('fake', 'fake', 'fake', admin=True) + self.project = self.manager.create_project('fake', 'fake', 'fake') + FLAGS.instances_path = '' + def test_get_uri_and_template(self): instance = { 'name' : 'i-cafebabe', 'id' : 'i-cafebabe', @@ -51,12 +60,6 @@ class LibvirtConnTestCase(test.TrialTestCase): (lambda t: t.find('.').get('type'), 'uml'), (lambda t: t.find('./os/type').text, 'uml')]), } - common_checks = [(lambda t: \ - t.find('./devices/interface/filterref/parameter') \ - .get('name'), 'IP'), - (lambda t: \ - t.find('./devices/interface/filterref/parameter') \ - .get('value'), '10.11.12.13')] for (libvirt_type,(expected_uri, checks)) in type_uri_map.iteritems(): FLAGS.libvirt_type = libvirt_type @@ -72,11 +75,6 @@ class LibvirtConnTestCase(test.TrialTestCase): expected_result, '%s failed check %d' % (xml, i)) - for i, (check, expected_result) in enumerate(common_checks): - self.assertEqual(check(tree), - expected_result, - '%s failed common check %d' % (xml, i)) - # Deliberately not just assigning this string to FLAGS.libvirt_uri and # checking against that later on. This way we make sure the # implementation doesn't fiddle around with the FLAGS. @@ -88,3 +86,7 @@ class LibvirtConnTestCase(test.TrialTestCase): uri, template = conn.get_uri_and_template() self.assertEquals(uri, testuri) + + def tearDown(self): + self.manager.delete_project(self.project) + self.manager.delete_user(self.user) -- cgit From 8f524607856dbf4cecf7c7503e53e14c42888307 Mon Sep 17 00:00:00 2001 From: Hisaki Ohara Date: Wed, 6 Oct 2010 18:04:18 +0900 Subject: Defined images_path for nova-compute. Without its setting, it fails to launch instances by exception at _fetch_local_image. --- nova/virt/images.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'nova') diff --git a/nova/virt/images.py b/nova/virt/images.py index a60bcc4c1..9ba5b7890 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -26,6 +26,7 @@ import time import urlparse from nova import flags +from nova import utils from nova import process from nova.auth import manager from nova.auth import signer @@ -34,6 +35,8 @@ from nova.auth import signer FLAGS = flags.FLAGS flags.DEFINE_bool('use_s3', True, 'whether to get images from s3 or use local copy') +flags.DEFINE_string('images_path', utils.abspath('../images'), + 'path to decrypted images') def fetch(image, path, user, project): -- cgit From b7028c0d0262d3d4395077a8bd2d95664c6bf16e Mon Sep 17 00:00:00 2001 From: Hisaki Ohara Date: Thu, 7 Oct 2010 23:03:43 +0900 Subject: Imported images_path from nova.objectstore for nova-compute. Without its setting, it fails to launch instances by exception at _fetch_local_image. --- nova/virt/images.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'nova') diff --git a/nova/virt/images.py b/nova/virt/images.py index 9ba5b7890..dc50764d9 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -26,17 +26,15 @@ import time import urlparse from nova import flags -from nova import utils from nova import process from nova.auth import manager from nova.auth import signer +from nova.objectstore import image FLAGS = flags.FLAGS flags.DEFINE_bool('use_s3', True, 'whether to get images from s3 or use local copy') -flags.DEFINE_string('images_path', utils.abspath('../images'), - 'path to decrypted images') def fetch(image, path, user, project): -- cgit From db87fd5a8145d045c4767a8d02cde5a0750113f8 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Fri, 8 Oct 2010 12:21:26 -0400 Subject: Remove redis dependency from Images controller. LocalImageService now works with integer ids, so there's no need for the translator. Once Glance exists we'll have to revisit this. --- nova/api/rackspace/backup_schedules.py | 1 - nova/api/rackspace/images.py | 10 +--------- nova/image/service.py | 16 +++++++++------- nova/tests/api/rackspace/fakes.py | 1 - 4 files changed, 10 insertions(+), 18 deletions(-) (limited to 'nova') diff --git a/nova/api/rackspace/backup_schedules.py b/nova/api/rackspace/backup_schedules.py index cb83023bc..9c0d41fa0 100644 --- a/nova/api/rackspace/backup_schedules.py +++ b/nova/api/rackspace/backup_schedules.py @@ -19,7 +19,6 @@ import time from webob import exc from nova import wsgi -from nova.api.rackspace import _id_translator from nova.api.rackspace import faults import nova.image.service diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py index d4ab8ce3c..82dcd2049 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/rackspace/images.py @@ -20,7 +20,6 @@ from webob import exc from nova import flags from nova import utils from nova import wsgi -from nova.api.rackspace import _id_translator import nova.api.rackspace import nova.image.service from nova.api.rackspace import faults @@ -41,8 +40,6 @@ class Controller(wsgi.Controller): def __init__(self): self._service = utils.import_object(FLAGS.image_service) - self._id_translator = _id_translator.RackspaceAPIIdTranslator( - "image", self._service.__class__.__name__) def index(self, req): """Return all public images in brief.""" @@ -53,16 +50,11 @@ class Controller(wsgi.Controller): """Return all public images in detail.""" data = self._service.index() data = nova.api.rackspace.limited(data, req) - 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) + return dict(image=self._service.show(id)) def delete(self, req, id): # Only public images are supported for now. diff --git a/nova/image/service.py b/nova/image/service.py index 2e570e8a4..5276e1312 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -225,7 +225,9 @@ class GlanceImageService(BaseImageService): class LocalImageService(BaseImageService): - """Image service storing images to local disk.""" + """Image service storing images to local disk. + + It assumes that image_ids are integers.""" def __init__(self): self._path = "/tmp/nova/images" @@ -234,12 +236,12 @@ class LocalImageService(BaseImageService): except OSError: # exists pass - def _path_to(self, image_id=''): - return os.path.join(self._path, image_id) + def _path_to(self, image_id): + return os.path.join(self._path, str(image_id)) def _ids(self): """The list of all image ids.""" - return os.listdir(self._path) + return [int(i) for i in os.listdir(self._path)] def index(self): return [ self.show(id) for id in self._ids() ] @@ -254,7 +256,7 @@ class LocalImageService(BaseImageService): """ Store the image data and return the new image id. """ - id = ''.join(random.choice(string.letters) for _ in range(20)) + id = random.randint(0, 2**32-1) data['id'] = id self.update(id, data) return id @@ -279,5 +281,5 @@ class LocalImageService(BaseImageService): """ Clears out all images in local directory """ - for f in os.listdir(self._path): - os.unlink(self._path_to(f)) + for id in self._ids(): + os.unlink(self._path_to(id)) diff --git a/nova/tests/api/rackspace/fakes.py b/nova/tests/api/rackspace/fakes.py index b5fba2dfa..6a25720a9 100644 --- a/nova/tests/api/rackspace/fakes.py +++ b/nova/tests/api/rackspace/fakes.py @@ -28,7 +28,6 @@ from nova import utils from nova import flags from nova import exception as exc import nova.api.rackspace.auth -import nova.api.rackspace._id_translator from nova.image import service from nova.wsgi import Router -- cgit From f1a48207dfc1948ba847f262d5a4ff825b02202c Mon Sep 17 00:00:00 2001 From: mdietz Date: Fri, 8 Oct 2010 18:56:32 +0000 Subject: Start stripping out the translators --- nova/api/rackspace/servers.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) (limited to 'nova') diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index b23867bbf..8c489ed83 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -25,7 +25,6 @@ from nova import rpc from nova import utils from nova import wsgi from nova.api import cloud -from nova.api.rackspace import _id_translator from nova.api.rackspace import context from nova.api.rackspace import faults from nova.compute import instance_types @@ -35,11 +34,6 @@ import nova.image.service FLAGS = flags.FLAGS -def _instance_id_translator(): - """ Helper method for initializing an id translator for Rackspace instance - ids """ - return _id_translator.RackspaceAPIIdTranslator( "instance", 'nova') - def _image_service(): """ Helper method for initializing the image id translator """ service = utils.import_object(FLAGS.image_service) @@ -182,13 +176,16 @@ class Controller(wsgi.Controller): def action(self, req, id): """ multi-purpose method used to reboot, rebuild, and resize a server """ + user_id = req.environ['nova.context']['user']['id'] input_dict = self._deserialize(req.body, req) try: reboot_type = input_dict['reboot']['type'] except Exception: raise faults.Fault(webob.exc.HTTPNotImplemented()) - opaque_id = _instance_id_translator().from_rs_id(int(id)) - cloud.reboot(opaque_id) + inst_ref = self.db.instance_get_by_internal_id(None, int(id)) + if not inst_ref or (inst_ref and not inst_ref.user_id == user_id): + return faults.Fault(exc.HTTPUnprocessableEntity()) + cloud.reboot(id) def _build_server_instance(self, req, env): """Build instance data structure and save it to the data store.""" -- cgit From 90f38090ecd586a39257b3efd2c86c2c60b7fdb9 Mon Sep 17 00:00:00 2001 From: mdietz Date: Fri, 8 Oct 2010 20:39:00 +0000 Subject: Mass renaming --- nova/api/__init__.py | 4 +- nova/api/openstack/__init__.py | 190 ++++++++++++++++ nova/api/openstack/_id_translator.py | 42 ++++ nova/api/openstack/auth.py | 101 +++++++++ nova/api/openstack/backup_schedules.py | 38 ++++ nova/api/openstack/context.py | 33 +++ nova/api/openstack/faults.py | 62 ++++++ nova/api/openstack/flavors.py | 58 +++++ nova/api/openstack/images.py | 71 ++++++ nova/api/openstack/notes.txt | 23 ++ nova/api/openstack/ratelimiting/__init__.py | 122 ++++++++++ nova/api/openstack/servers.py | 276 +++++++++++++++++++++++ nova/api/openstack/sharedipgroups.py | 20 ++ nova/api/rackspace/__init__.py | 190 ---------------- nova/api/rackspace/_id_translator.py | 42 ---- nova/api/rackspace/auth.py | 101 --------- nova/api/rackspace/backup_schedules.py | 38 ---- nova/api/rackspace/context.py | 33 --- nova/api/rackspace/faults.py | 62 ------ nova/api/rackspace/flavors.py | 58 ----- nova/api/rackspace/images.py | 71 ------ nova/api/rackspace/notes.txt | 23 -- nova/api/rackspace/ratelimiting/__init__.py | 122 ---------- nova/api/rackspace/ratelimiting/tests.py | 237 -------------------- nova/api/rackspace/servers.py | 283 ------------------------ nova/api/rackspace/sharedipgroups.py | 20 -- nova/tests/api/__init__.py | 6 +- nova/tests/api/openstack/__init__.py | 108 +++++++++ nova/tests/api/openstack/fakes.py | 205 +++++++++++++++++ nova/tests/api/openstack/test_auth.py | 108 +++++++++ nova/tests/api/openstack/test_faults.py | 40 ++++ nova/tests/api/openstack/test_flavors.py | 48 ++++ nova/tests/api/openstack/test_images.py | 141 ++++++++++++ nova/tests/api/openstack/test_ratelimiting.py | 237 ++++++++++++++++++++ nova/tests/api/openstack/test_servers.py | 249 +++++++++++++++++++++ nova/tests/api/openstack/test_sharedipgroups.py | 39 ++++ nova/tests/api/rackspace/__init__.py | 108 --------- nova/tests/api/rackspace/fakes.py | 205 ----------------- nova/tests/api/rackspace/test_auth.py | 108 --------- nova/tests/api/rackspace/test_faults.py | 40 ---- nova/tests/api/rackspace/test_flavors.py | 48 ---- nova/tests/api/rackspace/test_images.py | 141 ------------ nova/tests/api/rackspace/test_servers.py | 249 --------------------- nova/tests/api/rackspace/test_sharedipgroups.py | 39 ---- 44 files changed, 2216 insertions(+), 2223 deletions(-) create mode 100644 nova/api/openstack/__init__.py create mode 100644 nova/api/openstack/_id_translator.py create mode 100644 nova/api/openstack/auth.py create mode 100644 nova/api/openstack/backup_schedules.py create mode 100644 nova/api/openstack/context.py create mode 100644 nova/api/openstack/faults.py create mode 100644 nova/api/openstack/flavors.py create mode 100644 nova/api/openstack/images.py create mode 100644 nova/api/openstack/notes.txt create mode 100644 nova/api/openstack/ratelimiting/__init__.py create mode 100644 nova/api/openstack/servers.py create mode 100644 nova/api/openstack/sharedipgroups.py delete mode 100644 nova/api/rackspace/__init__.py delete mode 100644 nova/api/rackspace/_id_translator.py delete mode 100644 nova/api/rackspace/auth.py delete mode 100644 nova/api/rackspace/backup_schedules.py delete mode 100644 nova/api/rackspace/context.py delete mode 100644 nova/api/rackspace/faults.py delete mode 100644 nova/api/rackspace/flavors.py delete mode 100644 nova/api/rackspace/images.py delete mode 100644 nova/api/rackspace/notes.txt delete mode 100644 nova/api/rackspace/ratelimiting/__init__.py delete mode 100644 nova/api/rackspace/ratelimiting/tests.py delete mode 100644 nova/api/rackspace/servers.py delete mode 100644 nova/api/rackspace/sharedipgroups.py create mode 100644 nova/tests/api/openstack/__init__.py create mode 100644 nova/tests/api/openstack/fakes.py create mode 100644 nova/tests/api/openstack/test_auth.py create mode 100644 nova/tests/api/openstack/test_faults.py create mode 100644 nova/tests/api/openstack/test_flavors.py create mode 100644 nova/tests/api/openstack/test_images.py create mode 100644 nova/tests/api/openstack/test_ratelimiting.py create mode 100644 nova/tests/api/openstack/test_servers.py create mode 100644 nova/tests/api/openstack/test_sharedipgroups.py delete mode 100644 nova/tests/api/rackspace/__init__.py delete mode 100644 nova/tests/api/rackspace/fakes.py delete mode 100644 nova/tests/api/rackspace/test_auth.py delete mode 100644 nova/tests/api/rackspace/test_faults.py delete mode 100644 nova/tests/api/rackspace/test_flavors.py delete mode 100644 nova/tests/api/rackspace/test_images.py delete mode 100644 nova/tests/api/rackspace/test_servers.py delete mode 100644 nova/tests/api/rackspace/test_sharedipgroups.py (limited to 'nova') diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 744abd621..627883018 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -27,7 +27,7 @@ from nova import flags from nova import wsgi from nova.api import cloudpipe from nova.api import ec2 -from nova.api import rackspace +from nova.api import openstack from nova.api.ec2 import metadatarequesthandler @@ -57,7 +57,7 @@ class API(wsgi.Router): mapper.sub_domains = True mapper.connect("/", controller=self.rsapi_versions, conditions=rsdomain) - mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API(), + mapper.connect("/v1.0/{path_info:.*}", controller=openstack.API(), conditions=rsdomain) mapper.connect("/", controller=self.ec2api_versions, diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py new file mode 100644 index 000000000..5e81ba2bd --- /dev/null +++ b/nova/api/openstack/__init__.py @@ -0,0 +1,190 @@ +# 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 OpenStack API controllers. +""" + +import json +import time + +import routes +import webob.dec +import webob.exc +import webob + +from nova import flags +from nova import utils +from nova import wsgi +from nova.api.openstack import faults +from nova.api.openstack import backup_schedules +from nova.api.openstack import flavors +from nova.api.openstack import images +from nova.api.openstack import ratelimiting +from nova.api.openstack import servers +from nova.api.openstack import sharedipgroups +from nova.auth import manager + + +FLAGS = flags.FLAGS +flags.DEFINE_string('nova_api_auth', + 'nova.api.openstack.auth.BasicApiAuthManager', + 'The auth mechanism to use for the OpenStack API implemenation') + +class API(wsgi.Middleware): + """WSGI entry point for all OpenStack API requests.""" + + def __init__(self): + app = AuthMiddleware(RateLimitingMiddleware(APIRouter())) + super(API, self).__init__(app) + +class AuthMiddleware(wsgi.Middleware): + """Authorize the openstack API request or return an HTTP Forbidden.""" + + def __init__(self, application): + self.auth_driver = utils.import_class(FLAGS.nova_api_auth)() + super(AuthMiddleware, self).__init__(application) + + @webob.dec.wsgify + def __call__(self, req): + if not req.headers.has_key("X-Auth-Token"): + return self.auth_driver.authenticate(req) + + user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) + + if not user: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + if not req.environ.has_key('nova.context'): + req.environ['nova.context'] = {} + req.environ['nova.context']['user'] = user + return self.application + +class RateLimitingMiddleware(wsgi.Middleware): + """Rate limit incoming requests according to the OpenStack rate limits.""" + + def __init__(self, application, service_host=None): + """Create a rate limiting middleware that wraps the given application. + + By default, rate counters are stored in memory. If service_host is + specified, the middleware instead relies on the ratelimiting.WSGIApp + at the given host+port to keep rate counters. + """ + super(RateLimitingMiddleware, self).__init__(application) + if not service_host: + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = ratelimiting.Limiter(limits={ + 'DELETE': (100, ratelimiting.PER_MINUTE), + 'PUT': (10, ratelimiting.PER_MINUTE), + 'POST': (10, ratelimiting.PER_MINUTE), + 'POST servers': (50, ratelimiting.PER_DAY), + 'GET changes-since': (3, ratelimiting.PER_MINUTE), + }) + else: + self.limiter = ratelimiting.WSGIAppProxy(service_host) + + @webob.dec.wsgify + def __call__(self, req): + """Rate limit the request. + + If the request should be rate limited, return a 413 status with a + Retry-After header giving the time when the request would succeed. + """ + username = req.headers['X-Auth-User'] + action_name = self.get_action_name(req) + if not action_name: # not rate limited + return self.application + delay = self.get_delay(action_name, username) + if delay: + # TODO(gundlach): Get the retry-after format correct. + exc = webob.exc.HTTPRequestEntityTooLarge( + explanation='Too many requests.', + headers={'Retry-After': time.time() + delay}) + raise faults.Fault(exc) + return self.application + + def get_delay(self, action_name, username): + """Return the delay for the given action and username, or None if + the action would not be rate limited. + """ + if action_name == 'POST servers': + # "POST servers" is a POST, so it counts against "POST" too. + # Attempt the "POST" first, lest we are rate limited by "POST" but + # use up a precious "POST servers" call. + delay = self.limiter.perform("POST", username=username) + if delay: + return delay + return self.limiter.perform(action_name, username=username) + + def get_action_name(self, req): + """Return the action name for this request.""" + if req.method == 'GET' and 'changes-since' in req.GET: + return 'GET changes-since' + if req.method == 'POST' and req.path_info.startswith('/servers'): + return 'POST servers' + if req.method in ['PUT', 'POST', 'DELETE']: + return req.method + return None + + +class APIRouter(wsgi.Router): + """ + Routes requests on the OpenStack API to the appropriate controller + and method. + """ + + def __init__(self): + mapper = routes.Mapper() + mapper.resource("server", "servers", controller=servers.Controller(), + collection={ 'detail': 'GET'}, + member={'action':'POST'}) + + mapper.resource("backup_schedule", "backup_schedules", + controller=backup_schedules.Controller(), + parent_resource=dict(member_name='server', + collection_name = 'servers')) + + 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) + + +def limited(items, req): + """Return a slice of items according to requested offset and limit. + + items - a sliceable + req - wobob.Request possibly containing offset and limit GET variables. + offset is where to start in the list, and limit is the maximum number + of items to return. + + If limit is not specified, 0, or > 1000, defaults to 1000. + """ + offset = int(req.GET.get('offset', 0)) + limit = int(req.GET.get('limit', 0)) + if not limit: + limit = 1000 + limit = min(1000, limit) + range_end = offset + limit + return items[offset:range_end] + diff --git a/nova/api/openstack/_id_translator.py b/nova/api/openstack/_id_translator.py new file mode 100644 index 000000000..333aa8434 --- /dev/null +++ b/nova/api/openstack/_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, 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/openstack/auth.py b/nova/api/openstack/auth.py new file mode 100644 index 000000000..4c909293e --- /dev/null +++ b/nova/api/openstack/auth.py @@ -0,0 +1,101 @@ +import datetime +import hashlib +import json +import time + +import webob.exc +import webob.dec + +from nova import auth +from nova import db +from nova import flags +from nova import manager +from nova import utils +from nova.api.openstack import faults + +FLAGS = flags.FLAGS + +class Context(object): + pass + +class BasicApiAuthManager(object): + """ Implements a somewhat rudimentary version of OpenStack Auth""" + + def __init__(self, host=None, db_driver=None): + if not host: + host = FLAGS.host + self.host = host + if not db_driver: + db_driver = FLAGS.db_driver + self.db = utils.import_object(db_driver) + self.auth = auth.manager.AuthManager() + self.context = Context() + super(BasicApiAuthManager, self).__init__() + + def authenticate(self, req): + # Unless the request is explicitly made against // don't + # honor it + path_info = req.path_info + if len(path_info) > 1: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + try: + username, key = req.headers['X-Auth-User'], \ + req.headers['X-Auth-Key'] + except KeyError: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] + token, user = self._authorize_user(username, key) + if user and token: + res = webob.Response() + res.headers['X-Auth-Token'] = token['token_hash'] + res.headers['X-Server-Management-Url'] = \ + token['server_management_url'] + res.headers['X-Storage-Url'] = token['storage_url'] + res.headers['X-CDN-Management-Url'] = token['cdn_management_url'] + res.content_type = 'text/plain' + res.status = '204' + return res + else: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + def authorize_token(self, token_hash): + """ retrieves user information from the datastore given a token + + If the token has expired, returns None + If the token is not found, returns None + Otherwise returns the token + + This method will also remove the token if the timestamp is older than + 2 days ago. + """ + token = self.db.auth_get_token(self.context, token_hash) + if token: + delta = datetime.datetime.now() - token['created_at'] + if delta.days >= 2: + self.db.auth_destroy_token(self.context, token) + else: + user = self.auth.get_user(token['user_id']) + return { 'id':user['uid'] } + return None + + def _authorize_user(self, username, key): + """ Generates a new token and assigns it to a user """ + user = self.auth.get_user_from_access_key(key) + if user and user['name'] == username: + token_hash = hashlib.sha1('%s%s%f' % (username, key, + time.time())).hexdigest() + token = {} + token['token_hash'] = token_hash + token['cdn_management_url'] = '' + token['server_management_url'] = self._get_server_mgmt_url() + token['storage_url'] = '' + token['user_id'] = user['uid'] + self.db.auth_create_token(self.context, token) + return token, user + return None, None + + def _get_server_mgmt_url(self): + return 'https://%s/v1.0/' % self.host + diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py new file mode 100644 index 000000000..76ad6ef87 --- /dev/null +++ b/nova/api/openstack/backup_schedules.py @@ -0,0 +1,38 @@ +# 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 time +from webob import exc + +from nova import wsgi +from nova.api.openstack import faults +import nova.image.service + +class Controller(wsgi.Controller): + def __init__(self): + pass + + def index(self, req, server_id): + return faults.Fault(exc.HTTPNotFound()) + + def create(self, req, server_id): + """ No actual update method required, since the existing API allows + both create and update through a POST """ + return faults.Fault(exc.HTTPNotFound()) + + def delete(self, req, server_id): + return faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/openstack/context.py b/nova/api/openstack/context.py new file mode 100644 index 000000000..77394615b --- /dev/null +++ b/nova/api/openstack/context.py @@ -0,0 +1,33 @@ +# 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. + +""" +APIRequestContext +""" + +import random + +class Project(object): + def __init__(self, user_id): + self.id = user_id + +class APIRequestContext(object): + """ This is an adapter class to get around all of the assumptions made in + the FlatNetworking """ + def __init__(self, user_id): + self.user_id = user_id + self.project = Project(user_id) diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py new file mode 100644 index 000000000..32e5c866f --- /dev/null +++ b/nova/api/openstack/faults.py @@ -0,0 +1,62 @@ +# 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 webob.dec +import webob.exc + +from nova import wsgi + + +class Fault(webob.exc.HTTPException): + + """An RS API fault response.""" + + _fault_names = { + 400: "badRequest", + 401: "unauthorized", + 403: "resizeNotAllowed", + 404: "itemNotFound", + 405: "badMethod", + 409: "inProgress", + 413: "overLimit", + 415: "badMediaType", + 501: "notImplemented", + 503: "serviceUnavailable"} + + def __init__(self, exception): + """Create a Fault for the given webob.exc.exception.""" + self.wrapped_exc = exception + + @webob.dec.wsgify + def __call__(self, req): + """Generate a WSGI response based on the exception passed to ctor.""" + # Replace the body with fault details. + code = self.wrapped_exc.status_int + fault_name = self._fault_names.get(code, "cloudServersFault") + fault_data = { + fault_name: { + 'code': code, + 'message': self.wrapped_exc.explanation}} + if code == 413: + retry = self.wrapped_exc.headers['Retry-After'] + fault_data[fault_name]['retryAfter'] = retry + # 'code' is an attribute on the fault tag itself + metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} + serializer = wsgi.Serializer(req.environ, metadata) + self.wrapped_exc.body = serializer.to_content_type(fault_data) + return self.wrapped_exc diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py new file mode 100644 index 000000000..793984a5d --- /dev/null +++ b/nova/api/openstack/flavors.py @@ -0,0 +1,58 @@ +# 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 webob import exc + +from nova.api.openstack import faults +from nova.compute import instance_types +from nova import wsgi +import nova.api.openstack + +class Controller(wsgi.Controller): + """Flavor controller for the OpenStack 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()] + items = nova.api.openstack.limited(items, req) + 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 faults.Fault(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/openstack/images.py b/nova/api/openstack/images.py new file mode 100644 index 000000000..aa438739c --- /dev/null +++ b/nova/api/openstack/images.py @@ -0,0 +1,71 @@ +# 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 webob import exc + +from nova import flags +from nova import utils +from nova import wsgi +import nova.api.openstack +import nova.image.service +from nova.api.openstack import faults + + +FLAGS = flags.FLAGS + +class Controller(wsgi.Controller): + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "image": [ "id", "name", "updated", "created", "status", + "serverId", "progress" ] + } + } + } + + def __init__(self): + self._service = utils.import_object(FLAGS.image_service) + + 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() + data = nova.api.openstack.limited(data, req) + return dict(images=data) + + def show(self, req, id): + """Return data about the given image id.""" + return dict(image=self._service.show(id)) + + def delete(self, req, id): + # Only public images are supported for now. + raise faults.Fault(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 faults.Fault(exc.HTTPNotFound()) + + def update(self, req, id): + # Users may not modify public images, and that's all that + # we support for now. + raise faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/openstack/notes.txt b/nova/api/openstack/notes.txt new file mode 100644 index 000000000..2330f1002 --- /dev/null +++ b/nova/api/openstack/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. + +OpenstackAPITranslationStore: +translates RS server/images/flavor/etc ids into formats required +by a given ImageService strategy. + +api.openstack.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/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py new file mode 100644 index 000000000..f843bac0f --- /dev/null +++ b/nova/api/openstack/ratelimiting/__init__.py @@ -0,0 +1,122 @@ +"""Rate limiting of arbitrary actions.""" + +import httplib +import time +import urllib +import webob.dec +import webob.exc + + +# Convenience constants for the limits dictionary passed to Limiter(). +PER_SECOND = 1 +PER_MINUTE = 60 +PER_HOUR = 60 * 60 +PER_DAY = 60 * 60 * 24 + +class Limiter(object): + + """Class providing rate limiting of arbitrary actions.""" + + def __init__(self, limits): + """Create a rate limiter. + + limits: a dict mapping from action name to a tuple. The tuple contains + the number of times the action may be performed, and the time period + (in seconds) during which the number must not be exceeded for this + action. Example: dict(reboot=(10, ratelimiting.PER_MINUTE)) would + allow 10 'reboot' actions per minute. + """ + self.limits = limits + self._levels = {} + + def perform(self, action_name, username='nobody'): + """Attempt to perform an action by the given username. + + action_name: the string name of the action to perform. This must + be a key in the limits dict passed to the ctor. + + username: an optional string name of the user performing the action. + Each user has her own set of rate limiting counters. Defaults to + 'nobody' (so that if you never specify a username when calling + perform(), a single set of counters will be used.) + + Return None if the action may proceed. If the action may not proceed + because it has been rate limited, return the float number of seconds + until the action would succeed. + """ + # Think of rate limiting as a bucket leaking water at 1cc/second. The + # bucket can hold as many ccs as there are seconds in the rate + # limiting period (e.g. 3600 for per-hour ratelimits), and if you can + # perform N actions in that time, each action fills the bucket by + # 1/Nth of its volume. You may only perform an action if the bucket + # would not overflow. + now = time.time() + key = '%s:%s' % (username, action_name) + last_time_performed, water_level = self._levels.get(key, (now, 0)) + # The bucket leaks 1cc/second. + water_level -= (now - last_time_performed) + if water_level < 0: + water_level = 0 + num_allowed_per_period, period_in_secs = self.limits[action_name] + # Fill the bucket by 1/Nth its capacity, and hope it doesn't overflow. + capacity = period_in_secs + new_level = water_level + (capacity * 1.0 / num_allowed_per_period) + if new_level > capacity: + # Delay this many seconds. + return new_level - capacity + self._levels[key] = (now, new_level) + return None + + +# If one instance of this WSGIApps is unable to handle your load, put a +# sharding app in front that shards by username to one of many backends. + +class WSGIApp(object): + + """Application that tracks rate limits in memory. Send requests to it of + this form: + + POST /limiter// + + and receive a 200 OK, or a 403 Forbidden with an X-Wait-Seconds header + containing the number of seconds to wait before the action would succeed. + """ + + def __init__(self, limiter): + """Create the WSGI application using the given Limiter instance.""" + self.limiter = limiter + + @webob.dec.wsgify + def __call__(self, req): + parts = req.path_info.split('/') + # format: /limiter// + if req.method != 'POST': + raise webob.exc.HTTPMethodNotAllowed() + if len(parts) != 4 or parts[1] != 'limiter': + raise webob.exc.HTTPNotFound() + username = parts[2] + action_name = urllib.unquote(parts[3]) + delay = self.limiter.perform(action_name, username) + if delay: + return webob.exc.HTTPForbidden( + headers={'X-Wait-Seconds': "%.2f" % delay}) + else: + return '' # 200 OK + + +class WSGIAppProxy(object): + + """Limiter lookalike that proxies to a ratelimiting.WSGIApp.""" + + def __init__(self, service_host): + """Creates a proxy pointing to a ratelimiting.WSGIApp at the given + host.""" + self.service_host = service_host + + def perform(self, action, username='nobody'): + conn = httplib.HTTPConnection(self.service_host) + conn.request('POST', '/limiter/%s/%s' % (username, action)) + resp = conn.getresponse() + if resp.status == 200: + return None # no delay + return float(resp.getheader('X-Wait-Seconds')) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py new file mode 100644 index 000000000..f234af7de --- /dev/null +++ b/nova/api/openstack/servers.py @@ -0,0 +1,276 @@ +# 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 time + +import webob +from webob import exc + +from nova import flags +from nova import rpc +from nova import utils +from nova import wsgi +from nova.api import cloud +from nova.api.openstack import context +from nova.api.openstack import faults +from nova.compute import instance_types +from nova.compute import power_state +import nova.api.openstack +import nova.image.service + +FLAGS = flags.FLAGS + +def _filter_params(inst_dict): + """ Extracts all updatable parameters for a server update request """ + keys = dict(name='name', admin_pass='adminPass') + new_attrs = {} + for k, v in keys.items(): + if inst_dict.has_key(v): + new_attrs[k] = inst_dict[v] + return new_attrs + +def _entity_list(entities): + """ Coerces a list of servers into proper dictionary format """ + return dict(servers=entities) + +def _entity_detail(inst): + """ Maps everything to valid attributes for return""" + power_mapping = { + power_state.NOSTATE: 'build', + power_state.RUNNING: 'active', + power_state.BLOCKED: 'active', + power_state.PAUSED: 'suspended', + power_state.SHUTDOWN: 'active', + power_state.SHUTOFF: 'active', + power_state.CRASHED: 'error' + } + inst_dict = {} + + mapped_keys = dict(status='state', imageId='image_id', + flavorId='instance_type', name='server_name', id='id') + + for k, v in mapped_keys.iteritems(): + inst_dict[k] = inst[v] + + inst_dict['status'] = power_mapping[inst_dict['status']] + inst_dict['addresses'] = dict(public=[], private=[]) + inst_dict['metadata'] = {} + inst_dict['hostId'] = '' + + return dict(server=inst_dict) + +def _entity_inst(inst): + """ Filters all model attributes save for id and name """ + return dict(server=dict(id=inst['id'], name=inst['server_name'])) + +class Controller(wsgi.Controller): + """ The Server API controller for the OpenStack API """ + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "server": [ "id", "imageId", "name", "flavorId", "hostId", + "status", "progress", "progress" ] + } + } + } + + def __init__(self, db_driver=None): + if not db_driver: + db_driver = FLAGS.db_driver + self.db_driver = utils.import_object(db_driver) + super(Controller, self).__init__() + + def index(self, req): + """ Returns a list of server names and ids for a given user """ + return self._items(req, entity_maker=_entity_inst) + + def detail(self, req): + """ Returns a list of server details for a given user """ + return self._items(req, entity_maker=_entity_detail) + + def _items(self, req, entity_maker): + """Returns a list of servers for a given user. + + entity_maker - either _entity_detail or _entity_inst + """ + user_id = req.environ['nova.context']['user']['id'] + instance_list = self.db_driver.instance_get_all_by_user(None, user_id) + limited_list = nova.api.openstack.limited(instance_list, req) + res = [entity_maker(inst)['server'] for inst in limited_list] + return _entity_list(res) + + def show(self, req, id): + """ Returns server details by server id """ + user_id = req.environ['nova.context']['user']['id'] + inst = self.db_driver.instance_get_by_internal_id(None, int(id)) + if inst: + if inst.user_id == user_id: + return _entity_detail(inst) + raise faults.Fault(exc.HTTPNotFound()) + + def delete(self, req, id): + """ Destroys a server """ + user_id = req.environ['nova.context']['user']['id'] + instance = self.db_driver.instance_get_by_internal_id(None, int(id)) + if instance and instance['user_id'] == user_id: + self.db_driver.instance_destroy(None, id) + return faults.Fault(exc.HTTPAccepted()) + return faults.Fault(exc.HTTPNotFound()) + + def create(self, req): + """ Creates a new server for a given user """ + + env = self._deserialize(req.body, req) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + #try: + inst = self._build_server_instance(req, env) + #except Exception, e: + # return faults.Fault(exc.HTTPUnprocessableEntity()) + + rpc.cast( + FLAGS.compute_topic, { + "method": "run_instance", + "args": {"instance_id": inst['id']}}) + return _entity_inst(inst) + + def update(self, req, id): + """ Updates the server name or password """ + user_id = req.environ['nova.context']['user']['id'] + + inst_dict = self._deserialize(req.body, req) + + if not inst_dict: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + instance = self.db_driver.instance_get_by_internal_id(None, int(id)) + if not instance or instance.user_id != user_id: + return faults.Fault(exc.HTTPNotFound()) + + self.db_driver.instance_update(None, int(id), + _filter_params(inst_dict['server'])) + return faults.Fault(exc.HTTPNoContent()) + + def action(self, req, id): + """ multi-purpose method used to reboot, rebuild, and + resize a server """ + user_id = req.environ['nova.context']['user']['id'] + input_dict = self._deserialize(req.body, req) + try: + reboot_type = input_dict['reboot']['type'] + except Exception: + raise faults.Fault(webob.exc.HTTPNotImplemented()) + inst_ref = self.db.instance_get_by_internal_id(None, int(id)) + if not inst_ref or (inst_ref and not inst_ref.user_id == user_id): + return faults.Fault(exc.HTTPUnprocessableEntity()) + cloud.reboot(id) + + def _build_server_instance(self, req, env): + """Build instance data structure and save it to the data store.""" + ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) + inst = {} + + user_id = req.environ['nova.context']['user']['id'] + + flavor_id = env['server']['flavorId'] + + instance_type, flavor = [(k, v) for k, v in + instance_types.INSTANCE_TYPES.iteritems() + if v['flavorid'] == flavor_id][0] + + image_id = env['server']['imageId'] + + img_service = utils.import_object(FLAGS.image_service) + + image = img_service.show(image_id) + + if not image: + raise Exception, "Image not found" + + inst['server_name'] = env['server']['name'] + inst['image_id'] = image_id + inst['user_id'] = user_id + inst['launch_time'] = ltime + inst['mac_address'] = utils.generate_mac() + inst['project_id'] = user_id + + inst['state_description'] = 'scheduling' + inst['kernel_id'] = image.get('kernelId', FLAGS.default_kernel) + inst['ramdisk_id'] = image.get('ramdiskId', FLAGS.default_ramdisk) + inst['reservation_id'] = utils.generate_uid('r') + + inst['display_name'] = env['server']['name'] + inst['display_description'] = env['server']['name'] + + #TODO(dietz) this may be ill advised + key_pair_ref = self.db_driver.key_pair_get_all_by_user( + None, user_id)[0] + + inst['key_data'] = key_pair_ref['public_key'] + inst['key_name'] = key_pair_ref['name'] + + #TODO(dietz) stolen from ec2 api, see TODO there + inst['security_group'] = 'default' + + # Flavor related attributes + inst['instance_type'] = instance_type + inst['memory_mb'] = flavor['memory_mb'] + inst['vcpus'] = flavor['vcpus'] + inst['local_gb'] = flavor['local_gb'] + + ref = self.db_driver.instance_create(None, inst) + inst['id'] = ref.internal_id + + # TODO(dietz): this isn't explicitly necessary, but the networking + # calls depend on an object with a project_id property, and therefore + # should be cleaned up later + api_context = context.APIRequestContext(user_id) + + inst['mac_address'] = utils.generate_mac() + + #TODO(dietz) is this necessary? + inst['launch_index'] = 0 + + inst['hostname'] = str(ref.internal_id) + self.db_driver.instance_update(None, inst['id'], inst) + + network_manager = utils.import_object(FLAGS.network_manager) + address = network_manager.allocate_fixed_ip(api_context, + inst['id']) + + # TODO(vish): This probably should be done in the scheduler + # network is setup when host is assigned + network_topic = self._get_network_topic(user_id) + rpc.call(network_topic, + {"method": "setup_fixed_ip", + "args": {"context": None, + "address": address}}) + return inst + + def _get_network_topic(self, user_id): + """Retrieves the network host for a project""" + network_ref = self.db_driver.project_get_network(None, + user_id) + host = network_ref['host'] + if not host: + host = rpc.call(FLAGS.network_topic, + {"method": "set_network_host", + "args": {"context": None, + "project_id": user_id}}) + return self.db_driver.queue_get_for(None, FLAGS.network_topic, host) diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py new file mode 100644 index 000000000..4d2d0ede1 --- /dev/null +++ b/nova/api/openstack/sharedipgroups.py @@ -0,0 +1,20 @@ +# 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): pass diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py deleted file mode 100644 index 89a4693ad..000000000 --- a/nova/api/rackspace/__init__.py +++ /dev/null @@ -1,190 +0,0 @@ -# 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 -import webob - -from nova import flags -from nova import utils -from nova import wsgi -from nova.api.rackspace import faults -from nova.api.rackspace import backup_schedules -from nova.api.rackspace import flavors -from nova.api.rackspace import images -from nova.api.rackspace import ratelimiting -from nova.api.rackspace import servers -from nova.api.rackspace import sharedipgroups -from nova.auth import manager - - -FLAGS = flags.FLAGS -flags.DEFINE_string('nova_api_auth', - 'nova.api.rackspace.auth.BasicApiAuthManager', - 'The auth mechanism to use for the Rackspace API implemenation') - -class API(wsgi.Middleware): - """WSGI entry point for all Rackspace API requests.""" - - def __init__(self): - app = AuthMiddleware(RateLimitingMiddleware(APIRouter())) - super(API, self).__init__(app) - -class AuthMiddleware(wsgi.Middleware): - """Authorize the rackspace API request or return an HTTP Forbidden.""" - - def __init__(self, application): - self.auth_driver = utils.import_class(FLAGS.nova_api_auth)() - super(AuthMiddleware, self).__init__(application) - - @webob.dec.wsgify - def __call__(self, req): - if not req.headers.has_key("X-Auth-Token"): - return self.auth_driver.authenticate(req) - - user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) - - if not user: - return faults.Fault(webob.exc.HTTPUnauthorized()) - - if not req.environ.has_key('nova.context'): - req.environ['nova.context'] = {} - req.environ['nova.context']['user'] = user - return self.application - -class RateLimitingMiddleware(wsgi.Middleware): - """Rate limit incoming requests according to the OpenStack rate limits.""" - - def __init__(self, application, service_host=None): - """Create a rate limiting middleware that wraps the given application. - - By default, rate counters are stored in memory. If service_host is - specified, the middleware instead relies on the ratelimiting.WSGIApp - at the given host+port to keep rate counters. - """ - super(RateLimitingMiddleware, self).__init__(application) - if not service_host: - #TODO(gundlach): These limits were based on limitations of Cloud - #Servers. We should revisit them in Nova. - self.limiter = ratelimiting.Limiter(limits={ - 'DELETE': (100, ratelimiting.PER_MINUTE), - 'PUT': (10, ratelimiting.PER_MINUTE), - 'POST': (10, ratelimiting.PER_MINUTE), - 'POST servers': (50, ratelimiting.PER_DAY), - 'GET changes-since': (3, ratelimiting.PER_MINUTE), - }) - else: - self.limiter = ratelimiting.WSGIAppProxy(service_host) - - @webob.dec.wsgify - def __call__(self, req): - """Rate limit the request. - - If the request should be rate limited, return a 413 status with a - Retry-After header giving the time when the request would succeed. - """ - username = req.headers['X-Auth-User'] - action_name = self.get_action_name(req) - if not action_name: # not rate limited - return self.application - delay = self.get_delay(action_name, username) - if delay: - # TODO(gundlach): Get the retry-after format correct. - exc = webob.exc.HTTPRequestEntityTooLarge( - explanation='Too many requests.', - headers={'Retry-After': time.time() + delay}) - raise faults.Fault(exc) - return self.application - - def get_delay(self, action_name, username): - """Return the delay for the given action and username, or None if - the action would not be rate limited. - """ - if action_name == 'POST servers': - # "POST servers" is a POST, so it counts against "POST" too. - # Attempt the "POST" first, lest we are rate limited by "POST" but - # use up a precious "POST servers" call. - delay = self.limiter.perform("POST", username=username) - if delay: - return delay - return self.limiter.perform(action_name, username=username) - - def get_action_name(self, req): - """Return the action name for this request.""" - if req.method == 'GET' and 'changes-since' in req.GET: - return 'GET changes-since' - if req.method == 'POST' and req.path_info.startswith('/servers'): - return 'POST servers' - if req.method in ['PUT', 'POST', 'DELETE']: - return req.method - return None - - -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(), - collection={ 'detail': 'GET'}, - member={'action':'POST'}) - - mapper.resource("backup_schedule", "backup_schedules", - controller=backup_schedules.Controller(), - parent_resource=dict(member_name='server', - collection_name = 'servers')) - - 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) - - -def limited(items, req): - """Return a slice of items according to requested offset and limit. - - items - a sliceable - req - wobob.Request possibly containing offset and limit GET variables. - offset is where to start in the list, and limit is the maximum number - of items to return. - - If limit is not specified, 0, or > 1000, defaults to 1000. - """ - offset = int(req.GET.get('offset', 0)) - limit = int(req.GET.get('limit', 0)) - if not limit: - limit = 1000 - limit = min(1000, limit) - range_end = offset + limit - return items[offset:range_end] - diff --git a/nova/api/rackspace/_id_translator.py b/nova/api/rackspace/_id_translator.py deleted file mode 100644 index 333aa8434..000000000 --- a/nova/api/rackspace/_id_translator.py +++ /dev/null @@ -1,42 +0,0 @@ -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, 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/auth.py b/nova/api/rackspace/auth.py deleted file mode 100644 index c45156ebd..000000000 --- a/nova/api/rackspace/auth.py +++ /dev/null @@ -1,101 +0,0 @@ -import datetime -import hashlib -import json -import time - -import webob.exc -import webob.dec - -from nova import auth -from nova import db -from nova import flags -from nova import manager -from nova import utils -from nova.api.rackspace import faults - -FLAGS = flags.FLAGS - -class Context(object): - pass - -class BasicApiAuthManager(object): - """ Implements a somewhat rudimentary version of Rackspace Auth""" - - def __init__(self, host=None, db_driver=None): - if not host: - host = FLAGS.host - self.host = host - if not db_driver: - db_driver = FLAGS.db_driver - self.db = utils.import_object(db_driver) - self.auth = auth.manager.AuthManager() - self.context = Context() - super(BasicApiAuthManager, self).__init__() - - def authenticate(self, req): - # Unless the request is explicitly made against // don't - # honor it - path_info = req.path_info - if len(path_info) > 1: - return faults.Fault(webob.exc.HTTPUnauthorized()) - - try: - username, key = req.headers['X-Auth-User'], \ - req.headers['X-Auth-Key'] - except KeyError: - return faults.Fault(webob.exc.HTTPUnauthorized()) - - username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] - token, user = self._authorize_user(username, key) - if user and token: - res = webob.Response() - res.headers['X-Auth-Token'] = token['token_hash'] - res.headers['X-Server-Management-Url'] = \ - token['server_management_url'] - res.headers['X-Storage-Url'] = token['storage_url'] - res.headers['X-CDN-Management-Url'] = token['cdn_management_url'] - res.content_type = 'text/plain' - res.status = '204' - return res - else: - return faults.Fault(webob.exc.HTTPUnauthorized()) - - def authorize_token(self, token_hash): - """ retrieves user information from the datastore given a token - - If the token has expired, returns None - If the token is not found, returns None - Otherwise returns the token - - This method will also remove the token if the timestamp is older than - 2 days ago. - """ - token = self.db.auth_get_token(self.context, token_hash) - if token: - delta = datetime.datetime.now() - token['created_at'] - if delta.days >= 2: - self.db.auth_destroy_token(self.context, token) - else: - user = self.auth.get_user(token['user_id']) - return { 'id':user['uid'] } - return None - - def _authorize_user(self, username, key): - """ Generates a new token and assigns it to a user """ - user = self.auth.get_user_from_access_key(key) - if user and user['name'] == username: - token_hash = hashlib.sha1('%s%s%f' % (username, key, - time.time())).hexdigest() - token = {} - token['token_hash'] = token_hash - token['cdn_management_url'] = '' - token['server_management_url'] = self._get_server_mgmt_url() - token['storage_url'] = '' - token['user_id'] = user['uid'] - self.db.auth_create_token(self.context, token) - return token, user - return None, None - - def _get_server_mgmt_url(self): - return 'https://%s/v1.0/' % self.host - diff --git a/nova/api/rackspace/backup_schedules.py b/nova/api/rackspace/backup_schedules.py deleted file mode 100644 index 9c0d41fa0..000000000 --- a/nova/api/rackspace/backup_schedules.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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 time -from webob import exc - -from nova import wsgi -from nova.api.rackspace import faults -import nova.image.service - -class Controller(wsgi.Controller): - def __init__(self): - pass - - def index(self, req, server_id): - return faults.Fault(exc.HTTPNotFound()) - - def create(self, req, server_id): - """ No actual update method required, since the existing API allows - both create and update through a POST """ - return faults.Fault(exc.HTTPNotFound()) - - def delete(self, req, server_id): - return faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/rackspace/context.py b/nova/api/rackspace/context.py deleted file mode 100644 index 77394615b..000000000 --- a/nova/api/rackspace/context.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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. - -""" -APIRequestContext -""" - -import random - -class Project(object): - def __init__(self, user_id): - self.id = user_id - -class APIRequestContext(object): - """ This is an adapter class to get around all of the assumptions made in - the FlatNetworking """ - def __init__(self, user_id): - self.user_id = user_id - self.project = Project(user_id) diff --git a/nova/api/rackspace/faults.py b/nova/api/rackspace/faults.py deleted file mode 100644 index 32e5c866f..000000000 --- a/nova/api/rackspace/faults.py +++ /dev/null @@ -1,62 +0,0 @@ -# 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 webob.dec -import webob.exc - -from nova import wsgi - - -class Fault(webob.exc.HTTPException): - - """An RS API fault response.""" - - _fault_names = { - 400: "badRequest", - 401: "unauthorized", - 403: "resizeNotAllowed", - 404: "itemNotFound", - 405: "badMethod", - 409: "inProgress", - 413: "overLimit", - 415: "badMediaType", - 501: "notImplemented", - 503: "serviceUnavailable"} - - def __init__(self, exception): - """Create a Fault for the given webob.exc.exception.""" - self.wrapped_exc = exception - - @webob.dec.wsgify - def __call__(self, req): - """Generate a WSGI response based on the exception passed to ctor.""" - # Replace the body with fault details. - code = self.wrapped_exc.status_int - fault_name = self._fault_names.get(code, "cloudServersFault") - fault_data = { - fault_name: { - 'code': code, - 'message': self.wrapped_exc.explanation}} - if code == 413: - retry = self.wrapped_exc.headers['Retry-After'] - fault_data[fault_name]['retryAfter'] = retry - # 'code' is an attribute on the fault tag itself - metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} - serializer = wsgi.Serializer(req.environ, metadata) - self.wrapped_exc.body = serializer.to_content_type(fault_data) - return self.wrapped_exc diff --git a/nova/api/rackspace/flavors.py b/nova/api/rackspace/flavors.py deleted file mode 100644 index 916449854..000000000 --- a/nova/api/rackspace/flavors.py +++ /dev/null @@ -1,58 +0,0 @@ -# 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 webob import exc - -from nova.api.rackspace import faults -from nova.compute import instance_types -from nova import wsgi -import nova.api.rackspace - -class Controller(wsgi.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()] - items = nova.api.rackspace.limited(items, req) - 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 faults.Fault(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 deleted file mode 100644 index 82dcd2049..000000000 --- a/nova/api/rackspace/images.py +++ /dev/null @@ -1,71 +0,0 @@ -# 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 webob import exc - -from nova import flags -from nova import utils -from nova import wsgi -import nova.api.rackspace -import nova.image.service -from nova.api.rackspace import faults - - -FLAGS = flags.FLAGS - -class Controller(wsgi.Controller): - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "image": [ "id", "name", "updated", "created", "status", - "serverId", "progress" ] - } - } - } - - def __init__(self): - self._service = utils.import_object(FLAGS.image_service) - - 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() - data = nova.api.rackspace.limited(data, req) - return dict(images=data) - - def show(self, req, id): - """Return data about the given image id.""" - return dict(image=self._service.show(id)) - - def delete(self, req, id): - # Only public images are supported for now. - raise faults.Fault(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 faults.Fault(exc.HTTPNotFound()) - - def update(self, req, id): - # Users may not modify public images, and that's all that - # we support for now. - raise faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/rackspace/notes.txt b/nova/api/rackspace/notes.txt deleted file mode 100644 index e133bf5ea..000000000 --- a/nova/api/rackspace/notes.txt +++ /dev/null @@ -1,23 +0,0 @@ -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/ratelimiting/__init__.py b/nova/api/rackspace/ratelimiting/__init__.py deleted file mode 100644 index f843bac0f..000000000 --- a/nova/api/rackspace/ratelimiting/__init__.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Rate limiting of arbitrary actions.""" - -import httplib -import time -import urllib -import webob.dec -import webob.exc - - -# Convenience constants for the limits dictionary passed to Limiter(). -PER_SECOND = 1 -PER_MINUTE = 60 -PER_HOUR = 60 * 60 -PER_DAY = 60 * 60 * 24 - -class Limiter(object): - - """Class providing rate limiting of arbitrary actions.""" - - def __init__(self, limits): - """Create a rate limiter. - - limits: a dict mapping from action name to a tuple. The tuple contains - the number of times the action may be performed, and the time period - (in seconds) during which the number must not be exceeded for this - action. Example: dict(reboot=(10, ratelimiting.PER_MINUTE)) would - allow 10 'reboot' actions per minute. - """ - self.limits = limits - self._levels = {} - - def perform(self, action_name, username='nobody'): - """Attempt to perform an action by the given username. - - action_name: the string name of the action to perform. This must - be a key in the limits dict passed to the ctor. - - username: an optional string name of the user performing the action. - Each user has her own set of rate limiting counters. Defaults to - 'nobody' (so that if you never specify a username when calling - perform(), a single set of counters will be used.) - - Return None if the action may proceed. If the action may not proceed - because it has been rate limited, return the float number of seconds - until the action would succeed. - """ - # Think of rate limiting as a bucket leaking water at 1cc/second. The - # bucket can hold as many ccs as there are seconds in the rate - # limiting period (e.g. 3600 for per-hour ratelimits), and if you can - # perform N actions in that time, each action fills the bucket by - # 1/Nth of its volume. You may only perform an action if the bucket - # would not overflow. - now = time.time() - key = '%s:%s' % (username, action_name) - last_time_performed, water_level = self._levels.get(key, (now, 0)) - # The bucket leaks 1cc/second. - water_level -= (now - last_time_performed) - if water_level < 0: - water_level = 0 - num_allowed_per_period, period_in_secs = self.limits[action_name] - # Fill the bucket by 1/Nth its capacity, and hope it doesn't overflow. - capacity = period_in_secs - new_level = water_level + (capacity * 1.0 / num_allowed_per_period) - if new_level > capacity: - # Delay this many seconds. - return new_level - capacity - self._levels[key] = (now, new_level) - return None - - -# If one instance of this WSGIApps is unable to handle your load, put a -# sharding app in front that shards by username to one of many backends. - -class WSGIApp(object): - - """Application that tracks rate limits in memory. Send requests to it of - this form: - - POST /limiter// - - and receive a 200 OK, or a 403 Forbidden with an X-Wait-Seconds header - containing the number of seconds to wait before the action would succeed. - """ - - def __init__(self, limiter): - """Create the WSGI application using the given Limiter instance.""" - self.limiter = limiter - - @webob.dec.wsgify - def __call__(self, req): - parts = req.path_info.split('/') - # format: /limiter// - if req.method != 'POST': - raise webob.exc.HTTPMethodNotAllowed() - if len(parts) != 4 or parts[1] != 'limiter': - raise webob.exc.HTTPNotFound() - username = parts[2] - action_name = urllib.unquote(parts[3]) - delay = self.limiter.perform(action_name, username) - if delay: - return webob.exc.HTTPForbidden( - headers={'X-Wait-Seconds': "%.2f" % delay}) - else: - return '' # 200 OK - - -class WSGIAppProxy(object): - - """Limiter lookalike that proxies to a ratelimiting.WSGIApp.""" - - def __init__(self, service_host): - """Creates a proxy pointing to a ratelimiting.WSGIApp at the given - host.""" - self.service_host = service_host - - def perform(self, action, username='nobody'): - conn = httplib.HTTPConnection(self.service_host) - conn.request('POST', '/limiter/%s/%s' % (username, action)) - resp = conn.getresponse() - if resp.status == 200: - return None # no delay - return float(resp.getheader('X-Wait-Seconds')) diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py deleted file mode 100644 index 4c9510917..000000000 --- a/nova/api/rackspace/ratelimiting/tests.py +++ /dev/null @@ -1,237 +0,0 @@ -import httplib -import StringIO -import time -import unittest -import webob - -import nova.api.rackspace.ratelimiting as ratelimiting - -class LimiterTest(unittest.TestCase): - - def setUp(self): - self.limits = { - 'a': (5, ratelimiting.PER_SECOND), - 'b': (5, ratelimiting.PER_MINUTE), - 'c': (5, ratelimiting.PER_HOUR), - 'd': (1, ratelimiting.PER_SECOND), - 'e': (100, ratelimiting.PER_SECOND)} - self.rl = ratelimiting.Limiter(self.limits) - - def exhaust(self, action, times_until_exhausted, **kwargs): - for i in range(times_until_exhausted): - when = self.rl.perform(action, **kwargs) - self.assertEqual(when, None) - num, period = self.limits[action] - delay = period * 1.0 / num - # Verify that we are now thoroughly delayed - for i in range(10): - when = self.rl.perform(action, **kwargs) - self.assertAlmostEqual(when, delay, 2) - - def test_second(self): - self.exhaust('a', 5) - time.sleep(0.2) - self.exhaust('a', 1) - time.sleep(1) - self.exhaust('a', 5) - - def test_minute(self): - self.exhaust('b', 5) - - def test_one_per_period(self): - def allow_once_and_deny_once(): - when = self.rl.perform('d') - self.assertEqual(when, None) - when = self.rl.perform('d') - self.assertAlmostEqual(when, 1, 2) - return when - time.sleep(allow_once_and_deny_once()) - time.sleep(allow_once_and_deny_once()) - allow_once_and_deny_once() - - def test_we_can_go_indefinitely_if_we_spread_out_requests(self): - for i in range(200): - when = self.rl.perform('e') - self.assertEqual(when, None) - time.sleep(0.01) - - def test_users_get_separate_buckets(self): - self.exhaust('c', 5, username='alice') - self.exhaust('c', 5, username='bob') - self.exhaust('c', 5, username='chuck') - self.exhaust('c', 0, username='chuck') - self.exhaust('c', 0, username='bob') - self.exhaust('c', 0, username='alice') - - -class FakeLimiter(object): - """Fake Limiter class that you can tell how to behave.""" - def __init__(self, test): - self._action = self._username = self._delay = None - self.test = test - def mock(self, action, username, delay): - self._action = action - self._username = username - self._delay = delay - def perform(self, action, username): - self.test.assertEqual(action, self._action) - self.test.assertEqual(username, self._username) - return self._delay - - -class WSGIAppTest(unittest.TestCase): - - def setUp(self): - self.limiter = FakeLimiter(self) - self.app = ratelimiting.WSGIApp(self.limiter) - - def test_invalid_methods(self): - requests = [] - for method in ['GET', 'PUT', 'DELETE']: - req = webob.Request.blank('/limits/michael/breakdance', - dict(REQUEST_METHOD=method)) - requests.append(req) - for req in requests: - self.assertEqual(req.get_response(self.app).status_int, 405) - - def test_invalid_urls(self): - requests = [] - for prefix in ['limit', '', 'limiter2', 'limiter/limits', 'limiter/1']: - req = webob.Request.blank('/%s/michael/breakdance' % prefix, - dict(REQUEST_METHOD='POST')) - requests.append(req) - for req in requests: - self.assertEqual(req.get_response(self.app).status_int, 404) - - def verify(self, url, username, action, delay=None): - """Make sure that POSTing to the given url causes the given username - to perform the given action. Make the internal rate limiter return - delay and make sure that the WSGI app returns the correct response. - """ - req = webob.Request.blank(url, dict(REQUEST_METHOD='POST')) - self.limiter.mock(action, username, delay) - resp = req.get_response(self.app) - if not delay: - self.assertEqual(resp.status_int, 200) - else: - self.assertEqual(resp.status_int, 403) - self.assertEqual(resp.headers['X-Wait-Seconds'], "%.2f" % delay) - - def test_good_urls(self): - self.verify('/limiter/michael/hoot', 'michael', 'hoot') - - def test_escaping(self): - self.verify('/limiter/michael/jump%20up', 'michael', 'jump up') - - def test_response_to_delays(self): - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1) - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1.56) - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) - - -class FakeHttplibSocket(object): - """a fake socket implementation for httplib.HTTPResponse, trivial""" - - def __init__(self, response_string): - self._buffer = StringIO.StringIO(response_string) - - def makefile(self, _mode, _other): - """Returns the socket's internal buffer""" - return self._buffer - - -class FakeHttplibConnection(object): - """A fake httplib.HTTPConnection - - Requests made via this connection actually get translated and routed into - our WSGI app, we then wait for the response and turn it back into - an httplib.HTTPResponse. - """ - def __init__(self, app, host, is_secure=False): - self.app = app - self.host = host - - def request(self, method, path, data='', headers={}): - req = webob.Request.blank(path) - req.method = method - req.body = data - req.headers = headers - req.host = self.host - # Call the WSGI app, get the HTTP response - resp = str(req.get_response(self.app)) - # For some reason, the response doesn't have "HTTP/1.0 " prepended; I - # guess that's a function the web server usually provides. - resp = "HTTP/1.0 %s" % resp - sock = FakeHttplibSocket(resp) - self.http_response = httplib.HTTPResponse(sock) - self.http_response.begin() - - def getresponse(self): - return self.http_response - - -def wire_HTTPConnection_to_WSGI(host, app): - """Monkeypatches HTTPConnection so that if you try to connect to host, you - are instead routed straight to the given WSGI app. - - After calling this method, when any code calls - - httplib.HTTPConnection(host) - - the connection object will be a fake. Its requests will be sent directly - to the given WSGI app rather than through a socket. - - Code connecting to hosts other than host will not be affected. - - This method may be called multiple times to map different hosts to - different apps. - """ - class HTTPConnectionDecorator(object): - """Wraps the real HTTPConnection class so that when you instantiate - the class you might instead get a fake instance.""" - def __init__(self, wrapped): - self.wrapped = wrapped - def __call__(self, connection_host, *args, **kwargs): - if connection_host == host: - return FakeHttplibConnection(app, host) - else: - return self.wrapped(connection_host, *args, **kwargs) - httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) - - -class WSGIAppProxyTest(unittest.TestCase): - - def setUp(self): - """Our WSGIAppProxy is going to call across an HTTPConnection to a - WSGIApp running a limiter. The proxy will send input, and the proxy - should receive that same input, pass it to the limiter who gives a - result, and send the expected result back. - - The HTTPConnection isn't real -- it's monkeypatched to point straight - at the WSGIApp. And the limiter isn't real -- it's a fake that - behaves the way we tell it to. - """ - self.limiter = FakeLimiter(self) - app = ratelimiting.WSGIApp(self.limiter) - wire_HTTPConnection_to_WSGI('100.100.100.100:80', app) - self.proxy = ratelimiting.WSGIAppProxy('100.100.100.100:80') - - def test_200(self): - self.limiter.mock('conquer', 'caesar', None) - when = self.proxy.perform('conquer', 'caesar') - self.assertEqual(when, None) - - def test_403(self): - self.limiter.mock('grumble', 'proletariat', 1.5) - when = self.proxy.perform('grumble', 'proletariat') - self.assertEqual(when, 1.5) - - def test_failure(self): - def shouldRaise(): - self.limiter.mock('murder', 'brutus', None) - self.proxy.perform('stab', 'brutus') - self.assertRaises(AssertionError, shouldRaise) - - -if __name__ == '__main__': - unittest.main() diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py deleted file mode 100644 index 8c489ed83..000000000 --- a/nova/api/rackspace/servers.py +++ /dev/null @@ -1,283 +0,0 @@ -# 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 time - -import webob -from webob import exc - -from nova import flags -from nova import rpc -from nova import utils -from nova import wsgi -from nova.api import cloud -from nova.api.rackspace import context -from nova.api.rackspace import faults -from nova.compute import instance_types -from nova.compute import power_state -import nova.api.rackspace -import nova.image.service - -FLAGS = flags.FLAGS - -def _image_service(): - """ Helper method for initializing the image id translator """ - service = utils.import_object(FLAGS.image_service) - return (service, _id_translator.RackspaceAPIIdTranslator( - "image", service.__class__.__name__)) - -def _filter_params(inst_dict): - """ Extracts all updatable parameters for a server update request """ - keys = dict(name='name', admin_pass='adminPass') - new_attrs = {} - for k, v in keys.items(): - if inst_dict.has_key(v): - new_attrs[k] = inst_dict[v] - return new_attrs - -def _entity_list(entities): - """ Coerces a list of servers into proper dictionary format """ - return dict(servers=entities) - -def _entity_detail(inst): - """ Maps everything to Rackspace-like attributes for return""" - power_mapping = { - power_state.NOSTATE: 'build', - power_state.RUNNING: 'active', - power_state.BLOCKED: 'active', - power_state.PAUSED: 'suspended', - power_state.SHUTDOWN: 'active', - power_state.SHUTOFF: 'active', - power_state.CRASHED: 'error' - } - inst_dict = {} - - mapped_keys = dict(status='state', imageId='image_id', - flavorId='instance_type', name='server_name', id='id') - - for k, v in mapped_keys.iteritems(): - inst_dict[k] = inst[v] - - inst_dict['status'] = power_mapping[inst_dict['status']] - inst_dict['addresses'] = dict(public=[], private=[]) - inst_dict['metadata'] = {} - inst_dict['hostId'] = '' - - return dict(server=inst_dict) - -def _entity_inst(inst): - """ Filters all model attributes save for id and name """ - return dict(server=dict(id=inst['id'], name=inst['server_name'])) - -class Controller(wsgi.Controller): - """ The Server API controller for the Openstack API """ - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "server": [ "id", "imageId", "name", "flavorId", "hostId", - "status", "progress", "progress" ] - } - } - } - - def __init__(self, db_driver=None): - if not db_driver: - db_driver = FLAGS.db_driver - self.db_driver = utils.import_object(db_driver) - super(Controller, self).__init__() - - def index(self, req): - """ Returns a list of server names and ids for a given user """ - return self._items(req, entity_maker=_entity_inst) - - def detail(self, req): - """ Returns a list of server details for a given user """ - return self._items(req, entity_maker=_entity_detail) - - def _items(self, req, entity_maker): - """Returns a list of servers for a given user. - - entity_maker - either _entity_detail or _entity_inst - """ - user_id = req.environ['nova.context']['user']['id'] - instance_list = self.db_driver.instance_get_all_by_user(None, user_id) - limited_list = nova.api.rackspace.limited(instance_list, req) - res = [entity_maker(inst)['server'] for inst in limited_list] - return _entity_list(res) - - def show(self, req, id): - """ Returns server details by server id """ - user_id = req.environ['nova.context']['user']['id'] - inst = self.db_driver.instance_get_by_internal_id(None, int(id)) - if inst: - if inst.user_id == user_id: - return _entity_detail(inst) - raise faults.Fault(exc.HTTPNotFound()) - - def delete(self, req, id): - """ Destroys a server """ - user_id = req.environ['nova.context']['user']['id'] - instance = self.db_driver.instance_get_by_internal_id(None, int(id)) - if instance and instance['user_id'] == user_id: - self.db_driver.instance_destroy(None, id) - return faults.Fault(exc.HTTPAccepted()) - return faults.Fault(exc.HTTPNotFound()) - - def create(self, req): - """ Creates a new server for a given user """ - - env = self._deserialize(req.body, req) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - #try: - inst = self._build_server_instance(req, env) - #except Exception, e: - # return faults.Fault(exc.HTTPUnprocessableEntity()) - - rpc.cast( - FLAGS.compute_topic, { - "method": "run_instance", - "args": {"instance_id": inst['id']}}) - return _entity_inst(inst) - - def update(self, req, id): - """ Updates the server name or password """ - user_id = req.environ['nova.context']['user']['id'] - - inst_dict = self._deserialize(req.body, req) - - if not inst_dict: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - instance = self.db_driver.instance_get_by_internal_id(None, int(id)) - if not instance or instance.user_id != user_id: - return faults.Fault(exc.HTTPNotFound()) - - self.db_driver.instance_update(None, int(id), - _filter_params(inst_dict['server'])) - return faults.Fault(exc.HTTPNoContent()) - - def action(self, req, id): - """ multi-purpose method used to reboot, rebuild, and - resize a server """ - user_id = req.environ['nova.context']['user']['id'] - input_dict = self._deserialize(req.body, req) - try: - reboot_type = input_dict['reboot']['type'] - except Exception: - raise faults.Fault(webob.exc.HTTPNotImplemented()) - inst_ref = self.db.instance_get_by_internal_id(None, int(id)) - if not inst_ref or (inst_ref and not inst_ref.user_id == user_id): - return faults.Fault(exc.HTTPUnprocessableEntity()) - cloud.reboot(id) - - def _build_server_instance(self, req, env): - """Build instance data structure and save it to the data store.""" - ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) - inst = {} - - user_id = req.environ['nova.context']['user']['id'] - - flavor_id = env['server']['flavorId'] - - instance_type, flavor = [(k, v) for k, v in - instance_types.INSTANCE_TYPES.iteritems() - if v['flavorid'] == flavor_id][0] - - image_id = env['server']['imageId'] - - img_service, image_id_trans = _image_service() - - opaque_image_id = image_id_trans.to_rs_id(image_id) - image = img_service.show(opaque_image_id) - - if not image: - raise Exception, "Image not found" - - inst['server_name'] = env['server']['name'] - inst['image_id'] = opaque_image_id - inst['user_id'] = user_id - inst['launch_time'] = ltime - inst['mac_address'] = utils.generate_mac() - inst['project_id'] = user_id - - inst['state_description'] = 'scheduling' - inst['kernel_id'] = image.get('kernelId', FLAGS.default_kernel) - inst['ramdisk_id'] = image.get('ramdiskId', FLAGS.default_ramdisk) - inst['reservation_id'] = utils.generate_uid('r') - - inst['display_name'] = env['server']['name'] - inst['display_description'] = env['server']['name'] - - #TODO(dietz) this may be ill advised - key_pair_ref = self.db_driver.key_pair_get_all_by_user( - None, user_id)[0] - - inst['key_data'] = key_pair_ref['public_key'] - inst['key_name'] = key_pair_ref['name'] - - #TODO(dietz) stolen from ec2 api, see TODO there - inst['security_group'] = 'default' - - # Flavor related attributes - inst['instance_type'] = instance_type - inst['memory_mb'] = flavor['memory_mb'] - inst['vcpus'] = flavor['vcpus'] - inst['local_gb'] = flavor['local_gb'] - - ref = self.db_driver.instance_create(None, inst) - inst['id'] = ref.internal_id - - # TODO(dietz): this isn't explicitly necessary, but the networking - # calls depend on an object with a project_id property, and therefore - # should be cleaned up later - api_context = context.APIRequestContext(user_id) - - inst['mac_address'] = utils.generate_mac() - - #TODO(dietz) is this necessary? - inst['launch_index'] = 0 - - inst['hostname'] = str(ref.internal_id) - self.db_driver.instance_update(None, inst['id'], inst) - - network_manager = utils.import_object(FLAGS.network_manager) - address = network_manager.allocate_fixed_ip(api_context, - inst['id']) - - # TODO(vish): This probably should be done in the scheduler - # network is setup when host is assigned - network_topic = self._get_network_topic(user_id) - rpc.call(network_topic, - {"method": "setup_fixed_ip", - "args": {"context": None, - "address": address}}) - return inst - - def _get_network_topic(self, user_id): - """Retrieves the network host for a project""" - network_ref = self.db_driver.project_get_network(None, - user_id) - host = network_ref['host'] - if not host: - host = rpc.call(FLAGS.network_topic, - {"method": "set_network_host", - "args": {"context": None, - "project_id": user_id}}) - return self.db_driver.queue_get_for(None, FLAGS.network_topic, host) diff --git a/nova/api/rackspace/sharedipgroups.py b/nova/api/rackspace/sharedipgroups.py deleted file mode 100644 index 4d2d0ede1..000000000 --- a/nova/api/rackspace/sharedipgroups.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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): pass diff --git a/nova/tests/api/__init__.py b/nova/tests/api/__init__.py index ec76aa827..2c7f7fd3e 100644 --- a/nova/tests/api/__init__.py +++ b/nova/tests/api/__init__.py @@ -44,8 +44,8 @@ class Test(unittest.TestCase): req = webob.Request.blank(url, environ_keys) return req.get_response(api.API()) - def test_rackspace(self): - self.stubs.Set(api.rackspace, 'API', APIStub) + def test_openstack(self): + self.stubs.Set(api.openstack, 'API', APIStub) result = self._request('/v1.0/cloud', 'rs') self.assertEqual(result.body, "/cloud") @@ -56,7 +56,7 @@ class Test(unittest.TestCase): def test_not_found(self): self.stubs.Set(api.ec2, 'API', APIStub) - self.stubs.Set(api.rackspace, 'API', APIStub) + self.stubs.Set(api.openstack, 'API', APIStub) result = self._request('/test/cloud', 'ec2') self.assertNotEqual(result.body, "/cloud") diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py new file mode 100644 index 000000000..b534897f5 --- /dev/null +++ b/nova/tests/api/openstack/__init__.py @@ -0,0 +1,108 @@ +# 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 unittest + +from nova.api.openstack import limited +from nova.api.openstack import RateLimitingMiddleware +from nova.tests.api.fakes import APIStub +from webob import Request + + +class RateLimitingMiddlewareTest(unittest.TestCase): + + def test_get_action_name(self): + middleware = RateLimitingMiddleware(APIStub()) + def verify(method, url, action_name): + req = Request.blank(url) + req.method = method + action = middleware.get_action_name(req) + self.assertEqual(action, action_name) + verify('PUT', '/servers/4', 'PUT') + verify('DELETE', '/servers/4', 'DELETE') + verify('POST', '/images/4', 'POST') + verify('POST', '/servers/4', 'POST servers') + verify('GET', '/foo?a=4&changes-since=never&b=5', 'GET changes-since') + verify('GET', '/foo?a=4&monkeys-since=never&b=5', None) + verify('GET', '/servers/4', None) + verify('HEAD', '/servers/4', None) + + def exhaust(self, middleware, method, url, username, times): + req = Request.blank(url, dict(REQUEST_METHOD=method), + headers={'X-Auth-User': username}) + for i in range(times): + resp = req.get_response(middleware) + self.assertEqual(resp.status_int, 200) + resp = req.get_response(middleware) + self.assertEqual(resp.status_int, 413) + self.assertTrue('Retry-After' in resp.headers) + + def test_single_action(self): + middleware = RateLimitingMiddleware(APIStub()) + self.exhaust(middleware, 'DELETE', '/servers/4', 'usr1', 100) + self.exhaust(middleware, 'DELETE', '/servers/4', 'usr2', 100) + + def test_POST_servers_action_implies_POST_action(self): + middleware = RateLimitingMiddleware(APIStub()) + self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) + self.exhaust(middleware, 'POST', '/images/4', 'usr2', 10) + self.assertTrue(set(middleware.limiter._levels) == + set(['usr1:POST', 'usr1:POST servers', 'usr2:POST'])) + + def test_POST_servers_action_correctly_ratelimited(self): + middleware = RateLimitingMiddleware(APIStub()) + # Use up all of our "POST" allowance for the minute, 5 times + for i in range(5): + self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) + # Reset the 'POST' action counter. + del middleware.limiter._levels['usr1:POST'] + # All 50 daily "POST servers" actions should be all used up + self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 0) + + def test_proxy_ctor_works(self): + middleware = RateLimitingMiddleware(APIStub()) + self.assertEqual(middleware.limiter.__class__.__name__, "Limiter") + middleware = RateLimitingMiddleware(APIStub(), service_host='foobar') + self.assertEqual(middleware.limiter.__class__.__name__, "WSGIAppProxy") + + +class LimiterTest(unittest.TestCase): + + def test_limiter(self): + items = range(2000) + req = Request.blank('/') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=0') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=3') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=2005') + self.assertEqual(limited(items, req), []) + req = Request.blank('/?limit=10') + self.assertEqual(limited(items, req), items[ :10]) + req = Request.blank('/?limit=0') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?limit=3000') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=1&limit=3') + self.assertEqual(limited(items, req), items[1:4]) + req = Request.blank('/?offset=3&limit=0') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=3&limit=1500') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=3000&limit=10') + self.assertEqual(limited(items, req), []) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py new file mode 100644 index 000000000..1119fa714 --- /dev/null +++ b/nova/tests/api/openstack/fakes.py @@ -0,0 +1,205 @@ +# 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 datetime +import json +import random +import string + +import webob +import webob.dec + +from nova import auth +from nova import utils +from nova import flags +from nova import exception as exc +import nova.api.openstack.auth +from nova.image import service +from nova.wsgi import Router + + +FLAGS = flags.FLAGS + + +class Context(object): + pass + + +class FakeRouter(Router): + def __init__(self): + pass + + @webob.dec.wsgify + def __call__(self, req): + res = webob.Response() + res.status = '200' + res.headers['X-Test-Success'] = 'True' + return res + + +def fake_auth_init(self): + self.db = FakeAuthDatabase() + self.context = Context() + self.auth = FakeAuthManager() + self.host = 'foo' + + +@webob.dec.wsgify +def fake_wsgi(self, req): + req.environ['nova.context'] = dict(user=dict(id=1)) + if req.body: + req.environ['inst_dict'] = json.loads(req.body) + return self.application + + +def stub_out_key_pair_funcs(stubs): + def key_pair(context, user_id): + return [dict(name='key', public_key='public_key')] + stubs.Set(nova.db.api, 'key_pair_get_all_by_user', + key_pair) + + +def stub_out_image_service(stubs): + def fake_image_show(meh, id): + return dict(kernelId=1, ramdiskId=1) + + stubs.Set(nova.image.service.LocalImageService, 'show', fake_image_show) + +def stub_out_auth(stubs): + def fake_auth_init(self, app): + self.application = app + + stubs.Set(nova.api.openstack.AuthMiddleware, + '__init__', fake_auth_init) + stubs.Set(nova.api.openstack.AuthMiddleware, + '__call__', fake_wsgi) + + +def stub_out_rate_limiting(stubs): + def fake_rate_init(self, app): + super(nova.api.openstack.RateLimitingMiddleware, self).__init__(app) + self.application = app + + stubs.Set(nova.api.openstack.RateLimitingMiddleware, + '__init__', fake_rate_init) + + stubs.Set(nova.api.openstack.RateLimitingMiddleware, + '__call__', fake_wsgi) + + +def stub_out_networking(stubs): + def get_my_ip(): + return '127.0.0.1' + stubs.Set(nova.utils, 'get_my_ip', get_my_ip) + FLAGS.FAKE_subdomain = 'rs' + + +def stub_out_glance(stubs): + + class FakeParallaxClient: + + def __init__(self): + self.fixtures = {} + + def fake_get_images(self): + return self.fixtures + + def fake_get_image_metadata(self, image_id): + for k, f in self.fixtures.iteritems(): + if k == image_id: + return f + return None + + def fake_add_image_metadata(self, image_data): + id = ''.join(random.choice(string.letters) for _ in range(20)) + image_data['id'] = id + self.fixtures[id] = image_data + return id + + def fake_update_image_metadata(self, image_id, image_data): + + if image_id not in self.fixtures.keys(): + raise exc.NotFound + + self.fixtures[image_id].update(image_data) + + def fake_delete_image_metadata(self, image_id): + + if image_id not in self.fixtures.keys(): + raise exc.NotFound + + del self.fixtures[image_id] + + def fake_delete_all(self): + self.fixtures = {} + + fake_parallax_client = FakeParallaxClient() + stubs.Set(nova.image.service.ParallaxClient, 'get_images', + fake_parallax_client.fake_get_images) + stubs.Set(nova.image.service.ParallaxClient, 'get_image_metadata', + fake_parallax_client.fake_get_image_metadata) + stubs.Set(nova.image.service.ParallaxClient, 'add_image_metadata', + fake_parallax_client.fake_add_image_metadata) + stubs.Set(nova.image.service.ParallaxClient, 'update_image_metadata', + fake_parallax_client.fake_update_image_metadata) + stubs.Set(nova.image.service.ParallaxClient, 'delete_image_metadata', + fake_parallax_client.fake_delete_image_metadata) + stubs.Set(nova.image.service.GlanceImageService, 'delete_all', + fake_parallax_client.fake_delete_all) + + +class FakeAuthDatabase(object): + data = {} + + @staticmethod + def auth_get_token(context, token_hash): + return FakeAuthDatabase.data.get(token_hash, None) + + @staticmethod + def auth_create_token(context, token): + token['created_at'] = datetime.datetime.now() + FakeAuthDatabase.data[token['token_hash']] = token + + @staticmethod + def auth_destroy_token(context, token): + if FakeAuthDatabase.data.has_key(token['token_hash']): + del FakeAuthDatabase.data['token_hash'] + + +class FakeAuthManager(object): + auth_data = {} + + def add_user(self, key, user): + FakeAuthManager.auth_data[key] = user + + def get_user(self, uid): + for k, v in FakeAuthManager.auth_data.iteritems(): + if v['uid'] == uid: + return v + return None + + def get_user_from_access_key(self, key): + return FakeAuthManager.auth_data.get(key, None) + + +class FakeRateLimiter(object): + def __init__(self, application): + self.application = application + + @webob.dec.wsgify + def __call__(self, req): + return self.application diff --git a/nova/tests/api/openstack/test_auth.py b/nova/tests/api/openstack/test_auth.py new file mode 100644 index 000000000..d2ba80243 --- /dev/null +++ b/nova/tests/api/openstack/test_auth.py @@ -0,0 +1,108 @@ +import datetime +import unittest + +import stubout +import webob +import webob.dec + +import nova.api +import nova.api.openstack.auth +from nova import auth +from nova.tests.api.openstack import fakes + +class Test(unittest.TestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + self.stubs.Set(nova.api.openstack.auth.BasicApiAuthManager, + '__init__', fakes.fake_auth_init) + fakes.FakeAuthManager.auth_data = {} + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_networking(self.stubs) + + def tearDown(self): + self.stubs.UnsetAll() + fakes.fake_data_store = {} + + def test_authorize_user(self): + f = fakes.FakeAuthManager() + f.add_user('derp', { 'uid': 1, 'name':'herp' } ) + + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-User'] = 'herp' + req.headers['X-Auth-Key'] = 'derp' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '204 No Content') + self.assertEqual(len(result.headers['X-Auth-Token']), 40) + self.assertEqual(result.headers['X-CDN-Management-Url'], + "") + self.assertEqual(result.headers['X-Storage-Url'], "") + + def test_authorize_token(self): + f = fakes.FakeAuthManager() + f.add_user('derp', { 'uid': 1, 'name':'herp' } ) + + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-User'] = 'herp' + req.headers['X-Auth-Key'] = 'derp' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '204 No Content') + self.assertEqual(len(result.headers['X-Auth-Token']), 40) + self.assertEqual(result.headers['X-Server-Management-Url'], + "https://foo/v1.0/") + self.assertEqual(result.headers['X-CDN-Management-Url'], + "") + self.assertEqual(result.headers['X-Storage-Url'], "") + + token = result.headers['X-Auth-Token'] + self.stubs.Set(nova.api.openstack, 'APIRouter', + fakes.FakeRouter) + req = webob.Request.blank('/v1.0/fake') + req.headers['X-Auth-Token'] = token + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '200 OK') + self.assertEqual(result.headers['X-Test-Success'], 'True') + + def test_token_expiry(self): + self.destroy_called = False + token_hash = 'bacon' + + def destroy_token_mock(meh, context, token): + self.destroy_called = True + + def bad_token(meh, context, token_hash): + return { 'token_hash':token_hash, + 'created_at':datetime.datetime(1990, 1, 1) } + + self.stubs.Set(fakes.FakeAuthDatabase, 'auth_destroy_token', + destroy_token_mock) + + self.stubs.Set(fakes.FakeAuthDatabase, 'auth_get_token', + bad_token) + + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-Token'] = 'bacon' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '401 Unauthorized') + self.assertEqual(self.destroy_called, True) + + def test_bad_user(self): + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-User'] = 'herp' + req.headers['X-Auth-Key'] = 'derp' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '401 Unauthorized') + + def test_no_user(self): + req = webob.Request.blank('/v1.0/') + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '401 Unauthorized') + + def test_bad_token(self): + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-Token'] = 'baconbaconbacon' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '401 Unauthorized') + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/api/openstack/test_faults.py b/nova/tests/api/openstack/test_faults.py new file mode 100644 index 000000000..70a811469 --- /dev/null +++ b/nova/tests/api/openstack/test_faults.py @@ -0,0 +1,40 @@ +import unittest +import webob +import webob.dec +import webob.exc + +from nova.api.openstack import faults + +class TestFaults(unittest.TestCase): + + def test_fault_parts(self): + req = webob.Request.blank('/.xml') + f = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + resp = req.get_response(f) + + first_two_words = resp.body.strip().split()[:2] + self.assertEqual(first_two_words, ['']) + body_without_spaces = ''.join(resp.body.split()) + self.assertTrue('scram' in body_without_spaces) + + def test_retry_header(self): + req = webob.Request.blank('/.xml') + exc = webob.exc.HTTPRequestEntityTooLarge(explanation='sorry', + headers={'Retry-After': 4}) + f = faults.Fault(exc) + resp = req.get_response(f) + first_two_words = resp.body.strip().split()[:2] + self.assertEqual(first_two_words, ['']) + body_sans_spaces = ''.join(resp.body.split()) + self.assertTrue('sorry' in body_sans_spaces) + self.assertTrue('4' in body_sans_spaces) + self.assertEqual(resp.headers['Retry-After'], 4) + + def test_raise(self): + @webob.dec.wsgify + def raiser(req): + raise faults.Fault(webob.exc.HTTPNotFound(explanation='whut?')) + req = webob.Request.blank('/.xml') + resp = req.get_response(raiser) + self.assertEqual(resp.status_int, 404) + self.assertTrue('whut?' in resp.body) diff --git a/nova/tests/api/openstack/test_flavors.py b/nova/tests/api/openstack/test_flavors.py new file mode 100644 index 000000000..8dd4d1f29 --- /dev/null +++ b/nova/tests/api/openstack/test_flavors.py @@ -0,0 +1,48 @@ +# 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 unittest + +import stubout +import webob + +import nova.api +from nova.api.openstack import flavors +from nova.tests.api.openstack import fakes + + +class FlavorsTest(unittest.TestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.auth_data = {} + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + + def tearDown(self): + self.stubs.UnsetAll() + + def test_get_flavor_list(self): + req = webob.Request.blank('/v1.0/flavors') + res = req.get_response(nova.api.API()) + + def test_get_flavor_by_id(self): + pass + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py new file mode 100644 index 000000000..505fea3e2 --- /dev/null +++ b/nova/tests/api/openstack/test_images.py @@ -0,0 +1,141 @@ +# 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 logging +import unittest + +import stubout + +from nova import exception +from nova import utils +from nova.api.openstack import images +from nova.tests.api.openstack import fakes + + +class BaseImageServiceTests(): + + """Tasks to test for all image services""" + + def test_create(self): + + fixture = {'name': 'test image', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None} + + num_images = len(self.service.index()) + + id = self.service.create(fixture) + + self.assertNotEquals(None, id) + self.assertEquals(num_images + 1, len(self.service.index())) + + def test_create_and_show_non_existing_image(self): + + fixture = {'name': 'test image', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None} + + num_images = len(self.service.index()) + + id = self.service.create(fixture) + + self.assertNotEquals(None, id) + + self.assertRaises(exception.NotFound, + self.service.show, + 'bad image id') + + def test_update(self): + + fixture = {'name': 'test image', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None} + + id = self.service.create(fixture) + + fixture['status'] = 'in progress' + + self.service.update(id, fixture) + new_image_data = self.service.show(id) + self.assertEquals('in progress', new_image_data['status']) + + def test_delete(self): + + fixtures = [ + {'name': 'test image 1', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None}, + {'name': 'test image 2', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None}] + + ids = [] + for fixture in fixtures: + new_id = self.service.create(fixture) + ids.append(new_id) + + num_images = len(self.service.index()) + self.assertEquals(2, num_images) + + self.service.delete(ids[0]) + + num_images = len(self.service.index()) + self.assertEquals(1, num_images) + + +class LocalImageServiceTest(unittest.TestCase, + BaseImageServiceTests): + + """Tests the local image service""" + + def setUp(self): + self.stubs = stubout.StubOutForTesting() + self.service = utils.import_object('nova.image.service.LocalImageService') + + def tearDown(self): + self.service.delete_all() + self.stubs.UnsetAll() + + +class GlanceImageServiceTest(unittest.TestCase, + BaseImageServiceTests): + + """Tests the local image service""" + + def setUp(self): + self.stubs = stubout.StubOutForTesting() + fakes.stub_out_glance(self.stubs) + self.service = utils.import_object('nova.image.service.GlanceImageService') + + def tearDown(self): + self.service.delete_all() + self.stubs.UnsetAll() diff --git a/nova/tests/api/openstack/test_ratelimiting.py b/nova/tests/api/openstack/test_ratelimiting.py new file mode 100644 index 000000000..ad9e67454 --- /dev/null +++ b/nova/tests/api/openstack/test_ratelimiting.py @@ -0,0 +1,237 @@ +import httplib +import StringIO +import time +import unittest +import webob + +import nova.api.openstack.ratelimiting as ratelimiting + +class LimiterTest(unittest.TestCase): + + def setUp(self): + self.limits = { + 'a': (5, ratelimiting.PER_SECOND), + 'b': (5, ratelimiting.PER_MINUTE), + 'c': (5, ratelimiting.PER_HOUR), + 'd': (1, ratelimiting.PER_SECOND), + 'e': (100, ratelimiting.PER_SECOND)} + self.rl = ratelimiting.Limiter(self.limits) + + def exhaust(self, action, times_until_exhausted, **kwargs): + for i in range(times_until_exhausted): + when = self.rl.perform(action, **kwargs) + self.assertEqual(when, None) + num, period = self.limits[action] + delay = period * 1.0 / num + # Verify that we are now thoroughly delayed + for i in range(10): + when = self.rl.perform(action, **kwargs) + self.assertAlmostEqual(when, delay, 2) + + def test_second(self): + self.exhaust('a', 5) + time.sleep(0.2) + self.exhaust('a', 1) + time.sleep(1) + self.exhaust('a', 5) + + def test_minute(self): + self.exhaust('b', 5) + + def test_one_per_period(self): + def allow_once_and_deny_once(): + when = self.rl.perform('d') + self.assertEqual(when, None) + when = self.rl.perform('d') + self.assertAlmostEqual(when, 1, 2) + return when + time.sleep(allow_once_and_deny_once()) + time.sleep(allow_once_and_deny_once()) + allow_once_and_deny_once() + + def test_we_can_go_indefinitely_if_we_spread_out_requests(self): + for i in range(200): + when = self.rl.perform('e') + self.assertEqual(when, None) + time.sleep(0.01) + + def test_users_get_separate_buckets(self): + self.exhaust('c', 5, username='alice') + self.exhaust('c', 5, username='bob') + self.exhaust('c', 5, username='chuck') + self.exhaust('c', 0, username='chuck') + self.exhaust('c', 0, username='bob') + self.exhaust('c', 0, username='alice') + + +class FakeLimiter(object): + """Fake Limiter class that you can tell how to behave.""" + def __init__(self, test): + self._action = self._username = self._delay = None + self.test = test + def mock(self, action, username, delay): + self._action = action + self._username = username + self._delay = delay + def perform(self, action, username): + self.test.assertEqual(action, self._action) + self.test.assertEqual(username, self._username) + return self._delay + + +class WSGIAppTest(unittest.TestCase): + + def setUp(self): + self.limiter = FakeLimiter(self) + self.app = ratelimiting.WSGIApp(self.limiter) + + def test_invalid_methods(self): + requests = [] + for method in ['GET', 'PUT', 'DELETE']: + req = webob.Request.blank('/limits/michael/breakdance', + dict(REQUEST_METHOD=method)) + requests.append(req) + for req in requests: + self.assertEqual(req.get_response(self.app).status_int, 405) + + def test_invalid_urls(self): + requests = [] + for prefix in ['limit', '', 'limiter2', 'limiter/limits', 'limiter/1']: + req = webob.Request.blank('/%s/michael/breakdance' % prefix, + dict(REQUEST_METHOD='POST')) + requests.append(req) + for req in requests: + self.assertEqual(req.get_response(self.app).status_int, 404) + + def verify(self, url, username, action, delay=None): + """Make sure that POSTing to the given url causes the given username + to perform the given action. Make the internal rate limiter return + delay and make sure that the WSGI app returns the correct response. + """ + req = webob.Request.blank(url, dict(REQUEST_METHOD='POST')) + self.limiter.mock(action, username, delay) + resp = req.get_response(self.app) + if not delay: + self.assertEqual(resp.status_int, 200) + else: + self.assertEqual(resp.status_int, 403) + self.assertEqual(resp.headers['X-Wait-Seconds'], "%.2f" % delay) + + def test_good_urls(self): + self.verify('/limiter/michael/hoot', 'michael', 'hoot') + + def test_escaping(self): + self.verify('/limiter/michael/jump%20up', 'michael', 'jump up') + + def test_response_to_delays(self): + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1) + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1.56) + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) + + +class FakeHttplibSocket(object): + """a fake socket implementation for httplib.HTTPResponse, trivial""" + + def __init__(self, response_string): + self._buffer = StringIO.StringIO(response_string) + + def makefile(self, _mode, _other): + """Returns the socket's internal buffer""" + return self._buffer + + +class FakeHttplibConnection(object): + """A fake httplib.HTTPConnection + + Requests made via this connection actually get translated and routed into + our WSGI app, we then wait for the response and turn it back into + an httplib.HTTPResponse. + """ + def __init__(self, app, host, is_secure=False): + self.app = app + self.host = host + + def request(self, method, path, data='', headers={}): + req = webob.Request.blank(path) + req.method = method + req.body = data + req.headers = headers + req.host = self.host + # Call the WSGI app, get the HTTP response + resp = str(req.get_response(self.app)) + # For some reason, the response doesn't have "HTTP/1.0 " prepended; I + # guess that's a function the web server usually provides. + resp = "HTTP/1.0 %s" % resp + sock = FakeHttplibSocket(resp) + self.http_response = httplib.HTTPResponse(sock) + self.http_response.begin() + + def getresponse(self): + return self.http_response + + +def wire_HTTPConnection_to_WSGI(host, app): + """Monkeypatches HTTPConnection so that if you try to connect to host, you + are instead routed straight to the given WSGI app. + + After calling this method, when any code calls + + httplib.HTTPConnection(host) + + the connection object will be a fake. Its requests will be sent directly + to the given WSGI app rather than through a socket. + + Code connecting to hosts other than host will not be affected. + + This method may be called multiple times to map different hosts to + different apps. + """ + class HTTPConnectionDecorator(object): + """Wraps the real HTTPConnection class so that when you instantiate + the class you might instead get a fake instance.""" + def __init__(self, wrapped): + self.wrapped = wrapped + def __call__(self, connection_host, *args, **kwargs): + if connection_host == host: + return FakeHttplibConnection(app, host) + else: + return self.wrapped(connection_host, *args, **kwargs) + httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) + + +class WSGIAppProxyTest(unittest.TestCase): + + def setUp(self): + """Our WSGIAppProxy is going to call across an HTTPConnection to a + WSGIApp running a limiter. The proxy will send input, and the proxy + should receive that same input, pass it to the limiter who gives a + result, and send the expected result back. + + The HTTPConnection isn't real -- it's monkeypatched to point straight + at the WSGIApp. And the limiter isn't real -- it's a fake that + behaves the way we tell it to. + """ + self.limiter = FakeLimiter(self) + app = ratelimiting.WSGIApp(self.limiter) + wire_HTTPConnection_to_WSGI('100.100.100.100:80', app) + self.proxy = ratelimiting.WSGIAppProxy('100.100.100.100:80') + + def test_200(self): + self.limiter.mock('conquer', 'caesar', None) + when = self.proxy.perform('conquer', 'caesar') + self.assertEqual(when, None) + + def test_403(self): + self.limiter.mock('grumble', 'proletariat', 1.5) + when = self.proxy.perform('grumble', 'proletariat') + self.assertEqual(when, 1.5) + + def test_failure(self): + def shouldRaise(): + self.limiter.mock('murder', 'brutus', None) + self.proxy.perform('stab', 'brutus') + self.assertRaises(AssertionError, shouldRaise) + + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py new file mode 100644 index 000000000..d1ee533b6 --- /dev/null +++ b/nova/tests/api/openstack/test_servers.py @@ -0,0 +1,249 @@ +# 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 json +import unittest + +import stubout +import webob + +from nova import db +from nova import flags +import nova.api.openstack +from nova.api.openstack import servers +import nova.db.api +from nova.db.sqlalchemy.models import Instance +import nova.rpc +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS + +FLAGS.verbose = True + +def return_server(context, id): + return stub_instance(id) + + +def return_servers(context, user_id=1): + return [stub_instance(i, user_id) for i in xrange(5)] + + +def stub_instance(id, user_id=1): + return Instance( + id=id, state=0, image_id=10, server_name='server%s'%id, + user_id=user_id + ) + + +class ServersTest(unittest.TestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.auth_data = {} + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fakes.stub_out_image_service(self.stubs) + self.stubs.Set(nova.db.api, 'instance_get_all', return_servers) + self.stubs.Set(nova.db.api, 'instance_get_by_internal_id', return_server) + self.stubs.Set(nova.db.api, 'instance_get_all_by_user', + return_servers) + + def tearDown(self): + self.stubs.UnsetAll() + + def test_get_server_by_id(self): + req = webob.Request.blank('/v1.0/servers/1') + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['name'], 'server1') + + def test_get_server_list(self): + req = webob.Request.blank('/v1.0/servers') + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s['id'], i) + self.assertEqual(s['name'], 'server%d'%i) + self.assertEqual(s.get('imageId', None), None) + i += 1 + + def test_create_instance(self): + def server_update(context, id, params): + pass + + def instance_create(context, inst): + class Foo(object): + internal_id = 1 + return Foo() + + def fake_method(*args, **kwargs): + pass + + def project_get_network(context, user_id): + return dict(id='1', host='localhost') + + def queue_get_for(context, *args): + return 'network_topic' + + self.stubs.Set(nova.db.api, 'project_get_network', project_get_network) + self.stubs.Set(nova.db.api, 'instance_create', instance_create) + self.stubs.Set(nova.rpc, 'cast', fake_method) + self.stubs.Set(nova.rpc, 'call', fake_method) + self.stubs.Set(nova.db.api, 'instance_update', + server_update) + self.stubs.Set(nova.db.api, 'queue_get_for', queue_get_for) + self.stubs.Set(nova.network.manager.VlanManager, 'allocate_fixed_ip', + fake_method) + + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers') + req.method = 'POST' + req.body = json.dumps(body) + + res = req.get_response(nova.api.API()) + + self.assertEqual(res.status_int, 200) + + def test_update_no_body(self): + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status_int, 422) + + def test_update_bad_params(self): + """ Confirm that update is filtering params """ + inst_dict = dict(cat='leopard', name='server_test', adminPass='bacon') + self.body = json.dumps(dict(server=inst_dict)) + + def server_update(context, id, params): + self.update_called = True + filtered_dict = dict(name='server_test', admin_pass='bacon') + self.assertEqual(params, filtered_dict) + + self.stubs.Set(nova.db.api, 'instance_update', + server_update) + + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + req.body = self.body + req.get_response(nova.api.API()) + + def test_update_server(self): + inst_dict = dict(name='server_test', adminPass='bacon') + self.body = json.dumps(dict(server=inst_dict)) + + def server_update(context, id, params): + filtered_dict = dict(name='server_test', admin_pass='bacon') + self.assertEqual(params, filtered_dict) + + self.stubs.Set(nova.db.api, 'instance_update', + server_update) + + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + req.body = self.body + req.get_response(nova.api.API()) + + def test_create_backup_schedules(self): + req = webob.Request.blank('/v1.0/servers/1/backup_schedules') + req.method = 'POST' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_delete_backup_schedules(self): + req = webob.Request.blank('/v1.0/servers/1/backup_schedules') + req.method = 'DELETE' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_get_server_backup_schedules(self): + req = webob.Request.blank('/v1.0/servers/1/backup_schedules') + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_get_all_server_details(self): + req = webob.Request.blank('/v1.0/servers/detail') + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s['id'], i) + self.assertEqual(s['name'], 'server%d'%i) + self.assertEqual(s['imageId'], 10) + i += 1 + + def test_server_reboot(self): + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) + res = req.get_response(nova.api.API()) + + def test_server_rebuild(self): + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) + res = req.get_response(nova.api.API()) + + def test_server_resize(self): + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) + res = req.get_response(nova.api.API()) + + def test_delete_server_instance(self): + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'DELETE' + + self.server_delete_called = False + def instance_destroy_mock(context, id): + self.server_delete_called = True + + self.stubs.Set(nova.db.api, 'instance_destroy', + instance_destroy_mock) + + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '202 Accepted') + self.assertEqual(self.server_delete_called, True) + + +if __name__ == "__main__": + unittest.main() diff --git a/nova/tests/api/openstack/test_sharedipgroups.py b/nova/tests/api/openstack/test_sharedipgroups.py new file mode 100644 index 000000000..d199951d8 --- /dev/null +++ b/nova/tests/api/openstack/test_sharedipgroups.py @@ -0,0 +1,39 @@ +# 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 unittest + +import stubout + +from nova.api.openstack import sharedipgroups + + +class SharedIpGroupsTest(unittest.TestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + + def tearDown(self): + self.stubs.UnsetAll() + + def test_get_shared_ip_groups(self): + pass + + def test_create_shared_ip_group(self): + pass + + def test_delete_shared_ip_group(self): + pass diff --git a/nova/tests/api/rackspace/__init__.py b/nova/tests/api/rackspace/__init__.py deleted file mode 100644 index 1834f91b1..000000000 --- a/nova/tests/api/rackspace/__init__.py +++ /dev/null @@ -1,108 +0,0 @@ -# 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 unittest - -from nova.api.rackspace import limited -from nova.api.rackspace import RateLimitingMiddleware -from nova.tests.api.fakes import APIStub -from webob import Request - - -class RateLimitingMiddlewareTest(unittest.TestCase): - - def test_get_action_name(self): - middleware = RateLimitingMiddleware(APIStub()) - def verify(method, url, action_name): - req = Request.blank(url) - req.method = method - action = middleware.get_action_name(req) - self.assertEqual(action, action_name) - verify('PUT', '/servers/4', 'PUT') - verify('DELETE', '/servers/4', 'DELETE') - verify('POST', '/images/4', 'POST') - verify('POST', '/servers/4', 'POST servers') - verify('GET', '/foo?a=4&changes-since=never&b=5', 'GET changes-since') - verify('GET', '/foo?a=4&monkeys-since=never&b=5', None) - verify('GET', '/servers/4', None) - verify('HEAD', '/servers/4', None) - - def exhaust(self, middleware, method, url, username, times): - req = Request.blank(url, dict(REQUEST_METHOD=method), - headers={'X-Auth-User': username}) - for i in range(times): - resp = req.get_response(middleware) - self.assertEqual(resp.status_int, 200) - resp = req.get_response(middleware) - self.assertEqual(resp.status_int, 413) - self.assertTrue('Retry-After' in resp.headers) - - def test_single_action(self): - middleware = RateLimitingMiddleware(APIStub()) - self.exhaust(middleware, 'DELETE', '/servers/4', 'usr1', 100) - self.exhaust(middleware, 'DELETE', '/servers/4', 'usr2', 100) - - def test_POST_servers_action_implies_POST_action(self): - middleware = RateLimitingMiddleware(APIStub()) - self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) - self.exhaust(middleware, 'POST', '/images/4', 'usr2', 10) - self.assertTrue(set(middleware.limiter._levels) == - set(['usr1:POST', 'usr1:POST servers', 'usr2:POST'])) - - def test_POST_servers_action_correctly_ratelimited(self): - middleware = RateLimitingMiddleware(APIStub()) - # Use up all of our "POST" allowance for the minute, 5 times - for i in range(5): - self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) - # Reset the 'POST' action counter. - del middleware.limiter._levels['usr1:POST'] - # All 50 daily "POST servers" actions should be all used up - self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 0) - - def test_proxy_ctor_works(self): - middleware = RateLimitingMiddleware(APIStub()) - self.assertEqual(middleware.limiter.__class__.__name__, "Limiter") - middleware = RateLimitingMiddleware(APIStub(), service_host='foobar') - self.assertEqual(middleware.limiter.__class__.__name__, "WSGIAppProxy") - - -class LimiterTest(unittest.TestCase): - - def test_limiter(self): - items = range(2000) - req = Request.blank('/') - self.assertEqual(limited(items, req), items[ :1000]) - req = Request.blank('/?offset=0') - self.assertEqual(limited(items, req), items[ :1000]) - req = Request.blank('/?offset=3') - self.assertEqual(limited(items, req), items[3:1003]) - req = Request.blank('/?offset=2005') - self.assertEqual(limited(items, req), []) - req = Request.blank('/?limit=10') - self.assertEqual(limited(items, req), items[ :10]) - req = Request.blank('/?limit=0') - self.assertEqual(limited(items, req), items[ :1000]) - req = Request.blank('/?limit=3000') - self.assertEqual(limited(items, req), items[ :1000]) - req = Request.blank('/?offset=1&limit=3') - self.assertEqual(limited(items, req), items[1:4]) - req = Request.blank('/?offset=3&limit=0') - self.assertEqual(limited(items, req), items[3:1003]) - req = Request.blank('/?offset=3&limit=1500') - self.assertEqual(limited(items, req), items[3:1003]) - req = Request.blank('/?offset=3000&limit=10') - self.assertEqual(limited(items, req), []) diff --git a/nova/tests/api/rackspace/fakes.py b/nova/tests/api/rackspace/fakes.py deleted file mode 100644 index 6a25720a9..000000000 --- a/nova/tests/api/rackspace/fakes.py +++ /dev/null @@ -1,205 +0,0 @@ -# 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 datetime -import json -import random -import string - -import webob -import webob.dec - -from nova import auth -from nova import utils -from nova import flags -from nova import exception as exc -import nova.api.rackspace.auth -from nova.image import service -from nova.wsgi import Router - - -FLAGS = flags.FLAGS - - -class Context(object): - pass - - -class FakeRouter(Router): - def __init__(self): - pass - - @webob.dec.wsgify - def __call__(self, req): - res = webob.Response() - res.status = '200' - res.headers['X-Test-Success'] = 'True' - return res - - -def fake_auth_init(self): - self.db = FakeAuthDatabase() - self.context = Context() - self.auth = FakeAuthManager() - self.host = 'foo' - - -@webob.dec.wsgify -def fake_wsgi(self, req): - req.environ['nova.context'] = dict(user=dict(id=1)) - if req.body: - req.environ['inst_dict'] = json.loads(req.body) - return self.application - - -def stub_out_key_pair_funcs(stubs): - def key_pair(context, user_id): - return [dict(name='key', public_key='public_key')] - stubs.Set(nova.db.api, 'key_pair_get_all_by_user', - key_pair) - - -def stub_out_image_service(stubs): - def fake_image_show(meh, id): - return dict(kernelId=1, ramdiskId=1) - - stubs.Set(nova.image.service.LocalImageService, 'show', fake_image_show) - -def stub_out_auth(stubs): - def fake_auth_init(self, app): - self.application = app - - stubs.Set(nova.api.rackspace.AuthMiddleware, - '__init__', fake_auth_init) - stubs.Set(nova.api.rackspace.AuthMiddleware, - '__call__', fake_wsgi) - - -def stub_out_rate_limiting(stubs): - def fake_rate_init(self, app): - super(nova.api.rackspace.RateLimitingMiddleware, self).__init__(app) - self.application = app - - stubs.Set(nova.api.rackspace.RateLimitingMiddleware, - '__init__', fake_rate_init) - - stubs.Set(nova.api.rackspace.RateLimitingMiddleware, - '__call__', fake_wsgi) - - -def stub_out_networking(stubs): - def get_my_ip(): - return '127.0.0.1' - stubs.Set(nova.utils, 'get_my_ip', get_my_ip) - FLAGS.FAKE_subdomain = 'rs' - - -def stub_out_glance(stubs): - - class FakeParallaxClient: - - def __init__(self): - self.fixtures = {} - - def fake_get_images(self): - return self.fixtures - - def fake_get_image_metadata(self, image_id): - for k, f in self.fixtures.iteritems(): - if k == image_id: - return f - return None - - def fake_add_image_metadata(self, image_data): - id = ''.join(random.choice(string.letters) for _ in range(20)) - image_data['id'] = id - self.fixtures[id] = image_data - return id - - def fake_update_image_metadata(self, image_id, image_data): - - if image_id not in self.fixtures.keys(): - raise exc.NotFound - - self.fixtures[image_id].update(image_data) - - def fake_delete_image_metadata(self, image_id): - - if image_id not in self.fixtures.keys(): - raise exc.NotFound - - del self.fixtures[image_id] - - def fake_delete_all(self): - self.fixtures = {} - - fake_parallax_client = FakeParallaxClient() - stubs.Set(nova.image.service.ParallaxClient, 'get_images', - fake_parallax_client.fake_get_images) - stubs.Set(nova.image.service.ParallaxClient, 'get_image_metadata', - fake_parallax_client.fake_get_image_metadata) - stubs.Set(nova.image.service.ParallaxClient, 'add_image_metadata', - fake_parallax_client.fake_add_image_metadata) - stubs.Set(nova.image.service.ParallaxClient, 'update_image_metadata', - fake_parallax_client.fake_update_image_metadata) - stubs.Set(nova.image.service.ParallaxClient, 'delete_image_metadata', - fake_parallax_client.fake_delete_image_metadata) - stubs.Set(nova.image.service.GlanceImageService, 'delete_all', - fake_parallax_client.fake_delete_all) - - -class FakeAuthDatabase(object): - data = {} - - @staticmethod - def auth_get_token(context, token_hash): - return FakeAuthDatabase.data.get(token_hash, None) - - @staticmethod - def auth_create_token(context, token): - token['created_at'] = datetime.datetime.now() - FakeAuthDatabase.data[token['token_hash']] = token - - @staticmethod - def auth_destroy_token(context, token): - if FakeAuthDatabase.data.has_key(token['token_hash']): - del FakeAuthDatabase.data['token_hash'] - - -class FakeAuthManager(object): - auth_data = {} - - def add_user(self, key, user): - FakeAuthManager.auth_data[key] = user - - def get_user(self, uid): - for k, v in FakeAuthManager.auth_data.iteritems(): - if v['uid'] == uid: - return v - return None - - def get_user_from_access_key(self, key): - return FakeAuthManager.auth_data.get(key, None) - - -class FakeRateLimiter(object): - def __init__(self, application): - self.application = application - - @webob.dec.wsgify - def __call__(self, req): - return self.application diff --git a/nova/tests/api/rackspace/test_auth.py b/nova/tests/api/rackspace/test_auth.py deleted file mode 100644 index 374cfe42b..000000000 --- a/nova/tests/api/rackspace/test_auth.py +++ /dev/null @@ -1,108 +0,0 @@ -import datetime -import unittest - -import stubout -import webob -import webob.dec - -import nova.api -import nova.api.rackspace.auth -from nova import auth -from nova.tests.api.rackspace import fakes - -class Test(unittest.TestCase): - def setUp(self): - self.stubs = stubout.StubOutForTesting() - self.stubs.Set(nova.api.rackspace.auth.BasicApiAuthManager, - '__init__', fakes.fake_auth_init) - fakes.FakeAuthManager.auth_data = {} - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_networking(self.stubs) - - def tearDown(self): - self.stubs.UnsetAll() - fakes.fake_data_store = {} - - def test_authorize_user(self): - f = fakes.FakeAuthManager() - f.add_user('derp', { 'uid': 1, 'name':'herp' } ) - - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-User'] = 'herp' - req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '204 No Content') - self.assertEqual(len(result.headers['X-Auth-Token']), 40) - self.assertEqual(result.headers['X-CDN-Management-Url'], - "") - self.assertEqual(result.headers['X-Storage-Url'], "") - - def test_authorize_token(self): - f = fakes.FakeAuthManager() - f.add_user('derp', { 'uid': 1, 'name':'herp' } ) - - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-User'] = 'herp' - req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '204 No Content') - self.assertEqual(len(result.headers['X-Auth-Token']), 40) - self.assertEqual(result.headers['X-Server-Management-Url'], - "https://foo/v1.0/") - self.assertEqual(result.headers['X-CDN-Management-Url'], - "") - self.assertEqual(result.headers['X-Storage-Url'], "") - - token = result.headers['X-Auth-Token'] - self.stubs.Set(nova.api.rackspace, 'APIRouter', - fakes.FakeRouter) - req = webob.Request.blank('/v1.0/fake') - req.headers['X-Auth-Token'] = token - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '200 OK') - self.assertEqual(result.headers['X-Test-Success'], 'True') - - def test_token_expiry(self): - self.destroy_called = False - token_hash = 'bacon' - - def destroy_token_mock(meh, context, token): - self.destroy_called = True - - def bad_token(meh, context, token_hash): - return { 'token_hash':token_hash, - 'created_at':datetime.datetime(1990, 1, 1) } - - self.stubs.Set(fakes.FakeAuthDatabase, 'auth_destroy_token', - destroy_token_mock) - - self.stubs.Set(fakes.FakeAuthDatabase, 'auth_get_token', - bad_token) - - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-Token'] = 'bacon' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '401 Unauthorized') - self.assertEqual(self.destroy_called, True) - - def test_bad_user(self): - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-User'] = 'herp' - req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '401 Unauthorized') - - def test_no_user(self): - req = webob.Request.blank('/v1.0/') - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '401 Unauthorized') - - def test_bad_token(self): - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-Token'] = 'baconbaconbacon' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '401 Unauthorized') - -if __name__ == '__main__': - unittest.main() diff --git a/nova/tests/api/rackspace/test_faults.py b/nova/tests/api/rackspace/test_faults.py deleted file mode 100644 index b2931bc98..000000000 --- a/nova/tests/api/rackspace/test_faults.py +++ /dev/null @@ -1,40 +0,0 @@ -import unittest -import webob -import webob.dec -import webob.exc - -from nova.api.rackspace import faults - -class TestFaults(unittest.TestCase): - - def test_fault_parts(self): - req = webob.Request.blank('/.xml') - f = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) - resp = req.get_response(f) - - first_two_words = resp.body.strip().split()[:2] - self.assertEqual(first_two_words, ['']) - body_without_spaces = ''.join(resp.body.split()) - self.assertTrue('scram' in body_without_spaces) - - def test_retry_header(self): - req = webob.Request.blank('/.xml') - exc = webob.exc.HTTPRequestEntityTooLarge(explanation='sorry', - headers={'Retry-After': 4}) - f = faults.Fault(exc) - resp = req.get_response(f) - first_two_words = resp.body.strip().split()[:2] - self.assertEqual(first_two_words, ['']) - body_sans_spaces = ''.join(resp.body.split()) - self.assertTrue('sorry' in body_sans_spaces) - self.assertTrue('4' in body_sans_spaces) - self.assertEqual(resp.headers['Retry-After'], 4) - - def test_raise(self): - @webob.dec.wsgify - def raiser(req): - raise faults.Fault(webob.exc.HTTPNotFound(explanation='whut?')) - req = webob.Request.blank('/.xml') - resp = req.get_response(raiser) - self.assertEqual(resp.status_int, 404) - self.assertTrue('whut?' in resp.body) diff --git a/nova/tests/api/rackspace/test_flavors.py b/nova/tests/api/rackspace/test_flavors.py deleted file mode 100644 index affdd2406..000000000 --- a/nova/tests/api/rackspace/test_flavors.py +++ /dev/null @@ -1,48 +0,0 @@ -# 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 unittest - -import stubout -import webob - -import nova.api -from nova.api.rackspace import flavors -from nova.tests.api.rackspace import fakes - - -class FlavorsTest(unittest.TestCase): - def setUp(self): - self.stubs = stubout.StubOutForTesting() - fakes.FakeAuthManager.auth_data = {} - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_auth(self.stubs) - - def tearDown(self): - self.stubs.UnsetAll() - - def test_get_flavor_list(self): - req = webob.Request.blank('/v1.0/flavors') - res = req.get_response(nova.api.API()) - - def test_get_flavor_by_id(self): - pass - -if __name__ == '__main__': - unittest.main() diff --git a/nova/tests/api/rackspace/test_images.py b/nova/tests/api/rackspace/test_images.py deleted file mode 100644 index a7f320b46..000000000 --- a/nova/tests/api/rackspace/test_images.py +++ /dev/null @@ -1,141 +0,0 @@ -# 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 logging -import unittest - -import stubout - -from nova import exception -from nova import utils -from nova.api.rackspace import images -from nova.tests.api.rackspace import fakes - - -class BaseImageServiceTests(): - - """Tasks to test for all image services""" - - def test_create(self): - - fixture = {'name': 'test image', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None} - - num_images = len(self.service.index()) - - id = self.service.create(fixture) - - self.assertNotEquals(None, id) - self.assertEquals(num_images + 1, len(self.service.index())) - - def test_create_and_show_non_existing_image(self): - - fixture = {'name': 'test image', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None} - - num_images = len(self.service.index()) - - id = self.service.create(fixture) - - self.assertNotEquals(None, id) - - self.assertRaises(exception.NotFound, - self.service.show, - 'bad image id') - - def test_update(self): - - fixture = {'name': 'test image', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None} - - id = self.service.create(fixture) - - fixture['status'] = 'in progress' - - self.service.update(id, fixture) - new_image_data = self.service.show(id) - self.assertEquals('in progress', new_image_data['status']) - - def test_delete(self): - - fixtures = [ - {'name': 'test image 1', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None}, - {'name': 'test image 2', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None}] - - ids = [] - for fixture in fixtures: - new_id = self.service.create(fixture) - ids.append(new_id) - - num_images = len(self.service.index()) - self.assertEquals(2, num_images) - - self.service.delete(ids[0]) - - num_images = len(self.service.index()) - self.assertEquals(1, num_images) - - -class LocalImageServiceTest(unittest.TestCase, - BaseImageServiceTests): - - """Tests the local image service""" - - def setUp(self): - self.stubs = stubout.StubOutForTesting() - self.service = utils.import_object('nova.image.service.LocalImageService') - - def tearDown(self): - self.service.delete_all() - self.stubs.UnsetAll() - - -class GlanceImageServiceTest(unittest.TestCase, - BaseImageServiceTests): - - """Tests the local image service""" - - def setUp(self): - self.stubs = stubout.StubOutForTesting() - fakes.stub_out_glance(self.stubs) - self.service = utils.import_object('nova.image.service.GlanceImageService') - - def tearDown(self): - self.service.delete_all() - self.stubs.UnsetAll() diff --git a/nova/tests/api/rackspace/test_servers.py b/nova/tests/api/rackspace/test_servers.py deleted file mode 100644 index 57040621b..000000000 --- a/nova/tests/api/rackspace/test_servers.py +++ /dev/null @@ -1,249 +0,0 @@ -# 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 json -import unittest - -import stubout -import webob - -from nova import db -from nova import flags -import nova.api.rackspace -from nova.api.rackspace import servers -import nova.db.api -from nova.db.sqlalchemy.models import Instance -import nova.rpc -from nova.tests.api.rackspace import fakes - - -FLAGS = flags.FLAGS - -FLAGS.verbose = True - -def return_server(context, id): - return stub_instance(id) - - -def return_servers(context, user_id=1): - return [stub_instance(i, user_id) for i in xrange(5)] - - -def stub_instance(id, user_id=1): - return Instance( - id=id, state=0, image_id=10, server_name='server%s'%id, - user_id=user_id - ) - - -class ServersTest(unittest.TestCase): - def setUp(self): - self.stubs = stubout.StubOutForTesting() - fakes.FakeAuthManager.auth_data = {} - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_auth(self.stubs) - fakes.stub_out_key_pair_funcs(self.stubs) - fakes.stub_out_image_service(self.stubs) - self.stubs.Set(nova.db.api, 'instance_get_all', return_servers) - self.stubs.Set(nova.db.api, 'instance_get_by_internal_id', return_server) - self.stubs.Set(nova.db.api, 'instance_get_all_by_user', - return_servers) - - def tearDown(self): - self.stubs.UnsetAll() - - def test_get_server_by_id(self): - req = webob.Request.blank('/v1.0/servers/1') - res = req.get_response(nova.api.API()) - res_dict = json.loads(res.body) - self.assertEqual(res_dict['server']['id'], 1) - self.assertEqual(res_dict['server']['name'], 'server1') - - def test_get_server_list(self): - req = webob.Request.blank('/v1.0/servers') - res = req.get_response(nova.api.API()) - res_dict = json.loads(res.body) - - i = 0 - for s in res_dict['servers']: - self.assertEqual(s['id'], i) - self.assertEqual(s['name'], 'server%d'%i) - self.assertEqual(s.get('imageId', None), None) - i += 1 - - def test_create_instance(self): - def server_update(context, id, params): - pass - - def instance_create(context, inst): - class Foo(object): - internal_id = 1 - return Foo() - - def fake_method(*args, **kwargs): - pass - - def project_get_network(context, user_id): - return dict(id='1', host='localhost') - - def queue_get_for(context, *args): - return 'network_topic' - - self.stubs.Set(nova.db.api, 'project_get_network', project_get_network) - self.stubs.Set(nova.db.api, 'instance_create', instance_create) - self.stubs.Set(nova.rpc, 'cast', fake_method) - self.stubs.Set(nova.rpc, 'call', fake_method) - self.stubs.Set(nova.db.api, 'instance_update', - server_update) - self.stubs.Set(nova.db.api, 'queue_get_for', queue_get_for) - self.stubs.Set(nova.network.manager.VlanManager, 'allocate_fixed_ip', - fake_method) - - body = dict(server=dict( - name='server_test', imageId=2, flavorId=2, metadata={}, - personality = {} - )) - req = webob.Request.blank('/v1.0/servers') - req.method = 'POST' - req.body = json.dumps(body) - - res = req.get_response(nova.api.API()) - - self.assertEqual(res.status_int, 200) - - def test_update_no_body(self): - req = webob.Request.blank('/v1.0/servers/1') - req.method = 'PUT' - res = req.get_response(nova.api.API()) - self.assertEqual(res.status_int, 422) - - def test_update_bad_params(self): - """ Confirm that update is filtering params """ - inst_dict = dict(cat='leopard', name='server_test', adminPass='bacon') - self.body = json.dumps(dict(server=inst_dict)) - - def server_update(context, id, params): - self.update_called = True - filtered_dict = dict(name='server_test', admin_pass='bacon') - self.assertEqual(params, filtered_dict) - - self.stubs.Set(nova.db.api, 'instance_update', - server_update) - - req = webob.Request.blank('/v1.0/servers/1') - req.method = 'PUT' - req.body = self.body - req.get_response(nova.api.API()) - - def test_update_server(self): - inst_dict = dict(name='server_test', adminPass='bacon') - self.body = json.dumps(dict(server=inst_dict)) - - def server_update(context, id, params): - filtered_dict = dict(name='server_test', admin_pass='bacon') - self.assertEqual(params, filtered_dict) - - self.stubs.Set(nova.db.api, 'instance_update', - server_update) - - req = webob.Request.blank('/v1.0/servers/1') - req.method = 'PUT' - req.body = self.body - req.get_response(nova.api.API()) - - def test_create_backup_schedules(self): - req = webob.Request.blank('/v1.0/servers/1/backup_schedules') - req.method = 'POST' - res = req.get_response(nova.api.API()) - self.assertEqual(res.status, '404 Not Found') - - def test_delete_backup_schedules(self): - req = webob.Request.blank('/v1.0/servers/1/backup_schedules') - req.method = 'DELETE' - res = req.get_response(nova.api.API()) - self.assertEqual(res.status, '404 Not Found') - - def test_get_server_backup_schedules(self): - req = webob.Request.blank('/v1.0/servers/1/backup_schedules') - res = req.get_response(nova.api.API()) - self.assertEqual(res.status, '404 Not Found') - - def test_get_all_server_details(self): - req = webob.Request.blank('/v1.0/servers/detail') - res = req.get_response(nova.api.API()) - res_dict = json.loads(res.body) - - i = 0 - for s in res_dict['servers']: - self.assertEqual(s['id'], i) - self.assertEqual(s['name'], 'server%d'%i) - self.assertEqual(s['imageId'], 10) - i += 1 - - def test_server_reboot(self): - body = dict(server=dict( - name='server_test', imageId=2, flavorId=2, metadata={}, - personality = {} - )) - req = webob.Request.blank('/v1.0/servers/1/action') - req.method = 'POST' - req.content_type= 'application/json' - req.body = json.dumps(body) - res = req.get_response(nova.api.API()) - - def test_server_rebuild(self): - body = dict(server=dict( - name='server_test', imageId=2, flavorId=2, metadata={}, - personality = {} - )) - req = webob.Request.blank('/v1.0/servers/1/action') - req.method = 'POST' - req.content_type= 'application/json' - req.body = json.dumps(body) - res = req.get_response(nova.api.API()) - - def test_server_resize(self): - body = dict(server=dict( - name='server_test', imageId=2, flavorId=2, metadata={}, - personality = {} - )) - req = webob.Request.blank('/v1.0/servers/1/action') - req.method = 'POST' - req.content_type= 'application/json' - req.body = json.dumps(body) - res = req.get_response(nova.api.API()) - - def test_delete_server_instance(self): - req = webob.Request.blank('/v1.0/servers/1') - req.method = 'DELETE' - - self.server_delete_called = False - def instance_destroy_mock(context, id): - self.server_delete_called = True - - self.stubs.Set(nova.db.api, 'instance_destroy', - instance_destroy_mock) - - res = req.get_response(nova.api.API()) - self.assertEqual(res.status, '202 Accepted') - self.assertEqual(self.server_delete_called, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/nova/tests/api/rackspace/test_sharedipgroups.py b/nova/tests/api/rackspace/test_sharedipgroups.py deleted file mode 100644 index 31ce967d0..000000000 --- a/nova/tests/api/rackspace/test_sharedipgroups.py +++ /dev/null @@ -1,39 +0,0 @@ -# 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 unittest - -import stubout - -from nova.api.rackspace import sharedipgroups - - -class SharedIpGroupsTest(unittest.TestCase): - def setUp(self): - self.stubs = stubout.StubOutForTesting() - - def tearDown(self): - self.stubs.UnsetAll() - - def test_get_shared_ip_groups(self): - pass - - def test_create_shared_ip_group(self): - pass - - def test_delete_shared_ip_group(self): - pass -- cgit From c1190d55e130a80ac831ce15e6e30c28c5621aff Mon Sep 17 00:00:00 2001 From: mdietz Date: Fri, 8 Oct 2010 21:08:48 +0000 Subject: That's what I get for not using a good vimrc --- nova/api/openstack/servers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'nova') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index f234af7de..5d1ed9822 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -170,14 +170,14 @@ class Controller(wsgi.Controller): def action(self, req, id): """ multi-purpose method used to reboot, rebuild, and resize a server """ - user_id = req.environ['nova.context']['user']['id'] + user_id = req.environ['nova.context']['user']['id'] input_dict = self._deserialize(req.body, req) try: reboot_type = input_dict['reboot']['type'] except Exception: raise faults.Fault(webob.exc.HTTPNotImplemented()) - inst_ref = self.db.instance_get_by_internal_id(None, int(id)) - if not inst_ref or (inst_ref and not inst_ref.user_id == user_id): + inst_ref = self.db.instance_get_by_internal_id(None, int(id)) + if not inst_ref or (inst_ref and not inst_ref.user_id == user_id): return faults.Fault(exc.HTTPUnprocessableEntity()) cloud.reboot(id) -- cgit From f447e1a3a2234e0ab3a5e281442659626f8d99bd Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 11 Oct 2010 13:39:33 +0200 Subject: Rename ec2 get_console_output's instance ID argument to 'instance_id'. It's passed as a kwarg, based on key in the http query, so it must be named this way. --- nova/api/ec2/cloud.py | 6 +++--- nova/api/ec2/images.py | 3 +++ nova/fakerabbit.py | 14 ++++++++++++++ nova/rpc.py | 9 +++++++++ nova/tests/cloud_unittest.py | 31 ++++++++++++++++++++----------- 5 files changed, 49 insertions(+), 14 deletions(-) (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 175bb493c..11e54d2b5 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -258,9 +258,9 @@ class CloudController(object): def delete_security_group(self, context, group_name, **kwargs): return True - def get_console_output(self, context, ec2_id_list, **kwargs): - # ec2_id_list is passed in as a list of instances - ec2_id = ec2_id_list[0] + def get_console_output(self, context, instance_id, **kwargs): + # instance_id is passed in as a list of instances + ec2_id = instance_id[0] internal_id = ec2_id_to_internal_id(ec2_id) instance_ref = db.instance_get_by_internal_id(context, internal_id) return rpc.call('%s.%s' % (FLAGS.compute_topic, diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py index cb54cdda2..f0a43dad6 100644 --- a/nova/api/ec2/images.py +++ b/nova/api/ec2/images.py @@ -69,6 +69,9 @@ def list(context, filter_list=[]): optionally filtered by a list of image_id """ + if FLAGS.connection_type == 'fake': + return [{ 'imageId' : 'bar'}] + # FIXME: send along the list of only_images to check for response = conn(context).make_request( method='GET', diff --git a/nova/fakerabbit.py b/nova/fakerabbit.py index 068025249..835973810 100644 --- a/nova/fakerabbit.py +++ b/nova/fakerabbit.py @@ -22,6 +22,7 @@ import logging import Queue as queue from carrot.backends import base +from eventlet import greenthread class Message(base.BaseMessage): @@ -38,6 +39,7 @@ class Exchange(object): def publish(self, message, routing_key=None): logging.debug('(%s) publish (key: %s) %s', self.name, routing_key, message) + routing_key = routing_key.split('.')[0] if routing_key in self._routes: for f in self._routes[routing_key]: logging.debug('Publishing to route %s', f) @@ -94,6 +96,18 @@ class Backend(object): self._exchanges[exchange].bind(self._queues[queue].push, routing_key) + def declare_consumer(self, queue, callback, *args, **kwargs): + self.current_queue = queue + self.current_callback = callback + + def consume(self, *args, **kwargs): + while True: + item = self.get(self.current_queue) + if item: + self.current_callback(item) + raise StopIteration() + greenthread.sleep(0) + def get(self, queue, no_ack=False): if not queue in self._queues or not self._queues[queue].size(): return None diff --git a/nova/rpc.py b/nova/rpc.py index fe52ad35f..447ad3b93 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -28,6 +28,7 @@ import uuid from carrot import connection as carrot_connection from carrot import messaging +from eventlet import greenthread from twisted.internet import defer from twisted.internet import task @@ -107,6 +108,14 @@ class Consumer(messaging.Consumer): logging.exception("Failed to fetch message from queue") self.failed_connection = True + def attach_to_eventlet(self): + """Only needed for unit tests!""" + def fetch_repeatedly(): + while True: + self.fetch(enable_callbacks=True) + greenthread.sleep(0.1) + greenthread.spawn(fetch_repeatedly) + def attach_to_twisted(self): """Attach a callback to twisted that fires 10 times a second""" loop = task.LoopingCall(self.fetch, enable_callbacks=True) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 615e589cf..8e5881edb 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -16,6 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. +from base64 import b64decode import json import logging from M2Crypto import BIO @@ -63,11 +64,17 @@ class CloudTestCase(test.TrialTestCase): self.cloud = cloud.CloudController() # set up a service - self.compute = utils.import_class(FLAGS.compute_manager) + self.compute = utils.import_class(FLAGS.compute_manager)() self.compute_consumer = rpc.AdapterConsumer(connection=self.conn, topic=FLAGS.compute_topic, proxy=self.compute) - self.compute_consumer.attach_to_twisted() + self.compute_consumer.attach_to_eventlet() + self.network = utils.import_class(FLAGS.network_manager)() + self.network_consumer = rpc.AdapterConsumer(connection=self.conn, + topic=FLAGS.network_topic, + proxy=self.network) + self.network_consumer.attach_to_eventlet() + self.manager = manager.AuthManager() self.user = self.manager.create_user('admin', 'admin', 'admin', True) @@ -85,15 +92,17 @@ class CloudTestCase(test.TrialTestCase): return cloud._gen_key(self.context, self.context.user.id, name) def test_console_output(self): - if FLAGS.connection_type == 'fake': - logging.debug("Can't test instances without a real virtual env.") - return - instance_id = 'foo' - inst = yield self.compute.run_instance(instance_id) - output = yield self.cloud.get_console_output(self.context, [instance_id]) - logging.debug(output) - self.assert_(output) - rv = yield self.compute.terminate_instance(instance_id) + image_id = FLAGS.default_image + instance_type = FLAGS.default_instance_type + max_count = 1 + kwargs = {'image_id': image_id, + 'instance_type': instance_type, + 'max_count': max_count } + rv = yield self.cloud.run_instances(self.context, **kwargs) + instance_id = rv['instancesSet'][0]['instanceId'] + output = yield self.cloud.get_console_output(context=self.context, instance_id=[instance_id]) + self.assertEquals(b64decode(output['output']), 'FAKE CONSOLE OUTPUT') + rv = yield self.cloud.terminate_instances(self.context, [instance_id]) def test_key_generation(self): -- cgit From 3ca549942e96e4ff769e914f227919f3a4d98686 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 11 Oct 2010 14:09:24 +0200 Subject: If machine manifest includes a kernel and/or ramdisk id, include it in the image's metadata. --- nova/objectstore/image.py | 10 ++++++-- nova/tests/bundle/1mb.manifest.xml | 2 +- .../bundle/1mb.no_kernel_or_ramdisk.manifest.xml | 1 + nova/tests/objectstore_unittest.py | 29 ++++++++++++++++++---- 4 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 nova/tests/bundle/1mb.no_kernel_or_ramdisk.manifest.xml (limited to 'nova') diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index def1b8167..c01b041bb 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -191,14 +191,14 @@ class Image(object): if kernel_id == 'true': image_type = 'kernel' except: - pass + kernel_id = None try: ramdisk_id = manifest.find("machine_configuration/ramdisk_id").text if ramdisk_id == 'true': image_type = 'ramdisk' except: - pass + ramdisk_id = None info = { 'imageId': image_id, @@ -209,6 +209,12 @@ class Image(object): 'imageType' : image_type } + if kernel_id: + info['kernelId'] = kernel_id + + if ramdisk_id: + info['ramdiskId'] = ramdisk_id + def write_state(state): info['imageState'] = state with open(os.path.join(image_path, 'info.json'), "w") as f: diff --git a/nova/tests/bundle/1mb.manifest.xml b/nova/tests/bundle/1mb.manifest.xml index dc3315957..01648a544 100644 --- a/nova/tests/bundle/1mb.manifest.xml +++ b/nova/tests/bundle/1mb.manifest.xml @@ -1 +1 @@ -2007-10-10euca-tools1.231337x86_641mb42machineda39a3ee5e6b4b0d3255bfef95601890afd807091048576113633a2ea00dc64083dd9a10eb5e233635b42a7beb1670ab75452087d9de74c60aba1cd27c136fda56f62beb581de128fb1f10d072b9e556fd25e903107a57827c21f6ee8a93a4ff55b11311fcef217e3eefb07e81f71e88216f43b4b54029c1f2549f2925a839a73947d2d5aeecec4a62ece4af9156d557ae907978298296d99154c11147fd8caf92447e90ce339928933d7579244c2f8ffb07cc0ea35f8738da8b90eff6c7a49671a84500e993e9462e4c36d5c19c0b3a2b397d035b4c0cce742b58e12552175d81d129b0425e9f71ebacb9aeb539fa9dd2ac36749fb82876f6902e5fb24b6ec19f35ec4c20acd50437fd30966e99c4d9a0647577970a8fa302314bd082c9715f071160c69bbfb070f51d2ba1076775f1d988ccde150e515088156b248e4b5a64e46c4fe064feeeedfe14511f7fde478a51acb89f9b2f6c84b60593e5c3f792ba6b01fed9bf2158fdac03086374883b39d13a3ca74497eeaaf579fc3f26effc73bfd9446a2a8c4061f0874bfaca058905180e22d3d8881551cb38f7606f19f00e4e19535dd234b66b31b77e9c7bad3885d9c9efa75c863631fd4f82a009e17d789066d9cc6032a436f05384832f6d9a3283d3e63eab04fa0da5c8c87db9b17e854e842c3fb416507d067a266b44538125ce732e486098e8ebd1ca91fa3079f007fce7d14957a9b7e57282407ead3c6eb68fe975df3d83190021b1mb.part.0c4413423cf7a57e71187e19bfd5cd4b514a642831mb.part.19d4262e6589393d09a11a0332af169887bc2e57d4e00b5ba28114dda4a9df7eeae94be847ec46117a09a1cbe41e578660642f0660dda1776b39fb3bf826b6cfec019e2a5e9c566728d186b7400ebc989a30670eb1db26ce01e68bd9d3f31290370077a85b81c66b63c1e0d5499bac115c06c17a21a81b6d3a67ebbce6c17019095af7ab07f3796c708cc843e58efc12ddc788c5e \ No newline at end of file +2007-10-10euca-tools1.231337x86_64aki-testari-test1mb42machineda39a3ee5e6b4b0d3255bfef95601890afd807091048576113633a2ea00dc64083dd9a10eb5e233635b42a7beb1670ab75452087d9de74c60aba1cd27c136fda56f62beb581de128fb1f10d072b9e556fd25e903107a57827c21f6ee8a93a4ff55b11311fcef217e3eefb07e81f71e88216f43b4b54029c1f2549f2925a839a73947d2d5aeecec4a62ece4af9156d557ae907978298296d99154c11147fd8caf92447e90ce339928933d7579244c2f8ffb07cc0ea35f8738da8b90eff6c7a49671a84500e993e9462e4c36d5c19c0b3a2b397d035b4c0cce742b58e12552175d81d129b0425e9f71ebacb9aeb539fa9dd2ac36749fb82876f6902e5fb24b6ec19f35ec4c20acd50437fd30966e99c4d9a0647577970a8fa302314bd082c9715f071160c69bbfb070f51d2ba1076775f1d988ccde150e515088156b248e4b5a64e46c4fe064feeeedfe14511f7fde478a51acb89f9b2f6c84b60593e5c3f792ba6b01fed9bf2158fdac03086374883b39d13a3ca74497eeaaf579fc3f26effc73bfd9446a2a8c4061f0874bfaca058905180e22d3d8881551cb38f7606f19f00e4e19535dd234b66b31b77e9c7bad3885d9c9efa75c863631fd4f82a009e17d789066d9cc6032a436f05384832f6d9a3283d3e63eab04fa0da5c8c87db9b17e854e842c3fb416507d067a266b44538125ce732e486098e8ebd1ca91fa3079f007fce7d14957a9b7e57282407ead3c6eb68fe975df3d83190021b1mb.part.0c4413423cf7a57e71187e19bfd5cd4b514a642831mb.part.19d4262e6589393d09a11a0332af169887bc2e57d4e00b5ba28114dda4a9df7eeae94be847ec46117a09a1cbe41e578660642f0660dda1776b39fb3bf826b6cfec019e2a5e9c566728d186b7400ebc989a30670eb1db26ce01e68bd9d3f31290370077a85b81c66b63c1e0d5499bac115c06c17a21a81b6d3a67ebbce6c17019095af7ab07f3796c708cc843e58efc12ddc788c5e diff --git a/nova/tests/bundle/1mb.no_kernel_or_ramdisk.manifest.xml b/nova/tests/bundle/1mb.no_kernel_or_ramdisk.manifest.xml new file mode 100644 index 000000000..73d7ace00 --- /dev/null +++ b/nova/tests/bundle/1mb.no_kernel_or_ramdisk.manifest.xml @@ -0,0 +1 @@ +2007-10-10euca-tools1.231337x86_641mb42machineda39a3ee5e6b4b0d3255bfef95601890afd807091048576113633a2ea00dc64083dd9a10eb5e233635b42a7beb1670ab75452087d9de74c60aba1cd27c136fda56f62beb581de128fb1f10d072b9e556fd25e903107a57827c21f6ee8a93a4ff55b11311fcef217e3eefb07e81f71e88216f43b4b54029c1f2549f2925a839a73947d2d5aeecec4a62ece4af9156d557ae907978298296d99154c11147fd8caf92447e90ce339928933d7579244c2f8ffb07cc0ea35f8738da8b90eff6c7a49671a84500e993e9462e4c36d5c19c0b3a2b397d035b4c0cce742b58e12552175d81d129b0425e9f71ebacb9aeb539fa9dd2ac36749fb82876f6902e5fb24b6ec19f35ec4c20acd50437fd30966e99c4d9a0647577970a8fa302314bd082c9715f071160c69bbfb070f51d2ba1076775f1d988ccde150e515088156b248e4b5a64e46c4fe064feeeedfe14511f7fde478a51acb89f9b2f6c84b60593e5c3f792ba6b01fed9bf2158fdac03086374883b39d13a3ca74497eeaaf579fc3f26effc73bfd9446a2a8c4061f0874bfaca058905180e22d3d8881551cb38f7606f19f00e4e19535dd234b66b31b77e9c7bad3885d9c9efa75c863631fd4f82a009e17d789066d9cc6032a436f05384832f6d9a3283d3e63eab04fa0da5c8c87db9b17e854e842c3fb416507d067a266b44538125ce732e486098e8ebd1ca91fa3079f007fce7d14957a9b7e57282407ead3c6eb68fe975df3d83190021b1mb.part.0c4413423cf7a57e71187e19bfd5cd4b514a642831mb.part.19d4262e6589393d09a11a0332af169887bc2e57d4e00b5ba28114dda4a9df7eeae94be847ec46117a09a1cbe41e578660642f0660dda1776b39fb3bf826b6cfec019e2a5e9c566728d186b7400ebc989a30670eb1db26ce01e68bd9d3f31290370077a85b81c66b63c1e0d5499bac115c06c17a21a81b6d3a67ebbce6c17019095af7ab07f3796c708cc843e58efc12ddc788c5e diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index 5a599ff3a..eb2ee0406 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -133,13 +133,22 @@ class ObjectStoreTestCase(test.TrialTestCase): self.assertRaises(NotFound, objectstore.bucket.Bucket, 'new_bucket') def test_images(self): + self.do_test_images('1mb.manifest.xml', True, + 'image_bucket1', 'i-testing1') + + def test_images_no_kernel_or_ramdisk(self): + self.do_test_images('1mb.no_kernel_or_ramdisk.manifest.xml', + False, 'image_bucket2', 'i-testing2') + + def do_test_images(self, manifest_file, expect_kernel_and_ramdisk, + image_bucket, image_name): "Test the image API." self.context.user = self.auth_manager.get_user('user1') self.context.project = self.auth_manager.get_project('proj1') # create a bucket for our bundle - objectstore.bucket.Bucket.create('image_bucket', self.context) - bucket = objectstore.bucket.Bucket('image_bucket') + objectstore.bucket.Bucket.create(image_bucket, self.context) + bucket = objectstore.bucket.Bucket(image_bucket) # upload an image manifest/parts bundle_path = os.path.join(os.path.dirname(__file__), 'bundle') @@ -147,18 +156,28 @@ class ObjectStoreTestCase(test.TrialTestCase): bucket[os.path.basename(path)] = open(path, 'rb').read() # register an image - image.Image.register_aws_image('i-testing', - 'image_bucket/1mb.manifest.xml', + image.Image.register_aws_image(image_name, + '%s/%s' % (image_bucket, manifest_file), self.context) # verify image - my_img = image.Image('i-testing') + my_img = image.Image(image_name) result_image_file = os.path.join(my_img.path, 'image') self.assertEqual(os.stat(result_image_file).st_size, 1048576) sha = hashlib.sha1(open(result_image_file).read()).hexdigest() self.assertEqual(sha, '3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3') + if expect_kernel_and_ramdisk: + # Verify the default kernel and ramdisk are set + self.assertEqual(my_img.metadata['kernelId'], 'aki-test') + self.assertEqual(my_img.metadata['ramdiskId'], 'ari-test') + else: + # Verify that the default kernel and ramdisk (the one from FLAGS) + # doesn't get embedded in the metadata + self.assertFalse('kernelId' in my_img.metadata) + self.assertFalse('ramdiskId' in my_img.metadata) + # verify image permissions self.context.user = self.auth_manager.get_user('user2') self.context.project = self.auth_manager.get_project('proj2') -- cgit From 76a76244ccee2502903a67f3f17dda97664e6687 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 11 Oct 2010 11:21:26 -0400 Subject: Fix bug 658444 --- nova/api/ec2/cloud.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 175bb493c..ca0f64e99 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -659,7 +659,12 @@ class CloudController(object): return self._format_run_instances(context, reservation_id) - def terminate_instances(self, context, ec2_id_list, **kwargs): + def terminate_instances(self, context, instance_id, **kwargs): + """Terminate each instance in instance_id, which is a list of ec2 ids. + + instance_id is a kwarg so its name cannot be modified. + """ + ec2_id_list = instance_id logging.debug("Going to start terminating instances") for id_str in ec2_id_list: internal_id = ec2_id_to_internal_id(id_str) -- cgit From f9b2f70f22bdc8a9cf08ada5f7ec45eea6060866 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 11 Oct 2010 11:43:58 -0400 Subject: Rename rsapi to osapi, and make the default subdomain for OpenStack API calls be 'api' instead of 'rs'. --- nova/api/__init__.py | 26 +++++++++++++------------- nova/tests/api/__init__.py | 4 ++-- nova/tests/api/openstack/fakes.py | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) (limited to 'nova') diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 627883018..8ec7094d7 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -31,12 +31,12 @@ from nova.api import openstack from nova.api.ec2 import metadatarequesthandler -flags.DEFINE_string('rsapi_subdomain', 'rs', - 'subdomain running the RS API') +flags.DEFINE_string('osapi_subdomain', 'api', + 'subdomain running the OpenStack API') flags.DEFINE_string('ec2api_subdomain', 'ec2', 'subdomain running the EC2 API') flags.DEFINE_string('FAKE_subdomain', None, - 'set to rs or ec2 to fake the subdomain of the host for testing') + 'set to api or ec2 to fake the subdomain of the host for testing') FLAGS = flags.FLAGS @@ -44,21 +44,21 @@ class API(wsgi.Router): """Routes top-level requests to the appropriate controller.""" def __init__(self): - rsdomain = {'sub_domain': [FLAGS.rsapi_subdomain]} + osapidomain = {'sub_domain': [FLAGS.osapi_subdomain]} ec2domain = {'sub_domain': [FLAGS.ec2api_subdomain]} - # If someone wants to pretend they're hitting the RS subdomain - # on their local box, they can set FAKE_subdomain to 'rs', which - # removes subdomain restrictions from the RS routes below. - if FLAGS.FAKE_subdomain == 'rs': - rsdomain = {} + # If someone wants to pretend they're hitting the OSAPI subdomain + # on their local box, they can set FAKE_subdomain to 'api', which + # removes subdomain restrictions from the OpenStack API routes below. + if FLAGS.FAKE_subdomain == 'api': + osapidomain = {} elif FLAGS.FAKE_subdomain == 'ec2': ec2domain = {} mapper = routes.Mapper() mapper.sub_domains = True - mapper.connect("/", controller=self.rsapi_versions, - conditions=rsdomain) + mapper.connect("/", controller=self.osapi_versions, + conditions=osapidomain) mapper.connect("/v1.0/{path_info:.*}", controller=openstack.API(), - conditions=rsdomain) + conditions=osapidomain) mapper.connect("/", controller=self.ec2api_versions, conditions=ec2domain) @@ -81,7 +81,7 @@ class API(wsgi.Router): super(API, self).__init__(mapper) @webob.dec.wsgify - def rsapi_versions(self, req): + def osapi_versions(self, req): """Respond to a request for all OpenStack API versions.""" response = { "versions": [ diff --git a/nova/tests/api/__init__.py b/nova/tests/api/__init__.py index 2c7f7fd3e..f051e2390 100644 --- a/nova/tests/api/__init__.py +++ b/nova/tests/api/__init__.py @@ -46,7 +46,7 @@ class Test(unittest.TestCase): def test_openstack(self): self.stubs.Set(api.openstack, 'API', APIStub) - result = self._request('/v1.0/cloud', 'rs') + result = self._request('/v1.0/cloud', 'api') self.assertEqual(result.body, "/cloud") def test_ec2(self): @@ -61,7 +61,7 @@ class Test(unittest.TestCase): self.assertNotEqual(result.body, "/cloud") def test_query_api_versions(self): - result = self._request('/', 'rs') + result = self._request('/', 'api') self.assertTrue('CURRENT' in result.body) def test_metadata(self): diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 1119fa714..34bc1f2a9 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -105,7 +105,7 @@ def stub_out_networking(stubs): def get_my_ip(): return '127.0.0.1' stubs.Set(nova.utils, 'get_my_ip', get_my_ip) - FLAGS.FAKE_subdomain = 'rs' + FLAGS.FAKE_subdomain = 'api' def stub_out_glance(stubs): -- cgit From 3894e22d517447fb3d5e9c367ffd2e67162f4b0f Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 12 Oct 2010 13:09:35 -0400 Subject: Fix bug 659330 --- nova/db/sqlalchemy/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index ebcb73413..fc99a535d 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -169,7 +169,7 @@ class Instance(BASE, NovaBase): @property def name(self): - return self.internal_id + return "instance-%d" % self.internal_id image_id = Column(String(255)) kernel_id = Column(String(255)) -- cgit From 32ea289d13a7ec9d273a57d2bf30484b80bfebec Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 12 Oct 2010 13:42:43 -0400 Subject: Now that the ec2 id is not the same as the name of the instance, don't compare internal_id [nee ec2_id] to instance names provided by the virtualization driver. Compare names directly instead. --- nova/compute/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 131fac406..99705d3a9 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -67,7 +67,7 @@ class ComputeManager(manager.Manager): def run_instance(self, context, instance_id, **_kwargs): """Launch a new instance with specified options.""" instance_ref = self.db.instance_get(context, instance_id) - if instance_ref['internal_id'] in self.driver.list_instances(): + if instance_ref['name'] in self.driver.list_instances(): raise exception.Error("Instance has already been created") logging.debug("instance %s: starting...", instance_id) project_id = instance_ref['project_id'] -- cgit