diff options
Diffstat (limited to 'nova/api')
| -rw-r--r-- | nova/api/__init__.py | 1 | ||||
| -rw-r--r-- | nova/api/openstack/__init__.py | 126 | ||||
| -rw-r--r-- | nova/api/openstack/auth.py | 59 | ||||
| -rw-r--r-- | nova/api/openstack/common.py | 36 | ||||
| -rw-r--r-- | nova/api/openstack/flavors.py | 3 | ||||
| -rw-r--r-- | nova/api/openstack/images.py | 86 | ||||
| -rw-r--r-- | nova/api/openstack/ratelimiting/__init__.py | 96 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py | 32 | ||||
| -rw-r--r-- | nova/api/openstack/sharedipgroups.py | 20 |
9 files changed, 316 insertions, 143 deletions
diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 803470570..26fed847b 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -24,6 +24,7 @@ Root WSGI middleware for all API controllers. :ec2api_subdomain: subdomain running the EC2 API (default: ec2) """ +import logging import routes import webob.dec diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index de95ee548..bebcdc18c 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -45,10 +45,14 @@ from nova.auth import manager FLAGS = flags.FLAGS -flags.DEFINE_string('nova_api_auth', - 'nova.api.openstack.auth.BasicApiAuthManager', +flags.DEFINE_string('os_api_auth', + 'nova.api.openstack.auth.AuthMiddleware', 'The auth mechanism to use for the OpenStack API implemenation') +flags.DEFINE_string('os_api_ratelimiting', + 'nova.api.openstack.ratelimiting.RateLimitingMiddleware', + 'Default ratelimiting implementation for the Openstack API') + flags.DEFINE_bool('allow_admin_api', False, 'When True, this API service will accept admin operations.') @@ -58,7 +62,10 @@ class API(wsgi.Middleware): """WSGI entry point for all OpenStack API requests.""" def __init__(self): - app = AuthMiddleware(RateLimitingMiddleware(APIRouter())) + auth_middleware = utils.import_class(FLAGS.os_api_auth) + ratelimiting_middleware = \ + utils.import_class(FLAGS.os_api_ratelimiting) + app = auth_middleware(ratelimiting_middleware(APIRouter())) super(API, self).__init__(app) @webob.dec.wsgify @@ -67,101 +74,11 @@ class API(wsgi.Middleware): return req.get_response(self.application) except Exception as ex: logging.warn(_("Caught error: %s") % str(ex)) - logging.debug(traceback.format_exc()) + logging.error(traceback.format_exc()) exc = webob.exc.HTTPInternalServerError(explanation=str(ex)) return faults.Fault(exc) -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 'X-Auth-Token' not in req.headers: - 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()) - - req.environ['nova.context'] = context.RequestContext(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. - """ - action_name = self.get_action_name(req) - if not action_name: - # Not rate limited - return self.application - delay = self.get_delay(action_name, - req.environ['nova.context'].user_id) - 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 @@ -176,6 +93,8 @@ class APIRouter(wsgi.Router): logging.debug("Including admin operations in API.") server_members['pause'] = 'POST' server_members['unpause'] = 'POST' + server_members['suspend'] = 'POST' + server_members['resume'] = 'POST' mapper.resource("server", "servers", controller=servers.Controller(), collection={'detail': 'GET'}, @@ -194,22 +113,3 @@ class APIRouter(wsgi.Router): 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/auth.py b/nova/api/openstack/auth.py index fcda97ab1..e24e58fd3 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -1,3 +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.import datetime + import datetime import hashlib import json @@ -7,29 +24,45 @@ import webob.exc import webob.dec from nova import auth +from nova import context from nova import db from nova import flags from nova import manager from nova import utils +from nova import wsgi from nova.api.openstack import faults FLAGS = flags.FLAGS -class Context(object): - pass - - -class BasicApiAuthManager(object): - """ Implements a somewhat rudimentary version of OpenStack Auth""" +class AuthMiddleware(wsgi.Middleware): + """Authorize the openstack API request or return an HTTP Forbidden.""" - def __init__(self, db_driver=None): + def __init__(self, application, db_driver=None): 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__() + super(AuthMiddleware, self).__init__(application) + + @webob.dec.wsgify + def __call__(self, req): + if not self.has_authentication(req): + return self.authenticate(req) + + user = self.get_user_by_authentication(req) + + if not user: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + req.environ['nova.context'] = context.RequestContext(user, user) + return self.application + + def has_authentication(self, req): + return 'X-Auth-Token' in req.headers + + def get_user_by_authentication(self, req): + return self.authorize_token(req.headers["X-Auth-Token"]) def authenticate(self, req): # Unless the request is explicitly made against /<version>/ don't @@ -68,11 +101,12 @@ class BasicApiAuthManager(object): 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) + ctxt = context.get_admin_context() + token = self.db.auth_get_token(ctxt, token_hash) if token: delta = datetime.datetime.now() - token.created_at if delta.days >= 2: - self.db.auth_destroy_token(self.context, token) + self.db.auth_destroy_token(ctxt, token) else: return self.auth.get_user(token.user_id) return None @@ -84,6 +118,7 @@ class BasicApiAuthManager(object): key - string API key req - webob.Request object """ + ctxt = context.get_admin_context() user = self.auth.get_user_from_access_key(key) if user and user.name == username: token_hash = hashlib.sha1('%s%s%f' % (username, key, @@ -95,6 +130,6 @@ class BasicApiAuthManager(object): token_dict['server_management_url'] = req.url token_dict['storage_url'] = '' token_dict['user_id'] = user.id - token = self.db.auth_create_token(self.context, token_dict) + token = self.db.auth_create_token(ctxt, token_dict) return token, user return None, None diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py new file mode 100644 index 000000000..ac0572c96 --- /dev/null +++ b/nova/api/openstack/common.py @@ -0,0 +1,36 @@ +# 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. + + +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/flavors.py b/nova/api/openstack/flavors.py index f23f74fd1..f620d4107 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -18,6 +18,7 @@ from webob import exc from nova.api.openstack import faults +from nova.api.openstack import common from nova.compute import instance_types from nova import wsgi import nova.api.openstack @@ -39,7 +40,7 @@ class Controller(wsgi.Controller): 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) + items = common.limited(items, req) return dict(flavors=items) def show(self, req, id): diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 4a0a8e6f1..ba35fbc78 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -22,12 +22,73 @@ from nova import utils from nova import wsgi import nova.api.openstack import nova.image.service + +from nova.api.openstack import common from nova.api.openstack import faults FLAGS = flags.FLAGS +def _translate_keys(item): + """ + Maps key names to Rackspace-like attributes for return + also pares down attributes to those we want + item is a dict + + Note: should be removed when the set of keys expected by the api + and the set of keys returned by the image service are equivalent + + """ + # TODO(tr3buchet): this map is specific to s3 object store, + # replace with a list of keys for _filter_keys later + mapped_keys = {'status': 'imageState', + 'id': 'imageId', + 'name': 'imageLocation'} + + mapped_item = {} + # TODO(tr3buchet): + # this chunk of code works with s3 and the local image service/glance + # when we switch to glance/local image service it can be replaced with + # a call to _filter_keys, and mapped_keys can be changed to a list + try: + for k, v in mapped_keys.iteritems(): + # map s3 fields + mapped_item[k] = item[v] + except KeyError: + # return only the fields api expects + mapped_item = _filter_keys(item, mapped_keys.keys()) + + return mapped_item + + +def _translate_status(item): + """ + Translates status of image to match current Rackspace api bindings + item is a dict + + Note: should be removed when the set of statuses expected by the api + and the set of statuses returned by the image service are equivalent + + """ + status_mapping = { + 'pending': 'queued', + 'decrypting': 'preparing', + 'untarring': 'saving', + 'available': 'active'} + item['status'] = status_mapping[item['status']] + return item + + +def _filter_keys(item, keys): + """ + Filters all model attributes except for keys + item is a dict + + """ + return dict((k, v) for k, v in item.iteritems() if k in keys) + + class Controller(wsgi.Controller): _serialization_metadata = { @@ -40,24 +101,25 @@ class Controller(wsgi.Controller): 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']]) + """Return all public images in brief""" + items = self._service.index(req.environ['nova.context']) + items = common.limited(items, req) + items = [_filter_keys(item, ('id', 'name')) for item in items] + return dict(images=items) def detail(self, req): - """Return all public images in detail.""" + """Return all public images in detail""" try: - images = self._service.detail(req.environ['nova.context']) - images = nova.api.openstack.limited(images, req) + items = self._service.detail(req.environ['nova.context']) except NotImplementedError: - # Emulate detail() using repeated calls to show() - images = self._service.index(ctxt) - images = nova.api.openstack.limited(images, req) - images = [self._service.show(ctxt, i['id']) for i in images] - return dict(images=images) + items = self._service.index(req.environ['nova.context']) + items = common.limited(items, req) + items = [_translate_keys(item) for item in items] + items = [_translate_status(item) for item in items] + return dict(images=items) def show(self, req, id): - """Return data about the given image id.""" + """Return data about the given image id""" return dict(image=self._service.show(req.environ['nova.context'], id)) def delete(self, req, id): diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py index 918caf055..91a8b2e55 100644 --- a/nova/api/openstack/ratelimiting/__init__.py +++ b/nova/api/openstack/ratelimiting/__init__.py @@ -1,3 +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.import datetime + """Rate limiting of arbitrary actions.""" import httplib @@ -6,6 +23,8 @@ import urllib import webob.dec import webob.exc +from nova import wsgi +from nova.api.openstack import faults # Convenience constants for the limits dictionary passed to Limiter(). PER_SECOND = 1 @@ -14,6 +33,83 @@ PER_HOUR = 60 * 60 PER_DAY = 60 * 60 * 24 +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. + """ + if not service_host: + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = Limiter(limits={ + 'DELETE': (100, PER_MINUTE), + 'PUT': (10, PER_MINUTE), + 'POST': (10, PER_MINUTE), + 'POST servers': (50, PER_DAY), + 'GET changes-since': (3, PER_MINUTE), + }) + else: + self.limiter = WSGIAppProxy(service_host) + super(RateLimitingMiddleware, self).__init__(application) + + @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. + """ + return self.limited_request(req, self.application) + + def limited_request(self, req, application): + """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. + """ + action_name = self.get_action_name(req) + if not action_name: + # Not rate limited + return application + delay = self.get_delay(action_name, + req.environ['nova.context'].user_id) + 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 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 Limiter(object): """Class providing rate limiting of arbitrary actions.""" diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 45db89cbf..849d0bb93 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -22,6 +22,7 @@ from webob import exc from nova import exception from nova import wsgi +from nova.api.openstack import common from nova.api.openstack import faults from nova.auth import manager as auth_manager from nova.compute import api as compute_api @@ -45,7 +46,8 @@ def _entity_detail(inst): power_state.NOSTATE: 'build', power_state.RUNNING: 'active', power_state.BLOCKED: 'active', - power_state.PAUSED: 'suspended', + power_state.SUSPENDED: 'suspended', + power_state.PAUSED: 'error', power_state.SHUTDOWN: 'active', power_state.SHUTOFF: 'active', power_state.CRASHED: 'error'} @@ -98,7 +100,7 @@ class Controller(wsgi.Controller): """ instance_list = self.compute_api.get_instances( req.environ['nova.context']) - limited_list = nova.api.openstack.limited(instance_list, req) + limited_list = common.limited(instance_list, req) res = [entity_maker(inst)['server'] for inst in limited_list] return _entity_list(res) @@ -181,7 +183,7 @@ class Controller(wsgi.Controller): self.compute_api.pause(ctxt, id) except: readable = traceback.format_exc() - logging.error("Compute.api::pause %s", readable) + logging.error(_("Compute.api::pause %s"), readable) return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() @@ -192,7 +194,29 @@ class Controller(wsgi.Controller): self.compute_api.unpause(ctxt, id) except: readable = traceback.format_exc() - logging.error("Compute.api::unpause %s", readable) + logging.error(_("Compute.api::unpause %s"), readable) + return faults.Fault(exc.HTTPUnprocessableEntity()) + return exc.HTTPAccepted() + + def suspend(self, req, id): + """permit admins to suspend the server""" + context = req.environ['nova.context'] + try: + self.compute_api.suspend(context, id) + except: + readable = traceback.format_exc() + logging.error(_("compute.api::suspend %s"), readable) + return faults.Fault(exc.HTTPUnprocessableEntity()) + return exc.HTTPAccepted() + + def resume(self, req, id): + """permit admins to resume the server from suspend""" + context = req.environ['nova.context'] + try: + self.compute_api.resume(context, id) + except: + readable = traceback.format_exc() + logging.error(_("compute.api::resume %s"), readable) return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py index e805ca9f7..75d02905c 100644 --- a/nova/api/openstack/sharedipgroups.py +++ b/nova/api/openstack/sharedipgroups.py @@ -19,4 +19,22 @@ from nova import wsgi class Controller(wsgi.Controller): - pass + """ The Shared IP Groups Controller for the Openstack API """ + + def index(self, req): + raise NotImplementedError + + def show(self, req, id): + raise NotImplementedError + + def update(self, req, id): + raise NotImplementedError + + def delete(self, req, id): + raise NotImplementedError + + def detail(self, req): + raise NotImplementedError + + def create(self, req): + raise NotImplementedError |
