From a6bd6f8581b9c03da9aceed7d87f4664410d0998 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 26 Aug 2010 14:09:14 -0400 Subject: work endpoint/images.py into an S3ImageService. The translation isn't perfect, but it's a start. --- nova/endpoint/images.py | 30 +++++------------------------- nova/image/service.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/nova/endpoint/images.py b/nova/endpoint/images.py index 2a88d66af..cfea4c20b 100644 --- a/nova/endpoint/images.py +++ b/nova/endpoint/images.py @@ -26,6 +26,7 @@ import urllib import boto.s3.connection +from nova import image from nova import flags from nova import utils from nova.auth import manager @@ -35,7 +36,7 @@ FLAGS = flags.FLAGS def modify(context, image_id, operation): - conn(context).make_request( + image.S3ImageService(context)._conn().make_request( method='POST', bucket='_images', query_args=qs({'image_id': image_id, 'operation': operation})) @@ -47,7 +48,7 @@ def register(context, image_location): """ rpc call to register a new image based from a manifest """ image_id = utils.generate_uid('ami') - conn(context).make_request( + image.S3ImageService(context)._conn().make_request( method='PUT', bucket='_images', query_args=qs({'image_location': image_location, @@ -61,12 +62,7 @@ def list(context, filter_list=[]): optionally filtered by a list of image_id """ - # FIXME: send along the list of only_images to check for - response = conn(context).make_request( - method='GET', - bucket='_images') - - result = json.loads(response.read()) + result = image.S3ImageService(context).index().values() if not filter_list is None: return [i for i in result if i['imageId'] in filter_list] return result @@ -74,23 +70,7 @@ def list(context, filter_list=[]): def deregister(context, image_id): """ unregister an image """ - conn(context).make_request( - method='DELETE', - bucket='_images', - query_args=qs({'image_id': image_id})) - - -def conn(context): - access = manager.AuthManager().get_access_key(context.user, - context.project) - secret = str(context.user.secret) - calling = boto.s3.connection.OrdinaryCallingFormat() - return boto.s3.connection.S3Connection(aws_access_key_id=access, - aws_secret_access_key=secret, - is_secure=False, - calling_format=calling, - port=FLAGS.s3_port, - host=FLAGS.s3_host) + image.S3ImageService(context).delete(image_id) def qs(params): diff --git a/nova/image/service.py b/nova/image/service.py index 1a7a258b7..25e4bb675 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -38,6 +38,8 @@ class ImageService(object): def show(self, id): """ Returns a dict containing image data for the given opaque image id. + + Returns None if the id does not exist. """ @@ -88,3 +90,46 @@ class LocalImageService(ImageService): Delete the given image. Raises OSError if the image does not exist. """ os.unlink(self._path_to(image_id)) + + +# TODO(gundlach): before this can be loaded dynamically in ImageService.load(), +# we'll have to make __init__() not require a context. Right now it +# is only used by the AWS API, which hard-codes it, so that's OK. +class S3ImageService(ImageService): + """Service that stores images in an S3 provider.""" + + def __init__(self, context): + self._context = context + + def index(self): + response = self._conn().make_request( + method='GET', + bucket='_images') + items = json.loads(response.read()) + return dict((item['imageId'], item) for item in items) + + def show(self, id): + response = self._conn().make_request( + method='GET', + bucket='_images', + query_args=qs({'image_id': image_id})) + return json.loads(response.read()) + + def delete(self, image_id): + self._conn().make_request( + method='DELETE', + bucket='_images', + query_args=qs({'image_id': image_id})) + + def _conn(self): + """Return a boto S3Connection to the S3 store.""" + access = manager.AuthManager().get_access_key(self._context.user, + self._context.project) + secret = str(self._context.user.secret) + calling = boto.s3.connection.OrdinaryCallingFormat() + return boto.s3.connection.S3Connection(aws_access_key_id=access, + aws_secret_access_key=secret, + is_secure=False, + calling_format=calling, + port=FLAGS.s3_port, + host=FLAGS.s3_host) -- cgit From f0223b5135059ac6535739916a297654953751fc Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 30 Aug 2010 18:38:26 -0400 Subject: Notes for converting Tornado to Eventlet --- nova/endpoint/notes.txt | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 nova/endpoint/notes.txt diff --git a/nova/endpoint/notes.txt b/nova/endpoint/notes.txt new file mode 100644 index 000000000..c1d441de0 --- /dev/null +++ b/nova/endpoint/notes.txt @@ -0,0 +1,62 @@ +bin/nova-api: + somehow listens for 'cloud_topic' rpc messages and ties them to + the cloud controller (maybe so internal calls can hit the API + via Queuing instead of via HTTP?) + hands CloudController and AdminController to APIServerApplication + and hands that to Tornado. + + +api.py: + +APIServerApplication(tornado.web.Application) + maps routes to APIRequestHandler, CloudPipRequestHandler, MetadataRequestHandler, + RootRequestHandler(just lists versions) + (and to controllers which are passed to __init__) + magical twisted mapping to it + +APIRequestHandler + execute: + authenticates request + picks controller from APIServerApplication's list + picks action from incoming request arguments + dict = APIRequest(controller, action).send(Context(user, project)) + _write_callback(dict) + self.finish() + +APIRequest + send(context, **kwargs): + dict = controller.action(context, **kwargs) + return _render_response(dict) # turns into XML + + +CloudController and AdminController: + actions return dict (or True which is converted into dict(return=True)) + actions have @rbac.allow('list', 'of', 'roles', 'or', '"all"') + actions can have @defer.inlineCallbacks which is used for yield statements + can use rpc.cast and then defer a returnValue + + +==== STRATEGY TO CONVERT TO EVENTLET+WSGI ==== + +* Controllers: + move the @rbac.allow data into an auth WSGI that is right above the call + to the controller + verify @defer.inlineCallbacks is just to allow the yield statements, then + remove the yield statements (untangle from twisted) + +* nova-api: + verify that cloud_topic is going away which I seem to remember, so we can ignore rpc + +* apiserverapplication: + replace with a Router to a wsgi.Controller + root controller all goes into a "version" action + ??? dunno what cloudpipes or metadatarequesthandlers do... + apirequesthandler stuff goes into an "ec2" action + +* apirequesthandler + ec2() method on wsgi.Controller + - basically it's execute() from old APIRequestHandler + change to return data directly instead of _write_callback() and finish() + +* apirequest + doesn't need to change -- cgit From 909c24b9cd35d6752f9f051f4e9a80ce30eaee4d Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 30 Aug 2010 19:04:51 -0400 Subject: Move class into its own file --- nova/api/ec2/apirequesthandler.py | 126 ++++++++++++++++++++++++++++++++++++++ nova/endpoint/api.py | 101 ------------------------------ 2 files changed, 126 insertions(+), 101 deletions(-) create mode 100644 nova/api/ec2/apirequesthandler.py diff --git a/nova/api/ec2/apirequesthandler.py b/nova/api/ec2/apirequesthandler.py new file mode 100644 index 000000000..bbba60c02 --- /dev/null +++ b/nova/api/ec2/apirequesthandler.py @@ -0,0 +1,126 @@ +# 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. + +""" +APIRequestHandler, pulled unmodified out of nova.endpoint.api +""" + +import logging + +import tornado.web + +from nova import exception +from nova import utils +from nova.auth import manager + + +_log = logging.getLogger("api") +_log.setLevel(logging.DEBUG) + + +class APIRequestHandler(tornado.web.RequestHandler): + def get(self, controller_name): + self.execute(controller_name) + + @tornado.web.asynchronous + def execute(self, controller_name): + # Obtain the appropriate controller for this request. + try: + controller = self.application.controllers[controller_name] + except KeyError: + self._error('unhandled', 'no controller named %s' % controller_name) + return + + args = self.request.arguments + + # Read request signature. + try: + signature = args.pop('Signature')[0] + except: + raise tornado.web.HTTPError(400) + + # Make a copy of args for authentication and signature verification. + auth_params = {} + for key, value in args.items(): + auth_params[key] = value[0] + + # Get requested action and remove authentication args for final request. + try: + action = args.pop('Action')[0] + access = args.pop('AWSAccessKeyId')[0] + args.pop('SignatureMethod') + args.pop('SignatureVersion') + args.pop('Version') + args.pop('Timestamp') + except: + raise tornado.web.HTTPError(400) + + # Authenticate the request. + try: + (user, project) = manager.AuthManager().authenticate( + access, + signature, + auth_params, + self.request.method, + self.request.host, + self.request.path + ) + + except exception.Error, ex: + logging.debug("Authentication Failure: %s" % ex) + raise tornado.web.HTTPError(403) + + _log.debug('action: %s' % action) + + for key, value in args.items(): + _log.debug('arg: %s\t\tval: %s' % (key, value)) + + request = APIRequest(controller, action) + context = APIRequestContext(self, user, project) + d = request.send(context, **args) + # d.addCallback(utils.debug) + + # TODO: Wrap response in AWS XML format + d.addCallbacks(self._write_callback, self._error_callback) + + def _write_callback(self, data): + self.set_header('Content-Type', 'text/xml') + self.write(data) + self.finish() + + def _error_callback(self, failure): + try: + failure.raiseException() + except exception.ApiError as ex: + self._error(type(ex).__name__ + "." + ex.code, ex.message) + # TODO(vish): do something more useful with unknown exceptions + except Exception as ex: + self._error(type(ex).__name__, str(ex)) + raise + + def post(self, controller_name): + self.execute(controller_name) + + def _error(self, code, message): + self._status_code = 400 + self.set_header('Content-Type', 'text/xml') + self.write('\n') + self.write('%s' + '%s' + '?' % (code, message)) + self.finish() diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 40be00bb7..25ed613b9 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -21,7 +21,6 @@ Tornado REST API Request Handlers for Nova functions Most calls are proxied into the responsible controller. """ -import logging import multiprocessing import random import re @@ -33,10 +32,7 @@ import tornado.web from twisted.internet import defer from nova import crypto -from nova import exception from nova import flags -from nova import utils -from nova.auth import manager import nova.cloudpipe.api from nova.endpoint import cloud @@ -45,10 +41,6 @@ FLAGS = flags.FLAGS flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') -_log = logging.getLogger("api") -_log.setLevel(logging.DEBUG) - - _c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') @@ -230,99 +222,6 @@ class MetadataRequestHandler(tornado.web.RequestHandler): self.finish() -class APIRequestHandler(tornado.web.RequestHandler): - def get(self, controller_name): - self.execute(controller_name) - - @tornado.web.asynchronous - def execute(self, controller_name): - # Obtain the appropriate controller for this request. - try: - controller = self.application.controllers[controller_name] - except KeyError: - self._error('unhandled', 'no controller named %s' % controller_name) - return - - args = self.request.arguments - - # Read request signature. - try: - signature = args.pop('Signature')[0] - except: - raise tornado.web.HTTPError(400) - - # Make a copy of args for authentication and signature verification. - auth_params = {} - for key, value in args.items(): - auth_params[key] = value[0] - - # Get requested action and remove authentication args for final request. - try: - action = args.pop('Action')[0] - access = args.pop('AWSAccessKeyId')[0] - args.pop('SignatureMethod') - args.pop('SignatureVersion') - args.pop('Version') - args.pop('Timestamp') - except: - raise tornado.web.HTTPError(400) - - # Authenticate the request. - try: - (user, project) = manager.AuthManager().authenticate( - access, - signature, - auth_params, - self.request.method, - self.request.host, - self.request.path - ) - - except exception.Error, ex: - logging.debug("Authentication Failure: %s" % ex) - raise tornado.web.HTTPError(403) - - _log.debug('action: %s' % action) - - for key, value in args.items(): - _log.debug('arg: %s\t\tval: %s' % (key, value)) - - request = APIRequest(controller, action) - context = APIRequestContext(self, user, project) - d = request.send(context, **args) - # d.addCallback(utils.debug) - - # TODO: Wrap response in AWS XML format - d.addCallbacks(self._write_callback, self._error_callback) - - def _write_callback(self, data): - self.set_header('Content-Type', 'text/xml') - self.write(data) - self.finish() - - def _error_callback(self, failure): - try: - failure.raiseException() - except exception.ApiError as ex: - self._error(type(ex).__name__ + "." + ex.code, ex.message) - # TODO(vish): do something more useful with unknown exceptions - except Exception as ex: - self._error(type(ex).__name__, str(ex)) - raise - - def post(self, controller_name): - self.execute(controller_name) - - def _error(self, code, message): - self._status_code = 400 - self.set_header('Content-Type', 'text/xml') - self.write('\n') - self.write('%s' - '%s' - '?' % (code, message)) - self.finish() - - class APIServerApplication(tornado.web.Application): def __init__(self, controllers): tornado.web.Application.__init__(self, [ -- cgit From be2b529a987627bf454f7343df74d4e8ae670761 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 30 Aug 2010 19:08:22 -0400 Subject: Move APIRequest into its own file --- nova/api/ec2/apirequest.py | 132 +++++++++++++++++++++++++++++++++++++++++++++ nova/endpoint/api.py | 109 ------------------------------------- 2 files changed, 132 insertions(+), 109 deletions(-) create mode 100644 nova/api/ec2/apirequest.py diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py new file mode 100644 index 000000000..1fc84248b --- /dev/null +++ b/nova/api/ec2/apirequest.py @@ -0,0 +1,132 @@ +# 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. + +""" +APIRequest class +""" + +# TODO(termie): replace minidom with etree +from xml.dom import minidom + +from twisted.internet import defer + + +_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') + + +def _camelcase_to_underscore(str): + return _c2u.sub(r'_\1', str).lower().strip('_') + + +def _underscore_to_camelcase(str): + return ''.join([x[:1].upper() + x[1:] for x in str.split('_')]) + + +def _underscore_to_xmlcase(str): + res = _underscore_to_camelcase(str) + return res[:1].lower() + res[1:] + + +class APIRequest(object): + def __init__(self, controller, action): + self.controller = controller + self.action = action + + def send(self, context, **kwargs): + + try: + method = getattr(self.controller, + _camelcase_to_underscore(self.action)) + except AttributeError: + _error = ('Unsupported API request: controller = %s,' + 'action = %s') % (self.controller, self.action) + _log.warning(_error) + # TODO: Raise custom exception, trap in apiserver, + # and reraise as 400 error. + raise Exception(_error) + + args = {} + for key, value in kwargs.items(): + parts = key.split(".") + key = _camelcase_to_underscore(parts[0]) + if len(parts) > 1: + d = args.get(key, {}) + d[parts[1]] = value[0] + value = d + else: + value = value[0] + args[key] = value + + for key in args.keys(): + if isinstance(args[key], dict): + if args[key] != {} and args[key].keys()[0].isdigit(): + s = args[key].items() + s.sort() + args[key] = [v for k, v in s] + + d = defer.maybeDeferred(method, context, **args) + d.addCallback(self._render_response, context.request_id) + return d + + def _render_response(self, response_data, request_id): + xml = minidom.Document() + + response_el = xml.createElement(self.action + 'Response') + response_el.setAttribute('xmlns', + 'http://ec2.amazonaws.com/doc/2009-11-30/') + request_id_el = xml.createElement('requestId') + request_id_el.appendChild(xml.createTextNode(request_id)) + response_el.appendChild(request_id_el) + if(response_data == True): + self._render_dict(xml, response_el, {'return': 'true'}) + else: + self._render_dict(xml, response_el, response_data) + + xml.appendChild(response_el) + + response = xml.toxml() + xml.unlink() + _log.debug(response) + return response + + def _render_dict(self, xml, el, data): + try: + for key in data.keys(): + val = data[key] + el.appendChild(self._render_data(xml, key, val)) + except: + _log.debug(data) + raise + + def _render_data(self, xml, el_name, data): + el_name = _underscore_to_xmlcase(el_name) + data_el = xml.createElement(el_name) + + if isinstance(data, list): + for item in data: + data_el.appendChild(self._render_data(xml, 'item', item)) + elif isinstance(data, dict): + self._render_dict(xml, data_el, data) + elif hasattr(data, '__dict__'): + self._render_dict(xml, data_el, data.__dict__) + elif isinstance(data, bool): + data_el.appendChild(xml.createTextNode(str(data).lower())) + elif data != None: + data_el.appendChild(xml.createTextNode(str(data))) + + return data_el diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 25ed613b9..5a4e496a0 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -25,11 +25,8 @@ import multiprocessing import random import re import urllib -# TODO(termie): replace minidom with etree -from xml.dom import minidom import tornado.web -from twisted.internet import defer from nova import crypto from nova import flags @@ -41,22 +38,6 @@ FLAGS = flags.FLAGS flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') -_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') - - -def _camelcase_to_underscore(str): - return _c2u.sub(r'_\1', str).lower().strip('_') - - -def _underscore_to_camelcase(str): - return ''.join([x[:1].upper() + x[1:] for x in str.split('_')]) - - -def _underscore_to_xmlcase(str): - res = _underscore_to_camelcase(str) - return res[:1].lower() + res[1:] - - class APIRequestContext(object): def __init__(self, handler, user, project): self.handler = handler @@ -68,96 +49,6 @@ class APIRequestContext(object): ) -class APIRequest(object): - def __init__(self, controller, action): - self.controller = controller - self.action = action - - def send(self, context, **kwargs): - - try: - method = getattr(self.controller, - _camelcase_to_underscore(self.action)) - except AttributeError: - _error = ('Unsupported API request: controller = %s,' - 'action = %s') % (self.controller, self.action) - _log.warning(_error) - # TODO: Raise custom exception, trap in apiserver, - # and reraise as 400 error. - raise Exception(_error) - - args = {} - for key, value in kwargs.items(): - parts = key.split(".") - key = _camelcase_to_underscore(parts[0]) - if len(parts) > 1: - d = args.get(key, {}) - d[parts[1]] = value[0] - value = d - else: - value = value[0] - args[key] = value - - for key in args.keys(): - if isinstance(args[key], dict): - if args[key] != {} and args[key].keys()[0].isdigit(): - s = args[key].items() - s.sort() - args[key] = [v for k, v in s] - - d = defer.maybeDeferred(method, context, **args) - d.addCallback(self._render_response, context.request_id) - return d - - def _render_response(self, response_data, request_id): - xml = minidom.Document() - - response_el = xml.createElement(self.action + 'Response') - response_el.setAttribute('xmlns', - 'http://ec2.amazonaws.com/doc/2009-11-30/') - request_id_el = xml.createElement('requestId') - request_id_el.appendChild(xml.createTextNode(request_id)) - response_el.appendChild(request_id_el) - if(response_data == True): - self._render_dict(xml, response_el, {'return': 'true'}) - else: - self._render_dict(xml, response_el, response_data) - - xml.appendChild(response_el) - - response = xml.toxml() - xml.unlink() - _log.debug(response) - return response - - def _render_dict(self, xml, el, data): - try: - for key in data.keys(): - val = data[key] - el.appendChild(self._render_data(xml, key, val)) - except: - _log.debug(data) - raise - - def _render_data(self, xml, el_name, data): - el_name = _underscore_to_xmlcase(el_name) - data_el = xml.createElement(el_name) - - if isinstance(data, list): - for item in data: - data_el.appendChild(self._render_data(xml, 'item', item)) - elif isinstance(data, dict): - self._render_dict(xml, data_el, data) - elif hasattr(data, '__dict__'): - self._render_dict(xml, data_el, data.__dict__) - elif isinstance(data, bool): - data_el.appendChild(xml.createTextNode(str(data).lower())) - elif data != None: - data_el.appendChild(xml.createTextNode(str(data))) - - return data_el - - class RootRequestHandler(tornado.web.RequestHandler): def get(self): # available api versions -- cgit From 4bca41506c90e779a8d4a5defdca3add79073185 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 30 Aug 2010 19:10:17 -0400 Subject: Move APIRequestContext into its own file --- nova/api/ec2/apirequestcontext.py | 33 +++++++++++++++++++++++++++++++++ nova/endpoint/api.py | 12 ------------ 2 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 nova/api/ec2/apirequestcontext.py diff --git a/nova/api/ec2/apirequestcontext.py b/nova/api/ec2/apirequestcontext.py new file mode 100644 index 000000000..fb3118020 --- /dev/null +++ b/nova/api/ec2/apirequestcontext.py @@ -0,0 +1,33 @@ +# 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. + +""" +APIRequestContext +""" + +import random + +class APIRequestContext(object): + def __init__(self, handler, user, project): + self.handler = handler + self.user = user + self.project = project + self.request_id = ''.join( + [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-') + for x in xrange(20)] + ) diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 5a4e496a0..311fb1880 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -22,7 +22,6 @@ Most calls are proxied into the responsible controller. """ import multiprocessing -import random import re import urllib @@ -38,17 +37,6 @@ FLAGS = flags.FLAGS flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') -class APIRequestContext(object): - def __init__(self, handler, user, project): - self.handler = handler - self.user = user - self.project = project - self.request_id = ''.join( - [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-') - for x in xrange(20)] - ) - - class RootRequestHandler(tornado.web.RequestHandler): def get(self): # available api versions -- cgit From 1ef59040aa1304a4682c6bcdaa3333372e7f8629 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 30 Aug 2010 19:12:31 -0400 Subject: Delete __init__.py in prep for turning apirequesthandler into __init__ --- nova/api/ec2/__init__.py | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 nova/api/ec2/__init__.py diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py deleted file mode 100644 index 6eec0abf7..000000000 --- a/nova/api/ec2/__init__.py +++ /dev/null @@ -1,42 +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. - -""" -WSGI middleware for EC2 API controllers. -""" - -import routes -import webob.dec - -from nova import wsgi - - -class API(wsgi.Router): - """Routes EC2 requests to the appropriate controller.""" - - def __init__(self): - mapper = routes.Mapper() - mapper.connect(None, "{all:.*}", controller=self.dummy) - super(API, self).__init__(mapper) - - @staticmethod - @webob.dec.wsgify - def dummy(req): - """Temporary dummy controller.""" - msg = "dummy response -- please hook up __init__() to cloud.py instead" - return repr({'dummy': msg, - 'kwargs': repr(req.environ['wsgiorg.routing_args'][1])}) -- cgit From c54d6c3d1fcb0210e9f52097f1a1e85550c84bf6 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 10:03:51 -0400 Subject: First steps in reworking EC2 APIRequestHandler into separate Authenticate() and Router() WSGI apps --- nova/api/__init__.py | 4 + nova/api/ec2/__init__.py | 151 ++++++++++++++++++++++++++++++++++++++ nova/api/ec2/apirequesthandler.py | 126 ------------------------------- nova/endpoint/notes.txt | 10 +-- 4 files changed, 160 insertions(+), 131 deletions(-) create mode 100644 nova/api/ec2/__init__.py delete mode 100644 nova/api/ec2/apirequesthandler.py diff --git a/nova/api/__init__.py b/nova/api/__init__.py index b9b9e3988..0166b7fc1 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -32,6 +32,10 @@ class API(wsgi.Router): def __init__(self): mapper = routes.Mapper() + # TODO(gundlach): EC2 RootController is replaced by this class; + # MetadataRequestHandlers isn't part of the EC2 API and thus can + # be dropped; and I'm leaving off CloudPipeRequestHandler until + # I hear that we need it. mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API()) mapper.connect("/ec2/{path_info:.*}", controller=ec2.API()) super(API, self).__init__(mapper) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py new file mode 100644 index 000000000..a4d9b95f9 --- /dev/null +++ b/nova/api/ec2/__init__.py @@ -0,0 +1,151 @@ +# 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. + +""" +Starting point for routing EC2 requests +""" + +import logging +import routes +import webob.exc +from webob.dec import wsgify + +from nova.api.ec2 import admin +from nova.api.ec2 import cloud +from nova import exception +from nova import utils +from nova.auth import manager + + +_log = logging.getLogger("api") +_log.setLevel(logging.DEBUG) + + +class API(wsgi.Middleware): + """Routing for all EC2 API requests.""" + + def __init__(self): + self.application = Authenticate(Router()) + +class Authenticate(wsgi.Middleware): + """Authenticates an EC2 request.""" + + @webob.dec.wsgify + def __call__(self, req): + #TODO(gundlach): where do arguments come from? + args = self.request.arguments + + # Read request signature. + try: + signature = args.pop('Signature')[0] + except: + raise webob.exc.HTTPBadRequest() + + # Make a copy of args for authentication and signature verification. + auth_params = {} + for key, value in args.items(): + auth_params[key] = value[0] + + # Get requested action and remove authentication args for final request. + try: + action = args.pop('Action')[0] + access = args.pop('AWSAccessKeyId')[0] + args.pop('SignatureMethod') + args.pop('SignatureVersion') + args.pop('Version') + args.pop('Timestamp') + except: + raise webob.exc.HTTPBadRequest() + + # Authenticate the request. + try: + (user, project) = manager.AuthManager().authenticate( + access, + signature, + auth_params, + req.method, + req.host, + req.path + ) + + except exception.Error, ex: + logging.debug("Authentication Failure: %s" % ex) + raise webob.exc.HTTPForbidden() + + _log.debug('action: %s' % action) + + for key, value in args.items(): + _log.debug('arg: %s\t\tval: %s' % (key, value)) + + # Authenticated! + req.environ['ec2.action'] = action + req.environ['ec2.context'] = APIRequestContext(user, project) + return self.application + + +class Router(wsgi.Application): + """ + Finds controller for a request, executes environ['ec2.action'] upon it, and + returns a response. + """ + def __init__(self): + self.map = routes.Mapper() + self.map.connect("/{controller_name}/") + self.controllers = dict(Cloud=cloud.CloudController(), + Admin=admin.AdminController()) + + def __call__(self, req): + # Obtain the appropriate controller for this request. + match = self.map.match(req.path) + if not match: + raise webob.exc.HTTPNotFound() + controller_name = match['controller_name'] + + try: + controller = self.controllers[controller_name] + except KeyError: + self._error('unhandled', 'no controller named %s' % controller_name) + return + + request = APIRequest(controller, req.environ['ec2.action']) + context = req.environ['ec2.context'] + try: + data = request.send(context, **args) + req.headers['Content-Type'] = 'text/xml' + return data + #TODO(gundlach) under what conditions would _error_callbock used to + #be called? What was 'failure' that you could call .raiseException + #on it? + except Exception, ex: + try: + #TODO + failure.raiseException() + except exception.ApiError as ex: + self._error(req, type(ex).__name__ + "." + ex.code, ex.message) + # TODO(vish): do something more useful with unknown exceptions + except Exception as ex: + self._error(type(ex).__name__, str(ex)) + + def _error(self, req, code, message): + req.status = 400 + req.headers['Content-Type'] = 'text/xml' + req.response = ('\n' + '%s' + '%s' + '?') % (code, message)) + diff --git a/nova/api/ec2/apirequesthandler.py b/nova/api/ec2/apirequesthandler.py deleted file mode 100644 index bbba60c02..000000000 --- a/nova/api/ec2/apirequesthandler.py +++ /dev/null @@ -1,126 +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. - -""" -APIRequestHandler, pulled unmodified out of nova.endpoint.api -""" - -import logging - -import tornado.web - -from nova import exception -from nova import utils -from nova.auth import manager - - -_log = logging.getLogger("api") -_log.setLevel(logging.DEBUG) - - -class APIRequestHandler(tornado.web.RequestHandler): - def get(self, controller_name): - self.execute(controller_name) - - @tornado.web.asynchronous - def execute(self, controller_name): - # Obtain the appropriate controller for this request. - try: - controller = self.application.controllers[controller_name] - except KeyError: - self._error('unhandled', 'no controller named %s' % controller_name) - return - - args = self.request.arguments - - # Read request signature. - try: - signature = args.pop('Signature')[0] - except: - raise tornado.web.HTTPError(400) - - # Make a copy of args for authentication and signature verification. - auth_params = {} - for key, value in args.items(): - auth_params[key] = value[0] - - # Get requested action and remove authentication args for final request. - try: - action = args.pop('Action')[0] - access = args.pop('AWSAccessKeyId')[0] - args.pop('SignatureMethod') - args.pop('SignatureVersion') - args.pop('Version') - args.pop('Timestamp') - except: - raise tornado.web.HTTPError(400) - - # Authenticate the request. - try: - (user, project) = manager.AuthManager().authenticate( - access, - signature, - auth_params, - self.request.method, - self.request.host, - self.request.path - ) - - except exception.Error, ex: - logging.debug("Authentication Failure: %s" % ex) - raise tornado.web.HTTPError(403) - - _log.debug('action: %s' % action) - - for key, value in args.items(): - _log.debug('arg: %s\t\tval: %s' % (key, value)) - - request = APIRequest(controller, action) - context = APIRequestContext(self, user, project) - d = request.send(context, **args) - # d.addCallback(utils.debug) - - # TODO: Wrap response in AWS XML format - d.addCallbacks(self._write_callback, self._error_callback) - - def _write_callback(self, data): - self.set_header('Content-Type', 'text/xml') - self.write(data) - self.finish() - - def _error_callback(self, failure): - try: - failure.raiseException() - except exception.ApiError as ex: - self._error(type(ex).__name__ + "." + ex.code, ex.message) - # TODO(vish): do something more useful with unknown exceptions - except Exception as ex: - self._error(type(ex).__name__, str(ex)) - raise - - def post(self, controller_name): - self.execute(controller_name) - - def _error(self, code, message): - self._status_code = 400 - self.set_header('Content-Type', 'text/xml') - self.write('\n') - self.write('%s' - '%s' - '?' % (code, message)) - self.finish() diff --git a/nova/endpoint/notes.txt b/nova/endpoint/notes.txt index c1d441de0..7a85cdc93 100644 --- a/nova/endpoint/notes.txt +++ b/nova/endpoint/notes.txt @@ -17,7 +17,9 @@ APIServerApplication(tornado.web.Application) APIRequestHandler execute: authenticates request - picks controller from APIServerApplication's list + picks controller from APIServerApplication's list based on name that was at the + start of the URL (e.g. /services/Cloud has /services mapped here via + APIServerApplication then Cloud is controller_name) picks action from incoming request arguments dict = APIRequest(controller, action).send(Context(user, project)) _write_callback(dict) @@ -49,12 +51,10 @@ CloudController and AdminController: * apiserverapplication: replace with a Router to a wsgi.Controller - root controller all goes into a "version" action - ??? dunno what cloudpipes or metadatarequesthandlers do... - apirequesthandler stuff goes into an "ec2" action + apirequesthandler stuff is just an entry in api.APIRouter * apirequesthandler - ec2() method on wsgi.Controller + wsgi.Controller pointed to by api.APIRouter - basically it's execute() from old APIRequestHandler change to return data directly instead of _write_callback() and finish() -- cgit From 070d87df264ca949b51131df9287fbcee373d480 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 10:46:01 -0400 Subject: Get rid of some convoluted exception handling that we don't need in eventlet --- nova/api/ec2/__init__.py | 23 +++++++---------------- nova/api/ec2/apirequest.py | 7 ++++--- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index a4d9b95f9..46e543d0e 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -122,24 +122,15 @@ class Router(wsgi.Application): self._error('unhandled', 'no controller named %s' % controller_name) return - request = APIRequest(controller, req.environ['ec2.action']) + api_request = APIRequest(controller, req.environ['ec2.action']) context = req.environ['ec2.context'] try: - data = request.send(context, **args) - req.headers['Content-Type'] = 'text/xml' - return data - #TODO(gundlach) under what conditions would _error_callbock used to - #be called? What was 'failure' that you could call .raiseException - #on it? - except Exception, ex: - try: - #TODO - failure.raiseException() - except exception.ApiError as ex: - self._error(req, type(ex).__name__ + "." + ex.code, ex.message) - # TODO(vish): do something more useful with unknown exceptions - except Exception as ex: - self._error(type(ex).__name__, str(ex)) + return api_request.send(context, **args) + except exception.ApiError as ex: + self._error(req, type(ex).__name__ + "." + ex.code, ex.message) + # TODO(vish): do something more useful with unknown exceptions + except Exception as ex: + self._error(type(ex).__name__, str(ex)) def _error(self, req, code, message): req.status = 400 diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index 1fc84248b..77f1a7759 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -79,9 +79,10 @@ class APIRequest(object): s.sort() args[key] = [v for k, v in s] - d = defer.maybeDeferred(method, context, **args) - d.addCallback(self._render_response, context.request_id) - return d + result = method(context, **args) + + req.headers['Content-Type'] = 'text/xml' + return self._render_response(result, context.request_id) def _render_response(self, response_data, request_id): xml = minidom.Document() -- cgit From cb55d65827170dd9d54dbd22f32e5c2171f8e1b1 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 10:55:53 -0400 Subject: small import cleanup --- nova/api/ec2/__init__.py | 5 ++--- nova/api/ec2/apirequest.py | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 46e543d0e..7e345d297 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -22,13 +22,12 @@ Starting point for routing EC2 requests import logging import routes +import webob.dec import webob.exc -from webob.dec import wsgify from nova.api.ec2 import admin from nova.api.ec2 import cloud from nova import exception -from nova import utils from nova.auth import manager @@ -101,7 +100,7 @@ class Authenticate(wsgi.Middleware): class Router(wsgi.Application): """ Finds controller for a request, executes environ['ec2.action'] upon it, and - returns a response. + returns an XML response. If the action fails, returns a 400. """ def __init__(self): self.map = routes.Mapper() diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index 77f1a7759..261346a09 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -23,8 +23,6 @@ APIRequest class # TODO(termie): replace minidom with etree from xml.dom import minidom -from twisted.internet import defer - _c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') -- cgit From ab43c28e583116c4885b19afc6448192aae10096 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 12:15:29 -0400 Subject: Move cloudcontroller and admincontroller into new api --- nova/api/ec2/admin.py | 211 ++++++++++++++ nova/api/ec2/cloud.py | 739 +++++++++++++++++++++++++++++++++++++++++++++++++ nova/endpoint/admin.py | 211 -------------- nova/endpoint/cloud.py | 739 ------------------------------------------------- 4 files changed, 950 insertions(+), 950 deletions(-) create mode 100644 nova/api/ec2/admin.py create mode 100644 nova/api/ec2/cloud.py delete mode 100644 nova/endpoint/admin.py delete mode 100644 nova/endpoint/cloud.py diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py new file mode 100644 index 000000000..d6f622755 --- /dev/null +++ b/nova/api/ec2/admin.py @@ -0,0 +1,211 @@ +# 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. + +""" +Admin API controller, exposed through http via the api worker. +""" + +import base64 + +from nova.auth import manager +from nova.compute import model + + +def user_dict(user, base64_file=None): + """Convert the user object to a result dict""" + if user: + return { + 'username': user.id, + 'accesskey': user.access, + 'secretkey': user.secret, + 'file': base64_file} + else: + return {} + + +def project_dict(project): + """Convert the project object to a result dict""" + if project: + return { + 'projectname': project.id, + 'project_manager_id': project.project_manager_id, + 'description': project.description} + else: + return {} + + +def host_dict(host): + """Convert a host model object to a result dict""" + if host: + return host.state + else: + return {} + + +def admin_only(target): + """Decorator for admin-only API calls""" + def wrapper(*args, **kwargs): + """Internal wrapper method for admin-only API calls""" + context = args[1] + if context.user.is_admin(): + return target(*args, **kwargs) + else: + return {} + + return wrapper + + +class AdminController(object): + """ + API Controller for users, hosts, nodes, and workers. + Trivial admin_only wrapper will be replaced with RBAC, + allowing project managers to administer project users. + """ + + def __str__(self): + return 'AdminController' + + @admin_only + def describe_user(self, _context, name, **_kwargs): + """Returns user data, including access and secret keys.""" + return user_dict(manager.AuthManager().get_user(name)) + + @admin_only + def describe_users(self, _context, **_kwargs): + """Returns all users - should be changed to deal with a list.""" + return {'userSet': + [user_dict(u) for u in manager.AuthManager().get_users()] } + + @admin_only + def register_user(self, _context, name, **_kwargs): + """Creates a new user, and returns generated credentials.""" + return user_dict(manager.AuthManager().create_user(name)) + + @admin_only + def deregister_user(self, _context, name, **_kwargs): + """Deletes a single user (NOT undoable.) + Should throw an exception if the user has instances, + volumes, or buckets remaining. + """ + manager.AuthManager().delete_user(name) + + return True + + @admin_only + def describe_roles(self, context, project_roles=True, **kwargs): + """Returns a list of allowed roles.""" + roles = manager.AuthManager().get_roles(project_roles) + return { 'roles': [{'role': r} for r in roles]} + + @admin_only + def describe_user_roles(self, context, user, project=None, **kwargs): + """Returns a list of roles for the given user. + Omitting project will return any global roles that the user has. + Specifying project will return only project specific roles. + """ + roles = manager.AuthManager().get_user_roles(user, project=project) + return { 'roles': [{'role': r} for r in roles]} + + @admin_only + def modify_user_role(self, context, user, role, project=None, + operation='add', **kwargs): + """Add or remove a role for a user and project.""" + if operation == 'add': + manager.AuthManager().add_role(user, role, project) + elif operation == 'remove': + manager.AuthManager().remove_role(user, role, project) + else: + raise exception.ApiError('operation must be add or remove') + + return True + + @admin_only + def generate_x509_for_user(self, _context, name, project=None, **kwargs): + """Generates and returns an x509 certificate for a single user. + Is usually called from a client that will wrap this with + access and secret key info, and return a zip file. + """ + if project is None: + project = name + project = manager.AuthManager().get_project(project) + user = manager.AuthManager().get_user(name) + return user_dict(user, base64.b64encode(project.get_credentials(user))) + + @admin_only + def describe_project(self, context, name, **kwargs): + """Returns project data, including member ids.""" + return project_dict(manager.AuthManager().get_project(name)) + + @admin_only + def describe_projects(self, context, user=None, **kwargs): + """Returns all projects - should be changed to deal with a list.""" + return {'projectSet': + [project_dict(u) for u in + manager.AuthManager().get_projects(user=user)]} + + @admin_only + def register_project(self, context, name, manager_user, description=None, + member_users=None, **kwargs): + """Creates a new project""" + return project_dict( + manager.AuthManager().create_project( + name, + manager_user, + description=None, + member_users=None)) + + @admin_only + def deregister_project(self, context, name): + """Permanently deletes a project.""" + manager.AuthManager().delete_project(name) + return True + + @admin_only + def describe_project_members(self, context, name, **kwargs): + project = manager.AuthManager().get_project(name) + result = { + 'members': [{'member': m} for m in project.member_ids]} + return result + + @admin_only + def modify_project_member(self, context, user, project, operation, **kwargs): + """Add or remove a user from a project.""" + if operation =='add': + manager.AuthManager().add_to_project(user, project) + elif operation == 'remove': + manager.AuthManager().remove_from_project(user, project) + else: + raise exception.ApiError('operation must be add or remove') + return True + + @admin_only + def describe_hosts(self, _context, **_kwargs): + """Returns status info for all nodes. Includes: + * Disk Space + * Instance List + * RAM used + * CPU used + * DHCP servers running + * Iptables / bridges + """ + return {'hostSet': [host_dict(h) for h in model.Host.all()]} + + @admin_only + def describe_host(self, _context, name, **_kwargs): + """Returns status info for single node.""" + return host_dict(model.Host.lookup(name)) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py new file mode 100644 index 000000000..30634429d --- /dev/null +++ b/nova/api/ec2/cloud.py @@ -0,0 +1,739 @@ +# 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. + +""" +Cloud Controller: Implementation of EC2 REST API calls, which are +dispatched to other nodes via AMQP RPC. State is via distributed +datastore. +""" + +import base64 +import logging +import os +import time + +from twisted.internet import defer + +from nova import datastore +from nova import exception +from nova import flags +from nova import rpc +from nova import utils +from nova.auth import rbac +from nova.auth import manager +from nova.compute import model +from nova.compute.instance_types import INSTANCE_TYPES +from nova.endpoint import images +from nova.network import service as network_service +from nova.network import model as network_model +from nova.volume import service + + +FLAGS = flags.FLAGS +flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') + + +def _gen_key(user_id, key_name): + """ Tuck this into AuthManager """ + try: + mgr = manager.AuthManager() + private_key, fingerprint = mgr.generate_key_pair(user_id, key_name) + except Exception as ex: + return {'exception': ex} + return {'private_key': private_key, 'fingerprint': fingerprint} + + +class CloudController(object): + """ CloudController provides the critical dispatch between + inbound API calls through the endpoint and messages + sent to the other nodes. +""" + def __init__(self): + self.instdir = model.InstanceDirectory() + self.setup() + + @property + def instances(self): + """ All instances in the system, as dicts """ + return self.instdir.all + + @property + def volumes(self): + """ returns a list of all volumes """ + for volume_id in datastore.Redis.instance().smembers("volumes"): + volume = service.get_volume(volume_id) + yield volume + + def __str__(self): + return 'CloudController' + + def setup(self): + """ Ensure the keychains and folders exist. """ + # Create keys folder, if it doesn't exist + if not os.path.exists(FLAGS.keys_path): + os.makedirs(FLAGS.keys_path) + # Gen root CA, if we don't have one + root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file) + if not os.path.exists(root_ca_path): + start = os.getcwd() + os.chdir(FLAGS.ca_path) + utils.runthis("Generating root CA: %s", "sh genrootca.sh") + os.chdir(start) + # TODO: Do this with M2Crypto instead + + def get_instance_by_ip(self, ip): + return self.instdir.by_ip(ip) + + def _get_mpi_data(self, project_id): + result = {} + for instance in self.instdir.all: + if instance['project_id'] == project_id: + line = '%s slots=%d' % (instance['private_dns_name'], + INSTANCE_TYPES[instance['instance_type']]['vcpus']) + if instance['key_name'] in result: + result[instance['key_name']].append(line) + else: + result[instance['key_name']] = [line] + return result + + def get_metadata(self, ipaddress): + i = self.get_instance_by_ip(ipaddress) + if i is None: + return None + mpi = self._get_mpi_data(i['project_id']) + if i['key_name']: + keys = { + '0': { + '_name': i['key_name'], + 'openssh-key': i['key_data'] + } + } + else: + keys = '' + + address_record = network_model.FixedIp(i['private_dns_name']) + if address_record: + hostname = address_record['hostname'] + else: + hostname = 'ip-%s' % i['private_dns_name'].replace('.', '-') + data = { + 'user-data': base64.b64decode(i['user_data']), + 'meta-data': { + 'ami-id': i['image_id'], + 'ami-launch-index': i['ami_launch_index'], + 'ami-manifest-path': 'FIXME', # image property + 'block-device-mapping': { # TODO: replace with real data + 'ami': 'sda1', + 'ephemeral0': 'sda2', + 'root': '/dev/sda1', + 'swap': 'sda3' + }, + 'hostname': hostname, + 'instance-action': 'none', + 'instance-id': i['instance_id'], + 'instance-type': i.get('instance_type', ''), + 'local-hostname': hostname, + 'local-ipv4': i['private_dns_name'], # TODO: switch to IP + 'kernel-id': i.get('kernel_id', ''), + 'placement': { + 'availaibility-zone': i.get('availability_zone', 'nova'), + }, + 'public-hostname': hostname, + 'public-ipv4': i.get('dns_name', ''), # TODO: switch to IP + 'public-keys': keys, + 'ramdisk-id': i.get('ramdisk_id', ''), + 'reservation-id': i['reservation_id'], + 'security-groups': i.get('groups', ''), + 'mpi': mpi + } + } + if False: # TODO: store ancestor ids + data['ancestor-ami-ids'] = [] + if i.get('product_codes', None): + data['product-codes'] = i['product_codes'] + return data + + @rbac.allow('all') + def describe_availability_zones(self, context, **kwargs): + return {'availabilityZoneInfo': [{'zoneName': 'nova', + 'zoneState': 'available'}]} + + @rbac.allow('all') + def describe_regions(self, context, region_name=None, **kwargs): + # TODO(vish): region_name is an array. Support filtering + return {'regionInfo': [{'regionName': 'nova', + 'regionUrl': FLAGS.ec2_url}]} + + @rbac.allow('all') + def describe_snapshots(self, + context, + snapshot_id=None, + owner=None, + restorable_by=None, + **kwargs): + return {'snapshotSet': [{'snapshotId': 'fixme', + 'volumeId': 'fixme', + 'status': 'fixme', + 'startTime': 'fixme', + 'progress': 'fixme', + 'ownerId': 'fixme', + 'volumeSize': 0, + 'description': 'fixme'}]} + + @rbac.allow('all') + def describe_key_pairs(self, context, key_name=None, **kwargs): + key_pairs = context.user.get_key_pairs() + if not key_name is None: + key_pairs = [x for x in key_pairs if x.name in key_name] + + result = [] + for key_pair in key_pairs: + # filter out the vpn keys + suffix = FLAGS.vpn_key_suffix + if context.user.is_admin() or not key_pair.name.endswith(suffix): + result.append({ + 'keyName': key_pair.name, + 'keyFingerprint': key_pair.fingerprint, + }) + + return {'keypairsSet': result} + + @rbac.allow('all') + def create_key_pair(self, context, key_name, **kwargs): + dcall = defer.Deferred() + pool = context.handler.application.settings.get('pool') + def _complete(kwargs): + if 'exception' in kwargs: + dcall.errback(kwargs['exception']) + return + dcall.callback({'keyName': key_name, + 'keyFingerprint': kwargs['fingerprint'], + 'keyMaterial': kwargs['private_key']}) + pool.apply_async(_gen_key, [context.user.id, key_name], + callback=_complete) + return dcall + + @rbac.allow('all') + def delete_key_pair(self, context, key_name, **kwargs): + context.user.delete_key_pair(key_name) + # aws returns true even if the key doens't exist + return True + + @rbac.allow('all') + def describe_security_groups(self, context, group_names, **kwargs): + groups = {'securityGroupSet': []} + + # Stubbed for now to unblock other things. + return groups + + @rbac.allow('netadmin') + def create_security_group(self, context, group_name, **kwargs): + return True + + @rbac.allow('netadmin') + def delete_security_group(self, context, group_name, **kwargs): + return True + + @rbac.allow('projectmanager', 'sysadmin') + def get_console_output(self, context, instance_id, **kwargs): + # instance_id is passed in as a list of instances + instance = self._get_instance(context, instance_id[0]) + return rpc.call('%s.%s' % (FLAGS.compute_topic, instance['node_name']), + {"method": "get_console_output", + "args": {"instance_id": instance_id[0]}}) + + def _get_user_id(self, context): + if context and context.user: + return context.user.id + else: + return None + + @rbac.allow('projectmanager', 'sysadmin') + def describe_volumes(self, context, **kwargs): + volumes = [] + for volume in self.volumes: + if context.user.is_admin() or volume['project_id'] == context.project.id: + v = self.format_volume(context, volume) + volumes.append(v) + return defer.succeed({'volumeSet': volumes}) + + def format_volume(self, context, volume): + v = {} + v['volumeId'] = volume['volume_id'] + v['status'] = volume['status'] + v['size'] = volume['size'] + v['availabilityZone'] = volume['availability_zone'] + v['createTime'] = volume['create_time'] + if context.user.is_admin(): + v['status'] = '%s (%s, %s, %s, %s)' % ( + volume.get('status', None), + volume.get('user_id', None), + volume.get('node_name', None), + volume.get('instance_id', ''), + volume.get('mountpoint', '')) + if volume['attach_status'] == 'attached': + v['attachmentSet'] = [{'attachTime': volume['attach_time'], + 'deleteOnTermination': volume['delete_on_termination'], + 'device': volume['mountpoint'], + 'instanceId': volume['instance_id'], + 'status': 'attached', + 'volume_id': volume['volume_id']}] + else: + v['attachmentSet'] = [{}] + return v + + @rbac.allow('projectmanager', 'sysadmin') + @defer.inlineCallbacks + def create_volume(self, context, size, **kwargs): + # TODO(vish): refactor this to create the volume object here and tell service to create it + result = yield rpc.call(FLAGS.volume_topic, {"method": "create_volume", + "args": {"size": size, + "user_id": context.user.id, + "project_id": context.project.id}}) + # NOTE(vish): rpc returned value is in the result key in the dictionary + volume = self._get_volume(context, result) + defer.returnValue({'volumeSet': [self.format_volume(context, volume)]}) + + def _get_address(self, context, public_ip): + # FIXME(vish) this should move into network.py + address = network_model.ElasticIp.lookup(public_ip) + if address and (context.user.is_admin() or address['project_id'] == context.project.id): + return address + raise exception.NotFound("Address at ip %s not found" % public_ip) + + def _get_image(self, context, image_id): + """passes in context because + objectstore does its own authorization""" + result = images.list(context, [image_id]) + if not result: + raise exception.NotFound('Image %s could not be found' % image_id) + image = result[0] + return image + + def _get_instance(self, context, instance_id): + for instance in self.instdir.all: + if instance['instance_id'] == instance_id: + if context.user.is_admin() or instance['project_id'] == context.project.id: + return instance + raise exception.NotFound('Instance %s could not be found' % instance_id) + + def _get_volume(self, context, volume_id): + volume = service.get_volume(volume_id) + if context.user.is_admin() or volume['project_id'] == context.project.id: + return volume + raise exception.NotFound('Volume %s could not be found' % volume_id) + + @rbac.allow('projectmanager', 'sysadmin') + def attach_volume(self, context, volume_id, instance_id, device, **kwargs): + volume = self._get_volume(context, volume_id) + if volume['status'] == "attached": + raise exception.ApiError("Volume is already attached") + # TODO(vish): looping through all volumes is slow. We should probably maintain an index + for vol in self.volumes: + if vol['instance_id'] == instance_id and vol['mountpoint'] == device: + raise exception.ApiError("Volume %s is already attached to %s" % (vol['volume_id'], vol['mountpoint'])) + volume.start_attach(instance_id, device) + instance = self._get_instance(context, instance_id) + compute_node = instance['node_name'] + rpc.cast('%s.%s' % (FLAGS.compute_topic, compute_node), + {"method": "attach_volume", + "args": {"volume_id": volume_id, + "instance_id": instance_id, + "mountpoint": device}}) + return defer.succeed({'attachTime': volume['attach_time'], + 'device': volume['mountpoint'], + 'instanceId': instance_id, + 'requestId': context.request_id, + 'status': volume['attach_status'], + 'volumeId': volume_id}) + + @rbac.allow('projectmanager', 'sysadmin') + def detach_volume(self, context, volume_id, **kwargs): + volume = self._get_volume(context, volume_id) + instance_id = volume.get('instance_id', None) + if not instance_id: + raise exception.Error("Volume isn't attached to anything!") + if volume['status'] == "available": + raise exception.Error("Volume is already detached") + try: + volume.start_detach() + instance = self._get_instance(context, instance_id) + rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), + {"method": "detach_volume", + "args": {"instance_id": instance_id, + "volume_id": volume_id}}) + except exception.NotFound: + # If the instance doesn't exist anymore, + # then we need to call detach blind + volume.finish_detach() + return defer.succeed({'attachTime': volume['attach_time'], + 'device': volume['mountpoint'], + 'instanceId': instance_id, + 'requestId': context.request_id, + 'status': volume['attach_status'], + 'volumeId': volume_id}) + + def _convert_to_set(self, lst, label): + if lst == None or lst == []: + return None + if not isinstance(lst, list): + lst = [lst] + return [{label: x} for x in lst] + + @rbac.allow('all') + def describe_instances(self, context, **kwargs): + return defer.succeed(self._format_describe_instances(context)) + + def _format_describe_instances(self, context): + return { 'reservationSet': self._format_instances(context) } + + def _format_run_instances(self, context, reservation_id): + i = self._format_instances(context, reservation_id) + assert len(i) == 1 + return i[0] + + def _format_instances(self, context, reservation_id = None): + reservations = {} + if context.user.is_admin(): + instgenerator = self.instdir.all + else: + instgenerator = self.instdir.by_project(context.project.id) + for instance in instgenerator: + res_id = instance.get('reservation_id', 'Unknown') + if reservation_id != None and reservation_id != res_id: + continue + if not context.user.is_admin(): + if instance['image_id'] == FLAGS.vpn_image_id: + continue + i = {} + i['instance_id'] = instance.get('instance_id', None) + i['image_id'] = instance.get('image_id', None) + i['instance_state'] = { + 'code': instance.get('state', 0), + 'name': instance.get('state_description', 'pending') + } + i['public_dns_name'] = network_model.get_public_ip_for_instance( + i['instance_id']) + i['private_dns_name'] = instance.get('private_dns_name', None) + if not i['public_dns_name']: + i['public_dns_name'] = i['private_dns_name'] + i['dns_name'] = instance.get('dns_name', None) + i['key_name'] = instance.get('key_name', None) + if context.user.is_admin(): + i['key_name'] = '%s (%s, %s)' % (i['key_name'], + instance.get('project_id', None), + instance.get('node_name', '')) + i['product_codes_set'] = self._convert_to_set( + instance.get('product_codes', None), 'product_code') + i['instance_type'] = instance.get('instance_type', None) + i['launch_time'] = instance.get('launch_time', None) + i['ami_launch_index'] = instance.get('ami_launch_index', + None) + if not reservations.has_key(res_id): + r = {} + r['reservation_id'] = res_id + r['owner_id'] = instance.get('project_id', None) + r['group_set'] = self._convert_to_set( + instance.get('groups', None), 'group_id') + r['instances_set'] = [] + reservations[res_id] = r + reservations[res_id]['instances_set'].append(i) + + return list(reservations.values()) + + @rbac.allow('all') + def describe_addresses(self, context, **kwargs): + return self.format_addresses(context) + + def format_addresses(self, context): + addresses = [] + for address in network_model.ElasticIp.all(): + # TODO(vish): implement a by_project iterator for addresses + if (context.user.is_admin() or + address['project_id'] == context.project.id): + address_rv = { + 'public_ip': address['address'], + 'instance_id': address.get('instance_id', 'free') + } + if context.user.is_admin(): + address_rv['instance_id'] = "%s (%s, %s)" % ( + address['instance_id'], + address['user_id'], + address['project_id'], + ) + addresses.append(address_rv) + return {'addressesSet': addresses} + + @rbac.allow('netadmin') + @defer.inlineCallbacks + def allocate_address(self, context, **kwargs): + network_topic = yield self._get_network_topic(context) + public_ip = yield rpc.call(network_topic, + {"method": "allocate_elastic_ip", + "args": {"user_id": context.user.id, + "project_id": context.project.id}}) + defer.returnValue({'addressSet': [{'publicIp': public_ip}]}) + + @rbac.allow('netadmin') + @defer.inlineCallbacks + def release_address(self, context, public_ip, **kwargs): + # NOTE(vish): Should we make sure this works? + network_topic = yield self._get_network_topic(context) + rpc.cast(network_topic, + {"method": "deallocate_elastic_ip", + "args": {"elastic_ip": public_ip}}) + defer.returnValue({'releaseResponse': ["Address released."]}) + + @rbac.allow('netadmin') + @defer.inlineCallbacks + def associate_address(self, context, instance_id, public_ip, **kwargs): + instance = self._get_instance(context, instance_id) + address = self._get_address(context, public_ip) + network_topic = yield self._get_network_topic(context) + rpc.cast(network_topic, + {"method": "associate_elastic_ip", + "args": {"elastic_ip": address['address'], + "fixed_ip": instance['private_dns_name'], + "instance_id": instance['instance_id']}}) + defer.returnValue({'associateResponse': ["Address associated."]}) + + @rbac.allow('netadmin') + @defer.inlineCallbacks + def disassociate_address(self, context, public_ip, **kwargs): + address = self._get_address(context, public_ip) + network_topic = yield self._get_network_topic(context) + rpc.cast(network_topic, + {"method": "disassociate_elastic_ip", + "args": {"elastic_ip": address['address']}}) + defer.returnValue({'disassociateResponse': ["Address disassociated."]}) + + @defer.inlineCallbacks + def _get_network_topic(self, context): + """Retrieves the network host for a project""" + host = network_service.get_host_for_project(context.project.id) + if not host: + host = yield rpc.call(FLAGS.network_topic, + {"method": "set_network_host", + "args": {"user_id": context.user.id, + "project_id": context.project.id}}) + defer.returnValue('%s.%s' %(FLAGS.network_topic, host)) + + @rbac.allow('projectmanager', 'sysadmin') + @defer.inlineCallbacks + def run_instances(self, context, **kwargs): + # make sure user can access the image + # vpn image is private so it doesn't show up on lists + if kwargs['image_id'] != FLAGS.vpn_image_id: + image = self._get_image(context, kwargs['image_id']) + + # FIXME(ja): if image is cloudpipe, this breaks + + # get defaults from imagestore + image_id = image['imageId'] + kernel_id = image.get('kernelId', FLAGS.default_kernel) + ramdisk_id = image.get('ramdiskId', FLAGS.default_ramdisk) + + # API parameters overrides of defaults + kernel_id = kwargs.get('kernel_id', kernel_id) + ramdisk_id = kwargs.get('ramdisk_id', ramdisk_id) + + # make sure we have access to kernel and ramdisk + self._get_image(context, kernel_id) + self._get_image(context, ramdisk_id) + + logging.debug("Going to run instances...") + reservation_id = utils.generate_uid('r') + launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) + key_data = None + if kwargs.has_key('key_name'): + key_pair = context.user.get_key_pair(kwargs['key_name']) + if not key_pair: + raise exception.ApiError('Key Pair %s not found' % + kwargs['key_name']) + key_data = key_pair.public_key + network_topic = yield self._get_network_topic(context) + # TODO: Get the real security group of launch in here + security_group = "default" + for num in range(int(kwargs['max_count'])): + is_vpn = False + if image_id == FLAGS.vpn_image_id: + is_vpn = True + inst = self.instdir.new() + allocate_data = yield rpc.call(network_topic, + {"method": "allocate_fixed_ip", + "args": {"user_id": context.user.id, + "project_id": context.project.id, + "security_group": security_group, + "is_vpn": is_vpn, + "hostname": inst.instance_id}}) + inst['image_id'] = image_id + inst['kernel_id'] = kernel_id + inst['ramdisk_id'] = ramdisk_id + inst['user_data'] = kwargs.get('user_data', '') + inst['instance_type'] = kwargs.get('instance_type', 'm1.small') + inst['reservation_id'] = reservation_id + inst['launch_time'] = launch_time + inst['key_data'] = key_data or '' + inst['key_name'] = kwargs.get('key_name', '') + inst['user_id'] = context.user.id + inst['project_id'] = context.project.id + inst['ami_launch_index'] = num + inst['security_group'] = security_group + inst['hostname'] = inst.instance_id + for (key, value) in allocate_data.iteritems(): + inst[key] = value + + inst.save() + rpc.cast(FLAGS.compute_topic, + {"method": "run_instance", + "args": {"instance_id": inst.instance_id}}) + logging.debug("Casting to node for %s's instance with IP of %s" % + (context.user.name, inst['private_dns_name'])) + # TODO: Make Network figure out the network name from ip. + defer.returnValue(self._format_run_instances(context, reservation_id)) + + @rbac.allow('projectmanager', 'sysadmin') + @defer.inlineCallbacks + def terminate_instances(self, context, instance_id, **kwargs): + logging.debug("Going to start terminating instances") + network_topic = yield self._get_network_topic(context) + for i in instance_id: + logging.debug("Going to try and terminate %s" % i) + try: + instance = self._get_instance(context, i) + except exception.NotFound: + logging.warning("Instance %s was not found during terminate" + % i) + continue + elastic_ip = network_model.get_public_ip_for_instance(i) + if elastic_ip: + logging.debug("Disassociating address %s" % elastic_ip) + # NOTE(vish): Right now we don't really care if the ip is + # disassociated. We may need to worry about + # checking this later. Perhaps in the scheduler? + rpc.cast(network_topic, + {"method": "disassociate_elastic_ip", + "args": {"elastic_ip": elastic_ip}}) + + fixed_ip = instance.get('private_dns_name', None) + if fixed_ip: + logging.debug("Deallocating address %s" % fixed_ip) + # NOTE(vish): Right now we don't really care if the ip is + # actually removed. We may need to worry about + # checking this later. Perhaps in the scheduler? + rpc.cast(network_topic, + {"method": "deallocate_fixed_ip", + "args": {"fixed_ip": fixed_ip}}) + + if instance.get('node_name', 'unassigned') != 'unassigned': + # NOTE(joshua?): It's also internal default + rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), + {"method": "terminate_instance", + "args": {"instance_id": i}}) + else: + instance.destroy() + defer.returnValue(True) + + @rbac.allow('projectmanager', 'sysadmin') + def reboot_instances(self, context, instance_id, **kwargs): + """instance_id is a list of instance ids""" + for i in instance_id: + instance = self._get_instance(context, i) + rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), + {"method": "reboot_instance", + "args": {"instance_id": i}}) + return defer.succeed(True) + + @rbac.allow('projectmanager', 'sysadmin') + def delete_volume(self, context, volume_id, **kwargs): + # TODO: return error if not authorized + volume = self._get_volume(context, volume_id) + volume_node = volume['node_name'] + rpc.cast('%s.%s' % (FLAGS.volume_topic, volume_node), + {"method": "delete_volume", + "args": {"volume_id": volume_id}}) + return defer.succeed(True) + + @rbac.allow('all') + def describe_images(self, context, image_id=None, **kwargs): + # The objectstore does its own authorization for describe + imageSet = images.list(context, image_id) + return defer.succeed({'imagesSet': imageSet}) + + @rbac.allow('projectmanager', 'sysadmin') + def deregister_image(self, context, image_id, **kwargs): + # FIXME: should the objectstore be doing these authorization checks? + images.deregister(context, image_id) + return defer.succeed({'imageId': image_id}) + + @rbac.allow('projectmanager', 'sysadmin') + def register_image(self, context, image_location=None, **kwargs): + # FIXME: should the objectstore be doing these authorization checks? + if image_location is None and kwargs.has_key('name'): + image_location = kwargs['name'] + image_id = images.register(context, image_location) + logging.debug("Registered %s as %s" % (image_location, image_id)) + + return defer.succeed({'imageId': image_id}) + + @rbac.allow('all') + def describe_image_attribute(self, context, image_id, attribute, **kwargs): + if attribute != 'launchPermission': + raise exception.ApiError('attribute not supported: %s' % attribute) + try: + image = images.list(context, image_id)[0] + except IndexError: + raise exception.ApiError('invalid id: %s' % image_id) + result = {'image_id': image_id, 'launchPermission': []} + if image['isPublic']: + result['launchPermission'].append({'group': 'all'}) + return defer.succeed(result) + + @rbac.allow('projectmanager', 'sysadmin') + def modify_image_attribute(self, context, image_id, attribute, operation_type, **kwargs): + # TODO(devcamcar): Support users and groups other than 'all'. + if attribute != 'launchPermission': + raise exception.ApiError('attribute not supported: %s' % attribute) + if not 'user_group' in kwargs: + raise exception.ApiError('user or group not specified') + if len(kwargs['user_group']) != 1 and kwargs['user_group'][0] != 'all': + raise exception.ApiError('only group "all" is supported') + if not operation_type in ['add', 'remove']: + raise exception.ApiError('operation_type must be add or remove') + result = images.modify(context, image_id, operation_type) + return defer.succeed(result) + + def update_state(self, topic, value): + """ accepts status reports from the queue and consolidates them """ + # TODO(jmc): if an instance has disappeared from + # the node, call instance_death + if topic == "instances": + return defer.succeed(True) + aggregate_state = getattr(self, topic) + node_name = value.keys()[0] + items = value[node_name] + + logging.debug("Updating %s state for %s" % (topic, node_name)) + + for item_id in items.keys(): + if (aggregate_state.has_key('pending') and + aggregate_state['pending'].has_key(item_id)): + del aggregate_state['pending'][item_id] + aggregate_state[node_name] = items + + return defer.succeed(True) diff --git a/nova/endpoint/admin.py b/nova/endpoint/admin.py deleted file mode 100644 index d6f622755..000000000 --- a/nova/endpoint/admin.py +++ /dev/null @@ -1,211 +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. - -""" -Admin API controller, exposed through http via the api worker. -""" - -import base64 - -from nova.auth import manager -from nova.compute import model - - -def user_dict(user, base64_file=None): - """Convert the user object to a result dict""" - if user: - return { - 'username': user.id, - 'accesskey': user.access, - 'secretkey': user.secret, - 'file': base64_file} - else: - return {} - - -def project_dict(project): - """Convert the project object to a result dict""" - if project: - return { - 'projectname': project.id, - 'project_manager_id': project.project_manager_id, - 'description': project.description} - else: - return {} - - -def host_dict(host): - """Convert a host model object to a result dict""" - if host: - return host.state - else: - return {} - - -def admin_only(target): - """Decorator for admin-only API calls""" - def wrapper(*args, **kwargs): - """Internal wrapper method for admin-only API calls""" - context = args[1] - if context.user.is_admin(): - return target(*args, **kwargs) - else: - return {} - - return wrapper - - -class AdminController(object): - """ - API Controller for users, hosts, nodes, and workers. - Trivial admin_only wrapper will be replaced with RBAC, - allowing project managers to administer project users. - """ - - def __str__(self): - return 'AdminController' - - @admin_only - def describe_user(self, _context, name, **_kwargs): - """Returns user data, including access and secret keys.""" - return user_dict(manager.AuthManager().get_user(name)) - - @admin_only - def describe_users(self, _context, **_kwargs): - """Returns all users - should be changed to deal with a list.""" - return {'userSet': - [user_dict(u) for u in manager.AuthManager().get_users()] } - - @admin_only - def register_user(self, _context, name, **_kwargs): - """Creates a new user, and returns generated credentials.""" - return user_dict(manager.AuthManager().create_user(name)) - - @admin_only - def deregister_user(self, _context, name, **_kwargs): - """Deletes a single user (NOT undoable.) - Should throw an exception if the user has instances, - volumes, or buckets remaining. - """ - manager.AuthManager().delete_user(name) - - return True - - @admin_only - def describe_roles(self, context, project_roles=True, **kwargs): - """Returns a list of allowed roles.""" - roles = manager.AuthManager().get_roles(project_roles) - return { 'roles': [{'role': r} for r in roles]} - - @admin_only - def describe_user_roles(self, context, user, project=None, **kwargs): - """Returns a list of roles for the given user. - Omitting project will return any global roles that the user has. - Specifying project will return only project specific roles. - """ - roles = manager.AuthManager().get_user_roles(user, project=project) - return { 'roles': [{'role': r} for r in roles]} - - @admin_only - def modify_user_role(self, context, user, role, project=None, - operation='add', **kwargs): - """Add or remove a role for a user and project.""" - if operation == 'add': - manager.AuthManager().add_role(user, role, project) - elif operation == 'remove': - manager.AuthManager().remove_role(user, role, project) - else: - raise exception.ApiError('operation must be add or remove') - - return True - - @admin_only - def generate_x509_for_user(self, _context, name, project=None, **kwargs): - """Generates and returns an x509 certificate for a single user. - Is usually called from a client that will wrap this with - access and secret key info, and return a zip file. - """ - if project is None: - project = name - project = manager.AuthManager().get_project(project) - user = manager.AuthManager().get_user(name) - return user_dict(user, base64.b64encode(project.get_credentials(user))) - - @admin_only - def describe_project(self, context, name, **kwargs): - """Returns project data, including member ids.""" - return project_dict(manager.AuthManager().get_project(name)) - - @admin_only - def describe_projects(self, context, user=None, **kwargs): - """Returns all projects - should be changed to deal with a list.""" - return {'projectSet': - [project_dict(u) for u in - manager.AuthManager().get_projects(user=user)]} - - @admin_only - def register_project(self, context, name, manager_user, description=None, - member_users=None, **kwargs): - """Creates a new project""" - return project_dict( - manager.AuthManager().create_project( - name, - manager_user, - description=None, - member_users=None)) - - @admin_only - def deregister_project(self, context, name): - """Permanently deletes a project.""" - manager.AuthManager().delete_project(name) - return True - - @admin_only - def describe_project_members(self, context, name, **kwargs): - project = manager.AuthManager().get_project(name) - result = { - 'members': [{'member': m} for m in project.member_ids]} - return result - - @admin_only - def modify_project_member(self, context, user, project, operation, **kwargs): - """Add or remove a user from a project.""" - if operation =='add': - manager.AuthManager().add_to_project(user, project) - elif operation == 'remove': - manager.AuthManager().remove_from_project(user, project) - else: - raise exception.ApiError('operation must be add or remove') - return True - - @admin_only - def describe_hosts(self, _context, **_kwargs): - """Returns status info for all nodes. Includes: - * Disk Space - * Instance List - * RAM used - * CPU used - * DHCP servers running - * Iptables / bridges - """ - return {'hostSet': [host_dict(h) for h in model.Host.all()]} - - @admin_only - def describe_host(self, _context, name, **_kwargs): - """Returns status info for single node.""" - return host_dict(model.Host.lookup(name)) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py deleted file mode 100644 index 30634429d..000000000 --- a/nova/endpoint/cloud.py +++ /dev/null @@ -1,739 +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. - -""" -Cloud Controller: Implementation of EC2 REST API calls, which are -dispatched to other nodes via AMQP RPC. State is via distributed -datastore. -""" - -import base64 -import logging -import os -import time - -from twisted.internet import defer - -from nova import datastore -from nova import exception -from nova import flags -from nova import rpc -from nova import utils -from nova.auth import rbac -from nova.auth import manager -from nova.compute import model -from nova.compute.instance_types import INSTANCE_TYPES -from nova.endpoint import images -from nova.network import service as network_service -from nova.network import model as network_model -from nova.volume import service - - -FLAGS = flags.FLAGS -flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') - - -def _gen_key(user_id, key_name): - """ Tuck this into AuthManager """ - try: - mgr = manager.AuthManager() - private_key, fingerprint = mgr.generate_key_pair(user_id, key_name) - except Exception as ex: - return {'exception': ex} - return {'private_key': private_key, 'fingerprint': fingerprint} - - -class CloudController(object): - """ CloudController provides the critical dispatch between - inbound API calls through the endpoint and messages - sent to the other nodes. -""" - def __init__(self): - self.instdir = model.InstanceDirectory() - self.setup() - - @property - def instances(self): - """ All instances in the system, as dicts """ - return self.instdir.all - - @property - def volumes(self): - """ returns a list of all volumes """ - for volume_id in datastore.Redis.instance().smembers("volumes"): - volume = service.get_volume(volume_id) - yield volume - - def __str__(self): - return 'CloudController' - - def setup(self): - """ Ensure the keychains and folders exist. """ - # Create keys folder, if it doesn't exist - if not os.path.exists(FLAGS.keys_path): - os.makedirs(FLAGS.keys_path) - # Gen root CA, if we don't have one - root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file) - if not os.path.exists(root_ca_path): - start = os.getcwd() - os.chdir(FLAGS.ca_path) - utils.runthis("Generating root CA: %s", "sh genrootca.sh") - os.chdir(start) - # TODO: Do this with M2Crypto instead - - def get_instance_by_ip(self, ip): - return self.instdir.by_ip(ip) - - def _get_mpi_data(self, project_id): - result = {} - for instance in self.instdir.all: - if instance['project_id'] == project_id: - line = '%s slots=%d' % (instance['private_dns_name'], - INSTANCE_TYPES[instance['instance_type']]['vcpus']) - if instance['key_name'] in result: - result[instance['key_name']].append(line) - else: - result[instance['key_name']] = [line] - return result - - def get_metadata(self, ipaddress): - i = self.get_instance_by_ip(ipaddress) - if i is None: - return None - mpi = self._get_mpi_data(i['project_id']) - if i['key_name']: - keys = { - '0': { - '_name': i['key_name'], - 'openssh-key': i['key_data'] - } - } - else: - keys = '' - - address_record = network_model.FixedIp(i['private_dns_name']) - if address_record: - hostname = address_record['hostname'] - else: - hostname = 'ip-%s' % i['private_dns_name'].replace('.', '-') - data = { - 'user-data': base64.b64decode(i['user_data']), - 'meta-data': { - 'ami-id': i['image_id'], - 'ami-launch-index': i['ami_launch_index'], - 'ami-manifest-path': 'FIXME', # image property - 'block-device-mapping': { # TODO: replace with real data - 'ami': 'sda1', - 'ephemeral0': 'sda2', - 'root': '/dev/sda1', - 'swap': 'sda3' - }, - 'hostname': hostname, - 'instance-action': 'none', - 'instance-id': i['instance_id'], - 'instance-type': i.get('instance_type', ''), - 'local-hostname': hostname, - 'local-ipv4': i['private_dns_name'], # TODO: switch to IP - 'kernel-id': i.get('kernel_id', ''), - 'placement': { - 'availaibility-zone': i.get('availability_zone', 'nova'), - }, - 'public-hostname': hostname, - 'public-ipv4': i.get('dns_name', ''), # TODO: switch to IP - 'public-keys': keys, - 'ramdisk-id': i.get('ramdisk_id', ''), - 'reservation-id': i['reservation_id'], - 'security-groups': i.get('groups', ''), - 'mpi': mpi - } - } - if False: # TODO: store ancestor ids - data['ancestor-ami-ids'] = [] - if i.get('product_codes', None): - data['product-codes'] = i['product_codes'] - return data - - @rbac.allow('all') - def describe_availability_zones(self, context, **kwargs): - return {'availabilityZoneInfo': [{'zoneName': 'nova', - 'zoneState': 'available'}]} - - @rbac.allow('all') - def describe_regions(self, context, region_name=None, **kwargs): - # TODO(vish): region_name is an array. Support filtering - return {'regionInfo': [{'regionName': 'nova', - 'regionUrl': FLAGS.ec2_url}]} - - @rbac.allow('all') - def describe_snapshots(self, - context, - snapshot_id=None, - owner=None, - restorable_by=None, - **kwargs): - return {'snapshotSet': [{'snapshotId': 'fixme', - 'volumeId': 'fixme', - 'status': 'fixme', - 'startTime': 'fixme', - 'progress': 'fixme', - 'ownerId': 'fixme', - 'volumeSize': 0, - 'description': 'fixme'}]} - - @rbac.allow('all') - def describe_key_pairs(self, context, key_name=None, **kwargs): - key_pairs = context.user.get_key_pairs() - if not key_name is None: - key_pairs = [x for x in key_pairs if x.name in key_name] - - result = [] - for key_pair in key_pairs: - # filter out the vpn keys - suffix = FLAGS.vpn_key_suffix - if context.user.is_admin() or not key_pair.name.endswith(suffix): - result.append({ - 'keyName': key_pair.name, - 'keyFingerprint': key_pair.fingerprint, - }) - - return {'keypairsSet': result} - - @rbac.allow('all') - def create_key_pair(self, context, key_name, **kwargs): - dcall = defer.Deferred() - pool = context.handler.application.settings.get('pool') - def _complete(kwargs): - if 'exception' in kwargs: - dcall.errback(kwargs['exception']) - return - dcall.callback({'keyName': key_name, - 'keyFingerprint': kwargs['fingerprint'], - 'keyMaterial': kwargs['private_key']}) - pool.apply_async(_gen_key, [context.user.id, key_name], - callback=_complete) - return dcall - - @rbac.allow('all') - def delete_key_pair(self, context, key_name, **kwargs): - context.user.delete_key_pair(key_name) - # aws returns true even if the key doens't exist - return True - - @rbac.allow('all') - def describe_security_groups(self, context, group_names, **kwargs): - groups = {'securityGroupSet': []} - - # Stubbed for now to unblock other things. - return groups - - @rbac.allow('netadmin') - def create_security_group(self, context, group_name, **kwargs): - return True - - @rbac.allow('netadmin') - def delete_security_group(self, context, group_name, **kwargs): - return True - - @rbac.allow('projectmanager', 'sysadmin') - def get_console_output(self, context, instance_id, **kwargs): - # instance_id is passed in as a list of instances - instance = self._get_instance(context, instance_id[0]) - return rpc.call('%s.%s' % (FLAGS.compute_topic, instance['node_name']), - {"method": "get_console_output", - "args": {"instance_id": instance_id[0]}}) - - def _get_user_id(self, context): - if context and context.user: - return context.user.id - else: - return None - - @rbac.allow('projectmanager', 'sysadmin') - def describe_volumes(self, context, **kwargs): - volumes = [] - for volume in self.volumes: - if context.user.is_admin() or volume['project_id'] == context.project.id: - v = self.format_volume(context, volume) - volumes.append(v) - return defer.succeed({'volumeSet': volumes}) - - def format_volume(self, context, volume): - v = {} - v['volumeId'] = volume['volume_id'] - v['status'] = volume['status'] - v['size'] = volume['size'] - v['availabilityZone'] = volume['availability_zone'] - v['createTime'] = volume['create_time'] - if context.user.is_admin(): - v['status'] = '%s (%s, %s, %s, %s)' % ( - volume.get('status', None), - volume.get('user_id', None), - volume.get('node_name', None), - volume.get('instance_id', ''), - volume.get('mountpoint', '')) - if volume['attach_status'] == 'attached': - v['attachmentSet'] = [{'attachTime': volume['attach_time'], - 'deleteOnTermination': volume['delete_on_termination'], - 'device': volume['mountpoint'], - 'instanceId': volume['instance_id'], - 'status': 'attached', - 'volume_id': volume['volume_id']}] - else: - v['attachmentSet'] = [{}] - return v - - @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks - def create_volume(self, context, size, **kwargs): - # TODO(vish): refactor this to create the volume object here and tell service to create it - result = yield rpc.call(FLAGS.volume_topic, {"method": "create_volume", - "args": {"size": size, - "user_id": context.user.id, - "project_id": context.project.id}}) - # NOTE(vish): rpc returned value is in the result key in the dictionary - volume = self._get_volume(context, result) - defer.returnValue({'volumeSet': [self.format_volume(context, volume)]}) - - def _get_address(self, context, public_ip): - # FIXME(vish) this should move into network.py - address = network_model.ElasticIp.lookup(public_ip) - if address and (context.user.is_admin() or address['project_id'] == context.project.id): - return address - raise exception.NotFound("Address at ip %s not found" % public_ip) - - def _get_image(self, context, image_id): - """passes in context because - objectstore does its own authorization""" - result = images.list(context, [image_id]) - if not result: - raise exception.NotFound('Image %s could not be found' % image_id) - image = result[0] - return image - - def _get_instance(self, context, instance_id): - for instance in self.instdir.all: - if instance['instance_id'] == instance_id: - if context.user.is_admin() or instance['project_id'] == context.project.id: - return instance - raise exception.NotFound('Instance %s could not be found' % instance_id) - - def _get_volume(self, context, volume_id): - volume = service.get_volume(volume_id) - if context.user.is_admin() or volume['project_id'] == context.project.id: - return volume - raise exception.NotFound('Volume %s could not be found' % volume_id) - - @rbac.allow('projectmanager', 'sysadmin') - def attach_volume(self, context, volume_id, instance_id, device, **kwargs): - volume = self._get_volume(context, volume_id) - if volume['status'] == "attached": - raise exception.ApiError("Volume is already attached") - # TODO(vish): looping through all volumes is slow. We should probably maintain an index - for vol in self.volumes: - if vol['instance_id'] == instance_id and vol['mountpoint'] == device: - raise exception.ApiError("Volume %s is already attached to %s" % (vol['volume_id'], vol['mountpoint'])) - volume.start_attach(instance_id, device) - instance = self._get_instance(context, instance_id) - compute_node = instance['node_name'] - rpc.cast('%s.%s' % (FLAGS.compute_topic, compute_node), - {"method": "attach_volume", - "args": {"volume_id": volume_id, - "instance_id": instance_id, - "mountpoint": device}}) - return defer.succeed({'attachTime': volume['attach_time'], - 'device': volume['mountpoint'], - 'instanceId': instance_id, - 'requestId': context.request_id, - 'status': volume['attach_status'], - 'volumeId': volume_id}) - - @rbac.allow('projectmanager', 'sysadmin') - def detach_volume(self, context, volume_id, **kwargs): - volume = self._get_volume(context, volume_id) - instance_id = volume.get('instance_id', None) - if not instance_id: - raise exception.Error("Volume isn't attached to anything!") - if volume['status'] == "available": - raise exception.Error("Volume is already detached") - try: - volume.start_detach() - instance = self._get_instance(context, instance_id) - rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), - {"method": "detach_volume", - "args": {"instance_id": instance_id, - "volume_id": volume_id}}) - except exception.NotFound: - # If the instance doesn't exist anymore, - # then we need to call detach blind - volume.finish_detach() - return defer.succeed({'attachTime': volume['attach_time'], - 'device': volume['mountpoint'], - 'instanceId': instance_id, - 'requestId': context.request_id, - 'status': volume['attach_status'], - 'volumeId': volume_id}) - - def _convert_to_set(self, lst, label): - if lst == None or lst == []: - return None - if not isinstance(lst, list): - lst = [lst] - return [{label: x} for x in lst] - - @rbac.allow('all') - def describe_instances(self, context, **kwargs): - return defer.succeed(self._format_describe_instances(context)) - - def _format_describe_instances(self, context): - return { 'reservationSet': self._format_instances(context) } - - def _format_run_instances(self, context, reservation_id): - i = self._format_instances(context, reservation_id) - assert len(i) == 1 - return i[0] - - def _format_instances(self, context, reservation_id = None): - reservations = {} - if context.user.is_admin(): - instgenerator = self.instdir.all - else: - instgenerator = self.instdir.by_project(context.project.id) - for instance in instgenerator: - res_id = instance.get('reservation_id', 'Unknown') - if reservation_id != None and reservation_id != res_id: - continue - if not context.user.is_admin(): - if instance['image_id'] == FLAGS.vpn_image_id: - continue - i = {} - i['instance_id'] = instance.get('instance_id', None) - i['image_id'] = instance.get('image_id', None) - i['instance_state'] = { - 'code': instance.get('state', 0), - 'name': instance.get('state_description', 'pending') - } - i['public_dns_name'] = network_model.get_public_ip_for_instance( - i['instance_id']) - i['private_dns_name'] = instance.get('private_dns_name', None) - if not i['public_dns_name']: - i['public_dns_name'] = i['private_dns_name'] - i['dns_name'] = instance.get('dns_name', None) - i['key_name'] = instance.get('key_name', None) - if context.user.is_admin(): - i['key_name'] = '%s (%s, %s)' % (i['key_name'], - instance.get('project_id', None), - instance.get('node_name', '')) - i['product_codes_set'] = self._convert_to_set( - instance.get('product_codes', None), 'product_code') - i['instance_type'] = instance.get('instance_type', None) - i['launch_time'] = instance.get('launch_time', None) - i['ami_launch_index'] = instance.get('ami_launch_index', - None) - if not reservations.has_key(res_id): - r = {} - r['reservation_id'] = res_id - r['owner_id'] = instance.get('project_id', None) - r['group_set'] = self._convert_to_set( - instance.get('groups', None), 'group_id') - r['instances_set'] = [] - reservations[res_id] = r - reservations[res_id]['instances_set'].append(i) - - return list(reservations.values()) - - @rbac.allow('all') - def describe_addresses(self, context, **kwargs): - return self.format_addresses(context) - - def format_addresses(self, context): - addresses = [] - for address in network_model.ElasticIp.all(): - # TODO(vish): implement a by_project iterator for addresses - if (context.user.is_admin() or - address['project_id'] == context.project.id): - address_rv = { - 'public_ip': address['address'], - 'instance_id': address.get('instance_id', 'free') - } - if context.user.is_admin(): - address_rv['instance_id'] = "%s (%s, %s)" % ( - address['instance_id'], - address['user_id'], - address['project_id'], - ) - addresses.append(address_rv) - return {'addressesSet': addresses} - - @rbac.allow('netadmin') - @defer.inlineCallbacks - def allocate_address(self, context, **kwargs): - network_topic = yield self._get_network_topic(context) - public_ip = yield rpc.call(network_topic, - {"method": "allocate_elastic_ip", - "args": {"user_id": context.user.id, - "project_id": context.project.id}}) - defer.returnValue({'addressSet': [{'publicIp': public_ip}]}) - - @rbac.allow('netadmin') - @defer.inlineCallbacks - def release_address(self, context, public_ip, **kwargs): - # NOTE(vish): Should we make sure this works? - network_topic = yield self._get_network_topic(context) - rpc.cast(network_topic, - {"method": "deallocate_elastic_ip", - "args": {"elastic_ip": public_ip}}) - defer.returnValue({'releaseResponse': ["Address released."]}) - - @rbac.allow('netadmin') - @defer.inlineCallbacks - def associate_address(self, context, instance_id, public_ip, **kwargs): - instance = self._get_instance(context, instance_id) - address = self._get_address(context, public_ip) - network_topic = yield self._get_network_topic(context) - rpc.cast(network_topic, - {"method": "associate_elastic_ip", - "args": {"elastic_ip": address['address'], - "fixed_ip": instance['private_dns_name'], - "instance_id": instance['instance_id']}}) - defer.returnValue({'associateResponse': ["Address associated."]}) - - @rbac.allow('netadmin') - @defer.inlineCallbacks - def disassociate_address(self, context, public_ip, **kwargs): - address = self._get_address(context, public_ip) - network_topic = yield self._get_network_topic(context) - rpc.cast(network_topic, - {"method": "disassociate_elastic_ip", - "args": {"elastic_ip": address['address']}}) - defer.returnValue({'disassociateResponse': ["Address disassociated."]}) - - @defer.inlineCallbacks - def _get_network_topic(self, context): - """Retrieves the network host for a project""" - host = network_service.get_host_for_project(context.project.id) - if not host: - host = yield rpc.call(FLAGS.network_topic, - {"method": "set_network_host", - "args": {"user_id": context.user.id, - "project_id": context.project.id}}) - defer.returnValue('%s.%s' %(FLAGS.network_topic, host)) - - @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks - def run_instances(self, context, **kwargs): - # make sure user can access the image - # vpn image is private so it doesn't show up on lists - if kwargs['image_id'] != FLAGS.vpn_image_id: - image = self._get_image(context, kwargs['image_id']) - - # FIXME(ja): if image is cloudpipe, this breaks - - # get defaults from imagestore - image_id = image['imageId'] - kernel_id = image.get('kernelId', FLAGS.default_kernel) - ramdisk_id = image.get('ramdiskId', FLAGS.default_ramdisk) - - # API parameters overrides of defaults - kernel_id = kwargs.get('kernel_id', kernel_id) - ramdisk_id = kwargs.get('ramdisk_id', ramdisk_id) - - # make sure we have access to kernel and ramdisk - self._get_image(context, kernel_id) - self._get_image(context, ramdisk_id) - - logging.debug("Going to run instances...") - reservation_id = utils.generate_uid('r') - launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) - key_data = None - if kwargs.has_key('key_name'): - key_pair = context.user.get_key_pair(kwargs['key_name']) - if not key_pair: - raise exception.ApiError('Key Pair %s not found' % - kwargs['key_name']) - key_data = key_pair.public_key - network_topic = yield self._get_network_topic(context) - # TODO: Get the real security group of launch in here - security_group = "default" - for num in range(int(kwargs['max_count'])): - is_vpn = False - if image_id == FLAGS.vpn_image_id: - is_vpn = True - inst = self.instdir.new() - allocate_data = yield rpc.call(network_topic, - {"method": "allocate_fixed_ip", - "args": {"user_id": context.user.id, - "project_id": context.project.id, - "security_group": security_group, - "is_vpn": is_vpn, - "hostname": inst.instance_id}}) - inst['image_id'] = image_id - inst['kernel_id'] = kernel_id - inst['ramdisk_id'] = ramdisk_id - inst['user_data'] = kwargs.get('user_data', '') - inst['instance_type'] = kwargs.get('instance_type', 'm1.small') - inst['reservation_id'] = reservation_id - inst['launch_time'] = launch_time - inst['key_data'] = key_data or '' - inst['key_name'] = kwargs.get('key_name', '') - inst['user_id'] = context.user.id - inst['project_id'] = context.project.id - inst['ami_launch_index'] = num - inst['security_group'] = security_group - inst['hostname'] = inst.instance_id - for (key, value) in allocate_data.iteritems(): - inst[key] = value - - inst.save() - rpc.cast(FLAGS.compute_topic, - {"method": "run_instance", - "args": {"instance_id": inst.instance_id}}) - logging.debug("Casting to node for %s's instance with IP of %s" % - (context.user.name, inst['private_dns_name'])) - # TODO: Make Network figure out the network name from ip. - defer.returnValue(self._format_run_instances(context, reservation_id)) - - @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks - def terminate_instances(self, context, instance_id, **kwargs): - logging.debug("Going to start terminating instances") - network_topic = yield self._get_network_topic(context) - for i in instance_id: - logging.debug("Going to try and terminate %s" % i) - try: - instance = self._get_instance(context, i) - except exception.NotFound: - logging.warning("Instance %s was not found during terminate" - % i) - continue - elastic_ip = network_model.get_public_ip_for_instance(i) - if elastic_ip: - logging.debug("Disassociating address %s" % elastic_ip) - # NOTE(vish): Right now we don't really care if the ip is - # disassociated. We may need to worry about - # checking this later. Perhaps in the scheduler? - rpc.cast(network_topic, - {"method": "disassociate_elastic_ip", - "args": {"elastic_ip": elastic_ip}}) - - fixed_ip = instance.get('private_dns_name', None) - if fixed_ip: - logging.debug("Deallocating address %s" % fixed_ip) - # NOTE(vish): Right now we don't really care if the ip is - # actually removed. We may need to worry about - # checking this later. Perhaps in the scheduler? - rpc.cast(network_topic, - {"method": "deallocate_fixed_ip", - "args": {"fixed_ip": fixed_ip}}) - - if instance.get('node_name', 'unassigned') != 'unassigned': - # NOTE(joshua?): It's also internal default - rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), - {"method": "terminate_instance", - "args": {"instance_id": i}}) - else: - instance.destroy() - defer.returnValue(True) - - @rbac.allow('projectmanager', 'sysadmin') - def reboot_instances(self, context, instance_id, **kwargs): - """instance_id is a list of instance ids""" - for i in instance_id: - instance = self._get_instance(context, i) - rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), - {"method": "reboot_instance", - "args": {"instance_id": i}}) - return defer.succeed(True) - - @rbac.allow('projectmanager', 'sysadmin') - def delete_volume(self, context, volume_id, **kwargs): - # TODO: return error if not authorized - volume = self._get_volume(context, volume_id) - volume_node = volume['node_name'] - rpc.cast('%s.%s' % (FLAGS.volume_topic, volume_node), - {"method": "delete_volume", - "args": {"volume_id": volume_id}}) - return defer.succeed(True) - - @rbac.allow('all') - def describe_images(self, context, image_id=None, **kwargs): - # The objectstore does its own authorization for describe - imageSet = images.list(context, image_id) - return defer.succeed({'imagesSet': imageSet}) - - @rbac.allow('projectmanager', 'sysadmin') - def deregister_image(self, context, image_id, **kwargs): - # FIXME: should the objectstore be doing these authorization checks? - images.deregister(context, image_id) - return defer.succeed({'imageId': image_id}) - - @rbac.allow('projectmanager', 'sysadmin') - def register_image(self, context, image_location=None, **kwargs): - # FIXME: should the objectstore be doing these authorization checks? - if image_location is None and kwargs.has_key('name'): - image_location = kwargs['name'] - image_id = images.register(context, image_location) - logging.debug("Registered %s as %s" % (image_location, image_id)) - - return defer.succeed({'imageId': image_id}) - - @rbac.allow('all') - def describe_image_attribute(self, context, image_id, attribute, **kwargs): - if attribute != 'launchPermission': - raise exception.ApiError('attribute not supported: %s' % attribute) - try: - image = images.list(context, image_id)[0] - except IndexError: - raise exception.ApiError('invalid id: %s' % image_id) - result = {'image_id': image_id, 'launchPermission': []} - if image['isPublic']: - result['launchPermission'].append({'group': 'all'}) - return defer.succeed(result) - - @rbac.allow('projectmanager', 'sysadmin') - def modify_image_attribute(self, context, image_id, attribute, operation_type, **kwargs): - # TODO(devcamcar): Support users and groups other than 'all'. - if attribute != 'launchPermission': - raise exception.ApiError('attribute not supported: %s' % attribute) - if not 'user_group' in kwargs: - raise exception.ApiError('user or group not specified') - if len(kwargs['user_group']) != 1 and kwargs['user_group'][0] != 'all': - raise exception.ApiError('only group "all" is supported') - if not operation_type in ['add', 'remove']: - raise exception.ApiError('operation_type must be add or remove') - result = images.modify(context, image_id, operation_type) - return defer.succeed(result) - - def update_state(self, topic, value): - """ accepts status reports from the queue and consolidates them """ - # TODO(jmc): if an instance has disappeared from - # the node, call instance_death - if topic == "instances": - return defer.succeed(True) - aggregate_state = getattr(self, topic) - node_name = value.keys()[0] - items = value[node_name] - - logging.debug("Updating %s state for %s" % (topic, node_name)) - - for item_id in items.keys(): - if (aggregate_state.has_key('pending') and - aggregate_state['pending'].has_key(item_id)): - del aggregate_state['pending'][item_id] - aggregate_state[node_name] = items - - return defer.succeed(True) -- cgit From 4f3bb96df8a7e48735c078520e77a47dca7a2bd1 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 12:33:49 -0400 Subject: Remove inlineCallbacks and yield from cloud.py, as eventlet doesn't need it --- nova/api/ec2/__init__.py | 1 + nova/api/ec2/cloud.py | 30 ++++++++++-------------------- nova/endpoint/notes.txt | 16 ++++++++-------- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 7e345d297..b4a1894cc 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -108,6 +108,7 @@ class Router(wsgi.Application): self.controllers = dict(Cloud=cloud.CloudController(), Admin=admin.AdminController()) + @webob.dec.wsgify def __call__(self, req): # Obtain the appropriate controller for this request. match = self.map.match(req.path) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 30634429d..decd2a2c0 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -27,8 +27,6 @@ import logging import os import time -from twisted.internet import defer - from nova import datastore from nova import exception from nova import flags @@ -298,10 +296,9 @@ class CloudController(object): return v @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks def create_volume(self, context, size, **kwargs): # TODO(vish): refactor this to create the volume object here and tell service to create it - result = yield rpc.call(FLAGS.volume_topic, {"method": "create_volume", + result = rpc.call(FLAGS.volume_topic, {"method": "create_volume", "args": {"size": size, "user_id": context.user.id, "project_id": context.project.id}}) @@ -480,31 +477,28 @@ class CloudController(object): return {'addressesSet': addresses} @rbac.allow('netadmin') - @defer.inlineCallbacks def allocate_address(self, context, **kwargs): - network_topic = yield self._get_network_topic(context) - public_ip = yield rpc.call(network_topic, + network_topic = self._get_network_topic(context) + public_ip = rpc.call(network_topic, {"method": "allocate_elastic_ip", "args": {"user_id": context.user.id, "project_id": context.project.id}}) defer.returnValue({'addressSet': [{'publicIp': public_ip}]}) @rbac.allow('netadmin') - @defer.inlineCallbacks def release_address(self, context, public_ip, **kwargs): # NOTE(vish): Should we make sure this works? - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) rpc.cast(network_topic, {"method": "deallocate_elastic_ip", "args": {"elastic_ip": public_ip}}) defer.returnValue({'releaseResponse': ["Address released."]}) @rbac.allow('netadmin') - @defer.inlineCallbacks def associate_address(self, context, instance_id, public_ip, **kwargs): instance = self._get_instance(context, instance_id) address = self._get_address(context, public_ip) - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) rpc.cast(network_topic, {"method": "associate_elastic_ip", "args": {"elastic_ip": address['address'], @@ -513,28 +507,25 @@ class CloudController(object): defer.returnValue({'associateResponse': ["Address associated."]}) @rbac.allow('netadmin') - @defer.inlineCallbacks def disassociate_address(self, context, public_ip, **kwargs): address = self._get_address(context, public_ip) - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) rpc.cast(network_topic, {"method": "disassociate_elastic_ip", "args": {"elastic_ip": address['address']}}) defer.returnValue({'disassociateResponse': ["Address disassociated."]}) - @defer.inlineCallbacks def _get_network_topic(self, context): """Retrieves the network host for a project""" host = network_service.get_host_for_project(context.project.id) if not host: - host = yield rpc.call(FLAGS.network_topic, + host = rpc.call(FLAGS.network_topic, {"method": "set_network_host", "args": {"user_id": context.user.id, "project_id": context.project.id}}) defer.returnValue('%s.%s' %(FLAGS.network_topic, host)) @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks def run_instances(self, context, **kwargs): # make sure user can access the image # vpn image is private so it doesn't show up on lists @@ -566,7 +557,7 @@ class CloudController(object): raise exception.ApiError('Key Pair %s not found' % kwargs['key_name']) key_data = key_pair.public_key - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) # TODO: Get the real security group of launch in here security_group = "default" for num in range(int(kwargs['max_count'])): @@ -574,7 +565,7 @@ class CloudController(object): if image_id == FLAGS.vpn_image_id: is_vpn = True inst = self.instdir.new() - allocate_data = yield rpc.call(network_topic, + allocate_data = rpc.call(network_topic, {"method": "allocate_fixed_ip", "args": {"user_id": context.user.id, "project_id": context.project.id, @@ -608,10 +599,9 @@ class CloudController(object): defer.returnValue(self._format_run_instances(context, reservation_id)) @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks def terminate_instances(self, context, instance_id, **kwargs): logging.debug("Going to start terminating instances") - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) for i in instance_id: logging.debug("Going to try and terminate %s" % i) try: diff --git a/nova/endpoint/notes.txt b/nova/endpoint/notes.txt index 7a85cdc93..cbb7b0cd0 100644 --- a/nova/endpoint/notes.txt +++ b/nova/endpoint/notes.txt @@ -43,20 +43,20 @@ CloudController and AdminController: * Controllers: move the @rbac.allow data into an auth WSGI that is right above the call to the controller - verify @defer.inlineCallbacks is just to allow the yield statements, then - remove the yield statements (untangle from twisted) +x verify @defer.inlineCallbacks is just to allow the yield statements, then +x remove the yield statements (untangle from twisted) * nova-api: verify that cloud_topic is going away which I seem to remember, so we can ignore rpc * apiserverapplication: - replace with a Router to a wsgi.Controller - apirequesthandler stuff is just an entry in api.APIRouter +x replace with a Router to a wsgi.Controller +x apirequesthandler stuff is just an entry in api.APIRouter * apirequesthandler - wsgi.Controller pointed to by api.APIRouter - - basically it's execute() from old APIRequestHandler - change to return data directly instead of _write_callback() and finish() +x wsgi.Controller pointed to by api.APIRouter +x - basically it's execute() from old APIRequestHandler +x change to return data directly instead of _write_callback() and finish() * apirequest - doesn't need to change +x doesn't need to change -- cgit From f22c693e4cf638ef5278d9db444da2c4a99baae4 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 13:25:31 -0400 Subject: Remove all Twisted defer references from cloud.py --- nova/api/ec2/cloud.py | 86 ++++++++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 49 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index decd2a2c0..05fbf3861 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -48,11 +48,8 @@ flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') def _gen_key(user_id, key_name): """ Tuck this into AuthManager """ - try: - mgr = manager.AuthManager() - private_key, fingerprint = mgr.generate_key_pair(user_id, key_name) - except Exception as ex: - return {'exception': ex} + mgr = manager.AuthManager() + private_key, fingerprint = mgr.generate_key_pair(user_id, key_name) return {'private_key': private_key, 'fingerprint': fingerprint} @@ -213,18 +210,10 @@ class CloudController(object): @rbac.allow('all') def create_key_pair(self, context, key_name, **kwargs): - dcall = defer.Deferred() - pool = context.handler.application.settings.get('pool') - def _complete(kwargs): - if 'exception' in kwargs: - dcall.errback(kwargs['exception']) - return - dcall.callback({'keyName': key_name, - 'keyFingerprint': kwargs['fingerprint'], - 'keyMaterial': kwargs['private_key']}) - pool.apply_async(_gen_key, [context.user.id, key_name], - callback=_complete) - return dcall + data = _gen_key(context.user.id, key_name) + return {'keyName': key_name, + 'keyFingerprint': data['fingerprint'], + 'keyMaterial': data['private_key']} @rbac.allow('all') def delete_key_pair(self, context, key_name, **kwargs): @@ -268,7 +257,7 @@ class CloudController(object): if context.user.is_admin() or volume['project_id'] == context.project.id: v = self.format_volume(context, volume) volumes.append(v) - return defer.succeed({'volumeSet': volumes}) + return {'volumeSet': volumes} def format_volume(self, context, volume): v = {} @@ -304,7 +293,7 @@ class CloudController(object): "project_id": context.project.id}}) # NOTE(vish): rpc returned value is in the result key in the dictionary volume = self._get_volume(context, result) - defer.returnValue({'volumeSet': [self.format_volume(context, volume)]}) + return {'volumeSet': [self.format_volume(context, volume)]} def _get_address(self, context, public_ip): # FIXME(vish) this should move into network.py @@ -352,12 +341,12 @@ class CloudController(object): "args": {"volume_id": volume_id, "instance_id": instance_id, "mountpoint": device}}) - return defer.succeed({'attachTime': volume['attach_time'], - 'device': volume['mountpoint'], - 'instanceId': instance_id, - 'requestId': context.request_id, - 'status': volume['attach_status'], - 'volumeId': volume_id}) + return {'attachTime': volume['attach_time'], + 'device': volume['mountpoint'], + 'instanceId': instance_id, + 'requestId': context.request_id, + 'status': volume['attach_status'], + 'volumeId': volume_id} @rbac.allow('projectmanager', 'sysadmin') def detach_volume(self, context, volume_id, **kwargs): @@ -378,12 +367,12 @@ class CloudController(object): # If the instance doesn't exist anymore, # then we need to call detach blind volume.finish_detach() - return defer.succeed({'attachTime': volume['attach_time'], - 'device': volume['mountpoint'], - 'instanceId': instance_id, - 'requestId': context.request_id, - 'status': volume['attach_status'], - 'volumeId': volume_id}) + return {'attachTime': volume['attach_time'], + 'device': volume['mountpoint'], + 'instanceId': instance_id, + 'requestId': context.request_id, + 'status': volume['attach_status'], + 'volumeId': volume_id}) def _convert_to_set(self, lst, label): if lst == None or lst == []: @@ -394,7 +383,7 @@ class CloudController(object): @rbac.allow('all') def describe_instances(self, context, **kwargs): - return defer.succeed(self._format_describe_instances(context)) + return self._format_describe_instances(context) def _format_describe_instances(self, context): return { 'reservationSet': self._format_instances(context) } @@ -483,7 +472,7 @@ class CloudController(object): {"method": "allocate_elastic_ip", "args": {"user_id": context.user.id, "project_id": context.project.id}}) - defer.returnValue({'addressSet': [{'publicIp': public_ip}]}) + return {'addressSet': [{'publicIp': public_ip}]} @rbac.allow('netadmin') def release_address(self, context, public_ip, **kwargs): @@ -492,7 +481,7 @@ class CloudController(object): rpc.cast(network_topic, {"method": "deallocate_elastic_ip", "args": {"elastic_ip": public_ip}}) - defer.returnValue({'releaseResponse': ["Address released."]}) + return {'releaseResponse': ["Address released."]} @rbac.allow('netadmin') def associate_address(self, context, instance_id, public_ip, **kwargs): @@ -504,7 +493,7 @@ class CloudController(object): "args": {"elastic_ip": address['address'], "fixed_ip": instance['private_dns_name'], "instance_id": instance['instance_id']}}) - defer.returnValue({'associateResponse': ["Address associated."]}) + return {'associateResponse': ["Address associated."]} @rbac.allow('netadmin') def disassociate_address(self, context, public_ip, **kwargs): @@ -513,7 +502,7 @@ class CloudController(object): rpc.cast(network_topic, {"method": "disassociate_elastic_ip", "args": {"elastic_ip": address['address']}}) - defer.returnValue({'disassociateResponse': ["Address disassociated."]}) + return {'disassociateResponse': ["Address disassociated."]} def _get_network_topic(self, context): """Retrieves the network host for a project""" @@ -523,7 +512,7 @@ class CloudController(object): {"method": "set_network_host", "args": {"user_id": context.user.id, "project_id": context.project.id}}) - defer.returnValue('%s.%s' %(FLAGS.network_topic, host)) + return '%s.%s' %(FLAGS.network_topic, host) @rbac.allow('projectmanager', 'sysadmin') def run_instances(self, context, **kwargs): @@ -596,7 +585,7 @@ class CloudController(object): logging.debug("Casting to node for %s's instance with IP of %s" % (context.user.name, inst['private_dns_name'])) # TODO: Make Network figure out the network name from ip. - defer.returnValue(self._format_run_instances(context, reservation_id)) + return self._format_run_instances(context, reservation_id) @rbac.allow('projectmanager', 'sysadmin') def terminate_instances(self, context, instance_id, **kwargs): @@ -637,7 +626,7 @@ class CloudController(object): "args": {"instance_id": i}}) else: instance.destroy() - defer.returnValue(True) + return True @rbac.allow('projectmanager', 'sysadmin') def reboot_instances(self, context, instance_id, **kwargs): @@ -647,7 +636,7 @@ class CloudController(object): rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), {"method": "reboot_instance", "args": {"instance_id": i}}) - return defer.succeed(True) + return True @rbac.allow('projectmanager', 'sysadmin') def delete_volume(self, context, volume_id, **kwargs): @@ -657,19 +646,19 @@ class CloudController(object): rpc.cast('%s.%s' % (FLAGS.volume_topic, volume_node), {"method": "delete_volume", "args": {"volume_id": volume_id}}) - return defer.succeed(True) + return True @rbac.allow('all') def describe_images(self, context, image_id=None, **kwargs): # The objectstore does its own authorization for describe imageSet = images.list(context, image_id) - return defer.succeed({'imagesSet': imageSet}) + return {'imagesSet': imageSet} @rbac.allow('projectmanager', 'sysadmin') def deregister_image(self, context, image_id, **kwargs): # FIXME: should the objectstore be doing these authorization checks? images.deregister(context, image_id) - return defer.succeed({'imageId': image_id}) + return {'imageId': image_id} @rbac.allow('projectmanager', 'sysadmin') def register_image(self, context, image_location=None, **kwargs): @@ -679,7 +668,7 @@ class CloudController(object): image_id = images.register(context, image_location) logging.debug("Registered %s as %s" % (image_location, image_id)) - return defer.succeed({'imageId': image_id}) + return {'imageId': image_id} @rbac.allow('all') def describe_image_attribute(self, context, image_id, attribute, **kwargs): @@ -692,7 +681,7 @@ class CloudController(object): result = {'image_id': image_id, 'launchPermission': []} if image['isPublic']: result['launchPermission'].append({'group': 'all'}) - return defer.succeed(result) + return result @rbac.allow('projectmanager', 'sysadmin') def modify_image_attribute(self, context, image_id, attribute, operation_type, **kwargs): @@ -705,15 +694,14 @@ class CloudController(object): raise exception.ApiError('only group "all" is supported') if not operation_type in ['add', 'remove']: raise exception.ApiError('operation_type must be add or remove') - result = images.modify(context, image_id, operation_type) - return defer.succeed(result) + return images.modify(context, image_id, operation_type) def update_state(self, topic, value): """ accepts status reports from the queue and consolidates them """ # TODO(jmc): if an instance has disappeared from # the node, call instance_death if topic == "instances": - return defer.succeed(True) + return True aggregate_state = getattr(self, topic) node_name = value.keys()[0] items = value[node_name] @@ -726,4 +714,4 @@ class CloudController(object): del aggregate_state['pending'][item_id] aggregate_state[node_name] = items - return defer.succeed(True) + return True -- cgit From 544b73d35895ac79af910a40590095780f224abb Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 1 Sep 2010 10:50:31 -0400 Subject: Return error Responses properly, and don't muck with req.params -- make a copy instead --- nova/api/ec2/__init__.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index b4a1894cc..248a66f55 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -22,6 +22,7 @@ Starting point for routing EC2 requests import logging import routes +import webob import webob.dec import webob.exc @@ -46,8 +47,7 @@ class Authenticate(wsgi.Middleware): @webob.dec.wsgify def __call__(self, req): - #TODO(gundlach): where do arguments come from? - args = self.request.arguments + args = dict(req.params) # Read request signature. try: @@ -92,8 +92,9 @@ class Authenticate(wsgi.Middleware): _log.debug('arg: %s\t\tval: %s' % (key, value)) # Authenticated! - req.environ['ec2.action'] = action req.environ['ec2.context'] = APIRequestContext(user, project) + req.environ['ec2.action'] = action + req.environ['ec2.action_args'] = args return self.application @@ -119,24 +120,24 @@ class Router(wsgi.Application): try: controller = self.controllers[controller_name] except KeyError: - self._error('unhandled', 'no controller named %s' % controller_name) - return + return self._error('unhandled', 'no controller named %s' % controller_name) api_request = APIRequest(controller, req.environ['ec2.action']) context = req.environ['ec2.context'] try: - return api_request.send(context, **args) + return api_request.send(context, **req.environ['ec2.action_args']) except exception.ApiError as ex: - self._error(req, type(ex).__name__ + "." + ex.code, ex.message) + return self._error(req, type(ex).__name__ + "." + ex.code, ex.message) # TODO(vish): do something more useful with unknown exceptions except Exception as ex: - self._error(type(ex).__name__, str(ex)) + return self._error(type(ex).__name__, str(ex)) def _error(self, req, code, message): - req.status = 400 - req.headers['Content-Type'] = 'text/xml' - req.response = ('\n' + resp = webob.Response() + resp.status = 400 + resp.headers['Content-Type'] = 'text/xml' + resp.body = ('\n' '%s' '%s' '?') % (code, message)) - + return resp -- cgit From 8de182446993ac24e7b8fba12342f8adb3e179d4 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 1 Sep 2010 12:02:14 -0400 Subject: Break Router() into Router() and Executor(), and put Authorizer() (currently a stub) in between them. --- nova/api/ec2/__init__.py | 101 +++++++++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 39 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 248a66f55..87a72ca7c 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -40,36 +40,23 @@ class API(wsgi.Middleware): """Routing for all EC2 API requests.""" def __init__(self): - self.application = Authenticate(Router()) + self.application = Authenticate(Router(Authorizer(Executor()))) class Authenticate(wsgi.Middleware): - """Authenticates an EC2 request.""" + """Authenticate an EC2 request and add 'ec2.context' to WSGI environ.""" @webob.dec.wsgify def __call__(self, req): - args = dict(req.params) - - # Read request signature. + # Read request signature and access id. try: - signature = args.pop('Signature')[0] + signature = req.params['Signature'] + access = req.params['AWSAccessKeyId'] except: raise webob.exc.HTTPBadRequest() # Make a copy of args for authentication and signature verification. - auth_params = {} - for key, value in args.items(): - auth_params[key] = value[0] - - # Get requested action and remove authentication args for final request. - try: - action = args.pop('Action')[0] - access = args.pop('AWSAccessKeyId')[0] - args.pop('SignatureMethod') - args.pop('SignatureVersion') - args.pop('Version') - args.pop('Timestamp') - except: - raise webob.exc.HTTPBadRequest() + auth_params = dict(req.params) + auth_params.pop('Signature') # not part of authentication args # Authenticate the request. try: @@ -86,22 +73,15 @@ class Authenticate(wsgi.Middleware): logging.debug("Authentication Failure: %s" % ex) raise webob.exc.HTTPForbidden() - _log.debug('action: %s' % action) - - for key, value in args.items(): - _log.debug('arg: %s\t\tval: %s' % (key, value)) - # Authenticated! req.environ['ec2.context'] = APIRequestContext(user, project) - req.environ['ec2.action'] = action - req.environ['ec2.action_args'] = args + return self.application class Router(wsgi.Application): """ - Finds controller for a request, executes environ['ec2.action'] upon it, and - returns an XML response. If the action fails, returns a 400. + Add 'ec2.controller', 'ec2.action', and 'ec2.action_args' to WSGI environ. """ def __init__(self): self.map = routes.Mapper() @@ -111,21 +91,63 @@ class Router(wsgi.Application): @webob.dec.wsgify def __call__(self, req): - # Obtain the appropriate controller for this request. - match = self.map.match(req.path) - if not match: - raise webob.exc.HTTPNotFound() - controller_name = match['controller_name'] - + # Obtain the appropriate controller and action for this request. try: + match = self.map.match(req.path) + controller_name = match['controller_name'] controller = self.controllers[controller_name] - except KeyError: - return self._error('unhandled', 'no controller named %s' % controller_name) + except: + raise webob.exc.HTTPNotFound() + non_args = ['Action', 'Signature', 'AWSAccessKeyId', 'SignatureMethod', + 'SignatureVersion', 'Version', 'Timestamp'] + args = dict(req.params) + try: + action = req.params['Action'] # raise KeyError if omitted + for non_arg in non_args: + args.pop(non_arg) # remove, but raise KeyError if omitted + except: + raise webob.exc.HTTPBadRequest() - api_request = APIRequest(controller, req.environ['ec2.action']) + _log.debug('action: %s' % action) + for key, value in args.items(): + _log.debug('arg: %s\t\tval: %s' % (key, value)) + + # Success! + req.environ['ec2.controller'] = controller + req.environ['ec2.action'] = action + req.environ['ec2.action_args'] = args + + return self.application + + +class Authorization(wsgi.Middleware): + """ + Verify that ec2.controller and ec2.action in WSGI environ may be executed + in ec2.context. + """ + + @webob.dec.wsgify + def __call__(self, req): + #TODO(gundlach): put rbac information here. + return self.application + + +class Executor(wsg.Application): + """ + Executes 'ec2.action' upon 'ec2.controller', passing 'ec2.context' and + 'ec2.action_args' (all variables in WSGI environ.) Returns an XML + response, or a 400 upon failure. + """ + @webob.dec.wsgify + def __call__(self, req): context = req.environ['ec2.context'] + controller = req.environ['ec2.controller'] + action = req.environ['ec2.action'] + args = req.environ['ec2.action_args'] + + api_request = APIRequest(controller, action) try: - return api_request.send(context, **req.environ['ec2.action_args']) + return api_request.send(context, **args) except exception.ApiError as ex: return self._error(req, type(ex).__name__ + "." + ex.code, ex.message) # TODO(vish): do something more useful with unknown exceptions @@ -141,3 +163,4 @@ class Router(wsgi.Application): '%s' '?') % (code, message)) return resp + -- cgit From 83df968cfb050bdb6bac981dfcc2d0b1c3dd80db Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 1 Sep 2010 12:42:06 -0400 Subject: Delete rbac.py, moving @rbac decorator knowledge into api.ec2.Authorizer WSGI middleware. --- nova/api/ec2/__init__.py | 64 +++++++++++++++++++++++++++++++++++++++++--- nova/api/ec2/admin.py | 31 ---------------------- nova/api/ec2/cloud.py | 30 --------------------- nova/auth/rbac.py | 69 ------------------------------------------------ 4 files changed, 60 insertions(+), 134 deletions(-) delete mode 100644 nova/auth/rbac.py diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 87a72ca7c..aee9915d0 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -122,14 +122,70 @@ class Router(wsgi.Application): class Authorization(wsgi.Middleware): """ - Verify that ec2.controller and ec2.action in WSGI environ may be executed - in ec2.context. + Return a 401 if ec2.controller and ec2.action in WSGI environ may not be + executed in ec2.context. """ + def __init__(self, application): + super(Authorization, self).__init__(application) + self.action_roles = { + 'CloudController': { + 'DescribeAvailabilityzones': ['all'], + 'DescribeRegions': ['all'], + 'DescribeSnapshots': ['all'], + 'DescribeKeyPairs': ['all'], + 'CreateKeyPair': ['all'], + 'DeleteKeyPair': ['all'], + 'DescribeSecurityGroups': ['all'], + 'CreateSecurityGroup': ['netadmin'], + 'DeleteSecurityGroup': ['netadmin'], + 'GetConsoleOutput': ['projectmanager', 'sysadmin'], + 'DescribeVolumes': ['projectmanager', 'sysadmin'], + 'CreateVolume': ['projectmanager', 'sysadmin'], + 'AttachVolume': ['projectmanager', 'sysadmin'], + 'DetachVolume': ['projectmanager', 'sysadmin'], + 'DescribeInstances': ['all'], + 'DescribeAddresses': ['all'], + 'AllocateAddress': ['netadmin'], + 'ReleaseAddress': ['netadmin'], + 'AssociateAddress': ['netadmin'], + 'DisassociateAddress': ['netadmin'], + 'RunInstances': ['projectmanager', 'sysadmin'], + 'TerminateInstances': ['projectmanager', 'sysadmin'], + 'RebootInstances': ['projectmanager', 'sysadmin'], + 'DeleteVolume': ['projectmanager', 'sysadmin'], + 'DescribeImages': ['all'], + 'DeregisterImage': ['projectmanager', 'sysadmin'], + 'RegisterImage': ['projectmanager', 'sysadmin'], + 'DescribeImageAttribute': ['all'], + 'ModifyImageAttribute': ['projectmanager', 'sysadmin'], + }, + 'AdminController': { + # All actions have the same permission: [] (the default) + # admins will be allowed to run them + # all others will get HTTPUnauthorized. + }, + } + @webob.dec.wsgify def __call__(self, req): - #TODO(gundlach): put rbac information here. - return self.application + context = req.environ['ec2.context'] + controller_name = req.environ['ec2.controller'].__name__ + action = req.environ['ec2.action'] + allowed_roles = self.action_roles[controller_name].get(action, []) + if self._matches_any_role(context, allowed_roles): + return self.application + else: + raise webob.exc.HTTPUnauthorized() + + def _matches_any_role(self, context, roles): + """Return True if any role in roles is allowed in context.""" + if 'all' in roles: + return True + if 'none' in roles: + return False + return any(context.project.has_role(context.user.id, role) + for role in roles) class Executor(wsg.Application): diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index d6f622755..f0c643bbd 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -57,46 +57,27 @@ def host_dict(host): return {} -def admin_only(target): - """Decorator for admin-only API calls""" - def wrapper(*args, **kwargs): - """Internal wrapper method for admin-only API calls""" - context = args[1] - if context.user.is_admin(): - return target(*args, **kwargs) - else: - return {} - - return wrapper - - class AdminController(object): """ API Controller for users, hosts, nodes, and workers. - Trivial admin_only wrapper will be replaced with RBAC, - allowing project managers to administer project users. """ def __str__(self): return 'AdminController' - @admin_only def describe_user(self, _context, name, **_kwargs): """Returns user data, including access and secret keys.""" return user_dict(manager.AuthManager().get_user(name)) - @admin_only def describe_users(self, _context, **_kwargs): """Returns all users - should be changed to deal with a list.""" return {'userSet': [user_dict(u) for u in manager.AuthManager().get_users()] } - @admin_only def register_user(self, _context, name, **_kwargs): """Creates a new user, and returns generated credentials.""" return user_dict(manager.AuthManager().create_user(name)) - @admin_only def deregister_user(self, _context, name, **_kwargs): """Deletes a single user (NOT undoable.) Should throw an exception if the user has instances, @@ -106,13 +87,11 @@ class AdminController(object): return True - @admin_only def describe_roles(self, context, project_roles=True, **kwargs): """Returns a list of allowed roles.""" roles = manager.AuthManager().get_roles(project_roles) return { 'roles': [{'role': r} for r in roles]} - @admin_only def describe_user_roles(self, context, user, project=None, **kwargs): """Returns a list of roles for the given user. Omitting project will return any global roles that the user has. @@ -121,7 +100,6 @@ class AdminController(object): roles = manager.AuthManager().get_user_roles(user, project=project) return { 'roles': [{'role': r} for r in roles]} - @admin_only def modify_user_role(self, context, user, role, project=None, operation='add', **kwargs): """Add or remove a role for a user and project.""" @@ -134,7 +112,6 @@ class AdminController(object): return True - @admin_only def generate_x509_for_user(self, _context, name, project=None, **kwargs): """Generates and returns an x509 certificate for a single user. Is usually called from a client that will wrap this with @@ -146,19 +123,16 @@ class AdminController(object): user = manager.AuthManager().get_user(name) return user_dict(user, base64.b64encode(project.get_credentials(user))) - @admin_only def describe_project(self, context, name, **kwargs): """Returns project data, including member ids.""" return project_dict(manager.AuthManager().get_project(name)) - @admin_only def describe_projects(self, context, user=None, **kwargs): """Returns all projects - should be changed to deal with a list.""" return {'projectSet': [project_dict(u) for u in manager.AuthManager().get_projects(user=user)]} - @admin_only def register_project(self, context, name, manager_user, description=None, member_users=None, **kwargs): """Creates a new project""" @@ -169,20 +143,17 @@ class AdminController(object): description=None, member_users=None)) - @admin_only def deregister_project(self, context, name): """Permanently deletes a project.""" manager.AuthManager().delete_project(name) return True - @admin_only def describe_project_members(self, context, name, **kwargs): project = manager.AuthManager().get_project(name) result = { 'members': [{'member': m} for m in project.member_ids]} return result - @admin_only def modify_project_member(self, context, user, project, operation, **kwargs): """Add or remove a user from a project.""" if operation =='add': @@ -193,7 +164,6 @@ class AdminController(object): raise exception.ApiError('operation must be add or remove') return True - @admin_only def describe_hosts(self, _context, **_kwargs): """Returns status info for all nodes. Includes: * Disk Space @@ -205,7 +175,6 @@ class AdminController(object): """ return {'hostSet': [host_dict(h) for h in model.Host.all()]} - @admin_only def describe_host(self, _context, name, **_kwargs): """Returns status info for single node.""" return host_dict(model.Host.lookup(name)) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 05fbf3861..566887c1a 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -32,7 +32,6 @@ from nova import exception from nova import flags from nova import rpc from nova import utils -from nova.auth import rbac from nova.auth import manager from nova.compute import model from nova.compute.instance_types import INSTANCE_TYPES @@ -163,18 +162,15 @@ class CloudController(object): data['product-codes'] = i['product_codes'] return data - @rbac.allow('all') def describe_availability_zones(self, context, **kwargs): return {'availabilityZoneInfo': [{'zoneName': 'nova', 'zoneState': 'available'}]} - @rbac.allow('all') def describe_regions(self, context, region_name=None, **kwargs): # TODO(vish): region_name is an array. Support filtering return {'regionInfo': [{'regionName': 'nova', 'regionUrl': FLAGS.ec2_url}]} - @rbac.allow('all') def describe_snapshots(self, context, snapshot_id=None, @@ -190,7 +186,6 @@ class CloudController(object): 'volumeSize': 0, 'description': 'fixme'}]} - @rbac.allow('all') def describe_key_pairs(self, context, key_name=None, **kwargs): key_pairs = context.user.get_key_pairs() if not key_name is None: @@ -208,35 +203,29 @@ class CloudController(object): return {'keypairsSet': result} - @rbac.allow('all') def create_key_pair(self, context, key_name, **kwargs): data = _gen_key(context.user.id, key_name) return {'keyName': key_name, 'keyFingerprint': data['fingerprint'], 'keyMaterial': data['private_key']} - @rbac.allow('all') def delete_key_pair(self, context, key_name, **kwargs): context.user.delete_key_pair(key_name) # aws returns true even if the key doens't exist return True - @rbac.allow('all') def describe_security_groups(self, context, group_names, **kwargs): groups = {'securityGroupSet': []} # Stubbed for now to unblock other things. return groups - @rbac.allow('netadmin') def create_security_group(self, context, group_name, **kwargs): return True - @rbac.allow('netadmin') def delete_security_group(self, context, group_name, **kwargs): return True - @rbac.allow('projectmanager', 'sysadmin') def get_console_output(self, context, instance_id, **kwargs): # instance_id is passed in as a list of instances instance = self._get_instance(context, instance_id[0]) @@ -250,7 +239,6 @@ class CloudController(object): else: return None - @rbac.allow('projectmanager', 'sysadmin') def describe_volumes(self, context, **kwargs): volumes = [] for volume in self.volumes: @@ -284,7 +272,6 @@ class CloudController(object): v['attachmentSet'] = [{}] return v - @rbac.allow('projectmanager', 'sysadmin') def create_volume(self, context, size, **kwargs): # TODO(vish): refactor this to create the volume object here and tell service to create it result = rpc.call(FLAGS.volume_topic, {"method": "create_volume", @@ -324,7 +311,6 @@ class CloudController(object): return volume raise exception.NotFound('Volume %s could not be found' % volume_id) - @rbac.allow('projectmanager', 'sysadmin') def attach_volume(self, context, volume_id, instance_id, device, **kwargs): volume = self._get_volume(context, volume_id) if volume['status'] == "attached": @@ -348,7 +334,6 @@ class CloudController(object): 'status': volume['attach_status'], 'volumeId': volume_id} - @rbac.allow('projectmanager', 'sysadmin') def detach_volume(self, context, volume_id, **kwargs): volume = self._get_volume(context, volume_id) instance_id = volume.get('instance_id', None) @@ -381,7 +366,6 @@ class CloudController(object): lst = [lst] return [{label: x} for x in lst] - @rbac.allow('all') def describe_instances(self, context, **kwargs): return self._format_describe_instances(context) @@ -442,7 +426,6 @@ class CloudController(object): return list(reservations.values()) - @rbac.allow('all') def describe_addresses(self, context, **kwargs): return self.format_addresses(context) @@ -465,7 +448,6 @@ class CloudController(object): addresses.append(address_rv) return {'addressesSet': addresses} - @rbac.allow('netadmin') def allocate_address(self, context, **kwargs): network_topic = self._get_network_topic(context) public_ip = rpc.call(network_topic, @@ -474,7 +456,6 @@ class CloudController(object): "project_id": context.project.id}}) return {'addressSet': [{'publicIp': public_ip}]} - @rbac.allow('netadmin') def release_address(self, context, public_ip, **kwargs): # NOTE(vish): Should we make sure this works? network_topic = self._get_network_topic(context) @@ -483,7 +464,6 @@ class CloudController(object): "args": {"elastic_ip": public_ip}}) return {'releaseResponse': ["Address released."]} - @rbac.allow('netadmin') def associate_address(self, context, instance_id, public_ip, **kwargs): instance = self._get_instance(context, instance_id) address = self._get_address(context, public_ip) @@ -495,7 +475,6 @@ class CloudController(object): "instance_id": instance['instance_id']}}) return {'associateResponse': ["Address associated."]} - @rbac.allow('netadmin') def disassociate_address(self, context, public_ip, **kwargs): address = self._get_address(context, public_ip) network_topic = self._get_network_topic(context) @@ -514,7 +493,6 @@ class CloudController(object): "project_id": context.project.id}}) return '%s.%s' %(FLAGS.network_topic, host) - @rbac.allow('projectmanager', 'sysadmin') def run_instances(self, context, **kwargs): # make sure user can access the image # vpn image is private so it doesn't show up on lists @@ -587,7 +565,6 @@ class CloudController(object): # TODO: Make Network figure out the network name from ip. return self._format_run_instances(context, reservation_id) - @rbac.allow('projectmanager', 'sysadmin') def terminate_instances(self, context, instance_id, **kwargs): logging.debug("Going to start terminating instances") network_topic = self._get_network_topic(context) @@ -628,7 +605,6 @@ class CloudController(object): instance.destroy() return True - @rbac.allow('projectmanager', 'sysadmin') def reboot_instances(self, context, instance_id, **kwargs): """instance_id is a list of instance ids""" for i in instance_id: @@ -638,7 +614,6 @@ class CloudController(object): "args": {"instance_id": i}}) return True - @rbac.allow('projectmanager', 'sysadmin') def delete_volume(self, context, volume_id, **kwargs): # TODO: return error if not authorized volume = self._get_volume(context, volume_id) @@ -648,19 +623,16 @@ class CloudController(object): "args": {"volume_id": volume_id}}) return True - @rbac.allow('all') def describe_images(self, context, image_id=None, **kwargs): # The objectstore does its own authorization for describe imageSet = images.list(context, image_id) return {'imagesSet': imageSet} - @rbac.allow('projectmanager', 'sysadmin') def deregister_image(self, context, image_id, **kwargs): # FIXME: should the objectstore be doing these authorization checks? images.deregister(context, image_id) return {'imageId': image_id} - @rbac.allow('projectmanager', 'sysadmin') def register_image(self, context, image_location=None, **kwargs): # FIXME: should the objectstore be doing these authorization checks? if image_location is None and kwargs.has_key('name'): @@ -670,7 +642,6 @@ class CloudController(object): return {'imageId': image_id} - @rbac.allow('all') def describe_image_attribute(self, context, image_id, attribute, **kwargs): if attribute != 'launchPermission': raise exception.ApiError('attribute not supported: %s' % attribute) @@ -683,7 +654,6 @@ class CloudController(object): result['launchPermission'].append({'group': 'all'}) return result - @rbac.allow('projectmanager', 'sysadmin') def modify_image_attribute(self, context, image_id, attribute, operation_type, **kwargs): # TODO(devcamcar): Support users and groups other than 'all'. if attribute != 'launchPermission': diff --git a/nova/auth/rbac.py b/nova/auth/rbac.py deleted file mode 100644 index d157f44b3..000000000 --- a/nova/auth/rbac.py +++ /dev/null @@ -1,69 +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. - -"""Role-based access control decorators to use fpr wrapping other -methods with.""" - -from nova import exception - - -def allow(*roles): - """Allow the given roles access the wrapped function.""" - - def wrap(func): # pylint: disable-msg=C0111 - - def wrapped_func(self, context, *args, - **kwargs): # pylint: disable-msg=C0111 - if context.user.is_superuser(): - return func(self, context, *args, **kwargs) - for role in roles: - if __matches_role(context, role): - return func(self, context, *args, **kwargs) - raise exception.NotAuthorized() - - return wrapped_func - - return wrap - - -def deny(*roles): - """Deny the given roles access the wrapped function.""" - - def wrap(func): # pylint: disable-msg=C0111 - - def wrapped_func(self, context, *args, - **kwargs): # pylint: disable-msg=C0111 - if context.user.is_superuser(): - return func(self, context, *args, **kwargs) - for role in roles: - if __matches_role(context, role): - raise exception.NotAuthorized() - return func(self, context, *args, **kwargs) - - return wrapped_func - - return wrap - - -def __matches_role(context, role): - """Check if a role is allowed.""" - if role == 'all': - return True - if role == 'none': - return False - return context.project.has_role(context.user.id, role) -- cgit From 40778d77936cb63decfc56e6b75fa4c31c13a564 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 1 Sep 2010 13:28:30 -0400 Subject: notes -- conversion 'complete' except now the unit tests won't work and surely i have bugs :) --- nova/endpoint/notes.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/endpoint/notes.txt b/nova/endpoint/notes.txt index cbb7b0cd0..3e48f1749 100644 --- a/nova/endpoint/notes.txt +++ b/nova/endpoint/notes.txt @@ -41,8 +41,8 @@ CloudController and AdminController: ==== STRATEGY TO CONVERT TO EVENTLET+WSGI ==== * Controllers: - move the @rbac.allow data into an auth WSGI that is right above the call - to the controller +x move the @rbac.allow data into an auth WSGI that is right above the call +x to the controller x verify @defer.inlineCallbacks is just to allow the yield statements, then x remove the yield statements (untangle from twisted) -- cgit From b965dde9e95e16a9a207697d5729bd146c2dfd23 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 1 Sep 2010 13:55:38 -0400 Subject: Fix simple errors to the point where we can run the tests [but not pass] --- nova/api/ec2/__init__.py | 10 ++-- nova/api/ec2/apirequestcontext.py | 33 ----------- nova/api/ec2/cloud.py | 2 +- nova/api/ec2/context.py | 33 +++++++++++ nova/endpoint/api.py | 122 -------------------------------------- nova/objectstore/handler.py | 4 +- nova/tests/api_unittest.py | 3 +- nova/tests/auth_unittest.py | 2 +- nova/tests/cloud_unittest.py | 6 +- 9 files changed, 47 insertions(+), 168 deletions(-) delete mode 100644 nova/api/ec2/apirequestcontext.py create mode 100644 nova/api/ec2/context.py delete mode 100755 nova/endpoint/api.py diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index aee9915d0..3335338e0 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -26,9 +26,11 @@ import webob import webob.dec import webob.exc +from nova import exception +from nova import wsgi +from nova.api.ec2 import context from nova.api.ec2 import admin from nova.api.ec2 import cloud -from nova import exception from nova.auth import manager @@ -74,7 +76,7 @@ class Authenticate(wsgi.Middleware): raise webob.exc.HTTPForbidden() # Authenticated! - req.environ['ec2.context'] = APIRequestContext(user, project) + req.environ['ec2.context'] = context.APIRequestContext(user, project) return self.application @@ -188,7 +190,7 @@ class Authorization(wsgi.Middleware): for role in roles) -class Executor(wsg.Application): +class Executor(wsgi.Application): """ Executes 'ec2.action' upon 'ec2.controller', passing 'ec2.context' and 'ec2.action_args' (all variables in WSGI environ.) Returns an XML @@ -217,6 +219,6 @@ class Executor(wsg.Application): resp.body = ('\n' '%s' '%s' - '?') % (code, message)) + '?') % (code, message) return resp diff --git a/nova/api/ec2/apirequestcontext.py b/nova/api/ec2/apirequestcontext.py deleted file mode 100644 index fb3118020..000000000 --- a/nova/api/ec2/apirequestcontext.py +++ /dev/null @@ -1,33 +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. - -""" -APIRequestContext -""" - -import random - -class APIRequestContext(object): - def __init__(self, handler, user, project): - self.handler = handler - self.user = user - self.project = project - self.request_id = ''.join( - [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-') - for x in xrange(20)] - ) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 566887c1a..fc0eb2711 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -357,7 +357,7 @@ class CloudController(object): 'instanceId': instance_id, 'requestId': context.request_id, 'status': volume['attach_status'], - 'volumeId': volume_id}) + 'volumeId': volume_id} def _convert_to_set(self, lst, label): if lst == None or lst == []: diff --git a/nova/api/ec2/context.py b/nova/api/ec2/context.py new file mode 100644 index 000000000..fb3118020 --- /dev/null +++ b/nova/api/ec2/context.py @@ -0,0 +1,33 @@ +# 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. + +""" +APIRequestContext +""" + +import random + +class APIRequestContext(object): + def __init__(self, handler, user, project): + self.handler = handler + self.user = user + self.project = project + self.request_id = ''.join( + [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-') + for x in xrange(20)] + ) diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py deleted file mode 100755 index 311fb1880..000000000 --- a/nova/endpoint/api.py +++ /dev/null @@ -1,122 +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. - -""" -Tornado REST API Request Handlers for Nova functions -Most calls are proxied into the responsible controller. -""" - -import multiprocessing -import re -import urllib - -import tornado.web - -from nova import crypto -from nova import flags -import nova.cloudpipe.api -from nova.endpoint import cloud - - -FLAGS = flags.FLAGS -flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') - - -class RootRequestHandler(tornado.web.RequestHandler): - def get(self): - # available api versions - versions = [ - '1.0', - '2007-01-19', - '2007-03-01', - '2007-08-29', - '2007-10-10', - '2007-12-15', - '2008-02-01', - '2008-09-01', - '2009-04-04', - ] - for version in versions: - self.write('%s\n' % version) - self.finish() - - -class MetadataRequestHandler(tornado.web.RequestHandler): - def print_data(self, data): - if isinstance(data, dict): - output = '' - for key in data: - if key == '_name': - continue - output += key - if isinstance(data[key], dict): - if '_name' in data[key]: - output += '=' + str(data[key]['_name']) - else: - output += '/' - output += '\n' - self.write(output[:-1]) # cut off last \n - elif isinstance(data, list): - self.write('\n'.join(data)) - else: - self.write(str(data)) - - def lookup(self, path, data): - items = path.split('/') - for item in items: - if item: - if not isinstance(data, dict): - return data - if not item in data: - return None - data = data[item] - return data - - def get(self, path): - cc = self.application.controllers['Cloud'] - meta_data = cc.get_metadata(self.request.remote_ip) - if meta_data is None: - _log.error('Failed to get metadata for ip: %s' % - self.request.remote_ip) - raise tornado.web.HTTPError(404) - data = self.lookup(path, meta_data) - if data is None: - raise tornado.web.HTTPError(404) - self.print_data(data) - self.finish() - - -class APIServerApplication(tornado.web.Application): - def __init__(self, controllers): - tornado.web.Application.__init__(self, [ - (r'/', RootRequestHandler), - (r'/cloudpipe/(.*)', nova.cloudpipe.api.CloudPipeRequestHandler), - (r'/cloudpipe', nova.cloudpipe.api.CloudPipeRequestHandler), - (r'/services/([A-Za-z0-9]+)/', APIRequestHandler), - (r'/latest/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2009-04-04/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2008-09-01/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2008-02-01/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2007-12-15/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2007-10-10/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2007-08-29/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2007-03-01/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2007-01-19/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/1.0/([-A-Za-z0-9/]*)', MetadataRequestHandler), - ], pool=multiprocessing.Pool(4)) - self.controllers = controllers diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index 035e342ca..09591101b 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -55,7 +55,7 @@ from twisted.web import static from nova import exception from nova import flags from nova.auth import manager -from nova.endpoint import api +from nova.api.ec2 import context from nova.objectstore import bucket from nova.objectstore import image @@ -122,7 +122,7 @@ def get_context(request): request.uri, headers=request.getAllHeaders(), check_type='s3') - return api.APIRequestContext(None, user, project) + return context.APIRequestContext(None, user, project) except exception.Error as ex: logging.debug("Authentication Failure: %s" % ex) raise exception.NotAuthorized diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 9d072866c..d21ded75b 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -27,8 +27,7 @@ from twisted.internet import defer from nova import flags from nova import test from nova.auth import manager -from nova.endpoint import api -from nova.endpoint import cloud +from nova.api.ec2 import cloud FLAGS = flags.FLAGS diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index 0b404bfdc..b3b6800a1 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -26,7 +26,7 @@ from nova import crypto from nova import flags from nova import test from nova.auth import manager -from nova.endpoint import cloud +from nova.api.ec2 import cloud FLAGS = flags.FLAGS diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 3501771cc..545cbaede 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -29,8 +29,8 @@ from nova import rpc from nova import test from nova.auth import manager from nova.compute import service -from nova.endpoint import api -from nova.endpoint import cloud +from nova.api.ec2 import context +from nova.api.ec2 import cloud FLAGS = flags.FLAGS @@ -64,7 +64,7 @@ class CloudTestCase(test.BaseTestCase): except: pass admin = manager.AuthManager().get_user('admin') project = manager.AuthManager().create_project('proj', 'admin', 'proj') - self.context = api.APIRequestContext(handler=None,project=project,user=admin) + self.context = context.APIRequestContext(handler=None,project=project,user=admin) def tearDown(self): manager.AuthManager().delete_project('proj') -- cgit From 8169a2a26c5b646a4d6c63c77f15f6aaa6898cb4 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 13:04:05 -0400 Subject: Small typos, plus rework api_unittest to use WSGI instead of Tornado --- nova/api/__init__.py | 2 +- nova/api/ec2/__init__.py | 10 ++-- nova/tests/api_unittest.py | 120 +++++++++------------------------------------ 3 files changed, 29 insertions(+), 103 deletions(-) diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 0166b7fc1..786b246ec 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -37,5 +37,5 @@ class API(wsgi.Router): # be dropped; and I'm leaving off CloudPipeRequestHandler until # I hear that we need it. mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API()) - mapper.connect("/ec2/{path_info:.*}", controller=ec2.API()) + mapper.connect("/services/{path_info:.*}", controller=ec2.API()) super(API, self).__init__(mapper) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 3335338e0..a94bcb863 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -44,6 +44,7 @@ class API(wsgi.Middleware): def __init__(self): self.application = Authenticate(Router(Authorizer(Executor()))) + class Authenticate(wsgi.Middleware): """Authenticate an EC2 request and add 'ec2.context' to WSGI environ.""" @@ -81,11 +82,12 @@ class Authenticate(wsgi.Middleware): return self.application -class Router(wsgi.Application): +class Router(wsgi.Middleware): """ Add 'ec2.controller', 'ec2.action', and 'ec2.action_args' to WSGI environ. """ - def __init__(self): + def __init__(self, application): + super(Router, self).__init__(application) self.map = routes.Mapper() self.map.connect("/{controller_name}/") self.controllers = dict(Cloud=cloud.CloudController(), @@ -122,14 +124,14 @@ class Router(wsgi.Application): return self.application -class Authorization(wsgi.Middleware): +class Authorizer(wsgi.Middleware): """ Return a 401 if ec2.controller and ec2.action in WSGI environ may not be executed in ec2.context. """ def __init__(self, application): - super(Authorization, self).__init__(application) + super(Authorizer, self).__init__(application) self.action_roles = { 'CloudController': { 'DescribeAvailabilityzones': ['all'], diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index d21ded75b..a13bbdeed 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -21,53 +21,14 @@ from boto.ec2 import regioninfo import httplib import random import StringIO -from tornado import httpserver -from twisted.internet import defer +import webob -from nova import flags from nova import test from nova.auth import manager +from nova.api import ec2 from nova.api.ec2 import cloud -FLAGS = flags.FLAGS - - -# NOTE(termie): These are a bunch of helper methods and classes to short -# circuit boto calls and feed them into our tornado handlers, -# it's pretty damn circuitous so apologies if you have to fix -# a bug in it -def boto_to_tornado(method, path, headers, data, host, connection=None): - """ translate boto requests into tornado requests - - connection should be a FakeTornadoHttpConnection instance - """ - try: - headers = httpserver.HTTPHeaders() - except AttributeError: - from tornado import httputil - headers = httputil.HTTPHeaders() - for k, v in headers.iteritems(): - headers[k] = v - - req = httpserver.HTTPRequest(method=method, - uri=path, - headers=headers, - body=data, - host=host, - remote_ip='127.0.0.1', - connection=connection) - return req - - -def raw_to_httpresponse(s): - """ translate a raw tornado http response into an httplib.HTTPResponse """ - sock = FakeHttplibSocket(s) - resp = httplib.HTTPResponse(sock) - resp.begin() - return resp - - class FakeHttplibSocket(object): """ a fake socket implementation for httplib.HTTPResponse, trivial """ def __init__(self, s): @@ -77,73 +38,36 @@ class FakeHttplibSocket(object): return self.fp -class FakeTornadoStream(object): - """ a fake stream to satisfy tornado's assumptions, trivial """ - def set_close_callback(self, f): - pass - - -class FakeTornadoConnection(object): - """ a fake connection object for tornado to pass to its handlers - - web requests are expected to write to this as they get data and call - finish when they are done with the request, we buffer the writes and - kick off a callback when it is done so that we can feed the result back - into boto. - """ - def __init__(self, d): - self.d = d - self._buffer = StringIO.StringIO() - - def write(self, chunk): - self._buffer.write(chunk) - - def finish(self): - s = self._buffer.getvalue() - self.d.callback(s) - - xheaders = None - - @property - def stream(self): - return FakeTornadoStream() - - class FakeHttplibConnection(object): """ a fake httplib.HTTPConnection for boto to use requests made via this connection actually get translated and routed into - our tornado app, we then wait for the response and turn it back into + our WSGI app, we then wait for the response and turn it back into the httplib.HTTPResponse that boto expects. """ def __init__(self, app, host, is_secure=False): self.app = app self.host = host - self.deferred = defer.Deferred() def request(self, method, path, data, headers): - req = boto_to_tornado - conn = FakeTornadoConnection(self.deferred) - request = boto_to_tornado(connection=conn, - method=method, - path=path, - headers=headers, - data=data, - host=self.host) - handler = self.app(request) - self.deferred.addCallback(raw_to_httpresponse) + req = webob.Request.blank(path) + req.method = method + req.body = data + req.headers = headers + req.headers['Accept'] = 'text/html' + 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): - @defer.inlineCallbacks - def _waiter(): - result = yield self.deferred - defer.returnValue(result) - d = _waiter() - # NOTE(termie): defer.returnValue above should ensure that - # this deferred has already been called by the time - # we get here, we are going to cheat and return - # the result of the callback - return d.result + return self.http_response def close(self): pass @@ -158,20 +82,20 @@ class ApiEc2TestCase(test.BaseTestCase): self.host = '127.0.0.1' - self.app = api.APIServerApplication({'Cloud': self.cloud}) + self.app = ec2.API() self.ec2 = boto.connect_ec2( aws_access_key_id='fake', aws_secret_access_key='fake', is_secure=False, region=regioninfo.RegionInfo(None, 'test', self.host), - port=FLAGS.cc_port, + port=0, path='/services/Cloud') self.mox.StubOutWithMock(self.ec2, 'new_http_connection') def expect_http(self, host=None, is_secure=False): http = FakeHttplibConnection( - self.app, '%s:%d' % (self.host, FLAGS.cc_port), False) + self.app, '%s:0' % (self.host), False) self.ec2.new_http_connection(host, is_secure).AndReturn(http) return http -- cgit From 9fc2bb60f1b280e9bf28d68c20f04de2130bd398 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:07:24 -0400 Subject: Use port that boto expects --- nova/tests/api_unittest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 534833fba..8087a2e3b 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -93,7 +93,7 @@ class ApiEc2TestCase(test.BaseTestCase): aws_secret_access_key='fake', is_secure=False, region=regioninfo.RegionInfo(None, 'test', self.host), - port=0, + port=8773, path='/services/Cloud') self.mox.StubOutWithMock(self.ec2, 'new_http_connection') @@ -101,7 +101,7 @@ class ApiEc2TestCase(test.BaseTestCase): def expect_http(self, host=None, is_secure=False): """Returns a new EC2 connection""" http = FakeHttplibConnection( - self.app, '%s:0' % (self.host), False) + self.app, '%s:8773' % (self.host), False) # pylint: disable-msg=E1103 self.ec2.new_http_connection(host, is_secure).AndReturn(http) return http -- cgit From 0fa141a231107da931c396f113b00329d63ee430 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:10:55 -0400 Subject: Remove unused APIRequestContext.handler --- nova/api/ec2/context.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/api/ec2/context.py b/nova/api/ec2/context.py index fb3118020..f69747622 100644 --- a/nova/api/ec2/context.py +++ b/nova/api/ec2/context.py @@ -23,8 +23,7 @@ APIRequestContext import random class APIRequestContext(object): - def __init__(self, handler, user, project): - self.handler = handler + def __init__(self, user, project): self.user = user self.project = project self.request_id = ''.join( -- cgit From b360aded9cfeebfd7594b1b649bd2a1573203cd3 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:43:55 -0400 Subject: send requests to the main API instead of to the EC2 subset -- so that it can parse out the '/services/' prefix. Also, oops, match on path_info instead of path like we're supposed to. --- nova/api/ec2/__init__.py | 2 +- nova/tests/api_unittest.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index a94bcb863..1722617ae 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -97,7 +97,7 @@ class Router(wsgi.Middleware): def __call__(self, req): # Obtain the appropriate controller and action for this request. try: - match = self.map.match(req.path) + match = self.map.match(req.path_info) controller_name = match['controller_name'] controller = self.controllers[controller_name] except: diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 8087a2e3b..9f9d32784 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -27,8 +27,7 @@ import webob from nova import test from nova.auth import manager -from nova.api import ec2 -from nova.api.ec2 import cloud +from nova import api class FakeHttplibSocket(object): @@ -83,11 +82,10 @@ class ApiEc2TestCase(test.BaseTestCase): super(ApiEc2TestCase, self).setUp() self.manager = manager.AuthManager() - self.cloud = cloud.CloudController() self.host = '127.0.0.1' - self.app = ec2.API() + self.app = api.API() self.ec2 = boto.connect_ec2( aws_access_key_id='fake', aws_secret_access_key='fake', -- cgit From 3075cc7440a37118d7784057874887f751e1f6a3 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:53:57 -0400 Subject: OMG got api_unittests to pass --- nova/api/ec2/__init__.py | 11 +++++++---- nova/api/ec2/apirequest.py | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 1722617ae..e53e7d964 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -28,6 +28,7 @@ import webob.exc from nova import exception from nova import wsgi +from nova.api.ec2 import apirequest from nova.api.ec2 import context from nova.api.ec2 import admin from nova.api.ec2 import cloud @@ -174,7 +175,7 @@ class Authorizer(wsgi.Middleware): @webob.dec.wsgify def __call__(self, req): context = req.environ['ec2.context'] - controller_name = req.environ['ec2.controller'].__name__ + controller_name = req.environ['ec2.controller'].__class__.__name__ action = req.environ['ec2.action'] allowed_roles = self.action_roles[controller_name].get(action, []) if self._matches_any_role(context, allowed_roles): @@ -205,14 +206,16 @@ class Executor(wsgi.Application): action = req.environ['ec2.action'] args = req.environ['ec2.action_args'] - api_request = APIRequest(controller, action) + api_request = apirequest.APIRequest(controller, action) try: - return api_request.send(context, **args) + result = api_request.send(context, **args) + req.headers['Content-Type'] = 'text/xml' + return result except exception.ApiError as ex: return self._error(req, type(ex).__name__ + "." + ex.code, ex.message) # TODO(vish): do something more useful with unknown exceptions except Exception as ex: - return self._error(type(ex).__name__, str(ex)) + return self._error(req, type(ex).__name__, str(ex)) def _error(self, req, code, message): resp = webob.Response() diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index 261346a09..85ff2fa5e 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -20,9 +20,14 @@ APIRequest class """ +import logging +import re # TODO(termie): replace minidom with etree from xml.dom import minidom +_log = logging.getLogger("api") +_log.setLevel(logging.DEBUG) + _c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') @@ -79,7 +84,6 @@ class APIRequest(object): result = method(context, **args) - req.headers['Content-Type'] = 'text/xml' return self._render_response(result, context.request_id) def _render_response(self, response_data, request_id): -- cgit From 43f1e722b633945a8f5dca005e6fd60515bac4ae Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:55:30 -0400 Subject: Cloud tests pass --- nova/tests/cloud_unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 689251300..92e726ffa 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -60,7 +60,7 @@ class CloudTestCase(test.BaseTestCase): except: pass admin = manager.AuthManager().get_user('admin') project = manager.AuthManager().create_project('proj', 'admin', 'proj') - self.context = context.APIRequestContext(handler=None,project=project,user=admin) + self.context = context.APIRequestContext(project=project,user=admin) def tearDown(self): manager.AuthManager().delete_project('proj') -- cgit From 59adf260b59dcdcc6bc2df3260a331a4a05f535c Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:59:52 -0400 Subject: Move nova.endpoint.images to api.ec2 and delete nova.endpoint --- nova/api/ec2/cloud.py | 2 +- nova/api/ec2/images.py | 80 +++++++++++++++++++++++++++++++++++++++++++++++ nova/cloudpipe/pipelib.py | 4 +-- nova/endpoint/__init__.py | 0 nova/endpoint/images.py | 80 ----------------------------------------------- nova/endpoint/notes.txt | 62 ------------------------------------ 6 files changed, 83 insertions(+), 145 deletions(-) create mode 100644 nova/api/ec2/images.py delete mode 100644 nova/endpoint/__init__.py delete mode 100644 nova/endpoint/images.py delete mode 100644 nova/endpoint/notes.txt diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index e3122bbfc..5c9e1b170 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -35,7 +35,7 @@ from nova import utils from nova.auth import manager from nova.compute import model from nova.compute.instance_types import INSTANCE_TYPES -from nova.endpoint import images +from nova.api.ec2 import images from nova.network import service as network_service from nova.network import model as network_model from nova.volume import service diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py new file mode 100644 index 000000000..cfea4c20b --- /dev/null +++ b/nova/api/ec2/images.py @@ -0,0 +1,80 @@ +# 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. + +""" +Proxy AMI-related calls from the cloud controller, to the running +objectstore daemon. +""" + +import json +import urllib + +import boto.s3.connection + +from nova import image +from nova import flags +from nova import utils +from nova.auth import manager + + +FLAGS = flags.FLAGS + + +def modify(context, image_id, operation): + image.S3ImageService(context)._conn().make_request( + method='POST', + bucket='_images', + query_args=qs({'image_id': image_id, 'operation': operation})) + + return True + + +def register(context, image_location): + """ rpc call to register a new image based from a manifest """ + + image_id = utils.generate_uid('ami') + image.S3ImageService(context)._conn().make_request( + method='PUT', + bucket='_images', + query_args=qs({'image_location': image_location, + 'image_id': image_id})) + + return image_id + + +def list(context, filter_list=[]): + """ return a list of all images that a user can see + + optionally filtered by a list of image_id """ + + result = image.S3ImageService(context).index().values() + if not filter_list is None: + return [i for i in result if i['imageId'] in filter_list] + return result + + +def deregister(context, image_id): + """ unregister an image """ + image.S3ImageService(context).delete(image_id) + + +def qs(params): + pairs = [] + for key in params.keys(): + pairs.append(key + '=' + urllib.quote(params[key])) + return '&'.join(pairs) diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py index 2867bcb21..97272eda6 100644 --- a/nova/cloudpipe/pipelib.py +++ b/nova/cloudpipe/pipelib.py @@ -32,7 +32,7 @@ from nova import exception from nova import flags from nova import utils from nova.auth import manager -from nova.endpoint import api +from nova.api.ec2 import context FLAGS = flags.FLAGS @@ -60,7 +60,7 @@ class CloudPipe(object): key_name = self.setup_keypair(project.project_manager_id, project_id) zippy = open(zippath, "r") - context = api.APIRequestContext(handler=None, user=project.project_manager, project=project) + context = context.APIRequestContext(user=project.project_manager, project=project) reservation = self.controller.run_instances(context, # run instances expects encoded userdata, it is decoded in the get_metadata_call diff --git a/nova/endpoint/__init__.py b/nova/endpoint/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nova/endpoint/images.py b/nova/endpoint/images.py deleted file mode 100644 index cfea4c20b..000000000 --- a/nova/endpoint/images.py +++ /dev/null @@ -1,80 +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. - -""" -Proxy AMI-related calls from the cloud controller, to the running -objectstore daemon. -""" - -import json -import urllib - -import boto.s3.connection - -from nova import image -from nova import flags -from nova import utils -from nova.auth import manager - - -FLAGS = flags.FLAGS - - -def modify(context, image_id, operation): - image.S3ImageService(context)._conn().make_request( - method='POST', - bucket='_images', - query_args=qs({'image_id': image_id, 'operation': operation})) - - return True - - -def register(context, image_location): - """ rpc call to register a new image based from a manifest """ - - image_id = utils.generate_uid('ami') - image.S3ImageService(context)._conn().make_request( - method='PUT', - bucket='_images', - query_args=qs({'image_location': image_location, - 'image_id': image_id})) - - return image_id - - -def list(context, filter_list=[]): - """ return a list of all images that a user can see - - optionally filtered by a list of image_id """ - - result = image.S3ImageService(context).index().values() - if not filter_list is None: - return [i for i in result if i['imageId'] in filter_list] - return result - - -def deregister(context, image_id): - """ unregister an image """ - image.S3ImageService(context).delete(image_id) - - -def qs(params): - pairs = [] - for key in params.keys(): - pairs.append(key + '=' + urllib.quote(params[key])) - return '&'.join(pairs) diff --git a/nova/endpoint/notes.txt b/nova/endpoint/notes.txt deleted file mode 100644 index 3e48f1749..000000000 --- a/nova/endpoint/notes.txt +++ /dev/null @@ -1,62 +0,0 @@ -bin/nova-api: - somehow listens for 'cloud_topic' rpc messages and ties them to - the cloud controller (maybe so internal calls can hit the API - via Queuing instead of via HTTP?) - hands CloudController and AdminController to APIServerApplication - and hands that to Tornado. - - -api.py: - -APIServerApplication(tornado.web.Application) - maps routes to APIRequestHandler, CloudPipRequestHandler, MetadataRequestHandler, - RootRequestHandler(just lists versions) - (and to controllers which are passed to __init__) - magical twisted mapping to it - -APIRequestHandler - execute: - authenticates request - picks controller from APIServerApplication's list based on name that was at the - start of the URL (e.g. /services/Cloud has /services mapped here via - APIServerApplication then Cloud is controller_name) - picks action from incoming request arguments - dict = APIRequest(controller, action).send(Context(user, project)) - _write_callback(dict) - self.finish() - -APIRequest - send(context, **kwargs): - dict = controller.action(context, **kwargs) - return _render_response(dict) # turns into XML - - -CloudController and AdminController: - actions return dict (or True which is converted into dict(return=True)) - actions have @rbac.allow('list', 'of', 'roles', 'or', '"all"') - actions can have @defer.inlineCallbacks which is used for yield statements - can use rpc.cast and then defer a returnValue - - -==== STRATEGY TO CONVERT TO EVENTLET+WSGI ==== - -* Controllers: -x move the @rbac.allow data into an auth WSGI that is right above the call -x to the controller -x verify @defer.inlineCallbacks is just to allow the yield statements, then -x remove the yield statements (untangle from twisted) - -* nova-api: - verify that cloud_topic is going away which I seem to remember, so we can ignore rpc - -* apiserverapplication: -x replace with a Router to a wsgi.Controller -x apirequesthandler stuff is just an entry in api.APIRouter - -* apirequesthandler -x wsgi.Controller pointed to by api.APIRouter -x - basically it's execute() from old APIRequestHandler -x change to return data directly instead of _write_callback() and finish() - -* apirequest -x doesn't need to change -- cgit From 0cbde24bfc3ad13a67325e6c0e08d4650d05ea05 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 7 Sep 2010 16:00:01 -0400 Subject: Oops, APIRequestContext's signature has changed --- nova/objectstore/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index 49c647b4e..aabf6831f 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -131,7 +131,7 @@ def get_context(request): request.uri, headers=request.getAllHeaders(), check_type='s3') - return context.APIRequestContext(None, user, project) + return context.APIRequestContext(user, project) except exception.Error as ex: logging.debug("Authentication Failure: %s", ex) raise exception.NotAuthorized -- cgit From 4ddcc28c944975c6e03cccf7d360b3ec4c26e848 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 7 Sep 2010 16:01:26 -0400 Subject: New version of eventlet handles Twisted & eventlet running at the same time --- tools/pip-requires | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pip-requires b/tools/pip-requires index 13e8e5f45..79897c22b 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -6,7 +6,7 @@ amqplib==0.6.1 anyjson==0.2.4 boto==2.0b1 carrot==0.10.5 -eventlet==0.9.10 +eventlet==0.9.12 lockfile==0.8 python-daemon==1.5.5 python-gflags==1.3 -- cgit From 345749f514291928913a1ecb280b92daec2c0553 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 9 Sep 2010 19:23:27 -0400 Subject: Correct style issues brought up in termie's review --- nova/api/ec2/__init__.py | 37 +++++++++++++++++++------------------ nova/api/ec2/apirequest.py | 2 -- nova/api/ec2/cloud.py | 15 +++++++-------- nova/api/ec2/context.py | 1 + nova/api/ec2/images.py | 21 +++++++-------------- nova/image/service.py | 7 +++++++ nova/tests/api_unittest.py | 1 - 7 files changed, 41 insertions(+), 43 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index e53e7d964..d500b127c 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -16,9 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. -""" -Starting point for routing EC2 requests -""" +"""Starting point for routing EC2 requests""" import logging import routes @@ -40,6 +38,7 @@ _log.setLevel(logging.DEBUG) class API(wsgi.Middleware): + """Routing for all EC2 API requests.""" def __init__(self): @@ -47,6 +46,7 @@ class API(wsgi.Middleware): class Authenticate(wsgi.Middleware): + """Authenticate an EC2 request and add 'ec2.context' to WSGI environ.""" @webob.dec.wsgify @@ -65,28 +65,25 @@ class Authenticate(wsgi.Middleware): # Authenticate the request. try: (user, project) = manager.AuthManager().authenticate( - access, - signature, - auth_params, - req.method, - req.host, - req.path - ) - + access, + signature, + auth_params, + req.method, + req.host, + req.path) except exception.Error, ex: logging.debug("Authentication Failure: %s" % ex) raise webob.exc.HTTPForbidden() # Authenticated! req.environ['ec2.context'] = context.APIRequestContext(user, project) - return self.application class Router(wsgi.Middleware): - """ - Add 'ec2.controller', 'ec2.action', and 'ec2.action_args' to WSGI environ. - """ + + """Add ec2.'controller', .'action', and .'action_args' to WSGI environ.""" + def __init__(self, application): super(Router, self).__init__(application) self.map = routes.Mapper() @@ -121,12 +118,13 @@ class Router(wsgi.Middleware): req.environ['ec2.controller'] = controller req.environ['ec2.action'] = action req.environ['ec2.action_args'] = args - return self.application class Authorizer(wsgi.Middleware): - """ + + """Authorize an EC2 API request. + Return a 401 if ec2.controller and ec2.action in WSGI environ may not be executed in ec2.context. """ @@ -194,11 +192,14 @@ class Authorizer(wsgi.Middleware): class Executor(wsgi.Application): - """ + + """Execute an EC2 API request. + Executes 'ec2.action' upon 'ec2.controller', passing 'ec2.context' and 'ec2.action_args' (all variables in WSGI environ.) Returns an XML response, or a 400 upon failure. """ + @webob.dec.wsgify def __call__(self, req): context = req.environ['ec2.context'] diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index 85ff2fa5e..a3b20118f 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -51,7 +51,6 @@ class APIRequest(object): self.action = action def send(self, context, **kwargs): - try: method = getattr(self.controller, _camelcase_to_underscore(self.action)) @@ -83,7 +82,6 @@ class APIRequest(object): args[key] = [v for k, v in s] result = method(context, **args) - return self._render_response(result, context.request_id) def _render_response(self, response_data, request_id): diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 5c9e1b170..e1e04ca90 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -205,8 +205,8 @@ class CloudController(object): def create_key_pair(self, context, key_name, **kwargs): data = _gen_key(context.user.id, key_name) return {'keyName': key_name, - 'keyFingerprint': data['fingerprint'], - 'keyMaterial': data['private_key']} + 'keyFingerprint': data['fingerprint'], + 'keyMaterial': data['private_key']} def delete_key_pair(self, context, key_name, **kwargs): context.user.delete_key_pair(key_name) @@ -273,10 +273,11 @@ class CloudController(object): def create_volume(self, context, size, **kwargs): # TODO(vish): refactor this to create the volume object here and tell service to create it - result = rpc.call(FLAGS.volume_topic, {"method": "create_volume", - "args": {"size": size, - "user_id": context.user.id, - "project_id": context.project.id}}) + result = rpc.call(FLAGS.volume_topic, + {"method": "create_volume", + "args": {"size": size, + "user_id": context.user.id, + "project_id": context.project.id}}) # NOTE(vish): rpc returned value is in the result key in the dictionary volume = self._get_volume(context, result) return {'volumeSet': [self.format_volume(context, volume)]} @@ -638,7 +639,6 @@ class CloudController(object): image_location = kwargs['name'] image_id = images.register(context, image_location) logging.debug("Registered %s as %s" % (image_location, image_id)) - return {'imageId': image_id} def describe_image_attribute(self, context, image_id, attribute, **kwargs): @@ -682,5 +682,4 @@ class CloudController(object): aggregate_state['pending'].has_key(item_id)): del aggregate_state['pending'][item_id] aggregate_state[node_name] = items - return True diff --git a/nova/api/ec2/context.py b/nova/api/ec2/context.py index f69747622..c53ba98d9 100644 --- a/nova/api/ec2/context.py +++ b/nova/api/ec2/context.py @@ -22,6 +22,7 @@ APIRequestContext import random + class APIRequestContext(object): def __init__(self, user, project): self.user = user diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py index cfea4c20b..f0be7b899 100644 --- a/nova/api/ec2/images.py +++ b/nova/api/ec2/images.py @@ -26,20 +26,20 @@ import urllib import boto.s3.connection -from nova import image from nova import flags from nova import utils from nova.auth import manager +from nova.image import service FLAGS = flags.FLAGS def modify(context, image_id, operation): - image.S3ImageService(context)._conn().make_request( + service.S3ImageService(context)._conn().make_request( method='POST', bucket='_images', - query_args=qs({'image_id': image_id, 'operation': operation})) + query_args=service.qs({'image_id': image_id, 'operation': operation})) return True @@ -48,10 +48,10 @@ def register(context, image_location): """ rpc call to register a new image based from a manifest """ image_id = utils.generate_uid('ami') - image.S3ImageService(context)._conn().make_request( + service.S3ImageService(context)._conn().make_request( method='PUT', bucket='_images', - query_args=qs({'image_location': image_location, + query_args=service.qs({'image_location': image_location, 'image_id': image_id})) return image_id @@ -62,7 +62,7 @@ def list(context, filter_list=[]): optionally filtered by a list of image_id """ - result = image.S3ImageService(context).index().values() + result = service.S3ImageService(context).index().values() if not filter_list is None: return [i for i in result if i['imageId'] in filter_list] return result @@ -70,11 +70,4 @@ def list(context, filter_list=[]): def deregister(context, image_id): """ unregister an image """ - image.S3ImageService(context).delete(image_id) - - -def qs(params): - pairs = [] - for key in params.keys(): - pairs.append(key + '=' + urllib.quote(params[key])) - return '&'.join(pairs) + service.S3ImageService(context).delete(image_id) diff --git a/nova/image/service.py b/nova/image/service.py index 25e4bb675..f6719caec 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -133,3 +133,10 @@ class S3ImageService(ImageService): calling_format=calling, port=FLAGS.s3_port, host=FLAGS.s3_host) + + +def qs(params): + pairs = [] + for key in params.keys(): + pairs.append(key + '=' + urllib.quote(params[key])) + return '&'.join(pairs) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 9f9d32784..ffc78e71d 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -63,7 +63,6 @@ class FakeHttplibConnection(object): # 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() -- cgit From 5e02ee47c0e86986bb21f67a4d6556895de5d0ef Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 13 Sep 2010 11:53:53 -0400 Subject: Pull S3ImageService out of this mergeprop --- nova/api/ec2/images.py | 34 ++++++++++++++++++++++++++------- nova/image/service.py | 52 -------------------------------------------------- 2 files changed, 27 insertions(+), 59 deletions(-) diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py index f0be7b899..b5ce2b2cc 100644 --- a/nova/api/ec2/images.py +++ b/nova/api/ec2/images.py @@ -29,17 +29,16 @@ import boto.s3.connection from nova import flags from nova import utils from nova.auth import manager -from nova.image import service FLAGS = flags.FLAGS def modify(context, image_id, operation): - service.S3ImageService(context)._conn().make_request( + conn(context).make_request( method='POST', bucket='_images', - query_args=service.qs({'image_id': image_id, 'operation': operation})) + query_args=qs({'image_id': image_id, 'operation': operation})) return True @@ -48,10 +47,10 @@ def register(context, image_location): """ rpc call to register a new image based from a manifest """ image_id = utils.generate_uid('ami') - service.S3ImageService(context)._conn().make_request( + conn(context).make_request( method='PUT', bucket='_images', - query_args=service.qs({'image_location': image_location, + query_args=qs({'image_location': image_location, 'image_id': image_id})) return image_id @@ -62,7 +61,12 @@ def list(context, filter_list=[]): optionally filtered by a list of image_id """ - result = service.S3ImageService(context).index().values() + # FIXME: send along the list of only_images to check for + response = conn(context).make_request( + method='GET', + bucket='_images') + + result = json.loads(response.read()) if not filter_list is None: return [i for i in result if i['imageId'] in filter_list] return result @@ -70,4 +74,20 @@ def list(context, filter_list=[]): def deregister(context, image_id): """ unregister an image """ - service.S3ImageService(context).delete(image_id) + conn(context).make_request( + method='DELETE', + bucket='_images', + query_args=qs({'image_id': image_id})) + + +def conn(context): + access = manager.AuthManager().get_access_key(context.user, + context.project) + secret = str(context.user.secret) + calling = boto.s3.connection.OrdinaryCallingFormat() + return boto.s3.connection.S3Connection(aws_access_key_id=access, + aws_secret_access_key=secret, + is_secure=False, + calling_format=calling, + port=FLAGS.s3_port, + host=FLAGS.s3_host) diff --git a/nova/image/service.py b/nova/image/service.py index f6719caec..1a7a258b7 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -38,8 +38,6 @@ class ImageService(object): def show(self, id): """ Returns a dict containing image data for the given opaque image id. - - Returns None if the id does not exist. """ @@ -90,53 +88,3 @@ class LocalImageService(ImageService): Delete the given image. Raises OSError if the image does not exist. """ os.unlink(self._path_to(image_id)) - - -# TODO(gundlach): before this can be loaded dynamically in ImageService.load(), -# we'll have to make __init__() not require a context. Right now it -# is only used by the AWS API, which hard-codes it, so that's OK. -class S3ImageService(ImageService): - """Service that stores images in an S3 provider.""" - - def __init__(self, context): - self._context = context - - def index(self): - response = self._conn().make_request( - method='GET', - bucket='_images') - items = json.loads(response.read()) - return dict((item['imageId'], item) for item in items) - - def show(self, id): - response = self._conn().make_request( - method='GET', - bucket='_images', - query_args=qs({'image_id': image_id})) - return json.loads(response.read()) - - def delete(self, image_id): - self._conn().make_request( - method='DELETE', - bucket='_images', - query_args=qs({'image_id': image_id})) - - def _conn(self): - """Return a boto S3Connection to the S3 store.""" - access = manager.AuthManager().get_access_key(self._context.user, - self._context.project) - secret = str(self._context.user.secret) - calling = boto.s3.connection.OrdinaryCallingFormat() - return boto.s3.connection.S3Connection(aws_access_key_id=access, - aws_secret_access_key=secret, - is_secure=False, - calling_format=calling, - port=FLAGS.s3_port, - host=FLAGS.s3_host) - - -def qs(params): - pairs = [] - for key in params.keys(): - pairs.append(key + '=' + urllib.quote(params[key])) - return '&'.join(pairs) -- cgit From 2b87ea1ab445a5a9fb089acb0220189f736d420a Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 13 Sep 2010 12:02:50 -0400 Subject: Finish pulling S3ImageService out of this mergeprop --- nova/api/ec2/images.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py index b5ce2b2cc..2a88d66af 100644 --- a/nova/api/ec2/images.py +++ b/nova/api/ec2/images.py @@ -91,3 +91,10 @@ def conn(context): calling_format=calling, port=FLAGS.s3_port, host=FLAGS.s3_host) + + +def qs(params): + pairs = [] + for key in params.keys(): + pairs.append(key + '=' + urllib.quote(params[key])) + return '&'.join(pairs) -- cgit From 8e304fe0bf69fe5f6bad2fa3d5a71a93cb0612e8 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 16 Sep 2010 12:39:35 -0400 Subject: Fix things not quite merged perfectly -- all tests now pass --- nova/api/ec2/cloud.py | 2 +- nova/tests/network_unittest.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 7a9b5f5cf..c04e722cc 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -556,7 +556,7 @@ class CloudController(object): # NOTE(vish): Right now we don't really care if the ip is # disassociated. We may need to worry about # checking this later. Perhaps in the scheduler? - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) rpc.cast(network_topic, {"method": "disassociate_floating_ip", "args": {"context": None, diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index dc5277f02..da65b50a2 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -28,7 +28,7 @@ from nova import flags from nova import test from nova import utils from nova.auth import manager -from nova.endpoint import api +from nova.api.ec2 import context FLAGS = flags.FLAGS @@ -49,7 +49,7 @@ class NetworkTestCase(test.TrialTestCase): self.user = self.manager.create_user('netuser', 'netuser', 'netuser') self.projects = [] self.network = utils.import_object(FLAGS.network_manager) - self.context = api.APIRequestContext(None, project=None, user=self.user) + self.context = context.APIRequestContext(project=None, user=self.user) for i in range(5): name = 'project%s' % i self.projects.append(self.manager.create_project(name, -- cgit From 11b934f75ac4359b75f246fd9babfc3363a9a396 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 16 Sep 2010 14:41:51 -0500 Subject: Replaced the existing Rackspace Auth Mechanism with one that mirrors the implementation in the design document. --- nova/api/rackspace/__init__.py | 52 ++++++++++++++++----- nova/api/rackspace/auth.py | 37 +++++++++++++++ nova/tests/api/rackspace/auth.py | 81 +++++++++++++++++++++++++++++++++ nova/tests/api/rackspace/test_helper.py | 52 +++++++++++++++++++++ 4 files changed, 211 insertions(+), 11 deletions(-) create mode 100644 nova/api/rackspace/auth.py create mode 100644 nova/tests/api/rackspace/auth.py create mode 100644 nova/tests/api/rackspace/test_helper.py diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index b4d666d63..dbba97107 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -26,8 +26,10 @@ 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 flavors from nova.api.rackspace import images @@ -36,6 +38,10 @@ from nova.api.rackspace import sharedipgroups from nova.auth import manager +FLAGS = flags.FLAGS +flags.DEFINE_string('nova_api_auth', 'nova.api.rackspace.auth.FakeAuth', + 'The auth mechanism to use for the Rackspace API implemenation') + class API(wsgi.Middleware): """WSGI entry point for all Rackspace API requests.""" @@ -47,23 +53,47 @@ class API(wsgi.Middleware): class AuthMiddleware(wsgi.Middleware): """Authorize the rackspace API request or return an HTTP Forbidden.""" - #TODO(gundlach): isn't this the old Nova API's auth? Should it be replaced - #with correct RS API auth? + 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): - context = {} - if "HTTP_X_AUTH_TOKEN" in req.environ: - context['user'] = manager.AuthManager().get_user_from_access_key( - req.environ['HTTP_X_AUTH_TOKEN']) - if context['user']: - context['project'] = manager.AuthManager().get_project( - context['user'].name) - if "user" not in context: - return webob.exc.HTTPForbidden() + if not req.headers.has_key("X-Auth-Token"): + return self.authenticate(req) + + user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) + + if not user: + return webob.exc.HTTPUnauthorized() + context = {'user':user} req.environ['nova.context'] = context return self.application + def authenticate(self, req): + # Unless the request is explicitly made against // don't + # honor it + path_info = req.environ['wsgiorg.routing_args'][1]['path_info'] + if path_info: + return webob.exc.HTTPUnauthorized() + + if req.headers.has_key("X-Auth-User") and \ + req.headers.has_key("X-Auth-Key"): + username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] + token, user = self.auth_driver.authorize_user(username, key) + if user and token: + res = webob.Response() + res.headers['X-Auth-Token'] = token + res.headers['X-Server-Management-Url'] = \ + user['server_management_url'] + res.headers['X-Storage-Url'] = user['storage_url'] + res.headers['X-CDN-Management-Url'] = user['cdn_management_url'] + res.content_type = 'text/plain' + res.status = '204' + return res + else: + return webob.exc.HTTPUnauthorized() + return webob.exc.HTTPUnauthorized() class APIRouter(wsgi.Router): """ diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py new file mode 100644 index 000000000..d2b5193c3 --- /dev/null +++ b/nova/api/rackspace/auth.py @@ -0,0 +1,37 @@ +import json +from hashlib import sha1 +from nova import datastore + +class FakeAuth(object): + def __init__(self, store=datastore.Redis.instance): + self._store = store() + self.auth_hash = 'rs_fake_auth' + self._store.hsetnx(self.auth_hash, 'rs_last_id', 0) + + def authorize_token(self, token): + user = self._store.hget(self.auth_hash, token) + if user: + return json.loads(user) + return None + + def authorize_user(self, user, key): + token = sha1("%s_%s" % (user, key)).hexdigest() + user = self._store.hget(self.auth_hash, token) + if not user: + return None, None + else: + return token, json.loads(user) + + def add_user(self, user, key): + last_id = self._store.hget(self.auth_hash, 'rs_last_id') + token = sha1("%s_%s" % (user, key)).hexdigest() + user = { + 'id':last_id, + 'cdn_management_url':'cdn_management_url', + 'storage_url':'storage_url', + 'server_management_url':'server_management_url' + } + new_user = self._store.hsetnx(self.auth_hash, token, json.dumps(user)) + if new_user: + self._store.hincrby(self.auth_hash, 'rs_last_id') + diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py new file mode 100644 index 000000000..65264fae9 --- /dev/null +++ b/nova/tests/api/rackspace/auth.py @@ -0,0 +1,81 @@ +import webob +import webob.dec +import unittest +import stubout +import nova.api +import nova.api.rackspace.auth +from nova.tests.api.rackspace import test_helper + +class Test(unittest.TestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + self.stubs.Set(nova.api.rackspace.auth.FakeAuth, '__init__', + test_helper.fake_auth_init) + ds = test_helper.FakeRedis() + ds.hset(test_helper.auth_hash, 'rs_last_id', 0) + + def tearDown(self): + self.stubs.UnsetAll() + test_helper.fake_data_store = {} + + def test_authorize_user(self): + auth = nova.api.rackspace.auth.FakeAuth() + auth.add_user('herp', 'derp') + + 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'], + "server_management_url") + self.assertEqual(result.headers['X-CDN-Management-Url'], + "cdn_management_url") + self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + + def test_authorize_token(self): + auth = nova.api.rackspace.auth.FakeAuth() + auth.add_user('herp', 'derp') + + 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'], + "server_management_url") + self.assertEqual(result.headers['X-CDN-Management-Url'], + "cdn_management_url") + self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + + token = result.headers['X-Auth-Token'] + self.stubs.Set(nova.api.rackspace, 'APIRouter', + test_helper.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_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_helper.py b/nova/tests/api/rackspace/test_helper.py new file mode 100644 index 000000000..578b1e841 --- /dev/null +++ b/nova/tests/api/rackspace/test_helper.py @@ -0,0 +1,52 @@ +import webob +import webob.dec +from nova.wsgi import Router + +fake_data_store = {} +auth_hash = 'dummy_hash' + +class FakeRedis(object): + def __init__(self): + global fake_data_store + self.store = fake_data_store + + def hsetnx(self, hash_name, key, value): + if not self.store.has_key(hash_name): + self.store[hash_name] = {} + + if self.store[hash_name].has_key(key): + return 0 + self.store[hash_name][key] = value + return 1 + + def hset(self, hash_name, key, value): + if not self.store.has_key(hash_name): + self.store[hash_name] = {} + + self.store[hash_name][key] = value + return 1 + + def hget(self, hash_name, key): + if not self.store[hash_name].has_key(key): + return None + return self.store[hash_name][key] + + def hincrby(self, hash_name, key, amount=1): + self.store[hash_name][key] += amount + +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, store=FakeRedis): + global auth_hash + self._store = store() + self.auth_hash = auth_hash + -- cgit From 64dd3000c4a9b88719e86d1090097e35398d3838 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Mon, 20 Sep 2010 18:04:57 -0500 Subject: Refactored the auth branch based on review feedback --- nova/api/rackspace/__init__.py | 33 ++------- nova/api/rackspace/auth.py | 117 +++++++++++++++++++++++--------- nova/db/api.py | 15 ++++ nova/db/sqlalchemy/api.py | 23 +++++++ nova/db/sqlalchemy/models.py | 14 +++- nova/tests/api/rackspace/auth.py | 84 +++++++++++++++-------- nova/tests/api/rackspace/test_helper.py | 66 +++++++++--------- 7 files changed, 225 insertions(+), 127 deletions(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index dbba97107..f62ddc1c7 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -39,7 +39,8 @@ from nova.auth import manager FLAGS = flags.FLAGS -flags.DEFINE_string('nova_api_auth', 'nova.api.rackspace.auth.FakeAuth', +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): @@ -49,7 +50,6 @@ class API(wsgi.Middleware): app = AuthMiddleware(APIRouter()) super(API, self).__init__(app) - class AuthMiddleware(wsgi.Middleware): """Authorize the rackspace API request or return an HTTP Forbidden.""" @@ -60,41 +60,16 @@ class AuthMiddleware(wsgi.Middleware): @webob.dec.wsgify def __call__(self, req): if not req.headers.has_key("X-Auth-Token"): - return self.authenticate(req) + return self.auth_driver.authenticate(req) user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) if not user: return webob.exc.HTTPUnauthorized() - context = {'user':user} + context = {'user': user} req.environ['nova.context'] = context return self.application - def authenticate(self, req): - # Unless the request is explicitly made against // don't - # honor it - path_info = req.environ['wsgiorg.routing_args'][1]['path_info'] - if path_info: - return webob.exc.HTTPUnauthorized() - - if req.headers.has_key("X-Auth-User") and \ - req.headers.has_key("X-Auth-Key"): - username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] - token, user = self.auth_driver.authorize_user(username, key) - if user and token: - res = webob.Response() - res.headers['X-Auth-Token'] = token - res.headers['X-Server-Management-Url'] = \ - user['server_management_url'] - res.headers['X-Storage-Url'] = user['storage_url'] - res.headers['X-CDN-Management-Url'] = user['cdn_management_url'] - res.content_type = 'text/plain' - res.status = '204' - return res - else: - return webob.exc.HTTPUnauthorized() - return webob.exc.HTTPUnauthorized() - class APIRouter(wsgi.Router): """ Routes requests on the Rackspace API to the appropriate controller diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index d2b5193c3..b29596880 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -1,37 +1,88 @@ +import datetime import json -from hashlib import sha1 -from nova import datastore - -class FakeAuth(object): - def __init__(self, store=datastore.Redis.instance): - self._store = store() - self.auth_hash = 'rs_fake_auth' - self._store.hsetnx(self.auth_hash, 'rs_last_id', 0) - - def authorize_token(self, token): - user = self._store.hget(self.auth_hash, token) - if user: - return json.loads(user) - return None +import time +import webob.exc +import webob.dec +import hashlib + +from nova import auth +from nova import manager +from nova import db + +class Context(object): + pass + +class BasicApiAuthManager(manager.Manager): + """ Implements a somewhat rudimentary version of Rackspace Auth""" + + def __init__(self): + 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 webob.exc.HTTPUnauthorized() + + try: + username, key = req.headers['X-Auth-User'], \ + req.headers['X-Auth-Key'] + except KeyError: + return webob.exc.HTTPUnauthorized() - def authorize_user(self, user, key): - token = sha1("%s_%s" % (user, key)).hexdigest() - user = self._store.hget(self.auth_hash, token) - if not user: - return None, None + 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 token, json.loads(user) - - def add_user(self, user, key): - last_id = self._store.hget(self.auth_hash, 'rs_last_id') - token = sha1("%s_%s" % (user, key)).hexdigest() - user = { - 'id':last_id, - 'cdn_management_url':'cdn_management_url', - 'storage_url':'storage_url', - 'server_management_url':'server_management_url' - } - new_user = self._store.hsetnx(self.auth_hash, token, json.dumps(user)) - if new_user: - self._store.hincrby(self.auth_hash, 'rs_last_id') + return 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(self.context, token['user_id']) + return { 'id':user['id'] } + 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'] = '' + self.db.auth_create_token(self.context, token, user['id']) + return token, user + return None, None + + def _get_server_mgmt_url(self): + return 'https://%s/v1.0/' % self.host diff --git a/nova/db/api.py b/nova/db/api.py index d749ae50a..80dde7a7a 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -374,6 +374,21 @@ def export_device_create(context, values): return IMPL.export_device_create(context, values) +################### + +def auth_destroy_token(context, token): + """Destroy an auth token""" + return IMPL.auth_destroy_token(context, token) + +def auth_get_token(context, token_hash): + """Retrieves a token given the hash representing it""" + return IMPL.auth_get_token(context, token_hash) + +def auth_create_token(context, token, user_id): + """Creates a new token""" + return IMPL.auth_create_token(context, token_hash, token, user_id) + + ################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 485dca2b0..681dec15e 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -539,6 +539,29 @@ def export_device_create(_context, values): ################### +def auth_destroy_token(_context, token): + session = get_session() + session.delete(token) + +def auth_get_token(_context, token_hash): + session = get_session() + tk = session.query(models.AuthToken + ).filter_by(token_hash=token_hash) + if not tk: + raise exception.NotFound('Token %s does not exist' % token_hash) + return tk + +def auth_create_token(_context, token, user_id): + tk = models.AuthToken() + for k,v in token.iteritems(): + tk[k] = v + tk.save() + return tk + + +################### + + def volume_allocate_shelf_and_blade(_context, volume_id): session = get_session() with session.begin(): diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 6818f838c..df5848ec1 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -213,7 +213,7 @@ class Instance(BASE, NovaBase): image_id = Column(String(255)) kernel_id = Column(String(255)) - ramdisk_id = Column(String(255)) + # image_id = Column(Integer, ForeignKey('images.id'), nullable=True) # kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True) # ramdisk_id = Column(Integer, ForeignKey('images.id'), nullable=True) @@ -321,6 +321,18 @@ class NetworkIndex(BASE, NovaBase): network = relationship(Network, backref=backref('network_index', uselist=False)) +class AuthToken(BASE, NovaBase): + """Represents an authorization token for all API transactions. Fields + are a string representing the actual token and a user id for mapping + to the actual user""" + __tablename__ = 'auth_tokens' + token_hash = Column(String(255)) + user_id = Column(Integer) + server_manageent_url = Column(String(255)) + storage_url = Column(String(255)) + cdn_management_url = Column(String(255)) + + # TODO(vish): can these both come from the same baseclass? class FixedIp(BASE, NovaBase): diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index 65264fae9..8ab10d94c 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -4,23 +4,24 @@ import unittest import stubout import nova.api import nova.api.rackspace.auth +from nova import auth from nova.tests.api.rackspace import test_helper +import datetime class Test(unittest.TestCase): def setUp(self): self.stubs = stubout.StubOutForTesting() - self.stubs.Set(nova.api.rackspace.auth.FakeAuth, '__init__', - test_helper.fake_auth_init) - ds = test_helper.FakeRedis() - ds.hset(test_helper.auth_hash, 'rs_last_id', 0) + self.stubs.Set(nova.api.rackspace.auth.BasicApiAuthManager, + '__init__', test_helper.fake_auth_init) + test_helper.auth_data = {} def tearDown(self): self.stubs.UnsetAll() test_helper.fake_data_store = {} def test_authorize_user(self): - auth = nova.api.rackspace.auth.FakeAuth() - auth.add_user('herp', 'derp') + f = test_helper.FakeAuthManager() + f.add_user('derp', { 'id': 1, 'name':'herp' } ) req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'herp' @@ -29,35 +30,58 @@ class Test(unittest.TestCase): self.assertEqual(result.status, '204 No Content') self.assertEqual(len(result.headers['X-Auth-Token']), 40) self.assertEqual(result.headers['X-Server-Management-Url'], - "server_management_url") + "https://foo/v1.0/") self.assertEqual(result.headers['X-CDN-Management-Url'], - "cdn_management_url") - self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + "") + self.assertEqual(result.headers['X-Storage-Url'], "") - def test_authorize_token(self): - auth = nova.api.rackspace.auth.FakeAuth() - auth.add_user('herp', 'derp') + #def test_authorize_token(self): + # auth = nova.api.rackspace.auth.FakeAuth() + # auth.add_user('herp', 'derp') - 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'], - "server_management_url") - self.assertEqual(result.headers['X-CDN-Management-Url'], - "cdn_management_url") - self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + # 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'], + # "server_management_url") + # self.assertEqual(result.headers['X-CDN-Management-Url'], + # "cdn_management_url") + # self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + + # token = result.headers['X-Auth-Token'] + # self.stubs.Set(nova.api.rackspace, 'APIRouter', + # test_helper.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(test_helper.FakeAuthDatabase, 'auth_destroy_token', + destroy_token_mock) - token = result.headers['X-Auth-Token'] - self.stubs.Set(nova.api.rackspace, 'APIRouter', - test_helper.FakeRouter) - req = webob.Request.blank('/v1.0/fake') - req.headers['X-Auth-Token'] = token + self.stubs.Set(test_helper.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, '200 OK') - self.assertEqual(result.headers['X-Test-Success'], 'True') + self.assertEqual(result.status, '401 Unauthorized') + self.assertEqual(self.destroy_called, True) def test_bad_user(self): req = webob.Request.blank('/v1.0/') diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 578b1e841..8d784854f 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -1,38 +1,12 @@ import webob import webob.dec from nova.wsgi import Router +from nova import auth -fake_data_store = {} -auth_hash = 'dummy_hash' +auth_data = {} -class FakeRedis(object): - def __init__(self): - global fake_data_store - self.store = fake_data_store - - def hsetnx(self, hash_name, key, value): - if not self.store.has_key(hash_name): - self.store[hash_name] = {} - - if self.store[hash_name].has_key(key): - return 0 - self.store[hash_name][key] = value - return 1 - - def hset(self, hash_name, key, value): - if not self.store.has_key(hash_name): - self.store[hash_name] = {} - - self.store[hash_name][key] = value - return 1 - - def hget(self, hash_name, key): - if not self.store[hash_name].has_key(key): - return None - return self.store[hash_name][key] - - def hincrby(self, hash_name, key, amount=1): - self.store[hash_name][key] += amount +class Context(object): + pass class FakeRouter(Router): def __init__(self): @@ -45,8 +19,32 @@ class FakeRouter(Router): res.headers['X-Test-Success'] = 'True' return res -def fake_auth_init(self, store=FakeRedis): - global auth_hash - self._store = store() - self.auth_hash = auth_hash +def fake_auth_init(self): + self.db = FakeAuthDatabase() + self.context = Context() + self.auth = FakeAuthManager() + self.host = 'foo' + +class FakeAuthDatabase(object): + @staticmethod + def auth_get_token(context, token_hash): + pass + + @staticmethod + def auth_create_token(context, token, user_id): + pass + + @staticmethod + def auth_destroy_token(context, token): + pass + +class FakeAuthManager(object): + def __init__(self): + global auth_data + self.data = auth_data + + def add_user(self, key, user): + self.data[key] = user + def get_user_from_access_key(self, key): + return self.data.get(key, None) -- cgit From 0880e49a4e9c9a246e8f4d7cc805d79947de095a Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 21 Sep 2010 10:07:59 -0500 Subject: Some more refactoring and another unit test --- nova/api/rackspace/auth.py | 17 +++++++---- nova/db/api.py | 4 +-- nova/db/sqlalchemy/api.py | 2 +- nova/tests/api/rackspace/auth.py | 51 ++++++++++++++++----------------- nova/tests/api/rackspace/test_helper.py | 29 ++++++++++++------- 5 files changed, 58 insertions(+), 45 deletions(-) diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index b29596880..1ef90c324 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -4,7 +4,7 @@ import time import webob.exc import webob.dec import hashlib - +from nova import flags from nova import auth from nova import manager from nova import db @@ -12,10 +12,16 @@ from nova import db class Context(object): pass -class BasicApiAuthManager(manager.Manager): +class BasicApiAuthManager(object): """ Implements a somewhat rudimentary version of Rackspace Auth""" def __init__(self): + 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__() @@ -64,8 +70,8 @@ class BasicApiAuthManager(manager.Manager): if delta.days >= 2: self.db.auth_destroy_token(self.context, token) else: - user = self.auth.get_user(self.context, token['user_id']) - return { 'id':user['id'] } + user = self.auth.get_user(token['user_id']) + return { 'id':user['uid'] } return None def _authorize_user(self, username, key): @@ -79,7 +85,8 @@ class BasicApiAuthManager(manager.Manager): token['cdn_management_url'] = '' token['server_management_url'] = self._get_server_mgmt_url() token['storage_url'] = '' - self.db.auth_create_token(self.context, token, user['id']) + token['user_id'] = user['uid'] + self.db.auth_create_token(self.context, token) return token, user return None, None diff --git a/nova/db/api.py b/nova/db/api.py index 80dde7a7a..0f0549edf 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -384,9 +384,9 @@ def auth_get_token(context, token_hash): """Retrieves a token given the hash representing it""" return IMPL.auth_get_token(context, token_hash) -def auth_create_token(context, token, user_id): +def auth_create_token(context, token): """Creates a new token""" - return IMPL.auth_create_token(context, token_hash, token, user_id) + return IMPL.auth_create_token(context, token_hash, token) ################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 681dec15e..78bc23b7b 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -551,7 +551,7 @@ def auth_get_token(_context, token_hash): raise exception.NotFound('Token %s does not exist' % token_hash) return tk -def auth_create_token(_context, token, user_id): +def auth_create_token(_context, token): tk = models.AuthToken() for k,v in token.iteritems(): tk[k] = v diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index 8ab10d94c..0f38ce79d 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -13,7 +13,8 @@ class Test(unittest.TestCase): self.stubs = stubout.StubOutForTesting() self.stubs.Set(nova.api.rackspace.auth.BasicApiAuthManager, '__init__', test_helper.fake_auth_init) - test_helper.auth_data = {} + test_helper.FakeAuthManager.auth_data = {} + test_helper.FakeAuthDatabase.data = {} def tearDown(self): self.stubs.UnsetAll() @@ -21,8 +22,22 @@ class Test(unittest.TestCase): def test_authorize_user(self): f = test_helper.FakeAuthManager() - f.add_user('derp', { 'id': 1, 'name':'herp' } ) + 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 = test_helper.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' @@ -35,30 +50,14 @@ class Test(unittest.TestCase): "") self.assertEqual(result.headers['X-Storage-Url'], "") - #def test_authorize_token(self): - # auth = nova.api.rackspace.auth.FakeAuth() - # auth.add_user('herp', 'derp') - - # 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'], - # "server_management_url") - # self.assertEqual(result.headers['X-CDN-Management-Url'], - # "cdn_management_url") - # self.assertEqual(result.headers['X-Storage-Url'], "storage_url") - - # token = result.headers['X-Auth-Token'] - # self.stubs.Set(nova.api.rackspace, 'APIRouter', - # test_helper.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') + token = result.headers['X-Auth-Token'] + self.stubs.Set(nova.api.rackspace, 'APIRouter', + test_helper.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 diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 8d784854f..18d96d71e 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -1,10 +1,9 @@ import webob import webob.dec +import datetime from nova.wsgi import Router from nova import auth -auth_data = {} - class Context(object): pass @@ -26,25 +25,33 @@ def fake_auth_init(self): self.host = 'foo' class FakeAuthDatabase(object): + data = {} + @staticmethod def auth_get_token(context, token_hash): - pass + return FakeAuthDatabase.data.get(token_hash, None) @staticmethod - def auth_create_token(context, token, user_id): - pass + 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): - pass + if FakeAuthDatabase.data.has_key(token['token_hash']): + del FakeAuthDatabase.data['token_hash'] class FakeAuthManager(object): - def __init__(self): - global auth_data - self.data = auth_data + auth_data = {} def add_user(self, key, user): - self.data[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 self.data.get(key, None) + return FakeAuthManager.auth_data.get(key, None) -- cgit From dff6c134cb5b540ac1344faf9f0cbe7d19a8c9e7 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 21 Sep 2010 14:00:17 -0400 Subject: Fix quota unittest and don't run rbac unit tests for the moment --- nova/tests/api_unittest.py | 3 ++- nova/tests/cloud_unittest.py | 5 ++--- nova/tests/quota_unittest.py | 39 ++++++++++++++++++--------------------- run_tests.py | 3 ++- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 3d2724d12..0732c39bb 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -26,8 +26,9 @@ import StringIO import webob from nova import test -from nova.auth import manager from nova import api +from nova.api.ec2 import cloud +from nova.auth import manager class FakeHttplibSocket(object): diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 247ccc093..2f22982eb 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -63,9 +63,8 @@ class CloudTestCase(test.BaseTestCase): self.manager = manager.AuthManager() self.user = self.manager.create_user('admin', 'admin', 'admin', True) self.project = self.manager.create_project('proj', 'admin', 'proj') - self.context = context.APIRequestContext(handler=None, - user=self.user, - project=self.project) + self.context = context.APIRequestContext(user=self.user, + project=self.project) def tearDown(self): self.manager.delete_project(self.project) diff --git a/nova/tests/quota_unittest.py b/nova/tests/quota_unittest.py index cab9f663d..370ccd506 100644 --- a/nova/tests/quota_unittest.py +++ b/nova/tests/quota_unittest.py @@ -25,8 +25,8 @@ from nova import quota from nova import test from nova import utils from nova.auth import manager -from nova.endpoint import cloud -from nova.endpoint import api +from nova.api.ec2 import cloud +from nova.api.ec2 import context FLAGS = flags.FLAGS @@ -48,9 +48,8 @@ class QuotaTestCase(test.TrialTestCase): self.user = self.manager.create_user('admin', 'admin', 'admin', True) self.project = self.manager.create_project('admin', 'admin', 'admin') self.network = utils.import_object(FLAGS.network_manager) - self.context = api.APIRequestContext(handler=None, - project=self.project, - user=self.user) + self.context = context.APIRequestContext(project=self.project, + user=self.user) def tearDown(self): # pylint: disable-msg=C0103 manager.AuthManager().delete_project(self.project) @@ -95,11 +94,11 @@ class QuotaTestCase(test.TrialTestCase): for i in range(FLAGS.quota_instances): instance_id = self._create_instance() instance_ids.append(instance_id) - self.assertFailure(self.cloud.run_instances(self.context, - min_count=1, - max_count=1, - instance_type='m1.small'), - cloud.QuotaError) + self.assertRaises(cloud.QuotaError, self.cloud.run_instances, + self.context, + min_count=1, + max_count=1, + instance_type='m1.small') for instance_id in instance_ids: db.instance_destroy(self.context, instance_id) @@ -107,11 +106,11 @@ class QuotaTestCase(test.TrialTestCase): instance_ids = [] instance_id = self._create_instance(cores=4) instance_ids.append(instance_id) - self.assertFailure(self.cloud.run_instances(self.context, - min_count=1, - max_count=1, - instance_type='m1.small'), - cloud.QuotaError) + self.assertRaises(cloud.QuotaError, self.cloud.run_instances, + self.context, + min_count=1, + max_count=1, + instance_type='m1.small') for instance_id in instance_ids: db.instance_destroy(self.context, instance_id) @@ -120,10 +119,9 @@ class QuotaTestCase(test.TrialTestCase): for i in range(FLAGS.quota_volumes): volume_id = self._create_volume() volume_ids.append(volume_id) - self.assertRaises(cloud.QuotaError, - self.cloud.create_volume, - self.context, - size=10) + self.assertRaises(cloud.QuotaError, self.cloud.create_volume, + self.context, + size=10) for volume_id in volume_ids: db.volume_destroy(self.context, volume_id) @@ -151,5 +149,4 @@ class QuotaTestCase(test.TrialTestCase): # make an rpc.call, the test just finishes with OK. It # appears to be something in the magic inline callbacks # that is breaking. - self.assertFailure(self.cloud.allocate_address(self.context), - cloud.QuotaError) + self.assertRaises(cloud.QuotaError, self.cloud.allocate_address, self.context) diff --git a/run_tests.py b/run_tests.py index 4121f4c06..bea97c0b3 100644 --- a/run_tests.py +++ b/run_tests.py @@ -49,7 +49,8 @@ from nova import datastore from nova import flags from nova import twistd -from nova.tests.access_unittest import * +#TODO(gundlach): rewrite and readd this after merge +#from nova.tests.access_unittest import * from nova.tests.auth_unittest import * from nova.tests.api_unittest import * from nova.tests.cloud_unittest import * -- cgit From e027342cc647db080ee77de53b22126caf958339 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 21 Sep 2010 14:34:40 -0400 Subject: In desperation, I'm raising eventlet.__version__ so I can see why the trunk tests are failing. --- nova/tests/api_unittest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 0732c39bb..7e81d3dc8 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -107,6 +107,8 @@ class ApiEc2TestCase(test.BaseTestCase): def test_describe_instances(self): """Test that, after creating a user and a project, the describe instances call to the API works properly""" + import eventlet + raise KeyError(eventlet.__version__) self.expect_http() self.mox.ReplayAll() user = self.manager.create_user('fake', 'fake', 'fake') -- cgit From b82a9e3d3ca46e69a1583dea51a474456b867e6f Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 21 Sep 2010 15:00:43 -0400 Subject: Remove eventlet test, now that eventlet 0.9.10 has indeed been replaced by 0.9.12 per mtaylor --- nova/tests/api_unittest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 7e81d3dc8..0732c39bb 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -107,8 +107,6 @@ class ApiEc2TestCase(test.BaseTestCase): def test_describe_instances(self): """Test that, after creating a user and a project, the describe instances call to the API works properly""" - import eventlet - raise KeyError(eventlet.__version__) self.expect_http() self.mox.ReplayAll() user = self.manager.create_user('fake', 'fake', 'fake') -- cgit From 7a19f6f3978fc0942d5bc51a1ad3299968a4d215 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 21 Sep 2010 15:46:19 -0500 Subject: Missed the model include, and fixed a broke test after the merge --- nova/db/sqlalchemy/models.py | 3 ++- nova/tests/api/rackspace/auth.py | 2 ++ nova/tests/api/rackspace/test_helper.py | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index bd1e9164e..6e1c0ce16 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -475,7 +475,8 @@ def register_models(): """Register Models and create metadata""" from sqlalchemy import create_engine models = (Service, Instance, Volume, ExportDevice, - FixedIp, FloatingIp, Network, NetworkIndex) # , Image, Host) + FixedIp, FloatingIp, Network, NetworkIndex, + AuthToken) # , Image, Host) engine = create_engine(FLAGS.sql_connection, echo=False) for model in models: model.metadata.create_all(engine) diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index 0f38ce79d..429c22ad2 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -15,6 +15,8 @@ class Test(unittest.TestCase): '__init__', test_helper.fake_auth_init) test_helper.FakeAuthManager.auth_data = {} test_helper.FakeAuthDatabase.data = {} + self.stubs.Set(nova.api.rackspace, 'RateLimitingMiddleware', + test_helper.FakeRateLimiter) def tearDown(self): self.stubs.UnsetAll() diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 18d96d71e..be14e2de8 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -55,3 +55,11 @@ class FakeAuthManager(object): 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 -- cgit From 84fbfe09e10b330a5668e99422247801f370d0f9 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 21 Sep 2010 16:57:08 -0400 Subject: Rewrite rbac tests to use Authorizer middleware --- doc/source/auth.rst | 8 ---- nova/api/ec2/__init__.py | 3 ++ nova/auth/manager.py | 4 +- nova/tests/access_unittest.py | 108 ++++++++++++++++-------------------------- 4 files changed, 47 insertions(+), 76 deletions(-) diff --git a/doc/source/auth.rst b/doc/source/auth.rst index 70aca704a..3fcb309cd 100644 --- a/doc/source/auth.rst +++ b/doc/source/auth.rst @@ -172,14 +172,6 @@ Further Challenges -The :mod:`rbac` Module --------------------------- - -.. automodule:: nova.auth.rbac - :members: - :undoc-members: - :show-inheritance: - The :mod:`signer` Module ------------------------ diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index a7b10e428..b041787c2 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -25,6 +25,7 @@ import webob.dec import webob.exc from nova import exception +from nova import flags from nova import wsgi from nova.api.ec2 import apirequest from nova.api.ec2 import context @@ -33,6 +34,7 @@ from nova.api.ec2 import cloud from nova.auth import manager +FLAGS = flags.FLAGS _log = logging.getLogger("api") _log.setLevel(logging.DEBUG) @@ -176,6 +178,7 @@ class Authorizer(wsgi.Middleware): controller_name = req.environ['ec2.controller'].__class__.__name__ action = req.environ['ec2.action'] allowed_roles = self.action_roles[controller_name].get(action, []) + allowed_roles.extend(FLAGS.superuser_roles) if self._matches_any_role(context, allowed_roles): return self.application else: diff --git a/nova/auth/manager.py b/nova/auth/manager.py index bc3a8a12e..928e0fd69 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -44,7 +44,7 @@ flags.DEFINE_list('allowed_roles', # NOTE(vish): a user with one of these roles will be a superuser and # have access to all api commands flags.DEFINE_list('superuser_roles', ['cloudadmin'], - 'Roles that ignore rbac checking completely') + 'Roles that ignore authorization checking completely') # NOTE(vish): a user with one of these roles will have it for every # project, even if he or she is not a member of the project @@ -304,7 +304,7 @@ class AuthManager(object): return "%s:%s" % (user.access, Project.safe_id(project)) def is_superuser(self, user): - """Checks for superuser status, allowing user to bypass rbac + """Checks for superuser status, allowing user to bypass authorization @type user: User or uid @param user: User to check. diff --git a/nova/tests/access_unittest.py b/nova/tests/access_unittest.py index 59e1683db..d85f559f3 100644 --- a/nova/tests/access_unittest.py +++ b/nova/tests/access_unittest.py @@ -18,12 +18,13 @@ import unittest import logging +import webob from nova import exception from nova import flags from nova import test +from nova.api import ec2 from nova.auth import manager -from nova.auth import rbac FLAGS = flags.FLAGS @@ -72,9 +73,14 @@ class AccessTestCase(test.BaseTestCase): try: self.project.add_role(self.testsys, 'sysadmin') except: pass - self.context = Context() - self.context.project = self.project #user is set in each test + self.mw = ec2.Authorizer(lambda x,y: y('200 OK', []) and '') + self.mw.action_roles = {'str': { + '_allow_all': ['all'], + '_allow_none': [], + '_allow_project_manager': ['projectmanager'], + '_allow_sys_and_net': ['sysadmin', 'netadmin'], + '_allow_sysadmin': ['sysadmin']}} def tearDown(self): um = manager.AuthManager() @@ -87,76 +93,46 @@ class AccessTestCase(test.BaseTestCase): um.delete_user('testsys') super(AccessTestCase, self).tearDown() + def response_status(self, user, methodName): + context = Context() + context.project = self.project + context.user = user + environ = {'ec2.context' : context, + 'ec2.controller': 'some string', + 'ec2.action': methodName} + req = webob.Request.blank('/', environ) + resp = req.get_response(self.mw) + return resp.status_int + + def shouldAllow(self, user, methodName): + self.assertEqual(200, self.response_status(user, methodName)) + + def shouldDeny(self, user, methodName): + self.assertEqual(401, self.response_status(user, methodName)) + def test_001_allow_all(self): - self.context.user = self.testadmin - self.assertTrue(self._allow_all(self.context)) - self.context.user = self.testpmsys - self.assertTrue(self._allow_all(self.context)) - self.context.user = self.testnet - self.assertTrue(self._allow_all(self.context)) - self.context.user = self.testsys - self.assertTrue(self._allow_all(self.context)) + users = [self.testadmin, self.testpmsys, self.testnet, self.testsys] + for user in users: + self.shouldAllow(user, '_allow_all') def test_002_allow_none(self): - self.context.user = self.testadmin - self.assertTrue(self._allow_none(self.context)) - self.context.user = self.testpmsys - self.assertRaises(exception.NotAuthorized, self._allow_none, self.context) - self.context.user = self.testnet - self.assertRaises(exception.NotAuthorized, self._allow_none, self.context) - self.context.user = self.testsys - self.assertRaises(exception.NotAuthorized, self._allow_none, self.context) + self.shouldAllow(self.testadmin, '_allow_none') + users = [self.testpmsys, self.testnet, self.testsys] + for user in users: + self.shouldDeny(user, '_allow_none') def test_003_allow_project_manager(self): - self.context.user = self.testadmin - self.assertTrue(self._allow_project_manager(self.context)) - self.context.user = self.testpmsys - self.assertTrue(self._allow_project_manager(self.context)) - self.context.user = self.testnet - self.assertRaises(exception.NotAuthorized, self._allow_project_manager, self.context) - self.context.user = self.testsys - self.assertRaises(exception.NotAuthorized, self._allow_project_manager, self.context) + for user in [self.testadmin, self.testpmsys]: + self.shouldAllow(user, '_allow_project_manager') + for user in [self.testnet, self.testsys]: + self.shouldDeny(user, '_allow_project_manager') def test_004_allow_sys_and_net(self): - self.context.user = self.testadmin - self.assertTrue(self._allow_sys_and_net(self.context)) - self.context.user = self.testpmsys # doesn't have the per project sysadmin - self.assertRaises(exception.NotAuthorized, self._allow_sys_and_net, self.context) - self.context.user = self.testnet - self.assertTrue(self._allow_sys_and_net(self.context)) - self.context.user = self.testsys - self.assertTrue(self._allow_sys_and_net(self.context)) - - def test_005_allow_sys_no_pm(self): - self.context.user = self.testadmin - self.assertTrue(self._allow_sys_no_pm(self.context)) - self.context.user = self.testpmsys - self.assertRaises(exception.NotAuthorized, self._allow_sys_no_pm, self.context) - self.context.user = self.testnet - self.assertRaises(exception.NotAuthorized, self._allow_sys_no_pm, self.context) - self.context.user = self.testsys - self.assertTrue(self._allow_sys_no_pm(self.context)) - - @rbac.allow('all') - def _allow_all(self, context): - return True - - @rbac.allow('none') - def _allow_none(self, context): - return True - - @rbac.allow('projectmanager') - def _allow_project_manager(self, context): - return True - - @rbac.allow('sysadmin', 'netadmin') - def _allow_sys_and_net(self, context): - return True - - @rbac.allow('sysadmin') - @rbac.deny('projectmanager') - def _allow_sys_no_pm(self, context): - return True + for user in [self.testadmin, self.testnet, self.testsys]: + self.shouldAllow(user, '_allow_sys_and_net') + # denied because it doesn't have the per project sysadmin + for user in [self.testpmsys]: + self.shouldDeny(user, '_allow_sys_and_net') if __name__ == "__main__": # TODO: Implement use_fake as an option -- cgit From ffa426d68bfb3d1c2acaeef4c48d2662e88fc878 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 21 Sep 2010 16:58:08 -0400 Subject: Reenable access_unittest now that it works with new rbac --- run_tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/run_tests.py b/run_tests.py index bea97c0b3..4121f4c06 100644 --- a/run_tests.py +++ b/run_tests.py @@ -49,8 +49,7 @@ from nova import datastore from nova import flags from nova import twistd -#TODO(gundlach): rewrite and readd this after merge -#from nova.tests.access_unittest import * +from nova.tests.access_unittest import * from nova.tests.auth_unittest import * from nova.tests.api_unittest import * from nova.tests.cloud_unittest import * -- cgit From a8c5901faaa98b7f0c06db086a03a0d38a210986 Mon Sep 17 00:00:00 2001 From: mdietz Date: Wed, 22 Sep 2010 18:46:55 +0000 Subject: Added a primary_key to AuthToken, fixed some unbound variables, and now all unit tests pass --- nova/api/rackspace/auth.py | 5 ++++- nova/db/sqlalchemy/models.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index 1ef90c324..ce5a967eb 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -8,6 +8,9 @@ from nova import flags from nova import auth from nova import manager from nova import db +from nova import utils + +FLAGS = flags.FLAGS class Context(object): pass @@ -15,7 +18,7 @@ class Context(object): class BasicApiAuthManager(object): """ Implements a somewhat rudimentary version of Rackspace Auth""" - def __init__(self): + def __init__(self, host=None, db_driver=None): if not host: host = FLAGS.host self.host = host diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 6e1c0ce16..161c5f1bc 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -401,7 +401,7 @@ class AuthToken(BASE, NovaBase): are a string representing the actual token and a user id for mapping to the actual user""" __tablename__ = 'auth_tokens' - token_hash = Column(String(255)) + token_hash = Column(String(255), primary_key=True) user_id = Column(Integer) server_manageent_url = Column(String(255)) storage_url = Column(String(255)) -- cgit From f3f271644eac4ec74ce3786840a7743aac4f6032 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 22 Sep 2010 15:57:24 -0400 Subject: Responding to eday's feedback -- make a clearer inner wsgi app --- nova/tests/access_unittest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nova/tests/access_unittest.py b/nova/tests/access_unittest.py index d85f559f3..c8a49d2ca 100644 --- a/nova/tests/access_unittest.py +++ b/nova/tests/access_unittest.py @@ -74,7 +74,10 @@ class AccessTestCase(test.BaseTestCase): self.project.add_role(self.testsys, 'sysadmin') except: pass #user is set in each test - self.mw = ec2.Authorizer(lambda x,y: y('200 OK', []) and '') + def noopWSGIApp(environ, start_response): + start_response('200 OK', []) + return [''] + self.mw = ec2.Authorizer(noopWSGIApp) self.mw.action_roles = {'str': { '_allow_all': ['all'], '_allow_none': [], -- cgit From 6f82d0f84c9474e72ef70c9ff568d68031191e0a Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 22 Sep 2010 17:35:02 -0400 Subject: Soren's patch to fix part of ec2 --- nova/api/ec2/apirequest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index a3b20118f..a87c21fb3 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -68,10 +68,8 @@ class APIRequest(object): key = _camelcase_to_underscore(parts[0]) if len(parts) > 1: d = args.get(key, {}) - d[parts[1]] = value[0] + d[parts[1]] = value value = d - else: - value = value[0] args[key] = value for key in args.keys(): -- cgit From 9e12753508474b430c1b87fd7d59dcbc2d096042 Mon Sep 17 00:00:00 2001 From: mdietz Date: Wed, 22 Sep 2010 21:57:34 +0000 Subject: Re-added the ramdisk line I accidentally removed --- nova/db/sqlalchemy/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 161c5f1bc..f6ba7953f 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -213,7 +213,8 @@ class Instance(BASE, NovaBase): image_id = Column(String(255)) kernel_id = Column(String(255)) - + ramdisk_id = Column(String(255)) + # image_id = Column(Integer, ForeignKey('images.id'), nullable=True) # kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True) # ramdisk_id = Column(Integer, ForeignKey('images.id'), nullable=True) -- cgit From f188b5a02d34751e89fae60b4d3b1ef144f138d7 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 22 Sep 2010 19:11:04 -0400 Subject: Re-add root and metadata request handlers to EC2 API --- nova/api/__init__.py | 56 +++++++++++++++++++++++++-- nova/api/ec2/metadatarequesthandler.py | 71 ++++++++++++++++++++++++++++++++++ nova/tests/api/__init__.py | 30 ++++++++++++-- 3 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 nova/api/ec2/metadatarequesthandler.py diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 821f1deea..a0be05d86 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -23,9 +23,18 @@ Root WSGI middleware for all API controllers. import routes import webob.dec +from nova import flags from nova import wsgi from nova.api import ec2 from nova.api import rackspace +from nova.api.ec2 import metadatarequesthandler + + +flags.DEFINE_string('rsapi_subdomain', 'rs', + 'subdomain running the RS API') +flags.DEFINE_string('ec2api_subdomain', 'ec2', + 'subdomain running the EC2 API') +FLAGS = flags.FLAGS class API(wsgi.Router): @@ -33,13 +42,33 @@ class API(wsgi.Router): def __init__(self): mapper = routes.Mapper() - mapper.connect("/", controller=self.versions) - mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API()) - mapper.connect("/services/{path_info:.*}", controller=ec2.API()) + mapper.sub_domains = True + mapper.connect("/", controller=self.rsapi_versions, + conditions={'sub_domain': [FLAGS.rsapi_subdomain]}) + mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API(), + conditions={'sub_domain': [FLAGS.rsapi_subdomain]}) + + mapper.connect("/", controller=self.ec2api_versions, + conditions={'sub_domain': [FLAGS.ec2api_subdomain]}) + mapper.connect("/services/{path_info:.*}", controller=ec2.API(), + conditions={'sub_domain': [FLAGS.ec2api_subdomain]}) + mrh = metadatarequesthandler.MetadataRequestHandler() + for s in ['/latest', + '/2009-04-04', + '/2008-09-01', + '/2008-02-01', + '/2007-12-15', + '/2007-10-10', + '/2007-08-29', + '/2007-03-01', + '/2007-01-19', + '/1.0']: + mapper.connect('%s/{path_info:.*}' % s, controller=mrh, + conditions={'subdomain': FLAGS.ec2api_subdomain}) super(API, self).__init__(mapper) @webob.dec.wsgify - def versions(self, req): + def rsapi_versions(self, req): """Respond to a request for all OpenStack API versions.""" response = { "versions": [ @@ -48,3 +77,22 @@ class API(wsgi.Router): "application/xml": { "attributes": dict(version=["status", "id"])}} return wsgi.Serializer(req.environ, metadata).to_content_type(response) + + @webob.dec.wsgify + def ec2api_versions(self, req): + """Respond to a request for all EC2 versions.""" + # available api versions + versions = [ + '1.0', + '2007-01-19', + '2007-03-01', + '2007-08-29', + '2007-10-10', + '2007-12-15', + '2008-02-01', + '2008-09-01', + '2009-04-04', + ] + return ''.join('%s\n' % v for v in versions) + + diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py new file mode 100644 index 000000000..229e5a78d --- /dev/null +++ b/nova/api/ec2/metadatarequesthandler.py @@ -0,0 +1,71 @@ +# 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. + +"""Metadata request handler.""" + +import webob.dec +import webob.exc + +from nova.api.ec2 import cloud + + +class MetadataRequestHandler(object): + + """Serve metadata from the EC2 API.""" + + def print_data(self, data): + if isinstance(data, dict): + output = '' + for key in data: + if key == '_name': + continue + output += key + if isinstance(data[key], dict): + if '_name' in data[key]: + output += '=' + str(data[key]['_name']) + else: + output += '/' + output += '\n' + return output[:-1] # cut off last \n + elif isinstance(data, list): + return '\n'.join(data) + else: + return str(data) + + def lookup(self, path, data): + items = path.split('/') + for item in items: + if item: + if not isinstance(data, dict): + return data + if not item in data: + return None + data = data[item] + return data + + @webob.dec.wsgify + def __call__(self, req): + cc = cloud.CloudController() + meta_data = cc.get_metadata(req.remote_addr) + if meta_data is None: + _log.error('Failed to get metadata for ip: %s' % req.remote_addr) + raise webob.exc.HTTPNotFound() + data = self.lookup(path, meta_data) + if data is None: + raise webob.exc.HTTPNotFound() + return self.print_data(data) diff --git a/nova/tests/api/__init__.py b/nova/tests/api/__init__.py index 4682c094e..fc1ab9ae2 100644 --- a/nova/tests/api/__init__.py +++ b/nova/tests/api/__init__.py @@ -25,6 +25,7 @@ import stubout import webob import webob.dec +import nova.exception from nova import api from nova.tests.api.test_helper import * @@ -36,25 +37,46 @@ class Test(unittest.TestCase): def tearDown(self): # pylint: disable-msg=C0103 self.stubs.UnsetAll() + def _request(self, url, subdomain, **kwargs): + environ_keys = {'HTTP_HOST': '%s.example.com' % subdomain} + environ_keys.update(kwargs) + req = webob.Request.blank(url, environ_keys) + return req.get_response(api.API()) + def test_rackspace(self): self.stubs.Set(api.rackspace, 'API', APIStub) - result = webob.Request.blank('/v1.0/cloud').get_response(api.API()) + result = self._request('/v1.0/cloud', 'rs') self.assertEqual(result.body, "/cloud") def test_ec2(self): self.stubs.Set(api.ec2, 'API', APIStub) - result = webob.Request.blank('/ec2/cloud').get_response(api.API()) + result = self._request('/services/cloud', 'ec2') self.assertEqual(result.body, "/cloud") def test_not_found(self): self.stubs.Set(api.ec2, 'API', APIStub) self.stubs.Set(api.rackspace, 'API', APIStub) - result = webob.Request.blank('/test/cloud').get_response(api.API()) + result = self._request('/test/cloud', 'ec2') self.assertNotEqual(result.body, "/cloud") def test_query_api_versions(self): - result = webob.Request.blank('/').get_response(api.API()) + result = self._request('/', 'rs') self.assertTrue('CURRENT' in result.body) + def test_metadata(self): + def go(url): + result = self._request(url, 'ec2', + REMOTE_ADDR='128.192.151.2') + # Each should get to the ORM layer and fail to find the IP + self.assertRaises(nova.exception.NotFound, go, '/latest/') + self.assertRaises(nova.exception.NotFound, go, '/2009-04-04/') + self.assertRaises(nova.exception.NotFound, go, '/1.0/') + + def test_ec2_root(self): + result = self._request('/', 'ec2') + self.assertTrue('2007-12-15\n' in result.body) + + + if __name__ == '__main__': unittest.main() -- cgit From 54122c0a156d1562be76dfde41bd62006f9ed426 Mon Sep 17 00:00:00 2001 From: Eric Day Date: Wed, 22 Sep 2010 17:54:57 -0700 Subject: Various loose ends for endpoint and tornado removal cleanup, including cloudpipe API addition, rpc.call() cleanup by removing tornado ioloop, and fixing bin/* programs. Tornado still exists as part of some test cases and those should be reworked to not require it. --- bin/nova-api | 47 +++++++++-------------------- bin/nova-api-new | 45 ---------------------------- bin/nova-manage | 3 +- nova/api/__init__.py | 2 ++ nova/api/cloudpipe/__init__.py | 68 ++++++++++++++++++++++++++++++++++++++++++ nova/cloudpipe/api.py | 59 ------------------------------------ nova/cloudpipe/pipelib.py | 3 +- nova/rpc.py | 34 +++++++++++---------- nova/tests/cloud_unittest.py | 1 - 9 files changed, 105 insertions(+), 157 deletions(-) delete mode 100755 bin/nova-api-new create mode 100644 nova/api/cloudpipe/__init__.py delete mode 100644 nova/cloudpipe/api.py diff --git a/bin/nova-api b/bin/nova-api index ede09d38c..8625c487f 100755 --- a/bin/nova-api +++ b/bin/nova-api @@ -1,31 +1,28 @@ #!/usr/bin/env python +# pylint: disable-msg=C0103 # 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 +# 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 +# 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. - +# 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. """ -Tornado daemon for the main API endpoint. +Nova API daemon. """ -import logging import os import sys -from tornado import httpserver -from tornado import ioloop # If ../nova/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... @@ -35,30 +32,14 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) +from nova import api from nova import flags -from nova import server from nova import utils -from nova.endpoint import admin -from nova.endpoint import api -from nova.endpoint import cloud +from nova import wsgi FLAGS = flags.FLAGS - - -def main(_argv): - """Load the controllers and start the tornado I/O loop.""" - controllers = { - 'Cloud': cloud.CloudController(), - 'Admin': admin.AdminController()} - _app = api.APIServerApplication(controllers) - - io_inst = ioloop.IOLoop.instance() - http_server = httpserver.HTTPServer(_app) - http_server.listen(FLAGS.cc_port) - logging.debug('Started HTTP server on %s', FLAGS.cc_port) - io_inst.start() - +flags.DEFINE_integer('api_port', 8773, 'API port') if __name__ == '__main__': utils.default_flagfile() - server.serve('nova-api', main) + wsgi.run_server(api.API(), FLAGS.api_port) diff --git a/bin/nova-api-new b/bin/nova-api-new deleted file mode 100755 index 8625c487f..000000000 --- a/bin/nova-api-new +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# pylint: disable-msg=C0103 -# 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. -""" -Nova API daemon. -""" - -import os -import sys - -# If ../nova/__init__.py exists, add ../ to Python search path, so that -# it will override what happens to be installed in /usr/(local/)lib/python... -possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), - os.pardir, - os.pardir)) -if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): - sys.path.insert(0, possible_topdir) - -from nova import api -from nova import flags -from nova import utils -from nova import wsgi - -FLAGS = flags.FLAGS -flags.DEFINE_integer('api_port', 8773, 'API port') - -if __name__ == '__main__': - utils.default_flagfile() - wsgi.run_server(api.API(), FLAGS.api_port) diff --git a/bin/nova-manage b/bin/nova-manage index 824e00ac5..baa1cb4db 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -73,7 +73,6 @@ from nova import quota from nova import utils from nova.auth import manager from nova.cloudpipe import pipelib -from nova.endpoint import cloud FLAGS = flags.FLAGS @@ -84,7 +83,7 @@ class VpnCommands(object): def __init__(self): self.manager = manager.AuthManager() - self.pipe = pipelib.CloudPipe(cloud.CloudController()) + self.pipe = pipelib.CloudPipe() def list(self): """Print a listing of the VPNs for all projects.""" diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 821f1deea..ff9b94de9 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -24,6 +24,7 @@ import routes import webob.dec from nova import wsgi +from nova.api import cloudpipe from nova.api import ec2 from nova.api import rackspace @@ -36,6 +37,7 @@ class API(wsgi.Router): mapper.connect("/", controller=self.versions) mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API()) mapper.connect("/services/{path_info:.*}", controller=ec2.API()) + mapper.connect("/cloudpipe/{path_info:.*}", controller=cloudpipe.API()) super(API, self).__init__(mapper) @webob.dec.wsgify diff --git a/nova/api/cloudpipe/__init__.py b/nova/api/cloudpipe/__init__.py new file mode 100644 index 000000000..642f8ef6c --- /dev/null +++ b/nova/api/cloudpipe/__init__.py @@ -0,0 +1,68 @@ +# 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. + +""" +REST API Request Handlers for CloudPipe +""" + +import logging +import urllib +import webob +import webob.dec +import webob.exc + +from nova import crypto +from nova import wsgi +from nova.auth import manager +from nova.api.ec2 import cloud + + +_log = logging.getLogger("api") +_log.setLevel(logging.DEBUG) + + +class API(wsgi.Application): + + def __init__(self): + self.controller = cloud.CloudController() + + @webob.dec.wsgify + def __call__(self, req): + if req.method == 'POST': + return self.sign_csr(req) + _log.debug("Cloudpipe path is %s" % req.path_info) + if req.path_info.endswith("/getca/"): + return self.send_root_ca(req) + return webob.exc.HTTPNotFound() + + def get_project_id_from_ip(self, ip): + instance = self.controller.get_instance_by_ip(ip) + return instance['project_id'] + + def send_root_ca(self, req): + _log.debug("Getting root ca") + project_id = self.get_project_id_from_ip(req.remote_addr) + res = webob.Response() + res.headers["Content-Type"] = "text/plain" + res.body = crypto.fetch_ca(project_id) + return res + + def sign_csr(self, req): + project_id = self.get_project_id_from_ip(req.remote_addr) + cert = self.str_params['cert'] + return crypto.sign_csr(urllib.unquote(cert), project_id) diff --git a/nova/cloudpipe/api.py b/nova/cloudpipe/api.py deleted file mode 100644 index 56aa89834..000000000 --- a/nova/cloudpipe/api.py +++ /dev/null @@ -1,59 +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. - -""" -Tornado REST API Request Handlers for CloudPipe -""" - -import logging -import urllib - -import tornado.web - -from nova import crypto -from nova.auth import manager - - -_log = logging.getLogger("api") -_log.setLevel(logging.DEBUG) - - -class CloudPipeRequestHandler(tornado.web.RequestHandler): - def get(self, path): - path = self.request.path - _log.debug( "Cloudpipe path is %s" % path) - if path.endswith("/getca/"): - self.send_root_ca() - self.finish() - - def get_project_id_from_ip(self, ip): - cc = self.application.controllers['Cloud'] - instance = cc.get_instance_by_ip(ip) - instance['project_id'] - - def send_root_ca(self): - _log.debug( "Getting root ca") - project_id = self.get_project_id_from_ip(self.request.remote_ip) - self.set_header("Content-Type", "text/plain") - self.write(crypto.fetch_ca(project_id)) - - def post(self, *args, **kwargs): - project_id = self.get_project_id_from_ip(self.request.remote_ip) - cert = self.get_argument('cert', '') - self.write(crypto.sign_csr(urllib.unquote(cert), project_id)) - self.finish() diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py index b13a60292..abc14bbb6 100644 --- a/nova/cloudpipe/pipelib.py +++ b/nova/cloudpipe/pipelib.py @@ -32,6 +32,7 @@ from nova import exception from nova import flags from nova import utils from nova.auth import manager +from nova.api.ec2 import cloud from nova.api.ec2 import context @@ -43,7 +44,7 @@ flags.DEFINE_string('boot_script_template', class CloudPipe(object): def __init__(self, cloud_controller): - self.controller = cloud_controller + self.controller = cloud.CloudController() self.manager = manager.AuthManager() def launch_vpn_instance(self, project_id): diff --git a/nova/rpc.py b/nova/rpc.py index 84a9b5590..7e4d91a03 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -94,8 +94,6 @@ class Consumer(messaging.Consumer): injected.start() return injected - attachToTornado = attach_to_tornado - def fetch(self, no_ack=None, auto_ack=None, enable_callbacks=False): """Wraps the parent fetch with some logic for failed connections""" # TODO(vish): the logic for failed connections and logging should be @@ -266,27 +264,31 @@ def call(topic, msg): LOG.debug("MSG_ID is %s" % (msg_id)) conn = Connection.instance() - d = defer.Deferred() - consumer = DirectConsumer(connection=conn, msg_id=msg_id) - def deferred_receive(data, message): - """Acks message and callbacks or errbacks""" - message.ack() - if data['failure']: - return d.errback(RemoteError(*data['failure'])) - else: - return d.callback(data['result']) + class WaitMessage(object): - consumer.register_callback(deferred_receive) - injected = consumer.attach_to_tornado() + def __call__(self, data, message): + """Acks message and sets result.""" + message.ack() + if data['failure']: + self.result = RemoteError(*data['failure']) + else: + self.result = data['result'] - # clean up after the injected listened and return x - d.addCallback(lambda x: injected.stop() and x or x) + wait_msg = WaitMessage() + consumer = DirectConsumer(connection=conn, msg_id=msg_id) + consumer.register_callback(wait_msg) publisher = TopicPublisher(connection=conn, topic=topic) publisher.send(msg) publisher.close() - return d + + try: + consumer.wait(limit=1) + except StopIteration: + pass + consumer.close() + return wait_msg.result def cast(topic, msg): diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 2f22982eb..756ce519e 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -22,7 +22,6 @@ from M2Crypto import RSA import StringIO import time -from tornado import ioloop from twisted.internet import defer import unittest from xml.etree import ElementTree -- cgit From 378970b1495840a2a193dbecc3f9bb8701237744 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Thu, 23 Sep 2010 11:06:49 +0200 Subject: Compare project_id to '' using == (equality) rather than 'is' (identity). This is needed because '' isn't the same as u''. --- nova/auth/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index bc3a8a12e..2ec586419 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -266,7 +266,7 @@ class AuthManager(object): # NOTE(vish): if we stop using project name as id we need better # logic to find a default project for user - if project_id is '': + if project_id == '': project_id = user.name project = self.get_project(project_id) -- cgit From 08622cb48c200aa27e214fb14e47a741069b9bb0 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Thu, 23 Sep 2010 04:24:54 -0500 Subject: All timestamps should be in UTC. Without this patch, the scheduler unit tests fail for anyone sufficiently East of Greenwich. --- nova/scheduler/driver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index 2e6a5a835..c89d25a47 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -42,7 +42,8 @@ class Scheduler(object): def service_is_up(service): """Check whether a service is up based on last heartbeat.""" last_heartbeat = service['updated_at'] or service['created_at'] - elapsed = datetime.datetime.now() - last_heartbeat + # Timestamps in DB are UTC. + elapsed = datetime.datetime.utcnow() - last_heartbeat return elapsed < datetime.timedelta(seconds=FLAGS.service_down_time) def hosts_up(self, context, topic): -- cgit From d98c663d3e521d45586ed3922d93e0ca612a5639 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 23 Sep 2010 09:06:45 -0400 Subject: Added FLAGS.FAKE_subdomain letting you manually set the subdomain for testing on localhost. --- nova/api/__init__.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/nova/api/__init__.py b/nova/api/__init__.py index a0be05d86..8e4d844b2 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -34,6 +34,8 @@ flags.DEFINE_string('rsapi_subdomain', 'rs', 'subdomain running the RS 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') FLAGS = flags.FLAGS @@ -41,17 +43,26 @@ class API(wsgi.Router): """Routes top-level requests to the appropriate controller.""" def __init__(self): + rsdomain = {'sub_domain': [FLAGS.rsapi_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 = {} + elif FLAGS.FAKE_subdomain == 'ec2': + ec2domain = {} mapper = routes.Mapper() mapper.sub_domains = True mapper.connect("/", controller=self.rsapi_versions, - conditions={'sub_domain': [FLAGS.rsapi_subdomain]}) + conditions=rsdomain) mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API(), - conditions={'sub_domain': [FLAGS.rsapi_subdomain]}) + conditions=rsdomain) mapper.connect("/", controller=self.ec2api_versions, - conditions={'sub_domain': [FLAGS.ec2api_subdomain]}) + conditions=ec2domain) mapper.connect("/services/{path_info:.*}", controller=ec2.API(), - conditions={'sub_domain': [FLAGS.ec2api_subdomain]}) + conditions=ec2domain) mrh = metadatarequesthandler.MetadataRequestHandler() for s in ['/latest', '/2009-04-04', @@ -64,7 +75,7 @@ class API(wsgi.Router): '/2007-01-19', '/1.0']: mapper.connect('%s/{path_info:.*}' % s, controller=mrh, - conditions={'subdomain': FLAGS.ec2api_subdomain}) + conditions=ec2domain) super(API, self).__init__(mapper) @webob.dec.wsgify -- cgit From 24f589d421be9a15ad941c34128b4fa0bdc28db4 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 23 Sep 2010 09:13:27 -0400 Subject: Apply vish's patch --- bin/nova-api-new | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/nova-api-new b/bin/nova-api-new index 8625c487f..6f25ad8c7 100755 --- a/bin/nova-api-new +++ b/bin/nova-api-new @@ -34,12 +34,11 @@ if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): from nova import api from nova import flags -from nova import utils from nova import wsgi FLAGS = flags.FLAGS flags.DEFINE_integer('api_port', 8773, 'API port') if __name__ == '__main__': - utils.default_flagfile() + FLAGS(sys.argv) wsgi.run_server(api.API(), FLAGS.api_port) -- cgit From ebf71b08efc6ab3c590f71715aa16b925f17c38e Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Thu, 23 Sep 2010 15:47:29 +0200 Subject: Wrap WSGI container in server.serve to make it properly handle command line arguments as well as daemonise properly. Moved api and wsgi imports in the main() function to delay their inclusion until after python-daemon has closed all the file descriptors. Without this, eventlet's epoll fd gets opened before daemonize is called and thus its fd gets closed leading to very, very, very confusing errors. --- bin/nova-api-new | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bin/nova-api-new b/bin/nova-api-new index 6f25ad8c7..a5027700b 100755 --- a/bin/nova-api-new +++ b/bin/nova-api-new @@ -32,13 +32,18 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -from nova import api from nova import flags -from nova import wsgi +from nova import utils +from nova import server FLAGS = flags.FLAGS flags.DEFINE_integer('api_port', 8773, 'API port') -if __name__ == '__main__': - FLAGS(sys.argv) +def main(_args): + from nova import api + from nova import wsgi wsgi.run_server(api.API(), FLAGS.api_port) + +if __name__ == '__main__': + utils.default_flagfile() + server.serve('nova-api', main) -- cgit From 90669318581554a72890a6fd9c6837deb86c7e4c Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 23 Sep 2010 10:19:27 -0400 Subject: Spot-fix endpoint reference --- bin/nova-manage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index 824e00ac5..e9219c515 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -73,7 +73,7 @@ from nova import quota from nova import utils from nova.auth import manager from nova.cloudpipe import pipelib -from nova.endpoint import cloud +from nova.api.ec2 import cloud FLAGS = flags.FLAGS -- cgit From c9ac49b2425b932f60a87da80887d4556806ca60 Mon Sep 17 00:00:00 2001 From: Eric Day Date: Thu, 23 Sep 2010 11:21:14 -0700 Subject: Fixed cloudpipe lib init. --- nova/api/cloudpipe/__init__.py | 1 + nova/cloudpipe/pipelib.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nova/api/cloudpipe/__init__.py b/nova/api/cloudpipe/__init__.py index 642f8ef6c..6d40990a8 100644 --- a/nova/api/cloudpipe/__init__.py +++ b/nova/api/cloudpipe/__init__.py @@ -51,6 +51,7 @@ class API(wsgi.Application): return webob.exc.HTTPNotFound() def get_project_id_from_ip(self, ip): + # TODO(eday): This was removed with the ORM branch, fix! instance = self.controller.get_instance_by_ip(ip) return instance['project_id'] diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py index abc14bbb6..706a175d9 100644 --- a/nova/cloudpipe/pipelib.py +++ b/nova/cloudpipe/pipelib.py @@ -32,6 +32,7 @@ from nova import exception from nova import flags from nova import utils from nova.auth import manager +# TODO(eday): Eventually changes these to something not ec2-specific from nova.api.ec2 import cloud from nova.api.ec2 import context @@ -43,7 +44,7 @@ flags.DEFINE_string('boot_script_template', class CloudPipe(object): - def __init__(self, cloud_controller): + def __init__(self): self.controller = cloud.CloudController() self.manager = manager.AuthManager() -- cgit From 94b9d491d4f691f7ede3c0d5d8ca98288af1646f Mon Sep 17 00:00:00 2001 From: mdietz Date: Thu, 23 Sep 2010 18:28:49 +0000 Subject: Missed the model include, and fixed a broken test after the merge --- nova/db/sqlalchemy/models.py | 3 ++- nova/tests/api/rackspace/auth.py | 2 ++ nova/tests/api/rackspace/test_helper.py | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index bd1e9164e..6e1c0ce16 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -475,7 +475,8 @@ def register_models(): """Register Models and create metadata""" from sqlalchemy import create_engine models = (Service, Instance, Volume, ExportDevice, - FixedIp, FloatingIp, Network, NetworkIndex) # , Image, Host) + FixedIp, FloatingIp, Network, NetworkIndex, + AuthToken) # , Image, Host) engine = create_engine(FLAGS.sql_connection, echo=False) for model in models: model.metadata.create_all(engine) diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index 0f38ce79d..429c22ad2 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -15,6 +15,8 @@ class Test(unittest.TestCase): '__init__', test_helper.fake_auth_init) test_helper.FakeAuthManager.auth_data = {} test_helper.FakeAuthDatabase.data = {} + self.stubs.Set(nova.api.rackspace, 'RateLimitingMiddleware', + test_helper.FakeRateLimiter) def tearDown(self): self.stubs.UnsetAll() diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 18d96d71e..be14e2de8 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -55,3 +55,11 @@ class FakeAuthManager(object): 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 -- cgit From 020f1a304c15db3086169efe67994ca59ca04e0c Mon Sep 17 00:00:00 2001 From: mdietz Date: Thu, 23 Sep 2010 18:29:17 +0000 Subject: Added a primary_key to AuthToken, fixed some unbound variables, and now all unit tests pass --- nova/api/rackspace/auth.py | 5 ++++- nova/db/sqlalchemy/models.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index 1ef90c324..ce5a967eb 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -8,6 +8,9 @@ from nova import flags from nova import auth from nova import manager from nova import db +from nova import utils + +FLAGS = flags.FLAGS class Context(object): pass @@ -15,7 +18,7 @@ class Context(object): class BasicApiAuthManager(object): """ Implements a somewhat rudimentary version of Rackspace Auth""" - def __init__(self): + def __init__(self, host=None, db_driver=None): if not host: host = FLAGS.host self.host = host diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 6e1c0ce16..161c5f1bc 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -401,7 +401,7 @@ class AuthToken(BASE, NovaBase): are a string representing the actual token and a user id for mapping to the actual user""" __tablename__ = 'auth_tokens' - token_hash = Column(String(255)) + token_hash = Column(String(255), primary_key=True) user_id = Column(Integer) server_manageent_url = Column(String(255)) storage_url = Column(String(255)) -- cgit From ca854c764a21985fd07becf7b0686f5d00125851 Mon Sep 17 00:00:00 2001 From: mdietz Date: Thu, 23 Sep 2010 18:29:40 +0000 Subject: Re-added the ramdisk line I accidentally removed --- nova/db/sqlalchemy/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 161c5f1bc..f6ba7953f 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -213,7 +213,8 @@ class Instance(BASE, NovaBase): image_id = Column(String(255)) kernel_id = Column(String(255)) - + ramdisk_id = Column(String(255)) + # image_id = Column(Integer, ForeignKey('images.id'), nullable=True) # kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True) # ramdisk_id = Column(Integer, ForeignKey('images.id'), nullable=True) -- cgit From a6954efa3155868d31163236aa9e44f693f51b30 Mon Sep 17 00:00:00 2001 From: Eric Day Date: Thu, 23 Sep 2010 11:56:44 -0700 Subject: Fixed rpc consumer to use unique return connection to prevent overlap. This could be reworked to share a connection, but it should be a wait operation and not a fast poll like it was before. We could also keep a cache of opened connections to be used between requests. --- nova/rpc.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/nova/rpc.py b/nova/rpc.py index 7e4d91a03..6363335ea 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -46,9 +46,9 @@ LOG.setLevel(logging.DEBUG) class Connection(carrot_connection.BrokerConnection): """Connection instance object""" @classmethod - def instance(cls): + def instance(cls, new=False): """Returns the instance""" - if not hasattr(cls, '_instance'): + if new or not hasattr(cls, '_instance'): params = dict(hostname=FLAGS.rabbit_host, port=FLAGS.rabbit_port, userid=FLAGS.rabbit_userid, @@ -60,7 +60,10 @@ class Connection(carrot_connection.BrokerConnection): # NOTE(vish): magic is fun! # pylint: disable-msg=W0142 - cls._instance = cls(**params) + if new: + return cls(**params) + else: + cls._instance = cls(**params) return cls._instance @classmethod @@ -263,8 +266,6 @@ def call(topic, msg): msg.update({'_msg_id': msg_id}) LOG.debug("MSG_ID is %s" % (msg_id)) - conn = Connection.instance() - class WaitMessage(object): def __call__(self, data, message): @@ -276,9 +277,11 @@ def call(topic, msg): self.result = data['result'] wait_msg = WaitMessage() + conn = Connection.instance(True) consumer = DirectConsumer(connection=conn, msg_id=msg_id) consumer.register_callback(wait_msg) + conn = Connection.instance() publisher = TopicPublisher(connection=conn, topic=topic) publisher.send(msg) publisher.close() -- cgit From 2b30ffe2f3c79e3701487d18fe1d4eef671aa335 Mon Sep 17 00:00:00 2001 From: Eric Day Date: Thu, 23 Sep 2010 13:18:40 -0700 Subject: Applied vish's fixes. --- nova/api/ec2/metadatarequesthandler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py index 229e5a78d..08a8040ca 100644 --- a/nova/api/ec2/metadatarequesthandler.py +++ b/nova/api/ec2/metadatarequesthandler.py @@ -18,6 +18,8 @@ """Metadata request handler.""" +import logging + import webob.dec import webob.exc @@ -63,9 +65,9 @@ class MetadataRequestHandler(object): cc = cloud.CloudController() meta_data = cc.get_metadata(req.remote_addr) if meta_data is None: - _log.error('Failed to get metadata for ip: %s' % req.remote_addr) + logging.error('Failed to get metadata for ip: %s' % req.remote_addr) raise webob.exc.HTTPNotFound() - data = self.lookup(path, meta_data) + data = self.lookup(req.path_info, meta_data) if data is None: raise webob.exc.HTTPNotFound() return self.print_data(data) -- cgit From a70632890c610ece766bfd3c31eea4bc6eb4a316 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 23 Sep 2010 17:06:23 -0400 Subject: Apply vish's patch --- nova/api/ec2/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index b041787c2..f0aa57ee4 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -166,8 +166,8 @@ class Authorizer(wsgi.Middleware): 'ModifyImageAttribute': ['projectmanager', 'sysadmin'], }, 'AdminController': { - # All actions have the same permission: [] (the default) - # admins will be allowed to run them + # All actions have the same permission: ['none'] (the default) + # superusers will be allowed to run them # all others will get HTTPUnauthorized. }, } @@ -177,8 +177,7 @@ class Authorizer(wsgi.Middleware): context = req.environ['ec2.context'] controller_name = req.environ['ec2.controller'].__class__.__name__ action = req.environ['ec2.action'] - allowed_roles = self.action_roles[controller_name].get(action, []) - allowed_roles.extend(FLAGS.superuser_roles) + allowed_roles = self.action_roles[controller_name].get(action, ['none']) if self._matches_any_role(context, allowed_roles): return self.application else: @@ -186,6 +185,8 @@ class Authorizer(wsgi.Middleware): def _matches_any_role(self, context, roles): """Return True if any role in roles is allowed in context.""" + if context.user.is_superuser(): + return True if 'all' in roles: return True if 'none' in roles: -- cgit From be214c0ecece6d9cffced02f397ba9ce42be6d9f Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 23 Sep 2010 16:45:30 -0500 Subject: whatever --- nova/tests/api/rackspace/servers.py | 13 +++++++++++-- nova/tests/api/rackspace/test_helper.py | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 6d628e78a..2cfb8d45f 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -16,19 +16,25 @@ # under the License. import unittest - +import stubout from nova.api.rackspace import servers +import nova.api.rackspace from nova.tests.api.test_helper import * +from nova.tests.api.rackspace import test_helper class ServersTest(unittest.TestCase): def setUp(self): self.stubs = stubout.StubOutForTesting() + test_helper.FakeAuthManager.auth_data = {} + test_helper.FakeAuthDatabase.data = {} + test_helper.stub_out_auth(self.stubs) def tearDown(self): self.stubs.UnsetAll() def test_get_server_list(self): - pass + req = webob.Request.blank('/v1.0/servers') + req.get_response(nova.api.API()) def test_create_instance(self): pass @@ -56,3 +62,6 @@ class ServersTest(unittest.TestCase): def test_delete_server_instance(self): pass + +if __name__ == "__main__": + unittest.main() diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index be14e2de8..1fb2a19cc 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -3,6 +3,7 @@ import webob.dec import datetime from nova.wsgi import Router from nova import auth +import nova.api.rackspace.auth class Context(object): pass @@ -24,6 +25,27 @@ def fake_auth_init(self): self.auth = FakeAuthManager() self.host = 'foo' +def stub_out_auth(stubs): + def fake_auth_init(self, app): + self.application = app + + def fake_rate_init(self, app): + super(nova.api.rackspace.RateLimitingMiddleware, self).__init__(app) + self.application = app + + @webob.dec.wsgify + def fake_wsgi(self, req): + return self.application + + stubs.Set(nova.api.rackspace.AuthMiddleware, + '__init__', fake_auth_init) + stubs.Set(nova.api.rackspace.RateLimitingMiddleware, + '__init__', fake_rate_init) + stubs.Set(nova.api.rackspace.AuthMiddleware, + '__call__', fake_wsgi) + stubs.Set(nova.api.rackspace.RateLimitingMiddleware, + '__call__', fake_wsgi) + class FakeAuthDatabase(object): data = {} -- cgit