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(-) (limited to 'nova') 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 (limited to 'nova') 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 (limited to 'nova') 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 (limited to 'nova') 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 (limited to 'nova') 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 (limited to 'nova') 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 (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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 (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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 (limited to 'nova') 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(-) (limited to 'nova') 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 (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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 (limited to 'nova') 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(-) (limited to 'nova') 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 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(+) (limited to 'nova') 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(-) (limited to 'nova') 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 (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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 ++++++++++++++++++--------------------- 3 files changed, 22 insertions(+), 25 deletions(-) (limited to 'nova') 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) -- 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(+) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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 --- nova/api/ec2/__init__.py | 3 ++ nova/auth/manager.py | 4 +- nova/tests/access_unittest.py | 108 ++++++++++++++++-------------------------- 3 files changed, 47 insertions(+), 68 deletions(-) (limited to 'nova') 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 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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 (limited to 'nova') 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. --- 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 - 6 files changed, 90 insertions(+), 77 deletions(-) create mode 100644 nova/api/cloudpipe/__init__.py delete mode 100644 nova/cloudpipe/api.py (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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 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(-) (limited to 'nova') 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 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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(-) (limited to 'nova') 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