From 7f666230e37745b174998a485fe1d7626c4862ae Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 25 Aug 2010 16:45:59 +0000 Subject: A few small changes to install_venv to let venv builds work on the tarmac box. --- tools/install_venv.py | 4 ++++ tools/pip-requires | 1 + 2 files changed, 5 insertions(+) diff --git a/tools/install_venv.py b/tools/install_venv.py index 1f0fa3cc7..e764efff6 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -88,6 +88,10 @@ def create_virtualenv(venv=VENV): def install_dependencies(venv=VENV): print 'Installing dependencies with pip (this can take a while)...' + # Install greenlet by hand - just listing it in the requires file does not + # get it in stalled in the right order + run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, 'greenlet'], + redirect_output=False) run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, '-r', PIP_REQUIRES], redirect_output=False) run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, TWISTED_NOVA], diff --git a/tools/pip-requires b/tools/pip-requires index 13e8e5f45..9853252dc 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -18,3 +18,4 @@ wsgiref==0.1.2 zope.interface==3.6.1 mox==0.5.0 -f http://pymox.googlecode.com/files/mox-0.5.0.tar.gz +greenlet==0.3.1 -- cgit From a6bd6f8581b9c03da9aceed7d87f4664410d0998 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 26 Aug 2010 14:09:14 -0400 Subject: work endpoint/images.py into an S3ImageService. The translation isn't perfect, but it's a start. --- nova/endpoint/images.py | 30 +++++------------------------- nova/image/service.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/nova/endpoint/images.py b/nova/endpoint/images.py index 2a88d66af..cfea4c20b 100644 --- a/nova/endpoint/images.py +++ b/nova/endpoint/images.py @@ -26,6 +26,7 @@ import urllib import boto.s3.connection +from nova import image from nova import flags from nova import utils from nova.auth import manager @@ -35,7 +36,7 @@ FLAGS = flags.FLAGS def modify(context, image_id, operation): - conn(context).make_request( + image.S3ImageService(context)._conn().make_request( method='POST', bucket='_images', query_args=qs({'image_id': image_id, 'operation': operation})) @@ -47,7 +48,7 @@ def register(context, image_location): """ rpc call to register a new image based from a manifest """ image_id = utils.generate_uid('ami') - conn(context).make_request( + image.S3ImageService(context)._conn().make_request( method='PUT', bucket='_images', query_args=qs({'image_location': image_location, @@ -61,12 +62,7 @@ def list(context, filter_list=[]): optionally filtered by a list of image_id """ - # FIXME: send along the list of only_images to check for - response = conn(context).make_request( - method='GET', - bucket='_images') - - result = json.loads(response.read()) + result = image.S3ImageService(context).index().values() if not filter_list is None: return [i for i in result if i['imageId'] in filter_list] return result @@ -74,23 +70,7 @@ def list(context, filter_list=[]): def deregister(context, image_id): """ unregister an image """ - conn(context).make_request( - method='DELETE', - bucket='_images', - query_args=qs({'image_id': image_id})) - - -def conn(context): - access = manager.AuthManager().get_access_key(context.user, - context.project) - secret = str(context.user.secret) - calling = boto.s3.connection.OrdinaryCallingFormat() - return boto.s3.connection.S3Connection(aws_access_key_id=access, - aws_secret_access_key=secret, - is_secure=False, - calling_format=calling, - port=FLAGS.s3_port, - host=FLAGS.s3_host) + image.S3ImageService(context).delete(image_id) def qs(params): diff --git a/nova/image/service.py b/nova/image/service.py index 1a7a258b7..25e4bb675 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -38,6 +38,8 @@ class ImageService(object): def show(self, id): """ Returns a dict containing image data for the given opaque image id. + + Returns None if the id does not exist. """ @@ -88,3 +90,46 @@ class LocalImageService(ImageService): Delete the given image. Raises OSError if the image does not exist. """ os.unlink(self._path_to(image_id)) + + +# TODO(gundlach): before this can be loaded dynamically in ImageService.load(), +# we'll have to make __init__() not require a context. Right now it +# is only used by the AWS API, which hard-codes it, so that's OK. +class S3ImageService(ImageService): + """Service that stores images in an S3 provider.""" + + def __init__(self, context): + self._context = context + + def index(self): + response = self._conn().make_request( + method='GET', + bucket='_images') + items = json.loads(response.read()) + return dict((item['imageId'], item) for item in items) + + def show(self, id): + response = self._conn().make_request( + method='GET', + bucket='_images', + query_args=qs({'image_id': image_id})) + return json.loads(response.read()) + + def delete(self, image_id): + self._conn().make_request( + method='DELETE', + bucket='_images', + query_args=qs({'image_id': image_id})) + + def _conn(self): + """Return a boto S3Connection to the S3 store.""" + access = manager.AuthManager().get_access_key(self._context.user, + self._context.project) + secret = str(self._context.user.secret) + calling = boto.s3.connection.OrdinaryCallingFormat() + return boto.s3.connection.S3Connection(aws_access_key_id=access, + aws_secret_access_key=secret, + is_secure=False, + calling_format=calling, + port=FLAGS.s3_port, + host=FLAGS.s3_host) -- cgit From f0223b5135059ac6535739916a297654953751fc Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 30 Aug 2010 18:38:26 -0400 Subject: Notes for converting Tornado to Eventlet --- nova/endpoint/notes.txt | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 nova/endpoint/notes.txt diff --git a/nova/endpoint/notes.txt b/nova/endpoint/notes.txt new file mode 100644 index 000000000..c1d441de0 --- /dev/null +++ b/nova/endpoint/notes.txt @@ -0,0 +1,62 @@ +bin/nova-api: + somehow listens for 'cloud_topic' rpc messages and ties them to + the cloud controller (maybe so internal calls can hit the API + via Queuing instead of via HTTP?) + hands CloudController and AdminController to APIServerApplication + and hands that to Tornado. + + +api.py: + +APIServerApplication(tornado.web.Application) + maps routes to APIRequestHandler, CloudPipRequestHandler, MetadataRequestHandler, + RootRequestHandler(just lists versions) + (and to controllers which are passed to __init__) + magical twisted mapping to it + +APIRequestHandler + execute: + authenticates request + picks controller from APIServerApplication's list + picks action from incoming request arguments + dict = APIRequest(controller, action).send(Context(user, project)) + _write_callback(dict) + self.finish() + +APIRequest + send(context, **kwargs): + dict = controller.action(context, **kwargs) + return _render_response(dict) # turns into XML + + +CloudController and AdminController: + actions return dict (or True which is converted into dict(return=True)) + actions have @rbac.allow('list', 'of', 'roles', 'or', '"all"') + actions can have @defer.inlineCallbacks which is used for yield statements + can use rpc.cast and then defer a returnValue + + +==== STRATEGY TO CONVERT TO EVENTLET+WSGI ==== + +* Controllers: + move the @rbac.allow data into an auth WSGI that is right above the call + to the controller + verify @defer.inlineCallbacks is just to allow the yield statements, then + remove the yield statements (untangle from twisted) + +* nova-api: + verify that cloud_topic is going away which I seem to remember, so we can ignore rpc + +* apiserverapplication: + replace with a Router to a wsgi.Controller + root controller all goes into a "version" action + ??? dunno what cloudpipes or metadatarequesthandlers do... + apirequesthandler stuff goes into an "ec2" action + +* apirequesthandler + ec2() method on wsgi.Controller + - basically it's execute() from old APIRequestHandler + change to return data directly instead of _write_callback() and finish() + +* apirequest + doesn't need to change -- cgit From 909c24b9cd35d6752f9f051f4e9a80ce30eaee4d Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 30 Aug 2010 19:04:51 -0400 Subject: Move class into its own file --- nova/api/ec2/apirequesthandler.py | 126 ++++++++++++++++++++++++++++++++++++++ nova/endpoint/api.py | 101 ------------------------------ 2 files changed, 126 insertions(+), 101 deletions(-) create mode 100644 nova/api/ec2/apirequesthandler.py diff --git a/nova/api/ec2/apirequesthandler.py b/nova/api/ec2/apirequesthandler.py new file mode 100644 index 000000000..bbba60c02 --- /dev/null +++ b/nova/api/ec2/apirequesthandler.py @@ -0,0 +1,126 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +APIRequestHandler, pulled unmodified out of nova.endpoint.api +""" + +import logging + +import tornado.web + +from nova import exception +from nova import utils +from nova.auth import manager + + +_log = logging.getLogger("api") +_log.setLevel(logging.DEBUG) + + +class APIRequestHandler(tornado.web.RequestHandler): + def get(self, controller_name): + self.execute(controller_name) + + @tornado.web.asynchronous + def execute(self, controller_name): + # Obtain the appropriate controller for this request. + try: + controller = self.application.controllers[controller_name] + except KeyError: + self._error('unhandled', 'no controller named %s' % controller_name) + return + + args = self.request.arguments + + # Read request signature. + try: + signature = args.pop('Signature')[0] + except: + raise tornado.web.HTTPError(400) + + # Make a copy of args for authentication and signature verification. + auth_params = {} + for key, value in args.items(): + auth_params[key] = value[0] + + # Get requested action and remove authentication args for final request. + try: + action = args.pop('Action')[0] + access = args.pop('AWSAccessKeyId')[0] + args.pop('SignatureMethod') + args.pop('SignatureVersion') + args.pop('Version') + args.pop('Timestamp') + except: + raise tornado.web.HTTPError(400) + + # Authenticate the request. + try: + (user, project) = manager.AuthManager().authenticate( + access, + signature, + auth_params, + self.request.method, + self.request.host, + self.request.path + ) + + except exception.Error, ex: + logging.debug("Authentication Failure: %s" % ex) + raise tornado.web.HTTPError(403) + + _log.debug('action: %s' % action) + + for key, value in args.items(): + _log.debug('arg: %s\t\tval: %s' % (key, value)) + + request = APIRequest(controller, action) + context = APIRequestContext(self, user, project) + d = request.send(context, **args) + # d.addCallback(utils.debug) + + # TODO: Wrap response in AWS XML format + d.addCallbacks(self._write_callback, self._error_callback) + + def _write_callback(self, data): + self.set_header('Content-Type', 'text/xml') + self.write(data) + self.finish() + + def _error_callback(self, failure): + try: + failure.raiseException() + except exception.ApiError as ex: + self._error(type(ex).__name__ + "." + ex.code, ex.message) + # TODO(vish): do something more useful with unknown exceptions + except Exception as ex: + self._error(type(ex).__name__, str(ex)) + raise + + def post(self, controller_name): + self.execute(controller_name) + + def _error(self, code, message): + self._status_code = 400 + self.set_header('Content-Type', 'text/xml') + self.write('\n') + self.write('%s' + '%s' + '?' % (code, message)) + self.finish() diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 40be00bb7..25ed613b9 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -21,7 +21,6 @@ Tornado REST API Request Handlers for Nova functions Most calls are proxied into the responsible controller. """ -import logging import multiprocessing import random import re @@ -33,10 +32,7 @@ import tornado.web from twisted.internet import defer from nova import crypto -from nova import exception from nova import flags -from nova import utils -from nova.auth import manager import nova.cloudpipe.api from nova.endpoint import cloud @@ -45,10 +41,6 @@ FLAGS = flags.FLAGS flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') -_log = logging.getLogger("api") -_log.setLevel(logging.DEBUG) - - _c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') @@ -230,99 +222,6 @@ class MetadataRequestHandler(tornado.web.RequestHandler): self.finish() -class APIRequestHandler(tornado.web.RequestHandler): - def get(self, controller_name): - self.execute(controller_name) - - @tornado.web.asynchronous - def execute(self, controller_name): - # Obtain the appropriate controller for this request. - try: - controller = self.application.controllers[controller_name] - except KeyError: - self._error('unhandled', 'no controller named %s' % controller_name) - return - - args = self.request.arguments - - # Read request signature. - try: - signature = args.pop('Signature')[0] - except: - raise tornado.web.HTTPError(400) - - # Make a copy of args for authentication and signature verification. - auth_params = {} - for key, value in args.items(): - auth_params[key] = value[0] - - # Get requested action and remove authentication args for final request. - try: - action = args.pop('Action')[0] - access = args.pop('AWSAccessKeyId')[0] - args.pop('SignatureMethod') - args.pop('SignatureVersion') - args.pop('Version') - args.pop('Timestamp') - except: - raise tornado.web.HTTPError(400) - - # Authenticate the request. - try: - (user, project) = manager.AuthManager().authenticate( - access, - signature, - auth_params, - self.request.method, - self.request.host, - self.request.path - ) - - except exception.Error, ex: - logging.debug("Authentication Failure: %s" % ex) - raise tornado.web.HTTPError(403) - - _log.debug('action: %s' % action) - - for key, value in args.items(): - _log.debug('arg: %s\t\tval: %s' % (key, value)) - - request = APIRequest(controller, action) - context = APIRequestContext(self, user, project) - d = request.send(context, **args) - # d.addCallback(utils.debug) - - # TODO: Wrap response in AWS XML format - d.addCallbacks(self._write_callback, self._error_callback) - - def _write_callback(self, data): - self.set_header('Content-Type', 'text/xml') - self.write(data) - self.finish() - - def _error_callback(self, failure): - try: - failure.raiseException() - except exception.ApiError as ex: - self._error(type(ex).__name__ + "." + ex.code, ex.message) - # TODO(vish): do something more useful with unknown exceptions - except Exception as ex: - self._error(type(ex).__name__, str(ex)) - raise - - def post(self, controller_name): - self.execute(controller_name) - - def _error(self, code, message): - self._status_code = 400 - self.set_header('Content-Type', 'text/xml') - self.write('\n') - self.write('%s' - '%s' - '?' % (code, message)) - self.finish() - - class APIServerApplication(tornado.web.Application): def __init__(self, controllers): tornado.web.Application.__init__(self, [ -- cgit From be2b529a987627bf454f7343df74d4e8ae670761 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 30 Aug 2010 19:08:22 -0400 Subject: Move APIRequest into its own file --- nova/api/ec2/apirequest.py | 132 +++++++++++++++++++++++++++++++++++++++++++++ nova/endpoint/api.py | 109 ------------------------------------- 2 files changed, 132 insertions(+), 109 deletions(-) create mode 100644 nova/api/ec2/apirequest.py diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py new file mode 100644 index 000000000..1fc84248b --- /dev/null +++ b/nova/api/ec2/apirequest.py @@ -0,0 +1,132 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +APIRequest class +""" + +# TODO(termie): replace minidom with etree +from xml.dom import minidom + +from twisted.internet import defer + + +_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') + + +def _camelcase_to_underscore(str): + return _c2u.sub(r'_\1', str).lower().strip('_') + + +def _underscore_to_camelcase(str): + return ''.join([x[:1].upper() + x[1:] for x in str.split('_')]) + + +def _underscore_to_xmlcase(str): + res = _underscore_to_camelcase(str) + return res[:1].lower() + res[1:] + + +class APIRequest(object): + def __init__(self, controller, action): + self.controller = controller + self.action = action + + def send(self, context, **kwargs): + + try: + method = getattr(self.controller, + _camelcase_to_underscore(self.action)) + except AttributeError: + _error = ('Unsupported API request: controller = %s,' + 'action = %s') % (self.controller, self.action) + _log.warning(_error) + # TODO: Raise custom exception, trap in apiserver, + # and reraise as 400 error. + raise Exception(_error) + + args = {} + for key, value in kwargs.items(): + parts = key.split(".") + key = _camelcase_to_underscore(parts[0]) + if len(parts) > 1: + d = args.get(key, {}) + d[parts[1]] = value[0] + value = d + else: + value = value[0] + args[key] = value + + for key in args.keys(): + if isinstance(args[key], dict): + if args[key] != {} and args[key].keys()[0].isdigit(): + s = args[key].items() + s.sort() + args[key] = [v for k, v in s] + + d = defer.maybeDeferred(method, context, **args) + d.addCallback(self._render_response, context.request_id) + return d + + def _render_response(self, response_data, request_id): + xml = minidom.Document() + + response_el = xml.createElement(self.action + 'Response') + response_el.setAttribute('xmlns', + 'http://ec2.amazonaws.com/doc/2009-11-30/') + request_id_el = xml.createElement('requestId') + request_id_el.appendChild(xml.createTextNode(request_id)) + response_el.appendChild(request_id_el) + if(response_data == True): + self._render_dict(xml, response_el, {'return': 'true'}) + else: + self._render_dict(xml, response_el, response_data) + + xml.appendChild(response_el) + + response = xml.toxml() + xml.unlink() + _log.debug(response) + return response + + def _render_dict(self, xml, el, data): + try: + for key in data.keys(): + val = data[key] + el.appendChild(self._render_data(xml, key, val)) + except: + _log.debug(data) + raise + + def _render_data(self, xml, el_name, data): + el_name = _underscore_to_xmlcase(el_name) + data_el = xml.createElement(el_name) + + if isinstance(data, list): + for item in data: + data_el.appendChild(self._render_data(xml, 'item', item)) + elif isinstance(data, dict): + self._render_dict(xml, data_el, data) + elif hasattr(data, '__dict__'): + self._render_dict(xml, data_el, data.__dict__) + elif isinstance(data, bool): + data_el.appendChild(xml.createTextNode(str(data).lower())) + elif data != None: + data_el.appendChild(xml.createTextNode(str(data))) + + return data_el diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 25ed613b9..5a4e496a0 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -25,11 +25,8 @@ import multiprocessing import random import re import urllib -# TODO(termie): replace minidom with etree -from xml.dom import minidom import tornado.web -from twisted.internet import defer from nova import crypto from nova import flags @@ -41,22 +38,6 @@ FLAGS = flags.FLAGS flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') -_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') - - -def _camelcase_to_underscore(str): - return _c2u.sub(r'_\1', str).lower().strip('_') - - -def _underscore_to_camelcase(str): - return ''.join([x[:1].upper() + x[1:] for x in str.split('_')]) - - -def _underscore_to_xmlcase(str): - res = _underscore_to_camelcase(str) - return res[:1].lower() + res[1:] - - class APIRequestContext(object): def __init__(self, handler, user, project): self.handler = handler @@ -68,96 +49,6 @@ class APIRequestContext(object): ) -class APIRequest(object): - def __init__(self, controller, action): - self.controller = controller - self.action = action - - def send(self, context, **kwargs): - - try: - method = getattr(self.controller, - _camelcase_to_underscore(self.action)) - except AttributeError: - _error = ('Unsupported API request: controller = %s,' - 'action = %s') % (self.controller, self.action) - _log.warning(_error) - # TODO: Raise custom exception, trap in apiserver, - # and reraise as 400 error. - raise Exception(_error) - - args = {} - for key, value in kwargs.items(): - parts = key.split(".") - key = _camelcase_to_underscore(parts[0]) - if len(parts) > 1: - d = args.get(key, {}) - d[parts[1]] = value[0] - value = d - else: - value = value[0] - args[key] = value - - for key in args.keys(): - if isinstance(args[key], dict): - if args[key] != {} and args[key].keys()[0].isdigit(): - s = args[key].items() - s.sort() - args[key] = [v for k, v in s] - - d = defer.maybeDeferred(method, context, **args) - d.addCallback(self._render_response, context.request_id) - return d - - def _render_response(self, response_data, request_id): - xml = minidom.Document() - - response_el = xml.createElement(self.action + 'Response') - response_el.setAttribute('xmlns', - 'http://ec2.amazonaws.com/doc/2009-11-30/') - request_id_el = xml.createElement('requestId') - request_id_el.appendChild(xml.createTextNode(request_id)) - response_el.appendChild(request_id_el) - if(response_data == True): - self._render_dict(xml, response_el, {'return': 'true'}) - else: - self._render_dict(xml, response_el, response_data) - - xml.appendChild(response_el) - - response = xml.toxml() - xml.unlink() - _log.debug(response) - return response - - def _render_dict(self, xml, el, data): - try: - for key in data.keys(): - val = data[key] - el.appendChild(self._render_data(xml, key, val)) - except: - _log.debug(data) - raise - - def _render_data(self, xml, el_name, data): - el_name = _underscore_to_xmlcase(el_name) - data_el = xml.createElement(el_name) - - if isinstance(data, list): - for item in data: - data_el.appendChild(self._render_data(xml, 'item', item)) - elif isinstance(data, dict): - self._render_dict(xml, data_el, data) - elif hasattr(data, '__dict__'): - self._render_dict(xml, data_el, data.__dict__) - elif isinstance(data, bool): - data_el.appendChild(xml.createTextNode(str(data).lower())) - elif data != None: - data_el.appendChild(xml.createTextNode(str(data))) - - return data_el - - class RootRequestHandler(tornado.web.RequestHandler): def get(self): # available api versions -- cgit From 4bca41506c90e779a8d4a5defdca3add79073185 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 30 Aug 2010 19:10:17 -0400 Subject: Move APIRequestContext into its own file --- nova/api/ec2/apirequestcontext.py | 33 +++++++++++++++++++++++++++++++++ nova/endpoint/api.py | 12 ------------ 2 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 nova/api/ec2/apirequestcontext.py diff --git a/nova/api/ec2/apirequestcontext.py b/nova/api/ec2/apirequestcontext.py new file mode 100644 index 000000000..fb3118020 --- /dev/null +++ b/nova/api/ec2/apirequestcontext.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +APIRequestContext +""" + +import random + +class APIRequestContext(object): + def __init__(self, handler, user, project): + self.handler = handler + self.user = user + self.project = project + self.request_id = ''.join( + [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-') + for x in xrange(20)] + ) diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 5a4e496a0..311fb1880 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -22,7 +22,6 @@ Most calls are proxied into the responsible controller. """ import multiprocessing -import random import re import urllib @@ -38,17 +37,6 @@ FLAGS = flags.FLAGS flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') -class APIRequestContext(object): - def __init__(self, handler, user, project): - self.handler = handler - self.user = user - self.project = project - self.request_id = ''.join( - [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-') - for x in xrange(20)] - ) - - class RootRequestHandler(tornado.web.RequestHandler): def get(self): # available api versions -- cgit From 1ef59040aa1304a4682c6bcdaa3333372e7f8629 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 30 Aug 2010 19:12:31 -0400 Subject: Delete __init__.py in prep for turning apirequesthandler into __init__ --- nova/api/ec2/__init__.py | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 nova/api/ec2/__init__.py diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py deleted file mode 100644 index 6eec0abf7..000000000 --- a/nova/api/ec2/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -WSGI middleware for EC2 API controllers. -""" - -import routes -import webob.dec - -from nova import wsgi - - -class API(wsgi.Router): - """Routes EC2 requests to the appropriate controller.""" - - def __init__(self): - mapper = routes.Mapper() - mapper.connect(None, "{all:.*}", controller=self.dummy) - super(API, self).__init__(mapper) - - @staticmethod - @webob.dec.wsgify - def dummy(req): - """Temporary dummy controller.""" - msg = "dummy response -- please hook up __init__() to cloud.py instead" - return repr({'dummy': msg, - 'kwargs': repr(req.environ['wsgiorg.routing_args'][1])}) -- cgit From c54d6c3d1fcb0210e9f52097f1a1e85550c84bf6 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 10:03:51 -0400 Subject: First steps in reworking EC2 APIRequestHandler into separate Authenticate() and Router() WSGI apps --- nova/api/__init__.py | 4 + nova/api/ec2/__init__.py | 151 ++++++++++++++++++++++++++++++++++++++ nova/api/ec2/apirequesthandler.py | 126 ------------------------------- nova/endpoint/notes.txt | 10 +-- 4 files changed, 160 insertions(+), 131 deletions(-) create mode 100644 nova/api/ec2/__init__.py delete mode 100644 nova/api/ec2/apirequesthandler.py diff --git a/nova/api/__init__.py b/nova/api/__init__.py index b9b9e3988..0166b7fc1 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -32,6 +32,10 @@ class API(wsgi.Router): def __init__(self): mapper = routes.Mapper() + # TODO(gundlach): EC2 RootController is replaced by this class; + # MetadataRequestHandlers isn't part of the EC2 API and thus can + # be dropped; and I'm leaving off CloudPipeRequestHandler until + # I hear that we need it. mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API()) mapper.connect("/ec2/{path_info:.*}", controller=ec2.API()) super(API, self).__init__(mapper) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py new file mode 100644 index 000000000..a4d9b95f9 --- /dev/null +++ b/nova/api/ec2/__init__.py @@ -0,0 +1,151 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Starting point for routing EC2 requests +""" + +import logging +import routes +import webob.exc +from webob.dec import wsgify + +from nova.api.ec2 import admin +from nova.api.ec2 import cloud +from nova import exception +from nova import utils +from nova.auth import manager + + +_log = logging.getLogger("api") +_log.setLevel(logging.DEBUG) + + +class API(wsgi.Middleware): + """Routing for all EC2 API requests.""" + + def __init__(self): + self.application = Authenticate(Router()) + +class Authenticate(wsgi.Middleware): + """Authenticates an EC2 request.""" + + @webob.dec.wsgify + def __call__(self, req): + #TODO(gundlach): where do arguments come from? + args = self.request.arguments + + # Read request signature. + try: + signature = args.pop('Signature')[0] + except: + raise webob.exc.HTTPBadRequest() + + # Make a copy of args for authentication and signature verification. + auth_params = {} + for key, value in args.items(): + auth_params[key] = value[0] + + # Get requested action and remove authentication args for final request. + try: + action = args.pop('Action')[0] + access = args.pop('AWSAccessKeyId')[0] + args.pop('SignatureMethod') + args.pop('SignatureVersion') + args.pop('Version') + args.pop('Timestamp') + except: + raise webob.exc.HTTPBadRequest() + + # Authenticate the request. + try: + (user, project) = manager.AuthManager().authenticate( + access, + signature, + auth_params, + req.method, + req.host, + req.path + ) + + except exception.Error, ex: + logging.debug("Authentication Failure: %s" % ex) + raise webob.exc.HTTPForbidden() + + _log.debug('action: %s' % action) + + for key, value in args.items(): + _log.debug('arg: %s\t\tval: %s' % (key, value)) + + # Authenticated! + req.environ['ec2.action'] = action + req.environ['ec2.context'] = APIRequestContext(user, project) + return self.application + + +class Router(wsgi.Application): + """ + Finds controller for a request, executes environ['ec2.action'] upon it, and + returns a response. + """ + def __init__(self): + self.map = routes.Mapper() + self.map.connect("/{controller_name}/") + self.controllers = dict(Cloud=cloud.CloudController(), + Admin=admin.AdminController()) + + def __call__(self, req): + # Obtain the appropriate controller for this request. + match = self.map.match(req.path) + if not match: + raise webob.exc.HTTPNotFound() + controller_name = match['controller_name'] + + try: + controller = self.controllers[controller_name] + except KeyError: + self._error('unhandled', 'no controller named %s' % controller_name) + return + + request = APIRequest(controller, req.environ['ec2.action']) + context = req.environ['ec2.context'] + try: + data = request.send(context, **args) + req.headers['Content-Type'] = 'text/xml' + return data + #TODO(gundlach) under what conditions would _error_callbock used to + #be called? What was 'failure' that you could call .raiseException + #on it? + except Exception, ex: + try: + #TODO + failure.raiseException() + except exception.ApiError as ex: + self._error(req, type(ex).__name__ + "." + ex.code, ex.message) + # TODO(vish): do something more useful with unknown exceptions + except Exception as ex: + self._error(type(ex).__name__, str(ex)) + + def _error(self, req, code, message): + req.status = 400 + req.headers['Content-Type'] = 'text/xml' + req.response = ('\n' + '%s' + '%s' + '?') % (code, message)) + diff --git a/nova/api/ec2/apirequesthandler.py b/nova/api/ec2/apirequesthandler.py deleted file mode 100644 index bbba60c02..000000000 --- a/nova/api/ec2/apirequesthandler.py +++ /dev/null @@ -1,126 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -APIRequestHandler, pulled unmodified out of nova.endpoint.api -""" - -import logging - -import tornado.web - -from nova import exception -from nova import utils -from nova.auth import manager - - -_log = logging.getLogger("api") -_log.setLevel(logging.DEBUG) - - -class APIRequestHandler(tornado.web.RequestHandler): - def get(self, controller_name): - self.execute(controller_name) - - @tornado.web.asynchronous - def execute(self, controller_name): - # Obtain the appropriate controller for this request. - try: - controller = self.application.controllers[controller_name] - except KeyError: - self._error('unhandled', 'no controller named %s' % controller_name) - return - - args = self.request.arguments - - # Read request signature. - try: - signature = args.pop('Signature')[0] - except: - raise tornado.web.HTTPError(400) - - # Make a copy of args for authentication and signature verification. - auth_params = {} - for key, value in args.items(): - auth_params[key] = value[0] - - # Get requested action and remove authentication args for final request. - try: - action = args.pop('Action')[0] - access = args.pop('AWSAccessKeyId')[0] - args.pop('SignatureMethod') - args.pop('SignatureVersion') - args.pop('Version') - args.pop('Timestamp') - except: - raise tornado.web.HTTPError(400) - - # Authenticate the request. - try: - (user, project) = manager.AuthManager().authenticate( - access, - signature, - auth_params, - self.request.method, - self.request.host, - self.request.path - ) - - except exception.Error, ex: - logging.debug("Authentication Failure: %s" % ex) - raise tornado.web.HTTPError(403) - - _log.debug('action: %s' % action) - - for key, value in args.items(): - _log.debug('arg: %s\t\tval: %s' % (key, value)) - - request = APIRequest(controller, action) - context = APIRequestContext(self, user, project) - d = request.send(context, **args) - # d.addCallback(utils.debug) - - # TODO: Wrap response in AWS XML format - d.addCallbacks(self._write_callback, self._error_callback) - - def _write_callback(self, data): - self.set_header('Content-Type', 'text/xml') - self.write(data) - self.finish() - - def _error_callback(self, failure): - try: - failure.raiseException() - except exception.ApiError as ex: - self._error(type(ex).__name__ + "." + ex.code, ex.message) - # TODO(vish): do something more useful with unknown exceptions - except Exception as ex: - self._error(type(ex).__name__, str(ex)) - raise - - def post(self, controller_name): - self.execute(controller_name) - - def _error(self, code, message): - self._status_code = 400 - self.set_header('Content-Type', 'text/xml') - self.write('\n') - self.write('%s' - '%s' - '?' % (code, message)) - self.finish() diff --git a/nova/endpoint/notes.txt b/nova/endpoint/notes.txt index c1d441de0..7a85cdc93 100644 --- a/nova/endpoint/notes.txt +++ b/nova/endpoint/notes.txt @@ -17,7 +17,9 @@ APIServerApplication(tornado.web.Application) APIRequestHandler execute: authenticates request - picks controller from APIServerApplication's list + picks controller from APIServerApplication's list based on name that was at the + start of the URL (e.g. /services/Cloud has /services mapped here via + APIServerApplication then Cloud is controller_name) picks action from incoming request arguments dict = APIRequest(controller, action).send(Context(user, project)) _write_callback(dict) @@ -49,12 +51,10 @@ CloudController and AdminController: * apiserverapplication: replace with a Router to a wsgi.Controller - root controller all goes into a "version" action - ??? dunno what cloudpipes or metadatarequesthandlers do... - apirequesthandler stuff goes into an "ec2" action + apirequesthandler stuff is just an entry in api.APIRouter * apirequesthandler - ec2() method on wsgi.Controller + wsgi.Controller pointed to by api.APIRouter - basically it's execute() from old APIRequestHandler change to return data directly instead of _write_callback() and finish() -- cgit From 070d87df264ca949b51131df9287fbcee373d480 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 10:46:01 -0400 Subject: Get rid of some convoluted exception handling that we don't need in eventlet --- nova/api/ec2/__init__.py | 23 +++++++---------------- nova/api/ec2/apirequest.py | 7 ++++--- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index a4d9b95f9..46e543d0e 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -122,24 +122,15 @@ class Router(wsgi.Application): self._error('unhandled', 'no controller named %s' % controller_name) return - request = APIRequest(controller, req.environ['ec2.action']) + api_request = APIRequest(controller, req.environ['ec2.action']) context = req.environ['ec2.context'] try: - data = request.send(context, **args) - req.headers['Content-Type'] = 'text/xml' - return data - #TODO(gundlach) under what conditions would _error_callbock used to - #be called? What was 'failure' that you could call .raiseException - #on it? - except Exception, ex: - try: - #TODO - failure.raiseException() - except exception.ApiError as ex: - self._error(req, type(ex).__name__ + "." + ex.code, ex.message) - # TODO(vish): do something more useful with unknown exceptions - except Exception as ex: - self._error(type(ex).__name__, str(ex)) + return api_request.send(context, **args) + except exception.ApiError as ex: + self._error(req, type(ex).__name__ + "." + ex.code, ex.message) + # TODO(vish): do something more useful with unknown exceptions + except Exception as ex: + self._error(type(ex).__name__, str(ex)) def _error(self, req, code, message): req.status = 400 diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index 1fc84248b..77f1a7759 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -79,9 +79,10 @@ class APIRequest(object): s.sort() args[key] = [v for k, v in s] - d = defer.maybeDeferred(method, context, **args) - d.addCallback(self._render_response, context.request_id) - return d + result = method(context, **args) + + req.headers['Content-Type'] = 'text/xml' + return self._render_response(result, context.request_id) def _render_response(self, response_data, request_id): xml = minidom.Document() -- cgit From cb55d65827170dd9d54dbd22f32e5c2171f8e1b1 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 10:55:53 -0400 Subject: small import cleanup --- nova/api/ec2/__init__.py | 5 ++--- nova/api/ec2/apirequest.py | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 46e543d0e..7e345d297 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -22,13 +22,12 @@ Starting point for routing EC2 requests import logging import routes +import webob.dec import webob.exc -from webob.dec import wsgify from nova.api.ec2 import admin from nova.api.ec2 import cloud from nova import exception -from nova import utils from nova.auth import manager @@ -101,7 +100,7 @@ class Authenticate(wsgi.Middleware): class Router(wsgi.Application): """ Finds controller for a request, executes environ['ec2.action'] upon it, and - returns a response. + returns an XML response. If the action fails, returns a 400. """ def __init__(self): self.map = routes.Mapper() diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index 77f1a7759..261346a09 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -23,8 +23,6 @@ APIRequest class # TODO(termie): replace minidom with etree from xml.dom import minidom -from twisted.internet import defer - _c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') -- cgit From ab43c28e583116c4885b19afc6448192aae10096 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 12:15:29 -0400 Subject: Move cloudcontroller and admincontroller into new api --- nova/api/ec2/admin.py | 211 ++++++++++++++ nova/api/ec2/cloud.py | 739 +++++++++++++++++++++++++++++++++++++++++++++++++ nova/endpoint/admin.py | 211 -------------- nova/endpoint/cloud.py | 739 ------------------------------------------------- 4 files changed, 950 insertions(+), 950 deletions(-) create mode 100644 nova/api/ec2/admin.py create mode 100644 nova/api/ec2/cloud.py delete mode 100644 nova/endpoint/admin.py delete mode 100644 nova/endpoint/cloud.py diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py new file mode 100644 index 000000000..d6f622755 --- /dev/null +++ b/nova/api/ec2/admin.py @@ -0,0 +1,211 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Admin API controller, exposed through http via the api worker. +""" + +import base64 + +from nova.auth import manager +from nova.compute import model + + +def user_dict(user, base64_file=None): + """Convert the user object to a result dict""" + if user: + return { + 'username': user.id, + 'accesskey': user.access, + 'secretkey': user.secret, + 'file': base64_file} + else: + return {} + + +def project_dict(project): + """Convert the project object to a result dict""" + if project: + return { + 'projectname': project.id, + 'project_manager_id': project.project_manager_id, + 'description': project.description} + else: + return {} + + +def host_dict(host): + """Convert a host model object to a result dict""" + if host: + return host.state + else: + return {} + + +def admin_only(target): + """Decorator for admin-only API calls""" + def wrapper(*args, **kwargs): + """Internal wrapper method for admin-only API calls""" + context = args[1] + if context.user.is_admin(): + return target(*args, **kwargs) + else: + return {} + + return wrapper + + +class AdminController(object): + """ + API Controller for users, hosts, nodes, and workers. + Trivial admin_only wrapper will be replaced with RBAC, + allowing project managers to administer project users. + """ + + def __str__(self): + return 'AdminController' + + @admin_only + def describe_user(self, _context, name, **_kwargs): + """Returns user data, including access and secret keys.""" + return user_dict(manager.AuthManager().get_user(name)) + + @admin_only + def describe_users(self, _context, **_kwargs): + """Returns all users - should be changed to deal with a list.""" + return {'userSet': + [user_dict(u) for u in manager.AuthManager().get_users()] } + + @admin_only + def register_user(self, _context, name, **_kwargs): + """Creates a new user, and returns generated credentials.""" + return user_dict(manager.AuthManager().create_user(name)) + + @admin_only + def deregister_user(self, _context, name, **_kwargs): + """Deletes a single user (NOT undoable.) + Should throw an exception if the user has instances, + volumes, or buckets remaining. + """ + manager.AuthManager().delete_user(name) + + return True + + @admin_only + def describe_roles(self, context, project_roles=True, **kwargs): + """Returns a list of allowed roles.""" + roles = manager.AuthManager().get_roles(project_roles) + return { 'roles': [{'role': r} for r in roles]} + + @admin_only + def describe_user_roles(self, context, user, project=None, **kwargs): + """Returns a list of roles for the given user. + Omitting project will return any global roles that the user has. + Specifying project will return only project specific roles. + """ + roles = manager.AuthManager().get_user_roles(user, project=project) + return { 'roles': [{'role': r} for r in roles]} + + @admin_only + def modify_user_role(self, context, user, role, project=None, + operation='add', **kwargs): + """Add or remove a role for a user and project.""" + if operation == 'add': + manager.AuthManager().add_role(user, role, project) + elif operation == 'remove': + manager.AuthManager().remove_role(user, role, project) + else: + raise exception.ApiError('operation must be add or remove') + + return True + + @admin_only + def generate_x509_for_user(self, _context, name, project=None, **kwargs): + """Generates and returns an x509 certificate for a single user. + Is usually called from a client that will wrap this with + access and secret key info, and return a zip file. + """ + if project is None: + project = name + project = manager.AuthManager().get_project(project) + user = manager.AuthManager().get_user(name) + return user_dict(user, base64.b64encode(project.get_credentials(user))) + + @admin_only + def describe_project(self, context, name, **kwargs): + """Returns project data, including member ids.""" + return project_dict(manager.AuthManager().get_project(name)) + + @admin_only + def describe_projects(self, context, user=None, **kwargs): + """Returns all projects - should be changed to deal with a list.""" + return {'projectSet': + [project_dict(u) for u in + manager.AuthManager().get_projects(user=user)]} + + @admin_only + def register_project(self, context, name, manager_user, description=None, + member_users=None, **kwargs): + """Creates a new project""" + return project_dict( + manager.AuthManager().create_project( + name, + manager_user, + description=None, + member_users=None)) + + @admin_only + def deregister_project(self, context, name): + """Permanently deletes a project.""" + manager.AuthManager().delete_project(name) + return True + + @admin_only + def describe_project_members(self, context, name, **kwargs): + project = manager.AuthManager().get_project(name) + result = { + 'members': [{'member': m} for m in project.member_ids]} + return result + + @admin_only + def modify_project_member(self, context, user, project, operation, **kwargs): + """Add or remove a user from a project.""" + if operation =='add': + manager.AuthManager().add_to_project(user, project) + elif operation == 'remove': + manager.AuthManager().remove_from_project(user, project) + else: + raise exception.ApiError('operation must be add or remove') + return True + + @admin_only + def describe_hosts(self, _context, **_kwargs): + """Returns status info for all nodes. Includes: + * Disk Space + * Instance List + * RAM used + * CPU used + * DHCP servers running + * Iptables / bridges + """ + return {'hostSet': [host_dict(h) for h in model.Host.all()]} + + @admin_only + def describe_host(self, _context, name, **_kwargs): + """Returns status info for single node.""" + return host_dict(model.Host.lookup(name)) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py new file mode 100644 index 000000000..30634429d --- /dev/null +++ b/nova/api/ec2/cloud.py @@ -0,0 +1,739 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Cloud Controller: Implementation of EC2 REST API calls, which are +dispatched to other nodes via AMQP RPC. State is via distributed +datastore. +""" + +import base64 +import logging +import os +import time + +from twisted.internet import defer + +from nova import datastore +from nova import exception +from nova import flags +from nova import rpc +from nova import utils +from nova.auth import rbac +from nova.auth import manager +from nova.compute import model +from nova.compute.instance_types import INSTANCE_TYPES +from nova.endpoint import images +from nova.network import service as network_service +from nova.network import model as network_model +from nova.volume import service + + +FLAGS = flags.FLAGS +flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') + + +def _gen_key(user_id, key_name): + """ Tuck this into AuthManager """ + try: + mgr = manager.AuthManager() + private_key, fingerprint = mgr.generate_key_pair(user_id, key_name) + except Exception as ex: + return {'exception': ex} + return {'private_key': private_key, 'fingerprint': fingerprint} + + +class CloudController(object): + """ CloudController provides the critical dispatch between + inbound API calls through the endpoint and messages + sent to the other nodes. +""" + def __init__(self): + self.instdir = model.InstanceDirectory() + self.setup() + + @property + def instances(self): + """ All instances in the system, as dicts """ + return self.instdir.all + + @property + def volumes(self): + """ returns a list of all volumes """ + for volume_id in datastore.Redis.instance().smembers("volumes"): + volume = service.get_volume(volume_id) + yield volume + + def __str__(self): + return 'CloudController' + + def setup(self): + """ Ensure the keychains and folders exist. """ + # Create keys folder, if it doesn't exist + if not os.path.exists(FLAGS.keys_path): + os.makedirs(FLAGS.keys_path) + # Gen root CA, if we don't have one + root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file) + if not os.path.exists(root_ca_path): + start = os.getcwd() + os.chdir(FLAGS.ca_path) + utils.runthis("Generating root CA: %s", "sh genrootca.sh") + os.chdir(start) + # TODO: Do this with M2Crypto instead + + def get_instance_by_ip(self, ip): + return self.instdir.by_ip(ip) + + def _get_mpi_data(self, project_id): + result = {} + for instance in self.instdir.all: + if instance['project_id'] == project_id: + line = '%s slots=%d' % (instance['private_dns_name'], + INSTANCE_TYPES[instance['instance_type']]['vcpus']) + if instance['key_name'] in result: + result[instance['key_name']].append(line) + else: + result[instance['key_name']] = [line] + return result + + def get_metadata(self, ipaddress): + i = self.get_instance_by_ip(ipaddress) + if i is None: + return None + mpi = self._get_mpi_data(i['project_id']) + if i['key_name']: + keys = { + '0': { + '_name': i['key_name'], + 'openssh-key': i['key_data'] + } + } + else: + keys = '' + + address_record = network_model.FixedIp(i['private_dns_name']) + if address_record: + hostname = address_record['hostname'] + else: + hostname = 'ip-%s' % i['private_dns_name'].replace('.', '-') + data = { + 'user-data': base64.b64decode(i['user_data']), + 'meta-data': { + 'ami-id': i['image_id'], + 'ami-launch-index': i['ami_launch_index'], + 'ami-manifest-path': 'FIXME', # image property + 'block-device-mapping': { # TODO: replace with real data + 'ami': 'sda1', + 'ephemeral0': 'sda2', + 'root': '/dev/sda1', + 'swap': 'sda3' + }, + 'hostname': hostname, + 'instance-action': 'none', + 'instance-id': i['instance_id'], + 'instance-type': i.get('instance_type', ''), + 'local-hostname': hostname, + 'local-ipv4': i['private_dns_name'], # TODO: switch to IP + 'kernel-id': i.get('kernel_id', ''), + 'placement': { + 'availaibility-zone': i.get('availability_zone', 'nova'), + }, + 'public-hostname': hostname, + 'public-ipv4': i.get('dns_name', ''), # TODO: switch to IP + 'public-keys': keys, + 'ramdisk-id': i.get('ramdisk_id', ''), + 'reservation-id': i['reservation_id'], + 'security-groups': i.get('groups', ''), + 'mpi': mpi + } + } + if False: # TODO: store ancestor ids + data['ancestor-ami-ids'] = [] + if i.get('product_codes', None): + data['product-codes'] = i['product_codes'] + return data + + @rbac.allow('all') + def describe_availability_zones(self, context, **kwargs): + return {'availabilityZoneInfo': [{'zoneName': 'nova', + 'zoneState': 'available'}]} + + @rbac.allow('all') + def describe_regions(self, context, region_name=None, **kwargs): + # TODO(vish): region_name is an array. Support filtering + return {'regionInfo': [{'regionName': 'nova', + 'regionUrl': FLAGS.ec2_url}]} + + @rbac.allow('all') + def describe_snapshots(self, + context, + snapshot_id=None, + owner=None, + restorable_by=None, + **kwargs): + return {'snapshotSet': [{'snapshotId': 'fixme', + 'volumeId': 'fixme', + 'status': 'fixme', + 'startTime': 'fixme', + 'progress': 'fixme', + 'ownerId': 'fixme', + 'volumeSize': 0, + 'description': 'fixme'}]} + + @rbac.allow('all') + def describe_key_pairs(self, context, key_name=None, **kwargs): + key_pairs = context.user.get_key_pairs() + if not key_name is None: + key_pairs = [x for x in key_pairs if x.name in key_name] + + result = [] + for key_pair in key_pairs: + # filter out the vpn keys + suffix = FLAGS.vpn_key_suffix + if context.user.is_admin() or not key_pair.name.endswith(suffix): + result.append({ + 'keyName': key_pair.name, + 'keyFingerprint': key_pair.fingerprint, + }) + + return {'keypairsSet': result} + + @rbac.allow('all') + def create_key_pair(self, context, key_name, **kwargs): + dcall = defer.Deferred() + pool = context.handler.application.settings.get('pool') + def _complete(kwargs): + if 'exception' in kwargs: + dcall.errback(kwargs['exception']) + return + dcall.callback({'keyName': key_name, + 'keyFingerprint': kwargs['fingerprint'], + 'keyMaterial': kwargs['private_key']}) + pool.apply_async(_gen_key, [context.user.id, key_name], + callback=_complete) + return dcall + + @rbac.allow('all') + def delete_key_pair(self, context, key_name, **kwargs): + context.user.delete_key_pair(key_name) + # aws returns true even if the key doens't exist + return True + + @rbac.allow('all') + def describe_security_groups(self, context, group_names, **kwargs): + groups = {'securityGroupSet': []} + + # Stubbed for now to unblock other things. + return groups + + @rbac.allow('netadmin') + def create_security_group(self, context, group_name, **kwargs): + return True + + @rbac.allow('netadmin') + def delete_security_group(self, context, group_name, **kwargs): + return True + + @rbac.allow('projectmanager', 'sysadmin') + def get_console_output(self, context, instance_id, **kwargs): + # instance_id is passed in as a list of instances + instance = self._get_instance(context, instance_id[0]) + return rpc.call('%s.%s' % (FLAGS.compute_topic, instance['node_name']), + {"method": "get_console_output", + "args": {"instance_id": instance_id[0]}}) + + def _get_user_id(self, context): + if context and context.user: + return context.user.id + else: + return None + + @rbac.allow('projectmanager', 'sysadmin') + def describe_volumes(self, context, **kwargs): + volumes = [] + for volume in self.volumes: + if context.user.is_admin() or volume['project_id'] == context.project.id: + v = self.format_volume(context, volume) + volumes.append(v) + return defer.succeed({'volumeSet': volumes}) + + def format_volume(self, context, volume): + v = {} + v['volumeId'] = volume['volume_id'] + v['status'] = volume['status'] + v['size'] = volume['size'] + v['availabilityZone'] = volume['availability_zone'] + v['createTime'] = volume['create_time'] + if context.user.is_admin(): + v['status'] = '%s (%s, %s, %s, %s)' % ( + volume.get('status', None), + volume.get('user_id', None), + volume.get('node_name', None), + volume.get('instance_id', ''), + volume.get('mountpoint', '')) + if volume['attach_status'] == 'attached': + v['attachmentSet'] = [{'attachTime': volume['attach_time'], + 'deleteOnTermination': volume['delete_on_termination'], + 'device': volume['mountpoint'], + 'instanceId': volume['instance_id'], + 'status': 'attached', + 'volume_id': volume['volume_id']}] + else: + v['attachmentSet'] = [{}] + return v + + @rbac.allow('projectmanager', 'sysadmin') + @defer.inlineCallbacks + def create_volume(self, context, size, **kwargs): + # TODO(vish): refactor this to create the volume object here and tell service to create it + result = yield rpc.call(FLAGS.volume_topic, {"method": "create_volume", + "args": {"size": size, + "user_id": context.user.id, + "project_id": context.project.id}}) + # NOTE(vish): rpc returned value is in the result key in the dictionary + volume = self._get_volume(context, result) + defer.returnValue({'volumeSet': [self.format_volume(context, volume)]}) + + def _get_address(self, context, public_ip): + # FIXME(vish) this should move into network.py + address = network_model.ElasticIp.lookup(public_ip) + if address and (context.user.is_admin() or address['project_id'] == context.project.id): + return address + raise exception.NotFound("Address at ip %s not found" % public_ip) + + def _get_image(self, context, image_id): + """passes in context because + objectstore does its own authorization""" + result = images.list(context, [image_id]) + if not result: + raise exception.NotFound('Image %s could not be found' % image_id) + image = result[0] + return image + + def _get_instance(self, context, instance_id): + for instance in self.instdir.all: + if instance['instance_id'] == instance_id: + if context.user.is_admin() or instance['project_id'] == context.project.id: + return instance + raise exception.NotFound('Instance %s could not be found' % instance_id) + + def _get_volume(self, context, volume_id): + volume = service.get_volume(volume_id) + if context.user.is_admin() or volume['project_id'] == context.project.id: + return volume + raise exception.NotFound('Volume %s could not be found' % volume_id) + + @rbac.allow('projectmanager', 'sysadmin') + def attach_volume(self, context, volume_id, instance_id, device, **kwargs): + volume = self._get_volume(context, volume_id) + if volume['status'] == "attached": + raise exception.ApiError("Volume is already attached") + # TODO(vish): looping through all volumes is slow. We should probably maintain an index + for vol in self.volumes: + if vol['instance_id'] == instance_id and vol['mountpoint'] == device: + raise exception.ApiError("Volume %s is already attached to %s" % (vol['volume_id'], vol['mountpoint'])) + volume.start_attach(instance_id, device) + instance = self._get_instance(context, instance_id) + compute_node = instance['node_name'] + rpc.cast('%s.%s' % (FLAGS.compute_topic, compute_node), + {"method": "attach_volume", + "args": {"volume_id": volume_id, + "instance_id": instance_id, + "mountpoint": device}}) + return defer.succeed({'attachTime': volume['attach_time'], + 'device': volume['mountpoint'], + 'instanceId': instance_id, + 'requestId': context.request_id, + 'status': volume['attach_status'], + 'volumeId': volume_id}) + + @rbac.allow('projectmanager', 'sysadmin') + def detach_volume(self, context, volume_id, **kwargs): + volume = self._get_volume(context, volume_id) + instance_id = volume.get('instance_id', None) + if not instance_id: + raise exception.Error("Volume isn't attached to anything!") + if volume['status'] == "available": + raise exception.Error("Volume is already detached") + try: + volume.start_detach() + instance = self._get_instance(context, instance_id) + rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), + {"method": "detach_volume", + "args": {"instance_id": instance_id, + "volume_id": volume_id}}) + except exception.NotFound: + # If the instance doesn't exist anymore, + # then we need to call detach blind + volume.finish_detach() + return defer.succeed({'attachTime': volume['attach_time'], + 'device': volume['mountpoint'], + 'instanceId': instance_id, + 'requestId': context.request_id, + 'status': volume['attach_status'], + 'volumeId': volume_id}) + + def _convert_to_set(self, lst, label): + if lst == None or lst == []: + return None + if not isinstance(lst, list): + lst = [lst] + return [{label: x} for x in lst] + + @rbac.allow('all') + def describe_instances(self, context, **kwargs): + return defer.succeed(self._format_describe_instances(context)) + + def _format_describe_instances(self, context): + return { 'reservationSet': self._format_instances(context) } + + def _format_run_instances(self, context, reservation_id): + i = self._format_instances(context, reservation_id) + assert len(i) == 1 + return i[0] + + def _format_instances(self, context, reservation_id = None): + reservations = {} + if context.user.is_admin(): + instgenerator = self.instdir.all + else: + instgenerator = self.instdir.by_project(context.project.id) + for instance in instgenerator: + res_id = instance.get('reservation_id', 'Unknown') + if reservation_id != None and reservation_id != res_id: + continue + if not context.user.is_admin(): + if instance['image_id'] == FLAGS.vpn_image_id: + continue + i = {} + i['instance_id'] = instance.get('instance_id', None) + i['image_id'] = instance.get('image_id', None) + i['instance_state'] = { + 'code': instance.get('state', 0), + 'name': instance.get('state_description', 'pending') + } + i['public_dns_name'] = network_model.get_public_ip_for_instance( + i['instance_id']) + i['private_dns_name'] = instance.get('private_dns_name', None) + if not i['public_dns_name']: + i['public_dns_name'] = i['private_dns_name'] + i['dns_name'] = instance.get('dns_name', None) + i['key_name'] = instance.get('key_name', None) + if context.user.is_admin(): + i['key_name'] = '%s (%s, %s)' % (i['key_name'], + instance.get('project_id', None), + instance.get('node_name', '')) + i['product_codes_set'] = self._convert_to_set( + instance.get('product_codes', None), 'product_code') + i['instance_type'] = instance.get('instance_type', None) + i['launch_time'] = instance.get('launch_time', None) + i['ami_launch_index'] = instance.get('ami_launch_index', + None) + if not reservations.has_key(res_id): + r = {} + r['reservation_id'] = res_id + r['owner_id'] = instance.get('project_id', None) + r['group_set'] = self._convert_to_set( + instance.get('groups', None), 'group_id') + r['instances_set'] = [] + reservations[res_id] = r + reservations[res_id]['instances_set'].append(i) + + return list(reservations.values()) + + @rbac.allow('all') + def describe_addresses(self, context, **kwargs): + return self.format_addresses(context) + + def format_addresses(self, context): + addresses = [] + for address in network_model.ElasticIp.all(): + # TODO(vish): implement a by_project iterator for addresses + if (context.user.is_admin() or + address['project_id'] == context.project.id): + address_rv = { + 'public_ip': address['address'], + 'instance_id': address.get('instance_id', 'free') + } + if context.user.is_admin(): + address_rv['instance_id'] = "%s (%s, %s)" % ( + address['instance_id'], + address['user_id'], + address['project_id'], + ) + addresses.append(address_rv) + return {'addressesSet': addresses} + + @rbac.allow('netadmin') + @defer.inlineCallbacks + def allocate_address(self, context, **kwargs): + network_topic = yield self._get_network_topic(context) + public_ip = yield rpc.call(network_topic, + {"method": "allocate_elastic_ip", + "args": {"user_id": context.user.id, + "project_id": context.project.id}}) + defer.returnValue({'addressSet': [{'publicIp': public_ip}]}) + + @rbac.allow('netadmin') + @defer.inlineCallbacks + def release_address(self, context, public_ip, **kwargs): + # NOTE(vish): Should we make sure this works? + network_topic = yield self._get_network_topic(context) + rpc.cast(network_topic, + {"method": "deallocate_elastic_ip", + "args": {"elastic_ip": public_ip}}) + defer.returnValue({'releaseResponse': ["Address released."]}) + + @rbac.allow('netadmin') + @defer.inlineCallbacks + def associate_address(self, context, instance_id, public_ip, **kwargs): + instance = self._get_instance(context, instance_id) + address = self._get_address(context, public_ip) + network_topic = yield self._get_network_topic(context) + rpc.cast(network_topic, + {"method": "associate_elastic_ip", + "args": {"elastic_ip": address['address'], + "fixed_ip": instance['private_dns_name'], + "instance_id": instance['instance_id']}}) + defer.returnValue({'associateResponse': ["Address associated."]}) + + @rbac.allow('netadmin') + @defer.inlineCallbacks + def disassociate_address(self, context, public_ip, **kwargs): + address = self._get_address(context, public_ip) + network_topic = yield self._get_network_topic(context) + rpc.cast(network_topic, + {"method": "disassociate_elastic_ip", + "args": {"elastic_ip": address['address']}}) + defer.returnValue({'disassociateResponse': ["Address disassociated."]}) + + @defer.inlineCallbacks + def _get_network_topic(self, context): + """Retrieves the network host for a project""" + host = network_service.get_host_for_project(context.project.id) + if not host: + host = yield rpc.call(FLAGS.network_topic, + {"method": "set_network_host", + "args": {"user_id": context.user.id, + "project_id": context.project.id}}) + defer.returnValue('%s.%s' %(FLAGS.network_topic, host)) + + @rbac.allow('projectmanager', 'sysadmin') + @defer.inlineCallbacks + def run_instances(self, context, **kwargs): + # make sure user can access the image + # vpn image is private so it doesn't show up on lists + if kwargs['image_id'] != FLAGS.vpn_image_id: + image = self._get_image(context, kwargs['image_id']) + + # FIXME(ja): if image is cloudpipe, this breaks + + # get defaults from imagestore + image_id = image['imageId'] + kernel_id = image.get('kernelId', FLAGS.default_kernel) + ramdisk_id = image.get('ramdiskId', FLAGS.default_ramdisk) + + # API parameters overrides of defaults + kernel_id = kwargs.get('kernel_id', kernel_id) + ramdisk_id = kwargs.get('ramdisk_id', ramdisk_id) + + # make sure we have access to kernel and ramdisk + self._get_image(context, kernel_id) + self._get_image(context, ramdisk_id) + + logging.debug("Going to run instances...") + reservation_id = utils.generate_uid('r') + launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) + key_data = None + if kwargs.has_key('key_name'): + key_pair = context.user.get_key_pair(kwargs['key_name']) + if not key_pair: + raise exception.ApiError('Key Pair %s not found' % + kwargs['key_name']) + key_data = key_pair.public_key + network_topic = yield self._get_network_topic(context) + # TODO: Get the real security group of launch in here + security_group = "default" + for num in range(int(kwargs['max_count'])): + is_vpn = False + if image_id == FLAGS.vpn_image_id: + is_vpn = True + inst = self.instdir.new() + allocate_data = yield rpc.call(network_topic, + {"method": "allocate_fixed_ip", + "args": {"user_id": context.user.id, + "project_id": context.project.id, + "security_group": security_group, + "is_vpn": is_vpn, + "hostname": inst.instance_id}}) + inst['image_id'] = image_id + inst['kernel_id'] = kernel_id + inst['ramdisk_id'] = ramdisk_id + inst['user_data'] = kwargs.get('user_data', '') + inst['instance_type'] = kwargs.get('instance_type', 'm1.small') + inst['reservation_id'] = reservation_id + inst['launch_time'] = launch_time + inst['key_data'] = key_data or '' + inst['key_name'] = kwargs.get('key_name', '') + inst['user_id'] = context.user.id + inst['project_id'] = context.project.id + inst['ami_launch_index'] = num + inst['security_group'] = security_group + inst['hostname'] = inst.instance_id + for (key, value) in allocate_data.iteritems(): + inst[key] = value + + inst.save() + rpc.cast(FLAGS.compute_topic, + {"method": "run_instance", + "args": {"instance_id": inst.instance_id}}) + logging.debug("Casting to node for %s's instance with IP of %s" % + (context.user.name, inst['private_dns_name'])) + # TODO: Make Network figure out the network name from ip. + defer.returnValue(self._format_run_instances(context, reservation_id)) + + @rbac.allow('projectmanager', 'sysadmin') + @defer.inlineCallbacks + def terminate_instances(self, context, instance_id, **kwargs): + logging.debug("Going to start terminating instances") + network_topic = yield self._get_network_topic(context) + for i in instance_id: + logging.debug("Going to try and terminate %s" % i) + try: + instance = self._get_instance(context, i) + except exception.NotFound: + logging.warning("Instance %s was not found during terminate" + % i) + continue + elastic_ip = network_model.get_public_ip_for_instance(i) + if elastic_ip: + logging.debug("Disassociating address %s" % elastic_ip) + # NOTE(vish): Right now we don't really care if the ip is + # disassociated. We may need to worry about + # checking this later. Perhaps in the scheduler? + rpc.cast(network_topic, + {"method": "disassociate_elastic_ip", + "args": {"elastic_ip": elastic_ip}}) + + fixed_ip = instance.get('private_dns_name', None) + if fixed_ip: + logging.debug("Deallocating address %s" % fixed_ip) + # NOTE(vish): Right now we don't really care if the ip is + # actually removed. We may need to worry about + # checking this later. Perhaps in the scheduler? + rpc.cast(network_topic, + {"method": "deallocate_fixed_ip", + "args": {"fixed_ip": fixed_ip}}) + + if instance.get('node_name', 'unassigned') != 'unassigned': + # NOTE(joshua?): It's also internal default + rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), + {"method": "terminate_instance", + "args": {"instance_id": i}}) + else: + instance.destroy() + defer.returnValue(True) + + @rbac.allow('projectmanager', 'sysadmin') + def reboot_instances(self, context, instance_id, **kwargs): + """instance_id is a list of instance ids""" + for i in instance_id: + instance = self._get_instance(context, i) + rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), + {"method": "reboot_instance", + "args": {"instance_id": i}}) + return defer.succeed(True) + + @rbac.allow('projectmanager', 'sysadmin') + def delete_volume(self, context, volume_id, **kwargs): + # TODO: return error if not authorized + volume = self._get_volume(context, volume_id) + volume_node = volume['node_name'] + rpc.cast('%s.%s' % (FLAGS.volume_topic, volume_node), + {"method": "delete_volume", + "args": {"volume_id": volume_id}}) + return defer.succeed(True) + + @rbac.allow('all') + def describe_images(self, context, image_id=None, **kwargs): + # The objectstore does its own authorization for describe + imageSet = images.list(context, image_id) + return defer.succeed({'imagesSet': imageSet}) + + @rbac.allow('projectmanager', 'sysadmin') + def deregister_image(self, context, image_id, **kwargs): + # FIXME: should the objectstore be doing these authorization checks? + images.deregister(context, image_id) + return defer.succeed({'imageId': image_id}) + + @rbac.allow('projectmanager', 'sysadmin') + def register_image(self, context, image_location=None, **kwargs): + # FIXME: should the objectstore be doing these authorization checks? + if image_location is None and kwargs.has_key('name'): + image_location = kwargs['name'] + image_id = images.register(context, image_location) + logging.debug("Registered %s as %s" % (image_location, image_id)) + + return defer.succeed({'imageId': image_id}) + + @rbac.allow('all') + def describe_image_attribute(self, context, image_id, attribute, **kwargs): + if attribute != 'launchPermission': + raise exception.ApiError('attribute not supported: %s' % attribute) + try: + image = images.list(context, image_id)[0] + except IndexError: + raise exception.ApiError('invalid id: %s' % image_id) + result = {'image_id': image_id, 'launchPermission': []} + if image['isPublic']: + result['launchPermission'].append({'group': 'all'}) + return defer.succeed(result) + + @rbac.allow('projectmanager', 'sysadmin') + def modify_image_attribute(self, context, image_id, attribute, operation_type, **kwargs): + # TODO(devcamcar): Support users and groups other than 'all'. + if attribute != 'launchPermission': + raise exception.ApiError('attribute not supported: %s' % attribute) + if not 'user_group' in kwargs: + raise exception.ApiError('user or group not specified') + if len(kwargs['user_group']) != 1 and kwargs['user_group'][0] != 'all': + raise exception.ApiError('only group "all" is supported') + if not operation_type in ['add', 'remove']: + raise exception.ApiError('operation_type must be add or remove') + result = images.modify(context, image_id, operation_type) + return defer.succeed(result) + + def update_state(self, topic, value): + """ accepts status reports from the queue and consolidates them """ + # TODO(jmc): if an instance has disappeared from + # the node, call instance_death + if topic == "instances": + return defer.succeed(True) + aggregate_state = getattr(self, topic) + node_name = value.keys()[0] + items = value[node_name] + + logging.debug("Updating %s state for %s" % (topic, node_name)) + + for item_id in items.keys(): + if (aggregate_state.has_key('pending') and + aggregate_state['pending'].has_key(item_id)): + del aggregate_state['pending'][item_id] + aggregate_state[node_name] = items + + return defer.succeed(True) diff --git a/nova/endpoint/admin.py b/nova/endpoint/admin.py deleted file mode 100644 index d6f622755..000000000 --- a/nova/endpoint/admin.py +++ /dev/null @@ -1,211 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Admin API controller, exposed through http via the api worker. -""" - -import base64 - -from nova.auth import manager -from nova.compute import model - - -def user_dict(user, base64_file=None): - """Convert the user object to a result dict""" - if user: - return { - 'username': user.id, - 'accesskey': user.access, - 'secretkey': user.secret, - 'file': base64_file} - else: - return {} - - -def project_dict(project): - """Convert the project object to a result dict""" - if project: - return { - 'projectname': project.id, - 'project_manager_id': project.project_manager_id, - 'description': project.description} - else: - return {} - - -def host_dict(host): - """Convert a host model object to a result dict""" - if host: - return host.state - else: - return {} - - -def admin_only(target): - """Decorator for admin-only API calls""" - def wrapper(*args, **kwargs): - """Internal wrapper method for admin-only API calls""" - context = args[1] - if context.user.is_admin(): - return target(*args, **kwargs) - else: - return {} - - return wrapper - - -class AdminController(object): - """ - API Controller for users, hosts, nodes, and workers. - Trivial admin_only wrapper will be replaced with RBAC, - allowing project managers to administer project users. - """ - - def __str__(self): - return 'AdminController' - - @admin_only - def describe_user(self, _context, name, **_kwargs): - """Returns user data, including access and secret keys.""" - return user_dict(manager.AuthManager().get_user(name)) - - @admin_only - def describe_users(self, _context, **_kwargs): - """Returns all users - should be changed to deal with a list.""" - return {'userSet': - [user_dict(u) for u in manager.AuthManager().get_users()] } - - @admin_only - def register_user(self, _context, name, **_kwargs): - """Creates a new user, and returns generated credentials.""" - return user_dict(manager.AuthManager().create_user(name)) - - @admin_only - def deregister_user(self, _context, name, **_kwargs): - """Deletes a single user (NOT undoable.) - Should throw an exception if the user has instances, - volumes, or buckets remaining. - """ - manager.AuthManager().delete_user(name) - - return True - - @admin_only - def describe_roles(self, context, project_roles=True, **kwargs): - """Returns a list of allowed roles.""" - roles = manager.AuthManager().get_roles(project_roles) - return { 'roles': [{'role': r} for r in roles]} - - @admin_only - def describe_user_roles(self, context, user, project=None, **kwargs): - """Returns a list of roles for the given user. - Omitting project will return any global roles that the user has. - Specifying project will return only project specific roles. - """ - roles = manager.AuthManager().get_user_roles(user, project=project) - return { 'roles': [{'role': r} for r in roles]} - - @admin_only - def modify_user_role(self, context, user, role, project=None, - operation='add', **kwargs): - """Add or remove a role for a user and project.""" - if operation == 'add': - manager.AuthManager().add_role(user, role, project) - elif operation == 'remove': - manager.AuthManager().remove_role(user, role, project) - else: - raise exception.ApiError('operation must be add or remove') - - return True - - @admin_only - def generate_x509_for_user(self, _context, name, project=None, **kwargs): - """Generates and returns an x509 certificate for a single user. - Is usually called from a client that will wrap this with - access and secret key info, and return a zip file. - """ - if project is None: - project = name - project = manager.AuthManager().get_project(project) - user = manager.AuthManager().get_user(name) - return user_dict(user, base64.b64encode(project.get_credentials(user))) - - @admin_only - def describe_project(self, context, name, **kwargs): - """Returns project data, including member ids.""" - return project_dict(manager.AuthManager().get_project(name)) - - @admin_only - def describe_projects(self, context, user=None, **kwargs): - """Returns all projects - should be changed to deal with a list.""" - return {'projectSet': - [project_dict(u) for u in - manager.AuthManager().get_projects(user=user)]} - - @admin_only - def register_project(self, context, name, manager_user, description=None, - member_users=None, **kwargs): - """Creates a new project""" - return project_dict( - manager.AuthManager().create_project( - name, - manager_user, - description=None, - member_users=None)) - - @admin_only - def deregister_project(self, context, name): - """Permanently deletes a project.""" - manager.AuthManager().delete_project(name) - return True - - @admin_only - def describe_project_members(self, context, name, **kwargs): - project = manager.AuthManager().get_project(name) - result = { - 'members': [{'member': m} for m in project.member_ids]} - return result - - @admin_only - def modify_project_member(self, context, user, project, operation, **kwargs): - """Add or remove a user from a project.""" - if operation =='add': - manager.AuthManager().add_to_project(user, project) - elif operation == 'remove': - manager.AuthManager().remove_from_project(user, project) - else: - raise exception.ApiError('operation must be add or remove') - return True - - @admin_only - def describe_hosts(self, _context, **_kwargs): - """Returns status info for all nodes. Includes: - * Disk Space - * Instance List - * RAM used - * CPU used - * DHCP servers running - * Iptables / bridges - """ - return {'hostSet': [host_dict(h) for h in model.Host.all()]} - - @admin_only - def describe_host(self, _context, name, **_kwargs): - """Returns status info for single node.""" - return host_dict(model.Host.lookup(name)) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py deleted file mode 100644 index 30634429d..000000000 --- a/nova/endpoint/cloud.py +++ /dev/null @@ -1,739 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Cloud Controller: Implementation of EC2 REST API calls, which are -dispatched to other nodes via AMQP RPC. State is via distributed -datastore. -""" - -import base64 -import logging -import os -import time - -from twisted.internet import defer - -from nova import datastore -from nova import exception -from nova import flags -from nova import rpc -from nova import utils -from nova.auth import rbac -from nova.auth import manager -from nova.compute import model -from nova.compute.instance_types import INSTANCE_TYPES -from nova.endpoint import images -from nova.network import service as network_service -from nova.network import model as network_model -from nova.volume import service - - -FLAGS = flags.FLAGS -flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') - - -def _gen_key(user_id, key_name): - """ Tuck this into AuthManager """ - try: - mgr = manager.AuthManager() - private_key, fingerprint = mgr.generate_key_pair(user_id, key_name) - except Exception as ex: - return {'exception': ex} - return {'private_key': private_key, 'fingerprint': fingerprint} - - -class CloudController(object): - """ CloudController provides the critical dispatch between - inbound API calls through the endpoint and messages - sent to the other nodes. -""" - def __init__(self): - self.instdir = model.InstanceDirectory() - self.setup() - - @property - def instances(self): - """ All instances in the system, as dicts """ - return self.instdir.all - - @property - def volumes(self): - """ returns a list of all volumes """ - for volume_id in datastore.Redis.instance().smembers("volumes"): - volume = service.get_volume(volume_id) - yield volume - - def __str__(self): - return 'CloudController' - - def setup(self): - """ Ensure the keychains and folders exist. """ - # Create keys folder, if it doesn't exist - if not os.path.exists(FLAGS.keys_path): - os.makedirs(FLAGS.keys_path) - # Gen root CA, if we don't have one - root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file) - if not os.path.exists(root_ca_path): - start = os.getcwd() - os.chdir(FLAGS.ca_path) - utils.runthis("Generating root CA: %s", "sh genrootca.sh") - os.chdir(start) - # TODO: Do this with M2Crypto instead - - def get_instance_by_ip(self, ip): - return self.instdir.by_ip(ip) - - def _get_mpi_data(self, project_id): - result = {} - for instance in self.instdir.all: - if instance['project_id'] == project_id: - line = '%s slots=%d' % (instance['private_dns_name'], - INSTANCE_TYPES[instance['instance_type']]['vcpus']) - if instance['key_name'] in result: - result[instance['key_name']].append(line) - else: - result[instance['key_name']] = [line] - return result - - def get_metadata(self, ipaddress): - i = self.get_instance_by_ip(ipaddress) - if i is None: - return None - mpi = self._get_mpi_data(i['project_id']) - if i['key_name']: - keys = { - '0': { - '_name': i['key_name'], - 'openssh-key': i['key_data'] - } - } - else: - keys = '' - - address_record = network_model.FixedIp(i['private_dns_name']) - if address_record: - hostname = address_record['hostname'] - else: - hostname = 'ip-%s' % i['private_dns_name'].replace('.', '-') - data = { - 'user-data': base64.b64decode(i['user_data']), - 'meta-data': { - 'ami-id': i['image_id'], - 'ami-launch-index': i['ami_launch_index'], - 'ami-manifest-path': 'FIXME', # image property - 'block-device-mapping': { # TODO: replace with real data - 'ami': 'sda1', - 'ephemeral0': 'sda2', - 'root': '/dev/sda1', - 'swap': 'sda3' - }, - 'hostname': hostname, - 'instance-action': 'none', - 'instance-id': i['instance_id'], - 'instance-type': i.get('instance_type', ''), - 'local-hostname': hostname, - 'local-ipv4': i['private_dns_name'], # TODO: switch to IP - 'kernel-id': i.get('kernel_id', ''), - 'placement': { - 'availaibility-zone': i.get('availability_zone', 'nova'), - }, - 'public-hostname': hostname, - 'public-ipv4': i.get('dns_name', ''), # TODO: switch to IP - 'public-keys': keys, - 'ramdisk-id': i.get('ramdisk_id', ''), - 'reservation-id': i['reservation_id'], - 'security-groups': i.get('groups', ''), - 'mpi': mpi - } - } - if False: # TODO: store ancestor ids - data['ancestor-ami-ids'] = [] - if i.get('product_codes', None): - data['product-codes'] = i['product_codes'] - return data - - @rbac.allow('all') - def describe_availability_zones(self, context, **kwargs): - return {'availabilityZoneInfo': [{'zoneName': 'nova', - 'zoneState': 'available'}]} - - @rbac.allow('all') - def describe_regions(self, context, region_name=None, **kwargs): - # TODO(vish): region_name is an array. Support filtering - return {'regionInfo': [{'regionName': 'nova', - 'regionUrl': FLAGS.ec2_url}]} - - @rbac.allow('all') - def describe_snapshots(self, - context, - snapshot_id=None, - owner=None, - restorable_by=None, - **kwargs): - return {'snapshotSet': [{'snapshotId': 'fixme', - 'volumeId': 'fixme', - 'status': 'fixme', - 'startTime': 'fixme', - 'progress': 'fixme', - 'ownerId': 'fixme', - 'volumeSize': 0, - 'description': 'fixme'}]} - - @rbac.allow('all') - def describe_key_pairs(self, context, key_name=None, **kwargs): - key_pairs = context.user.get_key_pairs() - if not key_name is None: - key_pairs = [x for x in key_pairs if x.name in key_name] - - result = [] - for key_pair in key_pairs: - # filter out the vpn keys - suffix = FLAGS.vpn_key_suffix - if context.user.is_admin() or not key_pair.name.endswith(suffix): - result.append({ - 'keyName': key_pair.name, - 'keyFingerprint': key_pair.fingerprint, - }) - - return {'keypairsSet': result} - - @rbac.allow('all') - def create_key_pair(self, context, key_name, **kwargs): - dcall = defer.Deferred() - pool = context.handler.application.settings.get('pool') - def _complete(kwargs): - if 'exception' in kwargs: - dcall.errback(kwargs['exception']) - return - dcall.callback({'keyName': key_name, - 'keyFingerprint': kwargs['fingerprint'], - 'keyMaterial': kwargs['private_key']}) - pool.apply_async(_gen_key, [context.user.id, key_name], - callback=_complete) - return dcall - - @rbac.allow('all') - def delete_key_pair(self, context, key_name, **kwargs): - context.user.delete_key_pair(key_name) - # aws returns true even if the key doens't exist - return True - - @rbac.allow('all') - def describe_security_groups(self, context, group_names, **kwargs): - groups = {'securityGroupSet': []} - - # Stubbed for now to unblock other things. - return groups - - @rbac.allow('netadmin') - def create_security_group(self, context, group_name, **kwargs): - return True - - @rbac.allow('netadmin') - def delete_security_group(self, context, group_name, **kwargs): - return True - - @rbac.allow('projectmanager', 'sysadmin') - def get_console_output(self, context, instance_id, **kwargs): - # instance_id is passed in as a list of instances - instance = self._get_instance(context, instance_id[0]) - return rpc.call('%s.%s' % (FLAGS.compute_topic, instance['node_name']), - {"method": "get_console_output", - "args": {"instance_id": instance_id[0]}}) - - def _get_user_id(self, context): - if context and context.user: - return context.user.id - else: - return None - - @rbac.allow('projectmanager', 'sysadmin') - def describe_volumes(self, context, **kwargs): - volumes = [] - for volume in self.volumes: - if context.user.is_admin() or volume['project_id'] == context.project.id: - v = self.format_volume(context, volume) - volumes.append(v) - return defer.succeed({'volumeSet': volumes}) - - def format_volume(self, context, volume): - v = {} - v['volumeId'] = volume['volume_id'] - v['status'] = volume['status'] - v['size'] = volume['size'] - v['availabilityZone'] = volume['availability_zone'] - v['createTime'] = volume['create_time'] - if context.user.is_admin(): - v['status'] = '%s (%s, %s, %s, %s)' % ( - volume.get('status', None), - volume.get('user_id', None), - volume.get('node_name', None), - volume.get('instance_id', ''), - volume.get('mountpoint', '')) - if volume['attach_status'] == 'attached': - v['attachmentSet'] = [{'attachTime': volume['attach_time'], - 'deleteOnTermination': volume['delete_on_termination'], - 'device': volume['mountpoint'], - 'instanceId': volume['instance_id'], - 'status': 'attached', - 'volume_id': volume['volume_id']}] - else: - v['attachmentSet'] = [{}] - return v - - @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks - def create_volume(self, context, size, **kwargs): - # TODO(vish): refactor this to create the volume object here and tell service to create it - result = yield rpc.call(FLAGS.volume_topic, {"method": "create_volume", - "args": {"size": size, - "user_id": context.user.id, - "project_id": context.project.id}}) - # NOTE(vish): rpc returned value is in the result key in the dictionary - volume = self._get_volume(context, result) - defer.returnValue({'volumeSet': [self.format_volume(context, volume)]}) - - def _get_address(self, context, public_ip): - # FIXME(vish) this should move into network.py - address = network_model.ElasticIp.lookup(public_ip) - if address and (context.user.is_admin() or address['project_id'] == context.project.id): - return address - raise exception.NotFound("Address at ip %s not found" % public_ip) - - def _get_image(self, context, image_id): - """passes in context because - objectstore does its own authorization""" - result = images.list(context, [image_id]) - if not result: - raise exception.NotFound('Image %s could not be found' % image_id) - image = result[0] - return image - - def _get_instance(self, context, instance_id): - for instance in self.instdir.all: - if instance['instance_id'] == instance_id: - if context.user.is_admin() or instance['project_id'] == context.project.id: - return instance - raise exception.NotFound('Instance %s could not be found' % instance_id) - - def _get_volume(self, context, volume_id): - volume = service.get_volume(volume_id) - if context.user.is_admin() or volume['project_id'] == context.project.id: - return volume - raise exception.NotFound('Volume %s could not be found' % volume_id) - - @rbac.allow('projectmanager', 'sysadmin') - def attach_volume(self, context, volume_id, instance_id, device, **kwargs): - volume = self._get_volume(context, volume_id) - if volume['status'] == "attached": - raise exception.ApiError("Volume is already attached") - # TODO(vish): looping through all volumes is slow. We should probably maintain an index - for vol in self.volumes: - if vol['instance_id'] == instance_id and vol['mountpoint'] == device: - raise exception.ApiError("Volume %s is already attached to %s" % (vol['volume_id'], vol['mountpoint'])) - volume.start_attach(instance_id, device) - instance = self._get_instance(context, instance_id) - compute_node = instance['node_name'] - rpc.cast('%s.%s' % (FLAGS.compute_topic, compute_node), - {"method": "attach_volume", - "args": {"volume_id": volume_id, - "instance_id": instance_id, - "mountpoint": device}}) - return defer.succeed({'attachTime': volume['attach_time'], - 'device': volume['mountpoint'], - 'instanceId': instance_id, - 'requestId': context.request_id, - 'status': volume['attach_status'], - 'volumeId': volume_id}) - - @rbac.allow('projectmanager', 'sysadmin') - def detach_volume(self, context, volume_id, **kwargs): - volume = self._get_volume(context, volume_id) - instance_id = volume.get('instance_id', None) - if not instance_id: - raise exception.Error("Volume isn't attached to anything!") - if volume['status'] == "available": - raise exception.Error("Volume is already detached") - try: - volume.start_detach() - instance = self._get_instance(context, instance_id) - rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), - {"method": "detach_volume", - "args": {"instance_id": instance_id, - "volume_id": volume_id}}) - except exception.NotFound: - # If the instance doesn't exist anymore, - # then we need to call detach blind - volume.finish_detach() - return defer.succeed({'attachTime': volume['attach_time'], - 'device': volume['mountpoint'], - 'instanceId': instance_id, - 'requestId': context.request_id, - 'status': volume['attach_status'], - 'volumeId': volume_id}) - - def _convert_to_set(self, lst, label): - if lst == None or lst == []: - return None - if not isinstance(lst, list): - lst = [lst] - return [{label: x} for x in lst] - - @rbac.allow('all') - def describe_instances(self, context, **kwargs): - return defer.succeed(self._format_describe_instances(context)) - - def _format_describe_instances(self, context): - return { 'reservationSet': self._format_instances(context) } - - def _format_run_instances(self, context, reservation_id): - i = self._format_instances(context, reservation_id) - assert len(i) == 1 - return i[0] - - def _format_instances(self, context, reservation_id = None): - reservations = {} - if context.user.is_admin(): - instgenerator = self.instdir.all - else: - instgenerator = self.instdir.by_project(context.project.id) - for instance in instgenerator: - res_id = instance.get('reservation_id', 'Unknown') - if reservation_id != None and reservation_id != res_id: - continue - if not context.user.is_admin(): - if instance['image_id'] == FLAGS.vpn_image_id: - continue - i = {} - i['instance_id'] = instance.get('instance_id', None) - i['image_id'] = instance.get('image_id', None) - i['instance_state'] = { - 'code': instance.get('state', 0), - 'name': instance.get('state_description', 'pending') - } - i['public_dns_name'] = network_model.get_public_ip_for_instance( - i['instance_id']) - i['private_dns_name'] = instance.get('private_dns_name', None) - if not i['public_dns_name']: - i['public_dns_name'] = i['private_dns_name'] - i['dns_name'] = instance.get('dns_name', None) - i['key_name'] = instance.get('key_name', None) - if context.user.is_admin(): - i['key_name'] = '%s (%s, %s)' % (i['key_name'], - instance.get('project_id', None), - instance.get('node_name', '')) - i['product_codes_set'] = self._convert_to_set( - instance.get('product_codes', None), 'product_code') - i['instance_type'] = instance.get('instance_type', None) - i['launch_time'] = instance.get('launch_time', None) - i['ami_launch_index'] = instance.get('ami_launch_index', - None) - if not reservations.has_key(res_id): - r = {} - r['reservation_id'] = res_id - r['owner_id'] = instance.get('project_id', None) - r['group_set'] = self._convert_to_set( - instance.get('groups', None), 'group_id') - r['instances_set'] = [] - reservations[res_id] = r - reservations[res_id]['instances_set'].append(i) - - return list(reservations.values()) - - @rbac.allow('all') - def describe_addresses(self, context, **kwargs): - return self.format_addresses(context) - - def format_addresses(self, context): - addresses = [] - for address in network_model.ElasticIp.all(): - # TODO(vish): implement a by_project iterator for addresses - if (context.user.is_admin() or - address['project_id'] == context.project.id): - address_rv = { - 'public_ip': address['address'], - 'instance_id': address.get('instance_id', 'free') - } - if context.user.is_admin(): - address_rv['instance_id'] = "%s (%s, %s)" % ( - address['instance_id'], - address['user_id'], - address['project_id'], - ) - addresses.append(address_rv) - return {'addressesSet': addresses} - - @rbac.allow('netadmin') - @defer.inlineCallbacks - def allocate_address(self, context, **kwargs): - network_topic = yield self._get_network_topic(context) - public_ip = yield rpc.call(network_topic, - {"method": "allocate_elastic_ip", - "args": {"user_id": context.user.id, - "project_id": context.project.id}}) - defer.returnValue({'addressSet': [{'publicIp': public_ip}]}) - - @rbac.allow('netadmin') - @defer.inlineCallbacks - def release_address(self, context, public_ip, **kwargs): - # NOTE(vish): Should we make sure this works? - network_topic = yield self._get_network_topic(context) - rpc.cast(network_topic, - {"method": "deallocate_elastic_ip", - "args": {"elastic_ip": public_ip}}) - defer.returnValue({'releaseResponse': ["Address released."]}) - - @rbac.allow('netadmin') - @defer.inlineCallbacks - def associate_address(self, context, instance_id, public_ip, **kwargs): - instance = self._get_instance(context, instance_id) - address = self._get_address(context, public_ip) - network_topic = yield self._get_network_topic(context) - rpc.cast(network_topic, - {"method": "associate_elastic_ip", - "args": {"elastic_ip": address['address'], - "fixed_ip": instance['private_dns_name'], - "instance_id": instance['instance_id']}}) - defer.returnValue({'associateResponse': ["Address associated."]}) - - @rbac.allow('netadmin') - @defer.inlineCallbacks - def disassociate_address(self, context, public_ip, **kwargs): - address = self._get_address(context, public_ip) - network_topic = yield self._get_network_topic(context) - rpc.cast(network_topic, - {"method": "disassociate_elastic_ip", - "args": {"elastic_ip": address['address']}}) - defer.returnValue({'disassociateResponse': ["Address disassociated."]}) - - @defer.inlineCallbacks - def _get_network_topic(self, context): - """Retrieves the network host for a project""" - host = network_service.get_host_for_project(context.project.id) - if not host: - host = yield rpc.call(FLAGS.network_topic, - {"method": "set_network_host", - "args": {"user_id": context.user.id, - "project_id": context.project.id}}) - defer.returnValue('%s.%s' %(FLAGS.network_topic, host)) - - @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks - def run_instances(self, context, **kwargs): - # make sure user can access the image - # vpn image is private so it doesn't show up on lists - if kwargs['image_id'] != FLAGS.vpn_image_id: - image = self._get_image(context, kwargs['image_id']) - - # FIXME(ja): if image is cloudpipe, this breaks - - # get defaults from imagestore - image_id = image['imageId'] - kernel_id = image.get('kernelId', FLAGS.default_kernel) - ramdisk_id = image.get('ramdiskId', FLAGS.default_ramdisk) - - # API parameters overrides of defaults - kernel_id = kwargs.get('kernel_id', kernel_id) - ramdisk_id = kwargs.get('ramdisk_id', ramdisk_id) - - # make sure we have access to kernel and ramdisk - self._get_image(context, kernel_id) - self._get_image(context, ramdisk_id) - - logging.debug("Going to run instances...") - reservation_id = utils.generate_uid('r') - launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) - key_data = None - if kwargs.has_key('key_name'): - key_pair = context.user.get_key_pair(kwargs['key_name']) - if not key_pair: - raise exception.ApiError('Key Pair %s not found' % - kwargs['key_name']) - key_data = key_pair.public_key - network_topic = yield self._get_network_topic(context) - # TODO: Get the real security group of launch in here - security_group = "default" - for num in range(int(kwargs['max_count'])): - is_vpn = False - if image_id == FLAGS.vpn_image_id: - is_vpn = True - inst = self.instdir.new() - allocate_data = yield rpc.call(network_topic, - {"method": "allocate_fixed_ip", - "args": {"user_id": context.user.id, - "project_id": context.project.id, - "security_group": security_group, - "is_vpn": is_vpn, - "hostname": inst.instance_id}}) - inst['image_id'] = image_id - inst['kernel_id'] = kernel_id - inst['ramdisk_id'] = ramdisk_id - inst['user_data'] = kwargs.get('user_data', '') - inst['instance_type'] = kwargs.get('instance_type', 'm1.small') - inst['reservation_id'] = reservation_id - inst['launch_time'] = launch_time - inst['key_data'] = key_data or '' - inst['key_name'] = kwargs.get('key_name', '') - inst['user_id'] = context.user.id - inst['project_id'] = context.project.id - inst['ami_launch_index'] = num - inst['security_group'] = security_group - inst['hostname'] = inst.instance_id - for (key, value) in allocate_data.iteritems(): - inst[key] = value - - inst.save() - rpc.cast(FLAGS.compute_topic, - {"method": "run_instance", - "args": {"instance_id": inst.instance_id}}) - logging.debug("Casting to node for %s's instance with IP of %s" % - (context.user.name, inst['private_dns_name'])) - # TODO: Make Network figure out the network name from ip. - defer.returnValue(self._format_run_instances(context, reservation_id)) - - @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks - def terminate_instances(self, context, instance_id, **kwargs): - logging.debug("Going to start terminating instances") - network_topic = yield self._get_network_topic(context) - for i in instance_id: - logging.debug("Going to try and terminate %s" % i) - try: - instance = self._get_instance(context, i) - except exception.NotFound: - logging.warning("Instance %s was not found during terminate" - % i) - continue - elastic_ip = network_model.get_public_ip_for_instance(i) - if elastic_ip: - logging.debug("Disassociating address %s" % elastic_ip) - # NOTE(vish): Right now we don't really care if the ip is - # disassociated. We may need to worry about - # checking this later. Perhaps in the scheduler? - rpc.cast(network_topic, - {"method": "disassociate_elastic_ip", - "args": {"elastic_ip": elastic_ip}}) - - fixed_ip = instance.get('private_dns_name', None) - if fixed_ip: - logging.debug("Deallocating address %s" % fixed_ip) - # NOTE(vish): Right now we don't really care if the ip is - # actually removed. We may need to worry about - # checking this later. Perhaps in the scheduler? - rpc.cast(network_topic, - {"method": "deallocate_fixed_ip", - "args": {"fixed_ip": fixed_ip}}) - - if instance.get('node_name', 'unassigned') != 'unassigned': - # NOTE(joshua?): It's also internal default - rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), - {"method": "terminate_instance", - "args": {"instance_id": i}}) - else: - instance.destroy() - defer.returnValue(True) - - @rbac.allow('projectmanager', 'sysadmin') - def reboot_instances(self, context, instance_id, **kwargs): - """instance_id is a list of instance ids""" - for i in instance_id: - instance = self._get_instance(context, i) - rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), - {"method": "reboot_instance", - "args": {"instance_id": i}}) - return defer.succeed(True) - - @rbac.allow('projectmanager', 'sysadmin') - def delete_volume(self, context, volume_id, **kwargs): - # TODO: return error if not authorized - volume = self._get_volume(context, volume_id) - volume_node = volume['node_name'] - rpc.cast('%s.%s' % (FLAGS.volume_topic, volume_node), - {"method": "delete_volume", - "args": {"volume_id": volume_id}}) - return defer.succeed(True) - - @rbac.allow('all') - def describe_images(self, context, image_id=None, **kwargs): - # The objectstore does its own authorization for describe - imageSet = images.list(context, image_id) - return defer.succeed({'imagesSet': imageSet}) - - @rbac.allow('projectmanager', 'sysadmin') - def deregister_image(self, context, image_id, **kwargs): - # FIXME: should the objectstore be doing these authorization checks? - images.deregister(context, image_id) - return defer.succeed({'imageId': image_id}) - - @rbac.allow('projectmanager', 'sysadmin') - def register_image(self, context, image_location=None, **kwargs): - # FIXME: should the objectstore be doing these authorization checks? - if image_location is None and kwargs.has_key('name'): - image_location = kwargs['name'] - image_id = images.register(context, image_location) - logging.debug("Registered %s as %s" % (image_location, image_id)) - - return defer.succeed({'imageId': image_id}) - - @rbac.allow('all') - def describe_image_attribute(self, context, image_id, attribute, **kwargs): - if attribute != 'launchPermission': - raise exception.ApiError('attribute not supported: %s' % attribute) - try: - image = images.list(context, image_id)[0] - except IndexError: - raise exception.ApiError('invalid id: %s' % image_id) - result = {'image_id': image_id, 'launchPermission': []} - if image['isPublic']: - result['launchPermission'].append({'group': 'all'}) - return defer.succeed(result) - - @rbac.allow('projectmanager', 'sysadmin') - def modify_image_attribute(self, context, image_id, attribute, operation_type, **kwargs): - # TODO(devcamcar): Support users and groups other than 'all'. - if attribute != 'launchPermission': - raise exception.ApiError('attribute not supported: %s' % attribute) - if not 'user_group' in kwargs: - raise exception.ApiError('user or group not specified') - if len(kwargs['user_group']) != 1 and kwargs['user_group'][0] != 'all': - raise exception.ApiError('only group "all" is supported') - if not operation_type in ['add', 'remove']: - raise exception.ApiError('operation_type must be add or remove') - result = images.modify(context, image_id, operation_type) - return defer.succeed(result) - - def update_state(self, topic, value): - """ accepts status reports from the queue and consolidates them """ - # TODO(jmc): if an instance has disappeared from - # the node, call instance_death - if topic == "instances": - return defer.succeed(True) - aggregate_state = getattr(self, topic) - node_name = value.keys()[0] - items = value[node_name] - - logging.debug("Updating %s state for %s" % (topic, node_name)) - - for item_id in items.keys(): - if (aggregate_state.has_key('pending') and - aggregate_state['pending'].has_key(item_id)): - del aggregate_state['pending'][item_id] - aggregate_state[node_name] = items - - return defer.succeed(True) -- cgit From 4f3bb96df8a7e48735c078520e77a47dca7a2bd1 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 12:33:49 -0400 Subject: Remove inlineCallbacks and yield from cloud.py, as eventlet doesn't need it --- nova/api/ec2/__init__.py | 1 + nova/api/ec2/cloud.py | 30 ++++++++++-------------------- nova/endpoint/notes.txt | 16 ++++++++-------- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 7e345d297..b4a1894cc 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -108,6 +108,7 @@ class Router(wsgi.Application): self.controllers = dict(Cloud=cloud.CloudController(), Admin=admin.AdminController()) + @webob.dec.wsgify def __call__(self, req): # Obtain the appropriate controller for this request. match = self.map.match(req.path) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 30634429d..decd2a2c0 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -27,8 +27,6 @@ import logging import os import time -from twisted.internet import defer - from nova import datastore from nova import exception from nova import flags @@ -298,10 +296,9 @@ class CloudController(object): return v @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks def create_volume(self, context, size, **kwargs): # TODO(vish): refactor this to create the volume object here and tell service to create it - result = yield rpc.call(FLAGS.volume_topic, {"method": "create_volume", + result = rpc.call(FLAGS.volume_topic, {"method": "create_volume", "args": {"size": size, "user_id": context.user.id, "project_id": context.project.id}}) @@ -480,31 +477,28 @@ class CloudController(object): return {'addressesSet': addresses} @rbac.allow('netadmin') - @defer.inlineCallbacks def allocate_address(self, context, **kwargs): - network_topic = yield self._get_network_topic(context) - public_ip = yield rpc.call(network_topic, + network_topic = self._get_network_topic(context) + public_ip = rpc.call(network_topic, {"method": "allocate_elastic_ip", "args": {"user_id": context.user.id, "project_id": context.project.id}}) defer.returnValue({'addressSet': [{'publicIp': public_ip}]}) @rbac.allow('netadmin') - @defer.inlineCallbacks def release_address(self, context, public_ip, **kwargs): # NOTE(vish): Should we make sure this works? - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) rpc.cast(network_topic, {"method": "deallocate_elastic_ip", "args": {"elastic_ip": public_ip}}) defer.returnValue({'releaseResponse': ["Address released."]}) @rbac.allow('netadmin') - @defer.inlineCallbacks def associate_address(self, context, instance_id, public_ip, **kwargs): instance = self._get_instance(context, instance_id) address = self._get_address(context, public_ip) - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) rpc.cast(network_topic, {"method": "associate_elastic_ip", "args": {"elastic_ip": address['address'], @@ -513,28 +507,25 @@ class CloudController(object): defer.returnValue({'associateResponse': ["Address associated."]}) @rbac.allow('netadmin') - @defer.inlineCallbacks def disassociate_address(self, context, public_ip, **kwargs): address = self._get_address(context, public_ip) - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) rpc.cast(network_topic, {"method": "disassociate_elastic_ip", "args": {"elastic_ip": address['address']}}) defer.returnValue({'disassociateResponse': ["Address disassociated."]}) - @defer.inlineCallbacks def _get_network_topic(self, context): """Retrieves the network host for a project""" host = network_service.get_host_for_project(context.project.id) if not host: - host = yield rpc.call(FLAGS.network_topic, + host = rpc.call(FLAGS.network_topic, {"method": "set_network_host", "args": {"user_id": context.user.id, "project_id": context.project.id}}) defer.returnValue('%s.%s' %(FLAGS.network_topic, host)) @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks def run_instances(self, context, **kwargs): # make sure user can access the image # vpn image is private so it doesn't show up on lists @@ -566,7 +557,7 @@ class CloudController(object): raise exception.ApiError('Key Pair %s not found' % kwargs['key_name']) key_data = key_pair.public_key - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) # TODO: Get the real security group of launch in here security_group = "default" for num in range(int(kwargs['max_count'])): @@ -574,7 +565,7 @@ class CloudController(object): if image_id == FLAGS.vpn_image_id: is_vpn = True inst = self.instdir.new() - allocate_data = yield rpc.call(network_topic, + allocate_data = rpc.call(network_topic, {"method": "allocate_fixed_ip", "args": {"user_id": context.user.id, "project_id": context.project.id, @@ -608,10 +599,9 @@ class CloudController(object): defer.returnValue(self._format_run_instances(context, reservation_id)) @rbac.allow('projectmanager', 'sysadmin') - @defer.inlineCallbacks def terminate_instances(self, context, instance_id, **kwargs): logging.debug("Going to start terminating instances") - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) for i in instance_id: logging.debug("Going to try and terminate %s" % i) try: diff --git a/nova/endpoint/notes.txt b/nova/endpoint/notes.txt index 7a85cdc93..cbb7b0cd0 100644 --- a/nova/endpoint/notes.txt +++ b/nova/endpoint/notes.txt @@ -43,20 +43,20 @@ CloudController and AdminController: * Controllers: move the @rbac.allow data into an auth WSGI that is right above the call to the controller - verify @defer.inlineCallbacks is just to allow the yield statements, then - remove the yield statements (untangle from twisted) +x verify @defer.inlineCallbacks is just to allow the yield statements, then +x remove the yield statements (untangle from twisted) * nova-api: verify that cloud_topic is going away which I seem to remember, so we can ignore rpc * apiserverapplication: - replace with a Router to a wsgi.Controller - apirequesthandler stuff is just an entry in api.APIRouter +x replace with a Router to a wsgi.Controller +x apirequesthandler stuff is just an entry in api.APIRouter * apirequesthandler - wsgi.Controller pointed to by api.APIRouter - - basically it's execute() from old APIRequestHandler - change to return data directly instead of _write_callback() and finish() +x wsgi.Controller pointed to by api.APIRouter +x - basically it's execute() from old APIRequestHandler +x change to return data directly instead of _write_callback() and finish() * apirequest - doesn't need to change +x doesn't need to change -- cgit From f22c693e4cf638ef5278d9db444da2c4a99baae4 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 31 Aug 2010 13:25:31 -0400 Subject: Remove all Twisted defer references from cloud.py --- nova/api/ec2/cloud.py | 86 ++++++++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 49 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index decd2a2c0..05fbf3861 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -48,11 +48,8 @@ flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') def _gen_key(user_id, key_name): """ Tuck this into AuthManager """ - try: - mgr = manager.AuthManager() - private_key, fingerprint = mgr.generate_key_pair(user_id, key_name) - except Exception as ex: - return {'exception': ex} + mgr = manager.AuthManager() + private_key, fingerprint = mgr.generate_key_pair(user_id, key_name) return {'private_key': private_key, 'fingerprint': fingerprint} @@ -213,18 +210,10 @@ class CloudController(object): @rbac.allow('all') def create_key_pair(self, context, key_name, **kwargs): - dcall = defer.Deferred() - pool = context.handler.application.settings.get('pool') - def _complete(kwargs): - if 'exception' in kwargs: - dcall.errback(kwargs['exception']) - return - dcall.callback({'keyName': key_name, - 'keyFingerprint': kwargs['fingerprint'], - 'keyMaterial': kwargs['private_key']}) - pool.apply_async(_gen_key, [context.user.id, key_name], - callback=_complete) - return dcall + data = _gen_key(context.user.id, key_name) + return {'keyName': key_name, + 'keyFingerprint': data['fingerprint'], + 'keyMaterial': data['private_key']} @rbac.allow('all') def delete_key_pair(self, context, key_name, **kwargs): @@ -268,7 +257,7 @@ class CloudController(object): if context.user.is_admin() or volume['project_id'] == context.project.id: v = self.format_volume(context, volume) volumes.append(v) - return defer.succeed({'volumeSet': volumes}) + return {'volumeSet': volumes} def format_volume(self, context, volume): v = {} @@ -304,7 +293,7 @@ class CloudController(object): "project_id": context.project.id}}) # NOTE(vish): rpc returned value is in the result key in the dictionary volume = self._get_volume(context, result) - defer.returnValue({'volumeSet': [self.format_volume(context, volume)]}) + return {'volumeSet': [self.format_volume(context, volume)]} def _get_address(self, context, public_ip): # FIXME(vish) this should move into network.py @@ -352,12 +341,12 @@ class CloudController(object): "args": {"volume_id": volume_id, "instance_id": instance_id, "mountpoint": device}}) - return defer.succeed({'attachTime': volume['attach_time'], - 'device': volume['mountpoint'], - 'instanceId': instance_id, - 'requestId': context.request_id, - 'status': volume['attach_status'], - 'volumeId': volume_id}) + return {'attachTime': volume['attach_time'], + 'device': volume['mountpoint'], + 'instanceId': instance_id, + 'requestId': context.request_id, + 'status': volume['attach_status'], + 'volumeId': volume_id} @rbac.allow('projectmanager', 'sysadmin') def detach_volume(self, context, volume_id, **kwargs): @@ -378,12 +367,12 @@ class CloudController(object): # If the instance doesn't exist anymore, # then we need to call detach blind volume.finish_detach() - return defer.succeed({'attachTime': volume['attach_time'], - 'device': volume['mountpoint'], - 'instanceId': instance_id, - 'requestId': context.request_id, - 'status': volume['attach_status'], - 'volumeId': volume_id}) + return {'attachTime': volume['attach_time'], + 'device': volume['mountpoint'], + 'instanceId': instance_id, + 'requestId': context.request_id, + 'status': volume['attach_status'], + 'volumeId': volume_id}) def _convert_to_set(self, lst, label): if lst == None or lst == []: @@ -394,7 +383,7 @@ class CloudController(object): @rbac.allow('all') def describe_instances(self, context, **kwargs): - return defer.succeed(self._format_describe_instances(context)) + return self._format_describe_instances(context) def _format_describe_instances(self, context): return { 'reservationSet': self._format_instances(context) } @@ -483,7 +472,7 @@ class CloudController(object): {"method": "allocate_elastic_ip", "args": {"user_id": context.user.id, "project_id": context.project.id}}) - defer.returnValue({'addressSet': [{'publicIp': public_ip}]}) + return {'addressSet': [{'publicIp': public_ip}]} @rbac.allow('netadmin') def release_address(self, context, public_ip, **kwargs): @@ -492,7 +481,7 @@ class CloudController(object): rpc.cast(network_topic, {"method": "deallocate_elastic_ip", "args": {"elastic_ip": public_ip}}) - defer.returnValue({'releaseResponse': ["Address released."]}) + return {'releaseResponse': ["Address released."]} @rbac.allow('netadmin') def associate_address(self, context, instance_id, public_ip, **kwargs): @@ -504,7 +493,7 @@ class CloudController(object): "args": {"elastic_ip": address['address'], "fixed_ip": instance['private_dns_name'], "instance_id": instance['instance_id']}}) - defer.returnValue({'associateResponse': ["Address associated."]}) + return {'associateResponse': ["Address associated."]} @rbac.allow('netadmin') def disassociate_address(self, context, public_ip, **kwargs): @@ -513,7 +502,7 @@ class CloudController(object): rpc.cast(network_topic, {"method": "disassociate_elastic_ip", "args": {"elastic_ip": address['address']}}) - defer.returnValue({'disassociateResponse': ["Address disassociated."]}) + return {'disassociateResponse': ["Address disassociated."]} def _get_network_topic(self, context): """Retrieves the network host for a project""" @@ -523,7 +512,7 @@ class CloudController(object): {"method": "set_network_host", "args": {"user_id": context.user.id, "project_id": context.project.id}}) - defer.returnValue('%s.%s' %(FLAGS.network_topic, host)) + return '%s.%s' %(FLAGS.network_topic, host) @rbac.allow('projectmanager', 'sysadmin') def run_instances(self, context, **kwargs): @@ -596,7 +585,7 @@ class CloudController(object): logging.debug("Casting to node for %s's instance with IP of %s" % (context.user.name, inst['private_dns_name'])) # TODO: Make Network figure out the network name from ip. - defer.returnValue(self._format_run_instances(context, reservation_id)) + return self._format_run_instances(context, reservation_id) @rbac.allow('projectmanager', 'sysadmin') def terminate_instances(self, context, instance_id, **kwargs): @@ -637,7 +626,7 @@ class CloudController(object): "args": {"instance_id": i}}) else: instance.destroy() - defer.returnValue(True) + return True @rbac.allow('projectmanager', 'sysadmin') def reboot_instances(self, context, instance_id, **kwargs): @@ -647,7 +636,7 @@ class CloudController(object): rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), {"method": "reboot_instance", "args": {"instance_id": i}}) - return defer.succeed(True) + return True @rbac.allow('projectmanager', 'sysadmin') def delete_volume(self, context, volume_id, **kwargs): @@ -657,19 +646,19 @@ class CloudController(object): rpc.cast('%s.%s' % (FLAGS.volume_topic, volume_node), {"method": "delete_volume", "args": {"volume_id": volume_id}}) - return defer.succeed(True) + return True @rbac.allow('all') def describe_images(self, context, image_id=None, **kwargs): # The objectstore does its own authorization for describe imageSet = images.list(context, image_id) - return defer.succeed({'imagesSet': imageSet}) + return {'imagesSet': imageSet} @rbac.allow('projectmanager', 'sysadmin') def deregister_image(self, context, image_id, **kwargs): # FIXME: should the objectstore be doing these authorization checks? images.deregister(context, image_id) - return defer.succeed({'imageId': image_id}) + return {'imageId': image_id} @rbac.allow('projectmanager', 'sysadmin') def register_image(self, context, image_location=None, **kwargs): @@ -679,7 +668,7 @@ class CloudController(object): image_id = images.register(context, image_location) logging.debug("Registered %s as %s" % (image_location, image_id)) - return defer.succeed({'imageId': image_id}) + return {'imageId': image_id} @rbac.allow('all') def describe_image_attribute(self, context, image_id, attribute, **kwargs): @@ -692,7 +681,7 @@ class CloudController(object): result = {'image_id': image_id, 'launchPermission': []} if image['isPublic']: result['launchPermission'].append({'group': 'all'}) - return defer.succeed(result) + return result @rbac.allow('projectmanager', 'sysadmin') def modify_image_attribute(self, context, image_id, attribute, operation_type, **kwargs): @@ -705,15 +694,14 @@ class CloudController(object): raise exception.ApiError('only group "all" is supported') if not operation_type in ['add', 'remove']: raise exception.ApiError('operation_type must be add or remove') - result = images.modify(context, image_id, operation_type) - return defer.succeed(result) + return images.modify(context, image_id, operation_type) def update_state(self, topic, value): """ accepts status reports from the queue and consolidates them """ # TODO(jmc): if an instance has disappeared from # the node, call instance_death if topic == "instances": - return defer.succeed(True) + return True aggregate_state = getattr(self, topic) node_name = value.keys()[0] items = value[node_name] @@ -726,4 +714,4 @@ class CloudController(object): del aggregate_state['pending'][item_id] aggregate_state[node_name] = items - return defer.succeed(True) + return True -- cgit From 544b73d35895ac79af910a40590095780f224abb Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 1 Sep 2010 10:50:31 -0400 Subject: Return error Responses properly, and don't muck with req.params -- make a copy instead --- nova/api/ec2/__init__.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index b4a1894cc..248a66f55 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -22,6 +22,7 @@ Starting point for routing EC2 requests import logging import routes +import webob import webob.dec import webob.exc @@ -46,8 +47,7 @@ class Authenticate(wsgi.Middleware): @webob.dec.wsgify def __call__(self, req): - #TODO(gundlach): where do arguments come from? - args = self.request.arguments + args = dict(req.params) # Read request signature. try: @@ -92,8 +92,9 @@ class Authenticate(wsgi.Middleware): _log.debug('arg: %s\t\tval: %s' % (key, value)) # Authenticated! - req.environ['ec2.action'] = action req.environ['ec2.context'] = APIRequestContext(user, project) + req.environ['ec2.action'] = action + req.environ['ec2.action_args'] = args return self.application @@ -119,24 +120,24 @@ class Router(wsgi.Application): try: controller = self.controllers[controller_name] except KeyError: - self._error('unhandled', 'no controller named %s' % controller_name) - return + return self._error('unhandled', 'no controller named %s' % controller_name) api_request = APIRequest(controller, req.environ['ec2.action']) context = req.environ['ec2.context'] try: - return api_request.send(context, **args) + return api_request.send(context, **req.environ['ec2.action_args']) except exception.ApiError as ex: - self._error(req, type(ex).__name__ + "." + ex.code, ex.message) + return self._error(req, type(ex).__name__ + "." + ex.code, ex.message) # TODO(vish): do something more useful with unknown exceptions except Exception as ex: - self._error(type(ex).__name__, str(ex)) + return self._error(type(ex).__name__, str(ex)) def _error(self, req, code, message): - req.status = 400 - req.headers['Content-Type'] = 'text/xml' - req.response = ('\n' + resp = webob.Response() + resp.status = 400 + resp.headers['Content-Type'] = 'text/xml' + resp.body = ('\n' '%s' '%s' '?') % (code, message)) - + return resp -- cgit From 8de182446993ac24e7b8fba12342f8adb3e179d4 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 1 Sep 2010 12:02:14 -0400 Subject: Break Router() into Router() and Executor(), and put Authorizer() (currently a stub) in between them. --- nova/api/ec2/__init__.py | 101 +++++++++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 39 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 248a66f55..87a72ca7c 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -40,36 +40,23 @@ class API(wsgi.Middleware): """Routing for all EC2 API requests.""" def __init__(self): - self.application = Authenticate(Router()) + self.application = Authenticate(Router(Authorizer(Executor()))) class Authenticate(wsgi.Middleware): - """Authenticates an EC2 request.""" + """Authenticate an EC2 request and add 'ec2.context' to WSGI environ.""" @webob.dec.wsgify def __call__(self, req): - args = dict(req.params) - - # Read request signature. + # Read request signature and access id. try: - signature = args.pop('Signature')[0] + signature = req.params['Signature'] + access = req.params['AWSAccessKeyId'] except: raise webob.exc.HTTPBadRequest() # Make a copy of args for authentication and signature verification. - auth_params = {} - for key, value in args.items(): - auth_params[key] = value[0] - - # Get requested action and remove authentication args for final request. - try: - action = args.pop('Action')[0] - access = args.pop('AWSAccessKeyId')[0] - args.pop('SignatureMethod') - args.pop('SignatureVersion') - args.pop('Version') - args.pop('Timestamp') - except: - raise webob.exc.HTTPBadRequest() + auth_params = dict(req.params) + auth_params.pop('Signature') # not part of authentication args # Authenticate the request. try: @@ -86,22 +73,15 @@ class Authenticate(wsgi.Middleware): logging.debug("Authentication Failure: %s" % ex) raise webob.exc.HTTPForbidden() - _log.debug('action: %s' % action) - - for key, value in args.items(): - _log.debug('arg: %s\t\tval: %s' % (key, value)) - # Authenticated! req.environ['ec2.context'] = APIRequestContext(user, project) - req.environ['ec2.action'] = action - req.environ['ec2.action_args'] = args + return self.application class Router(wsgi.Application): """ - Finds controller for a request, executes environ['ec2.action'] upon it, and - returns an XML response. If the action fails, returns a 400. + Add 'ec2.controller', 'ec2.action', and 'ec2.action_args' to WSGI environ. """ def __init__(self): self.map = routes.Mapper() @@ -111,21 +91,63 @@ class Router(wsgi.Application): @webob.dec.wsgify def __call__(self, req): - # Obtain the appropriate controller for this request. - match = self.map.match(req.path) - if not match: - raise webob.exc.HTTPNotFound() - controller_name = match['controller_name'] - + # Obtain the appropriate controller and action for this request. try: + match = self.map.match(req.path) + controller_name = match['controller_name'] controller = self.controllers[controller_name] - except KeyError: - return self._error('unhandled', 'no controller named %s' % controller_name) + except: + raise webob.exc.HTTPNotFound() + non_args = ['Action', 'Signature', 'AWSAccessKeyId', 'SignatureMethod', + 'SignatureVersion', 'Version', 'Timestamp'] + args = dict(req.params) + try: + action = req.params['Action'] # raise KeyError if omitted + for non_arg in non_args: + args.pop(non_arg) # remove, but raise KeyError if omitted + except: + raise webob.exc.HTTPBadRequest() - api_request = APIRequest(controller, req.environ['ec2.action']) + _log.debug('action: %s' % action) + for key, value in args.items(): + _log.debug('arg: %s\t\tval: %s' % (key, value)) + + # Success! + req.environ['ec2.controller'] = controller + req.environ['ec2.action'] = action + req.environ['ec2.action_args'] = args + + return self.application + + +class Authorization(wsgi.Middleware): + """ + Verify that ec2.controller and ec2.action in WSGI environ may be executed + in ec2.context. + """ + + @webob.dec.wsgify + def __call__(self, req): + #TODO(gundlach): put rbac information here. + return self.application + + +class Executor(wsg.Application): + """ + Executes 'ec2.action' upon 'ec2.controller', passing 'ec2.context' and + 'ec2.action_args' (all variables in WSGI environ.) Returns an XML + response, or a 400 upon failure. + """ + @webob.dec.wsgify + def __call__(self, req): context = req.environ['ec2.context'] + controller = req.environ['ec2.controller'] + action = req.environ['ec2.action'] + args = req.environ['ec2.action_args'] + + api_request = APIRequest(controller, action) try: - return api_request.send(context, **req.environ['ec2.action_args']) + return api_request.send(context, **args) except exception.ApiError as ex: return self._error(req, type(ex).__name__ + "." + ex.code, ex.message) # TODO(vish): do something more useful with unknown exceptions @@ -141,3 +163,4 @@ class Router(wsgi.Application): '%s' '?') % (code, message)) return resp + -- cgit From 83df968cfb050bdb6bac981dfcc2d0b1c3dd80db Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 1 Sep 2010 12:42:06 -0400 Subject: Delete rbac.py, moving @rbac decorator knowledge into api.ec2.Authorizer WSGI middleware. --- nova/api/ec2/__init__.py | 64 +++++++++++++++++++++++++++++++++++++++++--- nova/api/ec2/admin.py | 31 ---------------------- nova/api/ec2/cloud.py | 30 --------------------- nova/auth/rbac.py | 69 ------------------------------------------------ 4 files changed, 60 insertions(+), 134 deletions(-) delete mode 100644 nova/auth/rbac.py diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 87a72ca7c..aee9915d0 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -122,14 +122,70 @@ class Router(wsgi.Application): class Authorization(wsgi.Middleware): """ - Verify that ec2.controller and ec2.action in WSGI environ may be executed - in ec2.context. + Return a 401 if ec2.controller and ec2.action in WSGI environ may not be + executed in ec2.context. """ + def __init__(self, application): + super(Authorization, self).__init__(application) + self.action_roles = { + 'CloudController': { + 'DescribeAvailabilityzones': ['all'], + 'DescribeRegions': ['all'], + 'DescribeSnapshots': ['all'], + 'DescribeKeyPairs': ['all'], + 'CreateKeyPair': ['all'], + 'DeleteKeyPair': ['all'], + 'DescribeSecurityGroups': ['all'], + 'CreateSecurityGroup': ['netadmin'], + 'DeleteSecurityGroup': ['netadmin'], + 'GetConsoleOutput': ['projectmanager', 'sysadmin'], + 'DescribeVolumes': ['projectmanager', 'sysadmin'], + 'CreateVolume': ['projectmanager', 'sysadmin'], + 'AttachVolume': ['projectmanager', 'sysadmin'], + 'DetachVolume': ['projectmanager', 'sysadmin'], + 'DescribeInstances': ['all'], + 'DescribeAddresses': ['all'], + 'AllocateAddress': ['netadmin'], + 'ReleaseAddress': ['netadmin'], + 'AssociateAddress': ['netadmin'], + 'DisassociateAddress': ['netadmin'], + 'RunInstances': ['projectmanager', 'sysadmin'], + 'TerminateInstances': ['projectmanager', 'sysadmin'], + 'RebootInstances': ['projectmanager', 'sysadmin'], + 'DeleteVolume': ['projectmanager', 'sysadmin'], + 'DescribeImages': ['all'], + 'DeregisterImage': ['projectmanager', 'sysadmin'], + 'RegisterImage': ['projectmanager', 'sysadmin'], + 'DescribeImageAttribute': ['all'], + 'ModifyImageAttribute': ['projectmanager', 'sysadmin'], + }, + 'AdminController': { + # All actions have the same permission: [] (the default) + # admins will be allowed to run them + # all others will get HTTPUnauthorized. + }, + } + @webob.dec.wsgify def __call__(self, req): - #TODO(gundlach): put rbac information here. - return self.application + context = req.environ['ec2.context'] + controller_name = req.environ['ec2.controller'].__name__ + action = req.environ['ec2.action'] + allowed_roles = self.action_roles[controller_name].get(action, []) + if self._matches_any_role(context, allowed_roles): + return self.application + else: + raise webob.exc.HTTPUnauthorized() + + def _matches_any_role(self, context, roles): + """Return True if any role in roles is allowed in context.""" + if 'all' in roles: + return True + if 'none' in roles: + return False + return any(context.project.has_role(context.user.id, role) + for role in roles) class Executor(wsg.Application): diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index d6f622755..f0c643bbd 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -57,46 +57,27 @@ def host_dict(host): return {} -def admin_only(target): - """Decorator for admin-only API calls""" - def wrapper(*args, **kwargs): - """Internal wrapper method for admin-only API calls""" - context = args[1] - if context.user.is_admin(): - return target(*args, **kwargs) - else: - return {} - - return wrapper - - class AdminController(object): """ API Controller for users, hosts, nodes, and workers. - Trivial admin_only wrapper will be replaced with RBAC, - allowing project managers to administer project users. """ def __str__(self): return 'AdminController' - @admin_only def describe_user(self, _context, name, **_kwargs): """Returns user data, including access and secret keys.""" return user_dict(manager.AuthManager().get_user(name)) - @admin_only def describe_users(self, _context, **_kwargs): """Returns all users - should be changed to deal with a list.""" return {'userSet': [user_dict(u) for u in manager.AuthManager().get_users()] } - @admin_only def register_user(self, _context, name, **_kwargs): """Creates a new user, and returns generated credentials.""" return user_dict(manager.AuthManager().create_user(name)) - @admin_only def deregister_user(self, _context, name, **_kwargs): """Deletes a single user (NOT undoable.) Should throw an exception if the user has instances, @@ -106,13 +87,11 @@ class AdminController(object): return True - @admin_only def describe_roles(self, context, project_roles=True, **kwargs): """Returns a list of allowed roles.""" roles = manager.AuthManager().get_roles(project_roles) return { 'roles': [{'role': r} for r in roles]} - @admin_only def describe_user_roles(self, context, user, project=None, **kwargs): """Returns a list of roles for the given user. Omitting project will return any global roles that the user has. @@ -121,7 +100,6 @@ class AdminController(object): roles = manager.AuthManager().get_user_roles(user, project=project) return { 'roles': [{'role': r} for r in roles]} - @admin_only def modify_user_role(self, context, user, role, project=None, operation='add', **kwargs): """Add or remove a role for a user and project.""" @@ -134,7 +112,6 @@ class AdminController(object): return True - @admin_only def generate_x509_for_user(self, _context, name, project=None, **kwargs): """Generates and returns an x509 certificate for a single user. Is usually called from a client that will wrap this with @@ -146,19 +123,16 @@ class AdminController(object): user = manager.AuthManager().get_user(name) return user_dict(user, base64.b64encode(project.get_credentials(user))) - @admin_only def describe_project(self, context, name, **kwargs): """Returns project data, including member ids.""" return project_dict(manager.AuthManager().get_project(name)) - @admin_only def describe_projects(self, context, user=None, **kwargs): """Returns all projects - should be changed to deal with a list.""" return {'projectSet': [project_dict(u) for u in manager.AuthManager().get_projects(user=user)]} - @admin_only def register_project(self, context, name, manager_user, description=None, member_users=None, **kwargs): """Creates a new project""" @@ -169,20 +143,17 @@ class AdminController(object): description=None, member_users=None)) - @admin_only def deregister_project(self, context, name): """Permanently deletes a project.""" manager.AuthManager().delete_project(name) return True - @admin_only def describe_project_members(self, context, name, **kwargs): project = manager.AuthManager().get_project(name) result = { 'members': [{'member': m} for m in project.member_ids]} return result - @admin_only def modify_project_member(self, context, user, project, operation, **kwargs): """Add or remove a user from a project.""" if operation =='add': @@ -193,7 +164,6 @@ class AdminController(object): raise exception.ApiError('operation must be add or remove') return True - @admin_only def describe_hosts(self, _context, **_kwargs): """Returns status info for all nodes. Includes: * Disk Space @@ -205,7 +175,6 @@ class AdminController(object): """ return {'hostSet': [host_dict(h) for h in model.Host.all()]} - @admin_only def describe_host(self, _context, name, **_kwargs): """Returns status info for single node.""" return host_dict(model.Host.lookup(name)) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 05fbf3861..566887c1a 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -32,7 +32,6 @@ from nova import exception from nova import flags from nova import rpc from nova import utils -from nova.auth import rbac from nova.auth import manager from nova.compute import model from nova.compute.instance_types import INSTANCE_TYPES @@ -163,18 +162,15 @@ class CloudController(object): data['product-codes'] = i['product_codes'] return data - @rbac.allow('all') def describe_availability_zones(self, context, **kwargs): return {'availabilityZoneInfo': [{'zoneName': 'nova', 'zoneState': 'available'}]} - @rbac.allow('all') def describe_regions(self, context, region_name=None, **kwargs): # TODO(vish): region_name is an array. Support filtering return {'regionInfo': [{'regionName': 'nova', 'regionUrl': FLAGS.ec2_url}]} - @rbac.allow('all') def describe_snapshots(self, context, snapshot_id=None, @@ -190,7 +186,6 @@ class CloudController(object): 'volumeSize': 0, 'description': 'fixme'}]} - @rbac.allow('all') def describe_key_pairs(self, context, key_name=None, **kwargs): key_pairs = context.user.get_key_pairs() if not key_name is None: @@ -208,35 +203,29 @@ class CloudController(object): return {'keypairsSet': result} - @rbac.allow('all') def create_key_pair(self, context, key_name, **kwargs): data = _gen_key(context.user.id, key_name) return {'keyName': key_name, 'keyFingerprint': data['fingerprint'], 'keyMaterial': data['private_key']} - @rbac.allow('all') def delete_key_pair(self, context, key_name, **kwargs): context.user.delete_key_pair(key_name) # aws returns true even if the key doens't exist return True - @rbac.allow('all') def describe_security_groups(self, context, group_names, **kwargs): groups = {'securityGroupSet': []} # Stubbed for now to unblock other things. return groups - @rbac.allow('netadmin') def create_security_group(self, context, group_name, **kwargs): return True - @rbac.allow('netadmin') def delete_security_group(self, context, group_name, **kwargs): return True - @rbac.allow('projectmanager', 'sysadmin') def get_console_output(self, context, instance_id, **kwargs): # instance_id is passed in as a list of instances instance = self._get_instance(context, instance_id[0]) @@ -250,7 +239,6 @@ class CloudController(object): else: return None - @rbac.allow('projectmanager', 'sysadmin') def describe_volumes(self, context, **kwargs): volumes = [] for volume in self.volumes: @@ -284,7 +272,6 @@ class CloudController(object): v['attachmentSet'] = [{}] return v - @rbac.allow('projectmanager', 'sysadmin') def create_volume(self, context, size, **kwargs): # TODO(vish): refactor this to create the volume object here and tell service to create it result = rpc.call(FLAGS.volume_topic, {"method": "create_volume", @@ -324,7 +311,6 @@ class CloudController(object): return volume raise exception.NotFound('Volume %s could not be found' % volume_id) - @rbac.allow('projectmanager', 'sysadmin') def attach_volume(self, context, volume_id, instance_id, device, **kwargs): volume = self._get_volume(context, volume_id) if volume['status'] == "attached": @@ -348,7 +334,6 @@ class CloudController(object): 'status': volume['attach_status'], 'volumeId': volume_id} - @rbac.allow('projectmanager', 'sysadmin') def detach_volume(self, context, volume_id, **kwargs): volume = self._get_volume(context, volume_id) instance_id = volume.get('instance_id', None) @@ -381,7 +366,6 @@ class CloudController(object): lst = [lst] return [{label: x} for x in lst] - @rbac.allow('all') def describe_instances(self, context, **kwargs): return self._format_describe_instances(context) @@ -442,7 +426,6 @@ class CloudController(object): return list(reservations.values()) - @rbac.allow('all') def describe_addresses(self, context, **kwargs): return self.format_addresses(context) @@ -465,7 +448,6 @@ class CloudController(object): addresses.append(address_rv) return {'addressesSet': addresses} - @rbac.allow('netadmin') def allocate_address(self, context, **kwargs): network_topic = self._get_network_topic(context) public_ip = rpc.call(network_topic, @@ -474,7 +456,6 @@ class CloudController(object): "project_id": context.project.id}}) return {'addressSet': [{'publicIp': public_ip}]} - @rbac.allow('netadmin') def release_address(self, context, public_ip, **kwargs): # NOTE(vish): Should we make sure this works? network_topic = self._get_network_topic(context) @@ -483,7 +464,6 @@ class CloudController(object): "args": {"elastic_ip": public_ip}}) return {'releaseResponse': ["Address released."]} - @rbac.allow('netadmin') def associate_address(self, context, instance_id, public_ip, **kwargs): instance = self._get_instance(context, instance_id) address = self._get_address(context, public_ip) @@ -495,7 +475,6 @@ class CloudController(object): "instance_id": instance['instance_id']}}) return {'associateResponse': ["Address associated."]} - @rbac.allow('netadmin') def disassociate_address(self, context, public_ip, **kwargs): address = self._get_address(context, public_ip) network_topic = self._get_network_topic(context) @@ -514,7 +493,6 @@ class CloudController(object): "project_id": context.project.id}}) return '%s.%s' %(FLAGS.network_topic, host) - @rbac.allow('projectmanager', 'sysadmin') def run_instances(self, context, **kwargs): # make sure user can access the image # vpn image is private so it doesn't show up on lists @@ -587,7 +565,6 @@ class CloudController(object): # TODO: Make Network figure out the network name from ip. return self._format_run_instances(context, reservation_id) - @rbac.allow('projectmanager', 'sysadmin') def terminate_instances(self, context, instance_id, **kwargs): logging.debug("Going to start terminating instances") network_topic = self._get_network_topic(context) @@ -628,7 +605,6 @@ class CloudController(object): instance.destroy() return True - @rbac.allow('projectmanager', 'sysadmin') def reboot_instances(self, context, instance_id, **kwargs): """instance_id is a list of instance ids""" for i in instance_id: @@ -638,7 +614,6 @@ class CloudController(object): "args": {"instance_id": i}}) return True - @rbac.allow('projectmanager', 'sysadmin') def delete_volume(self, context, volume_id, **kwargs): # TODO: return error if not authorized volume = self._get_volume(context, volume_id) @@ -648,19 +623,16 @@ class CloudController(object): "args": {"volume_id": volume_id}}) return True - @rbac.allow('all') def describe_images(self, context, image_id=None, **kwargs): # The objectstore does its own authorization for describe imageSet = images.list(context, image_id) return {'imagesSet': imageSet} - @rbac.allow('projectmanager', 'sysadmin') def deregister_image(self, context, image_id, **kwargs): # FIXME: should the objectstore be doing these authorization checks? images.deregister(context, image_id) return {'imageId': image_id} - @rbac.allow('projectmanager', 'sysadmin') def register_image(self, context, image_location=None, **kwargs): # FIXME: should the objectstore be doing these authorization checks? if image_location is None and kwargs.has_key('name'): @@ -670,7 +642,6 @@ class CloudController(object): return {'imageId': image_id} - @rbac.allow('all') def describe_image_attribute(self, context, image_id, attribute, **kwargs): if attribute != 'launchPermission': raise exception.ApiError('attribute not supported: %s' % attribute) @@ -683,7 +654,6 @@ class CloudController(object): result['launchPermission'].append({'group': 'all'}) return result - @rbac.allow('projectmanager', 'sysadmin') def modify_image_attribute(self, context, image_id, attribute, operation_type, **kwargs): # TODO(devcamcar): Support users and groups other than 'all'. if attribute != 'launchPermission': diff --git a/nova/auth/rbac.py b/nova/auth/rbac.py deleted file mode 100644 index d157f44b3..000000000 --- a/nova/auth/rbac.py +++ /dev/null @@ -1,69 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Role-based access control decorators to use fpr wrapping other -methods with.""" - -from nova import exception - - -def allow(*roles): - """Allow the given roles access the wrapped function.""" - - def wrap(func): # pylint: disable-msg=C0111 - - def wrapped_func(self, context, *args, - **kwargs): # pylint: disable-msg=C0111 - if context.user.is_superuser(): - return func(self, context, *args, **kwargs) - for role in roles: - if __matches_role(context, role): - return func(self, context, *args, **kwargs) - raise exception.NotAuthorized() - - return wrapped_func - - return wrap - - -def deny(*roles): - """Deny the given roles access the wrapped function.""" - - def wrap(func): # pylint: disable-msg=C0111 - - def wrapped_func(self, context, *args, - **kwargs): # pylint: disable-msg=C0111 - if context.user.is_superuser(): - return func(self, context, *args, **kwargs) - for role in roles: - if __matches_role(context, role): - raise exception.NotAuthorized() - return func(self, context, *args, **kwargs) - - return wrapped_func - - return wrap - - -def __matches_role(context, role): - """Check if a role is allowed.""" - if role == 'all': - return True - if role == 'none': - return False - return context.project.has_role(context.user.id, role) -- cgit From 40778d77936cb63decfc56e6b75fa4c31c13a564 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 1 Sep 2010 13:28:30 -0400 Subject: notes -- conversion 'complete' except now the unit tests won't work and surely i have bugs :) --- nova/endpoint/notes.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/endpoint/notes.txt b/nova/endpoint/notes.txt index cbb7b0cd0..3e48f1749 100644 --- a/nova/endpoint/notes.txt +++ b/nova/endpoint/notes.txt @@ -41,8 +41,8 @@ CloudController and AdminController: ==== STRATEGY TO CONVERT TO EVENTLET+WSGI ==== * Controllers: - move the @rbac.allow data into an auth WSGI that is right above the call - to the controller +x move the @rbac.allow data into an auth WSGI that is right above the call +x to the controller x verify @defer.inlineCallbacks is just to allow the yield statements, then x remove the yield statements (untangle from twisted) -- cgit From b965dde9e95e16a9a207697d5729bd146c2dfd23 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 1 Sep 2010 13:55:38 -0400 Subject: Fix simple errors to the point where we can run the tests [but not pass] --- nova/api/ec2/__init__.py | 10 ++-- nova/api/ec2/apirequestcontext.py | 33 ----------- nova/api/ec2/cloud.py | 2 +- nova/api/ec2/context.py | 33 +++++++++++ nova/endpoint/api.py | 122 -------------------------------------- nova/objectstore/handler.py | 4 +- nova/tests/api_unittest.py | 3 +- nova/tests/auth_unittest.py | 2 +- nova/tests/cloud_unittest.py | 6 +- 9 files changed, 47 insertions(+), 168 deletions(-) delete mode 100644 nova/api/ec2/apirequestcontext.py create mode 100644 nova/api/ec2/context.py delete mode 100755 nova/endpoint/api.py diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index aee9915d0..3335338e0 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -26,9 +26,11 @@ import webob import webob.dec import webob.exc +from nova import exception +from nova import wsgi +from nova.api.ec2 import context from nova.api.ec2 import admin from nova.api.ec2 import cloud -from nova import exception from nova.auth import manager @@ -74,7 +76,7 @@ class Authenticate(wsgi.Middleware): raise webob.exc.HTTPForbidden() # Authenticated! - req.environ['ec2.context'] = APIRequestContext(user, project) + req.environ['ec2.context'] = context.APIRequestContext(user, project) return self.application @@ -188,7 +190,7 @@ class Authorization(wsgi.Middleware): for role in roles) -class Executor(wsg.Application): +class Executor(wsgi.Application): """ Executes 'ec2.action' upon 'ec2.controller', passing 'ec2.context' and 'ec2.action_args' (all variables in WSGI environ.) Returns an XML @@ -217,6 +219,6 @@ class Executor(wsg.Application): resp.body = ('\n' '%s' '%s' - '?') % (code, message)) + '?') % (code, message) return resp diff --git a/nova/api/ec2/apirequestcontext.py b/nova/api/ec2/apirequestcontext.py deleted file mode 100644 index fb3118020..000000000 --- a/nova/api/ec2/apirequestcontext.py +++ /dev/null @@ -1,33 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -APIRequestContext -""" - -import random - -class APIRequestContext(object): - def __init__(self, handler, user, project): - self.handler = handler - self.user = user - self.project = project - self.request_id = ''.join( - [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-') - for x in xrange(20)] - ) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 566887c1a..fc0eb2711 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -357,7 +357,7 @@ class CloudController(object): 'instanceId': instance_id, 'requestId': context.request_id, 'status': volume['attach_status'], - 'volumeId': volume_id}) + 'volumeId': volume_id} def _convert_to_set(self, lst, label): if lst == None or lst == []: diff --git a/nova/api/ec2/context.py b/nova/api/ec2/context.py new file mode 100644 index 000000000..fb3118020 --- /dev/null +++ b/nova/api/ec2/context.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +APIRequestContext +""" + +import random + +class APIRequestContext(object): + def __init__(self, handler, user, project): + self.handler = handler + self.user = user + self.project = project + self.request_id = ''.join( + [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-') + for x in xrange(20)] + ) diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py deleted file mode 100755 index 311fb1880..000000000 --- a/nova/endpoint/api.py +++ /dev/null @@ -1,122 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Tornado REST API Request Handlers for Nova functions -Most calls are proxied into the responsible controller. -""" - -import multiprocessing -import re -import urllib - -import tornado.web - -from nova import crypto -from nova import flags -import nova.cloudpipe.api -from nova.endpoint import cloud - - -FLAGS = flags.FLAGS -flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') - - -class RootRequestHandler(tornado.web.RequestHandler): - def get(self): - # available api versions - versions = [ - '1.0', - '2007-01-19', - '2007-03-01', - '2007-08-29', - '2007-10-10', - '2007-12-15', - '2008-02-01', - '2008-09-01', - '2009-04-04', - ] - for version in versions: - self.write('%s\n' % version) - self.finish() - - -class MetadataRequestHandler(tornado.web.RequestHandler): - def print_data(self, data): - if isinstance(data, dict): - output = '' - for key in data: - if key == '_name': - continue - output += key - if isinstance(data[key], dict): - if '_name' in data[key]: - output += '=' + str(data[key]['_name']) - else: - output += '/' - output += '\n' - self.write(output[:-1]) # cut off last \n - elif isinstance(data, list): - self.write('\n'.join(data)) - else: - self.write(str(data)) - - def lookup(self, path, data): - items = path.split('/') - for item in items: - if item: - if not isinstance(data, dict): - return data - if not item in data: - return None - data = data[item] - return data - - def get(self, path): - cc = self.application.controllers['Cloud'] - meta_data = cc.get_metadata(self.request.remote_ip) - if meta_data is None: - _log.error('Failed to get metadata for ip: %s' % - self.request.remote_ip) - raise tornado.web.HTTPError(404) - data = self.lookup(path, meta_data) - if data is None: - raise tornado.web.HTTPError(404) - self.print_data(data) - self.finish() - - -class APIServerApplication(tornado.web.Application): - def __init__(self, controllers): - tornado.web.Application.__init__(self, [ - (r'/', RootRequestHandler), - (r'/cloudpipe/(.*)', nova.cloudpipe.api.CloudPipeRequestHandler), - (r'/cloudpipe', nova.cloudpipe.api.CloudPipeRequestHandler), - (r'/services/([A-Za-z0-9]+)/', APIRequestHandler), - (r'/latest/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2009-04-04/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2008-09-01/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2008-02-01/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2007-12-15/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2007-10-10/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2007-08-29/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2007-03-01/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/2007-01-19/([-A-Za-z0-9/]*)', MetadataRequestHandler), - (r'/1.0/([-A-Za-z0-9/]*)', MetadataRequestHandler), - ], pool=multiprocessing.Pool(4)) - self.controllers = controllers diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index 035e342ca..09591101b 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -55,7 +55,7 @@ from twisted.web import static from nova import exception from nova import flags from nova.auth import manager -from nova.endpoint import api +from nova.api.ec2 import context from nova.objectstore import bucket from nova.objectstore import image @@ -122,7 +122,7 @@ def get_context(request): request.uri, headers=request.getAllHeaders(), check_type='s3') - return api.APIRequestContext(None, user, project) + return context.APIRequestContext(None, user, project) except exception.Error as ex: logging.debug("Authentication Failure: %s" % ex) raise exception.NotAuthorized diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 9d072866c..d21ded75b 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -27,8 +27,7 @@ from twisted.internet import defer from nova import flags from nova import test from nova.auth import manager -from nova.endpoint import api -from nova.endpoint import cloud +from nova.api.ec2 import cloud FLAGS = flags.FLAGS diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index 0b404bfdc..b3b6800a1 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -26,7 +26,7 @@ from nova import crypto from nova import flags from nova import test from nova.auth import manager -from nova.endpoint import cloud +from nova.api.ec2 import cloud FLAGS = flags.FLAGS diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 3501771cc..545cbaede 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -29,8 +29,8 @@ from nova import rpc from nova import test from nova.auth import manager from nova.compute import service -from nova.endpoint import api -from nova.endpoint import cloud +from nova.api.ec2 import context +from nova.api.ec2 import cloud FLAGS = flags.FLAGS @@ -64,7 +64,7 @@ class CloudTestCase(test.BaseTestCase): except: pass admin = manager.AuthManager().get_user('admin') project = manager.AuthManager().create_project('proj', 'admin', 'proj') - self.context = api.APIRequestContext(handler=None,project=project,user=admin) + self.context = context.APIRequestContext(handler=None,project=project,user=admin) def tearDown(self): manager.AuthManager().delete_project('proj') -- cgit From 8169a2a26c5b646a4d6c63c77f15f6aaa6898cb4 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 13:04:05 -0400 Subject: Small typos, plus rework api_unittest to use WSGI instead of Tornado --- nova/api/__init__.py | 2 +- nova/api/ec2/__init__.py | 10 ++-- nova/tests/api_unittest.py | 120 +++++++++------------------------------------ 3 files changed, 29 insertions(+), 103 deletions(-) diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 0166b7fc1..786b246ec 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -37,5 +37,5 @@ class API(wsgi.Router): # be dropped; and I'm leaving off CloudPipeRequestHandler until # I hear that we need it. mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API()) - mapper.connect("/ec2/{path_info:.*}", controller=ec2.API()) + mapper.connect("/services/{path_info:.*}", controller=ec2.API()) super(API, self).__init__(mapper) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 3335338e0..a94bcb863 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -44,6 +44,7 @@ class API(wsgi.Middleware): def __init__(self): self.application = Authenticate(Router(Authorizer(Executor()))) + class Authenticate(wsgi.Middleware): """Authenticate an EC2 request and add 'ec2.context' to WSGI environ.""" @@ -81,11 +82,12 @@ class Authenticate(wsgi.Middleware): return self.application -class Router(wsgi.Application): +class Router(wsgi.Middleware): """ Add 'ec2.controller', 'ec2.action', and 'ec2.action_args' to WSGI environ. """ - def __init__(self): + def __init__(self, application): + super(Router, self).__init__(application) self.map = routes.Mapper() self.map.connect("/{controller_name}/") self.controllers = dict(Cloud=cloud.CloudController(), @@ -122,14 +124,14 @@ class Router(wsgi.Application): return self.application -class Authorization(wsgi.Middleware): +class Authorizer(wsgi.Middleware): """ Return a 401 if ec2.controller and ec2.action in WSGI environ may not be executed in ec2.context. """ def __init__(self, application): - super(Authorization, self).__init__(application) + super(Authorizer, self).__init__(application) self.action_roles = { 'CloudController': { 'DescribeAvailabilityzones': ['all'], diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index d21ded75b..a13bbdeed 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -21,53 +21,14 @@ from boto.ec2 import regioninfo import httplib import random import StringIO -from tornado import httpserver -from twisted.internet import defer +import webob -from nova import flags from nova import test from nova.auth import manager +from nova.api import ec2 from nova.api.ec2 import cloud -FLAGS = flags.FLAGS - - -# NOTE(termie): These are a bunch of helper methods and classes to short -# circuit boto calls and feed them into our tornado handlers, -# it's pretty damn circuitous so apologies if you have to fix -# a bug in it -def boto_to_tornado(method, path, headers, data, host, connection=None): - """ translate boto requests into tornado requests - - connection should be a FakeTornadoHttpConnection instance - """ - try: - headers = httpserver.HTTPHeaders() - except AttributeError: - from tornado import httputil - headers = httputil.HTTPHeaders() - for k, v in headers.iteritems(): - headers[k] = v - - req = httpserver.HTTPRequest(method=method, - uri=path, - headers=headers, - body=data, - host=host, - remote_ip='127.0.0.1', - connection=connection) - return req - - -def raw_to_httpresponse(s): - """ translate a raw tornado http response into an httplib.HTTPResponse """ - sock = FakeHttplibSocket(s) - resp = httplib.HTTPResponse(sock) - resp.begin() - return resp - - class FakeHttplibSocket(object): """ a fake socket implementation for httplib.HTTPResponse, trivial """ def __init__(self, s): @@ -77,73 +38,36 @@ class FakeHttplibSocket(object): return self.fp -class FakeTornadoStream(object): - """ a fake stream to satisfy tornado's assumptions, trivial """ - def set_close_callback(self, f): - pass - - -class FakeTornadoConnection(object): - """ a fake connection object for tornado to pass to its handlers - - web requests are expected to write to this as they get data and call - finish when they are done with the request, we buffer the writes and - kick off a callback when it is done so that we can feed the result back - into boto. - """ - def __init__(self, d): - self.d = d - self._buffer = StringIO.StringIO() - - def write(self, chunk): - self._buffer.write(chunk) - - def finish(self): - s = self._buffer.getvalue() - self.d.callback(s) - - xheaders = None - - @property - def stream(self): - return FakeTornadoStream() - - class FakeHttplibConnection(object): """ a fake httplib.HTTPConnection for boto to use requests made via this connection actually get translated and routed into - our tornado app, we then wait for the response and turn it back into + our WSGI app, we then wait for the response and turn it back into the httplib.HTTPResponse that boto expects. """ def __init__(self, app, host, is_secure=False): self.app = app self.host = host - self.deferred = defer.Deferred() def request(self, method, path, data, headers): - req = boto_to_tornado - conn = FakeTornadoConnection(self.deferred) - request = boto_to_tornado(connection=conn, - method=method, - path=path, - headers=headers, - data=data, - host=self.host) - handler = self.app(request) - self.deferred.addCallback(raw_to_httpresponse) + req = webob.Request.blank(path) + req.method = method + req.body = data + req.headers = headers + req.headers['Accept'] = 'text/html' + req.host = self.host + # Call the WSGI app, get the HTTP response + resp = str(req.get_response(self.app)) + # For some reason, the response doesn't have "HTTP/1.0 " prepended; I + # guess that's a function the web server usually provides. + resp = "HTTP/1.0 %s" % resp + + sock = FakeHttplibSocket(resp) + self.http_response = httplib.HTTPResponse(sock) + self.http_response.begin() def getresponse(self): - @defer.inlineCallbacks - def _waiter(): - result = yield self.deferred - defer.returnValue(result) - d = _waiter() - # NOTE(termie): defer.returnValue above should ensure that - # this deferred has already been called by the time - # we get here, we are going to cheat and return - # the result of the callback - return d.result + return self.http_response def close(self): pass @@ -158,20 +82,20 @@ class ApiEc2TestCase(test.BaseTestCase): self.host = '127.0.0.1' - self.app = api.APIServerApplication({'Cloud': self.cloud}) + self.app = ec2.API() self.ec2 = boto.connect_ec2( aws_access_key_id='fake', aws_secret_access_key='fake', is_secure=False, region=regioninfo.RegionInfo(None, 'test', self.host), - port=FLAGS.cc_port, + port=0, path='/services/Cloud') self.mox.StubOutWithMock(self.ec2, 'new_http_connection') def expect_http(self, host=None, is_secure=False): http = FakeHttplibConnection( - self.app, '%s:%d' % (self.host, FLAGS.cc_port), False) + self.app, '%s:0' % (self.host), False) self.ec2.new_http_connection(host, is_secure).AndReturn(http) return http -- cgit From 9fc2bb60f1b280e9bf28d68c20f04de2130bd398 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:07:24 -0400 Subject: Use port that boto expects --- nova/tests/api_unittest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 534833fba..8087a2e3b 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -93,7 +93,7 @@ class ApiEc2TestCase(test.BaseTestCase): aws_secret_access_key='fake', is_secure=False, region=regioninfo.RegionInfo(None, 'test', self.host), - port=0, + port=8773, path='/services/Cloud') self.mox.StubOutWithMock(self.ec2, 'new_http_connection') @@ -101,7 +101,7 @@ class ApiEc2TestCase(test.BaseTestCase): def expect_http(self, host=None, is_secure=False): """Returns a new EC2 connection""" http = FakeHttplibConnection( - self.app, '%s:0' % (self.host), False) + self.app, '%s:8773' % (self.host), False) # pylint: disable-msg=E1103 self.ec2.new_http_connection(host, is_secure).AndReturn(http) return http -- cgit From 0fa141a231107da931c396f113b00329d63ee430 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:10:55 -0400 Subject: Remove unused APIRequestContext.handler --- nova/api/ec2/context.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/api/ec2/context.py b/nova/api/ec2/context.py index fb3118020..f69747622 100644 --- a/nova/api/ec2/context.py +++ b/nova/api/ec2/context.py @@ -23,8 +23,7 @@ APIRequestContext import random class APIRequestContext(object): - def __init__(self, handler, user, project): - self.handler = handler + def __init__(self, user, project): self.user = user self.project = project self.request_id = ''.join( -- cgit From b360aded9cfeebfd7594b1b649bd2a1573203cd3 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:43:55 -0400 Subject: send requests to the main API instead of to the EC2 subset -- so that it can parse out the '/services/' prefix. Also, oops, match on path_info instead of path like we're supposed to. --- nova/api/ec2/__init__.py | 2 +- nova/tests/api_unittest.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index a94bcb863..1722617ae 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -97,7 +97,7 @@ class Router(wsgi.Middleware): def __call__(self, req): # Obtain the appropriate controller and action for this request. try: - match = self.map.match(req.path) + match = self.map.match(req.path_info) controller_name = match['controller_name'] controller = self.controllers[controller_name] except: diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 8087a2e3b..9f9d32784 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -27,8 +27,7 @@ import webob from nova import test from nova.auth import manager -from nova.api import ec2 -from nova.api.ec2 import cloud +from nova import api class FakeHttplibSocket(object): @@ -83,11 +82,10 @@ class ApiEc2TestCase(test.BaseTestCase): super(ApiEc2TestCase, self).setUp() self.manager = manager.AuthManager() - self.cloud = cloud.CloudController() self.host = '127.0.0.1' - self.app = ec2.API() + self.app = api.API() self.ec2 = boto.connect_ec2( aws_access_key_id='fake', aws_secret_access_key='fake', -- cgit From 3075cc7440a37118d7784057874887f751e1f6a3 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:53:57 -0400 Subject: OMG got api_unittests to pass --- nova/api/ec2/__init__.py | 11 +++++++---- nova/api/ec2/apirequest.py | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 1722617ae..e53e7d964 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -28,6 +28,7 @@ import webob.exc from nova import exception from nova import wsgi +from nova.api.ec2 import apirequest from nova.api.ec2 import context from nova.api.ec2 import admin from nova.api.ec2 import cloud @@ -174,7 +175,7 @@ class Authorizer(wsgi.Middleware): @webob.dec.wsgify def __call__(self, req): context = req.environ['ec2.context'] - controller_name = req.environ['ec2.controller'].__name__ + controller_name = req.environ['ec2.controller'].__class__.__name__ action = req.environ['ec2.action'] allowed_roles = self.action_roles[controller_name].get(action, []) if self._matches_any_role(context, allowed_roles): @@ -205,14 +206,16 @@ class Executor(wsgi.Application): action = req.environ['ec2.action'] args = req.environ['ec2.action_args'] - api_request = APIRequest(controller, action) + api_request = apirequest.APIRequest(controller, action) try: - return api_request.send(context, **args) + result = api_request.send(context, **args) + req.headers['Content-Type'] = 'text/xml' + return result except exception.ApiError as ex: return self._error(req, type(ex).__name__ + "." + ex.code, ex.message) # TODO(vish): do something more useful with unknown exceptions except Exception as ex: - return self._error(type(ex).__name__, str(ex)) + return self._error(req, type(ex).__name__, str(ex)) def _error(self, req, code, message): resp = webob.Response() diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index 261346a09..85ff2fa5e 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -20,9 +20,14 @@ APIRequest class """ +import logging +import re # TODO(termie): replace minidom with etree from xml.dom import minidom +_log = logging.getLogger("api") +_log.setLevel(logging.DEBUG) + _c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') @@ -79,7 +84,6 @@ class APIRequest(object): result = method(context, **args) - req.headers['Content-Type'] = 'text/xml' return self._render_response(result, context.request_id) def _render_response(self, response_data, request_id): -- cgit From 43f1e722b633945a8f5dca005e6fd60515bac4ae Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:55:30 -0400 Subject: Cloud tests pass --- nova/tests/cloud_unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 689251300..92e726ffa 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -60,7 +60,7 @@ class CloudTestCase(test.BaseTestCase): except: pass admin = manager.AuthManager().get_user('admin') project = manager.AuthManager().create_project('proj', 'admin', 'proj') - self.context = context.APIRequestContext(handler=None,project=project,user=admin) + self.context = context.APIRequestContext(project=project,user=admin) def tearDown(self): manager.AuthManager().delete_project('proj') -- cgit From 59adf260b59dcdcc6bc2df3260a331a4a05f535c Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 2 Sep 2010 15:59:52 -0400 Subject: Move nova.endpoint.images to api.ec2 and delete nova.endpoint --- nova/api/ec2/cloud.py | 2 +- nova/api/ec2/images.py | 80 +++++++++++++++++++++++++++++++++++++++++++++++ nova/cloudpipe/pipelib.py | 4 +-- nova/endpoint/__init__.py | 0 nova/endpoint/images.py | 80 ----------------------------------------------- nova/endpoint/notes.txt | 62 ------------------------------------ 6 files changed, 83 insertions(+), 145 deletions(-) create mode 100644 nova/api/ec2/images.py delete mode 100644 nova/endpoint/__init__.py delete mode 100644 nova/endpoint/images.py delete mode 100644 nova/endpoint/notes.txt diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index e3122bbfc..5c9e1b170 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -35,7 +35,7 @@ from nova import utils from nova.auth import manager from nova.compute import model from nova.compute.instance_types import INSTANCE_TYPES -from nova.endpoint import images +from nova.api.ec2 import images from nova.network import service as network_service from nova.network import model as network_model from nova.volume import service diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py new file mode 100644 index 000000000..cfea4c20b --- /dev/null +++ b/nova/api/ec2/images.py @@ -0,0 +1,80 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Proxy AMI-related calls from the cloud controller, to the running +objectstore daemon. +""" + +import json +import urllib + +import boto.s3.connection + +from nova import image +from nova import flags +from nova import utils +from nova.auth import manager + + +FLAGS = flags.FLAGS + + +def modify(context, image_id, operation): + image.S3ImageService(context)._conn().make_request( + method='POST', + bucket='_images', + query_args=qs({'image_id': image_id, 'operation': operation})) + + return True + + +def register(context, image_location): + """ rpc call to register a new image based from a manifest """ + + image_id = utils.generate_uid('ami') + image.S3ImageService(context)._conn().make_request( + method='PUT', + bucket='_images', + query_args=qs({'image_location': image_location, + 'image_id': image_id})) + + return image_id + + +def list(context, filter_list=[]): + """ return a list of all images that a user can see + + optionally filtered by a list of image_id """ + + result = image.S3ImageService(context).index().values() + if not filter_list is None: + return [i for i in result if i['imageId'] in filter_list] + return result + + +def deregister(context, image_id): + """ unregister an image """ + image.S3ImageService(context).delete(image_id) + + +def qs(params): + pairs = [] + for key in params.keys(): + pairs.append(key + '=' + urllib.quote(params[key])) + return '&'.join(pairs) diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py index 2867bcb21..97272eda6 100644 --- a/nova/cloudpipe/pipelib.py +++ b/nova/cloudpipe/pipelib.py @@ -32,7 +32,7 @@ from nova import exception from nova import flags from nova import utils from nova.auth import manager -from nova.endpoint import api +from nova.api.ec2 import context FLAGS = flags.FLAGS @@ -60,7 +60,7 @@ class CloudPipe(object): key_name = self.setup_keypair(project.project_manager_id, project_id) zippy = open(zippath, "r") - context = api.APIRequestContext(handler=None, user=project.project_manager, project=project) + context = context.APIRequestContext(user=project.project_manager, project=project) reservation = self.controller.run_instances(context, # run instances expects encoded userdata, it is decoded in the get_metadata_call diff --git a/nova/endpoint/__init__.py b/nova/endpoint/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nova/endpoint/images.py b/nova/endpoint/images.py deleted file mode 100644 index cfea4c20b..000000000 --- a/nova/endpoint/images.py +++ /dev/null @@ -1,80 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Proxy AMI-related calls from the cloud controller, to the running -objectstore daemon. -""" - -import json -import urllib - -import boto.s3.connection - -from nova import image -from nova import flags -from nova import utils -from nova.auth import manager - - -FLAGS = flags.FLAGS - - -def modify(context, image_id, operation): - image.S3ImageService(context)._conn().make_request( - method='POST', - bucket='_images', - query_args=qs({'image_id': image_id, 'operation': operation})) - - return True - - -def register(context, image_location): - """ rpc call to register a new image based from a manifest """ - - image_id = utils.generate_uid('ami') - image.S3ImageService(context)._conn().make_request( - method='PUT', - bucket='_images', - query_args=qs({'image_location': image_location, - 'image_id': image_id})) - - return image_id - - -def list(context, filter_list=[]): - """ return a list of all images that a user can see - - optionally filtered by a list of image_id """ - - result = image.S3ImageService(context).index().values() - if not filter_list is None: - return [i for i in result if i['imageId'] in filter_list] - return result - - -def deregister(context, image_id): - """ unregister an image """ - image.S3ImageService(context).delete(image_id) - - -def qs(params): - pairs = [] - for key in params.keys(): - pairs.append(key + '=' + urllib.quote(params[key])) - return '&'.join(pairs) diff --git a/nova/endpoint/notes.txt b/nova/endpoint/notes.txt deleted file mode 100644 index 3e48f1749..000000000 --- a/nova/endpoint/notes.txt +++ /dev/null @@ -1,62 +0,0 @@ -bin/nova-api: - somehow listens for 'cloud_topic' rpc messages and ties them to - the cloud controller (maybe so internal calls can hit the API - via Queuing instead of via HTTP?) - hands CloudController and AdminController to APIServerApplication - and hands that to Tornado. - - -api.py: - -APIServerApplication(tornado.web.Application) - maps routes to APIRequestHandler, CloudPipRequestHandler, MetadataRequestHandler, - RootRequestHandler(just lists versions) - (and to controllers which are passed to __init__) - magical twisted mapping to it - -APIRequestHandler - execute: - authenticates request - picks controller from APIServerApplication's list based on name that was at the - start of the URL (e.g. /services/Cloud has /services mapped here via - APIServerApplication then Cloud is controller_name) - picks action from incoming request arguments - dict = APIRequest(controller, action).send(Context(user, project)) - _write_callback(dict) - self.finish() - -APIRequest - send(context, **kwargs): - dict = controller.action(context, **kwargs) - return _render_response(dict) # turns into XML - - -CloudController and AdminController: - actions return dict (or True which is converted into dict(return=True)) - actions have @rbac.allow('list', 'of', 'roles', 'or', '"all"') - actions can have @defer.inlineCallbacks which is used for yield statements - can use rpc.cast and then defer a returnValue - - -==== STRATEGY TO CONVERT TO EVENTLET+WSGI ==== - -* Controllers: -x move the @rbac.allow data into an auth WSGI that is right above the call -x to the controller -x verify @defer.inlineCallbacks is just to allow the yield statements, then -x remove the yield statements (untangle from twisted) - -* nova-api: - verify that cloud_topic is going away which I seem to remember, so we can ignore rpc - -* apiserverapplication: -x replace with a Router to a wsgi.Controller -x apirequesthandler stuff is just an entry in api.APIRouter - -* apirequesthandler -x wsgi.Controller pointed to by api.APIRouter -x - basically it's execute() from old APIRequestHandler -x change to return data directly instead of _write_callback() and finish() - -* apirequest -x doesn't need to change -- cgit From e3c7f34bbcc09cbb169d9316c50da6d908ac623c Mon Sep 17 00:00:00 2001 From: Matt Dietz Date: Thu, 2 Sep 2010 15:34:33 -0500 Subject: Servers API remodeling and serialization handling --- nova/api/rackspace/base.py | 8 +--- nova/api/rackspace/servers.py | 89 ++++++++++++++++++++++++++++++------------- 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/nova/api/rackspace/base.py b/nova/api/rackspace/base.py index dd2c6543c..5e5bd6f54 100644 --- a/nova/api/rackspace/base.py +++ b/nova/api/rackspace/base.py @@ -21,10 +21,4 @@ from nova import wsgi class Controller(wsgi.Controller): """TODO(eday): Base controller for all rackspace controllers. What is this for? Is this just Rackspace specific? """ - - @classmethod - def render(cls, instance): - if isinstance(instance, list): - return {cls.entity_name: cls.render(instance)} - else: - return {"TODO": "TODO"} + pass diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 25d1fe9c8..940b7061f 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -18,56 +18,76 @@ from nova import rpc from nova.compute import model as compute from nova.api.rackspace import base +from webob import exc +from nova import flags + +FLAGS = flags.FLAGS class Controller(base.Controller): - entity_name = 'servers' - def index(self, **kwargs): - instances = [] - for inst in compute.InstanceDirectory().all: - instances.append(instance_details(inst)) + _serialization_metadata = { + 'application/xml': { + "plurals": "servers", + "attributes": { + "server": [ "id", "imageId", "flavorId", "hostId", "status", + "progress", "addresses", "metadata", "progress" ] + } + } + } + + def __init__(self): + self.instdir = compute.InstanceDirectory() + + def index(self, req): + return [_entity_inst(instance_details(inst)) for inst in instdir.all] - def show(self, **kwargs): - instance_id = kwargs['id'] - return compute.InstanceDirectory().get(instance_id) + def show(self, req, id): + inst = self.instdir.get(id) + if inst: + return _entity_inst(inst) + raise exc.HTTPNotFound() - def delete(self, **kwargs): - instance_id = kwargs['id'] - instance = compute.InstanceDirectory().get(instance_id) + def delete(self, req, id): + instance = self.instdir.get(id) if not instance: - raise ServerNotFound("The requested server was not found") + return exc.HTTPNotFound() instance.destroy() - return True + return exc.HTTPAccepted() - def create(self, **kwargs): - inst = self.build_server_instance(kwargs['server']) + def create(self, req): + inst = self._build_server_instance(req) rpc.cast( FLAGS.compute_topic, { "method": "run_instance", "args": {"instance_id": inst.instance_id}}) + return _entity_inst(inst) - def update(self, **kwargs): - instance_id = kwargs['id'] - instance = compute.InstanceDirectory().get(instance_id) + def update(self, req, id): + instance = self.instdir.get(instance_id) if not instance: - raise ServerNotFound("The requested server was not found") + return exc.HTTPNotFound() instance.update(kwargs['server']) instance.save() + return exc.HTTPNoContent() - def build_server_instance(self, env): + def _build_server_instance(self, req): """Build instance data structure and save it to the data store.""" - reservation = utils.generate_uid('r') ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) inst = self.instdir.new() inst['name'] = env['server']['name'] inst['image_id'] = env['server']['imageId'] inst['instance_type'] = env['server']['flavorId'] inst['user_id'] = env['user']['id'] - inst['project_id'] = env['project']['id'] - inst['reservation_id'] = reservation + inst['launch_time'] = ltime inst['mac_address'] = utils.generate_mac() + + # TODO(dietz) Do we need any of these? + inst['project_id'] = env['project']['id'] + inst['reservation_id'] = reservation + reservation = utils.generate_uid('r') + address = self.network.allocate_ip( inst['user_id'], inst['project_id'], @@ -77,7 +97,24 @@ class Controller(base.Controller): inst['user_id'], inst['project_id'], 'default')['bridge_name'] - # key_data, key_name, ami_launch_index - # TODO(todd): key data or root password + inst.save() - return inst + return _entity_inst(inst) + + def _entity_inst(self, inst): + """ Maps everything to Rackspace-like attributes for return""" + + translated_keys = dict(metadata={}, status=state_description, + id=instance_id, imageId=image_id, flavorId=instance_type,) + + for k,v in translated_keys.iteritems(): + inst[k] = inst[v] + + filtered_keys = ['instance_id', 'state_description', 'state', + 'reservation_id', 'project_id', 'launch_time', + 'bridge_name', 'mac_address', 'user_id'] + + for key in filtered_keys:: + del inst[key] + + return dict(server=inst) -- cgit From 0cbde24bfc3ad13a67325e6c0e08d4650d05ea05 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 7 Sep 2010 16:00:01 -0400 Subject: Oops, APIRequestContext's signature has changed --- nova/objectstore/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index 49c647b4e..aabf6831f 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -131,7 +131,7 @@ def get_context(request): request.uri, headers=request.getAllHeaders(), check_type='s3') - return context.APIRequestContext(None, user, project) + return context.APIRequestContext(user, project) except exception.Error as ex: logging.debug("Authentication Failure: %s", ex) raise exception.NotAuthorized -- cgit From 4ddcc28c944975c6e03cccf7d360b3ec4c26e848 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 7 Sep 2010 16:01:26 -0400 Subject: New version of eventlet handles Twisted & eventlet running at the same time --- tools/pip-requires | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pip-requires b/tools/pip-requires index 13e8e5f45..79897c22b 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -6,7 +6,7 @@ amqplib==0.6.1 anyjson==0.2.4 boto==2.0b1 carrot==0.10.5 -eventlet==0.9.10 +eventlet==0.9.12 lockfile==0.8 python-daemon==1.5.5 python-gflags==1.3 -- cgit From 9df460a5cb2de96133028949ad12ac7c16dbd7fc Mon Sep 17 00:00:00 2001 From: andy Date: Wed, 8 Sep 2010 19:57:29 +0200 Subject: Remove tornado-related code from almost everything. Left it in api where it is still being used pending gundlach's changes. --- nova/rpc.py | 16 +--------------- nova/test.py | 7 ++++--- nova/tests/access_unittest.py | 2 +- nova/tests/auth_unittest.py | 2 +- nova/tests/cloud_unittest.py | 14 ++++++++------ nova/tests/objectstore_unittest.py | 2 +- nova/tests/rpc_unittest.py | 4 ++-- 7 files changed, 18 insertions(+), 29 deletions(-) diff --git a/nova/rpc.py b/nova/rpc.py index 84a9b5590..d83dd7a7c 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -81,21 +81,6 @@ class Consumer(messaging.Consumer): self.failed_connection = False super(Consumer, self).__init__(*args, **kwargs) - # TODO(termie): it would be nice to give these some way of automatically - # cleaning up after themselves - def attach_to_tornado(self, io_inst=None): - """Attach a callback to tornado that fires 10 times a second""" - from tornado import ioloop - if io_inst is None: - io_inst = ioloop.IOLoop.instance() - - injected = ioloop.PeriodicCallback( - lambda: self.fetch(enable_callbacks=True), 100, io_loop=io_inst) - 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 @@ -123,6 +108,7 @@ class Consumer(messaging.Consumer): """Attach a callback to twisted that fires 10 times a second""" loop = task.LoopingCall(self.fetch, enable_callbacks=True) loop.start(interval=0.1) + return loop class Publisher(messaging.Publisher): diff --git a/nova/test.py b/nova/test.py index c392c8a84..5380d5743 100644 --- a/nova/test.py +++ b/nova/test.py @@ -62,6 +62,7 @@ class TrialTestCase(unittest.TestCase): self.mox = mox.Mox() self.stubs = stubout.StubOutForTesting() self.flag_overrides = {} + self.injected = [] def tearDown(self): # pylint: disable-msg=C0103 """Runs after each test method to finalize/tear down test environment""" @@ -72,6 +73,9 @@ class TrialTestCase(unittest.TestCase): self.stubs.SmartUnsetAll() self.mox.VerifyAll() + for x in self.injected: + x.stop() + if FLAGS.fake_rabbit: fakerabbit.reset_all() @@ -99,7 +103,6 @@ class BaseTestCase(TrialTestCase): super(BaseTestCase, self).setUp() # TODO(termie): we could possibly keep a more global registry of # the injected listeners... this is fine for now though - self.injected = [] self.ioloop = ioloop.IOLoop.instance() self._waiting = None @@ -109,8 +112,6 @@ class BaseTestCase(TrialTestCase): def tearDown(self):# pylint: disable-msg=C0103 """Runs after each test method to finalize/tear down test environment""" super(BaseTestCase, self).tearDown() - for x in self.injected: - x.stop() if FLAGS.fake_rabbit: fakerabbit.reset_all() diff --git a/nova/tests/access_unittest.py b/nova/tests/access_unittest.py index fa0a090a0..4cdf42091 100644 --- a/nova/tests/access_unittest.py +++ b/nova/tests/access_unittest.py @@ -30,7 +30,7 @@ FLAGS = flags.FLAGS class Context(object): pass -class AccessTestCase(test.BaseTestCase): +class AccessTestCase(test.TrialTestCase): def setUp(self): super(AccessTestCase, self).setUp() FLAGS.connection_type = 'fake' diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index 0b404bfdc..b219d0e3c 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -31,7 +31,7 @@ from nova.endpoint import cloud FLAGS = flags.FLAGS -class AuthTestCase(test.BaseTestCase): +class AuthTestCase(test.TrialTestCase): flush_db = False def setUp(self): super(AuthTestCase, self).setUp() diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 900ff5a97..0add53937 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -19,7 +19,6 @@ import logging import StringIO import time -from tornado import ioloop from twisted.internet import defer import unittest from xml.etree import ElementTree @@ -36,7 +35,7 @@ from nova.endpoint import cloud FLAGS = flags.FLAGS -class CloudTestCase(test.BaseTestCase): +class CloudTestCase(test.TrialTestCase): def setUp(self): super(CloudTestCase, self).setUp() self.flags(connection_type='fake', @@ -51,18 +50,21 @@ class CloudTestCase(test.BaseTestCase): # set up a service self.compute = service.ComputeService() self.compute_consumer = rpc.AdapterConsumer(connection=self.conn, - topic=FLAGS.compute_topic, - proxy=self.compute) - self.injected.append(self.compute_consumer.attach_to_tornado(self.ioloop)) + topic=FLAGS.compute_topic, + proxy=self.compute) + self.injected.append(self.compute_consumer.attach_to_twisted()) try: manager.AuthManager().create_user('admin', 'admin', 'admin') 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 = api.APIRequestContext(handler=None, + project=project, + user=admin) def tearDown(self): + super(CloudTestCase, self).tearDown() manager.AuthManager().delete_project('proj') manager.AuthManager().delete_user('admin') diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index dece4b5d5..b5970d405 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -53,7 +53,7 @@ os.makedirs(os.path.join(OSS_TEMPDIR, 'images')) os.makedirs(os.path.join(OSS_TEMPDIR, 'buckets')) -class ObjectStoreTestCase(test.BaseTestCase): +class ObjectStoreTestCase(test.TrialTestCase): """Test objectstore API directly.""" def setUp(self): # pylint: disable-msg=C0103 diff --git a/nova/tests/rpc_unittest.py b/nova/tests/rpc_unittest.py index e12a28fbc..e11967987 100644 --- a/nova/tests/rpc_unittest.py +++ b/nova/tests/rpc_unittest.py @@ -30,7 +30,7 @@ from nova import test FLAGS = flags.FLAGS -class RpcTestCase(test.BaseTestCase): +class RpcTestCase(test.TrialTestCase): """Test cases for rpc""" def setUp(self): # pylint: disable-msg=C0103 super(RpcTestCase, self).setUp() @@ -40,7 +40,7 @@ class RpcTestCase(test.BaseTestCase): topic='test', proxy=self.receiver) - self.injected.append(self.consumer.attach_to_tornado(self.ioloop)) + self.injected.append(self.consumer.attach_to_twisted()) def test_call_succeed(self): """Get a value through rpc call""" -- cgit From bd550806950bcfdcd32172a896f04bc3b1a76392 Mon Sep 17 00:00:00 2001 From: andy Date: Wed, 8 Sep 2010 20:06:27 +0200 Subject: Missed an instance of attach_to_tornado. Kind of crappy because testing didn't catch it, the test code certainly appears to be testing those features, however. --- nova/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/rpc.py b/nova/rpc.py index d83dd7a7c..7a89ca8d0 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -264,7 +264,7 @@ def call(topic, msg): return d.callback(data['result']) consumer.register_callback(deferred_receive) - injected = consumer.attach_to_tornado() + injected = consumer.attach_to_twisted() # clean up after the injected listened and return x d.addCallback(lambda x: injected.stop() and x or x) -- cgit From 387671f9bc0299116ffbab7acfc47127afb989aa Mon Sep 17 00:00:00 2001 From: andy Date: Wed, 8 Sep 2010 22:43:54 +0200 Subject: Tests turn things into inlineCallbacks. This is to duplicate the old behavior of BaseTestCase and to generally prevent the bad situation in that tests appear to be passing when in fact they haven't run because @defer.inlineCallbacks was forgotten. --- nova/rpc.py | 1 - nova/test.py | 51 ++++++++++++++++++++++++++++++++++++++++---- nova/tests/cloud_unittest.py | 2 +- nova/tests/rpc_unittest.py | 3 +-- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/nova/rpc.py b/nova/rpc.py index 7a89ca8d0..210d3cccf 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -250,7 +250,6 @@ def call(topic, msg): msg_id = uuid.uuid4().hex msg.update({'_msg_id': msg_id}) LOG.debug("MSG_ID is %s" % (msg_id)) - conn = Connection.instance() d = defer.Deferred() consumer = DirectConsumer(connection=conn, msg_id=msg_id) diff --git a/nova/test.py b/nova/test.py index 5380d5743..1f4b33272 100644 --- a/nova/test.py +++ b/nova/test.py @@ -33,6 +33,7 @@ from twisted.trial import unittest from nova import fakerabbit from nova import flags +from nova import rpc FLAGS = flags.FLAGS @@ -63,22 +64,28 @@ class TrialTestCase(unittest.TestCase): self.stubs = stubout.StubOutForTesting() self.flag_overrides = {} self.injected = [] + self._monkeyPatchAttach() def tearDown(self): # pylint: disable-msg=C0103 """Runs after each test method to finalize/tear down test environment""" - super(TrialTestCase, self).tearDown() self.reset_flags() self.mox.UnsetStubs() self.stubs.UnsetAll() self.stubs.SmartUnsetAll() self.mox.VerifyAll() - + + rpc.Consumer.attach_to_twisted = self.originalAttach for x in self.injected: - x.stop() + try: + x.stop() + except AssertionError: + pass if FLAGS.fake_rabbit: fakerabbit.reset_all() + super(TrialTestCase, self).tearDown() + def flags(self, **kw): """Override flag variables for a test""" for k, v in kw.iteritems(): @@ -94,10 +101,46 @@ class TrialTestCase(unittest.TestCase): for k, v in self.flag_overrides.iteritems(): setattr(FLAGS, k, v) + def run(self, result=None): + test_method = getattr(self, self._testMethodName) + setattr(self, + self._testMethodName, + self._maybeInlineCallbacks(test_method, result)) + rv = super(TrialTestCase, self).run(result) + setattr(self, self._testMethodName, test_method) + return rv + + def _maybeInlineCallbacks(self, func, result): + def _wrapped(): + g = func() + if isinstance(g, defer.Deferred): + return g + if not hasattr(g, 'send'): + return defer.succeed(g) + + inlined = defer.inlineCallbacks(func) + d = inlined() + return d + _wrapped.func_name = func.func_name + return _wrapped + + def _monkeyPatchAttach(self): + self.originalAttach = rpc.Consumer.attach_to_twisted + def _wrapped(innerSelf): + rv = self.originalAttach(innerSelf) + self.injected.append(rv) + return rv + + _wrapped.func_name = self.originalAttach.func_name + rpc.Consumer.attach_to_twisted = _wrapped + class BaseTestCase(TrialTestCase): # TODO(jaypipes): Can this be moved into the TrialTestCase class? - """Base test case class for all unit tests.""" + """Base test case class for all unit tests. + + DEPRECATED: This is being removed once Tornado is gone, use TrialTestCase. + """ def setUp(self): # pylint: disable-msg=C0103 """Run before each test method to initialize test environment""" super(BaseTestCase, self).setUp() diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 0add53937..893717fe4 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -52,7 +52,7 @@ class CloudTestCase(test.TrialTestCase): self.compute_consumer = rpc.AdapterConsumer(connection=self.conn, topic=FLAGS.compute_topic, proxy=self.compute) - self.injected.append(self.compute_consumer.attach_to_twisted()) + self.compute_consumer.attach_to_twisted() try: manager.AuthManager().create_user('admin', 'admin', 'admin') diff --git a/nova/tests/rpc_unittest.py b/nova/tests/rpc_unittest.py index e11967987..1bb2405c7 100644 --- a/nova/tests/rpc_unittest.py +++ b/nova/tests/rpc_unittest.py @@ -39,8 +39,7 @@ class RpcTestCase(test.TrialTestCase): self.consumer = rpc.AdapterConsumer(connection=self.conn, topic='test', proxy=self.receiver) - - self.injected.append(self.consumer.attach_to_twisted()) + self.consumer.attach_to_twisted() def test_call_succeed(self): """Get a value through rpc call""" -- cgit From 345749f514291928913a1ecb280b92daec2c0553 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 9 Sep 2010 19:23:27 -0400 Subject: Correct style issues brought up in termie's review --- nova/api/ec2/__init__.py | 37 +++++++++++++++++++------------------ nova/api/ec2/apirequest.py | 2 -- nova/api/ec2/cloud.py | 15 +++++++-------- nova/api/ec2/context.py | 1 + nova/api/ec2/images.py | 21 +++++++-------------- nova/image/service.py | 7 +++++++ nova/tests/api_unittest.py | 1 - 7 files changed, 41 insertions(+), 43 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index e53e7d964..d500b127c 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -16,9 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. -""" -Starting point for routing EC2 requests -""" +"""Starting point for routing EC2 requests""" import logging import routes @@ -40,6 +38,7 @@ _log.setLevel(logging.DEBUG) class API(wsgi.Middleware): + """Routing for all EC2 API requests.""" def __init__(self): @@ -47,6 +46,7 @@ class API(wsgi.Middleware): class Authenticate(wsgi.Middleware): + """Authenticate an EC2 request and add 'ec2.context' to WSGI environ.""" @webob.dec.wsgify @@ -65,28 +65,25 @@ class Authenticate(wsgi.Middleware): # Authenticate the request. try: (user, project) = manager.AuthManager().authenticate( - access, - signature, - auth_params, - req.method, - req.host, - req.path - ) - + access, + signature, + auth_params, + req.method, + req.host, + req.path) except exception.Error, ex: logging.debug("Authentication Failure: %s" % ex) raise webob.exc.HTTPForbidden() # Authenticated! req.environ['ec2.context'] = context.APIRequestContext(user, project) - return self.application class Router(wsgi.Middleware): - """ - Add 'ec2.controller', 'ec2.action', and 'ec2.action_args' to WSGI environ. - """ + + """Add ec2.'controller', .'action', and .'action_args' to WSGI environ.""" + def __init__(self, application): super(Router, self).__init__(application) self.map = routes.Mapper() @@ -121,12 +118,13 @@ class Router(wsgi.Middleware): req.environ['ec2.controller'] = controller req.environ['ec2.action'] = action req.environ['ec2.action_args'] = args - return self.application class Authorizer(wsgi.Middleware): - """ + + """Authorize an EC2 API request. + Return a 401 if ec2.controller and ec2.action in WSGI environ may not be executed in ec2.context. """ @@ -194,11 +192,14 @@ class Authorizer(wsgi.Middleware): class Executor(wsgi.Application): - """ + + """Execute an EC2 API request. + Executes 'ec2.action' upon 'ec2.controller', passing 'ec2.context' and 'ec2.action_args' (all variables in WSGI environ.) Returns an XML response, or a 400 upon failure. """ + @webob.dec.wsgify def __call__(self, req): context = req.environ['ec2.context'] diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index 85ff2fa5e..a3b20118f 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -51,7 +51,6 @@ class APIRequest(object): self.action = action def send(self, context, **kwargs): - try: method = getattr(self.controller, _camelcase_to_underscore(self.action)) @@ -83,7 +82,6 @@ class APIRequest(object): args[key] = [v for k, v in s] result = method(context, **args) - return self._render_response(result, context.request_id) def _render_response(self, response_data, request_id): diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 5c9e1b170..e1e04ca90 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -205,8 +205,8 @@ class CloudController(object): def create_key_pair(self, context, key_name, **kwargs): data = _gen_key(context.user.id, key_name) return {'keyName': key_name, - 'keyFingerprint': data['fingerprint'], - 'keyMaterial': data['private_key']} + 'keyFingerprint': data['fingerprint'], + 'keyMaterial': data['private_key']} def delete_key_pair(self, context, key_name, **kwargs): context.user.delete_key_pair(key_name) @@ -273,10 +273,11 @@ class CloudController(object): def create_volume(self, context, size, **kwargs): # TODO(vish): refactor this to create the volume object here and tell service to create it - result = rpc.call(FLAGS.volume_topic, {"method": "create_volume", - "args": {"size": size, - "user_id": context.user.id, - "project_id": context.project.id}}) + result = rpc.call(FLAGS.volume_topic, + {"method": "create_volume", + "args": {"size": size, + "user_id": context.user.id, + "project_id": context.project.id}}) # NOTE(vish): rpc returned value is in the result key in the dictionary volume = self._get_volume(context, result) return {'volumeSet': [self.format_volume(context, volume)]} @@ -638,7 +639,6 @@ class CloudController(object): image_location = kwargs['name'] image_id = images.register(context, image_location) logging.debug("Registered %s as %s" % (image_location, image_id)) - return {'imageId': image_id} def describe_image_attribute(self, context, image_id, attribute, **kwargs): @@ -682,5 +682,4 @@ class CloudController(object): aggregate_state['pending'].has_key(item_id)): del aggregate_state['pending'][item_id] aggregate_state[node_name] = items - return True diff --git a/nova/api/ec2/context.py b/nova/api/ec2/context.py index f69747622..c53ba98d9 100644 --- a/nova/api/ec2/context.py +++ b/nova/api/ec2/context.py @@ -22,6 +22,7 @@ APIRequestContext import random + class APIRequestContext(object): def __init__(self, user, project): self.user = user diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py index cfea4c20b..f0be7b899 100644 --- a/nova/api/ec2/images.py +++ b/nova/api/ec2/images.py @@ -26,20 +26,20 @@ import urllib import boto.s3.connection -from nova import image from nova import flags from nova import utils from nova.auth import manager +from nova.image import service FLAGS = flags.FLAGS def modify(context, image_id, operation): - image.S3ImageService(context)._conn().make_request( + service.S3ImageService(context)._conn().make_request( method='POST', bucket='_images', - query_args=qs({'image_id': image_id, 'operation': operation})) + query_args=service.qs({'image_id': image_id, 'operation': operation})) return True @@ -48,10 +48,10 @@ def register(context, image_location): """ rpc call to register a new image based from a manifest """ image_id = utils.generate_uid('ami') - image.S3ImageService(context)._conn().make_request( + service.S3ImageService(context)._conn().make_request( method='PUT', bucket='_images', - query_args=qs({'image_location': image_location, + query_args=service.qs({'image_location': image_location, 'image_id': image_id})) return image_id @@ -62,7 +62,7 @@ def list(context, filter_list=[]): optionally filtered by a list of image_id """ - result = image.S3ImageService(context).index().values() + result = service.S3ImageService(context).index().values() if not filter_list is None: return [i for i in result if i['imageId'] in filter_list] return result @@ -70,11 +70,4 @@ def list(context, filter_list=[]): def deregister(context, image_id): """ unregister an image """ - image.S3ImageService(context).delete(image_id) - - -def qs(params): - pairs = [] - for key in params.keys(): - pairs.append(key + '=' + urllib.quote(params[key])) - return '&'.join(pairs) + service.S3ImageService(context).delete(image_id) diff --git a/nova/image/service.py b/nova/image/service.py index 25e4bb675..f6719caec 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -133,3 +133,10 @@ class S3ImageService(ImageService): calling_format=calling, port=FLAGS.s3_port, host=FLAGS.s3_host) + + +def qs(params): + pairs = [] + for key in params.keys(): + pairs.append(key + '=' + urllib.quote(params[key])) + return '&'.join(pairs) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 9f9d32784..ffc78e71d 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -63,7 +63,6 @@ class FakeHttplibConnection(object): # For some reason, the response doesn't have "HTTP/1.0 " prepended; I # guess that's a function the web server usually provides. resp = "HTTP/1.0 %s" % resp - sock = FakeHttplibSocket(resp) self.http_response = httplib.HTTPResponse(sock) self.http_response.begin() -- cgit From 9330ebc110aeb7591567c66939b39f4345b5778d Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 04:52:48 -0700 Subject: added modify project command to allow project manager and description to be updated --- nova/auth/fakeldap.py | 5 ++++- nova/auth/ldapdriver.py | 18 ++++++++++++++++++ nova/auth/manager.py | 20 ++++++++++++++++++++ nova/tests/auth_unittest.py | 6 ++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/nova/auth/fakeldap.py b/nova/auth/fakeldap.py index bfc3433c5..2791dfde6 100644 --- a/nova/auth/fakeldap.py +++ b/nova/auth/fakeldap.py @@ -33,6 +33,7 @@ SCOPE_ONELEVEL = 1 # not implemented SCOPE_SUBTREE = 2 MOD_ADD = 0 MOD_DELETE = 1 +MOD_REPLACE = 2 class NO_SUCH_OBJECT(Exception): # pylint: disable-msg=C0103 @@ -175,7 +176,7 @@ class FakeLDAP(object): Args: dn -- a dn attrs -- a list of tuples in the following form: - ([MOD_ADD | MOD_DELETE], attribute, value) + ([MOD_ADD | MOD_DELETE | MOD_REPACE], attribute, value) """ redis = datastore.Redis.instance() @@ -185,6 +186,8 @@ class FakeLDAP(object): values = _from_json(redis.hget(key, k)) if cmd == MOD_ADD: values.append(v) + elif cmd == MOD_REPLACE: + values = [v] else: values.remove(v) values = redis.hset(key, k, _to_json(values)) diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py index 74ba011b5..cc8e2caa3 100644 --- a/nova/auth/ldapdriver.py +++ b/nova/auth/ldapdriver.py @@ -202,6 +202,24 @@ class LdapDriver(object): self.conn.add_s('cn=%s,%s' % (name, FLAGS.ldap_project_subtree), attr) return self.__to_project(dict(attr)) + def modify_project(self, project_id, manager_uid=None, description=None): + """Modify an existing project""" + if not manager_uid and not description: + return + attr = [] + if manager_uid: + if not self.__user_exists(manager_uid): + raise exception.NotFound("Project can't be modified because " + "manager %s doesn't exist" % + manager_uid) + manager_dn = self.__uid_to_dn(manager_uid) + attr.append((self.ldap.MOD_REPLACE, 'projectManager', manager_dn)) + if description: + attr.append((self.ldap.MOD_REPLACE, 'description', description)) + self.conn.modify_s('cn=%s,%s' % (project_id, + FLAGS.ldap_project_subtree), + attr) + def add_to_project(self, uid, project_id): """Add user to project""" dn = 'cn=%s,%s' % (project_id, FLAGS.ldap_project_subtree) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 284b29502..d094bb7e1 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -525,6 +525,26 @@ class AuthManager(object): if project_dict: return Project(**project_dict) + def modify_project(self, project, manager_user=None, description=None): + """Modify a project + + @type name: Project or project_id + @param project: The project to modify. + + @type manager_user: User or uid + @param manager_user: This user will be the new project manager. + + @type description: str + @param project: This will be the new description of the project. + + """ + if manager_user: + manager_user = User.safe_id(manager_user) + with self.driver() as drv: + drv.modify_project(Project.safe_id(project), + manager_user, + description) + def add_to_project(self, user, project): """Add user to project""" with self.driver() as drv: diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index 0b404bfdc..2fc780640 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -206,6 +206,12 @@ class AuthTestCase(test.BaseTestCase): self.assert_(len(self.manager.get_projects()) > 1) self.assertEqual(len(self.manager.get_projects('test2')), 1) + def test_220_can_modify_project(self): + self.manager.modify_project('testproj', 'test2', 'new description') + project = self.manager.get_project('testproj') + self.assertEqual(project.project_manager_id, 'test2') + self.assertEqual(project.description, 'new description') + def test_299_can_delete_project(self): self.manager.delete_project('testproj') self.assertFalse(filter(lambda p: p.name == 'testproj', self.manager.get_projects())) -- cgit From bc265bbc9b3b42e46e044c18252218a375192123 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 17:12:49 -0700 Subject: multi-region flag for describe regions --- nova/endpoint/cloud.py | 15 ++++++++++++--- nova/flags.py | 15 +++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 8e2beb1e3..180af0540 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -174,9 +174,18 @@ class CloudController(object): @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}]} + if FLAGS.region_list: + regions = [] + for region in FLAGS.region_list: + name, _sep, url = region.partition(',') + regions.append({'regionName': name, + 'regionUrl': url}) + else: + regions = [{'regionName': 'nova', + 'regionUrl': FLAGS.ec2_url}] + if region_name: + regions = [r for r in regions if r['regionName'] in region_name] + return {'regionInfo': regions } @rbac.allow('all') def describe_snapshots(self, diff --git a/nova/flags.py b/nova/flags.py index 2bca36f7e..19dcb96ba 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -34,7 +34,7 @@ class FlagValues(gflags.FlagValues): Unknown flags will be ignored when parsing the command line, but the command line will be kept so that it can be replayed if new flags are defined after the initial parsing. - + """ def __init__(self): @@ -50,7 +50,7 @@ class FlagValues(gflags.FlagValues): # leftover args at the end sneaky_unparsed_args = {"value": None} original_argv = list(argv) - + if self.IsGnuGetOpt(): orig_getopt = getattr(getopt, 'gnu_getopt') orig_name = 'gnu_getopt' @@ -81,7 +81,7 @@ class FlagValues(gflags.FlagValues): args = argv[:1] finally: setattr(getopt, orig_name, orig_getopt) - + # Store the arguments for later, we'll need them for new flags # added at runtime self.__dict__['__stored_argv'] = original_argv @@ -92,7 +92,7 @@ class FlagValues(gflags.FlagValues): def SetDirty(self, name): """Mark a flag as dirty so that accessing it will case a reparse.""" self.__dict__['__dirty'].append(name) - + def IsDirty(self, name): return name in self.__dict__['__dirty'] @@ -113,12 +113,12 @@ class FlagValues(gflags.FlagValues): for k in self.__dict__['__dirty']: setattr(self, k, getattr(new_flags, k)) self.ClearDirty() - + def __setitem__(self, name, flag): gflags.FlagValues.__setitem__(self, name, flag) if self.WasAlreadyParsed(): self.SetDirty(name) - + def __getitem__(self, name): if self.IsDirty(name): self.ParseNewFlags() @@ -166,6 +166,9 @@ def DECLARE(name, module_string, flag_values=FLAGS): # Define any app-specific flags in their own files, docs at: # http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#39 +DEFINE_list('region_list', + [], + 'list of region,url pairs') DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', '127.0.0.1', 's3 host') -- cgit From ee206cd08bd2d82bb5d64b84b6804ba51ab56b37 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 18:51:22 -0700 Subject: moved keypairs to db using the same interface --- nova/auth/manager.py | 36 +++++++++++++++--------------------- nova/db/api.py | 23 +++++++++++++++++++++++ nova/db/sqlalchemy/api.py | 32 ++++++++++++++++++++++++++++++++ nova/db/sqlalchemy/models.py | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 21 deletions(-) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index d5fbec7c5..4cb23bea6 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -668,42 +668,36 @@ class AuthManager(object): with self.driver() as drv: if not drv.get_user(uid): raise exception.NotFound("User %s doesn't exist" % user) - if drv.get_key_pair(uid, key_name): - raise exception.Duplicate("The keypair %s already exists" - % key_name) + try: + db.keypair_get(None, uid, key_name) + raise exception.Duplicate("The keypair %s already exists" + % key_name) + except exception.NotFound: + pass private_key, public_key, fingerprint = crypto.generate_key_pair() self.create_key_pair(uid, key_name, public_key, fingerprint) return private_key, fingerprint def create_key_pair(self, user, key_name, public_key, fingerprint): """Creates a key pair for user""" - with self.driver() as drv: - kp_dict = drv.create_key_pair(User.safe_id(user), - key_name, - public_key, - fingerprint) - if kp_dict: - return KeyPair(**kp_dict) + key = {} + key['user_id'] = User.safe_id(user) + key['name'] = key_name + key['public_key'] = public_key + key['fingerprint'] = fingerprint + return db.keypair_create(None, key) def get_key_pair(self, user, key_name): """Retrieves a key pair for user""" - with self.driver() as drv: - kp_dict = drv.get_key_pair(User.safe_id(user), key_name) - if kp_dict: - return KeyPair(**kp_dict) + return db.keypair_get(None, User.safe_id(user), key_name) def get_key_pairs(self, user): """Retrieves all key pairs for user""" - with self.driver() as drv: - kp_list = drv.get_key_pairs(User.safe_id(user)) - if not kp_list: - return [] - return [KeyPair(**kp_dict) for kp_dict in kp_list] + return db.keypair_get_all_by_user(None, User.safe_id(user)) def delete_key_pair(self, user, key_name): """Deletes a key pair for user""" - with self.driver() as drv: - drv.delete_key_pair(User.safe_id(user), key_name) + return db.keypair_destroy(None, User.safe_id(user), key_name) def get_credentials(self, user, project=None): """Get credential zip for user in project""" diff --git a/nova/db/api.py b/nova/db/api.py index d81673fad..1db978c52 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -254,6 +254,29 @@ def instance_update(context, instance_id, values): return IMPL.instance_update(context, instance_id, values) +################### + + +def keypair_create(context, values): + """Create a keypair from the values dictionary.""" + return IMPL.keypair_create(context, values) + + +def keypair_destroy(context, user_id, name): + """Destroy the keypair or raise if it does not exist.""" + return IMPL.keypair_destroy(context, user_id, name) + + +def keypair_get(context, user_id, name): + """Get a keypair or raise if it does not exist.""" + return IMPL.keypair_get(context, user_id, name) + + +def keypair_get_all_by_user(context, user_id): + """Get all keypairs by user.""" + return IMPL.keypair_get_all_by_user(context, user_id) + + #################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 02ebdd222..b3a307043 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -355,6 +355,38 @@ def instance_update(_context, instance_id, values): ################### +def keypair_create(_context, values): + keypair_ref = models.Keypair() + for (key, value) in values.iteritems(): + keypair_ref[key] = value + keypair_ref.save() + return keypair_ref + + +def keypair_destroy(_context, user_id, name): + session = get_session() + with session.begin(): + keypair_ref = models.Keypair.find_by_args(user_id, + name, + session=session) + keypair_ref.delete(session=session) + + +def keypair_get(_context, user_id, name): + return models.Keypair.find_by_args(user_id, name) + + +def keypair_get_all_by_user(_context, user_id): + session = get_session() + return session.query(models.Keypair + ).filter_by(user_id=user_id + ).filter_by(deleted=False + ).all() + + +################### + + def network_count(_context): return models.Network.count() diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 6818f838c..81c0a77a8 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -284,6 +284,42 @@ class ExportDevice(BASE, NovaBase): uselist=False)) +class Keypair(BASE, NovaBase): + """Represents a keypair""" + __tablename__ = 'keypairs' + id = Column(Integer, primary_key=True) + name = Column(String(255)) + + user_id = Column(String(255)) + + fingerprint = Column(String(255)) + public_key = Column(Text) + + @property + def str_id(self): + return '%s.%s' % (self.user_id, self.name) + + @classmethod + def find_by_str(cls, str_id, session=None, deleted=False): + user_id, _sep, name = str_id.partition('.') + return cls.find_by_str(user_id, name, session, deleted) + + @classmethod + def find_by_args(cls, user_id, name, session=None, deleted=False): + if not session: + session = get_session() + try: + return session.query(cls + ).filter_by(user_id=user_id + ).filter_by(name=name + ).filter_by(deleted=deleted + ).one() + except exc.NoResultFound: + new_exc = exception.NotFound("No model for user %s, name %s" % + (user_id, name)) + raise new_exc.__class__, new_exc, sys.exc_info()[2] + + class Network(BASE, NovaBase): """Represents a network""" __tablename__ = 'networks' -- cgit From d3273e594daf5f94f09c7904bac53fbb895ffeb6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 18:55:11 -0700 Subject: remove keypair from driver --- nova/auth/ldapdriver.py | 60 ------------------------------------------------- nova/auth/manager.py | 23 ------------------- 2 files changed, 83 deletions(-) diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py index 74ba011b5..4e9afc858 100644 --- a/nova/auth/ldapdriver.py +++ b/nova/auth/ldapdriver.py @@ -99,13 +99,6 @@ class LdapDriver(object): dn = FLAGS.ldap_user_subtree return self.__to_user(self.__find_object(dn, query)) - def get_key_pair(self, uid, key_name): - """Retrieve key pair by uid and key name""" - dn = 'cn=%s,%s' % (key_name, - self.__uid_to_dn(uid)) - attr = self.__find_object(dn, '(objectclass=novaKeyPair)') - return self.__to_key_pair(uid, attr) - def get_project(self, pid): """Retrieve project by id""" dn = 'cn=%s,%s' % (pid, @@ -119,12 +112,6 @@ class LdapDriver(object): '(objectclass=novaUser)') return [self.__to_user(attr) for attr in attrs] - def get_key_pairs(self, uid): - """Retrieve list of key pairs""" - attrs = self.__find_objects(self.__uid_to_dn(uid), - '(objectclass=novaKeyPair)') - return [self.__to_key_pair(uid, attr) for attr in attrs] - def get_projects(self, uid=None): """Retrieve list of projects""" pattern = '(objectclass=novaProject)' @@ -154,21 +141,6 @@ class LdapDriver(object): self.conn.add_s(self.__uid_to_dn(name), attr) return self.__to_user(dict(attr)) - def create_key_pair(self, uid, key_name, public_key, fingerprint): - """Create a key pair""" - # TODO(vish): possibly refactor this to store keys in their own ou - # and put dn reference in the user object - attr = [ - ('objectclass', ['novaKeyPair']), - ('cn', [key_name]), - ('sshPublicKey', [public_key]), - ('keyFingerprint', [fingerprint]), - ] - self.conn.add_s('cn=%s,%s' % (key_name, - self.__uid_to_dn(uid)), - attr) - return self.__to_key_pair(uid, dict(attr)) - def create_project(self, name, manager_uid, description=None, member_uids=None): """Create a project""" @@ -265,19 +237,10 @@ class LdapDriver(object): """Delete a user""" if not self.__user_exists(uid): raise exception.NotFound("User %s doesn't exist" % uid) - self.__delete_key_pairs(uid) self.__remove_from_all(uid) self.conn.delete_s('uid=%s,%s' % (uid, FLAGS.ldap_user_subtree)) - def delete_key_pair(self, uid, key_name): - """Delete a key pair""" - if not self.__key_pair_exists(uid, key_name): - raise exception.NotFound("Key Pair %s doesn't exist for user %s" % - (key_name, uid)) - self.conn.delete_s('cn=%s,uid=%s,%s' % (key_name, uid, - FLAGS.ldap_user_subtree)) - def delete_project(self, project_id): """Delete a project""" project_dn = 'cn=%s,%s' % (project_id, FLAGS.ldap_project_subtree) @@ -288,10 +251,6 @@ class LdapDriver(object): """Check if user exists""" return self.get_user(uid) != None - def __key_pair_exists(self, uid, key_name): - """Check if key pair exists""" - return self.get_key_pair(uid, key_name) != None - def __project_exists(self, project_id): """Check if project exists""" return self.get_project(project_id) != None @@ -341,13 +300,6 @@ class LdapDriver(object): """Check if group exists""" return self.__find_object(dn, '(objectclass=groupOfNames)') != None - def __delete_key_pairs(self, uid): - """Delete all key pairs for user""" - keys = self.get_key_pairs(uid) - if keys != None: - for key in keys: - self.delete_key_pair(uid, key['name']) - @staticmethod def __role_to_dn(role, project_id=None): """Convert role to corresponding dn""" @@ -472,18 +424,6 @@ class LdapDriver(object): 'secret': attr['secretKey'][0], 'admin': (attr['isAdmin'][0] == 'TRUE')} - @staticmethod - def __to_key_pair(owner, attr): - """Convert ldap attributes to KeyPair object""" - if attr == None: - return None - return { - 'id': attr['cn'][0], - 'name': attr['cn'][0], - 'owner_id': owner, - 'public_key': attr['sshPublicKey'][0], - 'fingerprint': attr['keyFingerprint'][0]} - def __to_project(self, attr): """Convert ldap attributes to Project object""" if attr == None: diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 4cb23bea6..ef6a5a486 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -154,29 +154,6 @@ class User(AuthBase): self.admin) -class KeyPair(AuthBase): - """Represents an ssh key returned from the datastore - - Even though this object is named KeyPair, only the public key and - fingerprint is stored. The user's private key is not saved. - """ - - def __init__(self, id, name, owner_id, public_key, fingerprint): - AuthBase.__init__(self) - self.id = id - self.name = name - self.owner_id = owner_id - self.public_key = public_key - self.fingerprint = fingerprint - - def __repr__(self): - return "KeyPair('%s', '%s', '%s', '%s', '%s')" % (self.id, - self.name, - self.owner_id, - self.public_key, - self.fingerprint) - - class Project(AuthBase): """Represents a Project returned from the datastore""" -- cgit From adb9cf9e71908844fd720e6f9bab9588610878e1 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 19:03:35 -0700 Subject: delete keypairs when a user is deleted --- nova/auth/manager.py | 8 ++++++-- nova/db/api.py | 5 +++++ nova/db/sqlalchemy/api.py | 8 ++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index ef6a5a486..e2bb748b0 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -620,9 +620,13 @@ class AuthManager(object): return User(**user_dict) def delete_user(self, user): - """Deletes a user""" + """Deletes a user + + Additionally deletes all users keypairs""" + uid = User.safe_id(user) + db.keypair_destroy_all_by_user(None, uid) with self.driver() as drv: - drv.delete_user(User.safe_id(user)) + drv.delete_user(uid) def generate_key_pair(self, user, key_name): """Generates a key pair for a user diff --git a/nova/db/api.py b/nova/db/api.py index 1db978c52..e96d803db 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -267,6 +267,11 @@ def keypair_destroy(context, user_id, name): return IMPL.keypair_destroy(context, user_id, name) +def keypair_destroy_all_by_user(context, user_id): + """Destroy all keypairs by user.""" + return IMPL.keypair_destroy_all_by_user(context, user_id) + + def keypair_get(context, user_id, name): """Get a keypair or raise if it does not exist.""" return IMPL.keypair_get(context, user_id, name) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index b3a307043..4fd1bf216 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -372,6 +372,14 @@ def keypair_destroy(_context, user_id, name): keypair_ref.delete(session=session) +def keypair_destroy_all_by_user(_context, user_id): + session = get_session() + with session.begin(): + # TODO(vish): do we have to use sql here? + session.execute('update keypairs set deleted=1 where user_id=:id', + {'id': user_id}) + + def keypair_get(_context, user_id, name): return models.Keypair.find_by_args(user_id, name) -- cgit From 8e834931087c54585a7aa2716c7a0708fd658f30 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 22:13:36 -0700 Subject: move keypair generation out of auth and fix tests --- nova/auth/manager.py | 70 -------------------------------------------- nova/endpoint/cloud.py | 48 +++++++++++++++++++++--------- nova/tests/api_unittest.py | 7 +++-- nova/tests/auth_unittest.py | 31 -------------------- nova/tests/cloud_unittest.py | 53 ++++++++++++++++++++++++++++----- 5 files changed, 83 insertions(+), 126 deletions(-) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index e2bb748b0..fb87847d5 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -128,24 +128,6 @@ class User(AuthBase): def is_project_manager(self, project): return AuthManager().is_project_manager(self, project) - def generate_key_pair(self, name): - return AuthManager().generate_key_pair(self.id, name) - - def create_key_pair(self, name, public_key, fingerprint): - return AuthManager().create_key_pair(self.id, - name, - public_key, - fingerprint) - - def get_key_pair(self, name): - return AuthManager().get_key_pair(self.id, name) - - def delete_key_pair(self, name): - return AuthManager().delete_key_pair(self.id, name) - - def get_key_pairs(self): - return AuthManager().get_key_pairs(self.id) - def __repr__(self): return "User('%s', '%s', '%s', '%s', %s)" % (self.id, self.name, @@ -628,58 +610,6 @@ class AuthManager(object): with self.driver() as drv: drv.delete_user(uid) - def generate_key_pair(self, user, key_name): - """Generates a key pair for a user - - Generates a public and private key, stores the public key using the - key_name, and returns the private key and fingerprint. - - @type user: User or uid - @param user: User for which to create key pair. - - @type key_name: str - @param key_name: Name to use for the generated KeyPair. - - @rtype: tuple (private_key, fingerprint) - @return: A tuple containing the private_key and fingerprint. - """ - # NOTE(vish): generating key pair is slow so check for legal - # creation before creating keypair - uid = User.safe_id(user) - with self.driver() as drv: - if not drv.get_user(uid): - raise exception.NotFound("User %s doesn't exist" % user) - try: - db.keypair_get(None, uid, key_name) - raise exception.Duplicate("The keypair %s already exists" - % key_name) - except exception.NotFound: - pass - private_key, public_key, fingerprint = crypto.generate_key_pair() - self.create_key_pair(uid, key_name, public_key, fingerprint) - return private_key, fingerprint - - def create_key_pair(self, user, key_name, public_key, fingerprint): - """Creates a key pair for user""" - key = {} - key['user_id'] = User.safe_id(user) - key['name'] = key_name - key['public_key'] = public_key - key['fingerprint'] = fingerprint - return db.keypair_create(None, key) - - def get_key_pair(self, user, key_name): - """Retrieves a key pair for user""" - return db.keypair_get(None, User.safe_id(user), key_name) - - def get_key_pairs(self, user): - """Retrieves all key pairs for user""" - return db.keypair_get_all_by_user(None, User.safe_id(user)) - - def delete_key_pair(self, user, key_name): - """Deletes a key pair for user""" - return db.keypair_destroy(None, User.safe_id(user), key_name) - def get_credentials(self, user, project=None): """Get credential zip for user in project""" if not isinstance(user, User): diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 6ca6855ca..172c65d79 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -29,13 +29,13 @@ import time from twisted.internet import defer +from nova import crypto from nova import db 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.instance_types import INSTANCE_TYPES from nova.endpoint import images @@ -44,14 +44,30 @@ FLAGS = flags.FLAGS flags.DECLARE('storage_availability_zone', 'nova.volume.manager') -def _gen_key(user_id, key_name): - """ Tuck this into AuthManager """ +def _gen_key(context, user_id, key_name): + """Generate a key + + This is a module level method because it is slow and we need to defer + it into a process pool.""" try: - mgr = manager.AuthManager() - private_key, fingerprint = mgr.generate_key_pair(user_id, key_name) + # NOTE(vish): generating key pair is slow so check for legal + # creation before creating keypair + try: + db.keypair_get(context, user_id, key_name) + raise exception.Duplicate("The keypair %s already exists" + % key_name) + except exception.NotFound: + pass + private_key, public_key, fingerprint = crypto.generate_key_pair() + key = {} + key['user_id'] = user_id + key['name'] = key_name + key['public_key'] = public_key + key['fingerprint'] = fingerprint + db.keypair_create(context, key) + return {'private_key': private_key, 'fingerprint': fingerprint} except Exception as ex: return {'exception': ex} - return {'private_key': private_key, 'fingerprint': fingerprint} class CloudController(object): @@ -177,18 +193,18 @@ class CloudController(object): @rbac.allow('all') def describe_key_pairs(self, context, key_name=None, **kwargs): - key_pairs = context.user.get_key_pairs() + key_pairs = db.keypair_get_all_by_user(context, context.user.id) if not key_name is None: - key_pairs = [x for x in key_pairs if x.name in key_name] + 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): + if context.user.is_admin() or not key_pair['name'].endswith(suffix): result.append({ - 'keyName': key_pair.name, - 'keyFingerprint': key_pair.fingerprint, + 'keyName': key_pair['name'], + 'keyFingerprint': key_pair['fingerprint'], }) return {'keypairsSet': result} @@ -204,14 +220,18 @@ class CloudController(object): dcall.callback({'keyName': key_name, 'keyFingerprint': kwargs['fingerprint'], 'keyMaterial': kwargs['private_key']}) - pool.apply_async(_gen_key, [context.user.id, key_name], + # TODO(vish): when context is no longer an object, pass it here + pool.apply_async(_gen_key, [None, 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 + try: + db.keypair_destroy(context, context.user.id, key_name) + except exception.NotFound: + # aws returns true even if the key doesn't exist + pass return True @rbac.allow('all') diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 462d1b295..fdb9e21d8 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -41,8 +41,8 @@ FLAGS = flags.FLAGS # it's pretty damn circuitous so apologies if you have to fix # a bug in it # NOTE(jaypipes) The pylint disables here are for R0913 (too many args) which -# isn't controllable since boto's HTTPRequest needs that many -# args, and for the version-differentiated import of tornado's +# isn't controllable since boto's HTTPRequest needs that many +# args, and for the version-differentiated import of tornado's # httputil. # NOTE(jaypipes): The disable-msg=E1101 and E1103 below is because pylint is # unable to introspect the deferred's return value properly @@ -224,7 +224,8 @@ class ApiEc2TestCase(test.BaseTestCase): for x in range(random.randint(4, 8))) user = self.manager.create_user('fake', 'fake', 'fake') project = self.manager.create_project('fake', 'fake', 'fake') - self.manager.generate_key_pair(user.id, keyname) + # NOTE(vish): create depends on pool, so call helper directly + cloud._gen_key(None, user.id, keyname) rv = self.ec2.get_all_key_pairs() results = [k for k in rv if k.name == keyname] diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index b54e68274..1b4e12677 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -17,8 +17,6 @@ # under the License. import logging -from M2Crypto import BIO -from M2Crypto import RSA from M2Crypto import X509 import unittest @@ -65,35 +63,6 @@ class AuthTestCase(test.BaseTestCase): 'export S3_URL="http://127.0.0.1:3333/"\n' + 'export EC2_USER_ID="test1"\n') - def test_006_test_key_storage(self): - user = self.manager.get_user('test1') - user.create_key_pair('public', 'key', 'fingerprint') - key = user.get_key_pair('public') - self.assertEqual('key', key.public_key) - self.assertEqual('fingerprint', key.fingerprint) - - def test_007_test_key_generation(self): - user = self.manager.get_user('test1') - private_key, fingerprint = user.generate_key_pair('public2') - key = RSA.load_key_string(private_key, callback=lambda: None) - bio = BIO.MemoryBuffer() - public_key = user.get_key_pair('public2').public_key - key.save_pub_key_bio(bio) - converted = crypto.ssl_pub_to_ssh_pub(bio.read()) - # assert key fields are equal - self.assertEqual(public_key.split(" ")[1].strip(), - converted.split(" ")[1].strip()) - - def test_008_can_list_key_pairs(self): - keys = self.manager.get_user('test1').get_key_pairs() - self.assertTrue(filter(lambda k: k.name == 'public', keys)) - self.assertTrue(filter(lambda k: k.name == 'public2', keys)) - - def test_009_can_delete_key_pair(self): - self.manager.get_user('test1').delete_key_pair('public') - keys = self.manager.get_user('test1').get_key_pairs() - self.assertFalse(filter(lambda k: k.name == 'public', keys)) - def test_010_can_list_users(self): users = self.manager.get_users() logging.warn(users) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 29947e03c..4bad25c2b 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -17,13 +17,18 @@ # under the License. import logging +from M2Crypto import BIO +from M2Crypto import RSA import StringIO import time + from tornado import ioloop from twisted.internet import defer import unittest from xml.etree import ElementTree +from nova import crypto +from nova import db from nova import flags from nova import rpc from nova import test @@ -54,16 +59,21 @@ class CloudTestCase(test.BaseTestCase): proxy=self.compute) self.injected.append(self.compute_consumer.attach_to_tornado(self.ioloop)) - try: - manager.AuthManager().create_user('admin', 'admin', 'admin') - 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.manager = manager.AuthManager() + self.user = self.manager.create_user('admin', 'admin', 'admin', True) + self.project = self.manager.create_project('proj', 'admin', 'proj') + self.context = api.APIRequestContext(handler=None, + user=self.user, + project=self.project) def tearDown(self): - manager.AuthManager().delete_project('proj') - manager.AuthManager().delete_user('admin') + self.manager.delete_project(self.project) + self.manager.delete_user(self.user) + super(CloudTestCase, self).setUp() + + def _create_key(self, name): + # NOTE(vish): create depends on pool, so just call helper directly + return cloud._gen_key(self.context, self.context.user.id, name) def test_console_output(self): if FLAGS.connection_type == 'fake': @@ -76,6 +86,33 @@ class CloudTestCase(test.BaseTestCase): self.assert_(output) rv = yield self.compute.terminate_instance(instance_id) + + def test_key_generation(self): + result = self._create_key('test') + private_key = result['private_key'] + key = RSA.load_key_string(private_key, callback=lambda: None) + bio = BIO.MemoryBuffer() + public_key = db.keypair_get(self.context, + self.context.user.id, + 'test')['public_key'] + key.save_pub_key_bio(bio) + converted = crypto.ssl_pub_to_ssh_pub(bio.read()) + # assert key fields are equal + self.assertEqual(public_key.split(" ")[1].strip(), + converted.split(" ")[1].strip()) + + def test_describe_key_pairs(self): + self._create_key('test1') + self._create_key('test2') + result = self.cloud.describe_key_pairs(self.context) + keys = result["keypairsSet"] + self.assertTrue(filter(lambda k: k['keyName'] == 'test1', keys)) + self.assertTrue(filter(lambda k: k['keyName'] == 'test2', keys)) + + def test_delete_key_pair(self): + self._create_key('test') + self.cloud.delete_key_pair(self.context, 'test') + def test_run_instances(self): if FLAGS.connection_type == 'fake': logging.debug("Can't test instances without a real virtual env.") -- cgit From 38070f19036dcf24367429bcc79ffb55fad4b3cd Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 10 Sep 2010 22:42:51 -0700 Subject: it is called regionEndpoint, and use pipe as a separator --- nova/endpoint/cloud.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 180af0540..02eb50b19 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -177,12 +177,12 @@ class CloudController(object): if FLAGS.region_list: regions = [] for region in FLAGS.region_list: - name, _sep, url = region.partition(',') + name, _sep, url = region.partition('|') regions.append({'regionName': name, - 'regionUrl': url}) + 'regionEndpoint': url}) else: regions = [{'regionName': 'nova', - 'regionUrl': FLAGS.ec2_url}] + 'regionEndpoint': FLAGS.ec2_url}] if region_name: regions = [r for r in regions if r['regionName'] in region_name] return {'regionInfo': regions } -- cgit From 66c583b1883af6e3452271df4b302fd32d1ee25d Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 04:18:30 -0700 Subject: fixed old key reference and made keypair name constistent -> key_pair --- nova/auth/manager.py | 4 ++-- nova/cloudpipe/pipelib.py | 4 ++-- nova/crypto.py | 2 +- nova/db/api.py | 30 +++++++++++++++--------------- nova/db/sqlalchemy/api.py | 28 ++++++++++++++-------------- nova/db/sqlalchemy/models.py | 6 +++--- nova/endpoint/cloud.py | 21 ++++++++++----------- nova/tests/cloud_unittest.py | 2 +- 8 files changed, 48 insertions(+), 49 deletions(-) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index fb87847d5..4e321c1bd 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -604,9 +604,9 @@ class AuthManager(object): def delete_user(self, user): """Deletes a user - Additionally deletes all users keypairs""" + Additionally deletes all users key_pairs""" uid = User.safe_id(user) - db.keypair_destroy_all_by_user(None, uid) + db.key_pair_destroy_all_by_user(None, uid) with self.driver() as drv: drv.delete_user(uid) diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py index 2867bcb21..de6a97fb6 100644 --- a/nova/cloudpipe/pipelib.py +++ b/nova/cloudpipe/pipelib.py @@ -58,7 +58,7 @@ class CloudPipe(object): z.write(FLAGS.boot_script_template,'autorun.sh') z.close() - key_name = self.setup_keypair(project.project_manager_id, project_id) + key_name = self.setup_key_pair(project.project_manager_id, project_id) zippy = open(zippath, "r") context = api.APIRequestContext(handler=None, user=project.project_manager, project=project) @@ -74,7 +74,7 @@ class CloudPipe(object): security_groups=["vpn-secgroup"]) zippy.close() - def setup_keypair(self, user_id, project_id): + def setup_key_pair(self, user_id, project_id): key_name = '%s%s' % (project_id, FLAGS.vpn_key_suffix) try: private_key, fingerprint = self.manager.generate_key_pair(user_id, key_name) diff --git a/nova/crypto.py b/nova/crypto.py index b05548ea1..1c6fe57ad 100644 --- a/nova/crypto.py +++ b/nova/crypto.py @@ -18,7 +18,7 @@ """ Wrappers around standard crypto, including root and intermediate CAs, -SSH keypairs and x509 certificates. +SSH key_pairs and x509 certificates. """ import base64 diff --git a/nova/db/api.py b/nova/db/api.py index e96d803db..507f70dd5 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -257,29 +257,29 @@ def instance_update(context, instance_id, values): ################### -def keypair_create(context, values): - """Create a keypair from the values dictionary.""" - return IMPL.keypair_create(context, values) +def key_pair_create(context, values): + """Create a key_pair from the values dictionary.""" + return IMPL.key_pair_create(context, values) -def keypair_destroy(context, user_id, name): - """Destroy the keypair or raise if it does not exist.""" - return IMPL.keypair_destroy(context, user_id, name) +def key_pair_destroy(context, user_id, name): + """Destroy the key_pair or raise if it does not exist.""" + return IMPL.key_pair_destroy(context, user_id, name) -def keypair_destroy_all_by_user(context, user_id): - """Destroy all keypairs by user.""" - return IMPL.keypair_destroy_all_by_user(context, user_id) +def key_pair_destroy_all_by_user(context, user_id): + """Destroy all key_pairs by user.""" + return IMPL.key_pair_destroy_all_by_user(context, user_id) -def keypair_get(context, user_id, name): - """Get a keypair or raise if it does not exist.""" - return IMPL.keypair_get(context, user_id, name) +def key_pair_get(context, user_id, name): + """Get a key_pair or raise if it does not exist.""" + return IMPL.key_pair_get(context, user_id, name) -def keypair_get_all_by_user(context, user_id): - """Get all keypairs by user.""" - return IMPL.keypair_get_all_by_user(context, user_id) +def key_pair_get_all_by_user(context, user_id): + """Get all key_pairs by user.""" + return IMPL.key_pair_get_all_by_user(context, user_id) #################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 4fd1bf216..ce97f6710 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -355,38 +355,38 @@ def instance_update(_context, instance_id, values): ################### -def keypair_create(_context, values): - keypair_ref = models.Keypair() +def key_pair_create(_context, values): + key_pair_ref = models.KeyPair() for (key, value) in values.iteritems(): - keypair_ref[key] = value - keypair_ref.save() - return keypair_ref + key_pair_ref[key] = value + key_pair_ref.save() + return key_pair_ref -def keypair_destroy(_context, user_id, name): +def key_pair_destroy(_context, user_id, name): session = get_session() with session.begin(): - keypair_ref = models.Keypair.find_by_args(user_id, + key_pair_ref = models.KeyPair.find_by_args(user_id, name, session=session) - keypair_ref.delete(session=session) + key_pair_ref.delete(session=session) -def keypair_destroy_all_by_user(_context, user_id): +def key_pair_destroy_all_by_user(_context, user_id): session = get_session() with session.begin(): # TODO(vish): do we have to use sql here? - session.execute('update keypairs set deleted=1 where user_id=:id', + session.execute('update key_pairs set deleted=1 where user_id=:id', {'id': user_id}) -def keypair_get(_context, user_id, name): - return models.Keypair.find_by_args(user_id, name) +def key_pair_get(_context, user_id, name): + return models.KeyPair.find_by_args(user_id, name) -def keypair_get_all_by_user(_context, user_id): +def key_pair_get_all_by_user(_context, user_id): session = get_session() - return session.query(models.Keypair + return session.query(models.KeyPair ).filter_by(user_id=user_id ).filter_by(deleted=False ).all() diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 81c0a77a8..0ecc48bae 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -284,9 +284,9 @@ class ExportDevice(BASE, NovaBase): uselist=False)) -class Keypair(BASE, NovaBase): - """Represents a keypair""" - __tablename__ = 'keypairs' +class KeyPair(BASE, NovaBase): + """Represents a public key pair for ssh""" + __tablename__ = 'key_pairs' id = Column(Integer, primary_key=True) name = Column(String(255)) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 172c65d79..f30565aca 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -51,10 +51,10 @@ def _gen_key(context, user_id, key_name): it into a process pool.""" try: # NOTE(vish): generating key pair is slow so check for legal - # creation before creating keypair + # creation before creating key_pair try: - db.keypair_get(context, user_id, key_name) - raise exception.Duplicate("The keypair %s already exists" + db.key_pair_get(context, user_id, key_name) + raise exception.Duplicate("The key_pair %s already exists" % key_name) except exception.NotFound: pass @@ -64,7 +64,7 @@ def _gen_key(context, user_id, key_name): key['name'] = key_name key['public_key'] = public_key key['fingerprint'] = fingerprint - db.keypair_create(context, key) + db.key_pair_create(context, key) return {'private_key': private_key, 'fingerprint': fingerprint} except Exception as ex: return {'exception': ex} @@ -193,7 +193,7 @@ class CloudController(object): @rbac.allow('all') def describe_key_pairs(self, context, key_name=None, **kwargs): - key_pairs = db.keypair_get_all_by_user(context, context.user.id) + key_pairs = db.key_pair_get_all_by_user(context, context.user.id) if not key_name is None: key_pairs = [x for x in key_pairs if x['name'] in key_name] @@ -228,7 +228,7 @@ class CloudController(object): @rbac.allow('all') def delete_key_pair(self, context, key_name, **kwargs): try: - db.keypair_destroy(context, context.user.id, key_name) + db.key_pair_destroy(context, context.user.id, key_name) except exception.NotFound: # aws returns true even if the key doesn't exist pass @@ -545,11 +545,10 @@ class CloudController(object): 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 + key_pair_ref = db.key_pair_get(context, + context.user.id, + kwargs['key_name']) + key_data = key_pair_ref['public_key'] # TODO: Get the real security group of launch in here security_group = "default" diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 4bad25c2b..e56ea6ac2 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -92,7 +92,7 @@ class CloudTestCase(test.BaseTestCase): private_key = result['private_key'] key = RSA.load_key_string(private_key, callback=lambda: None) bio = BIO.MemoryBuffer() - public_key = db.keypair_get(self.context, + public_key = db.key_pair_get(self.context, self.context.user.id, 'test')['public_key'] key.save_pub_key_bio(bio) -- cgit From 06a799d2668723bbaead7ca2afbfb4b0cbf28abb Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 18:16:10 -0700 Subject: use a string version of key name when constructing mpi dict because None doesn't work well in lookup --- nova/endpoint/cloud.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 622b4e2a4..45291ca34 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -88,10 +88,11 @@ class CloudController(object): if instance['fixed_ip']: line = '%s slots=%d' % (instance['fixed_ip']['str_id'], INSTANCE_TYPES[instance['instance_type']]['vcpus']) - if instance['key_name'] in result: - result[instance['key_name']].append(line) + key = str(instance['key_name']) + if key in result: + result[key].append(line) else: - result[instance['key_name']] = [line] + result[key] = [line] return result def get_metadata(self, address): -- cgit From 3dbb3fa96779fbed89251a7c24455acbddc013e5 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 20:05:32 -0700 Subject: fixed reversed args in nova-manage project environment --- bin/nova-manage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index 325245ac4..1c6dc03ac 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -252,7 +252,7 @@ class ProjectCommands(object): def environment(self, project_id, user_id, filename='novarc'): """Exports environment variables to an sourcable file arguments: project_id user_id [filename='novarc]""" - rc = self.manager.get_environment_rc(project_id, user_id) + rc = self.manager.get_environment_rc(user_id, project_id) with open(filename, 'w') as f: f.write(rc) -- cgit From ab0e61a1e05bdec8b39c22ff9d93845526a0a02b Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 20:22:19 -0700 Subject: implement floating_ip_get_all_by_project and renamed db methods that get more then one to get_all_by instead of get_by --- nova/db/api.py | 19 ++++++++++++------- nova/db/sqlalchemy/api.py | 14 +++++++++++--- nova/endpoint/cloud.py | 16 ++++++++-------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index d749ae50a..1529888ba 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -122,10 +122,15 @@ def floating_ip_get_all(context): def floating_ip_get_all_by_host(context, host): - """Get all floating ips.""" + """Get all floating ips by host.""" return IMPL.floating_ip_get_all_by_host(context, host) +def floating_ip_get_all_by_project(context, project_id): + """Get all floating ips by project.""" + return IMPL.floating_ip_get_all_by_project(context, project_id) + + def floating_ip_get_by_address(context, address): """Get a floating ip by address or raise if it doesn't exist.""" return IMPL.floating_ip_get_by_address(context, address) @@ -208,14 +213,14 @@ def instance_get_all(context): return IMPL.instance_get_all(context) -def instance_get_by_project(context, project_id): +def instance_get_all_by_project(context, project_id): """Get all instance belonging to a project.""" - return IMPL.instance_get_by_project(context, project_id) + return IMPL.instance_get_all_by_project(context, project_id) -def instance_get_by_reservation(context, reservation_id): +def instance_get_all_by_reservation(context, reservation_id): """Get all instance belonging to a reservation.""" - return IMPL.instance_get_by_reservation(context, reservation_id) + return IMPL.instance_get_all_by_reservation(context, reservation_id) def instance_get_fixed_address(context, instance_id): @@ -417,9 +422,9 @@ def volume_get_instance(context, volume_id): return IMPL.volume_get_instance(context, volume_id) -def volume_get_by_project(context, project_id): +def volume_get_all_by_project(context, project_id): """Get all volumes belonging to a project.""" - return IMPL.volume_get_by_project(context, project_id) + return IMPL.volume_get_all_by_project(context, project_id) def volume_get_by_str(context, str_id): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 485dca2b0..948bca224 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -159,6 +159,14 @@ def floating_ip_get_all_by_host(_context, host): ).filter_by(deleted=False ).all() +def floating_ip_get_all_by_project(_context, project_id): + session = get_session() + return session.query(models.FloatingIp + ).options(joinedload_all('fixed_ip.instance') + ).filter_by(project_id=project_id + ).filter_by(deleted=False + ).all() + def floating_ip_get_by_address(_context, address): return models.FloatingIp.find_by_str(address) @@ -288,7 +296,7 @@ def instance_get_all(context): ).all() -def instance_get_by_project(context, project_id): +def instance_get_all_by_project(context, project_id): session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -297,7 +305,7 @@ def instance_get_by_project(context, project_id): ).all() -def instance_get_by_reservation(_context, reservation_id): +def instance_get_all_by_reservation(_context, reservation_id): session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -606,7 +614,7 @@ def volume_get_all(context): return models.Volume.all(deleted=_deleted(context)) -def volume_get_by_project(context, project_id): +def volume_get_all_by_project(context, project_id): session = get_session() return session.query(models.Volume ).filter_by(project_id=project_id diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 622b4e2a4..00aa2ede8 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -84,7 +84,7 @@ class CloudController(object): def _get_mpi_data(self, project_id): result = {} - for instance in db.instance_get_by_project(None, project_id): + for instance in db.instance_get_all_by_project(None, project_id): if instance['fixed_ip']: line = '%s slots=%d' % (instance['fixed_ip']['str_id'], INSTANCE_TYPES[instance['instance_type']]['vcpus']) @@ -244,7 +244,7 @@ class CloudController(object): if context.user.is_admin(): volumes = db.volume_get_all(context) else: - volumes = db.volume_get_by_project(context, context.project.id) + volumes = db.volume_get_all_by_project(context, context.project.id) volumes = [self._format_volume(context, v) for v in volumes] @@ -363,14 +363,14 @@ class CloudController(object): def _format_instances(self, context, reservation_id=None): reservations = {} if reservation_id: - instances = db.instance_get_by_reservation(context, - reservation_id) + instances = db.instance_get_all_by_reservation(context, + reservation_id) else: if not context.user.is_admin(): instances = db.instance_get_all(context) else: - instances = db.instance_get_by_project(context, - context.project.id) + instances = db.instance_get_all_by_project(context, + context.project.id) for instance in instances: if not context.user.is_admin(): if instance['image_id'] == FLAGS.vpn_image_id: @@ -421,8 +421,8 @@ class CloudController(object): if context.user.is_admin(): iterator = db.floating_ip_get_all(context) else: - iterator = db.floating_ip_get_by_project(context, - context.project.id) + iterator = db.floating_ip_get_all_by_project(context, + context.project.id) for floating_ip_ref in iterator: address = floating_ip_ref['str_id'] instance_id = None -- cgit From dff482e992b25580728955ae83ea2e38a18e7736 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 20:54:12 -0700 Subject: manage command for project quotas --- bin/nova-manage | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 325245ac4..c7ef33a48 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -50,7 +50,6 @@ """ CLI interface for nova management. - Connects to the running ADMIN api in the api daemon. """ import os @@ -68,7 +67,9 @@ if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) from nova import db +from nova import exception from nova import flags +from nova import quota from nova import utils from nova.auth import manager from nova.cloudpipe import pipelib @@ -186,6 +187,13 @@ class RoleCommands(object): class UserCommands(object): """Class for managing users.""" + @staticmethod + def _print_export(user): + """Print export variables to use with API.""" + print 'export EC2_ACCESS_KEY=%s' % user.access + print 'export EC2_SECRET_KEY=%s' % user.secret + + def __init__(self): self.manager = manager.AuthManager() @@ -193,13 +201,13 @@ class UserCommands(object): """creates a new admin and prints exports arguments: name [access] [secret]""" user = self.manager.create_user(name, access, secret, True) - print_export(user) + self._print_export(user) def create(self, name, access=None, secret=None): """creates a new user and prints exports arguments: name [access] [secret]""" user = self.manager.create_user(name, access, secret, False) - print_export(user) + self._print_export(user) def delete(self, name): """deletes an existing user @@ -211,7 +219,7 @@ class UserCommands(object): arguments: name""" user = self.manager.get_user(name) if user: - print_export(user) + self._print_export(user) else: print "User %s doesn't exist" % name @@ -222,12 +230,6 @@ class UserCommands(object): print user.name -def print_export(user): - """Print export variables to use with API.""" - print 'export EC2_ACCESS_KEY=%s' % user.access - print 'export EC2_SECRET_KEY=%s' % user.secret - - class ProjectCommands(object): """Class for managing projects.""" @@ -262,6 +264,19 @@ class ProjectCommands(object): for project in self.manager.get_projects(): print project.name + def quota(self, project_id, key=None, value=None): + """Set or display quotas for project + arguments: project_id [key] [value]""" + if key: + quo = {'project_id': project_id, key: value} + try: + db.quota_update(None, project_id, quo) + except exception.NotFound: + db.quota_create(None, quo) + project_quota = quota._get_quota(None, project_id) + for key, value in project_quota.iteritems(): + print '%s: %s' % (key, value) + def remove(self, project, user): """Removes user from project arguments: project user""" @@ -274,6 +289,7 @@ class ProjectCommands(object): with open(filename, 'w') as f: f.write(zip_file) + class FloatingIpCommands(object): """Class for managing floating ip.""" @@ -306,6 +322,7 @@ class FloatingIpCommands(object): floating_ip['address'], instance) + CATEGORIES = [ ('user', UserCommands), ('project', ProjectCommands), -- cgit From 19040b7b2b908b2816bc7aca54a8437d54badd26 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 21:12:01 -0700 Subject: fixed reference to misnamed method --- nova/endpoint/cloud.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 00aa2ede8..41f23618f 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -464,14 +464,15 @@ class CloudController(object): @defer.inlineCallbacks def associate_address(self, context, instance_id, public_ip, **kwargs): instance_ref = db.instance_get_by_str(context, instance_id) - fixed_ip_ref = db.fixed_ip_get_by_instance(context, instance_ref['id']) + fixed_address = db.instance_get_fixed_address(context, + instance_ref['id']) floating_ip_ref = db.floating_ip_get_by_address(context, public_ip) network_topic = yield self._get_network_topic(context) rpc.cast(network_topic, {"method": "associate_floating_ip", "args": {"context": None, "floating_address": floating_ip_ref['str_id'], - "fixed_address": fixed_ip_ref['str_id']}}) + "fixed_address": fixed_address}}) defer.returnValue({'associateResponse': ["Address associated."]}) @rbac.allow('netadmin') -- cgit From 53bba81d1e9774eefadd4f0f2b25638838a7ad07 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 22:54:47 -0700 Subject: don't allocate the same floating ip multiple times --- nova/db/sqlalchemy/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 948bca224..cc496e558 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -80,6 +80,7 @@ def floating_ip_allocate_address(_context, host, project_id): floating_ip_ref = session.query(models.FloatingIp ).filter_by(host=host ).filter_by(fixed_ip_id=None + ).filter_by(project_id=None ).filter_by(deleted=False ).with_lockmode('update' ).first() -- cgit From e45a5dd2cbcfe5d43cc59c6a20e3d065d43ee161 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 23:32:03 -0700 Subject: speed up generation of dhcp_hosts and don't run into None errors if instance is deleted --- nova/db/sqlalchemy/api.py | 1 + nova/network/linux_net.py | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index cc496e558..4cc086006 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -437,6 +437,7 @@ def network_get(_context, network_id): def network_get_associated_fixed_ips(_context, network_id): session = get_session() return session.query(models.FixedIp + ).options(joinedload_all('instance') ).filter_by(network_id=network_id ).filter(models.FixedIp.instance_id != None ).filter_by(deleted=False diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 41aeb5da7..621ae54ea 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -132,8 +132,9 @@ def ensure_bridge(bridge, interface, net_attrs=None): def get_dhcp_hosts(context, network_id): """Get a string containing a network's hosts config in dnsmasq format""" hosts = [] - for fixed_ip in db.network_get_associated_fixed_ips(context, network_id): - hosts.append(_host_dhcp(fixed_ip['str_id'])) + for fixed_ip_ref in db.network_get_associated_fixed_ips(context, + network_id): + hosts.append(_host_dhcp(fixed_ip_ref)) return '\n'.join(hosts) @@ -171,12 +172,12 @@ def update_dhcp(context, network_id): _execute(command, addl_env=env) -def _host_dhcp(address): +def _host_dhcp(fixed_ip_ref): """Return a host string for an address""" - instance_ref = db.fixed_ip_get_instance(None, address) + instance_ref = fixed_ip_ref['instance'] return "%s,%s.novalocal,%s" % (instance_ref['mac_address'], instance_ref['hostname'], - address) + fixed_ip_ref['str_id']) def _execute(cmd, *args, **kwargs): -- cgit From 85876fc1fa3dc7cb289e59712ab5a2b20877fc58 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 23:34:32 -0700 Subject: disassociate floating is supposed to take floating_address --- nova/endpoint/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 41f23618f..5fff72642 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -605,7 +605,7 @@ class CloudController(object): rpc.cast(network_topic, {"method": "disassociate_floating_ip", "args": {"context": None, - "address": address}}) + "floating_address": address}}) address = db.instance_get_fixed_address(context, instance_ref['id']) -- cgit From be1b1e320c17630430cfa567d8685f8cfc5773e4 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 11 Sep 2010 23:51:28 -0700 Subject: make fixed_ip_get_by_address return the instance as well so we don't run into concurrency issues where it is disassociated in between --- nova/db/sqlalchemy/api.py | 16 ++++++++++++++-- nova/network/manager.py | 4 ++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 4cc086006..3d3b766fc 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -19,13 +19,15 @@ Implementation of SQLAlchemy backend """ +import sys + from nova import db from nova import exception from nova import flags from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ -from sqlalchemy.orm import joinedload_all +from sqlalchemy.orm import exc, joinedload_all FLAGS = flags.FLAGS @@ -243,7 +245,17 @@ def fixed_ip_disassociate(_context, address): def fixed_ip_get_by_address(_context, address): - return models.FixedIp.find_by_str(address) + session = get_session() + with session.begin(): + try: + return session.query(models.FixedIp + ).options(joinedload_all('instance') + ).filter_by(address=address + ).filter_by(deleted=False + ).one() + except exc.NoResultFound: + new_exc = exception.NotFound("No model for address %s" % address) + raise new_exc.__class__, new_exc, sys.exc_info()[2] def fixed_ip_get_instance(_context, address): diff --git a/nova/network/manager.py b/nova/network/manager.py index 7a3bcfc2f..9564a3e33 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -262,7 +262,7 @@ class VlanManager(NetworkManager): if not fixed_ip_ref['allocated']: logging.warn("IP %s leased that was already deallocated", address) return - instance_ref = self.db.fixed_ip_get_instance(context, address) + instance_ref = fixed_ip_ref['instance'] if not instance_ref: raise exception.Error("IP %s leased that isn't associated" % address) @@ -280,7 +280,7 @@ class VlanManager(NetworkManager): if not fixed_ip_ref['leased']: logging.warn("IP %s released that was not leased", address) return - instance_ref = self.db.fixed_ip_get_instance(context, address) + instance_ref = fixed_ip_ref['instance'] if not instance_ref: raise exception.Error("IP %s released that isn't associated" % address) -- cgit From e76fe31644ab616dbde14e1b2063ab8419410404 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 12 Sep 2010 16:02:22 -0700 Subject: Periodic callback for services and managers. Added code to automatically disassociate stale ip addresses --- nova/db/api.py | 5 +++++ nova/db/sqlalchemy/api.py | 14 ++++++++++++++ nova/manager.py | 6 ++++++ nova/network/manager.py | 37 +++++++++++++++++++++++++------------ nova/service.py | 24 ++++++++++++++++++++---- nova/tests/service_unittest.py | 9 +++++++-- 6 files changed, 77 insertions(+), 18 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index 1529888ba..43f50555f 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -170,6 +170,11 @@ def fixed_ip_disassociate(context, address): return IMPL.fixed_ip_disassociate(context, address) +def fixed_ip_disassociate_all_by_timeout(context, host, time): + """Disassociate old fixed ips from host""" + return IMPL.fixed_ip_disassociate_all_by_timeout(context, host, time) + + def fixed_ip_get_by_address(context, address): """Get a fixed ip by address or raise if it does not exist.""" return IMPL.fixed_ip_get_by_address(context, address) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 3d3b766fc..fa2981899 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -244,6 +244,20 @@ def fixed_ip_disassociate(_context, address): fixed_ip_ref.save(session=session) +def fixed_ip_disassociate_all_by_timeout(_context, host, time): + session = get_session() + result = session.execute('update fixed_ips set instance_id = NULL ' + 'WHERE id IN (SELECT fixed_ips.id FROM fixed_ips ' + 'INNER JOIN networks ' + 'ON fixed_ips.network_id = ' + 'networks.id ' + 'WHERE host = :host) ' + 'AND updated_at < :time ' + 'AND instance_id IS NOT NULL', + {'host': host, + 'time': time.isoformat()}) + return result.rowcount + def fixed_ip_get_by_address(_context, address): session = get_session() with session.begin(): diff --git a/nova/manager.py b/nova/manager.py index e9aa50c56..0694907bd 100644 --- a/nova/manager.py +++ b/nova/manager.py @@ -22,6 +22,7 @@ Base class for managers of different parts of the system from nova import utils from nova import flags +from twisted.internet import defer FLAGS = flags.FLAGS flags.DEFINE_string('db_driver', 'nova.db.api', @@ -37,3 +38,8 @@ class Manager(object): if not db_driver: db_driver = FLAGS.db_driver self.db = utils.import_object(db_driver) # pylint: disable-msg=C0103 + + @defer.inlineCallbacks + def periodic_tasks(self, context=None): + """Tasks to be run at a periodic interval""" + yield diff --git a/nova/network/manager.py b/nova/network/manager.py index 9564a3e33..b3541aacc 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -20,10 +20,12 @@ Network Hosts are responsible for allocating ips and setting up network """ +import datetime import logging import math import IPy +from twisted.internet import defer from nova import db from nova import exception @@ -62,7 +64,9 @@ flags.DEFINE_integer('cnt_vpn_clients', 5, flags.DEFINE_string('network_driver', 'nova.network.linux_net', 'Driver to use for network creation') flags.DEFINE_bool('update_dhcp_on_disassociate', False, - 'Whether to update dhcp when fixed_ip is disassocated') + 'Whether to update dhcp when fixed_ip is disassociated') +flags.DEFINE_integer('fixed_ip_disassociate_timeout', 600, + 'Seconds after which a deallocated ip is disassociated') class AddressAlreadyAllocated(exception.Error): @@ -90,7 +94,7 @@ class NetworkManager(manager.Manager): network_id = network_ref['id'] host = self.db.network_set_host(context, network_id, - FLAGS.host) + self.host) self._on_set_network_host(context, network_id) return host @@ -118,7 +122,7 @@ class NetworkManager(manager.Manager): """Gets an floating ip from the pool""" # TODO(vish): add floating ips through manage command return self.db.floating_ip_allocate_address(context, - FLAGS.host, + self.host, project_id) def associate_floating_ip(self, context, floating_address, fixed_address): @@ -218,6 +222,21 @@ class FlatManager(NetworkManager): class VlanManager(NetworkManager): """Vlan network with dhcp""" + + @defer.inlineCallbacks + def periodic_tasks(self, context=None): + """Tasks to be run at a periodic interval""" + yield super(VlanManager, self).periodic_tasks(context) + now = datetime.datetime.utcnow() + timeout = FLAGS.fixed_ip_disassociate_timeout + time = now - datetime.timedelta(seconds=timeout) + num = self.db.fixed_ip_disassociate_all_by_timeout(self, + self.host, + time) + if num: + logging.debug("Dissassociated %s stale fixed ip(s)", num) + + def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): """Gets a fixed ip from the pool""" network_ref = self.db.project_get_network(context, context.project.id) @@ -235,14 +254,6 @@ class VlanManager(NetworkManager): """Returns a fixed ip to the pool""" self.db.fixed_ip_update(context, address, {'allocated': False}) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) - if not fixed_ip_ref['leased']: - self.db.fixed_ip_disassociate(context, address) - # NOTE(vish): dhcp server isn't updated until next setup, this - # means there will stale entries in the conf file - # the code below will update the file if necessary - if FLAGS.update_dhcp_on_disassociate: - network_ref = self.db.fixed_ip_get_network(context, address) - self.driver.update_dhcp(context, network_ref['id']) def setup_fixed_ip(self, context, address): @@ -287,7 +298,9 @@ class VlanManager(NetworkManager): if instance_ref['mac_address'] != mac: raise exception.Error("IP %s released from bad mac %s vs %s" % (address, instance_ref['mac_address'], mac)) - self.db.fixed_ip_update(context, address, {'leased': False}) + self.db.fixed_ip_update(context, + fixed_ip_ref['str_id'], + {'leased': False}) if not fixed_ip_ref['allocated']: self.db.fixed_ip_disassociate(context, address) # NOTE(vish): dhcp server isn't updated until next setup, this diff --git a/nova/service.py b/nova/service.py index 870dd6ceb..df450025b 100644 --- a/nova/service.py +++ b/nova/service.py @@ -37,7 +37,11 @@ from nova import utils FLAGS = flags.FLAGS flags.DEFINE_integer('report_interval', 10, - 'seconds between nodes reporting state to cloud', + 'seconds between nodes reporting state to datastore', + lower_bound=1) + +flags.DEFINE_integer('periodic_interval', 60, + 'seconds between running periodic tasks', lower_bound=1) @@ -80,7 +84,8 @@ class Service(object, service.Service): binary=None, topic=None, manager=None, - report_interval=None): + report_interval=None, + periodic_interval=None): """Instantiates class and passes back application object. Args: @@ -89,6 +94,7 @@ class Service(object, service.Service): topic, defaults to bin_name - "nova-" part manager, defaults to FLAGS._manager report_interval, defaults to FLAGS.report_interval + periodic_interval, defaults to FLAGS.periodic_interval """ if not host: host = FLAGS.host @@ -100,6 +106,8 @@ class Service(object, service.Service): manager = FLAGS.get('%s_manager' % topic, None) if not report_interval: report_interval = FLAGS.report_interval + if not periodic_interval: + periodic_interval = FLAGS.periodic_interval logging.warn("Starting %s node", topic) service_obj = cls(host, binary, topic, manager) conn = rpc.Connection.instance() @@ -112,11 +120,14 @@ class Service(object, service.Service): topic='%s.%s' % (topic, host), proxy=service_obj) + consumer_all.attach_to_twisted() + consumer_node.attach_to_twisted() + pulse = task.LoopingCall(service_obj.report_state) pulse.start(interval=report_interval, now=False) - consumer_all.attach_to_twisted() - consumer_node.attach_to_twisted() + pulse = task.LoopingCall(service_obj.periodic_tasks) + pulse.start(interval=periodic_interval, now=False) # This is the parent service that twistd will be looking for when it # parses this file, return it so that we can get it into globals. @@ -131,6 +142,11 @@ class Service(object, service.Service): except exception.NotFound: logging.warn("Service killed that has no database entry") + @defer.inlineCallbacks + def periodic_tasks(self, context=None): + """Tasks to be run at a periodic interval""" + yield self.manager.periodic_tasks(context) + @defer.inlineCallbacks def report_state(self, context=None): """Update the state of this service in the datastore.""" diff --git a/nova/tests/service_unittest.py b/nova/tests/service_unittest.py index 01da0eb8a..06f80e82c 100644 --- a/nova/tests/service_unittest.py +++ b/nova/tests/service_unittest.py @@ -65,15 +65,20 @@ class ServiceTestCase(test.BaseTestCase): proxy=mox.IsA(service.Service)).AndReturn( rpc.AdapterConsumer) + rpc.AdapterConsumer.attach_to_twisted() + rpc.AdapterConsumer.attach_to_twisted() + # Stub out looping call a bit needlessly since we don't have an easy # way to cancel it (yet) when the tests finishes service.task.LoopingCall(mox.IgnoreArg()).AndReturn( service.task.LoopingCall) service.task.LoopingCall.start(interval=mox.IgnoreArg(), now=mox.IgnoreArg()) + service.task.LoopingCall(mox.IgnoreArg()).AndReturn( + service.task.LoopingCall) + service.task.LoopingCall.start(interval=mox.IgnoreArg(), + now=mox.IgnoreArg()) - rpc.AdapterConsumer.attach_to_twisted() - rpc.AdapterConsumer.attach_to_twisted() service_create = {'host': host, 'binary': binary, 'topic': topic, -- cgit From efeb5243ffd5e588748d8786ac82a04e302f0612 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 12 Sep 2010 17:24:06 -0700 Subject: workaround for mysql select in update --- nova/db/sqlalchemy/api.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index fa2981899..aa8670253 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -246,12 +246,19 @@ def fixed_ip_disassociate(_context, address): def fixed_ip_disassociate_all_by_timeout(_context, host, time): session = get_session() - result = session.execute('update fixed_ips set instance_id = NULL ' - 'WHERE id IN (SELECT fixed_ips.id FROM fixed_ips ' - 'INNER JOIN networks ' - 'ON fixed_ips.network_id = ' - 'networks.id ' - 'WHERE host = :host) ' + # NOTE(vish): The annoying nested select here is because SQLite doesn't + # support JOINs in UPDATEs and Mysql doesn't support SELECT + # from the same table you are updating without using a temp + # table. It would be great if we can coax sqlalchemy into + # generating this update for us without having to update + # each fixed_ip individually. + result = session.execute('UPDATE fixed_ips SET instance_id = NULL ' + 'WHERE id IN (SELECT x.id FROM ' + '(SELECT fixed_ips.id FROM fixed_ips ' + 'INNER JOIN networks ' + 'ON fixed_ips.network_id = ' + 'networks.id ' + 'WHERE host = :host) as x) ' 'AND updated_at < :time ' 'AND instance_id IS NOT NULL', {'host': host, -- cgit From cb2a28d82210e43a221a8fa8f2a03a7dbd6e8779 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 12 Sep 2010 17:56:28 -0700 Subject: speed up the query and make sure allocated is false --- nova/db/sqlalchemy/api.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index aa8670253..ca5e12f95 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -258,9 +258,10 @@ def fixed_ip_disassociate_all_by_timeout(_context, host, time): 'INNER JOIN networks ' 'ON fixed_ips.network_id = ' 'networks.id ' - 'WHERE host = :host) as x) ' - 'AND updated_at < :time ' - 'AND instance_id IS NOT NULL', + 'WHERE networks.host = :host ' + 'AND fixed_ip.updated_at < :time ' + 'AND fixed_ip.instance_id IS NOT NULL' + 'AND fixed_ip.allocated = 0) as x) ', {'host': host, 'time': time.isoformat()}) return result.rowcount -- cgit From 3f9118b2ae025cd707642b179fe58459dd39dbc3 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 12 Sep 2010 18:06:59 -0700 Subject: set leased = 0 as well on disassociate update --- nova/db/sqlalchemy/api.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ca5e12f95..b4b68fa02 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -252,16 +252,17 @@ def fixed_ip_disassociate_all_by_timeout(_context, host, time): # table. It would be great if we can coax sqlalchemy into # generating this update for us without having to update # each fixed_ip individually. - result = session.execute('UPDATE fixed_ips SET instance_id = NULL ' + result = session.execute('UPDATE fixed_ips SET instance_id = NULL, ' + 'leased = 0 ' 'WHERE id IN (SELECT x.id FROM ' - '(SELECT fixed_ips.id FROM fixed_ips ' - 'INNER JOIN networks ' - 'ON fixed_ips.network_id = ' - 'networks.id ' - 'WHERE networks.host = :host ' - 'AND fixed_ip.updated_at < :time ' - 'AND fixed_ip.instance_id IS NOT NULL' - 'AND fixed_ip.allocated = 0) as x) ', + '(SELECT fixed_ips.id FROM fixed_ips ' + 'INNER JOIN networks ' + 'ON fixed_ips.network_id = ' + 'networks.id ' + 'WHERE networks.host = :host ' + 'AND fixed_ip.updated_at < :time ' + 'AND fixed_ip.instance_id IS NOT NULL' + 'AND fixed_ip.allocated = 0) as x) ', {'host': host, 'time': time.isoformat()}) return result.rowcount -- cgit From 8e5a2ab3614da2676a3627cf62fedfedf7f3585c Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 12 Sep 2010 18:16:52 -0700 Subject: missed a space --- nova/db/sqlalchemy/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index b4b68fa02..5ec477666 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -261,7 +261,7 @@ def fixed_ip_disassociate_all_by_timeout(_context, host, time): 'networks.id ' 'WHERE networks.host = :host ' 'AND fixed_ip.updated_at < :time ' - 'AND fixed_ip.instance_id IS NOT NULL' + 'AND fixed_ip.instance_id IS NOT NULL ' 'AND fixed_ip.allocated = 0) as x) ', {'host': host, 'time': time.isoformat()}) -- cgit From dbb3358f851c245ccb5ea9a9c7ace636b4e73a80 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 12 Sep 2010 18:25:15 -0700 Subject: simplified query --- nova/db/sqlalchemy/api.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 5ec477666..18a43cad4 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -246,23 +246,15 @@ def fixed_ip_disassociate(_context, address): def fixed_ip_disassociate_all_by_timeout(_context, host, time): session = get_session() - # NOTE(vish): The annoying nested select here is because SQLite doesn't - # support JOINs in UPDATEs and Mysql doesn't support SELECT - # from the same table you are updating without using a temp - # table. It would be great if we can coax sqlalchemy into - # generating this update for us without having to update - # each fixed_ip individually. + # NOTE(vish): The nested select is because sqlite doesn't support + # JOINs in UPDATEs. result = session.execute('UPDATE fixed_ips SET instance_id = NULL, ' 'leased = 0 ' - 'WHERE id IN (SELECT x.id FROM ' - '(SELECT fixed_ips.id FROM fixed_ips ' - 'INNER JOIN networks ' - 'ON fixed_ips.network_id = ' - 'networks.id ' - 'WHERE networks.host = :host ' - 'AND fixed_ip.updated_at < :time ' - 'AND fixed_ip.instance_id IS NOT NULL ' - 'AND fixed_ip.allocated = 0) as x) ', + 'WHERE network_id IN (SELECT id FROM networks ' + 'WHERE host = :host) ' + 'AND updated_at < :time ' + 'AND instance_id IS NOT NULL ' + 'AND allocated = 0', {'host': host, 'time': time.isoformat()}) return result.rowcount -- cgit From 892e55724e8265865a1b298e478d98acd42c68d7 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 12 Sep 2010 22:12:24 -0700 Subject: move the warnings about leasing ips --- nova/network/manager.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index b3541aacc..b4c4a53b4 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -270,9 +270,6 @@ class VlanManager(NetworkManager): """Called by dhcp-bridge when ip is leased""" logging.debug("Leasing IP %s", address) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) - if not fixed_ip_ref['allocated']: - logging.warn("IP %s leased that was already deallocated", address) - return instance_ref = fixed_ip_ref['instance'] if not instance_ref: raise exception.Error("IP %s leased that isn't associated" % @@ -283,14 +280,13 @@ class VlanManager(NetworkManager): self.db.fixed_ip_update(context, fixed_ip_ref['str_id'], {'leased': True}) + if not fixed_ip_ref['allocated']: + logging.warn("IP %s leased that was already deallocated", address) def release_fixed_ip(self, context, mac, address): """Called by dhcp-bridge when ip is released""" logging.debug("Releasing IP %s", address) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) - if not fixed_ip_ref['leased']: - logging.warn("IP %s released that was not leased", address) - return instance_ref = fixed_ip_ref['instance'] if not instance_ref: raise exception.Error("IP %s released that isn't associated" % @@ -298,6 +294,8 @@ class VlanManager(NetworkManager): if instance_ref['mac_address'] != mac: raise exception.Error("IP %s released from bad mac %s vs %s" % (address, instance_ref['mac_address'], mac)) + if not fixed_ip_ref['leased']: + logging.warn("IP %s released that was not leased", address) self.db.fixed_ip_update(context, fixed_ip_ref['str_id'], {'leased': False}) -- cgit From 6cbf8b736cc2c9929c2ad69ddc8e8b4fc2d0f4ae Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 12 Sep 2010 23:09:15 -0700 Subject: removed second copy of ProcessExecutionError --- nova/utils.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/nova/utils.py b/nova/utils.py index 8939043e6..d18dd9843 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -39,17 +39,6 @@ from nova.exception import ProcessExecutionError FLAGS = flags.FLAGS TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" -class ProcessExecutionError(IOError): - def __init__( self, stdout=None, stderr=None, exit_code=None, cmd=None, - description=None): - if description is None: - description = "Unexpected error while running command." - if exit_code is None: - exit_code = '-' - message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % ( - description, cmd, exit_code, stdout, stderr) - IOError.__init__(self, message) - def import_class(import_str): """Returns a class from a string including module and class""" mod_str, _sep, class_str = import_str.rpartition('.') -- cgit From 86cd30b749e6da78d4ceb6c77f2116975429a81a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 13 Sep 2010 01:15:35 -0700 Subject: renamed _get_quota to get_quota and moved int(size) into quota.py --- bin/nova-manage | 2 +- nova/endpoint/cloud.py | 1 - nova/quota.py | 9 +++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index c7ef33a48..824e00ac5 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -273,7 +273,7 @@ class ProjectCommands(object): db.quota_update(None, project_id, quo) except exception.NotFound: db.quota_create(None, quo) - project_quota = quota._get_quota(None, project_id) + project_quota = quota.get_quota(None, project_id) for key, value in project_quota.iteritems(): print '%s: %s' % (key, value) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 749bf5f9c..1618b784b 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -284,7 +284,6 @@ class CloudController(object): @rbac.allow('projectmanager', 'sysadmin') def create_volume(self, context, size, **kwargs): # check quota - size = int(size) if quota.allowed_volumes(context, 1, size) < 1: logging.warn("Quota exceeeded for %s, tried to create %sG volume", context.project.id, size) diff --git a/nova/quota.py b/nova/quota.py index f0e51feeb..edbb83111 100644 --- a/nova/quota.py +++ b/nova/quota.py @@ -37,7 +37,7 @@ flags.DEFINE_integer('quota_gigabytes', 1000, flags.DEFINE_integer('quota_floating_ips', 10, 'number of floating ips allowed per project') -def _get_quota(context, project_id): +def get_quota(context, project_id): rval = {'instances': FLAGS.quota_instances, 'cores': FLAGS.quota_cores, 'volumes': FLAGS.quota_volumes, @@ -57,7 +57,7 @@ def allowed_instances(context, num_instances, instance_type): project_id = context.project.id used_instances, used_cores = db.instance_data_get_for_project(context, project_id) - quota = _get_quota(context, project_id) + quota = get_quota(context, project_id) allowed_instances = quota['instances'] - used_instances allowed_cores = quota['cores'] - used_cores type_cores = instance_types.INSTANCE_TYPES[instance_type]['vcpus'] @@ -72,9 +72,10 @@ def allowed_volumes(context, num_volumes, size): project_id = context.project.id used_volumes, used_gigabytes = db.volume_data_get_for_project(context, project_id) - quota = _get_quota(context, project_id) + quota = get_quota(context, project_id) allowed_volumes = quota['volumes'] - used_volumes allowed_gigabytes = quota['gigabytes'] - used_gigabytes + size = int(size) num_gigabytes = num_volumes * size allowed_volumes = min(allowed_volumes, int(allowed_gigabytes // size)) @@ -85,7 +86,7 @@ def allowed_floating_ips(context, num_floating_ips): """Check quota and return min(num_floating_ips, allowed_floating_ips)""" project_id = context.project.id used_floating_ips = db.floating_ip_count_by_project(context, project_id) - quota = _get_quota(context, project_id) + quota = get_quota(context, project_id) allowed_floating_ips = quota['floating_ips'] - used_floating_ips return min(num_floating_ips, allowed_floating_ips) -- cgit From 970654239267fc702f767bbaff3e22207576d0cd Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 13 Sep 2010 01:58:40 -0700 Subject: multiple network controllers will not create duplicate indexes --- nova/db/api.py | 9 ++++++--- nova/db/sqlalchemy/api.py | 8 ++++++-- nova/db/sqlalchemy/models.py | 2 +- nova/network/manager.py | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index 9f6ff99c3..a775dc301 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -365,9 +365,12 @@ def network_index_count(context): return IMPL.network_index_count(context) -def network_index_create(context, values): - """Create a network index from the values dict""" - return IMPL.network_index_create(context, values) +def network_index_create_safe(context, values): + """Create a network index from the values dict + + The index is not returned. If the create violates the unique + constraints because the index already exists, no exception is raised.""" + return IMPL.network_index_create_safe(context, values) def network_set_cidr(context, network_id, cidr): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index d612fe669..35585b1b0 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -25,6 +25,7 @@ from nova import flags from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import joinedload_all from sqlalchemy.sql import func @@ -567,11 +568,14 @@ def network_index_count(_context): return models.NetworkIndex.count() -def network_index_create(_context, values): +def network_index_create_safe(_context, values): network_index_ref = models.NetworkIndex() for (key, value) in values.iteritems(): network_index_ref[key] = value - network_index_ref.save() + try: + network_index_ref.save() + except IntegrityError: + pass def network_set_host(_context, network_id, host_id): diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 41013f41b..6ff0decb5 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -355,7 +355,7 @@ class NetworkIndex(BASE, NovaBase): """ __tablename__ = 'network_indexes' id = Column(Integer, primary_key=True) - index = Column(Integer) + index = Column(Integer, unique=True) network_id = Column(Integer, ForeignKey('networks.id'), nullable=True) network = relationship(Network, backref=backref('network_index', uselist=False)) diff --git a/nova/network/manager.py b/nova/network/manager.py index 191c1d364..570d5f8d8 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -343,7 +343,7 @@ class VlanManager(NetworkManager): This could use a manage command instead of keying off of a flag""" if not self.db.network_index_count(context): for index in range(FLAGS.num_networks): - self.db.network_index_create(context, {'index': index}) + self.db.network_index_create_safe(context, {'index': index}) def _on_set_network_host(self, context, network_id): """Called when this host becomes the host for a project""" -- cgit From 2774466197a0dda3763569fe7aa1a578baf5e059 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 13 Sep 2010 02:15:02 -0700 Subject: added missing yield in detach_volume --- nova/compute/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 954227b42..24538e4f1 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -189,7 +189,7 @@ class ComputeManager(manager.Manager): volume_id) instance_ref = self.db.instance_get(context, instance_id) volume_ref = self.db.volume_get(context, volume_id) - self.driver.detach_volume(instance_ref['str_id'], - volume_ref['mountpoint']) + yield self.driver.detach_volume(instance_ref['str_id'], + volume_ref['mountpoint']) self.db.volume_detached(context, volume_id) defer.returnValue(True) -- cgit From 5e02ee47c0e86986bb21f67a4d6556895de5d0ef Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 13 Sep 2010 11:53:53 -0400 Subject: Pull S3ImageService out of this mergeprop --- nova/api/ec2/images.py | 34 ++++++++++++++++++++++++++------- nova/image/service.py | 52 -------------------------------------------------- 2 files changed, 27 insertions(+), 59 deletions(-) diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py index f0be7b899..b5ce2b2cc 100644 --- a/nova/api/ec2/images.py +++ b/nova/api/ec2/images.py @@ -29,17 +29,16 @@ import boto.s3.connection from nova import flags from nova import utils from nova.auth import manager -from nova.image import service FLAGS = flags.FLAGS def modify(context, image_id, operation): - service.S3ImageService(context)._conn().make_request( + conn(context).make_request( method='POST', bucket='_images', - query_args=service.qs({'image_id': image_id, 'operation': operation})) + query_args=qs({'image_id': image_id, 'operation': operation})) return True @@ -48,10 +47,10 @@ def register(context, image_location): """ rpc call to register a new image based from a manifest """ image_id = utils.generate_uid('ami') - service.S3ImageService(context)._conn().make_request( + conn(context).make_request( method='PUT', bucket='_images', - query_args=service.qs({'image_location': image_location, + query_args=qs({'image_location': image_location, 'image_id': image_id})) return image_id @@ -62,7 +61,12 @@ def list(context, filter_list=[]): optionally filtered by a list of image_id """ - result = service.S3ImageService(context).index().values() + # FIXME: send along the list of only_images to check for + response = conn(context).make_request( + method='GET', + bucket='_images') + + result = json.loads(response.read()) if not filter_list is None: return [i for i in result if i['imageId'] in filter_list] return result @@ -70,4 +74,20 @@ def list(context, filter_list=[]): def deregister(context, image_id): """ unregister an image """ - service.S3ImageService(context).delete(image_id) + conn(context).make_request( + method='DELETE', + bucket='_images', + query_args=qs({'image_id': image_id})) + + +def conn(context): + access = manager.AuthManager().get_access_key(context.user, + context.project) + secret = str(context.user.secret) + calling = boto.s3.connection.OrdinaryCallingFormat() + return boto.s3.connection.S3Connection(aws_access_key_id=access, + aws_secret_access_key=secret, + is_secure=False, + calling_format=calling, + port=FLAGS.s3_port, + host=FLAGS.s3_host) diff --git a/nova/image/service.py b/nova/image/service.py index f6719caec..1a7a258b7 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -38,8 +38,6 @@ class ImageService(object): def show(self, id): """ Returns a dict containing image data for the given opaque image id. - - Returns None if the id does not exist. """ @@ -90,53 +88,3 @@ class LocalImageService(ImageService): Delete the given image. Raises OSError if the image does not exist. """ os.unlink(self._path_to(image_id)) - - -# TODO(gundlach): before this can be loaded dynamically in ImageService.load(), -# we'll have to make __init__() not require a context. Right now it -# is only used by the AWS API, which hard-codes it, so that's OK. -class S3ImageService(ImageService): - """Service that stores images in an S3 provider.""" - - def __init__(self, context): - self._context = context - - def index(self): - response = self._conn().make_request( - method='GET', - bucket='_images') - items = json.loads(response.read()) - return dict((item['imageId'], item) for item in items) - - def show(self, id): - response = self._conn().make_request( - method='GET', - bucket='_images', - query_args=qs({'image_id': image_id})) - return json.loads(response.read()) - - def delete(self, image_id): - self._conn().make_request( - method='DELETE', - bucket='_images', - query_args=qs({'image_id': image_id})) - - def _conn(self): - """Return a boto S3Connection to the S3 store.""" - access = manager.AuthManager().get_access_key(self._context.user, - self._context.project) - secret = str(self._context.user.secret) - calling = boto.s3.connection.OrdinaryCallingFormat() - return boto.s3.connection.S3Connection(aws_access_key_id=access, - aws_secret_access_key=secret, - is_secure=False, - calling_format=calling, - port=FLAGS.s3_port, - host=FLAGS.s3_host) - - -def qs(params): - pairs = [] - for key in params.keys(): - pairs.append(key + '=' + urllib.quote(params[key])) - return '&'.join(pairs) -- cgit From 2b87ea1ab445a5a9fb089acb0220189f736d420a Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 13 Sep 2010 12:02:50 -0400 Subject: Finish pulling S3ImageService out of this mergeprop --- nova/api/ec2/images.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py index b5ce2b2cc..2a88d66af 100644 --- a/nova/api/ec2/images.py +++ b/nova/api/ec2/images.py @@ -91,3 +91,10 @@ def conn(context): calling_format=calling, port=FLAGS.s3_port, host=FLAGS.s3_host) + + +def qs(params): + pairs = [] + for key in params.keys(): + pairs.append(key + '=' + urllib.quote(params[key])) + return '&'.join(pairs) -- cgit From 0936150221713d775a5cad4a2e978980c32b21c1 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Mon, 13 Sep 2010 23:56:32 -0700 Subject: now we can run files - thanks vish --- bin/nova-manage | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/nova-manage b/bin/nova-manage index 6e5266767..26f75faf7 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -156,6 +156,11 @@ class ShellCommands(object): readline.parse_and_bind("tab:complete") code.interact() + def script(self, path): + """Runs the script from the specifed path with flags set properly. + arguments: path""" + exec(compile(open(path).read(), path, 'exec'), locals(), globals()) + class RoleCommands(object): """Class for managing roles.""" -- cgit From 3d68f1f74cd7fe6ddb9eec003a9e31f8ad036b27 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 14 Sep 2010 16:26:19 -0400 Subject: Add ratelimiting package into Nova. After Austin it'll be pulled out into PyPI. --- nova/api/rackspace/ratelimiting/__init__.py | 103 ++++++++++++++++++++++++++++ nova/api/rackspace/ratelimiting/tests.py | 60 ++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 nova/api/rackspace/ratelimiting/__init__.py create mode 100644 nova/api/rackspace/ratelimiting/tests.py diff --git a/nova/api/rackspace/ratelimiting/__init__.py b/nova/api/rackspace/ratelimiting/__init__.py new file mode 100644 index 000000000..176e7d66e --- /dev/null +++ b/nova/api/rackspace/ratelimiting/__init__.py @@ -0,0 +1,103 @@ +"""Rate limiting of arbitrary actions.""" + +import time +import urllib +import webob.dec +import webob.exc + + +# Convenience constants for the limits dictionary passed to Limiter(). +PER_SECOND = 1 +PER_MINUTE = 60 +PER_HOUR = 60 * 60 +PER_DAY = 60 * 60 * 24 + +class Limiter(object): + + """Class providing rate limiting of arbitrary actions.""" + + def __init__(self, limits): + """Create a rate limiter. + + limits: a dict mapping from action name to a tuple. The tuple contains + the number of times the action may be performed, and the time period + (in seconds) during which the number must not be exceeded for this + action. Example: dict(reboot=(10, ratelimiting.PER_MINUTE)) would + allow 10 'reboot' actions per minute. + """ + self.limits = limits + self._levels = {} + + def perform(self, action_name, username='nobody'): + """Attempt to perform an action by the given username. + + action_name: the string name of the action to perform. This must + be a key in the limits dict passed to the ctor. + + username: an optional string name of the user performing the action. + Each user has her own set of rate limiting counters. Defaults to + 'nobody' (so that if you never specify a username when calling + perform(), a single set of counters will be used.) + + Return None if the action may proceed. If the action may not proceed + because it has been rate limited, return the float number of seconds + until the action would succeed. + """ + # Think of rate limiting as a bucket leaking water at 1cc/second. The + # bucket can hold as many ccs as there are seconds in the rate + # limiting period (e.g. 3600 for per-hour ratelimits), and if you can + # perform N actions in that time, each action fills the bucket by + # 1/Nth of its volume. You may only perform an action if the bucket + # would not overflow. + now = time.time() + key = '%s:%s' % (username, action_name) + last_time_performed, water_level = self._levels.get(key, (now, 0)) + # The bucket leaks 1cc/second. + water_level -= (now - last_time_performed) + if water_level < 0: + water_level = 0 + num_allowed_per_period, period_in_secs = self.limits[action_name] + # Fill the bucket by 1/Nth its capacity, and hope it doesn't overflow. + capacity = period_in_secs + new_level = water_level + (capacity * 1.0 / num_allowed_per_period) + if new_level > capacity: + # Delay this many seconds. + return new_level - capacity + self._levels[key] = (now, new_level) + return None + + +# If one instance of this WSGIApps is unable to handle your load, put a +# sharding app in front that shards by username to one of many backends. + +class WSGIApp(object): + + """Application that tracks rate limits in memory. Send requests to it of + this form: + + POST /limiter// + + and receive a 200 OK, or a 403 Forbidden with an X-Wait-Seconds header + containing the number of seconds to wait before the action would succeed. + """ + + def __init__(self, limiter): + """Create the WSGI application using the given Limiter instance.""" + self.limiter = limiter + + @webob.dec.wsgify + def __call__(req): + parts = req.path_info.split('/') + # format: /limiter// + if req.method != 'POST': + raise webob.exc.HTTPMethodNotAllowed() + if len(parts) != 4 or parts[1] != 'limiter': + raise webob.exc.HTTPNotFound() + username = parts[2] + action_name = urllib.unquote(parts[3]) + delay = self.limiter.perform(action_name, username) + if delay: + return webob.exc.HTTPForbidden( + headers={'X-Wait-Seconds': delay}) + else: + return '' # 200 OK diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py new file mode 100644 index 000000000..1983cdea8 --- /dev/null +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -0,0 +1,60 @@ +import ratelimiting +import time +import unittest + +class Test(unittest.TestCase): + + def setUp(self): + self.limits = { + 'a': (5, ratelimiting.PER_SECOND), + 'b': (5, ratelimiting.PER_MINUTE), + 'c': (5, ratelimiting.PER_HOUR), + 'd': (1, ratelimiting.PER_SECOND), + 'e': (100, ratelimiting.PER_SECOND)} + self.rl = ratelimiting.Limiter(self.limits) + + def exhaust(self, action, times_until_exhausted, **kwargs): + for i in range(times_until_exhausted): + when = self.rl.perform(action, **kwargs) + self.assertEqual(when, None) + num, period = self.limits[action] + delay = period * 1.0 / num + # Verify that we are now thoroughly delayed + for i in range(10): + when = self.rl.perform(action, **kwargs) + self.assertAlmostEqual(when, delay, 2) + + def test_second(self): + self.exhaust('a', 5) + time.sleep(0.2) + self.exhaust('a', 1) + time.sleep(1) + self.exhaust('a', 5) + + def test_minute(self): + self.exhaust('b', 5) + + def test_one_per_period(self): + def allow_once_and_deny_once(): + when = self.rl.perform('d') + self.assertEqual(when, None) + when = self.rl.perform('d') + self.assertAlmostEqual(when, 1, 2) + return when + time.sleep(allow_once_and_deny_once()) + time.sleep(allow_once_and_deny_once()) + allow_once_and_deny_once() + + def test_we_can_go_indefinitely_if_we_spread_out_requests(self): + for i in range(200): + when = self.rl.perform('e') + self.assertEqual(when, None) + time.sleep(0.01) + + def test_users_get_separate_buckets(self): + self.exhaust('c', 5, username='alice') + self.exhaust('c', 5, username='bob') + self.exhaust('c', 5, username='chuck') + self.exhaust('c', 0, username='chuck') + self.exhaust('c', 0, username='bob') + self.exhaust('c', 0, username='alice') -- cgit From 8138a35d3672e08640762b7533c1c527568d0b4f Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 14 Sep 2010 18:59:02 -0400 Subject: RateLimitingMiddleware --- nova/api/rackspace/__init__.py | 52 ++++++++++++++++++++++++++++++++- nova/tests/api/rackspace/__init__.py | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index b4d666d63..e35109b43 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -31,6 +31,7 @@ from nova import flags from nova import wsgi from nova.api.rackspace import flavors from nova.api.rackspace import images +from nova.api.rackspace import ratelimiting from nova.api.rackspace import servers from nova.api.rackspace import sharedipgroups from nova.auth import manager @@ -40,7 +41,7 @@ class API(wsgi.Middleware): """WSGI entry point for all Rackspace API requests.""" def __init__(self): - app = AuthMiddleware(APIRouter()) + app = AuthMiddleware(RateLimitingMiddleware(APIRouter())) super(API, self).__init__(app) @@ -65,6 +66,55 @@ class AuthMiddleware(wsgi.Middleware): return self.application +class RateLimitingMiddleware(wsgi.Middleware): + """Rate limit incoming requests according to the OpenStack rate limits.""" + + def __init__(self, application): + super(RateLimitingMiddleware, self).__init__(application) + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = ratelimiting.Limiter(limits={ + 'DELETE': (100, ratelimiting.PER_MINUTE), + 'PUT': (10, ratelimiting.PER_MINUTE), + 'POST': (10, ratelimiting.PER_MINUTE), + 'POST servers': (50, ratelimiting.PER_DAY), + 'GET changes-since': (3, ratelimiting.PER_MINUTE), + }) + + @webob.dec.wsgify + def __call__(self, req): + """Rate limit the request. + + If the request should be rate limited, return a 413 status with a + Retry-After header giving the time when the request would succeed. + """ + username = req.headers['X-Auth-User'] + action_name = self.get_action_name(req) + if not action_name: # not rate limited + return self.application + delay = self.limiter.perform(action_name, username=username) + if action_name == 'POST servers': + # "POST servers" is a POST, so it counts against "POST" too. + delay2 = self.limiter.perform('POST', username=username) + delay = max(delay or 0, delay2 or 0) + if delay: + # TODO(gundlach): Get the retry-after format correct. + raise webob.exc.HTTPRequestEntityTooLarge(headers={ + 'Retry-After': time.time() + delay}) + else: + return self.application + + def get_action_name(self, req): + """Return the action name for this request.""" + if req.method == 'GET' and 'changes-since' in req.GET: + return 'GET changes-since' + if req.method == 'POST' and req.path_info.starts_with('/servers'): + return 'POST servers' + if req.method in ['PUT', 'POST', 'DELETE']: + return req.method + return None + + class APIRouter(wsgi.Router): """ Routes requests on the Rackspace API to the appropriate controller diff --git a/nova/tests/api/rackspace/__init__.py b/nova/tests/api/rackspace/__init__.py index e69de29bb..f7537a4e7 100644 --- a/nova/tests/api/rackspace/__init__.py +++ b/nova/tests/api/rackspace/__init__.py @@ -0,0 +1,56 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +from nova.api.rackspace.ratelimiting import RateLimitingMiddleware +from nova.tests.api.test_helper import * +from webob import Request + +class RateLimitingMiddlewareTest(unittest.TestCase): + def setUp(self): + self.middleware = RateLimitingMiddleware(APIStub()) + self.stubs = stubout.StubOutForTesting() + + def tearDown(self): + self.stubs.UnsetAll() + + def test_get_action_name(self): + middleware = RateLimitingMiddleware(APIStub()) + def verify(method, url, action_name): + req = Request(url) + req.method = method + action = middleware.get_action_name(req) + self.assertEqual(action, action_name) + verify('PUT', '/servers/4', 'PUT') + verify('DELETE', '/servers/4', 'DELETE') + verify('POST', '/images/4', 'POST') + verify('POST', '/servers/4', 'POST servers') + verify('GET', '/foo?a=4&changes-since=never&b=5', 'GET changes-since') + verify('GET', '/foo?a=4&monkeys-since=never&b=5', None) + verify('GET', '/servers/4', None) + verify('HEAD', '/servers/4', None) + + def TODO_test_call(self): + pass + #mw = make_middleware() + #req = build_request('DELETE', '/servers/4') + #for i in range(5): + # resp = req.get_response(mw) + # assert resp is OK + #resp = req.get_response(mw) + #assert resp is rate limited -- cgit From 63ad073efd0b20f59f02bc37182c0180cac3f405 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 09:25:53 -0400 Subject: RateLimitingMiddleware tests --- nova/api/rackspace/__init__.py | 24 ++++++++++----- nova/api/rackspace/ratelimiting/tests.py | 3 ++ nova/tests/api/rackspace/__init__.py | 51 +++++++++++++++++++++----------- 3 files changed, 52 insertions(+), 26 deletions(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index e35109b43..66d80a5b7 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -92,23 +92,31 @@ class RateLimitingMiddleware(wsgi.Middleware): action_name = self.get_action_name(req) if not action_name: # not rate limited return self.application - delay = self.limiter.perform(action_name, username=username) - if action_name == 'POST servers': - # "POST servers" is a POST, so it counts against "POST" too. - delay2 = self.limiter.perform('POST', username=username) - delay = max(delay or 0, delay2 or 0) + delay = self.get_delay(action_name, username) if delay: # TODO(gundlach): Get the retry-after format correct. raise webob.exc.HTTPRequestEntityTooLarge(headers={ 'Retry-After': time.time() + delay}) - else: - return self.application + return self.application + + def get_delay(self, action_name, username): + """Return the delay for the given action and username, or None if + the action would not be rate limited. + """ + if action_name == 'POST servers': + # "POST servers" is a POST, so it counts against "POST" too. + # Attempt the "POST" first, lest we are rate limited by "POST" but + # use up a precious "POST servers" call. + delay = self.limiter.perform("POST", username=username) + if delay: + return delay + return self.limiter.perform(action_name, username=username) def get_action_name(self, req): """Return the action name for this request.""" if req.method == 'GET' and 'changes-since' in req.GET: return 'GET changes-since' - if req.method == 'POST' and req.path_info.starts_with('/servers'): + if req.method == 'POST' and req.path_info.startswith('/servers'): return 'POST servers' if req.method in ['PUT', 'POST', 'DELETE']: return req.method diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index 1983cdea8..545e1d1b6 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -58,3 +58,6 @@ class Test(unittest.TestCase): self.exhaust('c', 0, username='chuck') self.exhaust('c', 0, username='bob') self.exhaust('c', 0, username='alice') + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/api/rackspace/__init__.py b/nova/tests/api/rackspace/__init__.py index f7537a4e7..2fab1a4da 100644 --- a/nova/tests/api/rackspace/__init__.py +++ b/nova/tests/api/rackspace/__init__.py @@ -17,22 +17,15 @@ import unittest -from nova.api.rackspace.ratelimiting import RateLimitingMiddleware +from nova.api.rackspace import RateLimitingMiddleware from nova.tests.api.test_helper import * from webob import Request class RateLimitingMiddlewareTest(unittest.TestCase): - def setUp(self): - self.middleware = RateLimitingMiddleware(APIStub()) - self.stubs = stubout.StubOutForTesting() - - def tearDown(self): - self.stubs.UnsetAll() - def test_get_action_name(self): middleware = RateLimitingMiddleware(APIStub()) def verify(method, url, action_name): - req = Request(url) + req = Request.blank(url) req.method = method action = middleware.get_action_name(req) self.assertEqual(action, action_name) @@ -45,12 +38,34 @@ class RateLimitingMiddlewareTest(unittest.TestCase): verify('GET', '/servers/4', None) verify('HEAD', '/servers/4', None) - def TODO_test_call(self): - pass - #mw = make_middleware() - #req = build_request('DELETE', '/servers/4') - #for i in range(5): - # resp = req.get_response(mw) - # assert resp is OK - #resp = req.get_response(mw) - #assert resp is rate limited + def exhaust(self, middleware, method, url, username, times): + req = Request.blank(url, dict(REQUEST_METHOD=method), + headers={'X-Auth-User': username}) + for i in range(times): + resp = req.get_response(middleware) + self.assertEqual(resp.status_int, 200) + resp = req.get_response(middleware) + self.assertEqual(resp.status_int, 413) + self.assertTrue('Retry-After' in resp.headers) + + def test_single_action(self): + middleware = RateLimitingMiddleware(APIStub()) + self.exhaust(middleware, 'DELETE', '/servers/4', 'usr1', 100) + self.exhaust(middleware, 'DELETE', '/servers/4', 'usr2', 100) + + def test_POST_servers_action_implies_POST_action(self): + middleware = RateLimitingMiddleware(APIStub()) + self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) + self.exhaust(middleware, 'POST', '/images/4', 'usr2', 10) + self.assertTrue(set(middleware.limiter._levels) == + set(['usr1:POST', 'usr1:POST servers', 'usr2:POST'])) + + def test_POST_servers_action_correctly_ratelimited(self): + middleware = RateLimitingMiddleware(APIStub()) + # Use up all of our "POST" allowance for the minute, 5 times + for i in range(5): + self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) + # Reset the 'POST' action counter. + del middleware.limiter._levels['usr1:POST'] + # All 50 daily "POST servers" actions should be all used up + self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 0) -- cgit From fd4d5787d5b6f6e550d33c13eb76f4562a87a118 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 11:23:08 -0400 Subject: Test the WSGIApp --- nova/api/rackspace/ratelimiting/__init__.py | 2 +- nova/api/rackspace/ratelimiting/tests.py | 69 ++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/nova/api/rackspace/ratelimiting/__init__.py b/nova/api/rackspace/ratelimiting/__init__.py index 176e7d66e..64d5fff2c 100644 --- a/nova/api/rackspace/ratelimiting/__init__.py +++ b/nova/api/rackspace/ratelimiting/__init__.py @@ -86,7 +86,7 @@ class WSGIApp(object): self.limiter = limiter @webob.dec.wsgify - def __call__(req): + def __call__(self, req): parts = req.path_info.split('/') # format: /limiter// if req.method != 'POST': diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index 545e1d1b6..f924e7805 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -1,8 +1,10 @@ -import ratelimiting import time import unittest +import webob -class Test(unittest.TestCase): +import nova.api.rackspace.ratelimiting as ratelimiting + +class LimiterTest(unittest.TestCase): def setUp(self): self.limits = { @@ -59,5 +61,68 @@ class Test(unittest.TestCase): self.exhaust('c', 0, username='bob') self.exhaust('c', 0, username='alice') + +class WSGIAppTest(unittest.TestCase): + + def setUp(self): + test = self + class FakeLimiter(object): + def __init__(self): + self._action = self._username = self._delay = None + def mock(self, action, username, delay): + self._action = action + self._username = username + self._delay = delay + def perform(self, action, username): + test.assertEqual(action, self._action) + test.assertEqual(username, self._username) + return self._delay + self.limiter = FakeLimiter() + self.app = ratelimiting.WSGIApp(self.limiter) + + def test_invalid_methods(self): + requests = [] + for method in ['GET', 'PUT', 'DELETE']: + req = webob.Request.blank('/limits/michael/breakdance', + dict(REQUEST_METHOD=method)) + requests.append(req) + for req in requests: + self.assertEqual(req.get_response(self.app).status_int, 405) + + def test_invalid_urls(self): + requests = [] + for prefix in ['limit', '', 'limiter2', 'limiter/limits', 'limiter/1']: + req = webob.Request.blank('/%s/michael/breakdance' % prefix, + dict(REQUEST_METHOD='POST')) + requests.append(req) + for req in requests: + self.assertEqual(req.get_response(self.app).status_int, 404) + + def verify(self, url, username, action, delay=None): + """Make sure that POSTing to the given url causes the given username + to perform the given action. Make the internal rate limiter return + delay and make sure that the WSGI app returns the correct response. + """ + req = webob.Request.blank(url, dict(REQUEST_METHOD='POST')) + self.limiter.mock(action, username, delay) + resp = req.get_response(self.app) + if not delay: + self.assertEqual(resp.status_int, 200) + else: + self.assertEqual(resp.status_int, 403) + self.assertEqual(resp.headers['X-Wait-Seconds'], delay) + + def test_good_urls(self): + self.verify('/limiter/michael/hoot', 'michael', 'hoot') + + def test_escaping(self): + self.verify('/limiter/michael/jump%20up', 'michael', 'jump up') + + def test_response_to_delays(self): + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1) + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1.56) + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) + + if __name__ == '__main__': unittest.main() -- cgit From f200587ce068482ab94e777154de3ac777269fa0 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 13:54:38 -0400 Subject: Add support for middleware proxying to a ratelimiting.WSGIApp, for deployments that use more than one API Server and thus can't store ratelimiting counters in memory. --- nova/api/rackspace/__init__.py | 29 ++++-- nova/api/rackspace/ratelimiting/__init__.py | 21 ++++- nova/api/rackspace/ratelimiting/tests.py | 140 +++++++++++++++++++++++++--- nova/tests/api/rackspace/__init__.py | 8 ++ 4 files changed, 173 insertions(+), 25 deletions(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index 66d80a5b7..ac5365310 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -69,17 +69,26 @@ class AuthMiddleware(wsgi.Middleware): class RateLimitingMiddleware(wsgi.Middleware): """Rate limit incoming requests according to the OpenStack rate limits.""" - def __init__(self, application): + def __init__(self, application, service_host=None): + """Create a rate limiting middleware that wraps the given application. + + By default, rate counters are stored in memory. If service_host is + specified, the middleware instead relies on the ratelimiting.WSGIApp + at the given host+port to keep rate counters. + """ super(RateLimitingMiddleware, self).__init__(application) - #TODO(gundlach): These limits were based on limitations of Cloud - #Servers. We should revisit them in Nova. - self.limiter = ratelimiting.Limiter(limits={ - 'DELETE': (100, ratelimiting.PER_MINUTE), - 'PUT': (10, ratelimiting.PER_MINUTE), - 'POST': (10, ratelimiting.PER_MINUTE), - 'POST servers': (50, ratelimiting.PER_DAY), - 'GET changes-since': (3, ratelimiting.PER_MINUTE), - }) + if not service_host: + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = ratelimiting.Limiter(limits={ + 'DELETE': (100, ratelimiting.PER_MINUTE), + 'PUT': (10, ratelimiting.PER_MINUTE), + 'POST': (10, ratelimiting.PER_MINUTE), + 'POST servers': (50, ratelimiting.PER_DAY), + 'GET changes-since': (3, ratelimiting.PER_MINUTE), + }) + else: + self.limiter = ratelimiting.WSGIAppProxy(service_host) @webob.dec.wsgify def __call__(self, req): diff --git a/nova/api/rackspace/ratelimiting/__init__.py b/nova/api/rackspace/ratelimiting/__init__.py index 64d5fff2c..f843bac0f 100644 --- a/nova/api/rackspace/ratelimiting/__init__.py +++ b/nova/api/rackspace/ratelimiting/__init__.py @@ -1,5 +1,6 @@ """Rate limiting of arbitrary actions.""" +import httplib import time import urllib import webob.dec @@ -98,6 +99,24 @@ class WSGIApp(object): delay = self.limiter.perform(action_name, username) if delay: return webob.exc.HTTPForbidden( - headers={'X-Wait-Seconds': delay}) + headers={'X-Wait-Seconds': "%.2f" % delay}) else: return '' # 200 OK + + +class WSGIAppProxy(object): + + """Limiter lookalike that proxies to a ratelimiting.WSGIApp.""" + + def __init__(self, service_host): + """Creates a proxy pointing to a ratelimiting.WSGIApp at the given + host.""" + self.service_host = service_host + + def perform(self, action, username='nobody'): + conn = httplib.HTTPConnection(self.service_host) + conn.request('POST', '/limiter/%s/%s' % (username, action)) + resp = conn.getresponse() + if resp.status == 200: + return None # no delay + return float(resp.getheader('X-Wait-Seconds')) diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index f924e7805..13a47989b 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -1,3 +1,5 @@ +import httplib +import StringIO import time import unittest import webob @@ -62,22 +64,25 @@ class LimiterTest(unittest.TestCase): self.exhaust('c', 0, username='alice') +class FakeLimiter(object): + """Fake Limiter class that you can tell how to behave.""" + def __init__(self, test): + self._action = self._username = self._delay = None + self.test = test + def mock(self, action, username, delay): + self._action = action + self._username = username + self._delay = delay + def perform(self, action, username): + self.test.assertEqual(action, self._action) + self.test.assertEqual(username, self._username) + return self._delay + + class WSGIAppTest(unittest.TestCase): def setUp(self): - test = self - class FakeLimiter(object): - def __init__(self): - self._action = self._username = self._delay = None - def mock(self, action, username, delay): - self._action = action - self._username = username - self._delay = delay - def perform(self, action, username): - test.assertEqual(action, self._action) - test.assertEqual(username, self._username) - return self._delay - self.limiter = FakeLimiter() + self.limiter = FakeLimiter(self) self.app = ratelimiting.WSGIApp(self.limiter) def test_invalid_methods(self): @@ -110,7 +115,7 @@ class WSGIAppTest(unittest.TestCase): self.assertEqual(resp.status_int, 200) else: self.assertEqual(resp.status_int, 403) - self.assertEqual(resp.headers['X-Wait-Seconds'], delay) + self.assertEqual(resp.headers['X-Wait-Seconds'], "%.2f" % delay) def test_good_urls(self): self.verify('/limiter/michael/hoot', 'michael', 'hoot') @@ -124,5 +129,112 @@ class WSGIAppTest(unittest.TestCase): self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) +class FakeHttplibSocket(object): + """a fake socket implementation for httplib.HTTPResponse, trivial""" + + def __init__(self, response_string): + self._buffer = StringIO.StringIO(response_string) + + def makefile(self, _mode, _other): + """Returns the socket's internal buffer""" + return self._buffer + + +class FakeHttplibConnection(object): + """A fake httplib.HTTPConnection + + Requests made via this connection actually get translated and routed into + our WSGI app, we then wait for the response and turn it back into + an httplib.HTTPResponse. + """ + def __init__(self, app, host, is_secure=False): + self.app = app + self.host = host + + def request(self, method, path, data='', headers={}): + req = webob.Request.blank(path) + req.method = method + req.body = data + req.headers = headers + req.host = self.host + # Call the WSGI app, get the HTTP response + resp = str(req.get_response(self.app)) + # For some reason, the response doesn't have "HTTP/1.0 " prepended; I + # guess that's a function the web server usually provides. + resp = "HTTP/1.0 %s" % resp + sock = FakeHttplibSocket(resp) + self.http_response = httplib.HTTPResponse(sock) + self.http_response.begin() + + def getresponse(self): + return self.http_response + + +def wire_HTTPConnection_to_WSGI(host, app): + """Monkeypatches HTTPConnection so that if you try to connect to host, you + are instead routed straight to the given WSGI app. + + After calling this method, when any code calls + + httplib.HTTPConnection(host) + + the connection object will be a fake. Its requests will be sent directly + to the given WSGI app rather than through a socket. + + Code connecting to hosts other than host will not be affected. + + This method may be called multiple times to map different hosts to + different apps. + """ + class HTTPConnectionDecorator(object): + """Wraps the real HTTPConnection class so that when you instantiate + the class you might instead get a fake instance.""" + def __init__(self, wrapped): + self.wrapped = wrapped + def __call__(self, connection_host, *args, **kwargs): + if connection_host == host: + return FakeHttplibConnection(app, host) + else: + return self.wrapped(connection_host, *args, **kwargs) + httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) + + +class WSGIAppProxyTest(unittest.TestCase): + + def setUp(self): + """Our WSGIAppProxy is going to call across an HTTPConnection to a + WSGIApp running a limiter. The proxy will send input, and the proxy + should receive that same input, pass it to the limiter who gives a + result, and send the expected result back. + + The HTTPConnection isn't real -- it's monkeypatched to point straight + at the WSGIApp. And the limiter isn't real -- it's a fake that + behaves the way we tell it to. + """ + self.limiter = FakeLimiter(self) + app = ratelimiting.WSGIApp(self.limiter) + wire_HTTPConnection_to_WSGI('100.100.100.100:80', app) + self.proxy = ratelimiting.WSGIAppProxy('100.100.100.100:80') + + def test_200(self): + self.limiter.mock('conquer', 'caesar', None) + when = self.proxy.perform('conquer', 'caesar') + self.assertEqual(when, None) + + def test_403(self): + self.limiter.mock('grumble', 'proletariat', 1.5) + when = self.proxy.perform('grumble', 'proletariat') + self.assertEqual(when, 1.5) + + def test_failure(self): + self.limiter.mock('murder', 'brutus', None) + try: + when = self.proxy.perform('stab', 'brutus') + except AssertionError: + pass + else: + self.fail("I didn't perform the action I expected") + + if __name__ == '__main__': unittest.main() diff --git a/nova/tests/api/rackspace/__init__.py b/nova/tests/api/rackspace/__init__.py index 2fab1a4da..622cb4335 100644 --- a/nova/tests/api/rackspace/__init__.py +++ b/nova/tests/api/rackspace/__init__.py @@ -21,7 +21,9 @@ from nova.api.rackspace import RateLimitingMiddleware from nova.tests.api.test_helper import * from webob import Request + class RateLimitingMiddlewareTest(unittest.TestCase): + def test_get_action_name(self): middleware = RateLimitingMiddleware(APIStub()) def verify(method, url, action_name): @@ -69,3 +71,9 @@ class RateLimitingMiddlewareTest(unittest.TestCase): del middleware.limiter._levels['usr1:POST'] # All 50 daily "POST servers" actions should be all used up self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 0) + + def test_proxy_ctor_works(self): + middleware = RateLimitingMiddleware(APIStub()) + self.assertEqual(middleware.limiter.__class__.__name__, "Limiter") + middleware = RateLimitingMiddleware(APIStub(), service_host='foobar') + self.assertEqual(middleware.limiter.__class__.__name__, "WSGIAppProxy") -- cgit From 7437df558f3277e21a4c34a5b517a1cae5dd5a74 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 15 Sep 2010 17:17:20 -0400 Subject: Support querying version list --- nova/api/__init__.py | 13 +++++++++++++ nova/tests/api/__init__.py | 5 +++-- run_tests.py | 4 +++- tools/pip-requires | 4 ++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/nova/api/__init__.py b/nova/api/__init__.py index b9b9e3988..9f116dada 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -21,6 +21,7 @@ Root WSGI middleware for all API controllers. """ import routes +import webob.dec from nova import wsgi from nova.api import ec2 @@ -32,6 +33,18 @@ 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("/ec2/{path_info:.*}", controller=ec2.API()) super(API, self).__init__(mapper) + + @webob.dec.wsgify + def versions(self, req): + """Respond to a request for all OpenStack API versions.""" + response = { + "versions": [ + dict(status="CURRENT", id="v1.0")]} + metadata = { + "application/xml": { + "attributes": dict(version=["status", "id"])}} + return wsgi.Serializer(req.environ, metadata).to_content_type(response) diff --git a/nova/tests/api/__init__.py b/nova/tests/api/__init__.py index 59c4adc3d..4682c094e 100644 --- a/nova/tests/api/__init__.py +++ b/nova/tests/api/__init__.py @@ -52,8 +52,9 @@ class Test(unittest.TestCase): result = webob.Request.blank('/test/cloud').get_response(api.API()) self.assertNotEqual(result.body, "/cloud") - def test_query_api_version(self): - pass + def test_query_api_versions(self): + result = webob.Request.blank('/').get_response(api.API()) + self.assertTrue('CURRENT' in result.body) if __name__ == '__main__': unittest.main() diff --git a/run_tests.py b/run_tests.py index 77aa9088a..cf37b820e 100644 --- a/run_tests.py +++ b/run_tests.py @@ -50,8 +50,10 @@ from nova import flags from nova import twistd from nova.tests.access_unittest import * -from nova.tests.auth_unittest import * from nova.tests.api_unittest import * +from nova.tests.api import * +from nova.tests.api.rackspace import * +from nova.tests.auth_unittest import * from nova.tests.cloud_unittest import * from nova.tests.compute_unittest import * from nova.tests.flags_unittest import * diff --git a/tools/pip-requires b/tools/pip-requires index 13e8e5f45..9b8027451 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -6,14 +6,14 @@ amqplib==0.6.1 anyjson==0.2.4 boto==2.0b1 carrot==0.10.5 -eventlet==0.9.10 +eventlet==0.9.12 lockfile==0.8 python-daemon==1.5.5 python-gflags==1.3 redis==2.0.0 routes==1.12.3 tornado==1.0 -webob==0.9.8 +WebOb==0.9.8 wsgiref==0.1.2 zope.interface==3.6.1 mox==0.5.0 -- cgit From 01a757ee7bc3624c17dbbcfd3bc65d3e2f674b03 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Wed, 15 Sep 2010 17:40:12 -0700 Subject: Added iptables host initial configuration --- bin/nova-manage | 1 + nova/endpoint/api.py | 3 +- nova/flags.py | 5 ++- nova/manager.py | 10 +++++ nova/network/linux_net.py | 44 +++++++++++++++------- nova/network/manager.py | 7 ++++ nova/service.py | 1 + tools/setup_ipchains.sh | 94 +++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 tools/setup_ipchains.sh diff --git a/bin/nova-manage b/bin/nova-manage index 325245ac4..909435ede 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -384,3 +384,4 @@ def main(): if __name__ == '__main__': main() + diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 40be00bb7..6de3698e1 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -42,8 +42,6 @@ from nova.endpoint import cloud FLAGS = flags.FLAGS -flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') - _log = logging.getLogger("api") _log.setLevel(logging.DEBUG) @@ -342,3 +340,4 @@ class APIServerApplication(tornado.web.Application): (r'/1.0/([-A-Za-z0-9/]*)', MetadataRequestHandler), ], pool=multiprocessing.Pool(4)) self.controllers = controllers + diff --git a/nova/flags.py b/nova/flags.py index 7b0c95a3c..55b452fc3 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -184,7 +184,9 @@ DEFINE_string('rabbit_userid', 'guest', 'rabbit userid') DEFINE_string('rabbit_password', 'guest', 'rabbit password') DEFINE_string('rabbit_virtual_host', '/', 'rabbit virtual host') DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to') -DEFINE_string('ec2_url', 'http://127.0.0.1:8773/services/Cloud', +DEFINE_string('cc_ip', '127.0.0.1', 'ip of api server') +DEFINE_integer('cc_port', 8773, 'cloud controller port') +DEFINE_string('ec2_url', 'http://%s:%s/services/Cloud' % (FLAGS.cc_ip, FLAGS.cc_port), 'Url to ec2 api server') DEFINE_string('default_image', 'ami-11111', @@ -220,3 +222,4 @@ DEFINE_string('host', socket.gethostname(), # UNUSED DEFINE_string('node_availability_zone', 'nova', 'availability zone of this node') + diff --git a/nova/manager.py b/nova/manager.py index e9aa50c56..495b1f0d1 100644 --- a/nova/manager.py +++ b/nova/manager.py @@ -37,3 +37,13 @@ class Manager(object): if not db_driver: db_driver = FLAGS.db_driver self.db = utils.import_object(db_driver) # pylint: disable-msg=C0103 + + def init_host(self): + """Do any initialization that needs to be run if this is a standalone service. + + Child classes should override this method. + """ + + + + diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 41aeb5da7..604d11c93 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -36,13 +36,28 @@ flags.DEFINE_string('dhcpbridge_flagfile', flags.DEFINE_string('networks_path', utils.abspath('../networks'), 'Location to keep network config files') flags.DEFINE_string('public_interface', 'vlan1', - 'Interface for public IP addresses') + 'Interface for public IP addresses') flags.DEFINE_string('bridge_dev', 'eth0', - 'network device for bridges') - + 'network device for bridges') +flags.DEFINE_string('routing_source_ip', utils.get_my_ip(), + 'Public IP of network host') DEFAULT_PORTS = [("tcp", 80), ("tcp", 22), ("udp", 1194), ("tcp", 443)] +def init_host(): + """Basic networking setup goes here""" + # NOTE(devcamcar): Cloud public DNAT entries, CloudPipe port + # forwarding entries and a default DNAT entry. + _confirm_rule("-t nat -A nova_prerouting -s 0.0.0.0/0 " + "-d 169.254.169.254/32 -p tcp -m tcp --dport 80 -j DNAT " + "--to-destination %s:%s" % (FLAGS.cc_ip, FLAGS.cc_port)) + + # NOTE(devcamcar): Cloud public SNAT entries and the default + # SNAT rule for outbound traffic. + _confirm_rule("-t nat -A nova_postrouting -s %s " + "-j SNAT --to-source %s" + % (FLAGS.private_range, FLAGS.routing_source_ip)) + def bind_floating_ip(floating_ip): """Bind ip to public interface""" @@ -58,37 +73,37 @@ def unbind_floating_ip(floating_ip): def ensure_vlan_forward(public_ip, port, private_ip): """Sets up forwarding rules for vlan""" - _confirm_rule("FORWARD -d %s -p udp --dport 1194 -j ACCEPT" % private_ip) + _confirm_rule("nova_forward -d %s -p udp --dport 1194 -j ACCEPT" % private_ip) _confirm_rule( - "PREROUTING -t nat -d %s -p udp --dport %s -j DNAT --to %s:1194" + "nova_prerouting -t nat -d %s -p udp --dport %s -j DNAT --to %s:1194" % (public_ip, port, private_ip)) def ensure_floating_forward(floating_ip, fixed_ip): """Ensure floating ip forwarding rule""" - _confirm_rule("PREROUTING -t nat -d %s -j DNAT --to %s" + _confirm_rule("nova_prerouting -t nat -d %s -j DNAT --to %s" % (floating_ip, fixed_ip)) - _confirm_rule("POSTROUTING -t nat -s %s -j SNAT --to %s" + _confirm_rule("nova_postrouting -t nat -s %s -j SNAT --to %s" % (fixed_ip, floating_ip)) # TODO(joshua): Get these from the secgroup datastore entries - _confirm_rule("FORWARD -d %s -p icmp -j ACCEPT" + _confirm_rule("nova_forward -d %s -p icmp -j ACCEPT" % (fixed_ip)) for (protocol, port) in DEFAULT_PORTS: _confirm_rule( - "FORWARD -d %s -p %s --dport %s -j ACCEPT" + "nova_forward -d %s -p %s --dport %s -j ACCEPT" % (fixed_ip, protocol, port)) def remove_floating_forward(floating_ip, fixed_ip): """Remove forwarding for floating ip""" - _remove_rule("PREROUTING -t nat -d %s -j DNAT --to %s" + _remove_rule("nova_prerouting -t nat -d %s -j DNAT --to %s" % (floating_ip, fixed_ip)) - _remove_rule("POSTROUTING -t nat -s %s -j SNAT --to %s" + _remove_rule("nova_postrouting -t nat -s %s -j SNAT --to %s" % (fixed_ip, floating_ip)) - _remove_rule("FORWARD -d %s -p icmp -j ACCEPT" + _remove_rule("nova_forward -d %s -p icmp -j ACCEPT" % (fixed_ip)) for (protocol, port) in DEFAULT_PORTS: - _remove_rule("FORWARD -d %s -p %s --dport %s -j ACCEPT" + _remove_rule("nova_forward -d %s -p %s --dport %s -j ACCEPT" % (fixed_ip, protocol, port)) @@ -124,7 +139,7 @@ def ensure_bridge(bridge, interface, net_attrs=None): net_attrs['gateway'], net_attrs['broadcast'], net_attrs['netmask'])) - _confirm_rule("FORWARD --in-interface %s -j ACCEPT" % bridge) + _confirm_rule("nova_forward --in-interface %s -j ACCEPT" % bridge) else: _execute("sudo ifconfig %s up" % bridge) @@ -256,3 +271,4 @@ def _dnsmasq_pid_for(vlan): if os.path.exists(pid_file): with open(pid_file, 'r') as f: return int(f.read()) + diff --git a/nova/network/manager.py b/nova/network/manager.py index 7a3bcfc2f..87c3d8e46 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -218,6 +218,12 @@ class FlatManager(NetworkManager): class VlanManager(NetworkManager): """Vlan network with dhcp""" + + def init_host(self): + """Do any initialization that needs to be run if this is a standalone service. + """ + driver.init_host() + def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): """Gets a fixed ip from the pool""" network_ref = self.db.project_get_network(context, context.project.id) @@ -363,3 +369,4 @@ class VlanManager(NetworkManager): parent_reserved = super(VlanManager, self)._top_reserved_ips return parent_reserved + FLAGS.cnt_vpn_clients + diff --git a/nova/service.py b/nova/service.py index 870dd6ceb..8f1db1b8e 100644 --- a/nova/service.py +++ b/nova/service.py @@ -158,3 +158,4 @@ class Service(object, service.Service): self.model_disconnected = True logging.exception("model server went away") yield + diff --git a/tools/setup_ipchains.sh b/tools/setup_ipchains.sh new file mode 100644 index 000000000..b1ab1c6f7 --- /dev/null +++ b/tools/setup_ipchains.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +CMD="global" +IP="XXX" +PRIVATE_RANGE="10.128.0.0/12" + +if [ -n "$1" ]; then + CMD=$1 +fi + +if [ -n "$2" ]; then + IP=$2 +fi + +if [ -n "$3" ]; then + PRIVATE_RANGE=$3 +fi + +if [ "$CMD" == "global" ]; then + iptables -P INPUT DROP + iptables -A INPUT -m state --state INVALID -j DROP + iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -A INPUT -m tcp -p tcp -d $MGMT_IP --dport 22 -j ACCEPT + iptables -A INPUT -m udp -p udp --dport 123 -j ACCEPT + iptables -N nova_input + iptables -A INPUT -j nova_input + iptables -A INPUT -p icmp -j ACCEPT + iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset + iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable + + iptables -P FORWARD DROP + iptables -A FORWARD -m state --state INVALID -j DROP + iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu + iptables -N nova_forward + iptables -A FORWARD -j nova_forward + + iptables -P OUTPUT DROP + iptables -A OUTPUT -m state --state INVALID -j DROP + iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -N nova_output + iptables -A OUTPUT -j nova_output + + iptables -t nat -N nova_prerouting + iptables -t nat -A PREROUTING -j nova_prerouting + + iptables -t nat -N nova_postrouting + iptables -t nat -A POSTROUTING -j nova_postrouting + + iptables -t nat -N nova_output + iptables -t nat -A OUTPUT -j nova_output + + # ganglia (all hosts) + iptables -A nova_input -m tcp -p tcp -d $IP --dport 8649 -j ACCEPT + iptables -A nova_input -m udp -p udp -d $IP --dport 8649 -j ACCEPT +fi + +if [ "$CMD" == "dashboard" ]; then + # dashboard + iptables -A nova_input -m tcp -p tcp -d $IP --dport 80 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 443 -j ACCEPT +fi + +if [ "$CMD" == "objectstore" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 3333 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 8773 -j ACCEPT +fi + +if [ "$CMD" == "redis" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 6379 -j ACCEPT +fi + +if [ "$CMD" == "mysql" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 3306 -j ACCEPT +fi + +if [ "$CMD" == "rabbitmq" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 4369 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 5672 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 53284 -j ACCEPT +fi + +if [ "$CMD" == "dnsmasq" ]; then + # NOTE(vish): this could theoretically be setup per network + # for each host, but it seems like overkill + iptables -A nova_input -m tcp -p tcp -s $PRIVATE_RANGE --dport 53 -j ACCEPT + iptables -A nova_input -m udp -p udp -s $PRIVATE_RANGE --dport 53 -j ACCEPT + iptables -A nova_input -m udp -p udp --dport 67 -j ACCEPT + +if [ "$CMD" == "ldap" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 389 -j ACCEPT +fi + + -- cgit From bc2641148359352ed83d4190baaf1e208e00a6b9 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Wed, 15 Sep 2010 17:49:15 -0700 Subject: Added iptables host initial configuration --- nova/network/manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index 87c3d8e46..25a9ba474 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -219,10 +219,11 @@ class FlatManager(NetworkManager): class VlanManager(NetworkManager): """Vlan network with dhcp""" - def init_host(self): - """Do any initialization that needs to be run if this is a standalone service. - """ - driver.init_host() + def init_host(self): + """Do any initialization that needs to be run if this is a + standalone service. + """ + driver.init_host() def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): """Gets a fixed ip from the pool""" -- cgit From 8e304fe0bf69fe5f6bad2fa3d5a71a93cb0612e8 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 16 Sep 2010 12:39:35 -0400 Subject: Fix things not quite merged perfectly -- all tests now pass --- nova/api/ec2/cloud.py | 2 +- nova/tests/network_unittest.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 7a9b5f5cf..c04e722cc 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -556,7 +556,7 @@ class CloudController(object): # NOTE(vish): Right now we don't really care if the ip is # disassociated. We may need to worry about # checking this later. Perhaps in the scheduler? - network_topic = yield self._get_network_topic(context) + network_topic = self._get_network_topic(context) rpc.cast(network_topic, {"method": "disassociate_floating_ip", "args": {"context": None, diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index dc5277f02..da65b50a2 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -28,7 +28,7 @@ from nova import flags from nova import test from nova import utils from nova.auth import manager -from nova.endpoint import api +from nova.api.ec2 import context FLAGS = flags.FLAGS @@ -49,7 +49,7 @@ class NetworkTestCase(test.TrialTestCase): self.user = self.manager.create_user('netuser', 'netuser', 'netuser') self.projects = [] self.network = utils.import_object(FLAGS.network_manager) - self.context = api.APIRequestContext(None, project=None, user=self.user) + self.context = context.APIRequestContext(project=None, user=self.user) for i in range(5): name = 'project%s' % i self.projects.append(self.manager.create_project(name, -- cgit From e1ddec70bc7522a75b4a50953a0f4b20ace6cce1 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Thu, 16 Sep 2010 11:40:04 -0700 Subject: Added missing masquerade rules --- nova/network/linux_net.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 604d11c93..75acf2afc 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -58,6 +58,8 @@ def init_host(): "-j SNAT --to-source %s" % (FLAGS.private_range, FLAGS.routing_source_ip)) + _confirm_rule("-A nova_postrouting -s %s MASQUERADE" % FLAGS.private_range) + _confirm_rule("-A nova_postrouting -s %(range)s -d %(range)s" % {'range': FLAGS.private_range}) def bind_floating_ip(floating_ip): """Bind ip to public interface""" -- cgit From d0708205759880e7fb78fbb1df33df939f669413 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Thu, 16 Sep 2010 11:44:51 -0700 Subject: Whitespace fixes --- bin/nova-manage | 1 - nova/manager.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 909435ede..325245ac4 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -384,4 +384,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/nova/manager.py b/nova/manager.py index 495b1f0d1..b7b97bced 100644 --- a/nova/manager.py +++ b/nova/manager.py @@ -44,6 +44,3 @@ class Manager(object): Child classes should override this method. """ - - - -- cgit From 11b934f75ac4359b75f246fd9babfc3363a9a396 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 16 Sep 2010 14:41:51 -0500 Subject: Replaced the existing Rackspace Auth Mechanism with one that mirrors the implementation in the design document. --- nova/api/rackspace/__init__.py | 52 ++++++++++++++++----- nova/api/rackspace/auth.py | 37 +++++++++++++++ nova/tests/api/rackspace/auth.py | 81 +++++++++++++++++++++++++++++++++ nova/tests/api/rackspace/test_helper.py | 52 +++++++++++++++++++++ 4 files changed, 211 insertions(+), 11 deletions(-) create mode 100644 nova/api/rackspace/auth.py create mode 100644 nova/tests/api/rackspace/auth.py create mode 100644 nova/tests/api/rackspace/test_helper.py diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index b4d666d63..dbba97107 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -26,8 +26,10 @@ import time import routes import webob.dec import webob.exc +import webob from nova import flags +from nova import utils from nova import wsgi from nova.api.rackspace import flavors from nova.api.rackspace import images @@ -36,6 +38,10 @@ from nova.api.rackspace import sharedipgroups from nova.auth import manager +FLAGS = flags.FLAGS +flags.DEFINE_string('nova_api_auth', 'nova.api.rackspace.auth.FakeAuth', + 'The auth mechanism to use for the Rackspace API implemenation') + class API(wsgi.Middleware): """WSGI entry point for all Rackspace API requests.""" @@ -47,23 +53,47 @@ class API(wsgi.Middleware): class AuthMiddleware(wsgi.Middleware): """Authorize the rackspace API request or return an HTTP Forbidden.""" - #TODO(gundlach): isn't this the old Nova API's auth? Should it be replaced - #with correct RS API auth? + def __init__(self, application): + self.auth_driver = utils.import_class(FLAGS.nova_api_auth)() + super(AuthMiddleware, self).__init__(application) @webob.dec.wsgify def __call__(self, req): - context = {} - if "HTTP_X_AUTH_TOKEN" in req.environ: - context['user'] = manager.AuthManager().get_user_from_access_key( - req.environ['HTTP_X_AUTH_TOKEN']) - if context['user']: - context['project'] = manager.AuthManager().get_project( - context['user'].name) - if "user" not in context: - return webob.exc.HTTPForbidden() + if not req.headers.has_key("X-Auth-Token"): + return self.authenticate(req) + + user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) + + if not user: + return webob.exc.HTTPUnauthorized() + context = {'user':user} req.environ['nova.context'] = context return self.application + def authenticate(self, req): + # Unless the request is explicitly made against // don't + # honor it + path_info = req.environ['wsgiorg.routing_args'][1]['path_info'] + if path_info: + return webob.exc.HTTPUnauthorized() + + if req.headers.has_key("X-Auth-User") and \ + req.headers.has_key("X-Auth-Key"): + username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] + token, user = self.auth_driver.authorize_user(username, key) + if user and token: + res = webob.Response() + res.headers['X-Auth-Token'] = token + res.headers['X-Server-Management-Url'] = \ + user['server_management_url'] + res.headers['X-Storage-Url'] = user['storage_url'] + res.headers['X-CDN-Management-Url'] = user['cdn_management_url'] + res.content_type = 'text/plain' + res.status = '204' + return res + else: + return webob.exc.HTTPUnauthorized() + return webob.exc.HTTPUnauthorized() class APIRouter(wsgi.Router): """ diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py new file mode 100644 index 000000000..d2b5193c3 --- /dev/null +++ b/nova/api/rackspace/auth.py @@ -0,0 +1,37 @@ +import json +from hashlib import sha1 +from nova import datastore + +class FakeAuth(object): + def __init__(self, store=datastore.Redis.instance): + self._store = store() + self.auth_hash = 'rs_fake_auth' + self._store.hsetnx(self.auth_hash, 'rs_last_id', 0) + + def authorize_token(self, token): + user = self._store.hget(self.auth_hash, token) + if user: + return json.loads(user) + return None + + def authorize_user(self, user, key): + token = sha1("%s_%s" % (user, key)).hexdigest() + user = self._store.hget(self.auth_hash, token) + if not user: + return None, None + else: + return token, json.loads(user) + + def add_user(self, user, key): + last_id = self._store.hget(self.auth_hash, 'rs_last_id') + token = sha1("%s_%s" % (user, key)).hexdigest() + user = { + 'id':last_id, + 'cdn_management_url':'cdn_management_url', + 'storage_url':'storage_url', + 'server_management_url':'server_management_url' + } + new_user = self._store.hsetnx(self.auth_hash, token, json.dumps(user)) + if new_user: + self._store.hincrby(self.auth_hash, 'rs_last_id') + diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py new file mode 100644 index 000000000..65264fae9 --- /dev/null +++ b/nova/tests/api/rackspace/auth.py @@ -0,0 +1,81 @@ +import webob +import webob.dec +import unittest +import stubout +import nova.api +import nova.api.rackspace.auth +from nova.tests.api.rackspace import test_helper + +class Test(unittest.TestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + self.stubs.Set(nova.api.rackspace.auth.FakeAuth, '__init__', + test_helper.fake_auth_init) + ds = test_helper.FakeRedis() + ds.hset(test_helper.auth_hash, 'rs_last_id', 0) + + def tearDown(self): + self.stubs.UnsetAll() + test_helper.fake_data_store = {} + + def test_authorize_user(self): + auth = nova.api.rackspace.auth.FakeAuth() + auth.add_user('herp', 'derp') + + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-User'] = 'herp' + req.headers['X-Auth-Key'] = 'derp' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '204 No Content') + self.assertEqual(len(result.headers['X-Auth-Token']), 40) + self.assertEqual(result.headers['X-Server-Management-Url'], + "server_management_url") + self.assertEqual(result.headers['X-CDN-Management-Url'], + "cdn_management_url") + self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + + def test_authorize_token(self): + auth = nova.api.rackspace.auth.FakeAuth() + auth.add_user('herp', 'derp') + + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-User'] = 'herp' + req.headers['X-Auth-Key'] = 'derp' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '204 No Content') + self.assertEqual(len(result.headers['X-Auth-Token']), 40) + self.assertEqual(result.headers['X-Server-Management-Url'], + "server_management_url") + self.assertEqual(result.headers['X-CDN-Management-Url'], + "cdn_management_url") + self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + + token = result.headers['X-Auth-Token'] + self.stubs.Set(nova.api.rackspace, 'APIRouter', + test_helper.FakeRouter) + req = webob.Request.blank('/v1.0/fake') + req.headers['X-Auth-Token'] = token + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '200 OK') + self.assertEqual(result.headers['X-Test-Success'], 'True') + + def test_bad_user(self): + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-User'] = 'herp' + req.headers['X-Auth-Key'] = 'derp' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '401 Unauthorized') + + def test_no_user(self): + req = webob.Request.blank('/v1.0/') + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '401 Unauthorized') + + def test_bad_token(self): + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-Token'] = 'baconbaconbacon' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '401 Unauthorized') + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py new file mode 100644 index 000000000..578b1e841 --- /dev/null +++ b/nova/tests/api/rackspace/test_helper.py @@ -0,0 +1,52 @@ +import webob +import webob.dec +from nova.wsgi import Router + +fake_data_store = {} +auth_hash = 'dummy_hash' + +class FakeRedis(object): + def __init__(self): + global fake_data_store + self.store = fake_data_store + + def hsetnx(self, hash_name, key, value): + if not self.store.has_key(hash_name): + self.store[hash_name] = {} + + if self.store[hash_name].has_key(key): + return 0 + self.store[hash_name][key] = value + return 1 + + def hset(self, hash_name, key, value): + if not self.store.has_key(hash_name): + self.store[hash_name] = {} + + self.store[hash_name][key] = value + return 1 + + def hget(self, hash_name, key): + if not self.store[hash_name].has_key(key): + return None + return self.store[hash_name][key] + + def hincrby(self, hash_name, key, amount=1): + self.store[hash_name][key] += amount + +class FakeRouter(Router): + def __init__(self): + pass + + @webob.dec.wsgify + def __call__(self, req): + res = webob.Response() + res.status = '200' + res.headers['X-Test-Success'] = 'True' + return res + +def fake_auth_init(self, store=FakeRedis): + global auth_hash + self._store = store() + self.auth_hash = auth_hash + -- cgit From 5ff47a4513c3b5a7f8f90c417e1e62113797de8c Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Sun, 19 Sep 2010 18:23:41 -0700 Subject: updated docstring --- nova/flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/flags.py b/nova/flags.py index efa14f9d7..64dd9d456 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -169,7 +169,7 @@ def DECLARE(name, module_string, flag_values=FLAGS): DEFINE_list('region_list', [], - 'list of region,url pairs') + 'list of region|url pairs separated by commas') DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', '127.0.0.1', 's3 host') -- cgit From ae760b13c5382f2f4719dde445235c156cc27d18 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 20 Sep 2010 14:49:05 -0400 Subject: Use assertRaises --- nova/api/rackspace/ratelimiting/tests.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py index 13a47989b..4c9510917 100644 --- a/nova/api/rackspace/ratelimiting/tests.py +++ b/nova/api/rackspace/ratelimiting/tests.py @@ -227,13 +227,10 @@ class WSGIAppProxyTest(unittest.TestCase): self.assertEqual(when, 1.5) def test_failure(self): - self.limiter.mock('murder', 'brutus', None) - try: - when = self.proxy.perform('stab', 'brutus') - except AssertionError: - pass - else: - self.fail("I didn't perform the action I expected") + def shouldRaise(): + self.limiter.mock('murder', 'brutus', None) + self.proxy.perform('stab', 'brutus') + self.assertRaises(AssertionError, shouldRaise) if __name__ == '__main__': -- cgit From fc93548e99dea561dbf2f198b0fccc84467dbf8b Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 20 Sep 2010 17:02:32 -0400 Subject: Undo run_tests.py modification in the hopes of making this merge --- run_tests.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/run_tests.py b/run_tests.py index cf37b820e..77aa9088a 100644 --- a/run_tests.py +++ b/run_tests.py @@ -50,10 +50,8 @@ from nova import flags from nova import twistd from nova.tests.access_unittest import * -from nova.tests.api_unittest import * -from nova.tests.api import * -from nova.tests.api.rackspace import * from nova.tests.auth_unittest import * +from nova.tests.api_unittest import * from nova.tests.cloud_unittest import * from nova.tests.compute_unittest import * from nova.tests.flags_unittest import * -- cgit From 68633fadeb92a5a26d1ab613bed6094ddfa2a014 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Mon, 20 Sep 2010 15:35:44 -0700 Subject: Whitespace fixes --- nova/endpoint/api.py | 1 - nova/flags.py | 1 - nova/network/linux_net.py | 1 - nova/network/manager.py | 1 - nova/service.py | 1 - 5 files changed, 5 deletions(-) diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 6de3698e1..56481b2c0 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -340,4 +340,3 @@ class APIServerApplication(tornado.web.Application): (r'/1.0/([-A-Za-z0-9/]*)', MetadataRequestHandler), ], pool=multiprocessing.Pool(4)) self.controllers = controllers - diff --git a/nova/flags.py b/nova/flags.py index 55b452fc3..ce30d5033 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -222,4 +222,3 @@ DEFINE_string('host', socket.gethostname(), # UNUSED DEFINE_string('node_availability_zone', 'nova', 'availability zone of this node') - diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 75acf2afc..65dcf51ee 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -273,4 +273,3 @@ def _dnsmasq_pid_for(vlan): if os.path.exists(pid_file): with open(pid_file, 'r') as f: return int(f.read()) - diff --git a/nova/network/manager.py b/nova/network/manager.py index 25a9ba474..c7bcfa175 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -370,4 +370,3 @@ class VlanManager(NetworkManager): parent_reserved = super(VlanManager, self)._top_reserved_ips return parent_reserved + FLAGS.cnt_vpn_clients - diff --git a/nova/service.py b/nova/service.py index 8f1db1b8e..870dd6ceb 100644 --- a/nova/service.py +++ b/nova/service.py @@ -158,4 +158,3 @@ class Service(object, service.Service): self.model_disconnected = True logging.exception("model server went away") yield - -- cgit From 64dd3000c4a9b88719e86d1090097e35398d3838 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Mon, 20 Sep 2010 18:04:57 -0500 Subject: Refactored the auth branch based on review feedback --- nova/api/rackspace/__init__.py | 33 ++------- nova/api/rackspace/auth.py | 117 +++++++++++++++++++++++--------- nova/db/api.py | 15 ++++ nova/db/sqlalchemy/api.py | 23 +++++++ nova/db/sqlalchemy/models.py | 14 +++- nova/tests/api/rackspace/auth.py | 84 +++++++++++++++-------- nova/tests/api/rackspace/test_helper.py | 66 +++++++++--------- 7 files changed, 225 insertions(+), 127 deletions(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index dbba97107..f62ddc1c7 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -39,7 +39,8 @@ from nova.auth import manager FLAGS = flags.FLAGS -flags.DEFINE_string('nova_api_auth', 'nova.api.rackspace.auth.FakeAuth', +flags.DEFINE_string('nova_api_auth', + 'nova.api.rackspace.auth.BasicApiAuthManager', 'The auth mechanism to use for the Rackspace API implemenation') class API(wsgi.Middleware): @@ -49,7 +50,6 @@ class API(wsgi.Middleware): app = AuthMiddleware(APIRouter()) super(API, self).__init__(app) - class AuthMiddleware(wsgi.Middleware): """Authorize the rackspace API request or return an HTTP Forbidden.""" @@ -60,41 +60,16 @@ class AuthMiddleware(wsgi.Middleware): @webob.dec.wsgify def __call__(self, req): if not req.headers.has_key("X-Auth-Token"): - return self.authenticate(req) + return self.auth_driver.authenticate(req) user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) if not user: return webob.exc.HTTPUnauthorized() - context = {'user':user} + context = {'user': user} req.environ['nova.context'] = context return self.application - def authenticate(self, req): - # Unless the request is explicitly made against // don't - # honor it - path_info = req.environ['wsgiorg.routing_args'][1]['path_info'] - if path_info: - return webob.exc.HTTPUnauthorized() - - if req.headers.has_key("X-Auth-User") and \ - req.headers.has_key("X-Auth-Key"): - username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] - token, user = self.auth_driver.authorize_user(username, key) - if user and token: - res = webob.Response() - res.headers['X-Auth-Token'] = token - res.headers['X-Server-Management-Url'] = \ - user['server_management_url'] - res.headers['X-Storage-Url'] = user['storage_url'] - res.headers['X-CDN-Management-Url'] = user['cdn_management_url'] - res.content_type = 'text/plain' - res.status = '204' - return res - else: - return webob.exc.HTTPUnauthorized() - return webob.exc.HTTPUnauthorized() - class APIRouter(wsgi.Router): """ Routes requests on the Rackspace API to the appropriate controller diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index d2b5193c3..b29596880 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -1,37 +1,88 @@ +import datetime import json -from hashlib import sha1 -from nova import datastore - -class FakeAuth(object): - def __init__(self, store=datastore.Redis.instance): - self._store = store() - self.auth_hash = 'rs_fake_auth' - self._store.hsetnx(self.auth_hash, 'rs_last_id', 0) - - def authorize_token(self, token): - user = self._store.hget(self.auth_hash, token) - if user: - return json.loads(user) - return None +import time +import webob.exc +import webob.dec +import hashlib + +from nova import auth +from nova import manager +from nova import db + +class Context(object): + pass + +class BasicApiAuthManager(manager.Manager): + """ Implements a somewhat rudimentary version of Rackspace Auth""" + + def __init__(self): + self.auth = auth.manager.AuthManager() + self.context = Context() + super(BasicApiAuthManager, self).__init__() + + def authenticate(self, req): + # Unless the request is explicitly made against // don't + # honor it + path_info = req.path_info + if len(path_info) > 1: + return webob.exc.HTTPUnauthorized() + + try: + username, key = req.headers['X-Auth-User'], \ + req.headers['X-Auth-Key'] + except KeyError: + return webob.exc.HTTPUnauthorized() - def authorize_user(self, user, key): - token = sha1("%s_%s" % (user, key)).hexdigest() - user = self._store.hget(self.auth_hash, token) - if not user: - return None, None + username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] + token, user = self._authorize_user(username, key) + if user and token: + res = webob.Response() + res.headers['X-Auth-Token'] = token['token_hash'] + res.headers['X-Server-Management-Url'] = \ + token['server_management_url'] + res.headers['X-Storage-Url'] = token['storage_url'] + res.headers['X-CDN-Management-Url'] = token['cdn_management_url'] + res.content_type = 'text/plain' + res.status = '204' + return res else: - return token, json.loads(user) - - def add_user(self, user, key): - last_id = self._store.hget(self.auth_hash, 'rs_last_id') - token = sha1("%s_%s" % (user, key)).hexdigest() - user = { - 'id':last_id, - 'cdn_management_url':'cdn_management_url', - 'storage_url':'storage_url', - 'server_management_url':'server_management_url' - } - new_user = self._store.hsetnx(self.auth_hash, token, json.dumps(user)) - if new_user: - self._store.hincrby(self.auth_hash, 'rs_last_id') + return webob.exc.HTTPUnauthorized() + + def authorize_token(self, token_hash): + """ retrieves user information from the datastore given a token + + If the token has expired, returns None + If the token is not found, returns None + Otherwise returns the token + + This method will also remove the token if the timestamp is older than + 2 days ago. + """ + token = self.db.auth_get_token(self.context, token_hash) + if token: + delta = datetime.datetime.now() - token['created_at'] + if delta.days >= 2: + self.db.auth_destroy_token(self.context, token) + else: + user = self.auth.get_user(self.context, token['user_id']) + return { 'id':user['id'] } + return None + + def _authorize_user(self, username, key): + """ Generates a new token and assigns it to a user """ + user = self.auth.get_user_from_access_key(key) + if user and user['name'] == username: + token_hash = hashlib.sha1('%s%s%f' % (username, key, + time.time())).hexdigest() + token = {} + token['token_hash'] = token_hash + token['cdn_management_url'] = '' + token['server_management_url'] = self._get_server_mgmt_url() + token['storage_url'] = '' + self.db.auth_create_token(self.context, token, user['id']) + return token, user + return None, None + + def _get_server_mgmt_url(self): + return 'https://%s/v1.0/' % self.host diff --git a/nova/db/api.py b/nova/db/api.py index d749ae50a..80dde7a7a 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -374,6 +374,21 @@ def export_device_create(context, values): return IMPL.export_device_create(context, values) +################### + +def auth_destroy_token(context, token): + """Destroy an auth token""" + return IMPL.auth_destroy_token(context, token) + +def auth_get_token(context, token_hash): + """Retrieves a token given the hash representing it""" + return IMPL.auth_get_token(context, token_hash) + +def auth_create_token(context, token, user_id): + """Creates a new token""" + return IMPL.auth_create_token(context, token_hash, token, user_id) + + ################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 485dca2b0..681dec15e 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -539,6 +539,29 @@ def export_device_create(_context, values): ################### +def auth_destroy_token(_context, token): + session = get_session() + session.delete(token) + +def auth_get_token(_context, token_hash): + session = get_session() + tk = session.query(models.AuthToken + ).filter_by(token_hash=token_hash) + if not tk: + raise exception.NotFound('Token %s does not exist' % token_hash) + return tk + +def auth_create_token(_context, token, user_id): + tk = models.AuthToken() + for k,v in token.iteritems(): + tk[k] = v + tk.save() + return tk + + +################### + + def volume_allocate_shelf_and_blade(_context, volume_id): session = get_session() with session.begin(): diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 6818f838c..df5848ec1 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -213,7 +213,7 @@ class Instance(BASE, NovaBase): image_id = Column(String(255)) kernel_id = Column(String(255)) - ramdisk_id = Column(String(255)) + # image_id = Column(Integer, ForeignKey('images.id'), nullable=True) # kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True) # ramdisk_id = Column(Integer, ForeignKey('images.id'), nullable=True) @@ -321,6 +321,18 @@ class NetworkIndex(BASE, NovaBase): network = relationship(Network, backref=backref('network_index', uselist=False)) +class AuthToken(BASE, NovaBase): + """Represents an authorization token for all API transactions. Fields + are a string representing the actual token and a user id for mapping + to the actual user""" + __tablename__ = 'auth_tokens' + token_hash = Column(String(255)) + user_id = Column(Integer) + server_manageent_url = Column(String(255)) + storage_url = Column(String(255)) + cdn_management_url = Column(String(255)) + + # TODO(vish): can these both come from the same baseclass? class FixedIp(BASE, NovaBase): diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index 65264fae9..8ab10d94c 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -4,23 +4,24 @@ import unittest import stubout import nova.api import nova.api.rackspace.auth +from nova import auth from nova.tests.api.rackspace import test_helper +import datetime class Test(unittest.TestCase): def setUp(self): self.stubs = stubout.StubOutForTesting() - self.stubs.Set(nova.api.rackspace.auth.FakeAuth, '__init__', - test_helper.fake_auth_init) - ds = test_helper.FakeRedis() - ds.hset(test_helper.auth_hash, 'rs_last_id', 0) + self.stubs.Set(nova.api.rackspace.auth.BasicApiAuthManager, + '__init__', test_helper.fake_auth_init) + test_helper.auth_data = {} def tearDown(self): self.stubs.UnsetAll() test_helper.fake_data_store = {} def test_authorize_user(self): - auth = nova.api.rackspace.auth.FakeAuth() - auth.add_user('herp', 'derp') + f = test_helper.FakeAuthManager() + f.add_user('derp', { 'id': 1, 'name':'herp' } ) req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'herp' @@ -29,35 +30,58 @@ class Test(unittest.TestCase): self.assertEqual(result.status, '204 No Content') self.assertEqual(len(result.headers['X-Auth-Token']), 40) self.assertEqual(result.headers['X-Server-Management-Url'], - "server_management_url") + "https://foo/v1.0/") self.assertEqual(result.headers['X-CDN-Management-Url'], - "cdn_management_url") - self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + "") + self.assertEqual(result.headers['X-Storage-Url'], "") - def test_authorize_token(self): - auth = nova.api.rackspace.auth.FakeAuth() - auth.add_user('herp', 'derp') + #def test_authorize_token(self): + # auth = nova.api.rackspace.auth.FakeAuth() + # auth.add_user('herp', 'derp') - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-User'] = 'herp' - req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '204 No Content') - self.assertEqual(len(result.headers['X-Auth-Token']), 40) - self.assertEqual(result.headers['X-Server-Management-Url'], - "server_management_url") - self.assertEqual(result.headers['X-CDN-Management-Url'], - "cdn_management_url") - self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + # req = webob.Request.blank('/v1.0/') + # req.headers['X-Auth-User'] = 'herp' + # req.headers['X-Auth-Key'] = 'derp' + # result = req.get_response(nova.api.API()) + # self.assertEqual(result.status, '204 No Content') + # self.assertEqual(len(result.headers['X-Auth-Token']), 40) + # self.assertEqual(result.headers['X-Server-Management-Url'], + # "server_management_url") + # self.assertEqual(result.headers['X-CDN-Management-Url'], + # "cdn_management_url") + # self.assertEqual(result.headers['X-Storage-Url'], "storage_url") + + # token = result.headers['X-Auth-Token'] + # self.stubs.Set(nova.api.rackspace, 'APIRouter', + # test_helper.FakeRouter) + # req = webob.Request.blank('/v1.0/fake') + # req.headers['X-Auth-Token'] = token + # result = req.get_response(nova.api.API()) + # self.assertEqual(result.status, '200 OK') + # self.assertEqual(result.headers['X-Test-Success'], 'True') + + def test_token_expiry(self): + self.destroy_called = False + token_hash = 'bacon' + + def destroy_token_mock(meh, context, token): + self.destroy_called = True + + def bad_token(meh, context, token_hash): + return { 'token_hash':token_hash, + 'created_at':datetime.datetime(1990, 1, 1) } + + self.stubs.Set(test_helper.FakeAuthDatabase, 'auth_destroy_token', + destroy_token_mock) - token = result.headers['X-Auth-Token'] - self.stubs.Set(nova.api.rackspace, 'APIRouter', - test_helper.FakeRouter) - req = webob.Request.blank('/v1.0/fake') - req.headers['X-Auth-Token'] = token + self.stubs.Set(test_helper.FakeAuthDatabase, 'auth_get_token', + bad_token) + + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-Token'] = 'bacon' result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '200 OK') - self.assertEqual(result.headers['X-Test-Success'], 'True') + self.assertEqual(result.status, '401 Unauthorized') + self.assertEqual(self.destroy_called, True) def test_bad_user(self): req = webob.Request.blank('/v1.0/') diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 578b1e841..8d784854f 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -1,38 +1,12 @@ import webob import webob.dec from nova.wsgi import Router +from nova import auth -fake_data_store = {} -auth_hash = 'dummy_hash' +auth_data = {} -class FakeRedis(object): - def __init__(self): - global fake_data_store - self.store = fake_data_store - - def hsetnx(self, hash_name, key, value): - if not self.store.has_key(hash_name): - self.store[hash_name] = {} - - if self.store[hash_name].has_key(key): - return 0 - self.store[hash_name][key] = value - return 1 - - def hset(self, hash_name, key, value): - if not self.store.has_key(hash_name): - self.store[hash_name] = {} - - self.store[hash_name][key] = value - return 1 - - def hget(self, hash_name, key): - if not self.store[hash_name].has_key(key): - return None - return self.store[hash_name][key] - - def hincrby(self, hash_name, key, amount=1): - self.store[hash_name][key] += amount +class Context(object): + pass class FakeRouter(Router): def __init__(self): @@ -45,8 +19,32 @@ class FakeRouter(Router): res.headers['X-Test-Success'] = 'True' return res -def fake_auth_init(self, store=FakeRedis): - global auth_hash - self._store = store() - self.auth_hash = auth_hash +def fake_auth_init(self): + self.db = FakeAuthDatabase() + self.context = Context() + self.auth = FakeAuthManager() + self.host = 'foo' + +class FakeAuthDatabase(object): + @staticmethod + def auth_get_token(context, token_hash): + pass + + @staticmethod + def auth_create_token(context, token, user_id): + pass + + @staticmethod + def auth_destroy_token(context, token): + pass + +class FakeAuthManager(object): + def __init__(self): + global auth_data + self.data = auth_data + + def add_user(self, key, user): + self.data[key] = user + def get_user_from_access_key(self, key): + return self.data.get(key, None) -- cgit From ce1a8086f7ec947dd148855910a1a5a9696e33f7 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Mon, 20 Sep 2010 23:56:17 -0400 Subject: Don't use something the shell will escape as a separator. | is now =. --- nova/endpoint/cloud.py | 2 +- nova/flags.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index b28bb0dc3..2b67af96f 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -158,7 +158,7 @@ class CloudController(object): if FLAGS.region_list: regions = [] for region in FLAGS.region_list: - name, _sep, url = region.partition('|') + name, _sep, url = region.partition('=') regions.append({'regionName': name, 'regionEndpoint': url}) else: diff --git a/nova/flags.py b/nova/flags.py index 64dd9d456..c5dee2855 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -169,7 +169,7 @@ def DECLARE(name, module_string, flag_values=FLAGS): DEFINE_list('region_list', [], - 'list of region|url pairs separated by commas') + 'list of region=url pairs separated by commas') DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', '127.0.0.1', 's3 host') -- cgit From e74b8070f73d8bada01cfe2d26223e5180ab67fb Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Tue, 21 Sep 2010 00:03:53 -0700 Subject: Renamed cc_ip flag to cc_host --- nova/flags.py | 4 ++-- nova/network/linux_net.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/flags.py b/nova/flags.py index ce30d5033..9d27d336d 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -184,9 +184,9 @@ DEFINE_string('rabbit_userid', 'guest', 'rabbit userid') DEFINE_string('rabbit_password', 'guest', 'rabbit password') DEFINE_string('rabbit_virtual_host', '/', 'rabbit virtual host') DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to') -DEFINE_string('cc_ip', '127.0.0.1', 'ip of api server') +DEFINE_string('cc_host', '127.0.0.1', 'ip of api server') DEFINE_integer('cc_port', 8773, 'cloud controller port') -DEFINE_string('ec2_url', 'http://%s:%s/services/Cloud' % (FLAGS.cc_ip, FLAGS.cc_port), +DEFINE_string('ec2_url', 'http://%s:%s/services/Cloud' % (FLAGS.cc_host, FLAGS.cc_port), 'Url to ec2 api server') DEFINE_string('default_image', 'ami-11111', diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 65dcf51ee..53fb2df94 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -50,7 +50,7 @@ def init_host(): # forwarding entries and a default DNAT entry. _confirm_rule("-t nat -A nova_prerouting -s 0.0.0.0/0 " "-d 169.254.169.254/32 -p tcp -m tcp --dport 80 -j DNAT " - "--to-destination %s:%s" % (FLAGS.cc_ip, FLAGS.cc_port)) + "--to-destination %s:%s" % (FLAGS.cc_host, FLAGS.cc_port)) # NOTE(devcamcar): Cloud public SNAT entries and the default # SNAT rule for outbound traffic. -- cgit From 0880e49a4e9c9a246e8f4d7cc805d79947de095a Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 21 Sep 2010 10:07:59 -0500 Subject: Some more refactoring and another unit test --- nova/api/rackspace/auth.py | 17 +++++++---- nova/db/api.py | 4 +-- nova/db/sqlalchemy/api.py | 2 +- nova/tests/api/rackspace/auth.py | 51 ++++++++++++++++----------------- nova/tests/api/rackspace/test_helper.py | 29 ++++++++++++------- 5 files changed, 58 insertions(+), 45 deletions(-) diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index b29596880..1ef90c324 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -4,7 +4,7 @@ import time import webob.exc import webob.dec import hashlib - +from nova import flags from nova import auth from nova import manager from nova import db @@ -12,10 +12,16 @@ from nova import db class Context(object): pass -class BasicApiAuthManager(manager.Manager): +class BasicApiAuthManager(object): """ Implements a somewhat rudimentary version of Rackspace Auth""" def __init__(self): + if not host: + host = FLAGS.host + self.host = host + if not db_driver: + db_driver = FLAGS.db_driver + self.db = utils.import_object(db_driver) self.auth = auth.manager.AuthManager() self.context = Context() super(BasicApiAuthManager, self).__init__() @@ -64,8 +70,8 @@ class BasicApiAuthManager(manager.Manager): if delta.days >= 2: self.db.auth_destroy_token(self.context, token) else: - user = self.auth.get_user(self.context, token['user_id']) - return { 'id':user['id'] } + user = self.auth.get_user(token['user_id']) + return { 'id':user['uid'] } return None def _authorize_user(self, username, key): @@ -79,7 +85,8 @@ class BasicApiAuthManager(manager.Manager): token['cdn_management_url'] = '' token['server_management_url'] = self._get_server_mgmt_url() token['storage_url'] = '' - self.db.auth_create_token(self.context, token, user['id']) + token['user_id'] = user['uid'] + self.db.auth_create_token(self.context, token) return token, user return None, None diff --git a/nova/db/api.py b/nova/db/api.py index 80dde7a7a..0f0549edf 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -384,9 +384,9 @@ def auth_get_token(context, token_hash): """Retrieves a token given the hash representing it""" return IMPL.auth_get_token(context, token_hash) -def auth_create_token(context, token, user_id): +def auth_create_token(context, token): """Creates a new token""" - return IMPL.auth_create_token(context, token_hash, token, user_id) + return IMPL.auth_create_token(context, token_hash, token) ################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 681dec15e..78bc23b7b 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -551,7 +551,7 @@ def auth_get_token(_context, token_hash): raise exception.NotFound('Token %s does not exist' % token_hash) return tk -def auth_create_token(_context, token, user_id): +def auth_create_token(_context, token): tk = models.AuthToken() for k,v in token.iteritems(): tk[k] = v diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index 8ab10d94c..0f38ce79d 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -13,7 +13,8 @@ class Test(unittest.TestCase): self.stubs = stubout.StubOutForTesting() self.stubs.Set(nova.api.rackspace.auth.BasicApiAuthManager, '__init__', test_helper.fake_auth_init) - test_helper.auth_data = {} + test_helper.FakeAuthManager.auth_data = {} + test_helper.FakeAuthDatabase.data = {} def tearDown(self): self.stubs.UnsetAll() @@ -21,8 +22,22 @@ class Test(unittest.TestCase): def test_authorize_user(self): f = test_helper.FakeAuthManager() - f.add_user('derp', { 'id': 1, 'name':'herp' } ) + f.add_user('derp', { 'uid': 1, 'name':'herp' } ) + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-User'] = 'herp' + req.headers['X-Auth-Key'] = 'derp' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '204 No Content') + self.assertEqual(len(result.headers['X-Auth-Token']), 40) + self.assertEqual(result.headers['X-CDN-Management-Url'], + "") + self.assertEqual(result.headers['X-Storage-Url'], "") + + def test_authorize_token(self): + f = test_helper.FakeAuthManager() + f.add_user('derp', { 'uid': 1, 'name':'herp' } ) + req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'herp' req.headers['X-Auth-Key'] = 'derp' @@ -35,30 +50,14 @@ class Test(unittest.TestCase): "") self.assertEqual(result.headers['X-Storage-Url'], "") - #def test_authorize_token(self): - # auth = nova.api.rackspace.auth.FakeAuth() - # auth.add_user('herp', 'derp') - - # req = webob.Request.blank('/v1.0/') - # req.headers['X-Auth-User'] = 'herp' - # req.headers['X-Auth-Key'] = 'derp' - # result = req.get_response(nova.api.API()) - # self.assertEqual(result.status, '204 No Content') - # self.assertEqual(len(result.headers['X-Auth-Token']), 40) - # self.assertEqual(result.headers['X-Server-Management-Url'], - # "server_management_url") - # self.assertEqual(result.headers['X-CDN-Management-Url'], - # "cdn_management_url") - # self.assertEqual(result.headers['X-Storage-Url'], "storage_url") - - # token = result.headers['X-Auth-Token'] - # self.stubs.Set(nova.api.rackspace, 'APIRouter', - # test_helper.FakeRouter) - # req = webob.Request.blank('/v1.0/fake') - # req.headers['X-Auth-Token'] = token - # result = req.get_response(nova.api.API()) - # self.assertEqual(result.status, '200 OK') - # self.assertEqual(result.headers['X-Test-Success'], 'True') + token = result.headers['X-Auth-Token'] + self.stubs.Set(nova.api.rackspace, 'APIRouter', + test_helper.FakeRouter) + req = webob.Request.blank('/v1.0/fake') + req.headers['X-Auth-Token'] = token + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '200 OK') + self.assertEqual(result.headers['X-Test-Success'], 'True') def test_token_expiry(self): self.destroy_called = False diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 8d784854f..18d96d71e 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -1,10 +1,9 @@ import webob import webob.dec +import datetime from nova.wsgi import Router from nova import auth -auth_data = {} - class Context(object): pass @@ -26,25 +25,33 @@ def fake_auth_init(self): self.host = 'foo' class FakeAuthDatabase(object): + data = {} + @staticmethod def auth_get_token(context, token_hash): - pass + return FakeAuthDatabase.data.get(token_hash, None) @staticmethod - def auth_create_token(context, token, user_id): - pass + def auth_create_token(context, token): + token['created_at'] = datetime.datetime.now() + FakeAuthDatabase.data[token['token_hash']] = token @staticmethod def auth_destroy_token(context, token): - pass + if FakeAuthDatabase.data.has_key(token['token_hash']): + del FakeAuthDatabase.data['token_hash'] class FakeAuthManager(object): - def __init__(self): - global auth_data - self.data = auth_data + auth_data = {} def add_user(self, key, user): - self.data[key] = user + FakeAuthManager.auth_data[key] = user + + def get_user(self, uid): + for k, v in FakeAuthManager.auth_data.iteritems(): + if v['uid'] == uid: + return v + return None def get_user_from_access_key(self, key): - return self.data.get(key, None) + return FakeAuthManager.auth_data.get(key, None) -- cgit From dff6c134cb5b540ac1344faf9f0cbe7d19a8c9e7 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 21 Sep 2010 14:00:17 -0400 Subject: Fix quota unittest and don't run rbac unit tests for the moment --- nova/tests/api_unittest.py | 3 ++- nova/tests/cloud_unittest.py | 5 ++--- nova/tests/quota_unittest.py | 39 ++++++++++++++++++--------------------- run_tests.py | 3 ++- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 3d2724d12..0732c39bb 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -26,8 +26,9 @@ import StringIO import webob from nova import test -from nova.auth import manager from nova import api +from nova.api.ec2 import cloud +from nova.auth import manager class FakeHttplibSocket(object): diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 247ccc093..2f22982eb 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -63,9 +63,8 @@ class CloudTestCase(test.BaseTestCase): self.manager = manager.AuthManager() self.user = self.manager.create_user('admin', 'admin', 'admin', True) self.project = self.manager.create_project('proj', 'admin', 'proj') - self.context = context.APIRequestContext(handler=None, - user=self.user, - project=self.project) + self.context = context.APIRequestContext(user=self.user, + project=self.project) def tearDown(self): self.manager.delete_project(self.project) diff --git a/nova/tests/quota_unittest.py b/nova/tests/quota_unittest.py index cab9f663d..370ccd506 100644 --- a/nova/tests/quota_unittest.py +++ b/nova/tests/quota_unittest.py @@ -25,8 +25,8 @@ from nova import quota from nova import test from nova import utils from nova.auth import manager -from nova.endpoint import cloud -from nova.endpoint import api +from nova.api.ec2 import cloud +from nova.api.ec2 import context FLAGS = flags.FLAGS @@ -48,9 +48,8 @@ class QuotaTestCase(test.TrialTestCase): self.user = self.manager.create_user('admin', 'admin', 'admin', True) self.project = self.manager.create_project('admin', 'admin', 'admin') self.network = utils.import_object(FLAGS.network_manager) - self.context = api.APIRequestContext(handler=None, - project=self.project, - user=self.user) + self.context = context.APIRequestContext(project=self.project, + user=self.user) def tearDown(self): # pylint: disable-msg=C0103 manager.AuthManager().delete_project(self.project) @@ -95,11 +94,11 @@ class QuotaTestCase(test.TrialTestCase): for i in range(FLAGS.quota_instances): instance_id = self._create_instance() instance_ids.append(instance_id) - self.assertFailure(self.cloud.run_instances(self.context, - min_count=1, - max_count=1, - instance_type='m1.small'), - cloud.QuotaError) + self.assertRaises(cloud.QuotaError, self.cloud.run_instances, + self.context, + min_count=1, + max_count=1, + instance_type='m1.small') for instance_id in instance_ids: db.instance_destroy(self.context, instance_id) @@ -107,11 +106,11 @@ class QuotaTestCase(test.TrialTestCase): instance_ids = [] instance_id = self._create_instance(cores=4) instance_ids.append(instance_id) - self.assertFailure(self.cloud.run_instances(self.context, - min_count=1, - max_count=1, - instance_type='m1.small'), - cloud.QuotaError) + self.assertRaises(cloud.QuotaError, self.cloud.run_instances, + self.context, + min_count=1, + max_count=1, + instance_type='m1.small') for instance_id in instance_ids: db.instance_destroy(self.context, instance_id) @@ -120,10 +119,9 @@ class QuotaTestCase(test.TrialTestCase): for i in range(FLAGS.quota_volumes): volume_id = self._create_volume() volume_ids.append(volume_id) - self.assertRaises(cloud.QuotaError, - self.cloud.create_volume, - self.context, - size=10) + self.assertRaises(cloud.QuotaError, self.cloud.create_volume, + self.context, + size=10) for volume_id in volume_ids: db.volume_destroy(self.context, volume_id) @@ -151,5 +149,4 @@ class QuotaTestCase(test.TrialTestCase): # make an rpc.call, the test just finishes with OK. It # appears to be something in the magic inline callbacks # that is breaking. - self.assertFailure(self.cloud.allocate_address(self.context), - cloud.QuotaError) + self.assertRaises(cloud.QuotaError, self.cloud.allocate_address, self.context) diff --git a/run_tests.py b/run_tests.py index 4121f4c06..bea97c0b3 100644 --- a/run_tests.py +++ b/run_tests.py @@ -49,7 +49,8 @@ from nova import datastore from nova import flags from nova import twistd -from nova.tests.access_unittest import * +#TODO(gundlach): rewrite and readd this after merge +#from nova.tests.access_unittest import * from nova.tests.auth_unittest import * from nova.tests.api_unittest import * from nova.tests.cloud_unittest import * -- cgit From 6186634c0f8d6a44323fe1f7b2530528a539c64c Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 21 Sep 2010 11:12:04 -0700 Subject: bpython is amazing --- bin/nova-manage | 51 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index 26f75faf7..d23818348 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -135,15 +135,48 @@ class VpnCommands(object): class ShellCommands(object): - def run(self): - "Runs a Python interactive interpreter. Tries to use IPython, if it's available." - try: - import IPython - # Explicitly pass an empty list as arguments, because otherwise IPython - # would use sys.argv from this script. - shell = IPython.Shell.IPShell(argv=[]) - shell.mainloop() - except ImportError: + def bpython(self): + """Runs a bpython shell. + + Falls back to Ipython/python shell if unavailable""" + self.run('bpython') + + def ipython(self): + """Runs an Ipython shell. + + Falls back to Python shell if unavailable""" + self.run('ipython') + + def python(self): + """Runs an python shell. + + Falls back to Python shell if unavailable""" + self.run('python') + + def run(self, shell=None): + """Runs a Python interactive interpreter. + + args: [shell=bpython]""" + if not shell: + shell = 'bpython' + + if shell == 'bpython': + try: + import bpython + bpython.embed() + except ImportError: + shell = 'ipython' + if shell == 'ipython': + try: + import IPython + # Explicitly pass an empty list as arguments, because otherwise IPython + # would use sys.argv from this script. + shell = IPython.Shell.IPShell(argv=[]) + shell.mainloop() + except ImportError: + shell = 'python' + + if shell == 'python': import code try: # Try activating rlcompleter, because it's handy. import readline -- cgit From e027342cc647db080ee77de53b22126caf958339 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 21 Sep 2010 14:34:40 -0400 Subject: In desperation, I'm raising eventlet.__version__ so I can see why the trunk tests are failing. --- nova/tests/api_unittest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 0732c39bb..7e81d3dc8 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -107,6 +107,8 @@ class ApiEc2TestCase(test.BaseTestCase): def test_describe_instances(self): """Test that, after creating a user and a project, the describe instances call to the API works properly""" + import eventlet + raise KeyError(eventlet.__version__) self.expect_http() self.mox.ReplayAll() user = self.manager.create_user('fake', 'fake', 'fake') -- cgit From b82a9e3d3ca46e69a1583dea51a474456b867e6f Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 21 Sep 2010 15:00:43 -0400 Subject: Remove eventlet test, now that eventlet 0.9.10 has indeed been replaced by 0.9.12 per mtaylor --- nova/tests/api_unittest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 7e81d3dc8..0732c39bb 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -107,8 +107,6 @@ class ApiEc2TestCase(test.BaseTestCase): def test_describe_instances(self): """Test that, after creating a user and a project, the describe instances call to the API works properly""" - import eventlet - raise KeyError(eventlet.__version__) self.expect_http() self.mox.ReplayAll() user = self.manager.create_user('fake', 'fake', 'fake') -- cgit From 7a19f6f3978fc0942d5bc51a1ad3299968a4d215 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 21 Sep 2010 15:46:19 -0500 Subject: Missed the model include, and fixed a broke test after the merge --- nova/db/sqlalchemy/models.py | 3 ++- nova/tests/api/rackspace/auth.py | 2 ++ nova/tests/api/rackspace/test_helper.py | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index bd1e9164e..6e1c0ce16 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -475,7 +475,8 @@ def register_models(): """Register Models and create metadata""" from sqlalchemy import create_engine models = (Service, Instance, Volume, ExportDevice, - FixedIp, FloatingIp, Network, NetworkIndex) # , Image, Host) + FixedIp, FloatingIp, Network, NetworkIndex, + AuthToken) # , Image, Host) engine = create_engine(FLAGS.sql_connection, echo=False) for model in models: model.metadata.create_all(engine) diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index 0f38ce79d..429c22ad2 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -15,6 +15,8 @@ class Test(unittest.TestCase): '__init__', test_helper.fake_auth_init) test_helper.FakeAuthManager.auth_data = {} test_helper.FakeAuthDatabase.data = {} + self.stubs.Set(nova.api.rackspace, 'RateLimitingMiddleware', + test_helper.FakeRateLimiter) def tearDown(self): self.stubs.UnsetAll() diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 18d96d71e..be14e2de8 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -55,3 +55,11 @@ class FakeAuthManager(object): def get_user_from_access_key(self, key): return FakeAuthManager.auth_data.get(key, None) + +class FakeRateLimiter(object): + def __init__(self, application): + self.application = application + + @webob.dec.wsgify + def __call__(self, req): + return self.application -- cgit From 84fbfe09e10b330a5668e99422247801f370d0f9 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 21 Sep 2010 16:57:08 -0400 Subject: Rewrite rbac tests to use Authorizer middleware --- doc/source/auth.rst | 8 ---- nova/api/ec2/__init__.py | 3 ++ nova/auth/manager.py | 4 +- nova/tests/access_unittest.py | 108 ++++++++++++++++-------------------------- 4 files changed, 47 insertions(+), 76 deletions(-) diff --git a/doc/source/auth.rst b/doc/source/auth.rst index 70aca704a..3fcb309cd 100644 --- a/doc/source/auth.rst +++ b/doc/source/auth.rst @@ -172,14 +172,6 @@ Further Challenges -The :mod:`rbac` Module --------------------------- - -.. automodule:: nova.auth.rbac - :members: - :undoc-members: - :show-inheritance: - The :mod:`signer` Module ------------------------ diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index a7b10e428..b041787c2 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -25,6 +25,7 @@ import webob.dec import webob.exc from nova import exception +from nova import flags from nova import wsgi from nova.api.ec2 import apirequest from nova.api.ec2 import context @@ -33,6 +34,7 @@ from nova.api.ec2 import cloud from nova.auth import manager +FLAGS = flags.FLAGS _log = logging.getLogger("api") _log.setLevel(logging.DEBUG) @@ -176,6 +178,7 @@ class Authorizer(wsgi.Middleware): controller_name = req.environ['ec2.controller'].__class__.__name__ action = req.environ['ec2.action'] allowed_roles = self.action_roles[controller_name].get(action, []) + allowed_roles.extend(FLAGS.superuser_roles) if self._matches_any_role(context, allowed_roles): return self.application else: diff --git a/nova/auth/manager.py b/nova/auth/manager.py index bc3a8a12e..928e0fd69 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -44,7 +44,7 @@ flags.DEFINE_list('allowed_roles', # NOTE(vish): a user with one of these roles will be a superuser and # have access to all api commands flags.DEFINE_list('superuser_roles', ['cloudadmin'], - 'Roles that ignore rbac checking completely') + 'Roles that ignore authorization checking completely') # NOTE(vish): a user with one of these roles will have it for every # project, even if he or she is not a member of the project @@ -304,7 +304,7 @@ class AuthManager(object): return "%s:%s" % (user.access, Project.safe_id(project)) def is_superuser(self, user): - """Checks for superuser status, allowing user to bypass rbac + """Checks for superuser status, allowing user to bypass authorization @type user: User or uid @param user: User to check. diff --git a/nova/tests/access_unittest.py b/nova/tests/access_unittest.py index 59e1683db..d85f559f3 100644 --- a/nova/tests/access_unittest.py +++ b/nova/tests/access_unittest.py @@ -18,12 +18,13 @@ import unittest import logging +import webob from nova import exception from nova import flags from nova import test +from nova.api import ec2 from nova.auth import manager -from nova.auth import rbac FLAGS = flags.FLAGS @@ -72,9 +73,14 @@ class AccessTestCase(test.BaseTestCase): try: self.project.add_role(self.testsys, 'sysadmin') except: pass - self.context = Context() - self.context.project = self.project #user is set in each test + self.mw = ec2.Authorizer(lambda x,y: y('200 OK', []) and '') + self.mw.action_roles = {'str': { + '_allow_all': ['all'], + '_allow_none': [], + '_allow_project_manager': ['projectmanager'], + '_allow_sys_and_net': ['sysadmin', 'netadmin'], + '_allow_sysadmin': ['sysadmin']}} def tearDown(self): um = manager.AuthManager() @@ -87,76 +93,46 @@ class AccessTestCase(test.BaseTestCase): um.delete_user('testsys') super(AccessTestCase, self).tearDown() + def response_status(self, user, methodName): + context = Context() + context.project = self.project + context.user = user + environ = {'ec2.context' : context, + 'ec2.controller': 'some string', + 'ec2.action': methodName} + req = webob.Request.blank('/', environ) + resp = req.get_response(self.mw) + return resp.status_int + + def shouldAllow(self, user, methodName): + self.assertEqual(200, self.response_status(user, methodName)) + + def shouldDeny(self, user, methodName): + self.assertEqual(401, self.response_status(user, methodName)) + def test_001_allow_all(self): - self.context.user = self.testadmin - self.assertTrue(self._allow_all(self.context)) - self.context.user = self.testpmsys - self.assertTrue(self._allow_all(self.context)) - self.context.user = self.testnet - self.assertTrue(self._allow_all(self.context)) - self.context.user = self.testsys - self.assertTrue(self._allow_all(self.context)) + users = [self.testadmin, self.testpmsys, self.testnet, self.testsys] + for user in users: + self.shouldAllow(user, '_allow_all') def test_002_allow_none(self): - self.context.user = self.testadmin - self.assertTrue(self._allow_none(self.context)) - self.context.user = self.testpmsys - self.assertRaises(exception.NotAuthorized, self._allow_none, self.context) - self.context.user = self.testnet - self.assertRaises(exception.NotAuthorized, self._allow_none, self.context) - self.context.user = self.testsys - self.assertRaises(exception.NotAuthorized, self._allow_none, self.context) + self.shouldAllow(self.testadmin, '_allow_none') + users = [self.testpmsys, self.testnet, self.testsys] + for user in users: + self.shouldDeny(user, '_allow_none') def test_003_allow_project_manager(self): - self.context.user = self.testadmin - self.assertTrue(self._allow_project_manager(self.context)) - self.context.user = self.testpmsys - self.assertTrue(self._allow_project_manager(self.context)) - self.context.user = self.testnet - self.assertRaises(exception.NotAuthorized, self._allow_project_manager, self.context) - self.context.user = self.testsys - self.assertRaises(exception.NotAuthorized, self._allow_project_manager, self.context) + for user in [self.testadmin, self.testpmsys]: + self.shouldAllow(user, '_allow_project_manager') + for user in [self.testnet, self.testsys]: + self.shouldDeny(user, '_allow_project_manager') def test_004_allow_sys_and_net(self): - self.context.user = self.testadmin - self.assertTrue(self._allow_sys_and_net(self.context)) - self.context.user = self.testpmsys # doesn't have the per project sysadmin - self.assertRaises(exception.NotAuthorized, self._allow_sys_and_net, self.context) - self.context.user = self.testnet - self.assertTrue(self._allow_sys_and_net(self.context)) - self.context.user = self.testsys - self.assertTrue(self._allow_sys_and_net(self.context)) - - def test_005_allow_sys_no_pm(self): - self.context.user = self.testadmin - self.assertTrue(self._allow_sys_no_pm(self.context)) - self.context.user = self.testpmsys - self.assertRaises(exception.NotAuthorized, self._allow_sys_no_pm, self.context) - self.context.user = self.testnet - self.assertRaises(exception.NotAuthorized, self._allow_sys_no_pm, self.context) - self.context.user = self.testsys - self.assertTrue(self._allow_sys_no_pm(self.context)) - - @rbac.allow('all') - def _allow_all(self, context): - return True - - @rbac.allow('none') - def _allow_none(self, context): - return True - - @rbac.allow('projectmanager') - def _allow_project_manager(self, context): - return True - - @rbac.allow('sysadmin', 'netadmin') - def _allow_sys_and_net(self, context): - return True - - @rbac.allow('sysadmin') - @rbac.deny('projectmanager') - def _allow_sys_no_pm(self, context): - return True + for user in [self.testadmin, self.testnet, self.testsys]: + self.shouldAllow(user, '_allow_sys_and_net') + # denied because it doesn't have the per project sysadmin + for user in [self.testpmsys]: + self.shouldDeny(user, '_allow_sys_and_net') if __name__ == "__main__": # TODO: Implement use_fake as an option -- cgit From ffa426d68bfb3d1c2acaeef4c48d2662e88fc878 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 21 Sep 2010 16:58:08 -0400 Subject: Reenable access_unittest now that it works with new rbac --- run_tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/run_tests.py b/run_tests.py index bea97c0b3..4121f4c06 100644 --- a/run_tests.py +++ b/run_tests.py @@ -49,8 +49,7 @@ from nova import datastore from nova import flags from nova import twistd -#TODO(gundlach): rewrite and readd this after merge -#from nova.tests.access_unittest import * +from nova.tests.access_unittest import * from nova.tests.auth_unittest import * from nova.tests.api_unittest import * from nova.tests.cloud_unittest import * -- cgit From b68b73d08155483d19f4088baa6a4ffe73ef5f1d Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 21 Sep 2010 14:36:06 -0700 Subject: typo s/an/a --- bin/nova-manage | 2 +- nova/tests/rpc_unittest.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index 4ae2f20dc..c5c7322bf 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -149,7 +149,7 @@ class ShellCommands(object): self.run('ipython') def python(self): - """Runs an python shell. + """Runs a python shell. Falls back to Python shell if unavailable""" self.run('python') diff --git a/nova/tests/rpc_unittest.py b/nova/tests/rpc_unittest.py index e12a28fbc..f4d7b4b28 100644 --- a/nova/tests/rpc_unittest.py +++ b/nova/tests/rpc_unittest.py @@ -67,6 +67,17 @@ class RpcTestCase(test.BaseTestCase): except rpc.RemoteError as exc: self.assertEqual(int(exc.value), value) + def test_pass_object(self): + """Test that we can pass objects through rpc""" + class x(): + pass + obj = x() + x.foo = 'bar' + x.baz = 100 + + result = yield rpc.call('test', {"method": "echo", + "args": {"value": obj}}) + print result class TestReceiver(object): """Simple Proxy class so the consumer has methods to call -- cgit From d642716db42b229e879f6f4673f166beb8d55faa Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 21 Sep 2010 17:12:53 -0500 Subject: Added server index and detail differentiation --- nova/api/rackspace/__init__.py | 3 ++- nova/api/rackspace/servers.py | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index ac5365310..5f4020837 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -140,7 +140,8 @@ class APIRouter(wsgi.Router): def __init__(self): mapper = routes.Mapper() - mapper.resource("server", "servers", controller=servers.Controller()) + mapper.resource("server", "servers", controller=servers.Controller() + collection={'detail': 'GET'}) mapper.resource("image", "images", controller=images.Controller(), collection={'detail': 'GET'}) mapper.resource("flavor", "flavors", controller=flavors.Controller(), diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 5f685dbd4..3ba5af8cf 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -31,8 +31,9 @@ class Controller(base.Controller): 'application/xml': { "plurals": "servers", "attributes": { - "server": [ "id", "imageId", "flavorId", "hostId", "status", - "progress", "addresses", "metadata", "progress" ] + "server": [ "id", "imageId", "name", "flavorId", "hostId", + "status", "progress", "addresses", "metadata", + "progress" ] } } } @@ -41,7 +42,11 @@ class Controller(base.Controller): self.instdir = compute.InstanceDirectory() def index(self, req): - return [_entity_inst(instance_details(inst)) for inst in instdir.all] + allowed_keys = [ 'id', 'name'] + return [_entity_inst(inst, allowed_keys) for inst in instdir.all] + + def detail(self, req): + return [_entity_inst(inst) for inst in instdir.all] def show(self, req, id): inst = self.instdir.get(id) @@ -103,7 +108,7 @@ class Controller(base.Controller): inst.save() return _entity_inst(inst) - def _entity_inst(self, inst): + def _entity_inst(self, inst, allowed_keys=None): """ Maps everything to Rackspace-like attributes for return""" translated_keys = dict(metadata={}, status=state_description, @@ -119,4 +124,9 @@ class Controller(base.Controller): for key in filtered_keys:: del inst[key] + if allowed_keys: + for key in inst.keys(): + if key not in allowed_keys: + del inst[key] + return dict(server=inst) -- cgit From f3698b8da4bd63abfade32c9894ac2095672344e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 21 Sep 2010 15:44:49 -0700 Subject: cleaned up exception handling for fixed_ip_get --- nova/db/sqlalchemy/api.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 026cdf10d..daa9e991e 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -19,15 +19,13 @@ Implementation of SQLAlchemy backend """ -import sys - from nova import db from nova import exception from nova import flags from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ -from sqlalchemy.orm import exc, joinedload_all +from sqlalchemy.orm import joinedload_all from sqlalchemy.sql import func FLAGS = flags.FLAGS @@ -352,17 +350,14 @@ def fixed_ip_disassociate_all_by_timeout(_context, host, time): def fixed_ip_get_by_address(_context, address): session = get_session() - with session.begin(): - try: - return session.query(models.FixedIp - ).options(joinedload_all('instance') - ).filter_by(address=address - ).filter_by(deleted=False - ).one() - except exc.NoResultFound: - new_exc = exception.NotFound("No model for address %s" % address) - raise new_exc.__class__, new_exc, sys.exc_info()[2] - + result = session.query(models.FixedIp + ).options(joinedload_all('instance') + ).filter_by(address=address + ).filter_by(deleted=False + ).first() + if not result: + raise exception.NotFound("No model for address %s" % address) + return result def fixed_ip_get_instance(_context, address): session = get_session() -- cgit From b68ab98d6718d5a7237f5620e8caffc770dfe822 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 21 Sep 2010 19:24:19 -0400 Subject: User updatable name & description for images. --- nova/endpoint/cloud.py | 12 ++++++++++++ nova/endpoint/images.py | 6 ++++++ nova/objectstore/handler.py | 19 +++++++++++++++---- nova/objectstore/image.py | 12 ++++++++++++ nova/tests/cloud_unittest.py | 26 +++++++++++++++++++++++++- nova/tests/objectstore_unittest.py | 6 ++++++ 6 files changed, 76 insertions(+), 5 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index e29d09a89..b1678ecce 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -752,3 +752,15 @@ class CloudController(object): raise exception.ApiError('operation_type must be add or remove') result = images.modify(context, image_id, operation_type) return defer.succeed(result) + + @rbac.allow('projectmanager', 'sysadmin') + def set_image_name(self, context, image_id, name): + result = images.update_user_editable_field(context, image_id, + 'displayName', name) + return defer.succeed(result) + + @rbac.allow('projectmanager', 'sysadmin') + def set_image_description(self, context, image_id, name): + result = images.update_user_editable_field(context, image_id, + 'displayDescription', name) + return defer.succeed(result) diff --git a/nova/endpoint/images.py b/nova/endpoint/images.py index 4579cd81a..a63c059bb 100644 --- a/nova/endpoint/images.py +++ b/nova/endpoint/images.py @@ -43,6 +43,12 @@ def modify(context, image_id, operation): return True +def update_user_editable_field(context, image_id, field, value): + conn(context).make_request( + method='POST', + bucket='_images', + query_args=qs({'image_id': image_id, 'field': field, 'value': value})) + return True def register(context, image_location): """ rpc call to register a new image based from a manifest """ diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index 5c3dc286b..f4596e982 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -382,15 +382,26 @@ class ImagesResource(resource.Resource): def render_POST(self, request): # pylint: disable-msg=R0201 """Update image attributes: public/private""" + # image_id required for all requests image_id = get_argument(request, 'image_id', u'') - operation = get_argument(request, 'operation', u'') - image_object = image.Image(image_id) - if not image_object.is_authorized(request.context): + logging.debug("not authorized for handle_POST in images") raise exception.NotAuthorized - image_object.set_public(operation=='add') + operation = get_argument(request, 'operation', u'') + field = get_argument(request, 'field', u'') + value = get_argument(request, 'value', u'') + if operation: + # operation implies publicity toggle + logging.debug("handling publicity toggle") + image_object.set_public(operation=='add') + elif field: + # field implies user field editing (value can be blank) + logging.debug("update user field") + image_object.update_user_editable_field(field, value) + else: + logging.debug("unknown action for handle_POST in images") return '' diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index f3c02a425..ad1745af6 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -82,6 +82,18 @@ class Image(object): with open(os.path.join(self.path, 'info.json'), 'w') as f: json.dump(md, f) + def update_user_editable_field(self, field, value): + fields = ['displayName', 'displayDescription'] + if field not in fields: + raise KeyError("Invalid field: %s" % field) + info = self.metadata + if value: + info[field] = value + elif field in info: + del info[field] + with open(os.path.join(self.path, 'info.json'), 'w') as f: + json.dump(info, f) + @staticmethod def all(): images = [] diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index c36d5a34f..2e97180be 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -16,9 +16,13 @@ # License for the specific language governing permissions and limitations # under the License. +import json import logging +import os import StringIO +import tempfile import time + from tornado import ioloop from twisted.internet import defer import unittest @@ -32,15 +36,22 @@ from nova.auth import manager from nova.compute import power_state from nova.endpoint import api from nova.endpoint import cloud +from nova.objectstore import image FLAGS = flags.FLAGS +# Temp dirs for working with image attributes through the cloud controller +# (stole this from objectstore_unittest.py) +OSS_TEMPDIR = tempfile.mkdtemp(prefix='test_oss-') +IMAGES_PATH = os.path.join(OSS_TEMPDIR, 'images') +os.makedirs(IMAGES_PATH) + class CloudTestCase(test.BaseTestCase): def setUp(self): super(CloudTestCase, self).setUp() - self.flags(connection_type='fake') + self.flags(connection_type='fake', images_path=IMAGES_PATH) self.conn = rpc.Connection.instance() logging.getLogger().setLevel(logging.DEBUG) @@ -156,3 +167,16 @@ class CloudTestCase(test.BaseTestCase): #for i in xrange(4): # data = self.cloud.get_metadata(instance(i)['private_dns_name']) # self.assert_(data['meta-data']['ami-id'] == 'ami-%s' % i) + + def test_user_editable_endpoint(self): + pathdir = os.path.join(FLAGS.images_path, 'i-testing') + os.mkdir(pathdir) + info = {} + with open(os.path.join(pathdir, 'info.json'), 'w') as f: + json.dump(info, f) + yield self.cloud.set_image_description(self.context, 'i-testing', + 'Foo Img') + img = image.Image('i-testing') + self.assertEqual('Foo Img', img.metadata['displayDescription']) + self.cloud.set_image_description(self.context, 'i-testing', '') + self.assert_(not 'displayDescription' in img.metadata) diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index dece4b5d5..82c6e2c49 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -164,6 +164,12 @@ class ObjectStoreTestCase(test.BaseTestCase): self.context.project = self.auth_manager.get_project('proj2') self.assertFalse(my_img.is_authorized(self.context)) + # change user-editable fields + my_img.update_user_editable_field('displayName', 'my cool image') + self.assertEqual('my cool image', my_img.metadata['displayName']) + my_img.update_user_editable_field('displayName', '') + self.assert_(not 'displayName' in my_img.metadata) + class TestHTTPChannel(http.HTTPChannel): """Dummy site required for twisted.web""" -- cgit From a8c5901faaa98b7f0c06db086a03a0d38a210986 Mon Sep 17 00:00:00 2001 From: mdietz Date: Wed, 22 Sep 2010 18:46:55 +0000 Subject: Added a primary_key to AuthToken, fixed some unbound variables, and now all unit tests pass --- nova/api/rackspace/auth.py | 5 ++++- nova/db/sqlalchemy/models.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index 1ef90c324..ce5a967eb 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -8,6 +8,9 @@ from nova import flags from nova import auth from nova import manager from nova import db +from nova import utils + +FLAGS = flags.FLAGS class Context(object): pass @@ -15,7 +18,7 @@ class Context(object): class BasicApiAuthManager(object): """ Implements a somewhat rudimentary version of Rackspace Auth""" - def __init__(self): + def __init__(self, host=None, db_driver=None): if not host: host = FLAGS.host self.host = host diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 6e1c0ce16..161c5f1bc 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -401,7 +401,7 @@ class AuthToken(BASE, NovaBase): are a string representing the actual token and a user id for mapping to the actual user""" __tablename__ = 'auth_tokens' - token_hash = Column(String(255)) + token_hash = Column(String(255), primary_key=True) user_id = Column(Integer) server_manageent_url = Column(String(255)) storage_url = Column(String(255)) -- cgit From f3f271644eac4ec74ce3786840a7743aac4f6032 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 22 Sep 2010 15:57:24 -0400 Subject: Responding to eday's feedback -- make a clearer inner wsgi app --- nova/tests/access_unittest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nova/tests/access_unittest.py b/nova/tests/access_unittest.py index d85f559f3..c8a49d2ca 100644 --- a/nova/tests/access_unittest.py +++ b/nova/tests/access_unittest.py @@ -74,7 +74,10 @@ class AccessTestCase(test.BaseTestCase): self.project.add_role(self.testsys, 'sysadmin') except: pass #user is set in each test - self.mw = ec2.Authorizer(lambda x,y: y('200 OK', []) and '') + def noopWSGIApp(environ, start_response): + start_response('200 OK', []) + return [''] + self.mw = ec2.Authorizer(noopWSGIApp) self.mw.action_roles = {'str': { '_allow_all': ['all'], '_allow_none': [], -- cgit From 71c41338e4aac98ec03eec1c90ee99e43d51bcb7 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Wed, 22 Sep 2010 16:42:35 -0400 Subject: Add user display fields to instances & volumes. --- nova/db/sqlalchemy/models.py | 9 ++++++++- nova/endpoint/cloud.py | 32 ++++++++++++++++++++++++++++++++ nova/tests/cloud_unittest.py | 43 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 41013f41b..359087db0 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -233,7 +233,6 @@ class Instance(BASE, NovaBase): vcpus = Column(Integer) local_gb = Column(Integer) - hostname = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) @@ -247,6 +246,10 @@ class Instance(BASE, NovaBase): scheduled_at = Column(DateTime) launched_at = Column(DateTime) terminated_at = Column(DateTime) + + display_name = Column(String(255)) + display_description = Column(String(255)) + # TODO(vish): see Ewan's email about state improvements, probably # should be in a driver base class or some such # vmstate_state = running, halted, suspended, paused @@ -282,6 +285,10 @@ class Volume(BASE, NovaBase): launched_at = Column(DateTime) terminated_at = Column(DateTime) + display_name = Column(String(255)) + display_description = Column(String(255)) + + class Quota(BASE, NovaBase): """Represents quota overrides for a project""" __tablename__ = 'quotas' diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index b1678ecce..50fdee60b 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -280,6 +280,10 @@ class CloudController(object): 'volume_id': volume['str_id']}] else: v['attachmentSet'] = [{}] + if 'display_name' in volume: + v['display_name'] = volume['display_name'] + if 'display_description' in volume: + v['display_description'] = volume['display_description'] return v @rbac.allow('projectmanager', 'sysadmin') @@ -299,6 +303,8 @@ class CloudController(object): vol['availability_zone'] = FLAGS.storage_availability_zone vol['status'] = "creating" vol['attach_status'] = "detached" + vol['display_name'] = kwargs.get('display_name') + vol['display_description'] = kwargs.get('display_description') volume_ref = db.volume_create(context, vol) rpc.cast(FLAGS.scheduler_topic, @@ -367,6 +373,17 @@ class CloudController(object): lst = [lst] return [{label: x} for x in lst] + @rbac.allow('projectmanager', 'sysadmin') + def update_volume(self, context, volume_id, **kwargs): + updatable_fields = ['display_name', 'display_description'] + changes = {} + for field in updatable_fields: + if field in kwargs: + changes[field] = kwargs[field] + if changes: + db.volume_update(context, volume_id, kwargs) + return defer.succeed(True) + @rbac.allow('all') def describe_instances(self, context, **kwargs): return defer.succeed(self._format_describe_instances(context)) @@ -420,6 +437,8 @@ class CloudController(object): i['instanceType'] = instance['instance_type'] i['launchTime'] = instance['created_at'] i['amiLaunchIndex'] = instance['launch_index'] + i['displayName'] = instance['display_name'] + i['displayDescription'] = instance['display_description'] if not reservations.has_key(instance['reservation_id']): r = {} r['reservationId'] = instance['reservation_id'] @@ -589,6 +608,8 @@ class CloudController(object): base_options['user_data'] = kwargs.get('user_data', '') base_options['security_group'] = security_group base_options['instance_type'] = instance_type + base_options['display_name'] = kwargs.get('display_name') + base_options['display_description'] = kwargs.get('display_description') type_data = INSTANCE_TYPES[instance_type] base_options['memory_mb'] = type_data['memory_mb'] @@ -689,6 +710,17 @@ class CloudController(object): "instance_id": instance_ref['id']}}) return defer.succeed(True) + @rbac.allow('projectmanager', 'sysadmin') + def update_instance(self, context, instance_id, **kwargs): + updatable_fields = ['display_name', 'display_description'] + changes = {} + for field in updatable_fields: + if field in kwargs: + changes[field] = kwargs[field] + if changes: + db.instance_update(context, instance_id, kwargs) + return defer.succeed(True) + @rbac.allow('projectmanager', 'sysadmin') def delete_volume(self, context, volume_id, **kwargs): # TODO: return error if not authorized diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 2e97180be..18ad19ede 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -28,6 +28,7 @@ from twisted.internet import defer import unittest from xml.etree import ElementTree +from nova import db from nova import flags from nova import rpc from nova import test @@ -168,15 +169,47 @@ class CloudTestCase(test.BaseTestCase): # data = self.cloud.get_metadata(instance(i)['private_dns_name']) # self.assert_(data['meta-data']['ami-id'] == 'ami-%s' % i) - def test_user_editable_endpoint(self): - pathdir = os.path.join(FLAGS.images_path, 'i-testing') + def test_user_editable_image_endpoint(self): + pathdir = os.path.join(FLAGS.images_path, 'ami-testing') os.mkdir(pathdir) info = {} with open(os.path.join(pathdir, 'info.json'), 'w') as f: json.dump(info, f) - yield self.cloud.set_image_description(self.context, 'i-testing', + yield self.cloud.set_image_description(self.context, 'ami-testing', 'Foo Img') - img = image.Image('i-testing') + img = image.Image('ami-testing') self.assertEqual('Foo Img', img.metadata['displayDescription']) - self.cloud.set_image_description(self.context, 'i-testing', '') + self.cloud.set_image_description(self.context, 'ami-testing', '') self.assert_(not 'displayDescription' in img.metadata) + + def test_update_of_instance_display_fields(self): + inst = db.instance_create({}, {}) + self.cloud.update_instance(self.context, inst['id'], + display_name='c00l 1m4g3') + inst = db.instance_get({}, inst['id']) + self.assertEqual('c00l 1m4g3', inst['display_name']) + db.instance_destroy({}, inst['id']) + + def test_update_of_instance_wont_update_private_fields(self): + inst = db.instance_create({}, {}) + self.cloud.update_instance(self.context, inst['id'], + mac_address='DE:AD:BE:EF') + inst = db.instance_get({}, inst['id']) + self.assertEqual(None, inst['mac_address']) + db.instance_destroy({}, inst['id']) + + def test_update_of_volume_display_fields(self): + vol = db.volume_create({}, {}) + self.cloud.update_volume(self.context, vol['id'], + display_name='c00l v0lum3') + vol = db.volume_get({}, vol['id']) + self.assertEqual('c00l v0lum3', vol['display_name']) + db.volume_destroy({}, vol['id']) + + def test_update_of_volume_wont_update_private_fields(self): + vol = db.volume_create({}, {}) + self.cloud.update_volume(self.context, vol['id'], + mountpoint='/not/here') + vol = db.volume_get({}, vol['id']) + self.assertEqual(None, vol['mountpoint']) + db.volume_destroy({}, vol['id']) -- cgit From 6f82d0f84c9474e72ef70c9ff568d68031191e0a Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 22 Sep 2010 17:35:02 -0400 Subject: Soren's patch to fix part of ec2 --- nova/api/ec2/apirequest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index a3b20118f..a87c21fb3 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -68,10 +68,8 @@ class APIRequest(object): key = _camelcase_to_underscore(parts[0]) if len(parts) > 1: d = args.get(key, {}) - d[parts[1]] = value[0] + d[parts[1]] = value value = d - else: - value = value[0] args[key] = value for key in args.keys(): -- cgit From 9e12753508474b430c1b87fd7d59dcbc2d096042 Mon Sep 17 00:00:00 2001 From: mdietz Date: Wed, 22 Sep 2010 21:57:34 +0000 Subject: Re-added the ramdisk line I accidentally removed --- nova/db/sqlalchemy/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 161c5f1bc..f6ba7953f 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -213,7 +213,8 @@ class Instance(BASE, NovaBase): image_id = Column(String(255)) kernel_id = Column(String(255)) - + ramdisk_id = Column(String(255)) + # image_id = Column(Integer, ForeignKey('images.id'), nullable=True) # kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True) # ramdisk_id = Column(Integer, ForeignKey('images.id'), nullable=True) -- cgit From f188b5a02d34751e89fae60b4d3b1ef144f138d7 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 22 Sep 2010 19:11:04 -0400 Subject: Re-add root and metadata request handlers to EC2 API --- nova/api/__init__.py | 56 +++++++++++++++++++++++++-- nova/api/ec2/metadatarequesthandler.py | 71 ++++++++++++++++++++++++++++++++++ nova/tests/api/__init__.py | 30 ++++++++++++-- 3 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 nova/api/ec2/metadatarequesthandler.py diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 821f1deea..a0be05d86 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -23,9 +23,18 @@ Root WSGI middleware for all API controllers. import routes import webob.dec +from nova import flags from nova import wsgi from nova.api import ec2 from nova.api import rackspace +from nova.api.ec2 import metadatarequesthandler + + +flags.DEFINE_string('rsapi_subdomain', 'rs', + 'subdomain running the RS API') +flags.DEFINE_string('ec2api_subdomain', 'ec2', + 'subdomain running the EC2 API') +FLAGS = flags.FLAGS class API(wsgi.Router): @@ -33,13 +42,33 @@ class API(wsgi.Router): def __init__(self): mapper = routes.Mapper() - mapper.connect("/", controller=self.versions) - mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API()) - mapper.connect("/services/{path_info:.*}", controller=ec2.API()) + mapper.sub_domains = True + mapper.connect("/", controller=self.rsapi_versions, + conditions={'sub_domain': [FLAGS.rsapi_subdomain]}) + mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API(), + conditions={'sub_domain': [FLAGS.rsapi_subdomain]}) + + mapper.connect("/", controller=self.ec2api_versions, + conditions={'sub_domain': [FLAGS.ec2api_subdomain]}) + mapper.connect("/services/{path_info:.*}", controller=ec2.API(), + conditions={'sub_domain': [FLAGS.ec2api_subdomain]}) + mrh = metadatarequesthandler.MetadataRequestHandler() + for s in ['/latest', + '/2009-04-04', + '/2008-09-01', + '/2008-02-01', + '/2007-12-15', + '/2007-10-10', + '/2007-08-29', + '/2007-03-01', + '/2007-01-19', + '/1.0']: + mapper.connect('%s/{path_info:.*}' % s, controller=mrh, + conditions={'subdomain': FLAGS.ec2api_subdomain}) super(API, self).__init__(mapper) @webob.dec.wsgify - def versions(self, req): + def rsapi_versions(self, req): """Respond to a request for all OpenStack API versions.""" response = { "versions": [ @@ -48,3 +77,22 @@ class API(wsgi.Router): "application/xml": { "attributes": dict(version=["status", "id"])}} return wsgi.Serializer(req.environ, metadata).to_content_type(response) + + @webob.dec.wsgify + def ec2api_versions(self, req): + """Respond to a request for all EC2 versions.""" + # available api versions + versions = [ + '1.0', + '2007-01-19', + '2007-03-01', + '2007-08-29', + '2007-10-10', + '2007-12-15', + '2008-02-01', + '2008-09-01', + '2009-04-04', + ] + return ''.join('%s\n' % v for v in versions) + + diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py new file mode 100644 index 000000000..229e5a78d --- /dev/null +++ b/nova/api/ec2/metadatarequesthandler.py @@ -0,0 +1,71 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Metadata request handler.""" + +import webob.dec +import webob.exc + +from nova.api.ec2 import cloud + + +class MetadataRequestHandler(object): + + """Serve metadata from the EC2 API.""" + + def print_data(self, data): + if isinstance(data, dict): + output = '' + for key in data: + if key == '_name': + continue + output += key + if isinstance(data[key], dict): + if '_name' in data[key]: + output += '=' + str(data[key]['_name']) + else: + output += '/' + output += '\n' + return output[:-1] # cut off last \n + elif isinstance(data, list): + return '\n'.join(data) + else: + return str(data) + + def lookup(self, path, data): + items = path.split('/') + for item in items: + if item: + if not isinstance(data, dict): + return data + if not item in data: + return None + data = data[item] + return data + + @webob.dec.wsgify + def __call__(self, req): + cc = cloud.CloudController() + meta_data = cc.get_metadata(req.remote_addr) + if meta_data is None: + _log.error('Failed to get metadata for ip: %s' % req.remote_addr) + raise webob.exc.HTTPNotFound() + data = self.lookup(path, meta_data) + if data is None: + raise webob.exc.HTTPNotFound() + return self.print_data(data) diff --git a/nova/tests/api/__init__.py b/nova/tests/api/__init__.py index 4682c094e..fc1ab9ae2 100644 --- a/nova/tests/api/__init__.py +++ b/nova/tests/api/__init__.py @@ -25,6 +25,7 @@ import stubout import webob import webob.dec +import nova.exception from nova import api from nova.tests.api.test_helper import * @@ -36,25 +37,46 @@ class Test(unittest.TestCase): def tearDown(self): # pylint: disable-msg=C0103 self.stubs.UnsetAll() + def _request(self, url, subdomain, **kwargs): + environ_keys = {'HTTP_HOST': '%s.example.com' % subdomain} + environ_keys.update(kwargs) + req = webob.Request.blank(url, environ_keys) + return req.get_response(api.API()) + def test_rackspace(self): self.stubs.Set(api.rackspace, 'API', APIStub) - result = webob.Request.blank('/v1.0/cloud').get_response(api.API()) + result = self._request('/v1.0/cloud', 'rs') self.assertEqual(result.body, "/cloud") def test_ec2(self): self.stubs.Set(api.ec2, 'API', APIStub) - result = webob.Request.blank('/ec2/cloud').get_response(api.API()) + result = self._request('/services/cloud', 'ec2') self.assertEqual(result.body, "/cloud") def test_not_found(self): self.stubs.Set(api.ec2, 'API', APIStub) self.stubs.Set(api.rackspace, 'API', APIStub) - result = webob.Request.blank('/test/cloud').get_response(api.API()) + result = self._request('/test/cloud', 'ec2') self.assertNotEqual(result.body, "/cloud") def test_query_api_versions(self): - result = webob.Request.blank('/').get_response(api.API()) + result = self._request('/', 'rs') self.assertTrue('CURRENT' in result.body) + def test_metadata(self): + def go(url): + result = self._request(url, 'ec2', + REMOTE_ADDR='128.192.151.2') + # Each should get to the ORM layer and fail to find the IP + self.assertRaises(nova.exception.NotFound, go, '/latest/') + self.assertRaises(nova.exception.NotFound, go, '/2009-04-04/') + self.assertRaises(nova.exception.NotFound, go, '/1.0/') + + def test_ec2_root(self): + result = self._request('/', 'ec2') + self.assertTrue('2007-12-15\n' in result.body) + + + if __name__ == '__main__': unittest.main() -- cgit From 54122c0a156d1562be76dfde41bd62006f9ed426 Mon Sep 17 00:00:00 2001 From: Eric Day Date: Wed, 22 Sep 2010 17:54:57 -0700 Subject: Various loose ends for endpoint and tornado removal cleanup, including cloudpipe API addition, rpc.call() cleanup by removing tornado ioloop, and fixing bin/* programs. Tornado still exists as part of some test cases and those should be reworked to not require it. --- bin/nova-api | 47 +++++++++-------------------- bin/nova-api-new | 45 ---------------------------- bin/nova-manage | 3 +- nova/api/__init__.py | 2 ++ nova/api/cloudpipe/__init__.py | 68 ++++++++++++++++++++++++++++++++++++++++++ nova/cloudpipe/api.py | 59 ------------------------------------ nova/cloudpipe/pipelib.py | 3 +- nova/rpc.py | 34 +++++++++++---------- nova/tests/cloud_unittest.py | 1 - 9 files changed, 105 insertions(+), 157 deletions(-) delete mode 100755 bin/nova-api-new create mode 100644 nova/api/cloudpipe/__init__.py delete mode 100644 nova/cloudpipe/api.py diff --git a/bin/nova-api b/bin/nova-api index ede09d38c..8625c487f 100755 --- a/bin/nova-api +++ b/bin/nova-api @@ -1,31 +1,28 @@ #!/usr/bin/env python +# pylint: disable-msg=C0103 # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """ -Tornado daemon for the main API endpoint. +Nova API daemon. """ -import logging import os import sys -from tornado import httpserver -from tornado import ioloop # If ../nova/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... @@ -35,30 +32,14 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) +from nova import api from nova import flags -from nova import server from nova import utils -from nova.endpoint import admin -from nova.endpoint import api -from nova.endpoint import cloud +from nova import wsgi FLAGS = flags.FLAGS - - -def main(_argv): - """Load the controllers and start the tornado I/O loop.""" - controllers = { - 'Cloud': cloud.CloudController(), - 'Admin': admin.AdminController()} - _app = api.APIServerApplication(controllers) - - io_inst = ioloop.IOLoop.instance() - http_server = httpserver.HTTPServer(_app) - http_server.listen(FLAGS.cc_port) - logging.debug('Started HTTP server on %s', FLAGS.cc_port) - io_inst.start() - +flags.DEFINE_integer('api_port', 8773, 'API port') if __name__ == '__main__': utils.default_flagfile() - server.serve('nova-api', main) + wsgi.run_server(api.API(), FLAGS.api_port) diff --git a/bin/nova-api-new b/bin/nova-api-new deleted file mode 100755 index 8625c487f..000000000 --- a/bin/nova-api-new +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# pylint: disable-msg=C0103 -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Nova API daemon. -""" - -import os -import sys - -# If ../nova/__init__.py exists, add ../ to Python search path, so that -# it will override what happens to be installed in /usr/(local/)lib/python... -possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), - os.pardir, - os.pardir)) -if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): - sys.path.insert(0, possible_topdir) - -from nova import api -from nova import flags -from nova import utils -from nova import wsgi - -FLAGS = flags.FLAGS -flags.DEFINE_integer('api_port', 8773, 'API port') - -if __name__ == '__main__': - utils.default_flagfile() - wsgi.run_server(api.API(), FLAGS.api_port) diff --git a/bin/nova-manage b/bin/nova-manage index 824e00ac5..baa1cb4db 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -73,7 +73,6 @@ from nova import quota from nova import utils from nova.auth import manager from nova.cloudpipe import pipelib -from nova.endpoint import cloud FLAGS = flags.FLAGS @@ -84,7 +83,7 @@ class VpnCommands(object): def __init__(self): self.manager = manager.AuthManager() - self.pipe = pipelib.CloudPipe(cloud.CloudController()) + self.pipe = pipelib.CloudPipe() def list(self): """Print a listing of the VPNs for all projects.""" diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 821f1deea..ff9b94de9 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -24,6 +24,7 @@ import routes import webob.dec from nova import wsgi +from nova.api import cloudpipe from nova.api import ec2 from nova.api import rackspace @@ -36,6 +37,7 @@ class API(wsgi.Router): mapper.connect("/", controller=self.versions) mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API()) mapper.connect("/services/{path_info:.*}", controller=ec2.API()) + mapper.connect("/cloudpipe/{path_info:.*}", controller=cloudpipe.API()) super(API, self).__init__(mapper) @webob.dec.wsgify diff --git a/nova/api/cloudpipe/__init__.py b/nova/api/cloudpipe/__init__.py new file mode 100644 index 000000000..642f8ef6c --- /dev/null +++ b/nova/api/cloudpipe/__init__.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +REST API Request Handlers for CloudPipe +""" + +import logging +import urllib +import webob +import webob.dec +import webob.exc + +from nova import crypto +from nova import wsgi +from nova.auth import manager +from nova.api.ec2 import cloud + + +_log = logging.getLogger("api") +_log.setLevel(logging.DEBUG) + + +class API(wsgi.Application): + + def __init__(self): + self.controller = cloud.CloudController() + + @webob.dec.wsgify + def __call__(self, req): + if req.method == 'POST': + return self.sign_csr(req) + _log.debug("Cloudpipe path is %s" % req.path_info) + if req.path_info.endswith("/getca/"): + return self.send_root_ca(req) + return webob.exc.HTTPNotFound() + + def get_project_id_from_ip(self, ip): + instance = self.controller.get_instance_by_ip(ip) + return instance['project_id'] + + def send_root_ca(self, req): + _log.debug("Getting root ca") + project_id = self.get_project_id_from_ip(req.remote_addr) + res = webob.Response() + res.headers["Content-Type"] = "text/plain" + res.body = crypto.fetch_ca(project_id) + return res + + def sign_csr(self, req): + project_id = self.get_project_id_from_ip(req.remote_addr) + cert = self.str_params['cert'] + return crypto.sign_csr(urllib.unquote(cert), project_id) diff --git a/nova/cloudpipe/api.py b/nova/cloudpipe/api.py deleted file mode 100644 index 56aa89834..000000000 --- a/nova/cloudpipe/api.py +++ /dev/null @@ -1,59 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Tornado REST API Request Handlers for CloudPipe -""" - -import logging -import urllib - -import tornado.web - -from nova import crypto -from nova.auth import manager - - -_log = logging.getLogger("api") -_log.setLevel(logging.DEBUG) - - -class CloudPipeRequestHandler(tornado.web.RequestHandler): - def get(self, path): - path = self.request.path - _log.debug( "Cloudpipe path is %s" % path) - if path.endswith("/getca/"): - self.send_root_ca() - self.finish() - - def get_project_id_from_ip(self, ip): - cc = self.application.controllers['Cloud'] - instance = cc.get_instance_by_ip(ip) - instance['project_id'] - - def send_root_ca(self): - _log.debug( "Getting root ca") - project_id = self.get_project_id_from_ip(self.request.remote_ip) - self.set_header("Content-Type", "text/plain") - self.write(crypto.fetch_ca(project_id)) - - def post(self, *args, **kwargs): - project_id = self.get_project_id_from_ip(self.request.remote_ip) - cert = self.get_argument('cert', '') - self.write(crypto.sign_csr(urllib.unquote(cert), project_id)) - self.finish() diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py index b13a60292..abc14bbb6 100644 --- a/nova/cloudpipe/pipelib.py +++ b/nova/cloudpipe/pipelib.py @@ -32,6 +32,7 @@ from nova import exception from nova import flags from nova import utils from nova.auth import manager +from nova.api.ec2 import cloud from nova.api.ec2 import context @@ -43,7 +44,7 @@ flags.DEFINE_string('boot_script_template', class CloudPipe(object): def __init__(self, cloud_controller): - self.controller = cloud_controller + self.controller = cloud.CloudController() self.manager = manager.AuthManager() def launch_vpn_instance(self, project_id): diff --git a/nova/rpc.py b/nova/rpc.py index 84a9b5590..7e4d91a03 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -94,8 +94,6 @@ class Consumer(messaging.Consumer): injected.start() return injected - attachToTornado = attach_to_tornado - def fetch(self, no_ack=None, auto_ack=None, enable_callbacks=False): """Wraps the parent fetch with some logic for failed connections""" # TODO(vish): the logic for failed connections and logging should be @@ -266,27 +264,31 @@ def call(topic, msg): LOG.debug("MSG_ID is %s" % (msg_id)) conn = Connection.instance() - d = defer.Deferred() - consumer = DirectConsumer(connection=conn, msg_id=msg_id) - def deferred_receive(data, message): - """Acks message and callbacks or errbacks""" - message.ack() - if data['failure']: - return d.errback(RemoteError(*data['failure'])) - else: - return d.callback(data['result']) + class WaitMessage(object): - consumer.register_callback(deferred_receive) - injected = consumer.attach_to_tornado() + def __call__(self, data, message): + """Acks message and sets result.""" + message.ack() + if data['failure']: + self.result = RemoteError(*data['failure']) + else: + self.result = data['result'] - # clean up after the injected listened and return x - d.addCallback(lambda x: injected.stop() and x or x) + wait_msg = WaitMessage() + consumer = DirectConsumer(connection=conn, msg_id=msg_id) + consumer.register_callback(wait_msg) publisher = TopicPublisher(connection=conn, topic=topic) publisher.send(msg) publisher.close() - return d + + try: + consumer.wait(limit=1) + except StopIteration: + pass + consumer.close() + return wait_msg.result def cast(topic, msg): diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 2f22982eb..756ce519e 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -22,7 +22,6 @@ from M2Crypto import RSA import StringIO import time -from tornado import ioloop from twisted.internet import defer import unittest from xml.etree import ElementTree -- cgit From 378970b1495840a2a193dbecc3f9bb8701237744 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Thu, 23 Sep 2010 11:06:49 +0200 Subject: Compare project_id to '' using == (equality) rather than 'is' (identity). This is needed because '' isn't the same as u''. --- nova/auth/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index bc3a8a12e..2ec586419 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -266,7 +266,7 @@ class AuthManager(object): # NOTE(vish): if we stop using project name as id we need better # logic to find a default project for user - if project_id is '': + if project_id == '': project_id = user.name project = self.get_project(project_id) -- cgit From 08622cb48c200aa27e214fb14e47a741069b9bb0 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Thu, 23 Sep 2010 04:24:54 -0500 Subject: All timestamps should be in UTC. Without this patch, the scheduler unit tests fail for anyone sufficiently East of Greenwich. --- nova/scheduler/driver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index 2e6a5a835..c89d25a47 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -42,7 +42,8 @@ class Scheduler(object): def service_is_up(service): """Check whether a service is up based on last heartbeat.""" last_heartbeat = service['updated_at'] or service['created_at'] - elapsed = datetime.datetime.now() - last_heartbeat + # Timestamps in DB are UTC. + elapsed = datetime.datetime.utcnow() - last_heartbeat return elapsed < datetime.timedelta(seconds=FLAGS.service_down_time) def hosts_up(self, context, topic): -- cgit From d98c663d3e521d45586ed3922d93e0ca612a5639 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 23 Sep 2010 09:06:45 -0400 Subject: Added FLAGS.FAKE_subdomain letting you manually set the subdomain for testing on localhost. --- nova/api/__init__.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/nova/api/__init__.py b/nova/api/__init__.py index a0be05d86..8e4d844b2 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -34,6 +34,8 @@ flags.DEFINE_string('rsapi_subdomain', 'rs', 'subdomain running the RS API') flags.DEFINE_string('ec2api_subdomain', 'ec2', 'subdomain running the EC2 API') +flags.DEFINE_string('FAKE_subdomain', None, + 'set to rs or ec2 to fake the subdomain of the host for testing') FLAGS = flags.FLAGS @@ -41,17 +43,26 @@ class API(wsgi.Router): """Routes top-level requests to the appropriate controller.""" def __init__(self): + rsdomain = {'sub_domain': [FLAGS.rsapi_subdomain]} + ec2domain = {'sub_domain': [FLAGS.ec2api_subdomain]} + # If someone wants to pretend they're hitting the RS subdomain + # on their local box, they can set FAKE_subdomain to 'rs', which + # removes subdomain restrictions from the RS routes below. + if FLAGS.FAKE_subdomain == 'rs': + rsdomain = {} + elif FLAGS.FAKE_subdomain == 'ec2': + ec2domain = {} mapper = routes.Mapper() mapper.sub_domains = True mapper.connect("/", controller=self.rsapi_versions, - conditions={'sub_domain': [FLAGS.rsapi_subdomain]}) + conditions=rsdomain) mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API(), - conditions={'sub_domain': [FLAGS.rsapi_subdomain]}) + conditions=rsdomain) mapper.connect("/", controller=self.ec2api_versions, - conditions={'sub_domain': [FLAGS.ec2api_subdomain]}) + conditions=ec2domain) mapper.connect("/services/{path_info:.*}", controller=ec2.API(), - conditions={'sub_domain': [FLAGS.ec2api_subdomain]}) + conditions=ec2domain) mrh = metadatarequesthandler.MetadataRequestHandler() for s in ['/latest', '/2009-04-04', @@ -64,7 +75,7 @@ class API(wsgi.Router): '/2007-01-19', '/1.0']: mapper.connect('%s/{path_info:.*}' % s, controller=mrh, - conditions={'subdomain': FLAGS.ec2api_subdomain}) + conditions=ec2domain) super(API, self).__init__(mapper) @webob.dec.wsgify -- cgit From 24f589d421be9a15ad941c34128b4fa0bdc28db4 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 23 Sep 2010 09:13:27 -0400 Subject: Apply vish's patch --- bin/nova-api-new | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/nova-api-new b/bin/nova-api-new index 8625c487f..6f25ad8c7 100755 --- a/bin/nova-api-new +++ b/bin/nova-api-new @@ -34,12 +34,11 @@ if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): from nova import api from nova import flags -from nova import utils from nova import wsgi FLAGS = flags.FLAGS flags.DEFINE_integer('api_port', 8773, 'API port') if __name__ == '__main__': - utils.default_flagfile() + FLAGS(sys.argv) wsgi.run_server(api.API(), FLAGS.api_port) -- cgit From ebf71b08efc6ab3c590f71715aa16b925f17c38e Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Thu, 23 Sep 2010 15:47:29 +0200 Subject: Wrap WSGI container in server.serve to make it properly handle command line arguments as well as daemonise properly. Moved api and wsgi imports in the main() function to delay their inclusion until after python-daemon has closed all the file descriptors. Without this, eventlet's epoll fd gets opened before daemonize is called and thus its fd gets closed leading to very, very, very confusing errors. --- bin/nova-api-new | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bin/nova-api-new b/bin/nova-api-new index 6f25ad8c7..a5027700b 100755 --- a/bin/nova-api-new +++ b/bin/nova-api-new @@ -32,13 +32,18 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): sys.path.insert(0, possible_topdir) -from nova import api from nova import flags -from nova import wsgi +from nova import utils +from nova import server FLAGS = flags.FLAGS flags.DEFINE_integer('api_port', 8773, 'API port') -if __name__ == '__main__': - FLAGS(sys.argv) +def main(_args): + from nova import api + from nova import wsgi wsgi.run_server(api.API(), FLAGS.api_port) + +if __name__ == '__main__': + utils.default_flagfile() + server.serve('nova-api', main) -- cgit From 90669318581554a72890a6fd9c6837deb86c7e4c Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 23 Sep 2010 10:19:27 -0400 Subject: Spot-fix endpoint reference --- bin/nova-manage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index 824e00ac5..e9219c515 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -73,7 +73,7 @@ from nova import quota from nova import utils from nova.auth import manager from nova.cloudpipe import pipelib -from nova.endpoint import cloud +from nova.api.ec2 import cloud FLAGS = flags.FLAGS -- cgit From c9ac49b2425b932f60a87da80887d4556806ca60 Mon Sep 17 00:00:00 2001 From: Eric Day Date: Thu, 23 Sep 2010 11:21:14 -0700 Subject: Fixed cloudpipe lib init. --- nova/api/cloudpipe/__init__.py | 1 + nova/cloudpipe/pipelib.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nova/api/cloudpipe/__init__.py b/nova/api/cloudpipe/__init__.py index 642f8ef6c..6d40990a8 100644 --- a/nova/api/cloudpipe/__init__.py +++ b/nova/api/cloudpipe/__init__.py @@ -51,6 +51,7 @@ class API(wsgi.Application): return webob.exc.HTTPNotFound() def get_project_id_from_ip(self, ip): + # TODO(eday): This was removed with the ORM branch, fix! instance = self.controller.get_instance_by_ip(ip) return instance['project_id'] diff --git a/nova/cloudpipe/pipelib.py b/nova/cloudpipe/pipelib.py index abc14bbb6..706a175d9 100644 --- a/nova/cloudpipe/pipelib.py +++ b/nova/cloudpipe/pipelib.py @@ -32,6 +32,7 @@ from nova import exception from nova import flags from nova import utils from nova.auth import manager +# TODO(eday): Eventually changes these to something not ec2-specific from nova.api.ec2 import cloud from nova.api.ec2 import context @@ -43,7 +44,7 @@ flags.DEFINE_string('boot_script_template', class CloudPipe(object): - def __init__(self, cloud_controller): + def __init__(self): self.controller = cloud.CloudController() self.manager = manager.AuthManager() -- cgit From 4f2edd43ca2c4a175b4d9dce23ae9e28941122e2 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 23 Sep 2010 11:23:32 -0700 Subject: renamed ipchains to iptables --- setup_iptables.sh | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ tools/setup_ipchains.sh | 94 ------------------------------------------------- 2 files changed, 94 insertions(+), 94 deletions(-) create mode 100644 setup_iptables.sh delete mode 100644 tools/setup_ipchains.sh diff --git a/setup_iptables.sh b/setup_iptables.sh new file mode 100644 index 000000000..b1ab1c6f7 --- /dev/null +++ b/setup_iptables.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +CMD="global" +IP="XXX" +PRIVATE_RANGE="10.128.0.0/12" + +if [ -n "$1" ]; then + CMD=$1 +fi + +if [ -n "$2" ]; then + IP=$2 +fi + +if [ -n "$3" ]; then + PRIVATE_RANGE=$3 +fi + +if [ "$CMD" == "global" ]; then + iptables -P INPUT DROP + iptables -A INPUT -m state --state INVALID -j DROP + iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -A INPUT -m tcp -p tcp -d $MGMT_IP --dport 22 -j ACCEPT + iptables -A INPUT -m udp -p udp --dport 123 -j ACCEPT + iptables -N nova_input + iptables -A INPUT -j nova_input + iptables -A INPUT -p icmp -j ACCEPT + iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset + iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable + + iptables -P FORWARD DROP + iptables -A FORWARD -m state --state INVALID -j DROP + iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu + iptables -N nova_forward + iptables -A FORWARD -j nova_forward + + iptables -P OUTPUT DROP + iptables -A OUTPUT -m state --state INVALID -j DROP + iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -N nova_output + iptables -A OUTPUT -j nova_output + + iptables -t nat -N nova_prerouting + iptables -t nat -A PREROUTING -j nova_prerouting + + iptables -t nat -N nova_postrouting + iptables -t nat -A POSTROUTING -j nova_postrouting + + iptables -t nat -N nova_output + iptables -t nat -A OUTPUT -j nova_output + + # ganglia (all hosts) + iptables -A nova_input -m tcp -p tcp -d $IP --dport 8649 -j ACCEPT + iptables -A nova_input -m udp -p udp -d $IP --dport 8649 -j ACCEPT +fi + +if [ "$CMD" == "dashboard" ]; then + # dashboard + iptables -A nova_input -m tcp -p tcp -d $IP --dport 80 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 443 -j ACCEPT +fi + +if [ "$CMD" == "objectstore" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 3333 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 8773 -j ACCEPT +fi + +if [ "$CMD" == "redis" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 6379 -j ACCEPT +fi + +if [ "$CMD" == "mysql" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 3306 -j ACCEPT +fi + +if [ "$CMD" == "rabbitmq" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 4369 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 5672 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 53284 -j ACCEPT +fi + +if [ "$CMD" == "dnsmasq" ]; then + # NOTE(vish): this could theoretically be setup per network + # for each host, but it seems like overkill + iptables -A nova_input -m tcp -p tcp -s $PRIVATE_RANGE --dport 53 -j ACCEPT + iptables -A nova_input -m udp -p udp -s $PRIVATE_RANGE --dport 53 -j ACCEPT + iptables -A nova_input -m udp -p udp --dport 67 -j ACCEPT + +if [ "$CMD" == "ldap" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 389 -j ACCEPT +fi + + diff --git a/tools/setup_ipchains.sh b/tools/setup_ipchains.sh deleted file mode 100644 index b1ab1c6f7..000000000 --- a/tools/setup_ipchains.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash - -CMD="global" -IP="XXX" -PRIVATE_RANGE="10.128.0.0/12" - -if [ -n "$1" ]; then - CMD=$1 -fi - -if [ -n "$2" ]; then - IP=$2 -fi - -if [ -n "$3" ]; then - PRIVATE_RANGE=$3 -fi - -if [ "$CMD" == "global" ]; then - iptables -P INPUT DROP - iptables -A INPUT -m state --state INVALID -j DROP - iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT - iptables -A INPUT -m tcp -p tcp -d $MGMT_IP --dport 22 -j ACCEPT - iptables -A INPUT -m udp -p udp --dport 123 -j ACCEPT - iptables -N nova_input - iptables -A INPUT -j nova_input - iptables -A INPUT -p icmp -j ACCEPT - iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset - iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable - - iptables -P FORWARD DROP - iptables -A FORWARD -m state --state INVALID -j DROP - iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT - iptables -A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu - iptables -N nova_forward - iptables -A FORWARD -j nova_forward - - iptables -P OUTPUT DROP - iptables -A OUTPUT -m state --state INVALID -j DROP - iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT - iptables -N nova_output - iptables -A OUTPUT -j nova_output - - iptables -t nat -N nova_prerouting - iptables -t nat -A PREROUTING -j nova_prerouting - - iptables -t nat -N nova_postrouting - iptables -t nat -A POSTROUTING -j nova_postrouting - - iptables -t nat -N nova_output - iptables -t nat -A OUTPUT -j nova_output - - # ganglia (all hosts) - iptables -A nova_input -m tcp -p tcp -d $IP --dport 8649 -j ACCEPT - iptables -A nova_input -m udp -p udp -d $IP --dport 8649 -j ACCEPT -fi - -if [ "$CMD" == "dashboard" ]; then - # dashboard - iptables -A nova_input -m tcp -p tcp -d $IP --dport 80 -j ACCEPT - iptables -A nova_input -m tcp -p tcp -d $IP --dport 443 -j ACCEPT -fi - -if [ "$CMD" == "objectstore" ]; then - iptables -A nova_input -m tcp -p tcp -d $IP --dport 3333 -j ACCEPT - iptables -A nova_input -m tcp -p tcp -d $IP --dport 8773 -j ACCEPT -fi - -if [ "$CMD" == "redis" ]; then - iptables -A nova_input -m tcp -p tcp -d $IP --dport 6379 -j ACCEPT -fi - -if [ "$CMD" == "mysql" ]; then - iptables -A nova_input -m tcp -p tcp -d $IP --dport 3306 -j ACCEPT -fi - -if [ "$CMD" == "rabbitmq" ]; then - iptables -A nova_input -m tcp -p tcp -d $IP --dport 4369 -j ACCEPT - iptables -A nova_input -m tcp -p tcp -d $IP --dport 5672 -j ACCEPT - iptables -A nova_input -m tcp -p tcp -d $IP --dport 53284 -j ACCEPT -fi - -if [ "$CMD" == "dnsmasq" ]; then - # NOTE(vish): this could theoretically be setup per network - # for each host, but it seems like overkill - iptables -A nova_input -m tcp -p tcp -s $PRIVATE_RANGE --dport 53 -j ACCEPT - iptables -A nova_input -m udp -p udp -s $PRIVATE_RANGE --dport 53 -j ACCEPT - iptables -A nova_input -m udp -p udp --dport 67 -j ACCEPT - -if [ "$CMD" == "ldap" ]; then - iptables -A nova_input -m tcp -p tcp -d $IP --dport 389 -j ACCEPT -fi - - -- cgit From 94b9d491d4f691f7ede3c0d5d8ca98288af1646f Mon Sep 17 00:00:00 2001 From: mdietz Date: Thu, 23 Sep 2010 18:28:49 +0000 Subject: Missed the model include, and fixed a broken test after the merge --- nova/db/sqlalchemy/models.py | 3 ++- nova/tests/api/rackspace/auth.py | 2 ++ nova/tests/api/rackspace/test_helper.py | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index bd1e9164e..6e1c0ce16 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -475,7 +475,8 @@ def register_models(): """Register Models and create metadata""" from sqlalchemy import create_engine models = (Service, Instance, Volume, ExportDevice, - FixedIp, FloatingIp, Network, NetworkIndex) # , Image, Host) + FixedIp, FloatingIp, Network, NetworkIndex, + AuthToken) # , Image, Host) engine = create_engine(FLAGS.sql_connection, echo=False) for model in models: model.metadata.create_all(engine) diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index 0f38ce79d..429c22ad2 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -15,6 +15,8 @@ class Test(unittest.TestCase): '__init__', test_helper.fake_auth_init) test_helper.FakeAuthManager.auth_data = {} test_helper.FakeAuthDatabase.data = {} + self.stubs.Set(nova.api.rackspace, 'RateLimitingMiddleware', + test_helper.FakeRateLimiter) def tearDown(self): self.stubs.UnsetAll() diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 18d96d71e..be14e2de8 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -55,3 +55,11 @@ class FakeAuthManager(object): def get_user_from_access_key(self, key): return FakeAuthManager.auth_data.get(key, None) + +class FakeRateLimiter(object): + def __init__(self, application): + self.application = application + + @webob.dec.wsgify + def __call__(self, req): + return self.application -- cgit From 020f1a304c15db3086169efe67994ca59ca04e0c Mon Sep 17 00:00:00 2001 From: mdietz Date: Thu, 23 Sep 2010 18:29:17 +0000 Subject: Added a primary_key to AuthToken, fixed some unbound variables, and now all unit tests pass --- nova/api/rackspace/auth.py | 5 ++++- nova/db/sqlalchemy/models.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index 1ef90c324..ce5a967eb 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -8,6 +8,9 @@ from nova import flags from nova import auth from nova import manager from nova import db +from nova import utils + +FLAGS = flags.FLAGS class Context(object): pass @@ -15,7 +18,7 @@ class Context(object): class BasicApiAuthManager(object): """ Implements a somewhat rudimentary version of Rackspace Auth""" - def __init__(self): + def __init__(self, host=None, db_driver=None): if not host: host = FLAGS.host self.host = host diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 6e1c0ce16..161c5f1bc 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -401,7 +401,7 @@ class AuthToken(BASE, NovaBase): are a string representing the actual token and a user id for mapping to the actual user""" __tablename__ = 'auth_tokens' - token_hash = Column(String(255)) + token_hash = Column(String(255), primary_key=True) user_id = Column(Integer) server_manageent_url = Column(String(255)) storage_url = Column(String(255)) -- cgit From ca854c764a21985fd07becf7b0686f5d00125851 Mon Sep 17 00:00:00 2001 From: mdietz Date: Thu, 23 Sep 2010 18:29:40 +0000 Subject: Re-added the ramdisk line I accidentally removed --- nova/db/sqlalchemy/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 161c5f1bc..f6ba7953f 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -213,7 +213,8 @@ class Instance(BASE, NovaBase): image_id = Column(String(255)) kernel_id = Column(String(255)) - + ramdisk_id = Column(String(255)) + # image_id = Column(Integer, ForeignKey('images.id'), nullable=True) # kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True) # ramdisk_id = Column(Integer, ForeignKey('images.id'), nullable=True) -- cgit From 44a3fe22d72f7359f57e7eb9ce443c974391991c Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 23 Sep 2010 11:40:09 -0700 Subject: fixed a couple of typos --- nova/manager.py | 3 ++- nova/network/manager.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/nova/manager.py b/nova/manager.py index b7b97bced..65300354b 100644 --- a/nova/manager.py +++ b/nova/manager.py @@ -40,7 +40,8 @@ class Manager(object): def init_host(self): """Do any initialization that needs to be run if this is a standalone service. - + Child classes should override this method. """ + pass diff --git a/nova/network/manager.py b/nova/network/manager.py index dcca21127..abe4dcebc 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -218,12 +218,12 @@ class FlatManager(NetworkManager): class VlanManager(NetworkManager): """Vlan network with dhcp""" - + def init_host(self): """Do any initialization that needs to be run if this is a standalone service. """ - driver.init_host() + self.driver.init_host() def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): """Gets a fixed ip from the pool""" -- cgit From a6954efa3155868d31163236aa9e44f693f51b30 Mon Sep 17 00:00:00 2001 From: Eric Day Date: Thu, 23 Sep 2010 11:56:44 -0700 Subject: Fixed rpc consumer to use unique return connection to prevent overlap. This could be reworked to share a connection, but it should be a wait operation and not a fast poll like it was before. We could also keep a cache of opened connections to be used between requests. --- nova/rpc.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/nova/rpc.py b/nova/rpc.py index 7e4d91a03..6363335ea 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -46,9 +46,9 @@ LOG.setLevel(logging.DEBUG) class Connection(carrot_connection.BrokerConnection): """Connection instance object""" @classmethod - def instance(cls): + def instance(cls, new=False): """Returns the instance""" - if not hasattr(cls, '_instance'): + if new or not hasattr(cls, '_instance'): params = dict(hostname=FLAGS.rabbit_host, port=FLAGS.rabbit_port, userid=FLAGS.rabbit_userid, @@ -60,7 +60,10 @@ class Connection(carrot_connection.BrokerConnection): # NOTE(vish): magic is fun! # pylint: disable-msg=W0142 - cls._instance = cls(**params) + if new: + return cls(**params) + else: + cls._instance = cls(**params) return cls._instance @classmethod @@ -263,8 +266,6 @@ def call(topic, msg): msg.update({'_msg_id': msg_id}) LOG.debug("MSG_ID is %s" % (msg_id)) - conn = Connection.instance() - class WaitMessage(object): def __call__(self, data, message): @@ -276,9 +277,11 @@ def call(topic, msg): self.result = data['result'] wait_msg = WaitMessage() + conn = Connection.instance(True) consumer = DirectConsumer(connection=conn, msg_id=msg_id) consumer.register_callback(wait_msg) + conn = Connection.instance() publisher = TopicPublisher(connection=conn, topic=topic) publisher.send(msg) publisher.close() -- cgit From 47a957acb176d108aac4183cbf5a882149d7462d Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 23 Sep 2010 11:58:33 -0700 Subject: put setup_iptables in the right dir --- setup_iptables.sh | 94 ------------------------------------------------- tools/setup_iptables.sh | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 94 deletions(-) delete mode 100644 setup_iptables.sh create mode 100644 tools/setup_iptables.sh diff --git a/setup_iptables.sh b/setup_iptables.sh deleted file mode 100644 index b1ab1c6f7..000000000 --- a/setup_iptables.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash - -CMD="global" -IP="XXX" -PRIVATE_RANGE="10.128.0.0/12" - -if [ -n "$1" ]; then - CMD=$1 -fi - -if [ -n "$2" ]; then - IP=$2 -fi - -if [ -n "$3" ]; then - PRIVATE_RANGE=$3 -fi - -if [ "$CMD" == "global" ]; then - iptables -P INPUT DROP - iptables -A INPUT -m state --state INVALID -j DROP - iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT - iptables -A INPUT -m tcp -p tcp -d $MGMT_IP --dport 22 -j ACCEPT - iptables -A INPUT -m udp -p udp --dport 123 -j ACCEPT - iptables -N nova_input - iptables -A INPUT -j nova_input - iptables -A INPUT -p icmp -j ACCEPT - iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset - iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable - - iptables -P FORWARD DROP - iptables -A FORWARD -m state --state INVALID -j DROP - iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT - iptables -A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu - iptables -N nova_forward - iptables -A FORWARD -j nova_forward - - iptables -P OUTPUT DROP - iptables -A OUTPUT -m state --state INVALID -j DROP - iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT - iptables -N nova_output - iptables -A OUTPUT -j nova_output - - iptables -t nat -N nova_prerouting - iptables -t nat -A PREROUTING -j nova_prerouting - - iptables -t nat -N nova_postrouting - iptables -t nat -A POSTROUTING -j nova_postrouting - - iptables -t nat -N nova_output - iptables -t nat -A OUTPUT -j nova_output - - # ganglia (all hosts) - iptables -A nova_input -m tcp -p tcp -d $IP --dport 8649 -j ACCEPT - iptables -A nova_input -m udp -p udp -d $IP --dport 8649 -j ACCEPT -fi - -if [ "$CMD" == "dashboard" ]; then - # dashboard - iptables -A nova_input -m tcp -p tcp -d $IP --dport 80 -j ACCEPT - iptables -A nova_input -m tcp -p tcp -d $IP --dport 443 -j ACCEPT -fi - -if [ "$CMD" == "objectstore" ]; then - iptables -A nova_input -m tcp -p tcp -d $IP --dport 3333 -j ACCEPT - iptables -A nova_input -m tcp -p tcp -d $IP --dport 8773 -j ACCEPT -fi - -if [ "$CMD" == "redis" ]; then - iptables -A nova_input -m tcp -p tcp -d $IP --dport 6379 -j ACCEPT -fi - -if [ "$CMD" == "mysql" ]; then - iptables -A nova_input -m tcp -p tcp -d $IP --dport 3306 -j ACCEPT -fi - -if [ "$CMD" == "rabbitmq" ]; then - iptables -A nova_input -m tcp -p tcp -d $IP --dport 4369 -j ACCEPT - iptables -A nova_input -m tcp -p tcp -d $IP --dport 5672 -j ACCEPT - iptables -A nova_input -m tcp -p tcp -d $IP --dport 53284 -j ACCEPT -fi - -if [ "$CMD" == "dnsmasq" ]; then - # NOTE(vish): this could theoretically be setup per network - # for each host, but it seems like overkill - iptables -A nova_input -m tcp -p tcp -s $PRIVATE_RANGE --dport 53 -j ACCEPT - iptables -A nova_input -m udp -p udp -s $PRIVATE_RANGE --dport 53 -j ACCEPT - iptables -A nova_input -m udp -p udp --dport 67 -j ACCEPT - -if [ "$CMD" == "ldap" ]; then - iptables -A nova_input -m tcp -p tcp -d $IP --dport 389 -j ACCEPT -fi - - diff --git a/tools/setup_iptables.sh b/tools/setup_iptables.sh new file mode 100644 index 000000000..b1ab1c6f7 --- /dev/null +++ b/tools/setup_iptables.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +CMD="global" +IP="XXX" +PRIVATE_RANGE="10.128.0.0/12" + +if [ -n "$1" ]; then + CMD=$1 +fi + +if [ -n "$2" ]; then + IP=$2 +fi + +if [ -n "$3" ]; then + PRIVATE_RANGE=$3 +fi + +if [ "$CMD" == "global" ]; then + iptables -P INPUT DROP + iptables -A INPUT -m state --state INVALID -j DROP + iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -A INPUT -m tcp -p tcp -d $MGMT_IP --dport 22 -j ACCEPT + iptables -A INPUT -m udp -p udp --dport 123 -j ACCEPT + iptables -N nova_input + iptables -A INPUT -j nova_input + iptables -A INPUT -p icmp -j ACCEPT + iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset + iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable + + iptables -P FORWARD DROP + iptables -A FORWARD -m state --state INVALID -j DROP + iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu + iptables -N nova_forward + iptables -A FORWARD -j nova_forward + + iptables -P OUTPUT DROP + iptables -A OUTPUT -m state --state INVALID -j DROP + iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -N nova_output + iptables -A OUTPUT -j nova_output + + iptables -t nat -N nova_prerouting + iptables -t nat -A PREROUTING -j nova_prerouting + + iptables -t nat -N nova_postrouting + iptables -t nat -A POSTROUTING -j nova_postrouting + + iptables -t nat -N nova_output + iptables -t nat -A OUTPUT -j nova_output + + # ganglia (all hosts) + iptables -A nova_input -m tcp -p tcp -d $IP --dport 8649 -j ACCEPT + iptables -A nova_input -m udp -p udp -d $IP --dport 8649 -j ACCEPT +fi + +if [ "$CMD" == "dashboard" ]; then + # dashboard + iptables -A nova_input -m tcp -p tcp -d $IP --dport 80 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 443 -j ACCEPT +fi + +if [ "$CMD" == "objectstore" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 3333 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 8773 -j ACCEPT +fi + +if [ "$CMD" == "redis" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 6379 -j ACCEPT +fi + +if [ "$CMD" == "mysql" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 3306 -j ACCEPT +fi + +if [ "$CMD" == "rabbitmq" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 4369 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 5672 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 53284 -j ACCEPT +fi + +if [ "$CMD" == "dnsmasq" ]; then + # NOTE(vish): this could theoretically be setup per network + # for each host, but it seems like overkill + iptables -A nova_input -m tcp -p tcp -s $PRIVATE_RANGE --dport 53 -j ACCEPT + iptables -A nova_input -m udp -p udp -s $PRIVATE_RANGE --dport 53 -j ACCEPT + iptables -A nova_input -m udp -p udp --dport 67 -j ACCEPT + +if [ "$CMD" == "ldap" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 389 -j ACCEPT +fi + + -- cgit From 564105a3f0087f31a879460d70e73bc358e0e8c0 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 23 Sep 2010 12:18:56 -0700 Subject: made use of nova_ chains a flag and fixed a few typos --- nova/network/linux_net.py | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 53fb2df94..149848750 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -41,6 +41,8 @@ flags.DEFINE_string('bridge_dev', 'eth0', 'network device for bridges') flags.DEFINE_string('routing_source_ip', utils.get_my_ip(), 'Public IP of network host') +flags.DEFINE_string('use_nova_chains', False, + 'use the nova_ routing chains instead of default') DEFAULT_PORTS = [("tcp", 80), ("tcp", 22), ("udp", 1194), ("tcp", 443)] @@ -48,18 +50,18 @@ def init_host(): """Basic networking setup goes here""" # NOTE(devcamcar): Cloud public DNAT entries, CloudPipe port # forwarding entries and a default DNAT entry. - _confirm_rule("-t nat -A nova_prerouting -s 0.0.0.0/0 " + _confirm_rule("PREROUTING", "-t nat -s 0.0.0.0/0 " "-d 169.254.169.254/32 -p tcp -m tcp --dport 80 -j DNAT " "--to-destination %s:%s" % (FLAGS.cc_host, FLAGS.cc_port)) # NOTE(devcamcar): Cloud public SNAT entries and the default # SNAT rule for outbound traffic. - _confirm_rule("-t nat -A nova_postrouting -s %s " + _confirm_rule("POSTROUTING", "-t nat -s %s " "-j SNAT --to-source %s" % (FLAGS.private_range, FLAGS.routing_source_ip)) - _confirm_rule("-A nova_postrouting -s %s MASQUERADE" % FLAGS.private_range) - _confirm_rule("-A nova_postrouting -s %(range)s -d %(range)s" % {'range': FLAGS.private_range}) + _confirm_rule("POSTROUTING", "-t nat -s %s MASQUERADE" % FLAGS.private_range) + _confirm_rule("POSTROUTING", "-t nat -s %(range)s -d %(range)s" % {'range': FLAGS.private_range}) def bind_floating_ip(floating_ip): """Bind ip to public interface""" @@ -75,37 +77,37 @@ def unbind_floating_ip(floating_ip): def ensure_vlan_forward(public_ip, port, private_ip): """Sets up forwarding rules for vlan""" - _confirm_rule("nova_forward -d %s -p udp --dport 1194 -j ACCEPT" % private_ip) + _confirm_rule("FORWARD", "-d %s -p udp --dport 1194 -j ACCEPT" % private_ip) _confirm_rule( - "nova_prerouting -t nat -d %s -p udp --dport %s -j DNAT --to %s:1194" + "PREROUTING -t nat -d %s -p udp --dport %s -j DNAT --to %s:1194" % (public_ip, port, private_ip)) def ensure_floating_forward(floating_ip, fixed_ip): """Ensure floating ip forwarding rule""" - _confirm_rule("nova_prerouting -t nat -d %s -j DNAT --to %s" + _confirm_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s" % (floating_ip, fixed_ip)) - _confirm_rule("nova_postrouting -t nat -s %s -j SNAT --to %s" + _confirm_rule("POSTROUTING", "-t nat -s %s -j SNAT --to %s" % (fixed_ip, floating_ip)) # TODO(joshua): Get these from the secgroup datastore entries - _confirm_rule("nova_forward -d %s -p icmp -j ACCEPT" + _confirm_rule("FORWARD", "-d %s -p icmp -j ACCEPT" % (fixed_ip)) for (protocol, port) in DEFAULT_PORTS: _confirm_rule( - "nova_forward -d %s -p %s --dport %s -j ACCEPT" + "FORWARD -d %s -p %s --dport %s -j ACCEPT" % (fixed_ip, protocol, port)) def remove_floating_forward(floating_ip, fixed_ip): """Remove forwarding for floating ip""" - _remove_rule("nova_prerouting -t nat -d %s -j DNAT --to %s" + _remove_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s" % (floating_ip, fixed_ip)) - _remove_rule("nova_postrouting -t nat -s %s -j SNAT --to %s" + _remove_rule("POSTROUTING", "-t nat -s %s -j SNAT --to %s" % (fixed_ip, floating_ip)) - _remove_rule("nova_forward -d %s -p icmp -j ACCEPT" + _remove_rule("FORWARD", "-d %s -p icmp -j ACCEPT" % (fixed_ip)) for (protocol, port) in DEFAULT_PORTS: - _remove_rule("nova_forward -d %s -p %s --dport %s -j ACCEPT" + _remove_rule("FORWARD", "-d %s -p %s --dport %s -j ACCEPT" % (fixed_ip, protocol, port)) @@ -141,7 +143,7 @@ def ensure_bridge(bridge, interface, net_attrs=None): net_attrs['gateway'], net_attrs['broadcast'], net_attrs['netmask'])) - _confirm_rule("nova_forward --in-interface %s -j ACCEPT" % bridge) + _confirm_rule("FORWARD", "--in-interface %s -j ACCEPT" % bridge) else: _execute("sudo ifconfig %s up" % bridge) @@ -211,15 +213,19 @@ def _device_exists(device): return not err -def _confirm_rule(cmd): +def _confirm_rule(chain, cmd): """Delete and re-add iptables rule""" - _execute("sudo iptables --delete %s" % (cmd), check_exit_code=False) - _execute("sudo iptables -I %s" % (cmd)) + if FLAGS.use_nova_chains: + chain = "nova_%s" % chain.lower() + _execute("sudo iptables --delete %s %s" % (chain, cmd), check_exit_code=False) + _execute("sudo iptables -I %s %s" % (chain, cmd)) -def _remove_rule(cmd): +def _remove_rule(chain, cmd): """Remove iptables rule""" - _execute("sudo iptables --delete %s" % (cmd)) + if FLAGS.use_nova_chains: + chain = "%S" % chain.lower() + _execute("sudo iptables --delete %s %s" % (chain, cmd)) def _dnsmasq_cmd(net): -- cgit From 81fc2078ca3d3e07728a39b6cdec47af871f2f2f Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 23 Sep 2010 12:20:40 -0700 Subject: removed extra line in manage --- nova/manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/manager.py b/nova/manager.py index 65300354b..94e4ae959 100644 --- a/nova/manager.py +++ b/nova/manager.py @@ -44,4 +44,3 @@ class Manager(object): Child classes should override this method. """ pass - -- cgit From 065257fb0686d848fcf20235a4e04b76872a5b01 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Thu, 23 Sep 2010 12:43:41 -0700 Subject: fixed a few missing params from iptables rules --- nova/network/linux_net.py | 4 ++-- nova/service.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 149848750..38a616e83 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -60,8 +60,8 @@ def init_host(): "-j SNAT --to-source %s" % (FLAGS.private_range, FLAGS.routing_source_ip)) - _confirm_rule("POSTROUTING", "-t nat -s %s MASQUERADE" % FLAGS.private_range) - _confirm_rule("POSTROUTING", "-t nat -s %(range)s -d %(range)s" % {'range': FLAGS.private_range}) + _confirm_rule("POSTROUTING", "-t nat -s %s -j MASQUERADE" % FLAGS.private_range) + _confirm_rule("POSTROUTING", "-t nat -s %(range)s -d %(range)s -j ACCEPT" % {'range': FLAGS.private_range}) def bind_floating_ip(floating_ip): """Bind ip to public interface""" diff --git a/nova/service.py b/nova/service.py index 870dd6ceb..dcd2a09ef 100644 --- a/nova/service.py +++ b/nova/service.py @@ -50,6 +50,7 @@ class Service(object, service.Service): self.topic = topic manager_class = utils.import_class(manager) self.manager = manager_class(host=host, *args, **kwargs) + self.manager.init_host() self.model_disconnected = False super(Service, self).__init__(*args, **kwargs) try: -- cgit From 2b30ffe2f3c79e3701487d18fe1d4eef671aa335 Mon Sep 17 00:00:00 2001 From: Eric Day Date: Thu, 23 Sep 2010 13:18:40 -0700 Subject: Applied vish's fixes. --- nova/api/ec2/metadatarequesthandler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py index 229e5a78d..08a8040ca 100644 --- a/nova/api/ec2/metadatarequesthandler.py +++ b/nova/api/ec2/metadatarequesthandler.py @@ -18,6 +18,8 @@ """Metadata request handler.""" +import logging + import webob.dec import webob.exc @@ -63,9 +65,9 @@ class MetadataRequestHandler(object): cc = cloud.CloudController() meta_data = cc.get_metadata(req.remote_addr) if meta_data is None: - _log.error('Failed to get metadata for ip: %s' % req.remote_addr) + logging.error('Failed to get metadata for ip: %s' % req.remote_addr) raise webob.exc.HTTPNotFound() - data = self.lookup(path, meta_data) + data = self.lookup(req.path_info, meta_data) if data is None: raise webob.exc.HTTPNotFound() return self.print_data(data) -- cgit From a70632890c610ece766bfd3c31eea4bc6eb4a316 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 23 Sep 2010 17:06:23 -0400 Subject: Apply vish's patch --- nova/api/ec2/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index b041787c2..f0aa57ee4 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -166,8 +166,8 @@ class Authorizer(wsgi.Middleware): 'ModifyImageAttribute': ['projectmanager', 'sysadmin'], }, 'AdminController': { - # All actions have the same permission: [] (the default) - # admins will be allowed to run them + # All actions have the same permission: ['none'] (the default) + # superusers will be allowed to run them # all others will get HTTPUnauthorized. }, } @@ -177,8 +177,7 @@ class Authorizer(wsgi.Middleware): context = req.environ['ec2.context'] controller_name = req.environ['ec2.controller'].__class__.__name__ action = req.environ['ec2.action'] - allowed_roles = self.action_roles[controller_name].get(action, []) - allowed_roles.extend(FLAGS.superuser_roles) + allowed_roles = self.action_roles[controller_name].get(action, ['none']) if self._matches_any_role(context, allowed_roles): return self.application else: @@ -186,6 +185,8 @@ class Authorizer(wsgi.Middleware): def _matches_any_role(self, context, roles): """Return True if any role in roles is allowed in context.""" + if context.user.is_superuser(): + return True if 'all' in roles: return True if 'none' in roles: -- cgit From be214c0ecece6d9cffced02f397ba9ce42be6d9f Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 23 Sep 2010 16:45:30 -0500 Subject: whatever --- nova/tests/api/rackspace/servers.py | 13 +++++++++++-- nova/tests/api/rackspace/test_helper.py | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 6d628e78a..2cfb8d45f 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -16,19 +16,25 @@ # under the License. import unittest - +import stubout from nova.api.rackspace import servers +import nova.api.rackspace from nova.tests.api.test_helper import * +from nova.tests.api.rackspace import test_helper class ServersTest(unittest.TestCase): def setUp(self): self.stubs = stubout.StubOutForTesting() + test_helper.FakeAuthManager.auth_data = {} + test_helper.FakeAuthDatabase.data = {} + test_helper.stub_out_auth(self.stubs) def tearDown(self): self.stubs.UnsetAll() def test_get_server_list(self): - pass + req = webob.Request.blank('/v1.0/servers') + req.get_response(nova.api.API()) def test_create_instance(self): pass @@ -56,3 +62,6 @@ class ServersTest(unittest.TestCase): def test_delete_server_instance(self): pass + +if __name__ == "__main__": + unittest.main() diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index be14e2de8..1fb2a19cc 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -3,6 +3,7 @@ import webob.dec import datetime from nova.wsgi import Router from nova import auth +import nova.api.rackspace.auth class Context(object): pass @@ -24,6 +25,27 @@ def fake_auth_init(self): self.auth = FakeAuthManager() self.host = 'foo' +def stub_out_auth(stubs): + def fake_auth_init(self, app): + self.application = app + + def fake_rate_init(self, app): + super(nova.api.rackspace.RateLimitingMiddleware, self).__init__(app) + self.application = app + + @webob.dec.wsgify + def fake_wsgi(self, req): + return self.application + + stubs.Set(nova.api.rackspace.AuthMiddleware, + '__init__', fake_auth_init) + stubs.Set(nova.api.rackspace.RateLimitingMiddleware, + '__init__', fake_rate_init) + stubs.Set(nova.api.rackspace.AuthMiddleware, + '__call__', fake_wsgi) + stubs.Set(nova.api.rackspace.RateLimitingMiddleware, + '__call__', fake_wsgi) + class FakeAuthDatabase(object): data = {} -- cgit From e8bac42e5fb7a4bdefaf50db210777516c049166 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Thu, 23 Sep 2010 18:00:27 -0700 Subject: Add multi region support for adminclient --- nova/adminclient.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/nova/adminclient.py b/nova/adminclient.py index 0ca32b1e5..77a09e652 100644 --- a/nova/adminclient.py +++ b/nova/adminclient.py @@ -20,8 +20,8 @@ Nova User API client library. """ import base64 - import boto +import httplib from boto.ec2.regioninfo import RegionInfo @@ -68,13 +68,13 @@ class UserRole(object): def __init__(self, connection=None): self.connection = connection self.role = None - + def __repr__(self): return 'UserRole:%s' % self.role def startElement(self, name, attrs, connection): return None - + def endElement(self, name, value, connection): if name == 'role': self.role = value @@ -128,20 +128,20 @@ class ProjectMember(object): def __init__(self, connection=None): self.connection = connection self.memberId = None - + def __repr__(self): return 'ProjectMember:%s' % self.memberId def startElement(self, name, attrs, connection): return None - + def endElement(self, name, value, connection): if name == 'member': self.memberId = value else: setattr(self, name, str(value)) - + class HostInfo(object): """ Information about a Nova Host, as parsed through SAX: @@ -171,17 +171,20 @@ class HostInfo(object): class NovaAdminClient(object): - def __init__(self, clc_ip='127.0.0.1', region='nova', access_key='admin', - secret_key='admin', **kwargs): - self.clc_ip = clc_ip + def __init__(self, clc_url='http://127.0.0.1:8773', region='nova', + access_key='admin', secret_key='admin', **kwargs): + parts = httplib.urlsplit(clc_url) + is_secure = parts.scheme == 'https' + ip, port = parts.netloc.split(':') + self.region = region self.access = access_key self.secret = secret_key self.apiconn = boto.connect_ec2(aws_access_key_id=access_key, aws_secret_access_key=secret_key, - is_secure=False, - region=RegionInfo(None, region, clc_ip), - port=8773, + is_secure=is_secure, + region=RegionInfo(None, region, ip), + port=port, path='/services/Admin', **kwargs) self.apiconn.APIVersion = 'nova' @@ -289,7 +292,7 @@ class NovaAdminClient(object): if project.projectname != None: return project - + def create_project(self, projectname, manager_user, description=None, member_users=None): """ @@ -322,7 +325,7 @@ class NovaAdminClient(object): Adds a user to a project. """ return self.modify_project_member(user, project, operation='add') - + def remove_project_member(self, user, project): """ Removes a user from a project. -- cgit From ea33870fd0dee4e96b4f2aa18cdad02b66cd4f57 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Fri, 24 Sep 2010 10:19:28 +0200 Subject: nova-api-new is no more. Don't attempt to install it. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 1767b00f4..523a5c990 100644 --- a/setup.py +++ b/setup.py @@ -54,5 +54,4 @@ setup(name='nova', 'bin/nova-manage', 'bin/nova-network', 'bin/nova-objectstore', - 'bin/nova-api-new', 'bin/nova-volume']) -- cgit From 9abb45043c11125ecee36e44f939817bd03d70c4 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Fri, 24 Sep 2010 10:21:10 +0200 Subject: Install nova-scheduler. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 523a5c990..d420d3559 100644 --- a/setup.py +++ b/setup.py @@ -54,4 +54,5 @@ setup(name='nova', 'bin/nova-manage', 'bin/nova-network', 'bin/nova-objectstore', + 'bin/nova-scheduler', 'bin/nova-volume']) -- cgit From aa0af4b0a43f3eff3db1b1868a82ba65c5710f87 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Fri, 24 Sep 2010 17:42:55 -0700 Subject: Finished making admin client work for multi-region --- nova/adminclient.py | 59 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/nova/adminclient.py b/nova/adminclient.py index 77a09e652..8db5d9158 100644 --- a/nova/adminclient.py +++ b/nova/adminclient.py @@ -25,6 +25,12 @@ import httplib from boto.ec2.regioninfo import RegionInfo +DEFAULT_CLC_URL='http://127.0.0.1:8773' +DEFAULT_REGION='nova' +DEFAULT_ACCESS_KEY='admin' +DEFAULT_SECRET_KEY='admin' + + class UserInfo(object): """ Information about a Nova user, as parsed through SAX @@ -171,38 +177,57 @@ class HostInfo(object): class NovaAdminClient(object): - def __init__(self, clc_url='http://127.0.0.1:8773', region='nova', - access_key='admin', secret_key='admin', **kwargs): - parts = httplib.urlsplit(clc_url) - is_secure = parts.scheme == 'https' - ip, port = parts.netloc.split(':') + def __init__(self, clc_url=DEFAULT_CLC_URL, region=DEFAULT_REGION, + access_key=DEFAULT_ACCESS_KEY, secret_key=DEFAULT_SECRET_KEY, + **kwargs): + parts = self.split_clc_url(clc_url) + self.clc_url = clc_url self.region = region self.access = access_key self.secret = secret_key self.apiconn = boto.connect_ec2(aws_access_key_id=access_key, aws_secret_access_key=secret_key, - is_secure=is_secure, - region=RegionInfo(None, region, ip), - port=port, + is_secure=parts['is_secure'], + region=RegionInfo(None, + region, + parts['ip']), + port=parts['port'], path='/services/Admin', **kwargs) self.apiconn.APIVersion = 'nova' - def connection_for(self, username, project, **kwargs): + def connection_for(self, username, project, clc_url=None, region=None, + **kwargs): """ Returns a boto ec2 connection for the given username. """ + if not clc_url: + clc_url = self.clc_url + if not region: + region = self.region + parts = self.split_clc_url(clc_url) user = self.get_user(username) access_key = '%s:%s' % (user.accesskey, project) - return boto.connect_ec2( - aws_access_key_id=access_key, - aws_secret_access_key=user.secretkey, - is_secure=False, - region=RegionInfo(None, self.region, self.clc_ip), - port=8773, - path='/services/Cloud', - **kwargs) + return boto.connect_ec2(aws_access_key_id=access_key, + aws_secret_access_key=user.secretkey, + is_secure=parts['is_secure'], + region=RegionInfo(None, + self.region, + parts['ip']), + port=parts['port'], + path='/services/Cloud', + **kwargs) + + def split_clc_url(self, clc_url): + """ + Splits a cloud controller endpoint url. + """ + import logging; logging.debug('clc_url = %s', clc_url) + parts = httplib.urlsplit(clc_url) + is_secure = parts.scheme == 'https' + ip, port = parts.netloc.split(':') + return {'ip': ip, 'port': int(port), 'is_secure': is_secure} def get_users(self): """ grabs the list of all users """ -- cgit From 15c2678d3e3899e7ab6180dce457ae6d3e54937d Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 24 Sep 2010 18:21:58 -0700 Subject: improved the shell script for iptables --- tools/setup_iptables.sh | 124 ++++++++++++++++++++++++++++++------------------ 1 file changed, 78 insertions(+), 46 deletions(-) mode change 100644 => 100755 tools/setup_iptables.sh diff --git a/tools/setup_iptables.sh b/tools/setup_iptables.sh old mode 100644 new mode 100755 index b1ab1c6f7..fd32f6f82 --- a/tools/setup_iptables.sh +++ b/tools/setup_iptables.sh @@ -1,93 +1,125 @@ #!/usr/bin/env bash - -CMD="global" -IP="XXX" -PRIVATE_RANGE="10.128.0.0/12" +# 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. if [ -n "$1" ]; then CMD=$1 +else + CMD="all" fi if [ -n "$2" ]; then IP=$2 +else + # NOTE(vish): this will just get the first ip in the list, so if you + # have more than one eth device set up, this will fail + IP=`ifconfig | grep -m 1 'inet addr:'| cut -d: -f2 | awk '{print $1}'` fi if [ -n "$3" ]; then PRIVATE_RANGE=$3 +else + PRIVATE_RANGE="10.0.0.0/12" +fi + + +if [ -n "$4" ]; then + MGMT_IP=$4 +else + MGMT_IP="$IP" fi -if [ "$CMD" == "global" ]; then - iptables -P INPUT DROP - iptables -A INPUT -m state --state INVALID -j DROP - iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT - iptables -A INPUT -m tcp -p tcp -d $MGMT_IP --dport 22 -j ACCEPT - iptables -A INPUT -m udp -p udp --dport 123 -j ACCEPT - iptables -N nova_input - iptables -A INPUT -j nova_input - iptables -A INPUT -p icmp -j ACCEPT - iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset - iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable - - iptables -P FORWARD DROP - iptables -A FORWARD -m state --state INVALID -j DROP - iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT - iptables -A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu - iptables -N nova_forward - iptables -A FORWARD -j nova_forward - - iptables -P OUTPUT DROP - iptables -A OUTPUT -m state --state INVALID -j DROP - iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT - iptables -N nova_output - iptables -A OUTPUT -j nova_output - - iptables -t nat -N nova_prerouting - iptables -t nat -A PREROUTING -j nova_prerouting - - iptables -t nat -N nova_postrouting - iptables -t nat -A POSTROUTING -j nova_postrouting - - iptables -t nat -N nova_output - iptables -t nat -A OUTPUT -j nova_output - - # ganglia (all hosts) +iptables -F +iptables -P INPUT DROP +iptables -A INPUT -m state --state INVALID -j DROP +iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT +iptables -A INPUT -m tcp -p tcp -d $MGMT_IP --dport 22 -j ACCEPT +iptables -A INPUT -m udp -p udp --dport 123 -j ACCEPT +iptables -N nova_input +iptables -A INPUT -j nova_input +iptables -A INPUT -p icmp -j ACCEPT +iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset +iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable + +iptables -P FORWARD DROP +iptables -A FORWARD -m state --state INVALID -j DROP +iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT +iptables -A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu +iptables -N nova_forward +iptables -A FORWARD -j nova_forward + +iptables -P OUTPUT DROP +iptables -A OUTPUT -m state --state INVALID -j DROP +iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT +iptables -N nova_output +iptables -A OUTPUT -j nova_output + +iptables -t nat -N nova_prerouting +iptables -t nat -A PREROUTING -j nova_prerouting + +iptables -t nat -N nova_postrouting +iptables -t nat -A POSTROUTING -j nova_postrouting + +iptables -t nat -N nova_output +iptables -t nat -A OUTPUT -j nova_output + +if [ "$CMD" == "ganglia" ] || [ "$CMD" == "all" ]; then iptables -A nova_input -m tcp -p tcp -d $IP --dport 8649 -j ACCEPT iptables -A nova_input -m udp -p udp -d $IP --dport 8649 -j ACCEPT fi -if [ "$CMD" == "dashboard" ]; then +if [ "$CMD" == "dashboard" ] || [ "$CMD" == "all" ]; then # dashboard iptables -A nova_input -m tcp -p tcp -d $IP --dport 80 -j ACCEPT iptables -A nova_input -m tcp -p tcp -d $IP --dport 443 -j ACCEPT fi -if [ "$CMD" == "objectstore" ]; then +if [ "$CMD" == "objectstore" ] || [ "$CMD" == "all" ]; then iptables -A nova_input -m tcp -p tcp -d $IP --dport 3333 -j ACCEPT +fi + +if [ "$CMD" == "api" ] || [ "$CMD" == "all" ]; then iptables -A nova_input -m tcp -p tcp -d $IP --dport 8773 -j ACCEPT fi -if [ "$CMD" == "redis" ]; then +if [ "$CMD" == "redis" ] || [ "$CMD" == "all" ]; then iptables -A nova_input -m tcp -p tcp -d $IP --dport 6379 -j ACCEPT fi -if [ "$CMD" == "mysql" ]; then +if [ "$CMD" == "mysql" ] || [ "$CMD" == "all" ]; then iptables -A nova_input -m tcp -p tcp -d $IP --dport 3306 -j ACCEPT fi -if [ "$CMD" == "rabbitmq" ]; then +if [ "$CMD" == "rabbitmq" ] || [ "$CMD" == "all" ]; then iptables -A nova_input -m tcp -p tcp -d $IP --dport 4369 -j ACCEPT iptables -A nova_input -m tcp -p tcp -d $IP --dport 5672 -j ACCEPT iptables -A nova_input -m tcp -p tcp -d $IP --dport 53284 -j ACCEPT fi -if [ "$CMD" == "dnsmasq" ]; then +if [ "$CMD" == "dnsmasq" ] || [ "$CMD" == "all" ]; then # NOTE(vish): this could theoretically be setup per network # for each host, but it seems like overkill iptables -A nova_input -m tcp -p tcp -s $PRIVATE_RANGE --dport 53 -j ACCEPT iptables -A nova_input -m udp -p udp -s $PRIVATE_RANGE --dport 53 -j ACCEPT iptables -A nova_input -m udp -p udp --dport 67 -j ACCEPT +fi -if [ "$CMD" == "ldap" ]; then +if [ "$CMD" == "ldap" ] || [ "$CMD" == "all" ]; then iptables -A nova_input -m tcp -p tcp -d $IP --dport 389 -j ACCEPT fi -- cgit From 41a598f09baee94125608873f4d7118000fc55ea Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 24 Sep 2010 19:57:41 -0700 Subject: add a reset command --- tools/setup_iptables.sh | 74 +++++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/tools/setup_iptables.sh b/tools/setup_iptables.sh index fd32f6f82..7368fadf9 100755 --- a/tools/setup_iptables.sh +++ b/tools/setup_iptables.sh @@ -43,40 +43,48 @@ if [ -n "$4" ]; then else MGMT_IP="$IP" fi +if [ "$CMD" == "clear" ]; then + iptables -P INPUT ACCEPT + iptables -P FORWARD ACCEPT + iptables -P OUTPUT ACCEPT + iptables -F + iptables -X +fi -iptables -F -iptables -P INPUT DROP -iptables -A INPUT -m state --state INVALID -j DROP -iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT -iptables -A INPUT -m tcp -p tcp -d $MGMT_IP --dport 22 -j ACCEPT -iptables -A INPUT -m udp -p udp --dport 123 -j ACCEPT -iptables -N nova_input -iptables -A INPUT -j nova_input -iptables -A INPUT -p icmp -j ACCEPT -iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset -iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable - -iptables -P FORWARD DROP -iptables -A FORWARD -m state --state INVALID -j DROP -iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT -iptables -A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu -iptables -N nova_forward -iptables -A FORWARD -j nova_forward - -iptables -P OUTPUT DROP -iptables -A OUTPUT -m state --state INVALID -j DROP -iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT -iptables -N nova_output -iptables -A OUTPUT -j nova_output - -iptables -t nat -N nova_prerouting -iptables -t nat -A PREROUTING -j nova_prerouting - -iptables -t nat -N nova_postrouting -iptables -t nat -A POSTROUTING -j nova_postrouting - -iptables -t nat -N nova_output -iptables -t nat -A OUTPUT -j nova_output +if [ "$CMD" == "base" ] || [ "$CMD" == "all" ]; then + iptables -P INPUT DROP + iptables -A INPUT -m state --state INVALID -j DROP + iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -A INPUT -m tcp -p tcp -d $MGMT_IP --dport 22 -j ACCEPT + iptables -A INPUT -m udp -p udp --dport 123 -j ACCEPT + iptables -N nova_input + iptables -A INPUT -j nova_input + iptables -A INPUT -p icmp -j ACCEPT + iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset + iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable + + iptables -P FORWARD DROP + iptables -A FORWARD -m state --state INVALID -j DROP + iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu + iptables -N nova_forward + iptables -A FORWARD -j nova_forward + + iptables -P OUTPUT DROP + iptables -A OUTPUT -m state --state INVALID -j DROP + iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -N nova_output + iptables -A OUTPUT -j nova_output + + iptables -t nat -N nova_prerouting + iptables -t nat -A PREROUTING -j nova_prerouting + + iptables -t nat -N nova_postrouting + iptables -t nat -A POSTROUTING -j nova_postrouting + + iptables -t nat -N nova_output + iptables -t nat -A OUTPUT -j nova_output +fi if [ "$CMD" == "ganglia" ] || [ "$CMD" == "all" ]; then iptables -A nova_input -m tcp -p tcp -d $IP --dport 8649 -j ACCEPT -- cgit From 73cc0e446297781182fab1b8e8447e7c6d100b08 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Fri, 24 Sep 2010 23:32:00 -0400 Subject: Update auth manager to have a update_user method and better tests. --- nova/auth/ldapdriver.py | 16 +- nova/auth/manager.py | 6 + nova/tests/auth_unittest.py | 408 +++++++++++++++++++++++++++++--------------- 3 files changed, 290 insertions(+), 140 deletions(-) diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py index 021851ebf..640ea169e 100644 --- a/nova/auth/ldapdriver.py +++ b/nova/auth/ldapdriver.py @@ -256,8 +256,7 @@ class LdapDriver(object): if not self.__user_exists(uid): raise exception.NotFound("User %s doesn't exist" % uid) self.__remove_from_all(uid) - self.conn.delete_s('uid=%s,%s' % (uid, - FLAGS.ldap_user_subtree)) + self.conn.delete_s(self.__uid_to_dn(uid)) def delete_project(self, project_id): """Delete a project""" @@ -265,6 +264,19 @@ class LdapDriver(object): self.__delete_roles(project_dn) self.__delete_group(project_dn) + def modify_user(self, uid, access_key=None, secret_key=None, admin=None): + """Modify an existing project""" + if not access_key and not secret_key and admin is None: + return + attr = [] + if access_key: + attr.append((self.ldap.MOD_REPLACE, 'accessKey', access_key)) + if secret_key: + attr.append((self.ldap.MOD_REPLACE, 'secretKey', secret_key)) + if admin is not None: + attr.append((self.ldap.MOD_REPLACE, 'isAdmin', str(admin).upper())) + self.conn.modify_s(self.__uid_to_dn(uid), attr) + def __user_exists(self, uid): """Check if user exists""" return self.get_user(uid) != None diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 55fbf42aa..0bc12c80f 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -630,6 +630,12 @@ class AuthManager(object): with self.driver() as drv: drv.delete_user(uid) + def modify_user(self, user, access_key=None, secret_key=None, admin=None): + """Modify credentials for a user""" + uid = User.safe_id(user) + with self.driver() as drv: + drv.modify_user(uid, access_key, secret_key, admin) + def get_credentials(self, user, project=None): """Get credential zip for user in project""" if not isinstance(user, User): diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index 3235dea39..7065105c3 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -28,25 +28,73 @@ from nova.api.ec2 import cloud FLAGS = flags.FLAGS - +class user_generator(object): + def __init__(self, manager, **user_state): + if 'name' not in user_state: + user_state['name'] = 'test1' + self.manager = manager + self.user = manager.create_user(**user_state) + + def __enter__(self): + return self.user + + def __exit__(self, value, type, trace): + self.manager.delete_user(self.user) + +class project_generator(object): + def __init__(self, manager, **project_state): + if 'name' not in project_state: + project_state['name'] = 'testproj' + if 'manager_user' not in project_state: + project_state['manager_user'] = 'test1' + self.manager = manager + self.project = manager.create_project(**project_state) + + def __enter__(self): + return self.project + + def __exit__(self, value, type, trace): + self.manager.delete_project(self.project) + +class user_and_project_generator(object): + def __init__(self, manager, user_state={}, project_state={}): + self.manager = manager + if 'name' not in user_state: + user_state['name'] = 'test1' + if 'name' not in project_state: + project_state['name'] = 'testproj' + if 'manager_user' not in project_state: + project_state['manager_user'] = 'test1' + self.user = manager.create_user(**user_state) + self.project = manager.create_project(**project_state) + + def __enter__(self): + return (self.user, self.project) + + def __exit__(self, value, type, trace): + self.manager.delete_user(self.user) + self.manager.delete_project(self.project) + +# TODO(todd): Have a test class that tests just the AuthManager functions +# and different test classes for User and Project wrappers class AuthTestCase(test.BaseTestCase): def setUp(self): super(AuthTestCase, self).setUp() self.flags(connection_type='fake') self.manager = manager.AuthManager() - def test_001_can_create_users(self): - self.manager.create_user('test1', 'access', 'secret') - self.manager.create_user('test2') - - def test_002_can_get_user(self): - user = self.manager.get_user('test1') + def test_create_and_find_user(self): + with user_generator(self.manager): + self.assert_(self.manager.get_user('test1')) - def test_003_can_retreive_properties(self): - user = self.manager.get_user('test1') - self.assertEqual('test1', user.id) - self.assertEqual('access', user.access) - self.assertEqual('secret', user.secret) + def test_create_and_find_with_properties(self): + with user_generator(self.manager, name="herbert", secret="classified", + access="private-party"): + u = self.manager.get_user('herbert') + self.assertEqual('herbert', u.id) + self.assertEqual('herbert', u.name) + self.assertEqual('classified', u.secret) + self.assertEqual('private-party', u.access) def test_004_signature_is_valid(self): #self.assertTrue(self.manager.authenticate( **boto.generate_url ... ? ? ? )) @@ -63,133 +111,217 @@ class AuthTestCase(test.BaseTestCase): 'export S3_URL="http://127.0.0.1:3333/"\n' + 'export EC2_USER_ID="test1"\n') - def test_010_can_list_users(self): - users = self.manager.get_users() - logging.warn(users) - self.assertTrue(filter(lambda u: u.id == 'test1', users)) - - def test_101_can_add_user_role(self): - self.assertFalse(self.manager.has_role('test1', 'itsec')) - self.manager.add_role('test1', 'itsec') - self.assertTrue(self.manager.has_role('test1', 'itsec')) - - def test_199_can_remove_user_role(self): - self.assertTrue(self.manager.has_role('test1', 'itsec')) - self.manager.remove_role('test1', 'itsec') - self.assertFalse(self.manager.has_role('test1', 'itsec')) - - def test_201_can_create_project(self): - project = self.manager.create_project('testproj', 'test1', 'A test project', ['test1']) - self.assertTrue(filter(lambda p: p.name == 'testproj', self.manager.get_projects())) - self.assertEqual(project.name, 'testproj') - self.assertEqual(project.description, 'A test project') - self.assertEqual(project.project_manager_id, 'test1') - self.assertTrue(project.has_member('test1')) - - def test_202_user1_is_project_member(self): - self.assertTrue(self.manager.get_user('test1').is_project_member('testproj')) - - def test_203_user2_is_not_project_member(self): - self.assertFalse(self.manager.get_user('test2').is_project_member('testproj')) - - def test_204_user1_is_project_manager(self): - self.assertTrue(self.manager.get_user('test1').is_project_manager('testproj')) - - def test_205_user2_is_not_project_manager(self): - self.assertFalse(self.manager.get_user('test2').is_project_manager('testproj')) - - def test_206_can_add_user_to_project(self): - self.manager.add_to_project('test2', 'testproj') - self.assertTrue(self.manager.get_project('testproj').has_member('test2')) - - def test_207_can_remove_user_from_project(self): - self.manager.remove_from_project('test2', 'testproj') - self.assertFalse(self.manager.get_project('testproj').has_member('test2')) - - def test_208_can_remove_add_user_with_role(self): - self.manager.add_to_project('test2', 'testproj') - self.manager.add_role('test2', 'developer', 'testproj') - self.manager.remove_from_project('test2', 'testproj') - self.assertFalse(self.manager.has_role('test2', 'developer', 'testproj')) - self.manager.add_to_project('test2', 'testproj') - self.manager.remove_from_project('test2', 'testproj') - - def test_209_can_generate_x509(self): - # MUST HAVE RUN CLOUD SETUP BY NOW - self.cloud = cloud.CloudController() - self.cloud.setup() - _key, cert_str = self.manager._generate_x509_cert('test1', 'testproj') - logging.debug(cert_str) - - # Need to verify that it's signed by the right intermediate CA - full_chain = crypto.fetch_ca(project_id='testproj', chain=True) - int_cert = crypto.fetch_ca(project_id='testproj', chain=False) - cloud_cert = crypto.fetch_ca() - logging.debug("CA chain:\n\n =====\n%s\n\n=====" % full_chain) - signed_cert = X509.load_cert_string(cert_str) - chain_cert = X509.load_cert_string(full_chain) - int_cert = X509.load_cert_string(int_cert) - cloud_cert = X509.load_cert_string(cloud_cert) - self.assertTrue(signed_cert.verify(chain_cert.get_pubkey())) - self.assertTrue(signed_cert.verify(int_cert.get_pubkey())) - - if not FLAGS.use_intermediate_ca: - self.assertTrue(signed_cert.verify(cloud_cert.get_pubkey())) - else: - self.assertFalse(signed_cert.verify(cloud_cert.get_pubkey())) - - def test_210_can_add_project_role(self): - project = self.manager.get_project('testproj') - self.assertFalse(project.has_role('test1', 'sysadmin')) - self.manager.add_role('test1', 'sysadmin') - self.assertFalse(project.has_role('test1', 'sysadmin')) - project.add_role('test1', 'sysadmin') - self.assertTrue(project.has_role('test1', 'sysadmin')) - - def test_211_can_list_project_roles(self): - project = self.manager.get_project('testproj') - user = self.manager.get_user('test1') - self.manager.add_role(user, 'netadmin', project) - roles = self.manager.get_user_roles(user) - self.assertTrue('sysadmin' in roles) - self.assertFalse('netadmin' in roles) - project_roles = self.manager.get_user_roles(user, project) - self.assertTrue('sysadmin' in project_roles) - self.assertTrue('netadmin' in project_roles) - # has role should be false because global role is missing - self.assertFalse(self.manager.has_role(user, 'netadmin', project)) - - - def test_212_can_remove_project_role(self): - project = self.manager.get_project('testproj') - self.assertTrue(project.has_role('test1', 'sysadmin')) - project.remove_role('test1', 'sysadmin') - self.assertFalse(project.has_role('test1', 'sysadmin')) - self.manager.remove_role('test1', 'sysadmin') - self.assertFalse(project.has_role('test1', 'sysadmin')) - - def test_214_can_retrieve_project_by_user(self): - project = self.manager.create_project('testproj2', 'test2', 'Another test project', ['test2']) - self.assert_(len(self.manager.get_projects()) > 1) - self.assertEqual(len(self.manager.get_projects('test2')), 1) - - def test_220_can_modify_project(self): - self.manager.modify_project('testproj', 'test2', 'new description') - project = self.manager.get_project('testproj') - self.assertEqual(project.project_manager_id, 'test2') - self.assertEqual(project.description, 'new description') - - def test_299_can_delete_project(self): - self.manager.delete_project('testproj') - self.assertFalse(filter(lambda p: p.name == 'testproj', self.manager.get_projects())) - self.manager.delete_project('testproj2') - - def test_999_can_delete_users(self): + def test_can_list_users(self): + with user_generator(self.manager): + with user_generator(self.manager, name="test2"): + users = self.manager.get_users() + self.assert_(filter(lambda u: u.id == 'test1', users)) + self.assert_(filter(lambda u: u.id == 'test2', users)) + self.assert_(not filter(lambda u: u.id == 'test3', users)) + + def test_can_add_and_remove_user_role(self): + with user_generator(self.manager): + self.assertFalse(self.manager.has_role('test1', 'itsec')) + self.manager.add_role('test1', 'itsec') + self.assertTrue(self.manager.has_role('test1', 'itsec')) + self.manager.remove_role('test1', 'itsec') + self.assertFalse(self.manager.has_role('test1', 'itsec')) + + def test_can_create_and_get_project(self): + with user_and_project_generator(self.manager) as (u,p): + self.assert_(self.manager.get_user('test1')) + self.assert_(self.manager.get_user('test1')) + self.assert_(self.manager.get_project('testproj')) + + def test_can_list_projects(self): + with user_and_project_generator(self.manager): + with project_generator(self.manager, name="testproj2"): + projects = self.manager.get_projects() + self.assert_(filter(lambda p: p.name == 'testproj', projects)) + self.assert_(filter(lambda p: p.name == 'testproj2', projects)) + self.assert_(not filter(lambda p: p.name == 'testproj3', + projects)) + + def test_can_create_and_get_project_with_attributes(self): + with user_generator(self.manager): + with project_generator(self.manager, description='A test project'): + project = self.manager.get_project('testproj') + self.assertEqual('A test project', project.description) + + def test_can_create_project_with_manager(self): + with user_and_project_generator(self.manager) as (user, project): + self.assertEqual('test1', project.project_manager_id) + self.assertTrue(user.is_project_manager('testproj')) + + def test_create_project_assigns_manager_to_members(self): + with user_and_project_generator(self.manager) as (user, project): + self.assertTrue(user.is_project_member('testproj')) + self.assertTrue(project.has_member('test1')) + + def test_no_extra_project_members(self): + with user_generator(self.manager, name='test2') as baduser: + with user_and_project_generator(self.manager) as (user, project): + self.assertFalse(baduser.is_project_member('testproj')) + self.assertFalse(project.has_member('test2')) + + def test_no_extra_project_managers(self): + with user_generator(self.manager, name='test2') as baduser: + with user_and_project_generator(self.manager) as (user, project): + self.assertFalse(baduser.is_project_manager('testproj')) + + def test_can_add_user_to_project(self): + with user_generator(self.manager, name='test2') as user: + with user_and_project_generator(self.manager): + self.manager.add_to_project('test2', 'testproj') + self.assertTrue(user.is_project_member('testproj')) + # NOTE(todd): have to reload after the add_to_project + project = self.manager.get_project('testproj') + self.assertTrue(project.has_member('test2')) + + def test_can_remove_user_from_project(self): + with user_generator(self.manager, name='test2') as user: + with user_and_project_generator(self.manager): + self.manager.add_to_project('test2', 'testproj') + self.assertTrue(user.is_project_member('testproj')) + self.manager.remove_from_project('test2', 'testproj') + self.assertFalse(user.is_project_member('testproj')) + # NOTE(todd): have to reload after the membership modifications + project = self.manager.get_project('testproj') + self.assertFalse(project.has_member('test2')) + + def test_can_add_remove_user_with_role(self): + with user_generator(self.manager, name='test2') as user: + with user_and_project_generator(self.manager): + self.manager.add_to_project('test2', 'testproj') + self.manager.add_role('test2', 'developer', 'testproj') + self.assertTrue(user.is_project_member('testproj')) + self.manager.remove_from_project('test2', 'testproj') + self.assertFalse(self.manager.has_role('test2', 'developer', + 'testproj')) + self.assertFalse(user.is_project_member('testproj')) + + def test_can_generate_x509(self): + # NOTE(todd): this doesn't assert against the auth manager + # so it probably belongs in crypto_unittest + # but I'm leaving it where I found it. + with user_and_project_generator(self.manager) as (user, project): + # NOTE(todd): Should mention why we must setup controller first + # (somebody please clue me in) + cloud_controller = cloud.CloudController() + cloud_controller.setup() + _key, cert_str = self.manager._generate_x509_cert('test1', + 'testproj') + logging.debug(cert_str) + + # Need to verify that it's signed by the right intermediate CA + full_chain = crypto.fetch_ca(project_id='testproj', chain=True) + int_cert = crypto.fetch_ca(project_id='testproj', chain=False) + cloud_cert = crypto.fetch_ca() + logging.debug("CA chain:\n\n =====\n%s\n\n=====" % full_chain) + signed_cert = X509.load_cert_string(cert_str) + chain_cert = X509.load_cert_string(full_chain) + int_cert = X509.load_cert_string(int_cert) + cloud_cert = X509.load_cert_string(cloud_cert) + self.assertTrue(signed_cert.verify(chain_cert.get_pubkey())) + self.assertTrue(signed_cert.verify(int_cert.get_pubkey())) + if not FLAGS.use_intermediate_ca: + self.assertTrue(signed_cert.verify(cloud_cert.get_pubkey())) + else: + self.assertFalse(signed_cert.verify(cloud_cert.get_pubkey())) + + def test_adding_role_to_project_is_ignored_unless_added_to_user(self): + with user_and_project_generator(self.manager) as (user, project): + self.assertFalse(project.has_role('test1', 'sysadmin')) + project.add_role('test1', 'sysadmin') + # NOTE(todd): it will still show up in get_user_roles(u, project) + self.assertFalse(project.has_role('test1', 'sysadmin')) + self.manager.add_role('test1', 'sysadmin') + self.assertTrue(project.has_role('test1', 'sysadmin')) + + def test_add_user_role_doesnt_infect_project_roles(self): + with user_and_project_generator(self.manager) as (user, project): + self.assertFalse(project.has_role('test1', 'sysadmin')) + self.manager.add_role('test1', 'sysadmin') + self.assertFalse(project.has_role('test1', 'sysadmin')) + + def test_can_list_user_roles(self): + with user_and_project_generator(self.manager) as (user, project): + self.manager.add_role('test1', 'sysadmin') + roles = self.manager.get_user_roles(user) + self.assertTrue('sysadmin' in roles) + self.assertFalse('netadmin' in roles) + + def test_can_list_project_roles(self): + with user_and_project_generator(self.manager) as (user, project): + self.manager.add_role('test1', 'sysadmin') + project.add_role('test1', 'sysadmin') + project.add_role('test1', 'netadmin') + project_roles = self.manager.get_user_roles(user, project) + self.assertTrue('sysadmin' in project_roles) + self.assertTrue('netadmin' in project_roles) + # has role should be false user-level role is missing + self.assertFalse(self.manager.has_role(user, 'netadmin', project)) + + def test_can_remove_user_roles(self): + with user_and_project_generator(self.manager) as (user, project): + self.manager.add_role('test1', 'sysadmin') + self.assertTrue(user.has_role('sysadmin')) + self.manager.remove_role('test1', 'sysadmin') + self.assertFalse(user.has_role('sysadmin')) + + def test_removing_user_role_hides_it_from_project(self): + with user_and_project_generator(self.manager) as (user, project): + self.manager.add_role('test1', 'sysadmin') + project.add_role('test1', 'sysadmin') + self.assertTrue(project.has_role('test1', 'sysadmin')) + self.manager.remove_role('test1', 'sysadmin') + self.assertFalse(project.has_role('test1', 'sysadmin')) + + def test_can_remove_project_role_but_keep_user_role(self): + with user_and_project_generator(self.manager) as (user, project): + self.manager.add_role('test1', 'sysadmin') + project.add_role('test1', 'sysadmin') + self.assertTrue(user.has_role('sysadmin')) + self.assertTrue(project.has_role('test1', 'sysadmin')) + project.remove_role('test1', 'sysadmin') + self.assertFalse(project.has_role('test1', 'sysadmin')) + self.assertTrue(user.has_role('sysadmin')) + + def test_can_retrieve_project_by_user(self): + with user_and_project_generator(self.manager) as (user, project): + self.assertEqual(1, len(self.manager.get_projects('test1'))) + + def test_can_modify_project(self): + with user_and_project_generator(self.manager): + with user_generator(self.manager, name='test2'): + self.manager.modify_project('testproj', 'test2', 'new desc') + project = self.manager.get_project('testproj') + self.assertEqual('test2', project.project_manager_id) + self.assertEqual('new desc', project.description) + + def test_can_delete_project(self): + with user_generator(self.manager): + self.manager.create_project('testproj', 'test1') + self.assert_(self.manager.get_project('testproj')) + self.manager.delete_project('testproj') + projectlist = self.manager.get_projects() + self.assert_(not filter(lambda p: p.name == 'testproj', + projectlist)) + + def test_can_delete_user(self): + self.manager.create_user('test1') + self.assert_(self.manager.get_user('test1')) self.manager.delete_user('test1') - users = self.manager.get_users() - self.assertFalse(filter(lambda u: u.id == 'test1', users)) - self.manager.delete_user('test2') - self.assertEqual(self.manager.get_user('test2'), None) + userlist = self.manager.get_users() + self.assert_(not filter(lambda u: u.id == 'test1', userlist)) + + def test_can_modify_users(self): + with user_generator(self.manager): + self.manager.modify_user('test1', 'access', 'secret', True) + user = self.manager.get_user('test1') + self.assertEqual('access', user.access) + self.assertEqual('secret', user.secret) + self.assertTrue(user.is_admin()) if __name__ == "__main__": -- cgit From 39e06d83d265036eb0104cd55d4a828271f62e96 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Sat, 25 Sep 2010 00:35:59 -0400 Subject: Test the AuthManager interface explicitly, in case the user/project wrappers fail or change at some point. Those interfaces should be tested on their own. --- nova/tests/auth_unittest.py | 113 +++++++++++++++++++++----------------------- 1 file changed, 55 insertions(+), 58 deletions(-) diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index 7065105c3..c3efc577f 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -75,11 +75,9 @@ class user_and_project_generator(object): self.manager.delete_user(self.user) self.manager.delete_project(self.project) -# TODO(todd): Have a test class that tests just the AuthManager functions -# and different test classes for User and Project wrappers -class AuthTestCase(test.BaseTestCase): +class AuthManagerTestCase(test.BaseTestCase): def setUp(self): - super(AuthTestCase, self).setUp() + super(AuthManagerTestCase, self).setUp() self.flags(connection_type='fake') self.manager = manager.AuthManager() @@ -151,54 +149,54 @@ class AuthTestCase(test.BaseTestCase): def test_can_create_project_with_manager(self): with user_and_project_generator(self.manager) as (user, project): self.assertEqual('test1', project.project_manager_id) - self.assertTrue(user.is_project_manager('testproj')) + self.assertTrue(self.manager.is_project_manager(user, project)) def test_create_project_assigns_manager_to_members(self): with user_and_project_generator(self.manager) as (user, project): - self.assertTrue(user.is_project_member('testproj')) - self.assertTrue(project.has_member('test1')) + self.assertTrue(self.manager.is_project_member(user, project)) def test_no_extra_project_members(self): with user_generator(self.manager, name='test2') as baduser: with user_and_project_generator(self.manager) as (user, project): - self.assertFalse(baduser.is_project_member('testproj')) - self.assertFalse(project.has_member('test2')) + self.assertFalse(self.manager.is_project_member(baduser, + project)) def test_no_extra_project_managers(self): with user_generator(self.manager, name='test2') as baduser: with user_and_project_generator(self.manager) as (user, project): - self.assertFalse(baduser.is_project_manager('testproj')) + self.assertFalse(self.manager.is_project_manager(baduser, + project)) def test_can_add_user_to_project(self): with user_generator(self.manager, name='test2') as user: - with user_and_project_generator(self.manager): - self.manager.add_to_project('test2', 'testproj') - self.assertTrue(user.is_project_member('testproj')) - # NOTE(todd): have to reload after the add_to_project + with user_and_project_generator(self.manager) as (_user, project): + self.manager.add_to_project(user, project) project = self.manager.get_project('testproj') - self.assertTrue(project.has_member('test2')) + self.assertTrue(self.manager.is_project_member(user, project)) def test_can_remove_user_from_project(self): with user_generator(self.manager, name='test2') as user: - with user_and_project_generator(self.manager): - self.manager.add_to_project('test2', 'testproj') - self.assertTrue(user.is_project_member('testproj')) - self.manager.remove_from_project('test2', 'testproj') - self.assertFalse(user.is_project_member('testproj')) - # NOTE(todd): have to reload after the membership modifications + with user_and_project_generator(self.manager) as (_user, project): + self.manager.add_to_project(user, project) project = self.manager.get_project('testproj') - self.assertFalse(project.has_member('test2')) + self.assertTrue(self.manager.is_project_member(user, project)) + self.manager.remove_from_project(user, project) + project = self.manager.get_project('testproj') + self.assertFalse(self.manager.is_project_member(user, project)) def test_can_add_remove_user_with_role(self): with user_generator(self.manager, name='test2') as user: - with user_and_project_generator(self.manager): - self.manager.add_to_project('test2', 'testproj') - self.manager.add_role('test2', 'developer', 'testproj') - self.assertTrue(user.is_project_member('testproj')) - self.manager.remove_from_project('test2', 'testproj') - self.assertFalse(self.manager.has_role('test2', 'developer', - 'testproj')) - self.assertFalse(user.is_project_member('testproj')) + with user_and_project_generator(self.manager) as (_user, project): + # NOTE(todd): after modifying users you must reload project + self.manager.add_to_project(user, project) + project = self.manager.get_project('testproj') + self.manager.add_role(user, 'developer', project) + self.assertTrue(self.manager.is_project_member(user, project)) + self.manager.remove_from_project(user, project) + project = self.manager.get_project('testproj') + self.assertFalse(self.manager.has_role(user, 'developer', + project)) + self.assertFalse(self.manager.is_project_member(user, project)) def test_can_generate_x509(self): # NOTE(todd): this doesn't assert against the auth manager @@ -231,31 +229,31 @@ class AuthTestCase(test.BaseTestCase): def test_adding_role_to_project_is_ignored_unless_added_to_user(self): with user_and_project_generator(self.manager) as (user, project): - self.assertFalse(project.has_role('test1', 'sysadmin')) - project.add_role('test1', 'sysadmin') + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) + self.manager.add_role(user, 'sysadmin', project) # NOTE(todd): it will still show up in get_user_roles(u, project) - self.assertFalse(project.has_role('test1', 'sysadmin')) - self.manager.add_role('test1', 'sysadmin') - self.assertTrue(project.has_role('test1', 'sysadmin')) + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) + self.manager.add_role(user, 'sysadmin') + self.assertTrue(self.manager.has_role(user, 'sysadmin', project)) def test_add_user_role_doesnt_infect_project_roles(self): with user_and_project_generator(self.manager) as (user, project): - self.assertFalse(project.has_role('test1', 'sysadmin')) - self.manager.add_role('test1', 'sysadmin') - self.assertFalse(project.has_role('test1', 'sysadmin')) + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) + self.manager.add_role(user, 'sysadmin') + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) def test_can_list_user_roles(self): with user_and_project_generator(self.manager) as (user, project): - self.manager.add_role('test1', 'sysadmin') + self.manager.add_role(user, 'sysadmin') roles = self.manager.get_user_roles(user) self.assertTrue('sysadmin' in roles) self.assertFalse('netadmin' in roles) def test_can_list_project_roles(self): with user_and_project_generator(self.manager) as (user, project): - self.manager.add_role('test1', 'sysadmin') - project.add_role('test1', 'sysadmin') - project.add_role('test1', 'netadmin') + self.manager.add_role(user, 'sysadmin') + self.manager.add_role(user, 'sysadmin', project) + self.manager.add_role(user, 'netadmin', project) project_roles = self.manager.get_user_roles(user, project) self.assertTrue('sysadmin' in project_roles) self.assertTrue('netadmin' in project_roles) @@ -264,28 +262,27 @@ class AuthTestCase(test.BaseTestCase): def test_can_remove_user_roles(self): with user_and_project_generator(self.manager) as (user, project): - self.manager.add_role('test1', 'sysadmin') - self.assertTrue(user.has_role('sysadmin')) - self.manager.remove_role('test1', 'sysadmin') - self.assertFalse(user.has_role('sysadmin')) + self.manager.add_role(user, 'sysadmin') + self.assertTrue(self.manager.has_role(user, 'sysadmin')) + self.manager.remove_role(user, 'sysadmin') + self.assertFalse(self.manager.has_role(user, 'sysadmin')) def test_removing_user_role_hides_it_from_project(self): with user_and_project_generator(self.manager) as (user, project): - self.manager.add_role('test1', 'sysadmin') - project.add_role('test1', 'sysadmin') - self.assertTrue(project.has_role('test1', 'sysadmin')) - self.manager.remove_role('test1', 'sysadmin') - self.assertFalse(project.has_role('test1', 'sysadmin')) + self.manager.add_role(user, 'sysadmin') + self.manager.add_role(user, 'sysadmin', project) + self.assertTrue(self.manager.has_role(user, 'sysadmin', project)) + self.manager.remove_role(user, 'sysadmin') + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) def test_can_remove_project_role_but_keep_user_role(self): with user_and_project_generator(self.manager) as (user, project): - self.manager.add_role('test1', 'sysadmin') - project.add_role('test1', 'sysadmin') - self.assertTrue(user.has_role('sysadmin')) - self.assertTrue(project.has_role('test1', 'sysadmin')) - project.remove_role('test1', 'sysadmin') - self.assertFalse(project.has_role('test1', 'sysadmin')) - self.assertTrue(user.has_role('sysadmin')) + self.manager.add_role(user, 'sysadmin') + self.manager.add_role(user, 'sysadmin', project) + self.assertTrue(self.manager.has_role(user, 'sysadmin')) + self.manager.remove_role(user, 'sysadmin', project) + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) + self.assertTrue(self.manager.has_role(user, 'sysadmin')) def test_can_retrieve_project_by_user(self): with user_and_project_generator(self.manager) as (user, project): -- cgit From c3fcb1b2176f4b7afbffb3555da55c0754bacaad Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 25 Sep 2010 01:05:39 -0700 Subject: flush the nova chains --- tools/setup_iptables.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/setup_iptables.sh b/tools/setup_iptables.sh index 7368fadf9..d045b50cd 100755 --- a/tools/setup_iptables.sh +++ b/tools/setup_iptables.sh @@ -48,6 +48,9 @@ if [ "$CMD" == "clear" ]; then iptables -P FORWARD ACCEPT iptables -P OUTPUT ACCEPT iptables -F + iptables -F nova_input + iptables -F nova_output + iptables -F nova_forward iptables -X fi -- cgit From 125e69dd42f6f91f727258dc388d15ce63076d1f Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 25 Sep 2010 02:51:50 -0700 Subject: allow mgmt ip access to api --- tools/setup_iptables.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/setup_iptables.sh b/tools/setup_iptables.sh index d045b50cd..b7e2f9a11 100755 --- a/tools/setup_iptables.sh +++ b/tools/setup_iptables.sh @@ -106,6 +106,9 @@ fi if [ "$CMD" == "api" ] || [ "$CMD" == "all" ]; then iptables -A nova_input -m tcp -p tcp -d $IP --dport 8773 -j ACCEPT + if [ "$IP" != "$MGMT_IP" ]; then + iptables -A nova_input -m tcp -p tcp -d $MGMT_IP --dport 8773 -j ACCEPT + fi fi if [ "$CMD" == "redis" ] || [ "$CMD" == "all" ]; then -- cgit From 7ce67ea60f6e7d20665c10318b29e2659fd91513 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 25 Sep 2010 08:35:16 -0700 Subject: fix a few missed calls to _confirm_rule and 80 char issues --- nova/network/linux_net.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 38a616e83..8058c970d 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -60,8 +60,10 @@ def init_host(): "-j SNAT --to-source %s" % (FLAGS.private_range, FLAGS.routing_source_ip)) - _confirm_rule("POSTROUTING", "-t nat -s %s -j MASQUERADE" % FLAGS.private_range) - _confirm_rule("POSTROUTING", "-t nat -s %(range)s -d %(range)s -j ACCEPT" % {'range': FLAGS.private_range}) + _confirm_rule("POSTROUTING", "-t nat -s %s -j MASQUERADE" % + FLAGS.private_range) + _confirm_rule("POSTROUTING", "-t nat -s %(range)s -d %(range)s -j ACCEPT" % + {'range': FLAGS.private_range}) def bind_floating_ip(floating_ip): """Bind ip to public interface""" @@ -77,9 +79,10 @@ def unbind_floating_ip(floating_ip): def ensure_vlan_forward(public_ip, port, private_ip): """Sets up forwarding rules for vlan""" - _confirm_rule("FORWARD", "-d %s -p udp --dport 1194 -j ACCEPT" % private_ip) - _confirm_rule( - "PREROUTING -t nat -d %s -p udp --dport %s -j DNAT --to %s:1194" + _confirm_rule("FORWARD", "-d %s -p udp --dport 1194 -j ACCEPT" % + private_ip) + _confirm_rule("PREROUTING", + "-t nat -d %s -p udp --dport %s -j DNAT --to %s:1194" % (public_ip, port, private_ip)) @@ -93,8 +96,7 @@ def ensure_floating_forward(floating_ip, fixed_ip): _confirm_rule("FORWARD", "-d %s -p icmp -j ACCEPT" % (fixed_ip)) for (protocol, port) in DEFAULT_PORTS: - _confirm_rule( - "FORWARD -d %s -p %s --dport %s -j ACCEPT" + _confirm_rule("FORWARD","-d %s -p %s --dport %s -j ACCEPT" % (fixed_ip, protocol, port)) -- cgit From 888a99182ca3152f68b762dab4fc95d7d3f1cadb Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 25 Sep 2010 09:28:05 -0700 Subject: add forwarding ACCEPT for outgoing packets on compute host --- nova/network/linux_net.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 8058c970d..78cc64cb0 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -148,6 +148,7 @@ def ensure_bridge(bridge, interface, net_attrs=None): _confirm_rule("FORWARD", "--in-interface %s -j ACCEPT" % bridge) else: _execute("sudo ifconfig %s up" % bridge) + _confirm_rule("FORWARD", "--out-interface %s -j ACCEPT" % bridge) def get_dhcp_hosts(context, network_id): -- cgit From 6a3cd55a9c933c329da1117179d676e9141c5b4d Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 25 Sep 2010 10:47:51 -0700 Subject: disable output drop for the moment because it is too restrictive --- tools/setup_iptables.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/setup_iptables.sh b/tools/setup_iptables.sh index b7e2f9a11..dd91c76e0 100755 --- a/tools/setup_iptables.sh +++ b/tools/setup_iptables.sh @@ -52,6 +52,11 @@ if [ "$CMD" == "clear" ]; then iptables -F nova_output iptables -F nova_forward iptables -X + iptables -t nat -F + iptables -t nat -F nova_input + iptables -t nat -F nova_output + iptables -t nat -F nova_forward + iptables -t nat -X fi if [ "$CMD" == "base" ] || [ "$CMD" == "all" ]; then @@ -73,7 +78,7 @@ if [ "$CMD" == "base" ] || [ "$CMD" == "all" ]; then iptables -N nova_forward iptables -A FORWARD -j nova_forward - iptables -P OUTPUT DROP + # iptables -P OUTPUT DROP # too restrictive for the moment iptables -A OUTPUT -m state --state INVALID -j DROP iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT iptables -N nova_output -- cgit From 307b16447a16e438d78b8149418c0ef728c5300e Mon Sep 17 00:00:00 2001 From: Cerberus Date: Sat, 25 Sep 2010 13:00:19 -0500 Subject: Minor changes to be committed so trunk can be merged in --- nova/api/rackspace/servers.py | 26 +++++++++++++++++++++----- nova/tests/api/rackspace/servers.py | 5 +++-- nova/tests/api/rackspace/test_helper.py | 5 +++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 08b6768f9..9243f3ae6 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -22,11 +22,14 @@ from nova import rpc from nova import utils from nova import compute from nova.api.rackspace import base +from nova.api.rackspace import _id_translator from webob import exc from nova import flags FLAGS = flags.FLAGS +class ServersContext(object): pass + class Controller(base.Controller): _serialization_metadata = { 'application/xml': { @@ -39,12 +42,16 @@ class Controller(base.Controller): } } - def __init__(self): - self.instdir = compute.InstanceDirectory() + def __init__(self, db_driver=None): + self.context = ServersContext() + if not db_driver: + db_driver = FLAGS.db_driver + self.db = utils.import_object(db_driver) def index(self, req): - allowed_keys = [ 'id', 'name'] - return [_entity_inst(inst, allowed_keys) for inst in instdir.all] + unfiltered = [ 'id', 'name'] + instance_list = self.instance_get_all(self.context) + return [_entity_inst(inst, unfiltered) for inst in instance_list] def detail(self, req): return [_entity_inst(inst) for inst in instdir.all] @@ -79,12 +86,21 @@ class Controller(base.Controller): instance.save() return exc.HTTPNoContent() + def _id_translator(self): + service = nova.image.service.ImageService.load() + return _id_translator.RackspaceAPIIdTranslator( + "image", self.service.__class__.__name__) + def _build_server_instance(self, req): """Build instance data structure and save it to the data store.""" ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) inst = {} + + image_id = env['server']['imageId'] + opaque_id = self._id_translator.from_rs_id(image_id) + inst['name'] = env['server']['name'] - inst['image_id'] = env['server']['imageId'] + inst['image_id'] = opaque_id inst['instance_type'] = env['server']['flavorId'] inst['user_id'] = env['user']['id'] diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 2cfb8d45f..6addc4614 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -33,8 +33,9 @@ class ServersTest(unittest.TestCase): self.stubs.UnsetAll() def test_get_server_list(self): - req = webob.Request.blank('/v1.0/servers') - req.get_response(nova.api.API()) + req = webob.Request.blank('/v1.0/servers/') + res = req.get_response(nova.api.API()) + print res def test_create_instance(self): pass diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 1fb2a19cc..784f3e466 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -1,3 +1,4 @@ +from nova import utils import webob import webob.dec import datetime @@ -36,6 +37,9 @@ def stub_out_auth(stubs): @webob.dec.wsgify def fake_wsgi(self, req): return self.application + + def get_my_ip(): + return '127.0.0.1' stubs.Set(nova.api.rackspace.AuthMiddleware, '__init__', fake_auth_init) @@ -45,6 +49,7 @@ def stub_out_auth(stubs): '__call__', fake_wsgi) stubs.Set(nova.api.rackspace.RateLimitingMiddleware, '__call__', fake_wsgi) + stubs.Set(nova.utils, 'get_my_ip', get_my_ip) class FakeAuthDatabase(object): data = {} -- cgit From e627748aec6a4747e22975d6cd59c8f20bc00c70 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Sat, 25 Sep 2010 13:35:23 -0500 Subject: Modification of test stubbing to match new domain requirements for the router, and removal of the unnecessary rackspace base controller --- nova/api/rackspace/base.py | 24 ------------------------ nova/api/rackspace/flavors.py | 3 ++- nova/api/rackspace/images.py | 3 ++- nova/api/rackspace/servers.py | 29 ++++++++++++++++------------- nova/api/rackspace/sharedipgroups.py | 4 +++- nova/tests/api/rackspace/auth.py | 4 ++-- nova/tests/api/rackspace/servers.py | 4 +++- nova/tests/api/rackspace/test_helper.py | 30 +++++++++++++++++++----------- 8 files changed, 47 insertions(+), 54 deletions(-) delete mode 100644 nova/api/rackspace/base.py diff --git a/nova/api/rackspace/base.py b/nova/api/rackspace/base.py deleted file mode 100644 index 5e5bd6f54..000000000 --- a/nova/api/rackspace/base.py +++ /dev/null @@ -1,24 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from nova import wsgi - - -class Controller(wsgi.Controller): - """TODO(eday): Base controller for all rackspace controllers. What is this - for? Is this just Rackspace specific? """ - pass diff --git a/nova/api/rackspace/flavors.py b/nova/api/rackspace/flavors.py index 60b35c939..024011a71 100644 --- a/nova/api/rackspace/flavors.py +++ b/nova/api/rackspace/flavors.py @@ -17,9 +17,10 @@ from nova.api.rackspace import base from nova.compute import instance_types +from nova import wsgi from webob import exc -class Controller(base.Controller): +class Controller(wsgi.Controller): """Flavor controller for the Rackspace API.""" _serialization_metadata = { diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py index 2f3e928b9..9aaec52e2 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/rackspace/images.py @@ -16,11 +16,12 @@ # under the License. import nova.image.service +from nova import wsgi from nova.api.rackspace import base from nova.api.rackspace import _id_translator from webob import exc -class Controller(base.Controller): +class Controller(wsgi.Controller): _serialization_metadata = { 'application/xml': { diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 9243f3ae6..d94295861 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -21,16 +21,14 @@ from nova import flags from nova import rpc from nova import utils from nova import compute +from nova import flags from nova.api.rackspace import base from nova.api.rackspace import _id_translator from webob import exc -from nova import flags FLAGS = flags.FLAGS -class ServersContext(object): pass - -class Controller(base.Controller): +class Controller(wsgi.Controller): _serialization_metadata = { 'application/xml': { "plurals": "servers", @@ -43,27 +41,30 @@ class Controller(base.Controller): } def __init__(self, db_driver=None): - self.context = ServersContext() if not db_driver: db_driver = FLAGS.db_driver self.db = utils.import_object(db_driver) def index(self, req): unfiltered = [ 'id', 'name'] - instance_list = self.instance_get_all(self.context) - return [_entity_inst(inst, unfiltered) for inst in instance_list] + instance_list = self.db.instance_get_all(None) + res = [self._entity_inst(inst, unfiltered) for inst in \ + instance_list] + return self._entity_list(res) def detail(self, req): - return [_entity_inst(inst) for inst in instdir.all] + res = [self._entity_inst(inst) for inst in \ + self.db.instance_get_all(None)] + return self._entity_list(res) def show(self, req, id): - inst = self.instdir.get(id) + inst = self.db.instance_get(None, id) if inst: - return _entity_inst(inst) + return self._entity_inst(inst) raise exc.HTTPNotFound() def delete(self, req, id): - instance = self.instdir.get(id) + instance = self.db.instance_get(None, id) if not instance: return exc.HTTPNotFound() @@ -79,7 +80,7 @@ class Controller(base.Controller): return _entity_inst(inst) def update(self, req, id): - instance = self.instdir.get(instance_id) + instance = self.db.instance_get(None, id) if not instance: return exc.HTTPNotFound() instance.update(kwargs['server']) @@ -107,7 +108,6 @@ class Controller(base.Controller): inst['launch_time'] = ltime inst['mac_address'] = utils.generate_mac() - # TODO(dietz) Do we need any of these? inst['project_id'] = env['project']['id'] inst['reservation_id'] = reservation reservation = utils.generate_uid('r') @@ -125,6 +125,9 @@ class Controller(base.Controller): inst.save() return _entity_inst(inst) + def _entity_list(self, entities): + return dict(servers=entities) + def _entity_inst(self, inst, allowed_keys=None): """ Maps everything to Rackspace-like attributes for return""" diff --git a/nova/api/rackspace/sharedipgroups.py b/nova/api/rackspace/sharedipgroups.py index 986f11434..4d2d0ede1 100644 --- a/nova/api/rackspace/sharedipgroups.py +++ b/nova/api/rackspace/sharedipgroups.py @@ -15,4 +15,6 @@ # License for the specific language governing permissions and limitations # under the License. -class Controller(object): pass +from nova import wsgi + +class Controller(wsgi.Controller): pass diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index 429c22ad2..a6e10970f 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -15,8 +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) + test_helper.stub_out_rate_limiting(self.stubs) + test_helper.stub_for_testing(self.stubs) def tearDown(self): self.stubs.UnsetAll() diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 6addc4614..e50581632 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -27,13 +27,15 @@ class ServersTest(unittest.TestCase): self.stubs = stubout.StubOutForTesting() test_helper.FakeAuthManager.auth_data = {} test_helper.FakeAuthDatabase.data = {} + test_helper.stub_for_testing(self.stubs) + test_helper.stub_out_rate_limiting(self.stubs) test_helper.stub_out_auth(self.stubs) def tearDown(self): self.stubs.UnsetAll() def test_get_server_list(self): - req = webob.Request.blank('/v1.0/servers/') + req = webob.Request.blank('/v1.0/servers') res = req.get_response(nova.api.API()) print res diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 784f3e466..971eaf20a 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -5,6 +5,9 @@ import datetime from nova.wsgi import Router from nova import auth import nova.api.rackspace.auth +from nova import flags + +FLAGS = flags.FLAGS class Context(object): pass @@ -26,30 +29,35 @@ def fake_auth_init(self): self.auth = FakeAuthManager() self.host = 'foo' +@webob.dec.wsgify +def fake_wsgi(self, req): + return self.application + def stub_out_auth(stubs): def fake_auth_init(self, app): self.application = app + + stubs.Set(nova.api.rackspace.AuthMiddleware, + '__init__', fake_auth_init) + stubs.Set(nova.api.rackspace.AuthMiddleware, + '__call__', fake_wsgi) +def stub_out_rate_limiting(stubs): def fake_rate_init(self, app): super(nova.api.rackspace.RateLimitingMiddleware, self).__init__(app) self.application = app - @webob.dec.wsgify - def fake_wsgi(self, req): - return self.application - - def get_my_ip(): - return '127.0.0.1' - - 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) + +def stub_for_testing(stubs): + def get_my_ip(): + return '127.0.0.1' stubs.Set(nova.utils, 'get_my_ip', get_my_ip) + FLAGS.FAKE_subdomain = 'rs' class FakeAuthDatabase(object): data = {} -- cgit From 0d0884b2c1692d03e0994baecbb23ce24ef71e44 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 25 Sep 2010 13:53:29 -0700 Subject: allow in and out for network and compute hosts --- nova/network/linux_net.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 78cc64cb0..6b47b6d97 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -145,10 +145,10 @@ def ensure_bridge(bridge, interface, net_attrs=None): net_attrs['gateway'], net_attrs['broadcast'], net_attrs['netmask'])) - _confirm_rule("FORWARD", "--in-interface %s -j ACCEPT" % bridge) else: _execute("sudo ifconfig %s up" % bridge) - _confirm_rule("FORWARD", "--out-interface %s -j ACCEPT" % bridge) + _confirm_rule("FORWARD", "--in-interface %s -j ACCEPT" % bridge) + _confirm_rule("FORWARD", "--out-interface %s -j ACCEPT" % bridge) def get_dhcp_hosts(context, network_id): -- cgit From 5d6ab2b2540743e0a53b01129df722610b3ae3b6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 25 Sep 2010 18:33:27 -0700 Subject: reorganize iptables clear and make sure use_nova_chains is a boolean --- nova/network/linux_net.py | 4 ++-- tools/setup_iptables.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 6b47b6d97..01a1d2ad0 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -41,8 +41,8 @@ flags.DEFINE_string('bridge_dev', 'eth0', 'network device for bridges') flags.DEFINE_string('routing_source_ip', utils.get_my_ip(), 'Public IP of network host') -flags.DEFINE_string('use_nova_chains', False, - 'use the nova_ routing chains instead of default') +flags.DEFINE_boo('use_nova_chains', False, + 'use the nova_ routing chains instead of default') DEFAULT_PORTS = [("tcp", 80), ("tcp", 22), ("udp", 1194), ("tcp", 443)] diff --git a/tools/setup_iptables.sh b/tools/setup_iptables.sh index dd91c76e0..b6b8414e3 100755 --- a/tools/setup_iptables.sh +++ b/tools/setup_iptables.sh @@ -48,15 +48,15 @@ if [ "$CMD" == "clear" ]; then iptables -P FORWARD ACCEPT iptables -P OUTPUT ACCEPT iptables -F + iptables -t nat -F iptables -F nova_input iptables -F nova_output iptables -F nova_forward - iptables -X - iptables -t nat -F iptables -t nat -F nova_input iptables -t nat -F nova_output iptables -t nat -F nova_forward iptables -t nat -X + iptables -X fi if [ "$CMD" == "base" ] || [ "$CMD" == "all" ]; then -- cgit From 9f9f80a6282131ea944b6e3669527ea0c8c4705d Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Sun, 26 Sep 2010 14:45:27 -0700 Subject: Removed extra logging from debugging --- nova/adminclient.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nova/adminclient.py b/nova/adminclient.py index 8db5d9158..fc9fcfde0 100644 --- a/nova/adminclient.py +++ b/nova/adminclient.py @@ -223,7 +223,6 @@ class NovaAdminClient(object): """ Splits a cloud controller endpoint url. """ - import logging; logging.debug('clc_url = %s', clc_url) parts = httplib.urlsplit(clc_url) is_secure = parts.scheme == 'https' ip, port = parts.netloc.split(':') -- cgit From ab2bed9ed60c5333a0f9ba3e679df9893781b72f Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 27 Sep 2010 10:39:52 +0200 Subject: Apply IP configuration to bridge regardless of whether it existed before. The fixes a race condition on hosts running both compute and network where, if compute got there first, it would set up the bridge, but not do IP configuration (because that's meant to happen on the network host), and when network came around, it would see the interface already there and not configure it further. --- nova/network/linux_net.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 41aeb5da7..9d5bd8495 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -118,15 +118,16 @@ def ensure_bridge(bridge, interface, net_attrs=None): # _execute("sudo brctl setageing %s 10" % bridge) _execute("sudo brctl stp %s off" % bridge) _execute("sudo brctl addif %s %s" % (bridge, interface)) - if net_attrs: - _execute("sudo ifconfig %s %s broadcast %s netmask %s up" % \ - (bridge, - net_attrs['gateway'], - net_attrs['broadcast'], - net_attrs['netmask'])) - _confirm_rule("FORWARD --in-interface %s -j ACCEPT" % bridge) - else: - _execute("sudo ifconfig %s up" % bridge) + + if net_attrs: + _execute("sudo ifconfig %s %s broadcast %s netmask %s up" % \ + (bridge, + net_attrs['gateway'], + net_attrs['broadcast'], + net_attrs['netmask'])) + _confirm_rule("FORWARD --in-interface %s -j ACCEPT" % bridge) + else: + _execute("sudo ifconfig %s up" % bridge) def get_dhcp_hosts(context, network_id): -- cgit From b4dbc4efa576af61ddc26d1c277237ad4bcdfcfa Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 27 Sep 2010 12:07:55 +0200 Subject: Add db api methods for retrieving the networks for which a host is the designated network host. --- nova/db/api.py | 12 ++++++++++++ nova/db/sqlalchemy/api.py | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/nova/db/api.py b/nova/db/api.py index c1cb1953a..4657408db 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -554,3 +554,15 @@ def volume_update(context, volume_id, values): """ return IMPL.volume_update(context, volume_id, values) + + +################### + + +def host_get_networks(context, host): + """Return all networks for which the given host is the designated + network host + """ + return IMPL.host_get_networks(context, host) + + diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 2b0dd6ea6..6e6b0e3fc 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -848,3 +848,15 @@ def volume_update(_context, volume_id, values): for (key, value) in values.iteritems(): volume_ref[key] = value volume_ref.save(session=session) + + +################### + + +def host_get_networks(context, host): + session = get_session() + with session.begin(): + return session.query(models.Network + ).filter_by(deleted=False + ).filter_by(host=host + ).all() -- cgit From e70948dbec0b21664739b2b7cdb1cc3da92bd01b Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 27 Sep 2010 12:08:40 +0200 Subject: Set up network at manager instantiation time to ensure we're ready to handle the networks we're already supposed to handle. --- nova/network/manager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nova/network/manager.py b/nova/network/manager.py index 191c1d364..c17823f1e 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -80,6 +80,10 @@ class NetworkManager(manager.Manager): network_driver = FLAGS.network_driver self.driver = utils.import_object(network_driver) super(NetworkManager, self).__init__(*args, **kwargs) + # Set up networking for the projects for which we're already + # the designated network host. + for network in self.db.host_get_networks(None, host=kwargs['host']): + self._on_set_network_host(None, network['id']) def set_network_host(self, context, project_id): """Safely sets the host of the projects network""" -- cgit From 47cccfc21dfd4c1acf74b6d84ced8abba8c40e76 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 27 Sep 2010 12:14:20 +0200 Subject: Ensure dnsmasq can read updates to dnsmasq conffile. --- nova/network/linux_net.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 9d5bd8495..7d708968c 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -150,9 +150,14 @@ def update_dhcp(context, network_id): signal causing it to reload, otherwise spawn a new instance """ network_ref = db.network_get(context, network_id) - with open(_dhcp_file(network_ref['vlan'], 'conf'), 'w') as f: + + conffile = _dhcp_file(network_ref['vlan'], 'conf') + with open(conffile, 'w') as f: f.write(get_dhcp_hosts(context, network_id)) + # Make sure dnsmasq can actually read it (it setuid()s to "nobody") + os.chmod(conffile, 0644) + pid = _dnsmasq_pid_for(network_ref['vlan']) # if dnsmasq is already running, then tell it to reload -- cgit From 928df580e5973bc1fd3871a0aa31886302bb9268 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 27 Sep 2010 13:03:29 +0200 Subject: Add a flag the specifies where to find nova-dhcpbridge. --- nova/network/linux_net.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 7d708968c..bfa73dca0 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -28,6 +28,11 @@ from nova import flags from nova import utils +def _bin_file(script): + """Return the absolute path to scipt in the bin directory""" + return os.path.abspath(os.path.join(__file__, "../../../bin", script)) + + FLAGS = flags.FLAGS flags.DEFINE_string('dhcpbridge_flagfile', '/etc/nova/nova-dhcpbridge.conf', @@ -39,6 +44,8 @@ flags.DEFINE_string('public_interface', 'vlan1', 'Interface for public IP addresses') flags.DEFINE_string('bridge_dev', 'eth0', 'network device for bridges') +flags.DEFINE_string('dhcpbridge', _bin_file('nova-dhcpbridge'), + 'location of nova-dhcpbridge') DEFAULT_PORTS = [("tcp", 80), ("tcp", 22), ("udp", 1194), ("tcp", 443)] @@ -222,7 +229,7 @@ def _dnsmasq_cmd(net): ' --except-interface=lo', ' --dhcp-range=%s,static,120s' % net['dhcp_start'], ' --dhcp-hostsfile=%s' % _dhcp_file(net['vlan'], 'conf'), - ' --dhcp-script=%s' % _bin_file('nova-dhcpbridge'), + ' --dhcp-script=%s' % FLAGS.dhcpbridge, ' --leasefile-ro'] return ''.join(cmd) @@ -244,11 +251,6 @@ def _dhcp_file(vlan, kind): return os.path.abspath("%s/nova-%s.%s" % (FLAGS.networks_path, vlan, kind)) -def _bin_file(script): - """Return the absolute path to scipt in the bin directory""" - return os.path.abspath(os.path.join(__file__, "../../../bin", script)) - - def _dnsmasq_pid_for(vlan): """Returns he pid for prior dnsmasq instance for a vlan -- cgit From 04fa25e63bf37222d2b1cf88837f1c85cf944f54 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 27 Sep 2010 13:23:39 +0200 Subject: Only call _on_set_network_host on nova-network hosts. --- nova/network/manager.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index c17823f1e..2530f04b7 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -80,10 +80,13 @@ class NetworkManager(manager.Manager): network_driver = FLAGS.network_driver self.driver = utils.import_object(network_driver) super(NetworkManager, self).__init__(*args, **kwargs) - # Set up networking for the projects for which we're already - # the designated network host. - for network in self.db.host_get_networks(None, host=kwargs['host']): - self._on_set_network_host(None, network['id']) + # Host only gets passed if being instantiated as part of the network + # service. + if 'host' in kwargs: + # Set up networking for the projects for which we're already + # the designated network host. + for network in self.db.host_get_networks(None, host=kwargs['host']): + self._on_set_network_host(None, network['id']) def set_network_host(self, context, project_id): """Safely sets the host of the projects network""" -- cgit From 1c978e8414b5841c4caf856c80f385026600f54e Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 27 Sep 2010 12:50:20 -0400 Subject: Support content type detection in serializer --- nova/api/rackspace/__init__.py | 2 +- nova/api/rackspace/servers.py | 5 ++--- nova/tests/api/wsgi_test.py | 33 ++++++++++++++++++++++++++++++--- nova/wsgi.py | 21 ++++++++++++++------- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index 5f4020837..736486733 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -140,7 +140,7 @@ class APIRouter(wsgi.Router): def __init__(self): mapper = routes.Mapper() - mapper.resource("server", "servers", controller=servers.Controller() + mapper.resource("server", "servers", controller=servers.Controller(), collection={'detail': 'GET'}) mapper.resource("image", "images", controller=images.Controller(), collection={'detail': 'GET'}) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 3ba5af8cf..761ce2895 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -29,7 +29,6 @@ FLAGS = flags.FLAGS class Controller(base.Controller): _serialization_metadata = { 'application/xml': { - "plurals": "servers", "attributes": { "server": [ "id", "imageId", "name", "flavorId", "hostId", "status", "progress", "addresses", "metadata", @@ -39,7 +38,7 @@ class Controller(base.Controller): } def __init__(self): - self.instdir = compute.InstanceDirectory() + self.instdir = None # TODO(cerberus): compute doesn't exist. compute.InstanceDirectory() def index(self, req): allowed_keys = [ 'id', 'name'] @@ -121,7 +120,7 @@ class Controller(base.Controller): 'reservation_id', 'project_id', 'launch_time', 'bridge_name', 'mac_address', 'user_id'] - for key in filtered_keys:: + for key in filtered_keys: del inst[key] if allowed_keys: diff --git a/nova/tests/api/wsgi_test.py b/nova/tests/api/wsgi_test.py index 786dc1bce..145b1bfee 100644 --- a/nova/tests/api/wsgi_test.py +++ b/nova/tests/api/wsgi_test.py @@ -91,6 +91,33 @@ class Test(unittest.TestCase): result = webob.Request.blank('/test/123').get_response(Router()) self.assertNotEqual(result.body, "123") - def test_serializer(self): - # TODO(eday): Placeholder for serializer testing. - pass + +class SerializerTest(unittest.TestCase): + + def match(self, url, accept, expect): + input_dict = dict(servers=dict(a=(2,3))) + expected_xml = '(2,3)' + expected_json = '{"servers":{"a":[2,3]}}' + req = webob.Request.blank(url, headers=dict(Accept=accept)) + result = wsgi.Serializer(req.environ).to_content_type(input_dict) + result = result.replace('\n', '').replace(' ', '') + if expect == 'xml': + self.assertEqual(result, expected_xml) + elif expect == 'json': + self.assertEqual(result, expected_json) + else: + raise "Bad expect value" + + def test_basic(self): + self.match('/servers/4.json', None, expect='json') + self.match('/servers/4', 'application/json', expect='json') + self.match('/servers/4', 'application/xml', expect='xml') + self.match('/servers/4.xml', None, expect='xml') + + def test_defaults_to_json(self): + self.match('/servers/4', None, expect='json') + self.match('/servers/4', 'text/html', expect='json') + + def test_suffix_takes_precedence_over_accept_header(self): + self.match('/servers/4.xml', 'application/json', expect='xml') + self.match('/servers/4.xml.', 'application/json', expect='json') diff --git a/nova/wsgi.py b/nova/wsgi.py index 8a4e2a9f4..ac319db40 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -29,6 +29,7 @@ import eventlet.wsgi eventlet.patcher.monkey_patch(all=False, socket=True) import routes import routes.middleware +import webob import webob.dec import webob.exc @@ -239,11 +240,19 @@ class Serializer(object): 'metadata' is an optional dict mapping MIME types to information needed to serialize a dictionary to that type. """ - self.environ = environ self.metadata = metadata or {} - self._methods = { - 'application/json': self._to_json, - 'application/xml': self._to_xml} + req = webob.Request(environ) + suffix = req.path_info.split('.')[-1].lower() + if suffix == 'json': + self.handler = self._to_json + elif suffix == 'xml': + self.handler = self._to_xml + elif 'application/json' in req.accept: + self.handler = self._to_json + elif 'application/xml' in req.accept: + self.handler = self._to_xml + else: + self.handler = self._to_json # default def to_content_type(self, data): """ @@ -251,9 +260,7 @@ class Serializer(object): will be decided based on the Content Type requested in self.environ: by Accept: header, or by URL suffix. """ - mimetype = 'application/xml' - # TODO(gundlach): determine mimetype from request - return self._methods.get(mimetype, repr)(data) + return self.handler(data) def _to_json(self, data): import json -- cgit From 1d83acca365b13319bddbd628725d7b666879091 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Mon, 27 Sep 2010 12:58:35 -0500 Subject: More re-work around the ORM changes and testing --- nova/api/rackspace/servers.py | 47 +++++++++++++++++++------------------ nova/tests/api/rackspace/flavors.py | 15 +++++++++++- nova/tests/api/rackspace/servers.py | 37 ++++++++++++++++++++++------- 3 files changed, 67 insertions(+), 32 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index d94295861..7973fa770 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -16,6 +16,7 @@ # under the License. import time +from nova import wsgi from nova import db from nova import flags from nova import rpc @@ -29,13 +30,12 @@ from webob import exc FLAGS = flags.FLAGS class Controller(wsgi.Controller): + _serialization_metadata = { 'application/xml': { - "plurals": "servers", "attributes": { "server": [ "id", "imageId", "name", "flavorId", "hostId", - "status", "progress", "addresses", "metadata", - "progress" ] + "status", "progress", "progress" ] } } } @@ -46,21 +46,19 @@ class Controller(wsgi.Controller): self.db = utils.import_object(db_driver) def index(self, req): - unfiltered = [ 'id', 'name'] instance_list = self.db.instance_get_all(None) - res = [self._entity_inst(inst, unfiltered) for inst in \ - instance_list] + res = [self._entity_inst(inst)['server'] for inst in instance_list] return self._entity_list(res) def detail(self, req): - res = [self._entity_inst(inst) for inst in \ + res = [self._entity_detail(inst)['server'] for inst in \ self.db.instance_get_all(None)] return self._entity_list(res) def show(self, req, id): inst = self.db.instance_get(None, id) if inst: - return self._entity_inst(inst) + return self._entity_detail(inst) raise exc.HTTPNotFound() def delete(self, req, id): @@ -128,25 +126,28 @@ class Controller(wsgi.Controller): def _entity_list(self, entities): return dict(servers=entities) - def _entity_inst(self, inst, allowed_keys=None): + def _entity_detail(self, inst): """ Maps everything to Rackspace-like attributes for return""" + inst_dir = {} + + mapped_keys = dict(status='state_description', imageId='image_id', + flavorId='instance_type', name='name') - translated_keys = dict(metadata={}, status=state_description, - id=instance_id, imageId=image_id, flavorId=instance_type,) + for k,v in mapped_keys.iteritems(): + inst_dir[k] = inst[v] - for k,v in translated_keys.iteritems(): - inst[k] = inst[v] + inst_dir['addresses'] = dict(public=[], private=[]) + inst_dir['metadata'] = {} + inst_dir['hostId'] = '' - filtered_keys = ['instance_id', 'state_description', 'state', - 'reservation_id', 'project_id', 'launch_time', - 'bridge_name', 'mac_address', 'user_id'] + return dict(server=inst_dir) - for key in filtered_keys: - del inst[key] + def _entity_inst(self, inst): + """ Filters all model attributes save for id and name """ + return dict(server=dict(id=inst['id'], name=inst['name'])) - if allowed_keys: - for key in inst.keys(): - if key not in allowed_keys: - del inst[key] + def _to_rs_power_state(self, inst): + pass - return dict(server=inst) + def _from_rs_power_state(self, inst): + pass diff --git a/nova/tests/api/rackspace/flavors.py b/nova/tests/api/rackspace/flavors.py index fb8ba94a5..7bd1ea1c4 100644 --- a/nova/tests/api/rackspace/flavors.py +++ b/nova/tests/api/rackspace/flavors.py @@ -16,19 +16,32 @@ # under the License. import unittest +import stubout +import nova.api from nova.api.rackspace import flavors +from nova.tests.api.rackspace import test_helper from nova.tests.api.test_helper import * class FlavorsTest(unittest.TestCase): def setUp(self): self.stubs = stubout.StubOutForTesting() + test_helper.FakeAuthManager.auth_data = {} + test_helper.FakeAuthDatabase.data = {} + test_helper.stub_for_testing(self.stubs) + test_helper.stub_out_rate_limiting(self.stubs) + test_helper.stub_out_auth(self.stubs) def tearDown(self): self.stubs.UnsetAll() def test_get_flavor_list(self): - pass + req = webob.Request.blank('/v1.0/flavors') + res = req.get_response(nova.api.API()) + print res def test_get_flavor_by_id(self): pass + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index e50581632..0f3483207 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -17,10 +17,25 @@ import unittest import stubout -from nova.api.rackspace import servers import nova.api.rackspace +import nova.db.api +from nova import flags +from nova.api.rackspace import servers +from nova.db.sqlalchemy.models import Instance from nova.tests.api.test_helper import * from nova.tests.api.rackspace import test_helper +from nova import db + +FLAGS = flags.FLAGS + +def return_server(context, id): + return stub_instance(id) + +def return_servers(context): + return [stub_instance(i) for i in xrange(5)] + +def stub_instance(id): + return Instance(id=id, state=0, ) class ServersTest(unittest.TestCase): def setUp(self): @@ -30,26 +45,32 @@ class ServersTest(unittest.TestCase): test_helper.stub_for_testing(self.stubs) test_helper.stub_out_rate_limiting(self.stubs) test_helper.stub_out_auth(self.stubs) + self.stubs.Set(nova.db.api, 'instance_get_all', return_servers) + self.stubs.Set(nova.db.api, 'instance_get', return_server) def tearDown(self): self.stubs.UnsetAll() - def test_get_server_list(self): - req = webob.Request.blank('/v1.0/servers') + def test_get_server_by_id(self): + req = webob.Request.blank('/v1.0/servers/1') res = req.get_response(nova.api.API()) print res - def test_create_instance(self): + def test_get_backup_schedule(self): pass - def test_get_server_by_id(self): - pass + def test_get_server_list(self): + req = webob.Request.blank('/v1.0/servers') + res = req.get_response(nova.api.API()) + print res - def test_get_backup_schedule(self): + def test_create_instance(self): pass def test_get_server_details(self): - pass + req = webob.Request.blank('/v1.0/servers/detail') + res = req.get_response(nova.api.API()) + print res def test_get_server_ips(self): pass -- cgit From 0e6c3b6034ef4927e381b231bf120a4980512c4e Mon Sep 17 00:00:00 2001 From: Cerberus Date: Mon, 27 Sep 2010 17:01:37 -0500 Subject: Power state mapping --- nova/api/rackspace/servers.py | 42 +++++++++++++++++++++++-------------- nova/tests/api/rackspace/servers.py | 1 + nova/tests/api/test_helper.py | 1 + 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 7973fa770..53824ee1b 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -14,8 +14,9 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -import time +import time +import nova.image.service from nova import wsgi from nova import db from nova import flags @@ -23,6 +24,7 @@ from nova import rpc from nova import utils from nova import compute from nova import flags +from nova.compute import power_state from nova.api.rackspace import base from nova.api.rackspace import _id_translator from webob import exc @@ -31,6 +33,16 @@ FLAGS = flags.FLAGS class Controller(wsgi.Controller): + _power_mapping = { + power_state.NOSTATE: 'build', + power_state.RUNNING: 'active', + power_state.BLOCKED: 'active', + power_state.PAUSED: 'suspended', + power_state.SHUTDOWN: 'active', + power_state.SHUTOFF: 'active', + power_state.CRASHED: 'error' + } + _serialization_metadata = { 'application/xml': { "attributes": { @@ -74,7 +86,7 @@ class Controller(wsgi.Controller): rpc.cast( FLAGS.compute_topic, { "method": "run_instance", - "args": {"instance_id": inst.instance_id}}) + "args": {"instance_id": inst.id}}) return _entity_inst(inst) def update(self, req, id): @@ -121,33 +133,31 @@ class Controller(wsgi.Controller): 'default')['bridge_name'] inst.save() - return _entity_inst(inst) + return inst + + def _filter_params(self, inst_dict): + pass def _entity_list(self, entities): return dict(servers=entities) def _entity_detail(self, inst): """ Maps everything to Rackspace-like attributes for return""" - inst_dir = {} + inst_dict = {} - mapped_keys = dict(status='state_description', imageId='image_id', + mapped_keys = dict(status='state', imageId='image_id', flavorId='instance_type', name='name') for k,v in mapped_keys.iteritems(): - inst_dir[k] = inst[v] + inst_dict[k] = inst[v] - inst_dir['addresses'] = dict(public=[], private=[]) - inst_dir['metadata'] = {} - inst_dir['hostId'] = '' + inst_dict['status'] = Controller._power_mapping[inst_dict['status']] + inst_dict['addresses'] = dict(public=[], private=[]) + inst_dict['metadata'] = {} + inst_dict['hostId'] = '' - return dict(server=inst_dir) + return dict(server=inst_dict) def _entity_inst(self, inst): """ Filters all model attributes save for id and name """ return dict(server=dict(id=inst['id'], name=inst['name'])) - - def _to_rs_power_state(self, inst): - pass - - def _from_rs_power_state(self, inst): - pass diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 0f3483207..dd54a6ac8 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -53,6 +53,7 @@ class ServersTest(unittest.TestCase): def test_get_server_by_id(self): req = webob.Request.blank('/v1.0/servers/1') + req.headers['content-type'] = 'application/json' res = req.get_response(nova.api.API()) print res diff --git a/nova/tests/api/test_helper.py b/nova/tests/api/test_helper.py index 8151a4af6..d0a2cc027 100644 --- a/nova/tests/api/test_helper.py +++ b/nova/tests/api/test_helper.py @@ -1,4 +1,5 @@ import webob.dec +from nova import wsgi class APIStub(object): """Class to verify request and mark it was called.""" -- cgit From 2f72b2a9fc9fee508b16c0b96285124279ef89ca Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 28 Sep 2010 00:23:49 -0500 Subject: More cleanup, backup_schedules controller, server details and the beginnings of the servers action route --- nova/api/rackspace/__init__.py | 17 +++++-- nova/api/rackspace/backup_schedules.py | 37 ++++++++++++++ nova/api/rackspace/flavors.py | 1 - nova/api/rackspace/images.py | 1 - nova/api/rackspace/servers.py | 22 +++++---- nova/db/sqlalchemy/models.py | 4 +- nova/tests/api/rackspace/servers.py | 85 ++++++++++++++++++++++++++++----- nova/tests/api/rackspace/test_helper.py | 1 + 8 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 nova/api/rackspace/backup_schedules.py diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index 63b0edc6a..a10a9c6df 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -35,6 +35,7 @@ from nova.api.rackspace import flavors from nova.api.rackspace import images from nova.api.rackspace import ratelimiting from nova.api.rackspace import servers +from nova.api.rackspace import backup_schedules from nova.api.rackspace import sharedipgroups from nova.auth import manager @@ -67,8 +68,10 @@ class AuthMiddleware(wsgi.Middleware): if not user: return webob.exc.HTTPUnauthorized() - context = {'user': user} - req.environ['nova.context'] = context + + if not req.environ.has_key('nova.context'): + req.environ['nova.context'] = {} + req.environ['nova.context']['user'] = user return self.application class RateLimitingMiddleware(wsgi.Middleware): @@ -146,11 +149,19 @@ class APIRouter(wsgi.Router): def __init__(self): mapper = routes.Mapper() mapper.resource("server", "servers", controller=servers.Controller(), - collection={'detail': 'GET'}) + collection={ 'detail': 'GET'}, + member={'action':'POST'}) + + mapper.resource("backup_schedule", "backup_schedules", + controller=backup_schedules.Controller(), + parent_resource=dict(member_name='server', + collection_name = 'servers')) + mapper.resource("image", "images", controller=images.Controller(), collection={'detail': 'GET'}) mapper.resource("flavor", "flavors", controller=flavors.Controller(), collection={'detail': 'GET'}) mapper.resource("sharedipgroup", "sharedipgroups", controller=sharedipgroups.Controller()) + super(APIRouter, self).__init__(mapper) diff --git a/nova/api/rackspace/backup_schedules.py b/nova/api/rackspace/backup_schedules.py new file mode 100644 index 000000000..a18dfb87c --- /dev/null +++ b/nova/api/rackspace/backup_schedules.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import time +import nova.image.service +from nova import wsgi +from nova.api.rackspace import _id_translator +from webob import exc + +class Controller(wsgi.Controller): + def __init__(self): + pass + + def index(self, req, server_id): + return exc.HTTPNotFound() + + def create(self, req, server_id): + """ No actual update method required, since the existing API allows + both create and update through a POST """ + return exc.HTTPNotFound() + + def delete(self, req, server_id): + return exc.HTTPNotFound() diff --git a/nova/api/rackspace/flavors.py b/nova/api/rackspace/flavors.py index 024011a71..3bcf170e5 100644 --- a/nova/api/rackspace/flavors.py +++ b/nova/api/rackspace/flavors.py @@ -15,7 +15,6 @@ # License for the specific language governing permissions and limitations # under the License. -from nova.api.rackspace import base from nova.compute import instance_types from nova import wsgi from webob import exc diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py index 9aaec52e2..10e15de3f 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/rackspace/images.py @@ -17,7 +17,6 @@ import nova.image.service from nova import wsgi -from nova.api.rackspace import base from nova.api.rackspace import _id_translator from webob import exc diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 53824ee1b..d825e734e 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -25,7 +25,6 @@ from nova import utils from nova import compute from nova import flags from nova.compute import power_state -from nova.api.rackspace import base from nova.api.rackspace import _id_translator from webob import exc @@ -63,11 +62,12 @@ class Controller(wsgi.Controller): return self._entity_list(res) def detail(self, req): - res = [self._entity_detail(inst)['server'] for inst in \ + res = [self._entity_detail(inst)['server'] for inst in self.db.instance_get_all(None)] return self._entity_list(res) def show(self, req, id): + user = req.environ['nova.context']['user'] inst = self.db.instance_get(None, id) if inst: return self._entity_detail(inst) @@ -75,10 +75,9 @@ class Controller(wsgi.Controller): def delete(self, req, id): instance = self.db.instance_get(None, id) - if not instance: return exc.HTTPNotFound() - instance.destroy() + self.db.instance_destroy(None, id) return exc.HTTPAccepted() def create(self, req): @@ -93,10 +92,17 @@ class Controller(wsgi.Controller): instance = self.db.instance_get(None, id) if not instance: return exc.HTTPNotFound() - instance.update(kwargs['server']) - instance.save() + + attrs = req.environ['nova.context'].get('model_attributes', None) + if attrs: + self.db.instance_update(None, id, attrs) return exc.HTTPNoContent() + def action(self, req, id): + """ multi-purpose method used to reboot, rebuild, and + resize a server """ + return {} + def _id_translator(self): service = nova.image.service.ImageService.load() return _id_translator.RackspaceAPIIdTranslator( @@ -132,7 +138,7 @@ class Controller(wsgi.Controller): inst['project_id'], 'default')['bridge_name'] - inst.save() + self.db.instance_create(None, inst) return inst def _filter_params(self, inst_dict): @@ -146,7 +152,7 @@ class Controller(wsgi.Controller): inst_dict = {} mapped_keys = dict(status='state', imageId='image_id', - flavorId='instance_type', name='name') + flavorId='instance_type', name='name', id='id') for k,v in mapped_keys.iteritems(): inst_dict[k] = inst[v] diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index f6ba7953f..47c505f25 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -209,12 +209,14 @@ class Instance(BASE, NovaBase): @property def name(self): - return self.str_id + return self.server_name or self.str_id image_id = Column(String(255)) kernel_id = Column(String(255)) ramdisk_id = Column(String(255)) + server_name = 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) diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index dd54a6ac8..0ef0f4256 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import unittest import stubout import nova.api.rackspace @@ -35,7 +36,9 @@ def return_servers(context): return [stub_instance(i) for i in xrange(5)] def stub_instance(id): - return Instance(id=id, state=0, ) + return Instance( + id=id, state=0, image_id=10, server_name='server%s'%id + ) class ServersTest(unittest.TestCase): def setUp(self): @@ -53,9 +56,10 @@ class ServersTest(unittest.TestCase): def test_get_server_by_id(self): req = webob.Request.blank('/v1.0/servers/1') - req.headers['content-type'] = 'application/json' res = req.get_response(nova.api.API()) - print res + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], '1') + self.assertEqual(res_dict['server']['name'], 'server1') def test_get_backup_schedule(self): pass @@ -63,30 +67,85 @@ class ServersTest(unittest.TestCase): def test_get_server_list(self): req = webob.Request.blank('/v1.0/servers') res = req.get_response(nova.api.API()) - print res + res_dict = json.loads(res.body) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s['id'], i) + self.assertEqual(s['name'], 'server%d'%i) + self.assertEqual(s.get('imageId', None), None) + i += 1 def test_create_instance(self): pass - def test_get_server_details(self): - req = webob.Request.blank('/v1.0/servers/detail') - res = req.get_response(nova.api.API()) - print res + def test_update_server_password(self): + pass - def test_get_server_ips(self): + def test_update_server_name(self): pass + def test_create_backup_schedules(self): + req = webob.Request.blank('/v1.0/servers/1/backup_schedules') + req.method = 'POST' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_delete_backup_schedules(self): + req = webob.Request.blank('/v1.0/servers/1/backup_schedules') + req.method = 'DELETE' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_get_server_backup_schedules(self): + req = webob.Request.blank('/v1.0/servers/1/backup_schedules') + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_get_all_server_details(self): + req = webob.Request.blank('/v1.0/servers/detail') + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s['id'], i) + self.assertEqual(s['name'], 'server%d'%i) + self.assertEqual(s['imageId'], 10) + i += 1 + def test_server_reboot(self): - pass + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) def test_server_rebuild(self): - pass + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) def test_server_resize(self): - pass + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) def test_delete_server_instance(self): - pass + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'DELETE' + + self.server_delete_called = False + def instance_destroy_mock(context, id): + self.server_delete_called = True + + self.stubs.Set(nova.db.api, 'instance_destroy', + instance_destroy_mock) + + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '202 Accepted') + self.assertEqual(self.server_delete_called, True) if __name__ == "__main__": unittest.main() diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 971eaf20a..d44b8c30e 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -31,6 +31,7 @@ def fake_auth_init(self): @webob.dec.wsgify def fake_wsgi(self, req): + req.environ['nova.context'] = dict(user=dict(id=1)) return self.application def stub_out_auth(stubs): -- cgit From 7e25838ea1965231df09f29675fc3ab40e194483 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 28 Sep 2010 00:44:32 -0500 Subject: db api call to get instances by user and user checking in each of the server actions --- nova/api/rackspace/servers.py | 29 +++++++++++++++++++---------- nova/db/api.py | 3 +++ nova/db/sqlalchemy/api.py | 7 +++++++ nova/tests/api/rackspace/servers.py | 12 ++++++++---- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index d825e734e..becac9140 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -57,28 +57,32 @@ class Controller(wsgi.Controller): self.db = utils.import_object(db_driver) def index(self, req): - instance_list = self.db.instance_get_all(None) + user_id = req.environ['nova.context']['user']['id'] + instance_list = self.db.instance_get_all_by_user(None, user_id) res = [self._entity_inst(inst)['server'] for inst in instance_list] return self._entity_list(res) def detail(self, req): + user_id = req.environ['nova.context']['user']['id'] res = [self._entity_detail(inst)['server'] for inst in - self.db.instance_get_all(None)] + self.db.instance_get_all_by_user(None, user_id)] return self._entity_list(res) def show(self, req, id): - user = req.environ['nova.context']['user'] + user_id = req.environ['nova.context']['user']['id'] inst = self.db.instance_get(None, id) if inst: - return self._entity_detail(inst) + if inst.user_id == user_id: + return self._entity_detail(inst) raise exc.HTTPNotFound() def delete(self, req, id): + user_id = req.environ['nova.context']['user']['id'] instance = self.db.instance_get(None, id) - if not instance: - return exc.HTTPNotFound() - self.db.instance_destroy(None, id) - return exc.HTTPAccepted() + if instance and instance['user_id'] == user_id: + self.db.instance_destroy(None, id) + return exc.HTTPAccepted() + return exc.HTTPNotFound() def create(self, req): inst = self._build_server_instance(req) @@ -95,7 +99,7 @@ class Controller(wsgi.Controller): attrs = req.environ['nova.context'].get('model_attributes', None) if attrs: - self.db.instance_update(None, id, attrs) + self.db.instance_update(None, id, self._filter_params(attrs)) return exc.HTTPNoContent() def action(self, req, id): @@ -142,7 +146,12 @@ class Controller(wsgi.Controller): return inst def _filter_params(self, inst_dict): - pass + keys = ['name', 'adminPass'] + new_attrs = {} + for k in keys: + if inst_dict.has_key(k): + new_attrs[k] = inst_dict[k] + return new_attrs def _entity_list(self, entities): return dict(servers=entities) diff --git a/nova/db/api.py b/nova/db/api.py index c1cb1953a..b0b8f234d 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -251,6 +251,9 @@ def instance_get_all(context): """Get all instances.""" return IMPL.instance_get_all(context) +def instance_get_all_by_user(context, user_id): + """Get all instances.""" + return IMPL.instance_get_all(context, user_id) def instance_get_by_project(context, project_id): """Get all instance belonging to a project.""" diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 2b0dd6ea6..5c1ceee92 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -389,6 +389,13 @@ def instance_get_all(context): ).filter_by(deleted=_deleted(context) ).all() +def instance_get_all_by_user(context, user_id): + session = get_session() + return session.query(models.Instance + ).options(joinedload_all('fixed_ip.floating_ips') + ).filter_by(deleted=_deleted(context) + ).filter_by(user_id=user_id + ).all() def instance_get_by_project(context, project_id): session = get_session() diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 0ef0f4256..674dab0b6 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -32,12 +32,14 @@ FLAGS = flags.FLAGS def return_server(context, id): return stub_instance(id) -def return_servers(context): - return [stub_instance(i) for i in xrange(5)] +def return_servers(context, user_id=1): + return [stub_instance(i, user_id) for i in xrange(5)] -def stub_instance(id): + +def stub_instance(id, user_id=1): return Instance( - id=id, state=0, image_id=10, server_name='server%s'%id + id=id, state=0, image_id=10, server_name='server%s'%id, + user_id=user_id ) class ServersTest(unittest.TestCase): @@ -50,6 +52,8 @@ class ServersTest(unittest.TestCase): test_helper.stub_out_auth(self.stubs) self.stubs.Set(nova.db.api, 'instance_get_all', return_servers) self.stubs.Set(nova.db.api, 'instance_get', return_server) + self.stubs.Set(nova.db.api, 'instance_get_all_by_user', + return_servers) def tearDown(self): self.stubs.UnsetAll() -- cgit From afc782e0e80a71ac8d1eb2f1d70e67375ba62aca Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Tue, 28 Sep 2010 10:59:55 +0200 Subject: Make sure we also start dnsmasq on startup if we're managing networks. --- nova/network/manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/network/manager.py b/nova/network/manager.py index 2530f04b7..20d4fe0f7 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -358,6 +358,7 @@ class VlanManager(NetworkManager): self.driver.ensure_vlan_bridge(network_ref['vlan'], network_ref['bridge'], network_ref) + self.driver.update_dhcp(context, network_id) @property def _bottom_reserved_ips(self): -- cgit From 687a90d6a7ad947c4a5851b1766a19209bb5e46f Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Tue, 28 Sep 2010 11:09:40 +0200 Subject: Call out to 'sudo kill' instead of using os.kill. dnsmasq runs as root or nobody, nova may or may not be running as root, so os.kill won't work. --- nova/network/linux_net.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index bfa73dca0..50d2831c3 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -172,7 +172,7 @@ def update_dhcp(context, network_id): # TODO(ja): use "/proc/%d/cmdline" % (pid) to determine if pid refers # correct dnsmasq process try: - os.kill(pid, signal.SIGHUP) + _execute('sudo kill -HUP %d' % pid) return except Exception as exc: # pylint: disable-msg=W0703 logging.debug("Hupping dnsmasq threw %s", exc) @@ -240,7 +240,7 @@ def _stop_dnsmasq(network): if pid: try: - os.kill(pid, signal.SIGTERM) + _execute('sudo kill -TERM %d' % pid) except Exception as exc: # pylint: disable-msg=W0703 logging.debug("Killing dnsmasq threw %s", exc) -- cgit From fd41a784ccee500ae8a36311ad3c80963e866b31 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 28 Sep 2010 12:54:17 -0400 Subject: Add Serializer.deserialize(xml_or_json_string) --- nova/tests/api/wsgi_test.py | 24 +++++++++++++++++++++ nova/wsgi.py | 51 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/nova/tests/api/wsgi_test.py b/nova/tests/api/wsgi_test.py index 145b1bfee..9425b01d0 100644 --- a/nova/tests/api/wsgi_test.py +++ b/nova/tests/api/wsgi_test.py @@ -121,3 +121,27 @@ class SerializerTest(unittest.TestCase): def test_suffix_takes_precedence_over_accept_header(self): self.match('/servers/4.xml', 'application/json', expect='xml') self.match('/servers/4.xml.', 'application/json', expect='json') + + def test_deserialize(self): + xml = """ + + 123 + 1 + 1 + + """.strip() + as_dict = dict(a={ + 'a1': '1', + 'a2': '2', + 'bs': ['1', '2', '3', {'c': dict(c1='1')}], + 'd': {'e': '1'}, + 'f': '1'}) + metadata = {'application/xml': dict(plurals={'bs': 'b', 'ts': 't'})} + serializer = wsgi.Serializer({}, metadata) + self.assertEqual(serializer.deserialize(xml), as_dict) + + def test_deserialize_empty_xml(self): + xml = """""" + as_dict = {"a": {}} + serializer = wsgi.Serializer({}) + self.assertEqual(serializer.deserialize(xml), as_dict) diff --git a/nova/wsgi.py b/nova/wsgi.py index ac319db40..da9374542 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -21,8 +21,10 @@ Utility methods for working with WSGI servers """ +import json import logging import sys +from xml.dom import minidom import eventlet import eventlet.wsgi @@ -231,7 +233,7 @@ class Controller(object): class Serializer(object): """ - Serializes a dictionary to a Content Type specified by a WSGI environment. + Serializes and deserializes dictionaries to certain MIME types. """ def __init__(self, environ, metadata=None): @@ -256,21 +258,58 @@ class Serializer(object): def to_content_type(self, data): """ - Serialize a dictionary into a string. The format of the string - will be decided based on the Content Type requested in self.environ: - by Accept: header, or by URL suffix. + Serialize a dictionary into a string. + + The format of the string will be decided based on the Content Type + requested in self.environ: by Accept: header, or by URL suffix. """ return self.handler(data) + def deserialize(self, datastring): + """ + Deserialize a string to a dictionary. + + The string must be in the format of a supported MIME type. + """ + datastring = datastring.strip() + is_xml = (datastring[0] == '<') + if not is_xml: + return json.loads(datastring) + return self._from_xml(datastring) + + def _from_xml(self, datastring): + xmldata = self.metadata.get('application/xml', {}) + plurals = set(xmldata.get('plurals', {})) + node = minidom.parseString(datastring).childNodes[0] + return {node.nodeName: self._from_xml_node(node, plurals)} + + def _from_xml_node(self, node, listnames): + """ + Convert a minidom node to a simple Python type. + + listnames is a collection of names of XML nodes whose subnodes should + be considered list items. + """ + if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3: + return node.childNodes[0].nodeValue + elif node.nodeName in listnames: + return [self._from_xml_node(n, listnames) for n in node.childNodes] + else: + result = dict() + for attr in node.attributes.keys(): + result[attr] = node.attributes[attr].nodeValue + for child in node.childNodes: + if child.nodeType != node.TEXT_NODE: + result[child.nodeName] = self._from_xml_node(child, listnames) + return result + def _to_json(self, data): - import json return json.dumps(data) def _to_xml(self, data): metadata = self.metadata.get('application/xml', {}) # We expect data to contain a single key which is the XML root. root_key = data.keys()[0] - from xml.dom import minidom doc = minidom.Document() node = self._to_xml_node(doc, metadata, root_key, data[root_key]) return node.toprettyxml(indent=' ') -- cgit From 663ed27a2d7b3cb3a5290e0516eb8d602d7e65ba Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 28 Sep 2010 12:56:01 -0500 Subject: Testing testing testing --- nova/api/rackspace/_id_translator.py | 2 +- nova/api/rackspace/servers.py | 22 +++++++++++++++----- nova/tests/api/rackspace/servers.py | 37 +++++++++++++++++++++++++++------ nova/tests/api/rackspace/test_helper.py | 22 ++++++++++++++++++-- 4 files changed, 69 insertions(+), 14 deletions(-) diff --git a/nova/api/rackspace/_id_translator.py b/nova/api/rackspace/_id_translator.py index aec5fb6a5..333aa8434 100644 --- a/nova/api/rackspace/_id_translator.py +++ b/nova/api/rackspace/_id_translator.py @@ -37,6 +37,6 @@ class RackspaceAPIIdTranslator(object): # every int id be used.) return int(self._store.hget(self._fwd_key, str(opaque_id))) - def from_rs_id(self, strategy_name, rs_id): + def from_rs_id(self, rs_id): """Convert a Rackspace id to a strategy-specific one.""" return self._store.hget(self._rev_key, rs_id) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index becac9140..dec369ef0 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -85,7 +85,11 @@ class Controller(wsgi.Controller): return exc.HTTPNotFound() def create(self, req): + if not req.environ.has_key('inst_dict'): + return exc.HTTPUnprocessableEntity() + inst = self._build_server_instance(req) + rpc.cast( FLAGS.compute_topic, { "method": "run_instance", @@ -93,6 +97,9 @@ class Controller(wsgi.Controller): return _entity_inst(inst) def update(self, req, id): + if not req.environ.has_key('inst_dict'): + return exc.HTTPUnprocessableEntity() + instance = self.db.instance_get(None, id) if not instance: return exc.HTTPNotFound() @@ -105,25 +112,30 @@ class Controller(wsgi.Controller): def action(self, req, id): """ multi-purpose method used to reboot, rebuild, and resize a server """ - return {} + if not req.environ.has_key('inst_dict'): + return exc.HTTPUnprocessableEntity() - def _id_translator(self): + def translator_instance(self): service = nova.image.service.ImageService.load() return _id_translator.RackspaceAPIIdTranslator( - "image", self.service.__class__.__name__) + "image", service.__class__.__name__) def _build_server_instance(self, req): """Build instance data structure and save it to the data store.""" ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) inst = {} + env = req.environ['inst_dict'] + image_id = env['server']['imageId'] - opaque_id = self._id_translator.from_rs_id(image_id) + opaque_id = self.translator_instance().from_rs_id(image_id) inst['name'] = env['server']['name'] inst['image_id'] = opaque_id inst['instance_type'] = env['server']['flavorId'] - inst['user_id'] = env['user']['id'] + + user_id = req.environ['nova.context']['user']['id'] + inst['user_id'] = user_id inst['launch_time'] = ltime inst['mac_address'] = utils.generate_mac() diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 674dab0b6..a22736dd2 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -80,9 +80,19 @@ class ServersTest(unittest.TestCase): self.assertEqual(s.get('imageId', None), None) i += 1 - def test_create_instance(self): - pass - + #def test_create_instance(self): + # test_helper.stub_out_image_translator(self.stubs) + # body = dict(server=dict( + # name='server_test', imageId=2, flavorId=2, metadata={}, + # personality = {} + # )) + # req = webob.Request.blank('/v1.0/servers') + # req.method = 'POST' + # req.body = json.dumps(body) + + # res = req.get_response(nova.api.API()) + + # print res def test_update_server_password(self): pass @@ -119,22 +129,37 @@ class ServersTest(unittest.TestCase): i += 1 def test_server_reboot(self): + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) req = webob.Request.blank('/v1.0/servers/1/action') req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) res = req.get_response(nova.api.API()) - res_dict = json.loads(res.body) def test_server_rebuild(self): + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) req = webob.Request.blank('/v1.0/servers/1/action') req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) res = req.get_response(nova.api.API()) - res_dict = json.loads(res.body) def test_server_resize(self): + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) req = webob.Request.blank('/v1.0/servers/1/action') req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) res = req.get_response(nova.api.API()) - res_dict = json.loads(res.body) def test_delete_server_instance(self): req = webob.Request.blank('/v1.0/servers/1') diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index d44b8c30e..09ff26ac7 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -1,10 +1,12 @@ -from nova import utils +import json import webob import webob.dec import datetime +import nova.api.rackspace.auth +import nova.api.rackspace._id_translator from nova.wsgi import Router from nova import auth -import nova.api.rackspace.auth +from nova import utils from nova import flags FLAGS = flags.FLAGS @@ -32,8 +34,24 @@ def fake_auth_init(self): @webob.dec.wsgify def fake_wsgi(self, req): req.environ['nova.context'] = dict(user=dict(id=1)) + if req.body: + req.environ['inst_dict'] = json.loads(req.body) return self.application +def stub_out_image_translator(stubs): + class FakeTranslator(object): + def __init__(self, id_type, service_name): + pass + + def to_rs_id(self, id): + return id + + def from_rs_id(self, id): + return id + + stubs.Set(nova.api.rackspace._id_translator, + 'RackspaceAPIIdTranslator', FakeTranslator) + def stub_out_auth(stubs): def fake_auth_init(self, app): self.application = app -- cgit From 4b26e60e3c84d4535fbd4ba7a3b2bf29f2121072 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 28 Sep 2010 14:09:53 -0400 Subject: Instance & Image renaming fixes. --- nova/endpoint/cloud.py | 24 ++++++++++-------------- nova/endpoint/images.py | 6 ++++-- nova/objectstore/handler.py | 18 +++++++++--------- nova/objectstore/image.py | 14 ++++++-------- nova/tests/cloud_unittest.py | 2 +- nova/tests/objectstore_unittest.py | 6 +++--- 6 files changed, 33 insertions(+), 37 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 2c174bb81..9c96ced9c 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -310,10 +310,11 @@ class CloudController(object): 'volume_id': volume['str_id']}] else: v['attachmentSet'] = [{}] - if 'display_name' in volume: - v['display_name'] = volume['display_name'] - if 'display_description' in volume: - v['display_description'] = volume['display_description'] + + # TODO(todd): check api version and only pass back to nova-aware + # clients + v['display_name'] = volume['display_name'] + v['display_description'] = volume['display_description'] return v @rbac.allow('projectmanager', 'sysadmin') @@ -746,7 +747,9 @@ class CloudController(object): if field in kwargs: changes[field] = kwargs[field] if changes: - db.instance_update(context, instance_id, kwargs) + db_context = {} + inst = db.instance_get_by_str(db_context, instance_id) + db.instance_update(db_context, inst['id'], kwargs) return defer.succeed(True) @rbac.allow('projectmanager', 'sysadmin') @@ -814,13 +817,6 @@ class CloudController(object): return defer.succeed(result) @rbac.allow('projectmanager', 'sysadmin') - def set_image_name(self, context, image_id, name): - result = images.update_user_editable_field(context, image_id, - 'displayName', name) - return defer.succeed(result) - - @rbac.allow('projectmanager', 'sysadmin') - def set_image_description(self, context, image_id, name): - result = images.update_user_editable_field(context, image_id, - 'displayDescription', name) + def update_image(self, context, image_id, **kwargs): + result = images.update(context, image_id, dict(kwargs)) return defer.succeed(result) diff --git a/nova/endpoint/images.py b/nova/endpoint/images.py index a63c059bb..cb54cdda2 100644 --- a/nova/endpoint/images.py +++ b/nova/endpoint/images.py @@ -43,11 +43,13 @@ def modify(context, image_id, operation): return True -def update_user_editable_field(context, image_id, field, value): +def update(context, image_id, attributes): + """update an image's attributes / info.json""" + attributes.update({"image_id": image_id}) conn(context).make_request( method='POST', bucket='_images', - query_args=qs({'image_id': image_id, 'field': field, 'value': value})) + query_args=qs(attributes)) return True def register(context, image_location): diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index f4596e982..daf633ac2 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -352,6 +352,8 @@ class ImagesResource(resource.Resource): m[u'imageType'] = m['type'] elif 'imageType' in m: m[u'type'] = m['imageType'] + if 'displayName' not in m: + m[u'displayName'] = u'' return m request.write(json.dumps([decorate(i.metadata) for i in images])) @@ -386,23 +388,21 @@ class ImagesResource(resource.Resource): image_id = get_argument(request, 'image_id', u'') image_object = image.Image(image_id) if not image_object.is_authorized(request.context): - logging.debug("not authorized for handle_POST in images") + logging.debug("not authorized for render_POST in images") raise exception.NotAuthorized operation = get_argument(request, 'operation', u'') - field = get_argument(request, 'field', u'') - value = get_argument(request, 'value', u'') if operation: # operation implies publicity toggle logging.debug("handling publicity toggle") image_object.set_public(operation=='add') - elif field: - # field implies user field editing (value can be blank) - logging.debug("update user field") - image_object.update_user_editable_field(field, value) else: - logging.debug("unknown action for handle_POST in images") - + # other attributes imply update + logging.debug("update user fields") + clean_args = {} + for arg in request.args.keys(): + clean_args[arg] = request.args[arg][0] + image_object.update_user_editable_fields(clean_args) return '' def render_DELETE(self, request): # pylint: disable-msg=R0201 diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index ad1745af6..def1b8167 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -82,15 +82,13 @@ class Image(object): with open(os.path.join(self.path, 'info.json'), 'w') as f: json.dump(md, f) - def update_user_editable_field(self, field, value): - fields = ['displayName', 'displayDescription'] - if field not in fields: - raise KeyError("Invalid field: %s" % field) + def update_user_editable_fields(self, args): + """args is from the request parameters, so requires extra cleaning""" + fields = {'display_name': 'displayName', 'description': 'description'} info = self.metadata - if value: - info[field] = value - elif field in info: - del info[field] + for field in fields.keys(): + if field in args: + info[fields[field]] = args[field] with open(os.path.join(self.path, 'info.json'), 'w') as f: json.dump(info, f) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index cea2a67ce..f7340f12d 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -219,7 +219,7 @@ class CloudTestCase(test.BaseTestCase): def test_update_of_instance_display_fields(self): inst = db.instance_create({}, {}) - self.cloud.update_instance(self.context, inst['id'], + self.cloud.update_instance(self.context, inst['str_id'], display_name='c00l 1m4g3') inst = db.instance_get({}, inst['id']) self.assertEqual('c00l 1m4g3', inst['display_name']) diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index 82c6e2c49..4216cf190 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -165,10 +165,10 @@ class ObjectStoreTestCase(test.BaseTestCase): self.assertFalse(my_img.is_authorized(self.context)) # change user-editable fields - my_img.update_user_editable_field('displayName', 'my cool image') + my_img.update_user_editable_fields({'display_name': 'my cool image'}) self.assertEqual('my cool image', my_img.metadata['displayName']) - my_img.update_user_editable_field('displayName', '') - self.assert_(not 'displayName' in my_img.metadata) + my_img.update_user_editable_fields({'display_name': ''}) + self.assert_(not my_img.metadata['displayName']) class TestHTTPChannel(http.HTTPChannel): -- cgit From 7ebf1e292b4840e0da4190d2aaf3fa8fc5439846 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Tue, 28 Sep 2010 11:49:20 -0700 Subject: Implemented random instance and volume strings for ec2 api --- nova/api/ec2/cloud.py | 30 +++++++++++++++--------------- nova/db/api.py | 12 ++++++------ nova/db/sqlalchemy/api.py | 29 ++++++++++++++++++++++++----- nova/db/sqlalchemy/models.py | 7 +++++-- nova/volume/manager.py | 10 +++++----- 5 files changed, 55 insertions(+), 33 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 367511e3b..88d53527c 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -143,7 +143,7 @@ class CloudController(object): }, 'hostname': hostname, 'instance-action': 'none', - 'instance-id': instance_ref['str_id'], + 'instance-id': instance_ref['ec2_id'], 'instance-type': instance_ref['instance_type'], 'local-hostname': hostname, 'local-ipv4': address, @@ -245,7 +245,7 @@ class CloudController(object): def get_console_output(self, context, instance_id, **kwargs): # instance_id is passed in as a list of instances - instance_ref = db.instance_get_by_str(context, instance_id[0]) + instance_ref = db.instance_get_by_ec2_id(context, instance_id[0]) return rpc.call('%s.%s' % (FLAGS.compute_topic, instance_ref['host']), {"method": "get_console_output", @@ -264,7 +264,7 @@ class CloudController(object): def _format_volume(self, context, volume): v = {} - v['volumeId'] = volume['str_id'] + v['volumeId'] = volume['ec2_id'] v['status'] = volume['status'] v['size'] = volume['size'] v['availabilityZone'] = volume['availability_zone'] @@ -282,7 +282,7 @@ class CloudController(object): 'device': volume['mountpoint'], 'instanceId': volume['instance_id'], 'status': 'attached', - 'volume_id': volume['str_id']}] + 'volume_id': volume['ec2_id']}] else: v['attachmentSet'] = [{}] return v @@ -314,13 +314,13 @@ class CloudController(object): def attach_volume(self, context, volume_id, instance_id, device, **kwargs): - volume_ref = db.volume_get_by_str(context, volume_id) + volume_ref = db.volume_get_by_ec2_id(context, volume_id) # TODO(vish): abstract status checking? if volume_ref['status'] != "available": raise exception.ApiError("Volume status must be available") if volume_ref['attach_status'] == "attached": raise exception.ApiError("Volume is already attached") - instance_ref = db.instance_get_by_str(context, instance_id) + instance_ref = db.instance_get_by_ec2_id(context, instance_id) host = instance_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "attach_volume", @@ -336,7 +336,7 @@ class CloudController(object): 'volumeId': volume_ref['id']} def detach_volume(self, context, volume_id, **kwargs): - volume_ref = db.volume_get_by_str(context, volume_id) + volume_ref = db.volume_get_by_ec2_id(context, volume_id) instance_ref = db.volume_get_instance(context, volume_ref['id']) if not instance_ref: raise exception.ApiError("Volume isn't attached to anything!") @@ -356,7 +356,7 @@ class CloudController(object): db.volume_detached(context) return {'attachTime': volume_ref['attach_time'], 'device': volume_ref['mountpoint'], - 'instanceId': instance_ref['str_id'], + 'instanceId': instance_ref['ec2_id'], 'requestId': context.request_id, 'status': volume_ref['attach_status'], 'volumeId': volume_ref['id']} @@ -395,7 +395,7 @@ class CloudController(object): if instance['image_id'] == FLAGS.vpn_image_id: continue i = {} - i['instanceId'] = instance['str_id'] + i['instanceId'] = instance['ec2_id'] i['imageId'] = instance['image_id'] i['instanceState'] = { 'code': instance['state'], @@ -446,7 +446,7 @@ class CloudController(object): instance_id = None if (floating_ip_ref['fixed_ip'] and floating_ip_ref['fixed_ip']['instance']): - instance_id = floating_ip_ref['fixed_ip']['instance']['str_id'] + instance_id = floating_ip_ref['fixed_ip']['instance']['ec2_id'] address_rv = {'public_ip': address, 'instance_id': instance_id} if context.user.is_admin(): @@ -481,7 +481,7 @@ class CloudController(object): return {'releaseResponse': ["Address released."]} def associate_address(self, context, instance_id, public_ip, **kwargs): - instance_ref = db.instance_get_by_str(context, instance_id) + instance_ref = db.instance_get_by_ec2_id(context, instance_id) fixed_ip_ref = db.fixed_ip_get_by_instance(context, instance_ref['id']) floating_ip_ref = db.floating_ip_get_by_address(context, public_ip) network_topic = self._get_network_topic(context) @@ -589,7 +589,7 @@ class CloudController(object): inst = {} inst['mac_address'] = utils.generate_mac() inst['launch_index'] = num - inst['hostname'] = instance_ref['str_id'] + inst['hostname'] = instance_ref['ec2_id'] db.instance_update(context, inst_id, inst) address = self.network_manager.allocate_fixed_ip(context, inst_id, @@ -618,7 +618,7 @@ class CloudController(object): for id_str in instance_id: logging.debug("Going to try and terminate %s" % id_str) try: - instance_ref = db.instance_get_by_str(context, id_str) + instance_ref = db.instance_get_by_ec2_id(context, id_str) except exception.NotFound: logging.warning("Instance %s was not found during terminate" % id_str) @@ -664,7 +664,7 @@ class CloudController(object): def reboot_instances(self, context, instance_id, **kwargs): """instance_id is a list of instance ids""" for id_str in instance_id: - instance_ref = db.instance_get_by_str(context, id_str) + instance_ref = db.instance_get_by_ec2_id(context, id_str) host = instance_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "reboot_instance", @@ -674,7 +674,7 @@ class CloudController(object): def delete_volume(self, context, volume_id, **kwargs): # TODO: return error if not authorized - volume_ref = db.volume_get_by_str(context, volume_id) + volume_ref = db.volume_get_by_ec2_id(context, volume_id) if volume_ref['status'] != "available": raise exception.ApiError("Volume status must be available") now = datetime.datetime.utcnow() diff --git a/nova/db/api.py b/nova/db/api.py index c1cb1953a..d642a5942 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -272,9 +272,9 @@ def instance_get_floating_address(context, instance_id): return IMPL.instance_get_floating_address(context, instance_id) -def instance_get_by_str(context, str_id): - """Get an instance by string id.""" - return IMPL.instance_get_by_str(context, str_id) +def instance_get_by_ec2_id(context, ec2_id): + """Get an instance by ec2 id.""" + return IMPL.instance_get_by_ec2_id(context, ec2_id) def instance_is_vpn(context, instance_id): @@ -537,9 +537,9 @@ def volume_get_by_project(context, project_id): return IMPL.volume_get_by_project(context, project_id) -def volume_get_by_str(context, str_id): - """Get a volume by string id.""" - return IMPL.volume_get_by_str(context, str_id) +def volume_get_by_ec2_id(context, ec2_id): + """Get a volume by ec2 id.""" + return IMPL.volume_get_by_ec2_id(context, ec2_id) def volume_get_shelf_and_blade(context, volume_id): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 2b0dd6ea6..f9999caa3 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -22,6 +22,7 @@ Implementation of SQLAlchemy backend from nova import db from nova import exception from nova import flags +from nova import utils from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ @@ -356,6 +357,7 @@ def instance_create(_context, values): instance_ref = models.Instance() for (key, value) in values.iteritems(): instance_ref[key] = value + instance_ref.ec2_id = utils.generate_uid(instance_ref.__prefix__) instance_ref.save() return instance_ref @@ -408,8 +410,16 @@ def instance_get_by_reservation(_context, reservation_id): ).all() -def instance_get_by_str(context, str_id): - return models.Instance.find_by_str(str_id, deleted=_deleted(context)) +def instance_get_by_ec2_id(context, ec2_id): + session = get_session() + instance_ref = session.query(models.Instance + ).filter_by(ec2_id=ec2_id + ).filter_by(deleted=_deleted(context) + ).first() + if not instance_ref: + raise exception.NotFound('Instance %s not found' % (ec2_id)) + + return instance_ref def instance_get_fixed_address(_context, instance_id): @@ -699,7 +709,7 @@ def auth_create_token(_context, token): tk[k] = v tk.save() return tk - + ################### @@ -768,6 +778,7 @@ def volume_create(_context, values): volume_ref = models.Volume() for (key, value) in values.iteritems(): volume_ref[key] = value + volume_ref.ec2_id = utils.generate_uid(volume_ref.__prefix__) volume_ref.save() return volume_ref @@ -821,8 +832,16 @@ def volume_get_by_project(context, project_id): ).all() -def volume_get_by_str(context, str_id): - return models.Volume.find_by_str(str_id, deleted=_deleted(context)) +def volume_get_by_ec2_id(context, ec2_id): + session = get_session() + volume_ref = session.query(models.Volume + ).filter_by(ec2_id=ec2_id + ).filter_by(deleted=_deleted(context) + ).first() + if not volume_ref: + raise exception.NotFound('Volume %s not found' % (ec2_id)) + + return volume_ref def volume_get_instance(_context, volume_id): diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index f6ba7953f..baf48d306 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -126,6 +126,7 @@ class NovaBase(object): # __tablename__ = 'images' # __prefix__ = 'ami' # id = Column(Integer, primary_key=True) +# ec2_id = Column(String(12), unique=True) # user_id = Column(String(255)) # project_id = Column(String(255)) # image_type = Column(String(255)) @@ -195,6 +196,7 @@ class Instance(BASE, NovaBase): __tablename__ = 'instances' __prefix__ = 'i' id = Column(Integer, primary_key=True) + ec2_id = Column(String(10), unique=True) user_id = Column(String(255)) project_id = Column(String(255)) @@ -265,6 +267,7 @@ class Volume(BASE, NovaBase): __tablename__ = 'volumes' __prefix__ = 'vol' id = Column(Integer, primary_key=True) + ec2_id = Column(String(12), unique=True) user_id = Column(String(255)) project_id = Column(String(255)) @@ -398,8 +401,8 @@ class NetworkIndex(BASE, NovaBase): 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 + """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), primary_key=True) diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 034763512..8508f27b2 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -77,7 +77,7 @@ class AOEManager(manager.Manager): size = volume_ref['size'] logging.debug("volume %s: creating lv of size %sG", volume_id, size) - yield self.driver.create_volume(volume_ref['str_id'], size) + yield self.driver.create_volume(volume_ref['ec2_id'], size) logging.debug("volume %s: allocating shelf & blade", volume_id) self._ensure_blades(context) @@ -87,7 +87,7 @@ class AOEManager(manager.Manager): logging.debug("volume %s: exporting shelf %s & blade %s", volume_id, shelf_id, blade_id) - yield self.driver.create_export(volume_ref['str_id'], + yield self.driver.create_export(volume_ref['ec2_id'], shelf_id, blade_id) @@ -111,10 +111,10 @@ class AOEManager(manager.Manager): raise exception.Error("Volume is not local to this node") shelf_id, blade_id = self.db.volume_get_shelf_and_blade(context, volume_id) - yield self.driver.remove_export(volume_ref['str_id'], + yield self.driver.remove_export(volume_ref['ec2_id'], shelf_id, blade_id) - yield self.driver.delete_volume(volume_ref['str_id']) + yield self.driver.delete_volume(volume_ref['ec2_id']) self.db.volume_destroy(context, volume_id) defer.returnValue(True) @@ -125,7 +125,7 @@ class AOEManager(manager.Manager): Returns path to device. """ volume_ref = self.db.volume_get(context, volume_id) - yield self.driver.discover_volume(volume_ref['str_id']) + yield self.driver.discover_volume(volume_ref['ec2_id']) shelf_id, blade_id = self.db.volume_get_shelf_and_blade(context, volume_id) defer.returnValue("/dev/etherd/e%s.%s" % (shelf_id, blade_id)) -- cgit From 08a6dc59add5d72c6d925d1ca43868557a4b3148 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 28 Sep 2010 15:54:05 -0400 Subject: Hook the AuthManger#modify_user method into nova-manage commands. --- bin/nova-manage | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bin/nova-manage b/bin/nova-manage index baa1cb4db..da18e7d1b 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -228,6 +228,19 @@ class UserCommands(object): for user in self.manager.get_users(): print user.name + def modify(self, name, access_key, secret_key, is_admin): + """update a users keys & admin flag + arguments: accesskey secretkey admin + leave any field blank to ignore it, admin should be 'T', 'F', or blank + """ + if not is_admin: + is_admin = None + elif is_admin.upper()[0] == 'T': + is_admin = True + else: + is_admin = False + print "is_admin: %r" % is_admin + self.manager.modify_user(name, access_key, secret_key, is_admin) class ProjectCommands(object): """Class for managing projects.""" -- cgit From 5ebefd0d5de7a7c753297bcde8ae842c4f92e33e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 28 Sep 2010 13:21:24 -0700 Subject: add disabled column to services and check for it in scheduler --- nova/db/sqlalchemy/api.py | 4 +++- nova/db/sqlalchemy/models.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 2b0dd6ea6..2ecf21685 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -61,6 +61,7 @@ def service_get_all_by_topic(context, topic): session = get_session() return session.query(models.Service ).filter_by(deleted=False + ).filter_by(disabled=False ).filter_by(topic=topic ).all() @@ -70,6 +71,7 @@ def _service_get_all_topic_subquery(_context, session, topic, subq, label): return session.query(models.Service, func.coalesce(sort_value, 0) ).filter_by(topic=topic ).filter_by(deleted=False + ).filter_by(disabled=False ).outerjoin((subq, models.Service.host == subq.c.host) ).order_by(sort_value ).all() @@ -699,7 +701,7 @@ def auth_create_token(_context, token): tk[k] = v tk.save() return tk - + ################### diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index f6ba7953f..021091b60 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -173,6 +173,7 @@ class Service(BASE, NovaBase): binary = Column(String(255)) topic = Column(String(255)) report_count = Column(Integer, nullable=False, default=0) + disabled = Column(Boolean, default=False) @classmethod def find_by_args(cls, host, binary, session=None, deleted=False): -- cgit From c1f7914f9d8c4f7687c67de37c5eda5a95245a0d Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 28 Sep 2010 16:41:39 -0400 Subject: Remove TODO, since apparently newer boto doesn't die on extra fields. --- nova/api/ec2/cloud.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 4defef87e..528380f0f 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -286,8 +286,6 @@ class CloudController(object): else: v['attachmentSet'] = [{}] - # TODO(todd): check api version and only pass back to nova-aware - # clients v['display_name'] = volume['display_name'] v['display_description'] = volume['display_description'] return v -- cgit From c80c0786baadf521c86ceff21288e3760aaea5bd Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 28 Sep 2010 16:47:29 -0400 Subject: Add authorization info for cloud endpoints. --- nova/api/ec2/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index f0aa57ee4..7a958f841 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -158,12 +158,14 @@ class Authorizer(wsgi.Middleware): 'RunInstances': ['projectmanager', 'sysadmin'], 'TerminateInstances': ['projectmanager', 'sysadmin'], 'RebootInstances': ['projectmanager', 'sysadmin'], + 'UpdateInstance': ['projectmanager', 'sysadmin'], 'DeleteVolume': ['projectmanager', 'sysadmin'], 'DescribeImages': ['all'], 'DeregisterImage': ['projectmanager', 'sysadmin'], 'RegisterImage': ['projectmanager', 'sysadmin'], 'DescribeImageAttribute': ['all'], 'ModifyImageAttribute': ['projectmanager', 'sysadmin'], + 'UpdateImage': ['projectmanager', 'sysadmin'], }, 'AdminController': { # All actions have the same permission: ['none'] (the default) -- cgit From 641b6ee5630ed00ee3e921769cd408a8603ff62b Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 28 Sep 2010 16:46:21 -0500 Subject: Merge prop fixes and pylint/pep8 cleanup --- nova/api/rackspace/__init__.py | 2 +- nova/api/rackspace/auth.py | 8 +- nova/api/rackspace/backup_schedules.py | 5 +- nova/api/rackspace/images.py | 5 +- nova/api/rackspace/servers.py | 141 ++++++++++++++++++-------------- nova/db/sqlalchemy/models.py | 2 +- nova/tests/api/rackspace/servers.py | 8 +- nova/tests/api/rackspace/test_helper.py | 10 ++- pylintrc | 3 +- 9 files changed, 105 insertions(+), 79 deletions(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index a10a9c6df..98802663f 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -31,11 +31,11 @@ import webob from nova import flags from nova import utils from nova import wsgi +from nova.api.rackspace import backup_schedules from nova.api.rackspace import flavors from nova.api.rackspace import images from nova.api.rackspace import ratelimiting from nova.api.rackspace import servers -from nova.api.rackspace import backup_schedules from nova.api.rackspace import sharedipgroups from nova.auth import manager diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index ce5a967eb..8bfb0753e 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -1,13 +1,15 @@ import datetime +import hashlib import json 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 +from nova import flags +from nova import manager from nova import utils FLAGS = flags.FLAGS diff --git a/nova/api/rackspace/backup_schedules.py b/nova/api/rackspace/backup_schedules.py index a18dfb87c..46da778ee 100644 --- a/nova/api/rackspace/backup_schedules.py +++ b/nova/api/rackspace/backup_schedules.py @@ -16,10 +16,11 @@ # under the License. import time -import nova.image.service +from webob import exc + from nova import wsgi from nova.api.rackspace import _id_translator -from webob import exc +import nova.image.service class Controller(wsgi.Controller): def __init__(self): diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py index 10e15de3f..11b058dec 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/rackspace/images.py @@ -15,10 +15,11 @@ # License for the specific language governing permissions and limitations # under the License. -import nova.image.service +from webob import exc + from nova import wsgi from nova.api.rackspace import _id_translator -from webob import exc +import nova.image.service class Controller(wsgi.Controller): diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index dec369ef0..4ab04bde7 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -16,23 +16,43 @@ # under the License. import time -import nova.image.service -from nova import wsgi -from nova import db + +from webob import exc + from nova import flags from nova import rpc from nova import utils -from nova import compute -from nova import flags -from nova.compute import power_state +from nova import wsgi from nova.api.rackspace import _id_translator -from webob import exc +from nova.compute import power_state +import nova.image.service FLAGS = flags.FLAGS -class Controller(wsgi.Controller): - _power_mapping = { + +def translator_instance(): + """ Helper method for initializing the image id translator """ + service = nova.image.service.ImageService.load() + return _id_translator.RackspaceAPIIdTranslator( + "image", service.__class__.__name__) + +def _filter_params(inst_dict): + """ Extracts all updatable parameters for a server update request """ + keys = ['name', 'adminPass'] + new_attrs = {} + for k in keys: + if inst_dict.has_key(k): + new_attrs[k] = inst_dict[k] + return new_attrs + +def _entity_list(entities): + """ Coerces a list of servers into proper dictionary format """ + return dict(servers=entities) + +def _entity_detail(inst): + """ Maps everything to Rackspace-like attributes for return""" + power_mapping = { power_state.NOSTATE: 'build', power_state.RUNNING: 'active', power_state.BLOCKED: 'active', @@ -41,6 +61,28 @@ class Controller(wsgi.Controller): power_state.SHUTOFF: 'active', power_state.CRASHED: 'error' } + inst_dict = {} + + mapped_keys = dict(status='state', imageId='image_id', + flavorId='instance_type', name='server_name', id='id') + + for k, v in mapped_keys.iteritems(): + inst_dict[k] = inst[v] + + inst_dict['status'] = power_mapping[inst_dict['status']] + inst_dict['addresses'] = dict(public=[], private=[]) + inst_dict['metadata'] = {} + inst_dict['hostId'] = '' + + return dict(server=inst_dict) + +def _entity_inst(inst): + """ Filters all model attributes save for id and name """ + return dict(server=dict(id=inst['id'], name=inst['server_name'])) + +class Controller(wsgi.Controller): + """ The Server API controller for the Openstack API """ + _serialization_metadata = { 'application/xml': { @@ -54,37 +96,43 @@ class Controller(wsgi.Controller): def __init__(self, db_driver=None): if not db_driver: db_driver = FLAGS.db_driver - self.db = utils.import_object(db_driver) + self.db_driver = utils.import_object(db_driver) + super(Controller, self).__init__() def index(self, req): + """ Returns a list of server names and ids for a given user """ user_id = req.environ['nova.context']['user']['id'] - instance_list = self.db.instance_get_all_by_user(None, user_id) - res = [self._entity_inst(inst)['server'] for inst in instance_list] - return self._entity_list(res) + instance_list = self.db_driver.instance_get_all_by_user(None, user_id) + res = [_entity_inst(inst)['server'] for inst in instance_list] + return _entity_list(res) def detail(self, req): + """ Returns a list of server details for a given user """ user_id = req.environ['nova.context']['user']['id'] - res = [self._entity_detail(inst)['server'] for inst in - self.db.instance_get_all_by_user(None, user_id)] - return self._entity_list(res) + res = [_entity_detail(inst)['server'] for inst in + self.db_driver.instance_get_all_by_user(None, user_id)] + return _entity_list(res) def show(self, req, id): + """ Returns server details by server id """ user_id = req.environ['nova.context']['user']['id'] - inst = self.db.instance_get(None, id) + inst = self.db_driver.instance_get(None, id) if inst: if inst.user_id == user_id: - return self._entity_detail(inst) + return _entity_detail(inst) raise exc.HTTPNotFound() def delete(self, req, id): + """ Destroys a server """ user_id = req.environ['nova.context']['user']['id'] - instance = self.db.instance_get(None, id) + instance = self.db_driver.instance_get(None, id) if instance and instance['user_id'] == user_id: - self.db.instance_destroy(None, id) + self.db_driver.instance_destroy(None, id) return exc.HTTPAccepted() return exc.HTTPNotFound() def create(self, req): + """ Creates a new server for a given user """ if not req.environ.has_key('inst_dict'): return exc.HTTPUnprocessableEntity() @@ -93,20 +141,21 @@ class Controller(wsgi.Controller): rpc.cast( FLAGS.compute_topic, { "method": "run_instance", - "args": {"instance_id": inst.id}}) + "args": {"instance_id": inst['id']}}) return _entity_inst(inst) def update(self, req, id): + """ Updates the server name or password """ if not req.environ.has_key('inst_dict'): return exc.HTTPUnprocessableEntity() - instance = self.db.instance_get(None, id) + instance = self.db_driver.instance_get(None, id) if not instance: return exc.HTTPNotFound() attrs = req.environ['nova.context'].get('model_attributes', None) if attrs: - self.db.instance_update(None, id, self._filter_params(attrs)) + self.db_driver.instance_update(None, id, _filter_params(attrs)) return exc.HTTPNoContent() def action(self, req, id): @@ -115,11 +164,6 @@ class Controller(wsgi.Controller): if not req.environ.has_key('inst_dict'): return exc.HTTPUnprocessableEntity() - def translator_instance(self): - service = nova.image.service.ImageService.load() - return _id_translator.RackspaceAPIIdTranslator( - "image", service.__class__.__name__) - def _build_server_instance(self, req): """Build instance data structure and save it to the data store.""" ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) @@ -128,9 +172,9 @@ class Controller(wsgi.Controller): env = req.environ['inst_dict'] image_id = env['server']['imageId'] - opaque_id = self.translator_instance().from_rs_id(image_id) + opaque_id = translator_instance().from_rs_id(image_id) - inst['name'] = env['server']['name'] + inst['name'] = env['server']['server_name'] inst['image_id'] = opaque_id inst['instance_type'] = env['server']['flavorId'] @@ -148,43 +192,16 @@ class Controller(wsgi.Controller): inst['user_id'], inst['project_id'], mac=inst['mac_address']) + inst['private_dns_name'] = str(address) inst['bridge_name'] = network.BridgedNetwork.get_network_for_project( inst['user_id'], inst['project_id'], 'default')['bridge_name'] - self.db.instance_create(None, inst) + ref = self.db_driver.instance_create(None, inst) + inst['id'] = ref.id + return inst - def _filter_params(self, inst_dict): - keys = ['name', 'adminPass'] - new_attrs = {} - for k in keys: - if inst_dict.has_key(k): - new_attrs[k] = inst_dict[k] - return new_attrs - - def _entity_list(self, entities): - return dict(servers=entities) - - def _entity_detail(self, inst): - """ Maps everything to Rackspace-like attributes for return""" - inst_dict = {} - - mapped_keys = dict(status='state', imageId='image_id', - flavorId='instance_type', name='name', id='id') - - for k,v in mapped_keys.iteritems(): - inst_dict[k] = inst[v] - - inst_dict['status'] = Controller._power_mapping[inst_dict['status']] - inst_dict['addresses'] = dict(public=[], private=[]) - inst_dict['metadata'] = {} - inst_dict['hostId'] = '' - - return dict(server=inst_dict) - - def _entity_inst(self, inst): - """ Filters all model attributes save for id and name """ - return dict(server=dict(id=inst['id'], name=inst['name'])) + diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 47c505f25..63f8c6172 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -209,7 +209,7 @@ class Instance(BASE, NovaBase): @property def name(self): - return self.server_name or self.str_id + return self.str_id image_id = Column(String(255)) kernel_id = Column(String(255)) diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index a22736dd2..9fd8e5e88 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -17,15 +17,17 @@ import json import unittest + import stubout -import nova.api.rackspace -import nova.db.api + +from nova import db from nova import flags +import nova.api.rackspace from nova.api.rackspace import servers +import nova.db.api from nova.db.sqlalchemy.models import Instance from nova.tests.api.test_helper import * from nova.tests.api.rackspace import test_helper -from nova import db FLAGS = flags.FLAGS diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index 09ff26ac7..aa7fb382c 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -1,13 +1,15 @@ +import datetime import json + import webob import webob.dec -import datetime -import nova.api.rackspace.auth -import nova.api.rackspace._id_translator -from nova.wsgi import Router + from nova import auth from nova import utils from nova import flags +import nova.api.rackspace.auth +import nova.api.rackspace._id_translator +from nova.wsgi import Router FLAGS = flags.FLAGS diff --git a/pylintrc b/pylintrc index 6702ca895..024802835 100644 --- a/pylintrc +++ b/pylintrc @@ -1,7 +1,8 @@ [Messages Control] # W0511: TODOs in code comments are fine. # W0142: *args and **kwargs are fine. -disable-msg=W0511,W0142 +# W0622: Redefining id is fine. +disable-msg=W0511,W0142,W0622 [Basic] # Variable names can be 1 to 31 characters long, with lowercase and underscores -- cgit From c4df3d63c83c073664a9ed0abaefe2adbe4cd061 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 28 Sep 2010 14:59:32 -0700 Subject: fix test for editable image --- nova/tests/cloud_unittest.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 8db4aeb77..cce4a5568 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -202,18 +202,37 @@ class CloudTestCase(test.BaseTestCase): # data = self.cloud.get_metadata(instance(i)['private_dns_name']) # self.assert_(data['meta-data']['ami-id'] == 'ami-%s' % i) + @staticmethod + def _fake_set_image_description(ctxt, image_id, description): + from nova.objectstore import handler + class req: + pass + request = req() + request.context = ctxt + request.args = {'image_id': [image_id], + 'description': [description]} + + resource = handler.ImagesResource() + resource.render_POST(request) + def test_user_editable_image_endpoint(self): pathdir = os.path.join(FLAGS.images_path, 'ami-testing') os.mkdir(pathdir) - info = {} + info = {'isPublic': False} with open(os.path.join(pathdir, 'info.json'), 'w') as f: json.dump(info, f) - yield self.cloud.set_image_description(self.context, 'ami-testing', - 'Foo Img') img = image.Image('ami-testing') - self.assertEqual('Foo Img', img.metadata['displayDescription']) - self.cloud.set_image_description(self.context, 'ami-testing', '') - self.assert_(not 'displayDescription' in img.metadata) + # self.cloud.set_image_description(self.context, 'ami-testing', + # 'Foo Img') + # NOTE(vish): Above won't work unless we start objectstore or create + # a fake version of api/ec2/images.py conn that can + # call methods directly instead of going through boto. + # for now, just cheat and call the method directly + self._fake_set_image_description(self.context, 'ami-testing', + 'Foo Img') + self.assertEqual('Foo Img', img.metadata['description']) + self._fake_set_image_description(self.context, 'ami-testing', '') + self.assertEqual('', img.metadata['description']) def test_update_of_instance_display_fields(self): inst = db.instance_create({}, {}) -- cgit From 60cd2a9e292eb5a8a0bc45605d79d0a511525342 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Tue, 28 Sep 2010 15:15:05 -0700 Subject: Added checks for uniqueness for ec2 id --- nova/db/sqlalchemy/api.py | 35 +++++++++++++++++++++++++++++------ nova/db/sqlalchemy/models.py | 4 ---- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index f9999caa3..7229e5b2c 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -27,7 +27,7 @@ from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ from sqlalchemy.orm import joinedload_all -from sqlalchemy.sql import func +from sqlalchemy.sql import exists, func FLAGS = flags.FLAGS @@ -357,10 +357,15 @@ def instance_create(_context, values): instance_ref = models.Instance() for (key, value) in values.iteritems(): instance_ref[key] = value - instance_ref.ec2_id = utils.generate_uid(instance_ref.__prefix__) - instance_ref.save() - return instance_ref + session = get_session() + with session.begin(): + while instance_ref.ec2_id == None: + ec2_id = utils.generate_uid(instance_ref.__prefix__) + if not instance_ec2_id_exists(_context, ec2_id, session=session): + instance_ref.ec2_id = ec2_id + instance_ref.save(session=session) + return instance_ref def instance_data_get_for_project(_context, project_id): session = get_session() @@ -422,6 +427,12 @@ def instance_get_by_ec2_id(context, ec2_id): return instance_ref +def instance_ec2_id_exists(context, ec2_id, session=None): + if not session: + session = get_session() + return session.query(exists().where(models.Instance.id==ec2_id)).one()[0] + + def instance_get_fixed_address(_context, instance_id): session = get_session() with session.begin(): @@ -778,8 +789,14 @@ def volume_create(_context, values): volume_ref = models.Volume() for (key, value) in values.iteritems(): volume_ref[key] = value - volume_ref.ec2_id = utils.generate_uid(volume_ref.__prefix__) - volume_ref.save() + + session = get_session() + with session.begin(): + while volume_ref.ec2_id == None: + ec2_id = utils.generate_uid(volume_ref.__prefix__) + if not volume_ec2_id_exists(_context, ec2_id, session=session): + volume_ref.ec2_id = ec2_id + volume_ref.save(session=session) return volume_ref @@ -844,6 +861,12 @@ def volume_get_by_ec2_id(context, ec2_id): return volume_ref +def volume_ec2_id_exists(context, ec2_id, session=None): + if not session: + session = get_session() + return session.query(exists().where(models.Volume.id==ec2_id)).one()[0] + + def volume_get_instance(_context, volume_id): session = get_session() with session.begin(): diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index baf48d306..b898d8037 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -457,10 +457,6 @@ class FloatingIp(BASE, NovaBase): project_id = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) - @property - def str_id(self): - return self.address - @classmethod def find_by_str(cls, str_id, session=None, deleted=False): if not session: -- cgit From 116f0fc0b18c6b04a86de5123d2985205d954093 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Tue, 28 Sep 2010 16:10:47 -0700 Subject: Fixed name property on instance model --- nova/api/ec2/cloud.py | 16 ++++++++-------- nova/db/sqlalchemy/models.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 88d53527c..05e8065f3 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -103,7 +103,7 @@ class CloudController(object): result = {} for instance in db.instance_get_by_project(None, project_id): if instance['fixed_ip']: - line = '%s slots=%d' % (instance['fixed_ip']['str_id'], + line = '%s slots=%d' % (instance['fixed_ip']['address'], INSTANCE_TYPES[instance['instance_type']]['vcpus']) key = str(instance['key_name']) if key in result: @@ -404,10 +404,10 @@ class CloudController(object): fixed_addr = None floating_addr = None if instance['fixed_ip']: - fixed_addr = instance['fixed_ip']['str_id'] + fixed_addr = instance['fixed_ip']['address'] if instance['fixed_ip']['floating_ips']: fixed = instance['fixed_ip'] - floating_addr = fixed['floating_ips'][0]['str_id'] + floating_addr = fixed['floating_ips'][0]['address'] i['privateDnsName'] = fixed_addr i['publicDnsName'] = floating_addr i['dnsName'] = i['publicDnsName'] or i['privateDnsName'] @@ -442,7 +442,7 @@ class CloudController(object): iterator = db.floating_ip_get_by_project(context, context.project.id) for floating_ip_ref in iterator: - address = floating_ip_ref['str_id'] + address = floating_ip_ref['address'] instance_id = None if (floating_ip_ref['fixed_ip'] and floating_ip_ref['fixed_ip']['instance']): @@ -477,7 +477,7 @@ class CloudController(object): rpc.cast(network_topic, {"method": "deallocate_floating_ip", "args": {"context": None, - "floating_address": floating_ip_ref['str_id']}}) + "floating_address": floating_ip_ref['address']}}) return {'releaseResponse': ["Address released."]} def associate_address(self, context, instance_id, public_ip, **kwargs): @@ -488,8 +488,8 @@ class CloudController(object): rpc.cast(network_topic, {"method": "associate_floating_ip", "args": {"context": None, - "floating_address": floating_ip_ref['str_id'], - "fixed_address": fixed_ip_ref['str_id']}}) + "floating_address": floating_ip_ref['address'], + "fixed_address": fixed_ip_ref['address']}}) return {'associateResponse': ["Address associated."]} def disassociate_address(self, context, public_ip, **kwargs): @@ -498,7 +498,7 @@ class CloudController(object): rpc.cast(network_topic, {"method": "disassociate_floating_ip", "args": {"context": None, - "floating_address": floating_ip_ref['str_id']}}) + "floating_address": floating_ip_ref['address']}}) return {'disassociateResponse': ["Address disassociated."]} def _get_network_topic(self, context): diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index b898d8037..7549a7082 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -211,7 +211,7 @@ class Instance(BASE, NovaBase): @property def name(self): - return self.str_id + return self.ec2_id image_id = Column(String(255)) kernel_id = Column(String(255)) -- cgit From bbc00d9eca3b874e240e50bfa9f397afc36d0bee Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 28 Sep 2010 16:34:21 -0700 Subject: removed extra code that slipped in from a test branch --- nova/tests/rpc_unittest.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/nova/tests/rpc_unittest.py b/nova/tests/rpc_unittest.py index f4d7b4b28..e12a28fbc 100644 --- a/nova/tests/rpc_unittest.py +++ b/nova/tests/rpc_unittest.py @@ -67,17 +67,6 @@ class RpcTestCase(test.BaseTestCase): except rpc.RemoteError as exc: self.assertEqual(int(exc.value), value) - def test_pass_object(self): - """Test that we can pass objects through rpc""" - class x(): - pass - obj = x() - x.foo = 'bar' - x.baz = 100 - - result = yield rpc.call('test', {"method": "echo", - "args": {"value": obj}}) - print result class TestReceiver(object): """Simple Proxy class so the consumer has methods to call -- cgit From 1e4bca12e7e06698d3a13d6a208be90647f27555 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 28 Sep 2010 17:15:59 -0700 Subject: typo s/boo/bool --- nova/network/linux_net.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 01a1d2ad0..6f1d594fa 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -41,8 +41,8 @@ flags.DEFINE_string('bridge_dev', 'eth0', 'network device for bridges') flags.DEFINE_string('routing_source_ip', utils.get_my_ip(), 'Public IP of network host') -flags.DEFINE_boo('use_nova_chains', False, - 'use the nova_ routing chains instead of default') +flags.DEFINE_bool('use_nova_chains', False, + 'use the nova_ routing chains instead of default') DEFAULT_PORTS = [("tcp", 80), ("tcp", 22), ("udp", 1194), ("tcp", 443)] -- cgit From 533f72379931aa7bf67a0e7d1d7664ca151afda0 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 28 Sep 2010 17:24:25 -0700 Subject: fix flag defaults --- nova/flags.py | 2 +- nova/network/linux_net.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/flags.py b/nova/flags.py index 5ed0f92ee..92f6766cf 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -190,7 +190,7 @@ DEFINE_string('rabbit_virtual_host', '/', 'rabbit virtual host') DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to') DEFINE_string('cc_host', '127.0.0.1', 'ip of api server') DEFINE_integer('cc_port', 8773, 'cloud controller port') -DEFINE_string('ec2_url', 'http://%s:%s/services/Cloud' % (FLAGS.cc_host, FLAGS.cc_port), +DEFINE_string('ec2_url', 'http://127.0.0.1:8773/services/Cloud' 'Url to ec2 api server') DEFINE_string('default_image', 'ami-11111', diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 6f1d594fa..fa77c5ba8 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -39,7 +39,7 @@ flags.DEFINE_string('public_interface', 'vlan1', 'Interface for public IP addresses') flags.DEFINE_string('bridge_dev', 'eth0', 'network device for bridges') -flags.DEFINE_string('routing_source_ip', utils.get_my_ip(), +flags.DEFINE_string('routing_source_ip', '127.0.0.1', 'Public IP of network host') flags.DEFINE_bool('use_nova_chains', False, 'use the nova_ routing chains instead of default') -- cgit From 05e3e188e03624884ed019fe9cd8f216c9262f98 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 28 Sep 2010 20:36:50 -0400 Subject: Fault support --- nova/api/rackspace/__init__.py | 9 +++-- nova/api/rackspace/auth.py | 7 ++-- nova/api/rackspace/faults.py | 61 ++++++++++++++++++++++++++++++++++ nova/api/rackspace/flavors.py | 3 +- nova/api/rackspace/images.py | 7 ++-- nova/tests/api/rackspace/testfaults.py | 30 +++++++++++++++++ 6 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 nova/api/rackspace/faults.py create mode 100644 nova/tests/api/rackspace/testfaults.py diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index c24d08585..447037020 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -31,6 +31,7 @@ import webob from nova import flags from nova import utils from nova import wsgi +from nova.api.rackspace import faults from nova.api.rackspace import flavors from nova.api.rackspace import images from nova.api.rackspace import ratelimiting @@ -66,7 +67,7 @@ class AuthMiddleware(wsgi.Middleware): user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) if not user: - return webob.exc.HTTPUnauthorized() + return faults.Fault(webob.exc.HTTPUnauthorized()) context = {'user': user} req.environ['nova.context'] = context return self.application @@ -109,8 +110,10 @@ class RateLimitingMiddleware(wsgi.Middleware): delay = self.get_delay(action_name, username) if delay: # TODO(gundlach): Get the retry-after format correct. - raise webob.exc.HTTPRequestEntityTooLarge(headers={ - 'Retry-After': time.time() + delay}) + exc = webob.exc.HTTPRequestEntityTooLarge( + explanation='Too many requests.', + headers={'Retry-After': time.time() + delay}) + raise faults.Fault(exc) return self.application def get_delay(self, action_name, username): diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index ce5a967eb..519263367 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -9,6 +9,7 @@ from nova import auth from nova import manager from nova import db from nova import utils +from nova.api.rackspace import faults FLAGS = flags.FLAGS @@ -34,13 +35,13 @@ class BasicApiAuthManager(object): # honor it path_info = req.path_info if len(path_info) > 1: - return webob.exc.HTTPUnauthorized() + return faults.Fault(webob.exc.HTTPUnauthorized()) try: username, key = req.headers['X-Auth-User'], \ req.headers['X-Auth-Key'] except KeyError: - return webob.exc.HTTPUnauthorized() + return faults.Fault(webob.exc.HTTPUnauthorized()) username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] token, user = self._authorize_user(username, key) @@ -55,7 +56,7 @@ class BasicApiAuthManager(object): res.status = '204' return res else: - return webob.exc.HTTPUnauthorized() + return faults.Fault(webob.exc.HTTPUnauthorized()) def authorize_token(self, token_hash): """ retrieves user information from the datastore given a token diff --git a/nova/api/rackspace/faults.py b/nova/api/rackspace/faults.py new file mode 100644 index 000000000..fd6bc3623 --- /dev/null +++ b/nova/api/rackspace/faults.py @@ -0,0 +1,61 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import webob.dec + +from nova import wsgi + + +class Fault(wsgi.Application): + + """An RS API fault response.""" + + _fault_names = { + 400: "badRequest", + 401: "unauthorized", + 403: "resizeNotAllowed", + 404: "itemNotFound", + 405: "badMethod", + 409: "inProgress", + 413: "overLimit", + 415: "badMediaType", + 501: "notImplemented", + 503: "serviceUnavailable"} + + def __init__(self, exception): + """Create a Fault for the given webob.exc.exception.""" + self.exception = exception + + @webob.dec.wsgify + def __call__(self, req): + """Generate a WSGI response based on self.exception.""" + # Replace the body with fault details. + code = self.exception.status_int + fault_name = self._fault_names.get(code, "cloudServersFault") + fault_data = { + fault_name: { + 'code': code, + 'message': self.exception.explanation}} + if code == 413: + retry = self.exception.headers['Retry-After'] + fault_data[fault_name]['retryAfter'] = retry + # 'code' is an attribute on the fault tag itself + metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} + serializer = wsgi.Serializer(req.environ, metadata) + self.exception.body = serializer.to_content_type(fault_data) + return self.exception diff --git a/nova/api/rackspace/flavors.py b/nova/api/rackspace/flavors.py index 60b35c939..6cc57be33 100644 --- a/nova/api/rackspace/flavors.py +++ b/nova/api/rackspace/flavors.py @@ -16,6 +16,7 @@ # under the License. from nova.api.rackspace import base +from nova.api.rackspace import faults from nova.compute import instance_types from webob import exc @@ -47,7 +48,7 @@ class Controller(base.Controller): item = dict(ram=val['memory_mb'], disk=val['local_gb'], id=val['flavorid'], name=name) return dict(flavor=item) - raise exc.HTTPNotFound() + raise faults.Fault(exc.HTTPNotFound()) def _all_ids(self): """Return the list of all flavorids.""" diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py index 2f3e928b9..1c50d0bec 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/rackspace/images.py @@ -18,6 +18,7 @@ import nova.image.service from nova.api.rackspace import base from nova.api.rackspace import _id_translator +from nova.api.rackspace import faults from webob import exc class Controller(base.Controller): @@ -57,14 +58,14 @@ class Controller(base.Controller): def delete(self, req, id): # Only public images are supported for now. - raise exc.HTTPNotFound() + raise faults.Fault(exc.HTTPNotFound()) def create(self, req): # Only public images are supported for now, so a request to # make a backup of a server cannot be supproted. - raise exc.HTTPNotFound() + raise faults.Fault(exc.HTTPNotFound()) def update(self, req, id): # Users may not modify public images, and that's all that # we support for now. - raise exc.HTTPNotFound() + raise faults.Fault(exc.HTTPNotFound()) diff --git a/nova/tests/api/rackspace/testfaults.py b/nova/tests/api/rackspace/testfaults.py new file mode 100644 index 000000000..74caffd30 --- /dev/null +++ b/nova/tests/api/rackspace/testfaults.py @@ -0,0 +1,30 @@ +import unittest +import webob +import webob.exc + +from nova.api.rackspace import faults + +class TestFaults(unittest.TestCase): + + def test_fault_parts(self): + req = webob.Request.blank('/.xml') + f = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + resp = req.get_response(f) + + first_two_words = resp.body.strip().split()[:2] + self.assertEqual(first_two_words, ['']) + body_without_spaces = ''.join(resp.body.split()) + self.assertTrue('scram' in body_without_spaces) + + def test_retry_header(self): + req = webob.Request.blank('/.xml') + exc = webob.exc.HTTPRequestEntityTooLarge(explanation='sorry', + headers={'Retry-After': 4}) + f = faults.Fault(exc) + resp = req.get_response(f) + first_two_words = resp.body.strip().split()[:2] + self.assertEqual(first_two_words, ['']) + body_sans_spaces = ''.join(resp.body.split()) + self.assertTrue('sorry' in body_sans_spaces) + self.assertTrue('4' in body_sans_spaces) + self.assertEqual(resp.headers['Retry-After'], 4) -- cgit From d1c454ba4331794931e94cc2864f4e1a6ef5bf22 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 28 Sep 2010 17:41:57 -0700 Subject: improved commenting --- tools/setup_iptables.sh | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tools/setup_iptables.sh b/tools/setup_iptables.sh index b6b8414e3..673353eb4 100755 --- a/tools/setup_iptables.sh +++ b/tools/setup_iptables.sh @@ -17,6 +17,13 @@ # License for the specific language governing permissions and limitations # under the License. +# NOTE(vish): This script sets up some reasonable defaults for iptables and +# creates nova-specific chains. If you use this script you should +# run nova-network and nova-compute with --use_nova_chains=True + +# NOTE(vish): If you run nova-api on a different port, make sure to change +# the port here +API_PORT=${API_PORT:-"8773"} if [ -n "$1" ]; then CMD=$1 else @@ -26,8 +33,9 @@ fi if [ -n "$2" ]; then IP=$2 else - # NOTE(vish): this will just get the first ip in the list, so if you - # have more than one eth device set up, this will fail + # NOTE(vish): This will just get the first ip in the list, so if you + # have more than one eth device set up, this will fail, and + # you should explicitly pass in the ip of the instance IP=`ifconfig | grep -m 1 'inet addr:'| cut -d: -f2 | awk '{print $1}'` fi @@ -39,6 +47,8 @@ fi if [ -n "$4" ]; then + # NOTE(vish): Management IP is the ip over which to allow ssh traffic. It + # will also allow traffic to nova-api MGMT_IP=$4 else MGMT_IP="$IP" @@ -78,7 +88,9 @@ if [ "$CMD" == "base" ] || [ "$CMD" == "all" ]; then iptables -N nova_forward iptables -A FORWARD -j nova_forward - # iptables -P OUTPUT DROP # too restrictive for the moment + # NOTE(vish): DROP on output is too restrictive for now. We need to add + # in a bunch of more specific output rules to use it. + # iptables -P OUTPUT DROP iptables -A OUTPUT -m state --state INVALID -j DROP iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT iptables -N nova_output @@ -99,8 +111,9 @@ if [ "$CMD" == "ganglia" ] || [ "$CMD" == "all" ]; then iptables -A nova_input -m udp -p udp -d $IP --dport 8649 -j ACCEPT fi -if [ "$CMD" == "dashboard" ] || [ "$CMD" == "all" ]; then - # dashboard +if [ "$CMD" == "web" ] || [ "$CMD" == "all" ]; then + # NOTE(vish): This opens up ports for web access, allowing web-based + # dashboards to work. iptables -A nova_input -m tcp -p tcp -d $IP --dport 80 -j ACCEPT iptables -A nova_input -m tcp -p tcp -d $IP --dport 443 -j ACCEPT fi @@ -110,9 +123,9 @@ if [ "$CMD" == "objectstore" ] || [ "$CMD" == "all" ]; then fi if [ "$CMD" == "api" ] || [ "$CMD" == "all" ]; then - iptables -A nova_input -m tcp -p tcp -d $IP --dport 8773 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport $API_PORT -j ACCEPT if [ "$IP" != "$MGMT_IP" ]; then - iptables -A nova_input -m tcp -p tcp -d $MGMT_IP --dport 8773 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $MGMT_IP --dport $API_PORT -j ACCEPT fi fi -- cgit From bc88c73a4e986289be7835b95ec97ffb7a50f7d7 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 28 Sep 2010 17:53:27 -0700 Subject: missed a comma --- nova/flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/flags.py b/nova/flags.py index 92f6766cf..c32cdd7a4 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -190,7 +190,7 @@ DEFINE_string('rabbit_virtual_host', '/', 'rabbit virtual host') DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to') DEFINE_string('cc_host', '127.0.0.1', 'ip of api server') DEFINE_integer('cc_port', 8773, 'cloud controller port') -DEFINE_string('ec2_url', 'http://127.0.0.1:8773/services/Cloud' +DEFINE_string('ec2_url', 'http://127.0.0.1:8773/services/Cloud', 'Url to ec2 api server') DEFINE_string('default_image', 'ami-11111', -- cgit From 2327378a1e5c9fa942d56001919caaeb1be1c7cb Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Tue, 28 Sep 2010 18:38:19 -0700 Subject: Removed str_id from FixedIp references --- bin/nova-manage | 2 +- nova/compute/manager.py | 10 +++++----- nova/network/linux_net.py | 2 +- nova/network/manager.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/nova-manage b/bin/nova-manage index baa1cb4db..d0fde6f17 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -316,7 +316,7 @@ class FloatingIpCommands(object): for floating_ip in floating_ips: instance = None if floating_ip['fixed_ip']: - instance = floating_ip['fixed_ip']['instance']['str_id'] + instance = floating_ip['fixed_ip']['instance']['address'] print "%s\t%s\t%s" % (floating_ip['host'], floating_ip['address'], instance) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 24538e4f1..f370ede8b 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -67,7 +67,7 @@ class ComputeManager(manager.Manager): def run_instance(self, context, instance_id, **_kwargs): """Launch a new instance with specified options.""" instance_ref = self.db.instance_get(context, instance_id) - if instance_ref['str_id'] in self.driver.list_instances(): + if instance_ref['ec2_id'] in self.driver.list_instances(): raise exception.Error("Instance has already been created") logging.debug("instance %s: starting...", instance_id) project_id = instance_ref['project_id'] @@ -129,7 +129,7 @@ class ComputeManager(manager.Manager): raise exception.Error( 'trying to reboot a non-running' 'instance: %s (state: %s excepted: %s)' % - (instance_ref['str_id'], + (instance_ref['ec2_id'], instance_ref['state'], power_state.RUNNING)) @@ -151,7 +151,7 @@ class ComputeManager(manager.Manager): if FLAGS.connection_type == 'libvirt': fname = os.path.abspath(os.path.join(FLAGS.instances_path, - instance_ref['str_id'], + instance_ref['ec2_id'], 'console.log')) with open(fname, 'r') as f: output = f.read() @@ -174,7 +174,7 @@ class ComputeManager(manager.Manager): instance_ref = self.db.instance_get(context, instance_id) dev_path = yield self.volume_manager.setup_compute_volume(context, volume_id) - yield self.driver.attach_volume(instance_ref['str_id'], + yield self.driver.attach_volume(instance_ref['ec2_id'], dev_path, mountpoint) self.db.volume_attached(context, volume_id, instance_id, mountpoint) @@ -189,7 +189,7 @@ class ComputeManager(manager.Manager): volume_id) instance_ref = self.db.instance_get(context, instance_id) volume_ref = self.db.volume_get(context, volume_id) - yield self.driver.detach_volume(instance_ref['str_id'], + yield self.driver.detach_volume(instance_ref['ec2_id'], volume_ref['mountpoint']) self.db.volume_detached(context, volume_id) defer.returnValue(True) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 41aeb5da7..25a9456b9 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -133,7 +133,7 @@ def get_dhcp_hosts(context, network_id): """Get a string containing a network's hosts config in dnsmasq format""" hosts = [] for fixed_ip in db.network_get_associated_fixed_ips(context, network_id): - hosts.append(_host_dhcp(fixed_ip['str_id'])) + hosts.append(_host_dhcp(fixed_ip['address'])) return '\n'.join(hosts) diff --git a/nova/network/manager.py b/nova/network/manager.py index 191c1d364..321dac914 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -270,7 +270,7 @@ class VlanManager(NetworkManager): raise exception.Error("IP %s leased to bad mac %s vs %s" % (address, instance_ref['mac_address'], mac)) self.db.fixed_ip_update(context, - fixed_ip_ref['str_id'], + fixed_ip_ref['address'], {'leased': True}) def release_fixed_ip(self, context, mac, address): -- cgit From e3102b6b9be148597a2f502d2b2baf750ecc0a34 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Tue, 28 Sep 2010 18:43:36 -0700 Subject: Fixed tests --- bin/nova-manage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index d0fde6f17..a5087bfec 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -316,7 +316,7 @@ class FloatingIpCommands(object): for floating_ip in floating_ips: instance = None if floating_ip['fixed_ip']: - instance = floating_ip['fixed_ip']['instance']['address'] + instance = floating_ip['fixed_ip']['instance']['ec2_id'] print "%s\t%s\t%s" % (floating_ip['host'], floating_ip['address'], instance) -- cgit From fe139bbdee60aadd720cb7a83d0846f2824c078f Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Wed, 29 Sep 2010 00:49:04 -0700 Subject: Began wiring up context authorization --- nova/db/sqlalchemy/api.py | 50 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 9c3caf9af..b5847d299 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -19,6 +19,7 @@ Implementation of SQLAlchemy backend """ +import logging import sys from nova import db @@ -48,6 +49,24 @@ def _deleted(context): return context.get('deleted', False) +def is_admin_context(context): + if not context: + logging.warning('Use of empty request context is deprecated') + return True + if not context.user: + return True + return context.user.is_admin() + + +def is_user_context(context): + if not context: + logging.warning('Use of empty request context is deprecated') + return False + if not context.user or not context.project: + return False + return True + + ################### @@ -869,14 +888,41 @@ def volume_detached(_context, volume_id): def volume_get(context, volume_id): - return models.Volume.find(volume_id, deleted=_deleted(context)) + session = get_session() + + if is_admin_context(context): + volume_ref = session.query(models.Volume + ).filter_by(id=volume_id + ).filter_by(deleted=_deleted(context) + ).first() + if not volume_ref: + raise exception.NotFound('No volume for id %s' % volume_id) + + if is_user_context(context): + volume_ref = session.query(models.Volume + ).filter_by(project_id=project_id + ).filter_by(id=volume_id + ).filter_by(deleted=False + ).first() + if not volume_ref: + raise exception.NotFound('No volume for id %s' % volume_id) + + raise exception.NotAuthorized() def volume_get_all(context): - return models.Volume.all(deleted=_deleted(context)) + if is_admin_context(context): + return models.Volume.all(deleted=_deleted(context)) + raise exception.NotAuthorized() def volume_get_all_by_project(context, project_id): + if is_user_context(context): + if context.project.id != project_id: + raise exception.NotAuthorized() + elif not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() return session.query(models.Volume ).filter_by(project_id=project_id -- cgit From 4c1aa3d96f0c44d3e01864ca3128e9b052d1d7fd Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 29 Sep 2010 10:17:10 -0400 Subject: After update from trunk, a few more exceptions that need to be converted to Faults --- nova/api/rackspace/backup_schedules.py | 7 ++++--- nova/api/rackspace/servers.py | 17 +++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/nova/api/rackspace/backup_schedules.py b/nova/api/rackspace/backup_schedules.py index 46da778ee..cb83023bc 100644 --- a/nova/api/rackspace/backup_schedules.py +++ b/nova/api/rackspace/backup_schedules.py @@ -20,6 +20,7 @@ from webob import exc from nova import wsgi from nova.api.rackspace import _id_translator +from nova.api.rackspace import faults import nova.image.service class Controller(wsgi.Controller): @@ -27,12 +28,12 @@ class Controller(wsgi.Controller): pass def index(self, req, server_id): - return exc.HTTPNotFound() + return faults.Fault(exc.HTTPNotFound()) def create(self, req, server_id): """ No actual update method required, since the existing API allows both create and update through a POST """ - return exc.HTTPNotFound() + return faults.Fault(exc.HTTPNotFound()) def delete(self, req, server_id): - return exc.HTTPNotFound() + return faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 4ab04bde7..888d67542 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -24,6 +24,7 @@ from nova import rpc from nova import utils from nova import wsgi from nova.api.rackspace import _id_translator +from nova.api.rackspace import faults from nova.compute import power_state import nova.image.service @@ -120,7 +121,7 @@ class Controller(wsgi.Controller): if inst: if inst.user_id == user_id: return _entity_detail(inst) - raise exc.HTTPNotFound() + raise faults.Fault(exc.HTTPNotFound()) def delete(self, req, id): """ Destroys a server """ @@ -128,13 +129,13 @@ class Controller(wsgi.Controller): instance = self.db_driver.instance_get(None, id) if instance and instance['user_id'] == user_id: self.db_driver.instance_destroy(None, id) - return exc.HTTPAccepted() - return exc.HTTPNotFound() + return faults.Fault(exc.HTTPAccepted()) + return faults.Fault(exc.HTTPNotFound()) def create(self, req): """ Creates a new server for a given user """ if not req.environ.has_key('inst_dict'): - return exc.HTTPUnprocessableEntity() + return faults.Fault(exc.HTTPUnprocessableEntity()) inst = self._build_server_instance(req) @@ -147,22 +148,22 @@ class Controller(wsgi.Controller): def update(self, req, id): """ Updates the server name or password """ if not req.environ.has_key('inst_dict'): - return exc.HTTPUnprocessableEntity() + return faults.Fault(exc.HTTPUnprocessableEntity()) instance = self.db_driver.instance_get(None, id) if not instance: - return exc.HTTPNotFound() + return faults.Fault(exc.HTTPNotFound()) attrs = req.environ['nova.context'].get('model_attributes', None) if attrs: self.db_driver.instance_update(None, id, _filter_params(attrs)) - return exc.HTTPNoContent() + return faults.Fault(exc.HTTPNoContent()) def action(self, req, id): """ multi-purpose method used to reboot, rebuild, and resize a server """ if not req.environ.has_key('inst_dict'): - return exc.HTTPUnprocessableEntity() + return faults.Fault(exc.HTTPUnprocessableEntity()) def _build_server_instance(self, req): """Build instance data structure and save it to the data store.""" -- cgit From 29eca7e7992fc5c073d70f7c8ca5e5bc03f62af7 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 29 Sep 2010 11:37:26 -0400 Subject: Limit entity lists by &offset and &limit --- nova/api/rackspace/__init__.py | 20 ++++++++++++++++++++ nova/api/rackspace/flavors.py | 5 ++++- nova/api/rackspace/images.py | 2 ++ nova/api/rackspace/servers.py | 18 ++++++++++++------ nova/tests/api/rackspace/__init__.py | 29 +++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 7 deletions(-) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index 98802663f..48104f6df 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -165,3 +165,23 @@ class APIRouter(wsgi.Router): controller=sharedipgroups.Controller()) super(APIRouter, self).__init__(mapper) + + +def limited(items, req): + """Return a slice of items according to requested offset and limit. + + items - a sliceable + req - wobob.Request possibly containing offset and limit GET variables. + offset is where to start in the list, and limit is the maximum number + of items to return. + + If limit is not specified, 0, or > 1000, defaults to 1000. + """ + offset = int(req.GET.get('offset', 0)) + limit = int(req.GET.get('limit', 0)) + if not limit: + limit = 1000 + limit = min(1000, limit) + range_end = offset + limit + return items[offset:range_end] + diff --git a/nova/api/rackspace/flavors.py b/nova/api/rackspace/flavors.py index 3bcf170e5..ba7aa937c 100644 --- a/nova/api/rackspace/flavors.py +++ b/nova/api/rackspace/flavors.py @@ -15,9 +15,11 @@ # License for the specific language governing permissions and limitations # under the License. +from webob import exc + from nova.compute import instance_types from nova import wsgi -from webob import exc +import nova.api.rackspace class Controller(wsgi.Controller): """Flavor controller for the Rackspace API.""" @@ -38,6 +40,7 @@ class Controller(wsgi.Controller): def detail(self, req): """Return all flavors in detail.""" items = [self.show(req, id)['flavor'] for id in self._all_ids()] + items = nova.api.rackspace.limited(items, req) return dict(flavors=items) def show(self, req, id): diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py index 11b058dec..7da17e6a7 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/rackspace/images.py @@ -19,6 +19,7 @@ from webob import exc from nova import wsgi from nova.api.rackspace import _id_translator +import nova.api.rackspace import nova.image.service class Controller(wsgi.Controller): @@ -45,6 +46,7 @@ class Controller(wsgi.Controller): def detail(self, req): """Return all public images in detail.""" data = self._service.index() + data = nova.api.rackspace.limited(data, req) for img in data: img['id'] = self._id_translator.to_rs_id(img['id']) return dict(images=data) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 4ab04bde7..958fc86a3 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -25,6 +25,7 @@ from nova import utils from nova import wsgi from nova.api.rackspace import _id_translator from nova.compute import power_state +import nova.api.rackspace import nova.image.service FLAGS = flags.FLAGS @@ -101,16 +102,21 @@ class Controller(wsgi.Controller): def index(self, req): """ Returns a list of server names and ids for a given user """ - user_id = req.environ['nova.context']['user']['id'] - instance_list = self.db_driver.instance_get_all_by_user(None, user_id) - res = [_entity_inst(inst)['server'] for inst in instance_list] - return _entity_list(res) + return self._items(req, entity_maker=_entity_inst) def detail(self, req): """ Returns a list of server details for a given user """ + return self._items(req, entity_maker=_entity_detail) + + def _items(self, req, entity_maker): + """Returns a list of servers for a given user. + + entity_maker - either _entity_detail or _entity_inst + """ user_id = req.environ['nova.context']['user']['id'] - res = [_entity_detail(inst)['server'] for inst in - self.db_driver.instance_get_all_by_user(None, user_id)] + instance_list = self.db_driver.instance_get_all_by_user(None, user_id) + limited_list = nova.api.rackspace.limited(instance_list, req) + res = [entity_maker(inst)['server'] for inst in limited_list] return _entity_list(res) def show(self, req, id): diff --git a/nova/tests/api/rackspace/__init__.py b/nova/tests/api/rackspace/__init__.py index 622cb4335..bfd0f87a7 100644 --- a/nova/tests/api/rackspace/__init__.py +++ b/nova/tests/api/rackspace/__init__.py @@ -17,6 +17,7 @@ import unittest +from nova.api.rackspace import limited from nova.api.rackspace import RateLimitingMiddleware from nova.tests.api.test_helper import * from webob import Request @@ -77,3 +78,31 @@ class RateLimitingMiddlewareTest(unittest.TestCase): self.assertEqual(middleware.limiter.__class__.__name__, "Limiter") middleware = RateLimitingMiddleware(APIStub(), service_host='foobar') self.assertEqual(middleware.limiter.__class__.__name__, "WSGIAppProxy") + + +class LimiterTest(unittest.TestCase): + + def testLimiter(self): + items = range(2000) + req = Request.blank('/') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=0') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=3') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=2005') + self.assertEqual(limited(items, req), []) + req = Request.blank('/?limit=10') + self.assertEqual(limited(items, req), items[ :10]) + req = Request.blank('/?limit=0') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?limit=3000') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=1&limit=3') + self.assertEqual(limited(items, req), items[1:4]) + req = Request.blank('/?offset=3&limit=0') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=3&limit=1500') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=3000&limit=10') + self.assertEqual(limited(items, req), []) -- cgit From 0868bcee453665b1ce24d43a90b3addfaab8c49d Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 29 Sep 2010 12:16:53 -0500 Subject: Server update name and password --- nova/api/rackspace/servers.py | 61 +++++++++++++++++++++++-------------- nova/db/sqlalchemy/models.py | 3 ++ nova/tests/api/rackspace/servers.py | 36 +++++++++++++++++++--- nova/wsgi.py | 9 ++++++ 4 files changed, 82 insertions(+), 27 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 4ab04bde7..c156309bd 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime import time from webob import exc @@ -25,11 +26,13 @@ from nova import utils from nova import wsgi from nova.api.rackspace import _id_translator from nova.compute import power_state +from nova.wsgi import Serializer import nova.image.service FLAGS = flags.FLAGS - +flags.DEFINE_string('rs_network_manager', 'nova.network.manager.FlatManager', + 'Networking for rackspace') def translator_instance(): """ Helper method for initializing the image id translator """ @@ -82,7 +85,6 @@ def _entity_inst(inst): class Controller(wsgi.Controller): """ The Server API controller for the Openstack API """ - _serialization_metadata = { 'application/xml': { @@ -146,16 +148,19 @@ class Controller(wsgi.Controller): def update(self, req, id): """ Updates the server name or password """ - if not req.environ.has_key('inst_dict'): + user_id = req.environ['nova.context']['user']['id'] + + inst_dict = self._deserialize(req.body, req) + + if not inst_dict: return exc.HTTPUnprocessableEntity() instance = self.db_driver.instance_get(None, id) - if not instance: + if not instance or instance.user_id != user_id: return exc.HTTPNotFound() - attrs = req.environ['nova.context'].get('model_attributes', None) - if attrs: - self.db_driver.instance_update(None, id, _filter_params(attrs)) + self.db_driver.instance_update(None, id, + _filter_params(inst_dict['server'])) return exc.HTTPNoContent() def action(self, req, id): @@ -170,34 +175,44 @@ class Controller(wsgi.Controller): inst = {} env = req.environ['inst_dict'] + user_id = req.environ['nova.context']['user']['id'] + inst['rs_id'] = _new_rs_id(user_id) image_id = env['server']['imageId'] + opaque_id = translator_instance().from_rs_id(image_id) - inst['name'] = env['server']['server_name'] + inst['name'] = env['server']['name'] inst['image_id'] = opaque_id inst['instance_type'] = env['server']['flavorId'] - - user_id = req.environ['nova.context']['user']['id'] inst['user_id'] = user_id - inst['launch_time'] = ltime inst['mac_address'] = utils.generate_mac() - inst['project_id'] = env['project']['id'] - inst['reservation_id'] = reservation - reservation = utils.generate_uid('r') + #TODO(dietz) These are the attributes I'm unsure of + inst['state_description'] = 'scheduling' + inst['kernel_id'] = '' + inst['ramdisk_id'] = '' + inst['reservation_id'] = utils.generate_uid('r') + inst['key_data'] = '' + inst['key_name'] = '' + inst['security_group'] = '' + + # Flavor related attributes + inst['instance_type'] = '' + inst['memory_mb'] = '' + inst['vcpus'] = '' + inst['local_gb'] = '' - address = self.network.allocate_ip( - inst['user_id'], - inst['project_id'], - mac=inst['mac_address']) + + + #TODO(dietz): This seems necessary. How do these apply across + #the Rackspace implementation? + inst['project_id'] = '' - inst['private_dns_name'] = str(address) - inst['bridge_name'] = network.BridgedNetwork.get_network_for_project( - inst['user_id'], - inst['project_id'], - 'default')['bridge_name'] + self.network_manager = utils.import_object(FLAGS.rs_network_manager) + + address = self.network_manager.allocate_fixed_ip( None, inst['id']) ref = self.db_driver.instance_create(None, inst) inst['id'] = ref.id diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 3d2460c39..83fc7c852 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -199,6 +199,9 @@ class Instance(BASE, NovaBase): id = Column(Integer, primary_key=True) ec2_id = Column(String(10), unique=True) + rs_id = Column(String(255)) + rs_root_password = Column(String(255)) + user_id = Column(String(255)) project_id = Column(String(255)) diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 9fd8e5e88..81682288c 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -95,11 +95,39 @@ class ServersTest(unittest.TestCase): # res = req.get_response(nova.api.API()) # print res - def test_update_server_password(self): - pass - def test_update_server_name(self): - pass + def test_update_bad_params(self): + """ Confirm that update is filtering params """ + inst_dict = dict(cat='leopard', name='server_test', adminPass='bacon') + self.body = json.dumps(dict(server=inst_dict)) + + def server_update(context, id, params): + self.update_called = True + filtered_dict = dict(name='server_test', adminPass='bacon') + self.assertEqual(params, filtered_dict) + + self.stubs.Set(nova.db.api, 'instance_update', + server_update) + + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + req.body = self.body + req.get_response(nova.api.API()) + + def test_update_server(self): + inst_dict = dict(name='server_test', adminPass='bacon') + self.body = json.dumps(dict(server=inst_dict)) + + def server_update(context, id, params): + self.assertEqual(params, json.loads(self.body)['server']) + + self.stubs.Set(nova.db.api, 'instance_update', + server_update) + + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + req.body = self.body + req.get_response(nova.api.API()) def test_create_backup_schedules(self): req = webob.Request.blank('/v1.0/servers/1/backup_schedules') diff --git a/nova/wsgi.py b/nova/wsgi.py index da9374542..48c4dabc2 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -230,6 +230,15 @@ class Controller(object): serializer = Serializer(request.environ, _metadata) return serializer.to_content_type(data) + def _deserialize(self, data, request): + """ + Deserialize the request body to the response type requested in request. + Uses self._serialization_metadata if it exists, which is a dict mapping + MIME types to information needed to serialize to that type. + """ + _metadata = getattr(type(self), "_serialization_metadata", {}) + serializer = Serializer(request.environ, _metadata) + return serializer.deserialize(data) class Serializer(object): """ -- cgit From 9c4319a83a6e7d61ffa6b78e9f17ea35821c5526 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 29 Sep 2010 12:58:40 -0500 Subject: Make update work correctly --- nova/api/rackspace/servers.py | 20 ++++++++++++++------ nova/db/sqlalchemy/models.py | 2 +- nova/tests/api/rackspace/servers.py | 5 +++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index c156309bd..aa955c222 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -34,7 +34,12 @@ FLAGS = flags.FLAGS flags.DEFINE_string('rs_network_manager', 'nova.network.manager.FlatManager', 'Networking for rackspace') -def translator_instance(): +def _instance_id_translator(): + """ Helper method for initializing an id translator for Rackspace instance + ids """ + return _id_translator.RackspaceAPIIdTranslator( "instance", 'nova') + +def _image_id_translator(): """ Helper method for initializing the image id translator """ service = nova.image.service.ImageService.load() return _id_translator.RackspaceAPIIdTranslator( @@ -42,11 +47,11 @@ def translator_instance(): def _filter_params(inst_dict): """ Extracts all updatable parameters for a server update request """ - keys = ['name', 'adminPass'] + keys = dict(name='name', admin_pass='adminPass') new_attrs = {} - for k in keys: - if inst_dict.has_key(k): - new_attrs[k] = inst_dict[k] + for k, v in keys.items(): + if inst_dict.has_key(v): + new_attrs[k] = inst_dict[v] return new_attrs def _entity_list(entities): @@ -174,10 +179,13 @@ class Controller(wsgi.Controller): ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) inst = {} + inst_id_trans = _instance_id_translator() + image_id_trans = _image_id_translator() + env = req.environ['inst_dict'] user_id = req.environ['nova.context']['user']['id'] - inst['rs_id'] = _new_rs_id(user_id) + inst['rs_id'] = inst_id_trans.to_rs_id() image_id = env['server']['imageId'] opaque_id = translator_instance().from_rs_id(image_id) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 83fc7c852..2a314b037 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -200,7 +200,7 @@ class Instance(BASE, NovaBase): ec2_id = Column(String(10), unique=True) rs_id = Column(String(255)) - rs_root_password = Column(String(255)) + admin_pass = Column(String(255)) user_id = Column(String(255)) project_id = Column(String(255)) diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 81682288c..e08c9c6fd 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -103,7 +103,7 @@ class ServersTest(unittest.TestCase): def server_update(context, id, params): self.update_called = True - filtered_dict = dict(name='server_test', adminPass='bacon') + filtered_dict = dict(name='server_test', admin_pass='bacon') self.assertEqual(params, filtered_dict) self.stubs.Set(nova.db.api, 'instance_update', @@ -119,7 +119,8 @@ class ServersTest(unittest.TestCase): self.body = json.dumps(dict(server=inst_dict)) def server_update(context, id, params): - self.assertEqual(params, json.loads(self.body)['server']) + filtered_dict = dict(name='server_test', admin_pass='bacon') + self.assertEqual(params, filtered_dict) self.stubs.Set(nova.db.api, 'instance_update', server_update) -- cgit From 724c15b6583b47baabd2f01090580cb248ad1244 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Wed, 29 Sep 2010 14:12:02 -0400 Subject: Remove debuggish print statement. --- bin/nova-manage | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index 7a9a4c3d1..bf3c67612 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -277,7 +277,6 @@ class UserCommands(object): is_admin = True else: is_admin = False - print "is_admin: %r" % is_admin self.manager.modify_user(name, access_key, secret_key, is_admin) class ProjectCommands(object): -- cgit From 2136f12d29cef9acc7dc6ee0a5901fa3878160f8 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 29 Sep 2010 15:09:39 -0400 Subject: Make Fault raiseable by inheriting from webob.exc.HTTPException. Change from using self.exception which is reserved by HTTPException to self.wrapped_exc. --- nova/api/rackspace/faults.py | 17 +++++++++-------- nova/tests/api/rackspace/testfaults.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/nova/api/rackspace/faults.py b/nova/api/rackspace/faults.py index fd6bc3623..32e5c866f 100644 --- a/nova/api/rackspace/faults.py +++ b/nova/api/rackspace/faults.py @@ -17,11 +17,12 @@ import webob.dec +import webob.exc from nova import wsgi -class Fault(wsgi.Application): +class Fault(webob.exc.HTTPException): """An RS API fault response.""" @@ -39,23 +40,23 @@ class Fault(wsgi.Application): def __init__(self, exception): """Create a Fault for the given webob.exc.exception.""" - self.exception = exception + self.wrapped_exc = exception @webob.dec.wsgify def __call__(self, req): - """Generate a WSGI response based on self.exception.""" + """Generate a WSGI response based on the exception passed to ctor.""" # Replace the body with fault details. - code = self.exception.status_int + code = self.wrapped_exc.status_int fault_name = self._fault_names.get(code, "cloudServersFault") fault_data = { fault_name: { 'code': code, - 'message': self.exception.explanation}} + 'message': self.wrapped_exc.explanation}} if code == 413: - retry = self.exception.headers['Retry-After'] + retry = self.wrapped_exc.headers['Retry-After'] fault_data[fault_name]['retryAfter'] = retry # 'code' is an attribute on the fault tag itself metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} serializer = wsgi.Serializer(req.environ, metadata) - self.exception.body = serializer.to_content_type(fault_data) - return self.exception + self.wrapped_exc.body = serializer.to_content_type(fault_data) + return self.wrapped_exc diff --git a/nova/tests/api/rackspace/testfaults.py b/nova/tests/api/rackspace/testfaults.py index 74caffd30..b2931bc98 100644 --- a/nova/tests/api/rackspace/testfaults.py +++ b/nova/tests/api/rackspace/testfaults.py @@ -1,5 +1,6 @@ import unittest import webob +import webob.dec import webob.exc from nova.api.rackspace import faults @@ -28,3 +29,12 @@ class TestFaults(unittest.TestCase): self.assertTrue('sorry' in body_sans_spaces) self.assertTrue('4' in body_sans_spaces) self.assertEqual(resp.headers['Retry-After'], 4) + + def test_raise(self): + @webob.dec.wsgify + def raiser(req): + raise faults.Fault(webob.exc.HTTPNotFound(explanation='whut?')) + req = webob.Request.blank('/.xml') + resp = req.get_response(raiser) + self.assertEqual(resp.status_int, 404) + self.assertTrue('whut?' in resp.body) -- cgit From 072661db01ed196eac92ceb1e942429a0e380e4a Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Wed, 29 Sep 2010 15:52:02 -0400 Subject: Support reboot in api.rackspace by extracting reboot function from api.ec2 into api.cloud. --- nova/api/cloud.py | 42 ++++++++++++++++++++++++++++++ nova/api/ec2/cloud.py | 8 ++---- nova/api/rackspace/servers.py | 10 +++++-- nova/tests/api/rackspace/images.py | 1 + nova/tests/api/rackspace/sharedipgroups.py | 1 + 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 nova/api/cloud.py diff --git a/nova/api/cloud.py b/nova/api/cloud.py new file mode 100644 index 000000000..345677d4f --- /dev/null +++ b/nova/api/cloud.py @@ -0,0 +1,42 @@ +# 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. + +""" +Methods for API calls to control instances via AMQP. +""" + + +from nova import db +from nova import flags +from nova import rpc + +FLAGS = flags.FLAGS + + +def reboot(instance_id, context=None): + """Reboot the given instance. + + #TODO(gundlach) not actually sure what context is used for by ec2 here + -- I think we can just remove it and use None all the time. + """ + instance_ref = db.instance_get_by_ec2_id(None, instance_id) + host = instance_ref['host'] + rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), + {"method": "reboot_instance", + "args": {"context": None, + "instance_id": instance_ref['id']}}) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 05e8065f3..4d962fcdd 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -36,6 +36,7 @@ from nova import quota from nova import rpc from nova import utils from nova.compute.instance_types import INSTANCE_TYPES +from nova.api import cloud from nova.api.ec2 import images @@ -664,12 +665,7 @@ class CloudController(object): def reboot_instances(self, context, instance_id, **kwargs): """instance_id is a list of instance ids""" for id_str in instance_id: - instance_ref = db.instance_get_by_ec2_id(context, id_str) - host = instance_ref['host'] - rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), - {"method": "reboot_instance", - "args": {"context": None, - "instance_id": instance_ref['id']}}) + cloud.reboot(id_str, context=context) return True def delete_volume(self, context, volume_id, **kwargs): diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index aa955c222..4f81e25f9 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -24,6 +24,7 @@ from nova import flags from nova import rpc from nova import utils from nova import wsgi +from nova.api import cloud from nova.api.rackspace import _id_translator from nova.compute import power_state from nova.wsgi import Serializer @@ -171,8 +172,13 @@ class Controller(wsgi.Controller): def action(self, req, id): """ multi-purpose method used to reboot, rebuild, and resize a server """ - if not req.environ.has_key('inst_dict'): - return exc.HTTPUnprocessableEntity() + input_dict = self._deserialize(req.body, req) + try: + reboot_type = input_dict['reboot']['type'] + except Exception: + raise faults.Fault(webob.exc.HTTPNotImplemented()) + opaque_id = _instance_id_translator().from_rsapi_id(id) + cloud.reboot(opaque_id) def _build_server_instance(self, req): """Build instance data structure and save it to the data store.""" diff --git a/nova/tests/api/rackspace/images.py b/nova/tests/api/rackspace/images.py index 560d8c898..4c9987e8b 100644 --- a/nova/tests/api/rackspace/images.py +++ b/nova/tests/api/rackspace/images.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import stubout import unittest from nova.api.rackspace import images diff --git a/nova/tests/api/rackspace/sharedipgroups.py b/nova/tests/api/rackspace/sharedipgroups.py index b4b281db7..1906b54f5 100644 --- a/nova/tests/api/rackspace/sharedipgroups.py +++ b/nova/tests/api/rackspace/sharedipgroups.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import stubout import unittest from nova.api.rackspace import sharedipgroups -- cgit From e258998923b7e8fa92656aa409f875b640df930c Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Wed, 29 Sep 2010 13:26:14 -0700 Subject: Progress on volumes Fixed foreign keys to respect deleted flag --- nova/db/sqlalchemy/api.py | 130 ++++++++++++++++++++++++++++++------------- nova/db/sqlalchemy/models.py | 35 +++++++++--- 2 files changed, 118 insertions(+), 47 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index b5847d299..28b937233 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -30,7 +30,7 @@ from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import joinedload_all +from sqlalchemy.orm import joinedload, joinedload_all from sqlalchemy.sql import exists, func FLAGS = flags.FLAGS @@ -811,6 +811,7 @@ def quota_destroy(_context, project_id): def volume_allocate_shelf_and_blade(_context, volume_id): + # TODO(devcamcar): Make admin only session = get_session() with session.begin(): export_device = session.query(models.ExportDevice @@ -839,7 +840,7 @@ def volume_attached(_context, volume_id, instance_id, mountpoint): volume_ref.save(session=session) -def volume_create(_context, values): +def volume_create(context, values): volume_ref = models.Volume() for (key, value) in values.iteritems(): volume_ref[key] = value @@ -848,7 +849,7 @@ def volume_create(_context, values): with session.begin(): while volume_ref.ec2_id == None: ec2_id = utils.generate_uid(volume_ref.__prefix__) - if not volume_ec2_id_exists(_context, ec2_id, session=session): + if not volume_ec2_id_exists(context, ec2_id, session=session): volume_ref.ec2_id = ec2_id volume_ref.save(session=session) return volume_ref @@ -876,10 +877,10 @@ def volume_destroy(_context, volume_id): {'id': volume_id}) -def volume_detached(_context, volume_id): +def volume_detached(context, volume_id): session = get_session() with session.begin(): - volume_ref = models.Volume.find(volume_id, session=session) + volume_ref = volume_get(context, volume_id, session=session) volume_ref['status'] = 'available' volume_ref['mountpoint'] = None volume_ref['attach_status'] = 'detached' @@ -887,27 +888,29 @@ def volume_detached(_context, volume_id): volume_ref.save(session=session) -def volume_get(context, volume_id): - session = get_session() +def volume_get(context, volume_id, session=None): + if not session: + session = get_session() + result = None if is_admin_context(context): - volume_ref = session.query(models.Volume - ).filter_by(id=volume_id - ).filter_by(deleted=_deleted(context) - ).first() - if not volume_ref: - raise exception.NotFound('No volume for id %s' % volume_id) + result = session.query(models.Volume + ).filter_by(id=volume_id + ).filter_by(deleted=_deleted(context) + ).first() + elif is_user_context(context): + result = session.query(models.Volume + ).filter_by(project_id=context.project.project_id + ).filter_by(id=volume_id + ).filter_by(deleted=False + ).first() + else: + raise exception.NotAuthorized() - if is_user_context(context): - volume_ref = session.query(models.Volume - ).filter_by(project_id=project_id - ).filter_by(id=volume_id - ).filter_by(deleted=False - ).first() - if not volume_ref: - raise exception.NotFound('No volume for id %s' % volume_id) + if not result: + raise exception.NotFound('No volume for id %s' % volume_id) - raise exception.NotAuthorized() + return result def volume_get_all(context): @@ -916,6 +919,7 @@ def volume_get_all(context): raise exception.NotAuthorized() + def volume_get_all_by_project(context, project_id): if is_user_context(context): if context.project.id != project_id: @@ -932,42 +936,92 @@ def volume_get_all_by_project(context, project_id): def volume_get_by_ec2_id(context, ec2_id): session = get_session() - volume_ref = session.query(models.Volume + result = None + + if is_admin_context(context): + result = session.query(models.Volume ).filter_by(ec2_id=ec2_id ).filter_by(deleted=_deleted(context) ).first() - if not volume_ref: - raise exception.NotFound('Volume %s not found' % (ec2_id)) + elif is_user_context(context): + result = session.query(models.Volume + ).filter_by(project_id=context.project.id + ).filter_by(ec2_id=ec2_id + ).filter_by(deleted=False + ).first() + else: + raise exception.NotAuthorized() - return volume_ref + if not result: + raise exception.NotFound('Volume %s not found' % ec2_id) + + return result def volume_ec2_id_exists(context, ec2_id, session=None): if not session: session = get_session() - return session.query(exists().where(models.Volume.id==ec2_id)).one()[0] + + if is_admin_context(context) or is_user_context(context): + return session.query(exists( + ).where(models.Volume.id==ec2_id) + ).one()[0] + else: + raise exception.NotAuthorized() -def volume_get_instance(_context, volume_id): +def volume_get_instance(context, volume_id): session = get_session() - with session.begin(): - return models.Volume.find(volume_id, session=session).instance + result = None + + if is_admin_context(context): + result = session.query(models.Volume + ).filter_by(id=volume_id + ).filter_by(deleted=_deleted(context) + ).options(joinedload('instance') + ).first() + elif is_user_context(context): + result = session.query(models.Volume + ).filter_by(project_id=context.project.id + ).filter_by(deleted=False + ).options(joinedload('instance') + ).first() + else: + raise exception.NotAuthorized() + + if not result: + raise exception.NotFound('Volume %s not found' % ec2_id) + + return result.instance -def volume_get_shelf_and_blade(_context, volume_id): +def volume_get_shelf_and_blade(context, volume_id): session = get_session() - export_device = session.query(models.ExportDevice - ).filter_by(volume_id=volume_id - ).first() - if not export_device: + result = None + + if is_admin_context(context): + result = session.query(models.ExportDevice + ).filter_by(volume_id=volume_id + ).first() + elif is_user_context(context): + result = session.query(models.ExportDevice + ).join(models.Volume + ).filter(models.Volume.project_id==context.project.id + ).filter_by(volume_id=volume_id + ).first() + else: + raise exception.NotAuthorized() + + if not result: raise exception.NotFound() - return (export_device.shelf_id, export_device.blade_id) + return (result.shelf_id, result.blade_id) -def volume_update(_context, volume_id, values): + +def volume_update(context, volume_id, values): session = get_session() with session.begin(): - volume_ref = models.Volume.find(volume_id, session=session) + volume_ref = volume_get(context, volume_id, session=session) for (key, value) in values.iteritems(): volume_ref[key] = value volume_ref.save(session=session) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 01e58b05e..1b9edf475 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -282,7 +282,11 @@ class Volume(BASE, NovaBase): size = Column(Integer) availability_zone = Column(String(255)) # TODO(vish): foreign key? instance_id = Column(Integer, ForeignKey('instances.id'), nullable=True) - instance = relationship(Instance, backref=backref('volumes')) + instance = relationship(Instance, + backref=backref('volumes'), + foreign_keys=instance_id, + primaryjoin='and_(Volume.instance_id==Instance.id,' + 'Volume.deleted==False)') mountpoint = Column(String(255)) attach_time = Column(String(255)) # TODO(vish): datetime status = Column(String(255)) # TODO(vish): enum? @@ -333,8 +337,11 @@ class ExportDevice(BASE, NovaBase): shelf_id = Column(Integer) blade_id = Column(Integer) volume_id = Column(Integer, ForeignKey('volumes.id'), nullable=True) - volume = relationship(Volume, backref=backref('export_device', - uselist=False)) + volume = relationship(Volume, + backref=backref('export_device', uselist=False), + foreign_keys=volume_id, + primaryjoin='and_(ExportDevice.volume_id==Volume.id,' + 'ExportDevice.deleted==False)') class KeyPair(BASE, NovaBase): @@ -407,8 +414,12 @@ class NetworkIndex(BASE, NovaBase): id = Column(Integer, primary_key=True) index = Column(Integer, unique=True) network_id = Column(Integer, ForeignKey('networks.id'), nullable=True) - network = relationship(Network, backref=backref('network_index', - uselist=False)) + network = relationship(Network, + backref=backref('network_index', uselist=False), + foreign_keys=network_id, + primaryjoin='and_(NetworkIndex.network_id==Network.id,' + 'NetworkIndex.deleted==False)') + class AuthToken(BASE, NovaBase): """Represents an authorization token for all API transactions. Fields @@ -432,8 +443,11 @@ class FixedIp(BASE, NovaBase): network_id = Column(Integer, ForeignKey('networks.id'), nullable=True) network = relationship(Network, backref=backref('fixed_ips')) instance_id = Column(Integer, ForeignKey('instances.id'), nullable=True) - instance = relationship(Instance, backref=backref('fixed_ip', - uselist=False)) + instance = relationship(Instance, + backref=backref('fixed_ip', uselist=False), + foreign_keys=instance_id, + primaryjoin='and_(FixedIp.instance_id==Instance.id,' + 'FixedIp.deleted==False)') allocated = Column(Boolean, default=False) leased = Column(Boolean, default=False) reserved = Column(Boolean, default=False) @@ -462,8 +476,11 @@ class FloatingIp(BASE, NovaBase): id = Column(Integer, primary_key=True) address = Column(String(255)) fixed_ip_id = Column(Integer, ForeignKey('fixed_ips.id'), nullable=True) - fixed_ip = relationship(FixedIp, backref=backref('floating_ips')) - + fixed_ip = relationship(FixedIp, + backref=backref('floating_ips'), + foreign_keys=fixed_ip_id, + primaryjoin='and_(FloatingIp.fixed_ip_id==FixedIp.id,' + 'FloatingIp.deleted==False)') project_id = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) -- cgit From 128ec65cf39e74b53903dd9788a58c8eb513abe8 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 29 Sep 2010 16:21:28 -0500 Subject: Server creation up to, but not including, network configuration --- nova/api/rackspace/servers.py | 97 +++++++++++++++++++++------------ nova/db/sqlalchemy/models.py | 1 - nova/tests/api/rackspace/auth.py | 8 ++- nova/tests/api/rackspace/servers.py | 48 +++++++++++----- nova/tests/api/rackspace/test_helper.py | 17 +++++- nova/wsgi.py | 11 ++-- 6 files changed, 123 insertions(+), 59 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index aa955c222..cc971adc0 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -25,6 +25,7 @@ from nova import rpc from nova import utils from nova import wsgi from nova.api.rackspace import _id_translator +from nova.compute import instance_types from nova.compute import power_state from nova.wsgi import Serializer import nova.image.service @@ -39,11 +40,11 @@ def _instance_id_translator(): ids """ return _id_translator.RackspaceAPIIdTranslator( "instance", 'nova') -def _image_id_translator(): +def _image_service(): """ Helper method for initializing the image id translator """ service = nova.image.service.ImageService.load() - return _id_translator.RackspaceAPIIdTranslator( - "image", service.__class__.__name__) + return (service, _id_translator.RackspaceAPIIdTranslator( + "image", service.__class__.__name__)) def _filter_params(inst_dict): """ Extracts all updatable parameters for a server update request """ @@ -122,8 +123,11 @@ class Controller(wsgi.Controller): def show(self, req, id): """ Returns server details by server id """ + inst_id_trans = _instance_id_translator() + inst_id = inst_id_trans.from_rs_id(id) + user_id = req.environ['nova.context']['user']['id'] - inst = self.db_driver.instance_get(None, id) + inst = self.db_driver.instance_get_by_ec2_id(None, inst_id) if inst: if inst.user_id == user_id: return _entity_detail(inst) @@ -131,8 +135,11 @@ class Controller(wsgi.Controller): def delete(self, req, id): """ Destroys a server """ + inst_id_trans = _instance_id_translator() + inst_id = inst_id_trans.from_rs_id(id) + user_id = req.environ['nova.context']['user']['id'] - instance = self.db_driver.instance_get(None, id) + instance = self.db_driver.instance_get_by_ec2_id(None, inst_id) if instance and instance['user_id'] == user_id: self.db_driver.instance_destroy(None, id) return exc.HTTPAccepted() @@ -140,10 +147,16 @@ class Controller(wsgi.Controller): def create(self, req): """ Creates a new server for a given user """ - if not req.environ.has_key('inst_dict'): + + env = self._deserialize(req.body, req) + if not env: return exc.HTTPUnprocessableEntity() - inst = self._build_server_instance(req) + #try: + inst = self._build_server_instance(req, env) + #except Exception, e: + # print e + # return exc.HTTPUnprocessableEntity() rpc.cast( FLAGS.compute_topic, { @@ -153,6 +166,8 @@ class Controller(wsgi.Controller): def update(self, req, id): """ Updates the server name or password """ + inst_id_trans = _instance_id_translator() + inst_id = inst_id_trans.from_rs_id(id) user_id = req.environ['nova.context']['user']['id'] inst_dict = self._deserialize(req.body, req) @@ -160,7 +175,7 @@ class Controller(wsgi.Controller): if not inst_dict: return exc.HTTPUnprocessableEntity() - instance = self.db_driver.instance_get(None, id) + instance = self.db_driver.instance_get_by_ec2_id(None, inst_id) if not instance or instance.user_id != user_id: return exc.HTTPNotFound() @@ -174,56 +189,68 @@ class Controller(wsgi.Controller): if not req.environ.has_key('inst_dict'): return exc.HTTPUnprocessableEntity() - def _build_server_instance(self, req): + def _build_server_instance(self, req, env): """Build instance data structure and save it to the data store.""" ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) inst = {} inst_id_trans = _instance_id_translator() - image_id_trans = _image_id_translator() - env = req.environ['inst_dict'] user_id = req.environ['nova.context']['user']['id'] - inst['rs_id'] = inst_id_trans.to_rs_id() + instance_type, flavor = None, None + for k, v in instance_types.INSTANCE_TYPES.iteritems(): + if v['flavorid'] == env['server']['flavorId']: + instance_type, flavor = k, v + break + + if not flavor: + raise Exception, "Flavor not found" + image_id = env['server']['imageId'] - opaque_id = translator_instance().from_rs_id(image_id) + img_service, image_id_trans = _image_service() + + opaque_image_id = image_id_trans.to_rs_id(image_id) + image = img_service.show(opaque_image_id) - inst['name'] = env['server']['name'] - inst['image_id'] = opaque_id - inst['instance_type'] = env['server']['flavorId'] + if not image: + raise Exception, "Image not found" + + inst['server_name'] = env['server']['name'] + inst['image_id'] = opaque_image_id inst['user_id'] = user_id inst['launch_time'] = ltime inst['mac_address'] = utils.generate_mac() + inst['project_id'] = user_id - #TODO(dietz) These are the attributes I'm unsure of inst['state_description'] = 'scheduling' - inst['kernel_id'] = '' - inst['ramdisk_id'] = '' + inst['kernel_id'] = image.get('kernelId', FLAGS.default_kernel) + inst['ramdisk_id'] = image.get('ramdiskId', FLAGS.default_ramdisk) inst['reservation_id'] = utils.generate_uid('r') - inst['key_data'] = '' - inst['key_name'] = '' - inst['security_group'] = '' - # Flavor related attributes - inst['instance_type'] = '' - inst['memory_mb'] = '' - inst['vcpus'] = '' - inst['local_gb'] = '' + #TODO(dietz) this may be ill advised + key_pair_ref = self.db_driver.key_pair_get_all_by_user( + None, user_id)[0] - + inst['key_data'] = key_pair_ref['public_key'] + inst['key_name'] = key_pair_ref['name'] - #TODO(dietz): This seems necessary. How do these apply across - #the Rackspace implementation? - inst['project_id'] = '' + #TODO(dietz) stolen from ec2 api, see TODO there + inst['security_group'] = 'default' - self.network_manager = utils.import_object(FLAGS.rs_network_manager) - - address = self.network_manager.allocate_fixed_ip( None, inst['id']) + # Flavor related attributes + inst['instance_type'] = instance_type + inst['memory_mb'] = flavor['memory_mb'] + inst['vcpus'] = flavor['vcpus'] + inst['local_gb'] = flavor['local_gb'] ref = self.db_driver.instance_create(None, inst) - inst['id'] = ref.id + inst['id'] = inst_id_trans.to_rs_id(ref.ec2_id) + + #self.network_manager = utils.import_object(FLAGS.rs_network_manager) + # + #address = self.network_manager.allocate_fixed_ip( None, inst['id']) return inst diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 2a314b037..22446fc64 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -199,7 +199,6 @@ class Instance(BASE, NovaBase): id = Column(Integer, primary_key=True) ec2_id = Column(String(10), unique=True) - rs_id = Column(String(255)) admin_pass = Column(String(255)) user_id = Column(String(255)) diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index a6e10970f..56677c2f4 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -1,12 +1,14 @@ -import webob -import webob.dec +import datetime import unittest + import stubout +import webob +import webob.dec + import nova.api import nova.api.rackspace.auth from nova import auth from nova.tests.api.rackspace import test_helper -import datetime class Test(unittest.TestCase): def setUp(self): diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index e08c9c6fd..d759eba3c 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -26,6 +26,7 @@ import nova.api.rackspace from nova.api.rackspace import servers import nova.db.api from nova.db.sqlalchemy.models import Instance +import nova.rpc from nova.tests.api.test_helper import * from nova.tests.api.rackspace import test_helper @@ -52,8 +53,11 @@ class ServersTest(unittest.TestCase): test_helper.stub_for_testing(self.stubs) test_helper.stub_out_rate_limiting(self.stubs) test_helper.stub_out_auth(self.stubs) + test_helper.stub_out_id_translator(self.stubs) + test_helper.stub_out_key_pair_funcs(self.stubs) + test_helper.stub_out_image_service(self.stubs) self.stubs.Set(nova.db.api, 'instance_get_all', return_servers) - self.stubs.Set(nova.db.api, 'instance_get', return_server) + self.stubs.Set(nova.db.api, 'instance_get_by_ec2_id', return_server) self.stubs.Set(nova.db.api, 'instance_get_all_by_user', return_servers) @@ -67,9 +71,6 @@ class ServersTest(unittest.TestCase): self.assertEqual(res_dict['server']['id'], '1') self.assertEqual(res_dict['server']['name'], 'server1') - def test_get_backup_schedule(self): - pass - def test_get_server_list(self): req = webob.Request.blank('/v1.0/servers') res = req.get_response(nova.api.API()) @@ -82,19 +83,36 @@ class ServersTest(unittest.TestCase): self.assertEqual(s.get('imageId', None), None) i += 1 - #def test_create_instance(self): - # test_helper.stub_out_image_translator(self.stubs) - # body = dict(server=dict( - # name='server_test', imageId=2, flavorId=2, metadata={}, - # personality = {} - # )) - # req = webob.Request.blank('/v1.0/servers') - # req.method = 'POST' - # req.body = json.dumps(body) + def test_create_instance(self): + def instance_create(context, inst): + class Foo(object): + ec2_id = 1 + return Foo() - # res = req.get_response(nova.api.API()) + def fake_cast(*args, **kwargs): + pass - # print res + self.stubs.Set(nova.db.api, 'instance_create', instance_create) + self.stubs.Set(nova.rpc, 'cast', fake_cast) + + test_helper.stub_out_id_translator(self.stubs) + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers') + req.method = 'POST' + req.body = json.dumps(body) + + res = req.get_response(nova.api.API()) + + self.assertEqual(res.status_int, 200) + + def test_update_no_body(self): + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status_int, 422) def test_update_bad_params(self): """ Confirm that update is filtering params """ diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index aa7fb382c..962f0ba4c 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -9,6 +9,7 @@ from nova import utils from nova import flags import nova.api.rackspace.auth import nova.api.rackspace._id_translator +from nova.image import service from nova.wsgi import Router FLAGS = flags.FLAGS @@ -40,7 +41,21 @@ def fake_wsgi(self, req): req.environ['inst_dict'] = json.loads(req.body) return self.application -def stub_out_image_translator(stubs): + + +def stub_out_key_pair_funcs(stubs): + def key_pair(context, user_id): + return [dict(name='key', public_key='public_key')] + stubs.Set(nova.db.api, 'key_pair_get_all_by_user', + key_pair) + +def stub_out_image_service(stubs): + def fake_image_show(meh, id): + return dict(kernelId=1, ramdiskId=1) + + stubs.Set(nova.image.service.LocalImageService, 'show', fake_image_show) + +def stub_out_id_translator(stubs): class FakeTranslator(object): def __init__(self, id_type, service_name): pass diff --git a/nova/wsgi.py b/nova/wsgi.py index 48c4dabc2..b91d91121 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -281,10 +281,13 @@ class Serializer(object): The string must be in the format of a supported MIME type. """ datastring = datastring.strip() - is_xml = (datastring[0] == '<') - if not is_xml: - return json.loads(datastring) - return self._from_xml(datastring) + try: + is_xml = (datastring[0] == '<') + if not is_xml: + return json.loads(datastring) + return self._from_xml(datastring) + except: + return None def _from_xml(self, datastring): xmldata = self.metadata.get('application/xml', {}) -- cgit From f4cf49ec3761bdd38dd1a6cb064875b90e65ad4e Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Wed, 29 Sep 2010 14:27:31 -0700 Subject: Wired up context auth for services --- nova/db/sqlalchemy/api.py | 111 ++++++++++++++++++++++++++++++++++--------- nova/db/sqlalchemy/models.py | 15 ------ 2 files changed, 89 insertions(+), 37 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 28b937233..01a5af38b 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -71,16 +71,37 @@ def is_user_context(context): def service_destroy(context, service_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): - service_ref = models.Service.find(service_id, session=session) + service_ref = service_get(context, service_id, session=session) service_ref.delete(session=session) -def service_get(_context, service_id): - return models.Service.find(service_id) + +def service_get(context, service_id, session=None): + if not is_admin_context(context): + raise exception.NotAuthorized() + + if not session: + session = get_session() + + result = session.query(models.Service + ).filter_by(id=service_id + ).filter_by(deleted=_deleted(context) + ).first() + + if not result: + raise exception.NotFound('No service for id %s' % service_id) + + return result def service_get_all_by_topic(context, topic): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() return session.query(models.Service ).filter_by(deleted=False @@ -89,7 +110,10 @@ def service_get_all_by_topic(context, topic): ).all() -def _service_get_all_topic_subquery(_context, session, topic, subq, label): +def _service_get_all_topic_subquery(context, session, topic, subq, label): + if not is_admin_context(context): + raise exception.NotAuthorized() + sort_value = getattr(subq.c, label) return session.query(models.Service, func.coalesce(sort_value, 0) ).filter_by(topic=topic @@ -101,6 +125,9 @@ def _service_get_all_topic_subquery(_context, session, topic, subq, label): def service_get_all_compute_sorted(context): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): # NOTE(vish): The intended query is below @@ -125,6 +152,9 @@ def service_get_all_compute_sorted(context): def service_get_all_network_sorted(context): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): topic = 'network' @@ -142,6 +172,9 @@ def service_get_all_network_sorted(context): def service_get_all_volume_sorted(context): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): topic = 'volume' @@ -158,11 +191,27 @@ def service_get_all_volume_sorted(context): label) -def service_get_by_args(_context, host, binary): - return models.Service.find_by_args(host, binary) +def service_get_by_args(context, host, binary): + if not is_admin_context(context): + raise exception.NotAuthorized() + + session = get_session() + result = session.query(models.Service + ).filter_by(host=host + ).filter_by(binary=binary + ).filter_by(deleted=_deleted(context) + ).first() + + if not result: + raise exception.NotFound('No service for %s, %s' % (host, binary)) + + return result + +def service_create(context, values): + if not is_admin_context(context): + return exception.NotAuthorized() -def service_create(_context, values): service_ref = models.Service() for (key, value) in values.iteritems(): service_ref[key] = value @@ -170,10 +219,13 @@ def service_create(_context, values): return service_ref -def service_update(_context, service_id, values): +def service_update(context, service_id, values): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): - service_ref = models.Service.find(service_id, session=session) + service_ref = session_get(context, service_id, session=session) for (key, value) in values.iteritems(): service_ref[key] = value service_ref.save(session=session) @@ -428,8 +480,8 @@ def instance_destroy(_context, instance_id): instance_ref.delete(session=session) -def instance_get(context, instance_id): - return models.Instance.find(instance_id, deleted=_deleted(context)) +def instance_get(context, instance_id, session=None): + return models.Instance.find(instance_id, session=session, deleted=_deleted(context)) def instance_get_all(context): @@ -810,8 +862,10 @@ def quota_destroy(_context, project_id): ################### -def volume_allocate_shelf_and_blade(_context, volume_id): - # TODO(devcamcar): Make admin only +def volume_allocate_shelf_and_blade(context, volume_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): export_device = session.query(models.ExportDevice @@ -828,15 +882,17 @@ def volume_allocate_shelf_and_blade(_context, volume_id): return (export_device.shelf_id, export_device.blade_id) -def volume_attached(_context, volume_id, instance_id, mountpoint): +def volume_attached(context, volume_id, instance_id, mountpoint): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): - volume_ref = models.Volume.find(volume_id, session=session) + volume_ref = volume_get(context, volume_id, session=session) volume_ref['status'] = 'in-use' volume_ref['mountpoint'] = mountpoint volume_ref['attach_status'] = 'attached' - volume_ref.instance = models.Instance.find(instance_id, - session=session) + volume_ref.instance = instance_get(context, instance_id, session=session) volume_ref.save(session=session) @@ -855,7 +911,10 @@ def volume_create(context, values): return volume_ref -def volume_data_get_for_project(_context, project_id): +def volume_data_get_for_project(context, project_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() result = session.query(func.count(models.Volume.id), func.sum(models.Volume.size) @@ -866,7 +925,10 @@ def volume_data_get_for_project(_context, project_id): return (result[0] or 0, result[1] or 0) -def volume_destroy(_context, volume_id): +def volume_destroy(context, volume_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): # TODO(vish): do we have to use sql here? @@ -878,6 +940,9 @@ def volume_destroy(_context, volume_id): def volume_detached(context, volume_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): volume_ref = volume_get(context, volume_id, session=session) @@ -914,10 +979,12 @@ def volume_get(context, volume_id, session=None): def volume_get_all(context): - if is_admin_context(context): - return models.Volume.all(deleted=_deleted(context)) + if not is_admin_context(context): + raise exception.NotAuthorized() - raise exception.NotAuthorized() + return session.query(models.Volume + ).filter_by(deleted=_deleted(context) + ).all() def volume_get_all_by_project(context, project_id): diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 1b9edf475..b9bb8e4f2 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -176,21 +176,6 @@ class Service(BASE, NovaBase): report_count = Column(Integer, nullable=False, default=0) disabled = Column(Boolean, default=False) - @classmethod - def find_by_args(cls, host, binary, session=None, deleted=False): - if not session: - session = get_session() - try: - return session.query(cls - ).filter_by(host=host - ).filter_by(binary=binary - ).filter_by(deleted=deleted - ).one() - except exc.NoResultFound: - new_exc = exception.NotFound("No model for %s, %s" % (host, - binary)) - raise new_exc.__class__, new_exc, sys.exc_info()[2] - class Instance(BASE, NovaBase): """Represents a guest vm""" -- cgit From 35741ff23bec2b4f301b93128fd018e9c8e70945 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 29 Sep 2010 17:06:35 -0500 Subject: Servers stuff --- nova/api/rackspace/servers.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 357e0895f..40cd4f691 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -26,6 +26,8 @@ from nova import utils from nova import wsgi from nova.api import cloud from nova.api.rackspace import _id_translator +from nova.api.rackspace import context +from nova.api.rackspace import faults from nova.compute import instance_types from nova.compute import power_state from nova.wsgi import Serializer @@ -254,10 +256,38 @@ class Controller(wsgi.Controller): ref = self.db_driver.instance_create(None, inst) inst['id'] = inst_id_trans.to_rs_id(ref.ec2_id) - #self.network_manager = utils.import_object(FLAGS.rs_network_manager) - # - #address = self.network_manager.allocate_fixed_ip( None, inst['id']) + # TODO(dietz): this isn't explicitly necessary, but the networking + # calls depend on an object with a project_id property + context = context.APIRequestContext(user_id) + + inst['mac_address'] = utils.generate_mac() + + #TODO(dietz) is this necessary? + inst['launch_index'] = 0 + + inst['hostname'] = instance_ref['ec2_id'] + self.db_driver.instance_update(None, inst_id, inst) + self.network_manager = utils.import_object(FLAGS.rs_network_manager) + address = self.network_manager.allocate_fixed_ip(context, inst_id) + + # TODO(vish): This probably should be done in the scheduler + # network is setup when host is assigned + network_topic = self._get_network_topic(user_id) + rpc.call(network_topic, + {"method": "setup_fixed_ip", + "args": {"context": None, + "address": address}}) return inst - + def _get_network_topic(self, user_id): + """Retrieves the network host for a project""" + network_ref = self.db_driver.project_get_network(None, + user_id) + host = network_ref['host'] + if not host: + host = rpc.call(FLAGS.network_topic, + {"method": "set_network_host", + "args": {"context": None, + "project_id": user_id}}) + return self.db_driver.queue_get_for(None, FLAGS.network_topic, host) -- cgit From 33f101c309852f358ab30a8af64ef64a848f16ae Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 29 Sep 2010 17:36:26 -0500 Subject: Some minor cleanup --- nova/api/rackspace/servers.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index c3d56debd..0d285999b 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -213,14 +213,11 @@ class Controller(wsgi.Controller): user_id = req.environ['nova.context']['user']['id'] - instance_type, flavor = None, None - for k, v in instance_types.INSTANCE_TYPES.iteritems(): - if v['flavorid'] == env['server']['flavorId']: - instance_type, flavor = k, v - break + flavor_id = env['server']['flavorId'] - if not flavor: - raise Exception, "Flavor not found" + instance_type, flavor = [(k, v) for k, v in + instance_types.INSTANCE_TYPES.iteritems() + if v['flavorid'] == flavor_id][0] image_id = env['server']['imageId'] @@ -262,10 +259,10 @@ class Controller(wsgi.Controller): ref = self.db_driver.instance_create(None, inst) inst['id'] = inst_id_trans.to_rs_id(ref.ec2_id) - # TODO(dietz): this isn't explicitly necessary, but the networking - # calls depend on an object with a project_id property + # calls depend on an object with a project_id property, and therefore + # should be cleaned up later api_context = context.APIRequestContext(user_id) inst['mac_address'] = utils.generate_mac() -- cgit From 41e940a5cfa62d56e7d4f111827217c64f0ec61d Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 29 Sep 2010 17:40:46 -0500 Subject: Forgot the context module --- nova/api/rackspace/context.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 nova/api/rackspace/context.py diff --git a/nova/api/rackspace/context.py b/nova/api/rackspace/context.py new file mode 100644 index 000000000..3a7941f0f --- /dev/null +++ b/nova/api/rackspace/context.py @@ -0,0 +1,32 @@ +# 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): + """ This is an adapter class to get around all of the assumptions made in + the FlatNetworking """ + def __init__(self, user_id): + class Dummy(object): pass + self.user_id = user_id + self.project = Dummy() + self.project.id = self.user_id -- cgit From 6b932780b6bf10b387ad04be6ec88395cae6b564 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 29 Sep 2010 17:52:15 -0500 Subject: pylint and pep8 cleanup --- nova/api/rackspace/context.py | 8 +++++--- nova/api/rackspace/servers.py | 8 +++----- nova/tests/api/rackspace/flavors.py | 1 - 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/nova/api/rackspace/context.py b/nova/api/rackspace/context.py index 3a7941f0f..924ee151e 100644 --- a/nova/api/rackspace/context.py +++ b/nova/api/rackspace/context.py @@ -22,11 +22,13 @@ APIRequestContext import random +class Project(object): + def __init__(self, user_id): + self.id = user_id + class APIRequestContext(object): """ This is an adapter class to get around all of the assumptions made in the FlatNetworking """ def __init__(self, user_id): - class Dummy(object): pass self.user_id = user_id - self.project = Dummy() - self.project.id = self.user_id + self.project = Project(user_id) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 0d285999b..965deb402 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -15,7 +15,6 @@ # License for the specific language governing permissions and limitations # under the License. -import datetime import time import webob @@ -31,7 +30,6 @@ from nova.api.rackspace import context from nova.api.rackspace import faults from nova.compute import instance_types from nova.compute import power_state -from nova.wsgi import Serializer import nova.api.rackspace import nova.image.service @@ -201,7 +199,7 @@ class Controller(wsgi.Controller): reboot_type = input_dict['reboot']['type'] except Exception: raise faults.Fault(webob.exc.HTTPNotImplemented()) - opaque_id = _instance_id_translator().from_rsapi_id(id) + opaque_id = _instance_id_translator().from_rs_id(id) cloud.reboot(opaque_id) def _build_server_instance(self, req, env): @@ -273,8 +271,8 @@ class Controller(wsgi.Controller): inst['hostname'] = ref.ec2_id self.db_driver.instance_update(None, inst['id'], inst) - self.network_manager = utils.import_object(FLAGS.rs_network_manager) - address = self.network_manager.allocate_fixed_ip(api_context, + network_manager = utils.import_object(FLAGS.rs_network_manager) + address = network_manager.allocate_fixed_ip(api_context, inst['id']) # TODO(vish): This probably should be done in the scheduler diff --git a/nova/tests/api/rackspace/flavors.py b/nova/tests/api/rackspace/flavors.py index 7bd1ea1c4..d25a2e2be 100644 --- a/nova/tests/api/rackspace/flavors.py +++ b/nova/tests/api/rackspace/flavors.py @@ -38,7 +38,6 @@ class FlavorsTest(unittest.TestCase): def test_get_flavor_list(self): req = webob.Request.blank('/v1.0/flavors') res = req.get_response(nova.api.API()) - print res def test_get_flavor_by_id(self): pass -- cgit From 13a73f2606f4b9dee4e51cccbb7e48c8ce322b76 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 29 Sep 2010 18:00:24 -0500 Subject: Missed a few attributes while mirroring the ec2 instance spin up --- nova/api/rackspace/servers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 965deb402..2395cb358 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -239,6 +239,9 @@ class Controller(wsgi.Controller): inst['ramdisk_id'] = image.get('ramdiskId', FLAGS.default_ramdisk) inst['reservation_id'] = utils.generate_uid('r') + inst['display_name'] = env['server']['name'] + inst['display_description'] = env['server']['name'] + #TODO(dietz) this may be ill advised key_pair_ref = self.db_driver.key_pair_get_all_by_user( None, user_id)[0] -- cgit From b075b504a0a402fc4e8c24379804633139883008 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 29 Sep 2010 19:28:14 -0500 Subject: Whoops, forgot the exception handling bit --- nova/api/rackspace/servers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 2395cb358..11efd8aef 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -160,11 +160,10 @@ class Controller(wsgi.Controller): if not env: return faults.Fault(exc.HTTPUnprocessableEntity()) - #try: - inst = self._build_server_instance(req, env) - #except Exception, e: - # print e - # return exc.HTTPUnprocessableEntity() + try: + inst = self._build_server_instance(req, env) + except Exception, e: + return faults.Fault(exc.HTTPUnprocessableEntity()) rpc.cast( FLAGS.compute_topic, { -- cgit From 734df1fbad8195e7cd7072d0d0aeb5b94841f121 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Wed, 29 Sep 2010 19:09:00 -0700 Subject: Made network tests pass again --- nova/db/api.py | 1 - nova/db/sqlalchemy/api.py | 233 +++++++++++++++++++++++++++++------------ nova/db/sqlalchemy/models.py | 26 ----- nova/network/manager.py | 3 +- nova/tests/network_unittest.py | 1 + 5 files changed, 170 insertions(+), 94 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index b68a0fe8f..4cfdd788c 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -175,7 +175,6 @@ def floating_ip_get_by_address(context, address): return IMPL.floating_ip_get_by_address(context, address) -def floating_ip_get_instance(context, address): """Get an instance for a floating ip by address.""" return IMPL.floating_ip_get_instance(context, address) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 01a5af38b..d129df2be 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -234,7 +234,13 @@ def service_update(context, service_id, values): ################### -def floating_ip_allocate_address(_context, host, project_id): +def floating_ip_allocate_address(context, host, project_id): + if is_user_context(context): + if context.project.id != project_id: + raise exception.NotAuthorized() + elif not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): floating_ip_ref = session.query(models.FloatingIp @@ -253,7 +259,10 @@ def floating_ip_allocate_address(_context, host, project_id): return floating_ip_ref['address'] -def floating_ip_create(_context, values): +def floating_ip_create(context, values): + if not is_user_context(context) and not is_admin_context(context): + raise exception.NotAuthorized() + floating_ip_ref = models.FloatingIp() for (key, value) in values.iteritems(): floating_ip_ref[key] = value @@ -261,7 +270,13 @@ def floating_ip_create(_context, values): return floating_ip_ref['address'] -def floating_ip_count_by_project(_context, project_id): +def floating_ip_count_by_project(context, project_id): + if is_user_context(context): + if context.project.id != project_id: + raise exception.NotAuthorized() + elif not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() return session.query(models.FloatingIp ).filter_by(project_id=project_id @@ -269,39 +284,63 @@ def floating_ip_count_by_project(_context, project_id): ).count() -def floating_ip_fixed_ip_associate(_context, floating_address, fixed_address): +#@require_context +def floating_ip_fixed_ip_associate(context, floating_address, fixed_address): + if not is_user_context(context) and not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): - floating_ip_ref = models.FloatingIp.find_by_str(floating_address, - session=session) - fixed_ip_ref = models.FixedIp.find_by_str(fixed_address, - session=session) + # TODO(devcamcar): How to ensure floating_id belongs to user? + floating_ip_ref = floating_ip_get_by_address(context, + floating_address, + session=session) + fixed_ip_ref = fixed_ip_get_by_address(context, + fixed_address, + session=session) floating_ip_ref.fixed_ip = fixed_ip_ref floating_ip_ref.save(session=session) -def floating_ip_deallocate(_context, address): +#@require_context +def floating_ip_deallocate(context, address): + if not is_user_context(context) and not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): - floating_ip_ref = models.FloatingIp.find_by_str(address, - session=session) + # TODO(devcamcar): How to ensure floating id belongs to user? + floating_ip_ref = floating_ip_get_by_address(context, + address, + session=session) floating_ip_ref['project_id'] = None floating_ip_ref.save(session=session) +#@require_context +def floating_ip_destroy(context, address): + if not is_user_context(context) and not is_admin_context(context): + raise exception.NotAuthorized() -def floating_ip_destroy(_context, address): session = get_session() with session.begin(): - floating_ip_ref = models.FloatingIp.find_by_str(address, - session=session) + # TODO(devcamcar): Ensure address belongs to user. + floating_ip_ref = get_floating_ip_by_address(context, + address, + session=session) floating_ip_ref.delete(session=session) -def floating_ip_disassociate(_context, address): +def floating_ip_disassociate(context, address): + if not is_user_context(context) and is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): - floating_ip_ref = models.FloatingIp.find_by_str(address, - session=session) + # TODO(devcamcar): Ensure address belongs to user. + # Does get_floating_ip_by_address handle this? + floating_ip_ref = floating_ip_get_by_address(context, + address, + session=session) fixed_ip_ref = floating_ip_ref.fixed_ip if fixed_ip_ref: fixed_ip_address = fixed_ip_ref['address'] @@ -311,16 +350,22 @@ def floating_ip_disassociate(_context, address): floating_ip_ref.save(session=session) return fixed_ip_address +#@require_admin_context +def floating_ip_get_all(context): + if not is_admin_context(context): + raise exception.NotAuthorized() -def floating_ip_get_all(_context): session = get_session() return session.query(models.FloatingIp ).options(joinedload_all('fixed_ip.instance') ).filter_by(deleted=False ).all() +#@require_admin_context +def floating_ip_get_all_by_host(context, host): + if not is_admin_context(context): + raise exception.NotAuthorized() -def floating_ip_get_all_by_host(_context, host): session = get_session() return session.query(models.FloatingIp ).options(joinedload_all('fixed_ip.instance') @@ -328,7 +373,15 @@ def floating_ip_get_all_by_host(_context, host): ).filter_by(deleted=False ).all() -def floating_ip_get_all_by_project(_context, project_id): +#@require_context +def floating_ip_get_all_by_project(context, project_id): + # TODO(devcamcar): Change to decorate and check project_id separately. + if is_user_context(context): + if context.project.id != project_id: + raise exception.NotAuthorized() + elif not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() return session.query(models.FloatingIp ).options(joinedload_all('fixed_ip.instance') @@ -336,22 +389,38 @@ def floating_ip_get_all_by_project(_context, project_id): ).filter_by(deleted=False ).all() -def floating_ip_get_by_address(_context, address): - return models.FloatingIp.find_by_str(address) +#@require_context +def floating_ip_get_by_address(context, address, session=None): + # TODO(devcamcar): Ensure the address belongs to user. + if not is_user_context(context) and not is_admin_context(context): + raise exception.NotAuthorized() + + if not session: + session = get_session() + + result = session.query(models.FloatingIp + ).filter_by(address=address + ).filter_by(deleted=_deleted(context) + ).first() + if not result: + raise exception.NotFound('No fixed ip for address %s' % address) + return result -def floating_ip_get_instance(_context, address): - session = get_session() - with session.begin(): - floating_ip_ref = models.FloatingIp.find_by_str(address, - session=session) - return floating_ip_ref.fixed_ip.instance + + # floating_ip_ref = get_floating_ip_by_address(context, + # address, + # session=session) + # return floating_ip_ref.fixed_ip.instance ################### +#@require_context +def fixed_ip_associate(context, address, instance_id): + if not is_user_context(context) and not is_admin_context(context): + raise exception.NotAuthorized() -def fixed_ip_associate(_context, address, instance_id): session = get_session() with session.begin(): fixed_ip_ref = session.query(models.FixedIp @@ -364,12 +433,17 @@ def fixed_ip_associate(_context, address, instance_id): # then this has concurrency issues if not fixed_ip_ref: raise db.NoMoreAddresses() - fixed_ip_ref.instance = models.Instance.find(instance_id, - session=session) + fixed_ip_ref.instance = instance_get(context, + instance_id, + session=session) session.add(fixed_ip_ref) -def fixed_ip_associate_pool(_context, network_id, instance_id): +#@require_admin_context +def fixed_ip_associate_pool(context, network_id, instance_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): network_or_none = or_(models.FixedIp.network_id == network_id, @@ -386,14 +460,16 @@ def fixed_ip_associate_pool(_context, network_id, instance_id): if not fixed_ip_ref: raise db.NoMoreAddresses() if not fixed_ip_ref.network: - fixed_ip_ref.network = models.Network.find(network_id, - session=session) - fixed_ip_ref.instance = models.Instance.find(instance_id, - session=session) + fixed_ip_ref.network = network_get(context, + network_id, + session=session) + fixed_ip_ref.instance = instance_get(context, + instance_id, + session=session) session.add(fixed_ip_ref) return fixed_ip_ref['address'] - +#@require_context def fixed_ip_create(_context, values): fixed_ip_ref = models.FixedIp() for (key, value) in values.iteritems(): @@ -401,45 +477,56 @@ def fixed_ip_create(_context, values): fixed_ip_ref.save() return fixed_ip_ref['address'] - -def fixed_ip_disassociate(_context, address): +#@require_context +def fixed_ip_disassociate(context, address): session = get_session() with session.begin(): - fixed_ip_ref = models.FixedIp.find_by_str(address, session=session) + fixed_ip_ref = fixed_ip_get_by_address(context, + address, + session=session) fixed_ip_ref.instance = None fixed_ip_ref.save(session=session) -def fixed_ip_get_by_address(_context, address): - session = get_session() - with session.begin(): - try: - return session.query(models.FixedIp - ).options(joinedload_all('instance') - ).filter_by(address=address - ).filter_by(deleted=False - ).one() - except exc.NoResultFound: - new_exc = exception.NotFound("No model for address %s" % address) - raise new_exc.__class__, new_exc, sys.exc_info()[2] - - -def fixed_ip_get_instance(_context, address): - session = get_session() - with session.begin(): - return models.FixedIp.find_by_str(address, session=session).instance +#@require_context +def fixed_ip_get_by_address(context, address, session=None): + # TODO(devcamcar): Ensure floating ip belongs to user. + # Only possible if it is associated with an instance. + # May have to use system context for this always. + if not session: + session = get_session() + result = session.query(models.FixedIp + ).filter_by(address=address + ).filter_by(deleted=_deleted(context) + ).options(joinedload('network') + ).options(joinedload('instance') + ).first() + if not result: + raise exception.NotFound('No floating ip for address %s' % address) + + return result + + +#@require_context +def fixed_ip_get_instance(context, address): + fixed_ip_ref = fixed_ip_get_by_address(context, address) + return fixed_ip_ref.instance -def fixed_ip_get_network(_context, address): - session = get_session() - with session.begin(): - return models.FixedIp.find_by_str(address, session=session).network +#@require_admin_context +def fixed_ip_get_network(context, address): + fixed_ip_ref = fixed_ip_get_by_address(context, address) + return fixed_ip_ref.network -def fixed_ip_update(_context, address, values): + +#@require_context +def fixed_ip_update(context, address, values): session = get_session() with session.begin(): - fixed_ip_ref = models.FixedIp.find_by_str(address, session=session) + fixed_ip_ref = fixed_ip_get_by_address(context, + address, + session=session) for (key, value) in values.iteritems(): fixed_ip_ref[key] = value fixed_ip_ref.save(session=session) @@ -462,7 +549,9 @@ def instance_create(_context, values): instance_ref.save(session=session) return instance_ref + def instance_data_get_for_project(_context, project_id): + # TODO(devmcar): Admin only session = get_session() result = session.query(func.count(models.Instance.id), func.sum(models.Instance.vcpus) @@ -474,6 +563,7 @@ def instance_data_get_for_project(_context, project_id): def instance_destroy(_context, instance_id): + # TODO(devcamcar): Support user context session = get_session() with session.begin(): instance_ref = models.Instance.find(instance_id, session=session) @@ -481,17 +571,21 @@ def instance_destroy(_context, instance_id): def instance_get(context, instance_id, session=None): + # TODO(devcamcar): Support user context return models.Instance.find(instance_id, session=session, deleted=_deleted(context)) def instance_get_all(context): + # TODO(devcamcar): Admin only session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') ).filter_by(deleted=_deleted(context) ).all() + def instance_get_all_by_user(context, user_id): + # TODO(devcamcar): Admin only session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -499,7 +593,9 @@ def instance_get_all_by_user(context, user_id): ).filter_by(user_id=user_id ).all() + def instance_get_all_by_project(context, project_id): + # TODO(devcamcar): Support user context session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -509,6 +605,7 @@ def instance_get_all_by_project(context, project_id): def instance_get_all_by_reservation(_context, reservation_id): + # TODO(devcamcar): Support user context session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -518,6 +615,7 @@ def instance_get_all_by_reservation(_context, reservation_id): def instance_get_by_ec2_id(context, ec2_id): + # TODO(devcamcar): Support user context session = get_session() instance_ref = session.query(models.Instance ).filter_by(ec2_id=ec2_id @@ -536,6 +634,7 @@ def instance_ec2_id_exists(context, ec2_id, session=None): def instance_get_fixed_address(_context, instance_id): + # TODO(devcamcar): Support user context session = get_session() with session.begin(): instance_ref = models.Instance.find(instance_id, session=session) @@ -545,6 +644,7 @@ def instance_get_fixed_address(_context, instance_id): def instance_get_floating_address(_context, instance_id): + # TODO(devcamcar): Support user context session = get_session() with session.begin(): instance_ref = models.Instance.find(instance_id, session=session) @@ -557,6 +657,7 @@ def instance_get_floating_address(_context, instance_id): def instance_is_vpn(context, instance_id): + # TODO(devcamcar): Admin only # TODO(vish): Move this into image code somewhere instance_ref = instance_get(context, instance_id) return instance_ref['image_id'] == FLAGS.vpn_image_id @@ -683,8 +784,8 @@ def network_destroy(_context, network_id): {'id': network_id}) -def network_get(_context, network_id): - return models.Network.find(network_id) +def network_get(_context, network_id, session=None): + return models.Network.find(network_id, session=session) # NOTE(vish): pylint complains because of the long method name, but diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index b9bb8e4f2..7a085c4df 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -441,19 +441,6 @@ class FixedIp(BASE, NovaBase): def str_id(self): return self.address - @classmethod - def find_by_str(cls, str_id, session=None, deleted=False): - if not session: - session = get_session() - try: - return session.query(cls - ).filter_by(address=str_id - ).filter_by(deleted=deleted - ).one() - except exc.NoResultFound: - new_exc = exception.NotFound("No model for address %s" % str_id) - raise new_exc.__class__, new_exc, sys.exc_info()[2] - class FloatingIp(BASE, NovaBase): """Represents a floating ip that dynamically forwards to a fixed ip""" @@ -469,19 +456,6 @@ class FloatingIp(BASE, NovaBase): project_id = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) - @classmethod - def find_by_str(cls, str_id, session=None, deleted=False): - if not session: - session = get_session() - try: - return session.query(cls - ).filter_by(address=str_id - ).filter_by(deleted=deleted - ).one() - except exc.NoResultFound: - new_exc = exception.NotFound("No model for address %s" % str_id) - raise new_exc.__class__, new_exc, sys.exc_info()[2] - def register_models(): """Register Models and create metadata""" diff --git a/nova/network/manager.py b/nova/network/manager.py index a7126ea4f..d125d28d8 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -232,7 +232,8 @@ class VlanManager(NetworkManager): address = network_ref['vpn_private_address'] self.db.fixed_ip_associate(context, address, instance_id) else: - address = self.db.fixed_ip_associate_pool(context, + # TODO(devcamcar) Pass system context here. + address = self.db.fixed_ip_associate_pool(None, network_ref['id'], instance_id) self.db.fixed_ip_update(context, address, {'allocated': True}) diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index da65b50a2..110e8430c 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -84,6 +84,7 @@ class NetworkTestCase(test.TrialTestCase): def test_public_network_association(self): """Makes sure that we can allocaate a public ip""" # TODO(vish): better way of adding floating ips + self.context.project = self.projects[0] pubnet = IPy.IP(flags.FLAGS.public_range) address = str(pubnet[0]) try: -- cgit From d32d95e08d67084ea04ccd1565ce6faffb1766ce Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Wed, 29 Sep 2010 20:29:55 -0700 Subject: Finished instance context auth --- nova/db/sqlalchemy/api.py | 185 ++++++++++++++++++++++++++++++----------- nova/tests/compute_unittest.py | 2 + nova/tests/network_unittest.py | 4 +- 3 files changed, 141 insertions(+), 50 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index d129df2be..9ab53b89b 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -41,9 +41,10 @@ FLAGS = flags.FLAGS # pylint: disable-msg=C0111 def _deleted(context): """Calculates whether to include deleted objects based on context. - - Currently just looks for a flag called deleted in the context dict. + Currently just looks for a flag called deleted in the context dict. """ + if is_user_context(context): + return False if not hasattr(context, 'get'): return False return context.get('deleted', False) @@ -69,7 +70,7 @@ def is_user_context(context): ################### - +#@require_admin_context def service_destroy(context, service_id): if not is_admin_context(context): raise exception.NotAuthorized() @@ -80,6 +81,7 @@ def service_destroy(context, service_id): service_ref.delete(session=session) +#@require_admin_context def service_get(context, service_id, session=None): if not is_admin_context(context): raise exception.NotAuthorized() @@ -98,6 +100,7 @@ def service_get(context, service_id, session=None): return result +#@require_admin_context def service_get_all_by_topic(context, topic): if not is_admin_context(context): raise exception.NotAuthorized() @@ -110,6 +113,7 @@ def service_get_all_by_topic(context, topic): ).all() +#@require_admin_context def _service_get_all_topic_subquery(context, session, topic, subq, label): if not is_admin_context(context): raise exception.NotAuthorized() @@ -124,6 +128,7 @@ def _service_get_all_topic_subquery(context, session, topic, subq, label): ).all() +#@require_admin_context def service_get_all_compute_sorted(context): if not is_admin_context(context): raise exception.NotAuthorized() @@ -151,6 +156,7 @@ def service_get_all_compute_sorted(context): label) +#@require_admin_context def service_get_all_network_sorted(context): if not is_admin_context(context): raise exception.NotAuthorized() @@ -171,6 +177,7 @@ def service_get_all_network_sorted(context): label) +#@require_admin_context def service_get_all_volume_sorted(context): if not is_admin_context(context): raise exception.NotAuthorized() @@ -191,6 +198,7 @@ def service_get_all_volume_sorted(context): label) +#@require_admin_context def service_get_by_args(context, host, binary): if not is_admin_context(context): raise exception.NotAuthorized() @@ -208,6 +216,7 @@ def service_get_by_args(context, host, binary): return result +#@require_admin_context def service_create(context, values): if not is_admin_context(context): return exception.NotAuthorized() @@ -219,6 +228,7 @@ def service_create(context, values): return service_ref +#@require_admin_context def service_update(context, service_id, values): if not is_admin_context(context): raise exception.NotAuthorized() @@ -234,12 +244,11 @@ def service_update(context, service_id, values): ################### +#@require_context def floating_ip_allocate_address(context, host, project_id): if is_user_context(context): if context.project.id != project_id: raise exception.NotAuthorized() - elif not is_admin_context(context): - raise exception.NotAuthorized() session = get_session() with session.begin(): @@ -259,6 +268,7 @@ def floating_ip_allocate_address(context, host, project_id): return floating_ip_ref['address'] +#@require_context def floating_ip_create(context, values): if not is_user_context(context) and not is_admin_context(context): raise exception.NotAuthorized() @@ -270,12 +280,11 @@ def floating_ip_create(context, values): return floating_ip_ref['address'] +#@require_context def floating_ip_count_by_project(context, project_id): if is_user_context(context): if context.project.id != project_id: raise exception.NotAuthorized() - elif not is_admin_context(context): - raise exception.NotAuthorized() session = get_session() return session.query(models.FloatingIp @@ -316,6 +325,7 @@ def floating_ip_deallocate(context, address): floating_ip_ref['project_id'] = None floating_ip_ref.save(session=session) + #@require_context def floating_ip_destroy(context, address): if not is_user_context(context) and not is_admin_context(context): @@ -330,6 +340,7 @@ def floating_ip_destroy(context, address): floating_ip_ref.delete(session=session) +#@require_context def floating_ip_disassociate(context, address): if not is_user_context(context) and is_admin_context(context): raise exception.NotAuthorized() @@ -350,6 +361,7 @@ def floating_ip_disassociate(context, address): floating_ip_ref.save(session=session) return fixed_ip_address + #@require_admin_context def floating_ip_get_all(context): if not is_admin_context(context): @@ -361,6 +373,7 @@ def floating_ip_get_all(context): ).filter_by(deleted=False ).all() + #@require_admin_context def floating_ip_get_all_by_host(context, host): if not is_admin_context(context): @@ -373,6 +386,7 @@ def floating_ip_get_all_by_host(context, host): ).filter_by(deleted=False ).all() + #@require_context def floating_ip_get_all_by_project(context, project_id): # TODO(devcamcar): Change to decorate and check project_id separately. @@ -389,6 +403,7 @@ def floating_ip_get_all_by_project(context, project_id): ).filter_by(deleted=False ).all() + #@require_context def floating_ip_get_by_address(context, address, session=None): # TODO(devcamcar): Ensure the address belongs to user. @@ -408,14 +423,9 @@ def floating_ip_get_by_address(context, address, session=None): return result - # floating_ip_ref = get_floating_ip_by_address(context, - # address, - # session=session) - # return floating_ip_ref.fixed_ip.instance - - ################### + #@require_context def fixed_ip_associate(context, address, instance_id): if not is_user_context(context) and not is_admin_context(context): @@ -469,6 +479,7 @@ def fixed_ip_associate_pool(context, network_id, instance_id): session.add(fixed_ip_ref) return fixed_ip_ref['address'] + #@require_context def fixed_ip_create(_context, values): fixed_ip_ref = models.FixedIp() @@ -477,6 +488,7 @@ def fixed_ip_create(_context, values): fixed_ip_ref.save() return fixed_ip_ref['address'] + #@require_context def fixed_ip_disassociate(context, address): session = get_session() @@ -535,7 +547,8 @@ def fixed_ip_update(context, address, values): ################### -def instance_create(_context, values): +#@require_context +def instance_create(context, values): instance_ref = models.Instance() for (key, value) in values.iteritems(): instance_ref[key] = value @@ -544,14 +557,14 @@ def instance_create(_context, values): with session.begin(): while instance_ref.ec2_id == None: ec2_id = utils.generate_uid(instance_ref.__prefix__) - if not instance_ec2_id_exists(_context, ec2_id, session=session): + if not instance_ec2_id_exists(context, ec2_id, session=session): instance_ref.ec2_id = ec2_id instance_ref.save(session=session) return instance_ref -def instance_data_get_for_project(_context, project_id): - # TODO(devmcar): Admin only +#@require_admin_context +def instance_data_get_for_project(context, project_id): session = get_session() result = session.query(func.count(models.Instance.id), func.sum(models.Instance.vcpus) @@ -562,21 +575,42 @@ def instance_data_get_for_project(_context, project_id): return (result[0] or 0, result[1] or 0) -def instance_destroy(_context, instance_id): - # TODO(devcamcar): Support user context +#@require_context +def instance_destroy(context, instance_id): session = get_session() with session.begin(): - instance_ref = models.Instance.find(instance_id, session=session) + instance_ref = instance_get(context, instance_id, session=session) instance_ref.delete(session=session) +#@require_context def instance_get(context, instance_id, session=None): - # TODO(devcamcar): Support user context - return models.Instance.find(instance_id, session=session, deleted=_deleted(context)) + if not session: + session = get_session() + result = None + + if is_admin_context(context): + result = session.query(models.Instance + ).filter_by(id=instance_id + ).filter_by(deleted=_deleted(context) + ).first() + elif is_user_context(context): + result = session.query(models.Instance + ).filter_by(project_id=context.project.id + ).filter_by(id=instance_id + ).filter_by(deleted=False + ).first() + if not result: + raise exception.NotFound('No instance for id %s' % instance_id) + + return result +#@require_admin_context def instance_get_all(context): - # TODO(devcamcar): Admin only + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -584,8 +618,11 @@ def instance_get_all(context): ).all() +#@require_admin_context def instance_get_all_by_user(context, user_id): - # TODO(devcamcar): Admin only + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -594,8 +631,12 @@ def instance_get_all_by_user(context, user_id): ).all() +#@require_context def instance_get_all_by_project(context, project_id): - # TODO(devcamcar): Support user context + if is_user_context(context): + if context.project.id != project_id: + raise exception.NotAuthorized() + session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -604,50 +645,68 @@ def instance_get_all_by_project(context, project_id): ).all() -def instance_get_all_by_reservation(_context, reservation_id): - # TODO(devcamcar): Support user context +#@require_context +def instance_get_all_by_reservation(context, reservation_id): session = get_session() - return session.query(models.Instance - ).options(joinedload_all('fixed_ip.floating_ips') - ).filter_by(reservation_id=reservation_id - ).filter_by(deleted=False - ).all() + + if is_admin_context(context): + return session.query(models.Instance + ).options(joinedload_all('fixed_ip.floating_ips') + ).filter_by(reservation_id=reservation_id + ).filter_by(deleted=_deleted(context) + ).all() + elif is_user_context(context): + return session.query(models.Instance + ).options(joinedload_all('fixed_ip.floating_ips') + ).filter_by(project_id=context.project.id + ).filter_by(reservation_id=reservation_id + ).filter_by(deleted=False + ).all() +#@require_context def instance_get_by_ec2_id(context, ec2_id): - # TODO(devcamcar): Support user context session = get_session() - instance_ref = session.query(models.Instance + + if is_admin_context(context): + result = session.query(models.Instance ).filter_by(ec2_id=ec2_id ).filter_by(deleted=_deleted(context) ).first() - if not instance_ref: + elif is_user_context(context): + result = session.query(models.Instance + ).filter_by(project_id=context.project.id + ).filter_by(ec2_id=ec2_id + ).filter_by(deleted=False + ).first() + if not result: raise exception.NotFound('Instance %s not found' % (ec2_id)) - return instance_ref + return result +#@require_context def instance_ec2_id_exists(context, ec2_id, session=None): if not session: session = get_session() return session.query(exists().where(models.Instance.id==ec2_id)).one()[0] -def instance_get_fixed_address(_context, instance_id): - # TODO(devcamcar): Support user context +#@require_context +def instance_get_fixed_address(context, instance_id): session = get_session() with session.begin(): - instance_ref = models.Instance.find(instance_id, session=session) + instance_ref = instance_get(context, instance_id, session=session) if not instance_ref.fixed_ip: return None return instance_ref.fixed_ip['address'] -def instance_get_floating_address(_context, instance_id): - # TODO(devcamcar): Support user context +#@require_context +def instance_get_floating_address(context, instance_id): session = get_session() with session.begin(): - instance_ref = models.Instance.find(instance_id, session=session) + instance_ref = instance_get(context, instance_id, session=session) if not instance_ref.fixed_ip: return None if not instance_ref.fixed_ip.floating_ips: @@ -656,14 +715,20 @@ def instance_get_floating_address(_context, instance_id): return instance_ref.fixed_ip.floating_ips[0]['address'] +#@require_admin_context def instance_is_vpn(context, instance_id): - # TODO(devcamcar): Admin only + if not is_admin_context(context): + raise exception.NotAuthorized() # TODO(vish): Move this into image code somewhere instance_ref = instance_get(context, instance_id) return instance_ref['image_id'] == FLAGS.vpn_image_id +#@require_admin_context def instance_set_state(context, instance_id, state, description=None): + if not is_admin_context(context): + raise exception.NotAuthorized() + # TODO(devcamcar): Move this out of models and into driver from nova.compute import power_state if not description: @@ -674,10 +739,11 @@ def instance_set_state(context, instance_id, state, description=None): 'state_description': description}) -def instance_update(_context, instance_id, values): +#@require_context +def instance_update(context, instance_id, values): session = get_session() with session.begin(): - instance_ref = models.Instance.find(instance_id, session=session) + instance_ref = instance_get(context, instance_id, session=session) for (key, value) in values.iteritems(): instance_ref[key] = value instance_ref.save(session=session) @@ -686,6 +752,7 @@ def instance_update(_context, instance_id, values): ################### +#@require_context def key_pair_create(_context, values): key_pair_ref = models.KeyPair() for (key, value) in values.iteritems(): @@ -694,7 +761,8 @@ def key_pair_create(_context, values): return key_pair_ref -def key_pair_destroy(_context, user_id, name): +#@require_context +def key_pair_destroy(context, user_id, name): session = get_session() with session.begin(): key_pair_ref = models.KeyPair.find_by_args(user_id, @@ -784,8 +852,27 @@ def network_destroy(_context, network_id): {'id': network_id}) -def network_get(_context, network_id, session=None): - return models.Network.find(network_id, session=session) +#@require_context +def network_get(context, network_id, session=None): + if not session: + session = get_session() + result = None + + if is_admin_context(context): + result = session.query(models.Network + ).filter_by(id=network_id + ).filter_by(deleted=_deleted(context) + ).first() + elif is_user_context(context): + result = session.query(models.Network + ).filter_by(project_id=context.project.id + ).filter_by(id=network_id + ).filter_by(deleted=False + ).first() + if not result: + raise exception.NotFound('No network for id %s' % network_id) + + return result # NOTE(vish): pylint complains because of the long method name, but @@ -1066,7 +1153,7 @@ def volume_get(context, volume_id, session=None): ).first() elif is_user_context(context): result = session.query(models.Volume - ).filter_by(project_id=context.project.project_id + ).filter_by(project_id=context.project.id ).filter_by(id=volume_id ).filter_by(deleted=False ).first() diff --git a/nova/tests/compute_unittest.py b/nova/tests/compute_unittest.py index f5c0f1c09..e705c2552 100644 --- a/nova/tests/compute_unittest.py +++ b/nova/tests/compute_unittest.py @@ -96,6 +96,8 @@ class ComputeTestCase(test.TrialTestCase): self.assertEqual(instance_ref['deleted_at'], None) terminate = datetime.datetime.utcnow() yield self.compute.terminate_instance(self.context, instance_id) + # TODO(devcamcar): Pass deleted in using system context. + # context.read_deleted ? instance_ref = db.instance_get({'deleted': True}, instance_id) self.assert_(instance_ref['launched_at'] < terminate) self.assert_(instance_ref['deleted_at'] > terminate) diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index 110e8430c..ca6a4bbc2 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -56,7 +56,9 @@ class NetworkTestCase(test.TrialTestCase): 'netuser', name)) # create the necessary network data for the project - self.network.set_network_host(self.context, self.projects[i].id) + user_context = context.APIRequestContext(project=self.projects[i], + user=self.user) + self.network.set_network_host(user_context, self.projects[i].id) instance_ref = db.instance_create(None, {'mac_address': utils.generate_mac()}) self.instance_id = instance_ref['id'] -- cgit From ea5dcda819f2656589df177331f693f945d98f4a Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Wed, 29 Sep 2010 20:35:24 -0700 Subject: Finished instance context auth --- nova/db/sqlalchemy/api.py | 32 +++++++++++++++++++++++++++++--- nova/tests/network_unittest.py | 1 + 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 9ab53b89b..2d553d98d 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -794,11 +794,21 @@ def key_pair_get_all_by_user(_context, user_id): ################### -def network_count(_context): - return models.Network.count() +#@require_admin_context +def network_count(context): + if not is_admin_context(context): + raise exception.NotAuthorized() + return session.query(models.Network + ).filter_by(deleted=deleted + ).count() + +#@require_admin_context def network_count_allocated_ips(_context, network_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() return session.query(models.FixedIp ).filter_by(network_id=network_id @@ -807,7 +817,11 @@ def network_count_allocated_ips(_context, network_id): ).count() +#@require_admin_context def network_count_available_ips(_context, network_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() return session.query(models.FixedIp ).filter_by(network_id=network_id @@ -817,7 +831,11 @@ def network_count_available_ips(_context, network_id): ).count() +#@require_admin_context def network_count_reserved_ips(_context, network_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() return session.query(models.FixedIp ).filter_by(network_id=network_id @@ -826,7 +844,11 @@ def network_count_reserved_ips(_context, network_id): ).count() +#@require_admin_context def network_create(_context, values): + if not is_admin_context(context): + raise exception.NotAuthorized() + network_ref = models.Network() for (key, value) in values.iteritems(): network_ref[key] = value @@ -834,7 +856,11 @@ def network_create(_context, values): return network_ref -def network_destroy(_context, network_id): +#@require_admin_context +def network_destroy(context, network_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): # TODO(vish): do we have to use sql here? diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index ca6a4bbc2..e01d7cff9 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -49,6 +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) + # TODO(devcamcar): Passing project=None is Bad(tm). self.context = context.APIRequestContext(project=None, user=self.user) for i in range(5): name = 'project%s' % i -- cgit From e716990fd58521f8c0166330ec9bc62c7cd91b7e Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Wed, 29 Sep 2010 20:54:15 -0700 Subject: Finished context auth for network --- nova/db/sqlalchemy/api.py | 103 ++++++++++++++++++++++++++++++++-------------- nova/network/manager.py | 3 +- 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 2d553d98d..23589b7d8 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -799,13 +799,14 @@ def network_count(context): if not is_admin_context(context): raise exception.NotAuthorized() + session = get_session() return session.query(models.Network - ).filter_by(deleted=deleted + ).filter_by(deleted=_deleted(context) ).count() #@require_admin_context -def network_count_allocated_ips(_context, network_id): +def network_count_allocated_ips(context, network_id): if not is_admin_context(context): raise exception.NotAuthorized() @@ -818,7 +819,7 @@ def network_count_allocated_ips(_context, network_id): #@require_admin_context -def network_count_available_ips(_context, network_id): +def network_count_available_ips(context, network_id): if not is_admin_context(context): raise exception.NotAuthorized() @@ -832,7 +833,7 @@ def network_count_available_ips(_context, network_id): #@require_admin_context -def network_count_reserved_ips(_context, network_id): +def network_count_reserved_ips(context, network_id): if not is_admin_context(context): raise exception.NotAuthorized() @@ -845,7 +846,7 @@ def network_count_reserved_ips(_context, network_id): #@require_admin_context -def network_create(_context, values): +def network_create(context, values): if not is_admin_context(context): raise exception.NotAuthorized() @@ -904,7 +905,11 @@ def network_get(context, network_id, session=None): # NOTE(vish): pylint complains because of the long method name, but # it fits with the names of the rest of the methods # pylint: disable-msg=C0103 -def network_get_associated_fixed_ips(_context, network_id): +#@require_admin_context +def network_get_associated_fixed_ips(context, network_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() return session.query(models.FixedIp ).options(joinedload_all('instance') @@ -914,18 +919,28 @@ def network_get_associated_fixed_ips(_context, network_id): ).all() -def network_get_by_bridge(_context, bridge): +#@require_admin_context +def network_get_by_bridge(context, bridge): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() - rv = session.query(models.Network + result = session.query(models.Network ).filter_by(bridge=bridge ).filter_by(deleted=False ).first() - if not rv: + + if not result: raise exception.NotFound('No network for bridge %s' % bridge) - return rv + + return result -def network_get_index(_context, network_id): +#@require_admin_context +def network_get_index(context, network_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): network_index = session.query(models.NetworkIndex @@ -933,19 +948,34 @@ def network_get_index(_context, network_id): ).filter_by(deleted=False ).with_lockmode('update' ).first() + if not network_index: raise db.NoMoreNetworks() - network_index['network'] = models.Network.find(network_id, - session=session) + + network_index['network'] = network_get(context, + network_id, + session=session) session.add(network_index) + return network_index['index'] -def network_index_count(_context): - return models.NetworkIndex.count() +#@require_admin_context +def network_index_count(context): + if not is_admin_context(context): + raise exception.NotAuthorized() + + session = get_session() + return session.query(models.NetworkIndex + ).filter_by(deleted=_deleted(context) + ).count() + +#@require_admin_context +def network_index_create_safe(context, values): + if not is_admin_context(context): + raise exception.NotAuthorized() -def network_index_create_safe(_context, values): network_index_ref = models.NetworkIndex() for (key, value) in values.iteritems(): network_index_ref[key] = value @@ -955,29 +985,35 @@ def network_index_create_safe(_context, values): pass -def network_set_host(_context, network_id, host_id): +#@require_admin_context +def network_set_host(context, network_id, host_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): - network = session.query(models.Network - ).filter_by(id=network_id - ).filter_by(deleted=False - ).with_lockmode('update' - ).first() - if not network: - raise exception.NotFound("Couldn't find network with %s" % - network_id) + network_ref = session.query(models.Network + ).filter_by(id=network_id + ).filter_by(deleted=False + ).with_lockmode('update' + ).first() + if not network_ref: + raise exception.NotFound('No network for id %s' % network_id) + # NOTE(vish): if with_lockmode isn't supported, as in sqlite, # then this has concurrency issues - if not network['host']: - network['host'] = host_id - session.add(network) - return network['host'] + if not network_ref['host']: + network_ref['host'] = host_id + session.add(network_ref) + + return network_ref['host'] -def network_update(_context, network_id, values): +#@require_context +def network_update(context, network_id, values): session = get_session() with session.begin(): - network_ref = models.Network.find(network_id, session=session) + network_ref = network_get(context, network_id, session=session) for (key, value) in values.iteritems(): network_ref[key] = value network_ref.save(session=session) @@ -985,7 +1021,10 @@ def network_update(_context, network_id, values): ################### - +# YOU ARE HERE. +# random idea for system user: +# ctx = context.system_user(on_behalf_of=user, read_deleted=False) +# TODO(devcamcar): Rename to network_get_all_by_project def project_get_network(_context, project_id): session = get_session() rv = session.query(models.Network diff --git a/nova/network/manager.py b/nova/network/manager.py index d125d28d8..ecf2fa2c2 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -88,7 +88,8 @@ class NetworkManager(manager.Manager): # TODO(vish): can we minimize db access by just getting the # id here instead of the ref? network_id = network_ref['id'] - host = self.db.network_set_host(context, + # TODO(devcamcar): Replace with system context + host = self.db.network_set_host(None, network_id, self.host) self._on_set_network_host(context, network_id) -- cgit From 98cac90592658773791eb15b19ed60adf0a57d96 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Thu, 30 Sep 2010 00:36:10 -0700 Subject: Completed quota context auth --- nova/db/sqlalchemy/api.py | 103 +++++++++++++++++++++++++++++++------------ nova/db/sqlalchemy/models.py | 12 ----- 2 files changed, 75 insertions(+), 40 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 23589b7d8..b225a6a88 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1021,19 +1021,22 @@ def network_update(context, network_id, values): ################### -# YOU ARE HERE. -# random idea for system user: -# ctx = context.system_user(on_behalf_of=user, read_deleted=False) -# TODO(devcamcar): Rename to network_get_all_by_project -def project_get_network(_context, project_id): + +#@require_context +def project_get_network(context, project_id): + if not is_admin_context(context) and not is_user_context(context): + raise error.NotAuthorized() + session = get_session() - rv = session.query(models.Network + result= session.query(models.Network ).filter_by(project_id=project_id ).filter_by(deleted=False ).first() - if not rv: + + if not result: raise exception.NotFound('No network for project: %s' % project_id) - return rv + + return result ################### @@ -1043,14 +1046,26 @@ def queue_get_for(_context, topic, physical_node_id): # FIXME(ja): this should be servername? return "%s.%s" % (topic, physical_node_id) + ################### -def export_device_count(_context): - return models.ExportDevice.count() +#@require_admin_context +def export_device_count(context): + if not is_admin_context(context): + raise exception.notauthorized() + + session = get_session() + return session.query(models.ExportDevice + ).filter_by(deleted=_deleted(context) + ).count() + +#@require_admin_context +def export_device_create(context, values): + if not is_admin_context(context): + raise exception.notauthorized() -def export_device_create(_context, values): export_device_ref = models.ExportDevice() for (key, value) in values.iteritems(): export_device_ref[key] = value @@ -1084,7 +1099,29 @@ def auth_create_token(_context, token): ################### +#@require_admin_context +def quota_get(context, project_id, session=None): + if not is_admin_context(context): + raise exception.NotAuthorized() + + if not session: + session = get_session() + + result = session.query(models.Quota + ).filter_by(project_id=project_id + ).filter_by(deleted=_deleted(context) + ).first() + if not result: + raise exception.NotFound('No quota for project_id %s' % project_id) + + return result + + +#@require_admin_context def quota_create(_context, values): + if not is_admin_context(context): + raise exception.NotAuthorized() + quota_ref = models.Quota() for (key, value) in values.iteritems(): quota_ref[key] = value @@ -1092,29 +1129,34 @@ def quota_create(_context, values): return quota_ref -def quota_get(_context, project_id): - return models.Quota.find_by_str(project_id) - +#@require_admin_context +def quota_update(context, project_id, values): + if not is_admin_context(context): + raise exception.NotAuthorized() -def quota_update(_context, project_id, values): session = get_session() with session.begin(): - quota_ref = models.Quota.find_by_str(project_id, session=session) + quota_ref = quota_get(context, project_id, session=session) for (key, value) in values.iteritems(): quota_ref[key] = value quota_ref.save(session=session) -def quota_destroy(_context, project_id): +#@require_admin_context +def quota_destroy(context, project_id): + if not is_admin_context(context): + raise exception.NotAuthorized() + session = get_session() with session.begin(): - quota_ref = models.Quota.find_by_str(project_id, session=session) + quota_ref = quota_get(context, project_id, session=session) quota_ref.delete(session=session) ################### +#@require_admin_context def volume_allocate_shelf_and_blade(context, volume_id): if not is_admin_context(context): raise exception.NotAuthorized() @@ -1135,6 +1177,7 @@ def volume_allocate_shelf_and_blade(context, volume_id): return (export_device.shelf_id, export_device.blade_id) +#@require_admin_context def volume_attached(context, volume_id, instance_id, mountpoint): if not is_admin_context(context): raise exception.NotAuthorized() @@ -1149,6 +1192,7 @@ def volume_attached(context, volume_id, instance_id, mountpoint): volume_ref.save(session=session) +#@require_context def volume_create(context, values): volume_ref = models.Volume() for (key, value) in values.iteritems(): @@ -1164,6 +1208,7 @@ def volume_create(context, values): return volume_ref +#@require_admin_context def volume_data_get_for_project(context, project_id): if not is_admin_context(context): raise exception.NotAuthorized() @@ -1178,6 +1223,7 @@ def volume_data_get_for_project(context, project_id): return (result[0] or 0, result[1] or 0) +#@require_admin_context def volume_destroy(context, volume_id): if not is_admin_context(context): raise exception.NotAuthorized() @@ -1192,6 +1238,7 @@ def volume_destroy(context, volume_id): {'id': volume_id}) +#@require_admin_context def volume_detached(context, volume_id): if not is_admin_context(context): raise exception.NotAuthorized() @@ -1206,6 +1253,7 @@ def volume_detached(context, volume_id): volume_ref.save(session=session) +#@require_context def volume_get(context, volume_id, session=None): if not session: session = get_session() @@ -1222,15 +1270,13 @@ def volume_get(context, volume_id, session=None): ).filter_by(id=volume_id ).filter_by(deleted=False ).first() - else: - raise exception.NotAuthorized() - if not result: raise exception.NotFound('No volume for id %s' % volume_id) return result +#@require_admin_context def volume_get_all(context): if not is_admin_context(context): raise exception.NotAuthorized() @@ -1239,7 +1285,7 @@ def volume_get_all(context): ).filter_by(deleted=_deleted(context) ).all() - +#@require_context def volume_get_all_by_project(context, project_id): if is_user_context(context): if context.project.id != project_id: @@ -1254,6 +1300,7 @@ def volume_get_all_by_project(context, project_id): ).all() +#@require_context def volume_get_by_ec2_id(context, ec2_id): session = get_session() result = None @@ -1278,6 +1325,7 @@ def volume_get_by_ec2_id(context, ec2_id): return result +#@require_context def volume_ec2_id_exists(context, ec2_id, session=None): if not session: session = get_session() @@ -1286,10 +1334,9 @@ def volume_ec2_id_exists(context, ec2_id, session=None): return session.query(exists( ).where(models.Volume.id==ec2_id) ).one()[0] - else: - raise exception.NotAuthorized() +#@require_context def volume_get_instance(context, volume_id): session = get_session() result = None @@ -1315,6 +1362,7 @@ def volume_get_instance(context, volume_id): return result.instance +#@require_context def volume_get_shelf_and_blade(context, volume_id): session = get_session() result = None @@ -1329,15 +1377,14 @@ def volume_get_shelf_and_blade(context, volume_id): ).filter(models.Volume.project_id==context.project.id ).filter_by(volume_id=volume_id ).first() - else: - raise exception.NotAuthorized() - if not result: - raise exception.NotFound() + raise exception.NotFound('No export device found for volume %s' % + volume_id) return (result.shelf_id, result.blade_id) +#@require_context def volume_update(context, volume_id, values): session = get_session() with session.begin(): diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 7a085c4df..76444127f 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -302,18 +302,6 @@ class Quota(BASE, NovaBase): def str_id(self): return self.project_id - @classmethod - def find_by_str(cls, str_id, session=None, deleted=False): - if not session: - session = get_session() - try: - return session.query(cls - ).filter_by(project_id=str_id - ).filter_by(deleted=deleted - ).one() - except exc.NoResultFound: - new_exc = exception.NotFound("No model for project_id %s" % str_id) - raise new_exc.__class__, new_exc, sys.exc_info()[2] class ExportDevice(BASE, NovaBase): """Represates a shelf and blade that a volume can be exported on""" -- cgit From 30541d48b17ab4626791d969388871b3a1b7758f Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Thu, 30 Sep 2010 01:07:05 -0700 Subject: Wired up context auth for keypairs --- nova/db/sqlalchemy/api.py | 46 +++++++++++++++++++++++++++++++++++--------- nova/db/sqlalchemy/models.py | 20 ------------------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index b225a6a88..302322979 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -753,7 +753,7 @@ def instance_update(context, instance_id, values): #@require_context -def key_pair_create(_context, values): +def key_pair_create(context, values): key_pair_ref = models.KeyPair() for (key, value) in values.iteritems(): key_pair_ref[key] = value @@ -763,15 +763,22 @@ def key_pair_create(_context, values): #@require_context def key_pair_destroy(context, user_id, name): + if is_user_context(context): + if context.user.id != user_id: + raise exception.NotAuthorized() + session = get_session() with session.begin(): - key_pair_ref = models.KeyPair.find_by_args(user_id, - name, - session=session) + key_pair_ref = key_pair_get(context, user_id, name, session=session) key_pair_ref.delete(session=session) -def key_pair_destroy_all_by_user(_context, user_id): +#@require_context +def key_pair_destroy_all_by_user(context, user_id): + if is_user_context(context): + if context.user.id != user_id: + raise exception.NotAuthorized() + session = get_session() with session.begin(): # TODO(vish): do we have to use sql here? @@ -779,11 +786,32 @@ def key_pair_destroy_all_by_user(_context, user_id): {'id': user_id}) -def key_pair_get(_context, user_id, name): - return models.KeyPair.find_by_args(user_id, name) +#@require_context +def key_pair_get(context, user_id, name, session=None): + if is_user_context(context): + if context.user.id != user_id: + raise exception.NotAuthorized() + + if not session: + session = get_session() + + result = session.query(models.KeyPair + ).filter_by(user_id=user_id + ).filter_by(name=name + ).filter_by(deleted=_deleted(context) + ).first() + if not result: + raise exception.NotFound('no keypair for user %s, name %s' % + (user_id, name)) + return result + +#@require_context +def key_pair_get_all_by_user(context, user_id): + if is_user_context(context): + if context.user.id != user_id: + raise exception.NotAuthorized() -def key_pair_get_all_by_user(_context, user_id): session = get_session() return session.query(models.KeyPair ).filter_by(user_id=user_id @@ -1118,7 +1146,7 @@ def quota_get(context, project_id, session=None): #@require_admin_context -def quota_create(_context, values): +def quota_create(context, values): if not is_admin_context(context): raise exception.NotAuthorized() diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 76444127f..1f5bdf9f5 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -332,26 +332,6 @@ class KeyPair(BASE, NovaBase): def str_id(self): return '%s.%s' % (self.user_id, self.name) - @classmethod - def find_by_str(cls, str_id, session=None, deleted=False): - user_id, _sep, name = str_id.partition('.') - return cls.find_by_str(user_id, name, session, deleted) - - @classmethod - def find_by_args(cls, user_id, name, session=None, deleted=False): - if not session: - session = get_session() - try: - return session.query(cls - ).filter_by(user_id=user_id - ).filter_by(name=name - ).filter_by(deleted=deleted - ).one() - except exc.NoResultFound: - new_exc = exception.NotFound("No model for user %s, name %s" % - (user_id, name)) - raise new_exc.__class__, new_exc, sys.exc_info()[2] - class Network(BASE, NovaBase): """Represents a network""" -- cgit From 336523b36ceb8f5302acd443b7f1171b67575f73 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Thu, 30 Sep 2010 01:11:16 -0700 Subject: Removed deprecated bits from NovaBase --- nova/db/sqlalchemy/models.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 1f5bdf9f5..a29090c60 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -50,44 +50,6 @@ class NovaBase(object): deleted_at = Column(DateTime) deleted = Column(Boolean, default=False) - @classmethod - def all(cls, session=None, deleted=False): - """Get all objects of this type""" - if not session: - session = get_session() - return session.query(cls - ).filter_by(deleted=deleted - ).all() - - @classmethod - def count(cls, session=None, deleted=False): - """Count objects of this type""" - if not session: - session = get_session() - return session.query(cls - ).filter_by(deleted=deleted - ).count() - - @classmethod - def find(cls, obj_id, session=None, deleted=False): - """Find object by id""" - if not session: - session = get_session() - try: - return session.query(cls - ).filter_by(id=obj_id - ).filter_by(deleted=deleted - ).one() - except exc.NoResultFound: - new_exc = exception.NotFound("No model for id %s" % obj_id) - raise new_exc.__class__, new_exc, sys.exc_info()[2] - - @classmethod - def find_by_str(cls, str_id, session=None, deleted=False): - """Find object by str_id""" - int_id = int(str_id.rpartition('-')[2]) - return cls.find(int_id, session=session, deleted=deleted) - @property def str_id(self): """Get string id of object (generally prefix + '-' + id)""" -- cgit From 8bd81f3ec811e19f6e7faf7a4fe271f85fbc7fc7 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Thu, 30 Sep 2010 02:02:14 -0700 Subject: Simplified authorization with decorators" " --- nova/db/sqlalchemy/api.py | 408 ++++++++++++++++------------------------------ 1 file changed, 142 insertions(+), 266 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 302322979..0e7d2e664 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -51,6 +51,7 @@ def _deleted(context): def is_admin_context(context): + """Indicates if the request context is an administrator.""" if not context: logging.warning('Use of empty request context is deprecated') return True @@ -60,6 +61,7 @@ def is_admin_context(context): def is_user_context(context): + """Indicates if the request context is a normal user.""" if not context: logging.warning('Use of empty request context is deprecated') return False @@ -68,24 +70,62 @@ def is_user_context(context): return True +def authorize_project_context(context, project_id): + """Ensures that the request context has permission to access the + given project. + """ + if is_user_context(context): + if not context.project: + raise exception.NotAuthorized() + elif context.project.id != project_id: + raise exception.NotAuthorized() + + +def authorize_user_context(context, user_id): + """Ensures that the request context has permission to access the + given user. + """ + if is_user_context(context): + if not context.user: + raise exception.NotAuthorized() + elif context.user.id != user_id: + raise exception.NotAuthorized() + + +def require_admin_context(f): + """Decorator used to indicate that the method requires an + administrator context. + """ + def wrapper(*args, **kwargs): + if not is_admin_context(args[0]): + raise exception.NotAuthorized() + return f(*args, **kwargs) + return wrapper + + +def require_context(f): + """Decorator used to indicate that the method requires either + an administrator or normal user context. + """ + def wrapper(*args, **kwargs): + if not is_admin_context(args[0]) and not is_user_context(args[0]): + raise exception.NotAuthorized() + return f(*args, **kwargs) + return wrapper + + ################### -#@require_admin_context +@require_admin_context def service_destroy(context, service_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): service_ref = service_get(context, service_id, session=session) service_ref.delete(session=session) -#@require_admin_context +@require_admin_context def service_get(context, service_id, session=None): - if not is_admin_context(context): - raise exception.NotAuthorized() - if not session: session = get_session() @@ -100,11 +140,8 @@ def service_get(context, service_id, session=None): return result -#@require_admin_context +@require_admin_context def service_get_all_by_topic(context, topic): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() return session.query(models.Service ).filter_by(deleted=False @@ -113,11 +150,8 @@ def service_get_all_by_topic(context, topic): ).all() -#@require_admin_context +@require_admin_context def _service_get_all_topic_subquery(context, session, topic, subq, label): - if not is_admin_context(context): - raise exception.NotAuthorized() - sort_value = getattr(subq.c, label) return session.query(models.Service, func.coalesce(sort_value, 0) ).filter_by(topic=topic @@ -128,11 +162,8 @@ def _service_get_all_topic_subquery(context, session, topic, subq, label): ).all() -#@require_admin_context +@require_admin_context def service_get_all_compute_sorted(context): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): # NOTE(vish): The intended query is below @@ -156,11 +187,8 @@ def service_get_all_compute_sorted(context): label) -#@require_admin_context +@require_admin_context def service_get_all_network_sorted(context): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): topic = 'network' @@ -177,11 +205,8 @@ def service_get_all_network_sorted(context): label) -#@require_admin_context +@require_admin_context def service_get_all_volume_sorted(context): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): topic = 'volume' @@ -198,11 +223,8 @@ def service_get_all_volume_sorted(context): label) -#@require_admin_context +@require_admin_context def service_get_by_args(context, host, binary): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() result = session.query(models.Service ).filter_by(host=host @@ -216,11 +238,8 @@ def service_get_by_args(context, host, binary): return result -#@require_admin_context +@require_admin_context def service_create(context, values): - if not is_admin_context(context): - return exception.NotAuthorized() - service_ref = models.Service() for (key, value) in values.iteritems(): service_ref[key] = value @@ -228,11 +247,8 @@ def service_create(context, values): return service_ref -#@require_admin_context +@require_admin_context def service_update(context, service_id, values): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): service_ref = session_get(context, service_id, session=session) @@ -244,11 +260,9 @@ def service_update(context, service_id, values): ################### -#@require_context +@require_context def floating_ip_allocate_address(context, host, project_id): - if is_user_context(context): - if context.project.id != project_id: - raise exception.NotAuthorized() + authorize_project_context(context, project_id) session = get_session() with session.begin(): @@ -268,11 +282,8 @@ def floating_ip_allocate_address(context, host, project_id): return floating_ip_ref['address'] -#@require_context +@require_context def floating_ip_create(context, values): - if not is_user_context(context) and not is_admin_context(context): - raise exception.NotAuthorized() - floating_ip_ref = models.FloatingIp() for (key, value) in values.iteritems(): floating_ip_ref[key] = value @@ -280,11 +291,9 @@ def floating_ip_create(context, values): return floating_ip_ref['address'] -#@require_context +@require_context def floating_ip_count_by_project(context, project_id): - if is_user_context(context): - if context.project.id != project_id: - raise exception.NotAuthorized() + authorize_project_context(context, project_id) session = get_session() return session.query(models.FloatingIp @@ -293,11 +302,8 @@ def floating_ip_count_by_project(context, project_id): ).count() -#@require_context +@require_context def floating_ip_fixed_ip_associate(context, floating_address, fixed_address): - if not is_user_context(context) and not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): # TODO(devcamcar): How to ensure floating_id belongs to user? @@ -311,11 +317,8 @@ def floating_ip_fixed_ip_associate(context, floating_address, fixed_address): floating_ip_ref.save(session=session) -#@require_context +@require_context def floating_ip_deallocate(context, address): - if not is_user_context(context) and not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): # TODO(devcamcar): How to ensure floating id belongs to user? @@ -326,11 +329,8 @@ def floating_ip_deallocate(context, address): floating_ip_ref.save(session=session) -#@require_context +@require_context def floating_ip_destroy(context, address): - if not is_user_context(context) and not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): # TODO(devcamcar): Ensure address belongs to user. @@ -340,11 +340,8 @@ def floating_ip_destroy(context, address): floating_ip_ref.delete(session=session) -#@require_context +@require_context def floating_ip_disassociate(context, address): - if not is_user_context(context) and is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): # TODO(devcamcar): Ensure address belongs to user. @@ -362,11 +359,8 @@ def floating_ip_disassociate(context, address): return fixed_ip_address -#@require_admin_context +@require_admin_context def floating_ip_get_all(context): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() return session.query(models.FloatingIp ).options(joinedload_all('fixed_ip.instance') @@ -374,11 +368,8 @@ def floating_ip_get_all(context): ).all() -#@require_admin_context +@require_admin_context def floating_ip_get_all_by_host(context, host): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() return session.query(models.FloatingIp ).options(joinedload_all('fixed_ip.instance') @@ -387,14 +378,9 @@ def floating_ip_get_all_by_host(context, host): ).all() -#@require_context +@require_context def floating_ip_get_all_by_project(context, project_id): - # TODO(devcamcar): Change to decorate and check project_id separately. - if is_user_context(context): - if context.project.id != project_id: - raise exception.NotAuthorized() - elif not is_admin_context(context): - raise exception.NotAuthorized() + authorize_project_context(context, project_id) session = get_session() return session.query(models.FloatingIp @@ -404,12 +390,9 @@ def floating_ip_get_all_by_project(context, project_id): ).all() -#@require_context +@require_context def floating_ip_get_by_address(context, address, session=None): # TODO(devcamcar): Ensure the address belongs to user. - if not is_user_context(context) and not is_admin_context(context): - raise exception.NotAuthorized() - if not session: session = get_session() @@ -426,11 +409,8 @@ def floating_ip_get_by_address(context, address, session=None): ################### -#@require_context +@require_context def fixed_ip_associate(context, address, instance_id): - if not is_user_context(context) and not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): fixed_ip_ref = session.query(models.FixedIp @@ -449,11 +429,8 @@ def fixed_ip_associate(context, address, instance_id): session.add(fixed_ip_ref) -#@require_admin_context +@require_admin_context def fixed_ip_associate_pool(context, network_id, instance_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): network_or_none = or_(models.FixedIp.network_id == network_id, @@ -480,7 +457,7 @@ def fixed_ip_associate_pool(context, network_id, instance_id): return fixed_ip_ref['address'] -#@require_context +@require_context def fixed_ip_create(_context, values): fixed_ip_ref = models.FixedIp() for (key, value) in values.iteritems(): @@ -489,7 +466,7 @@ def fixed_ip_create(_context, values): return fixed_ip_ref['address'] -#@require_context +@require_context def fixed_ip_disassociate(context, address): session = get_session() with session.begin(): @@ -500,7 +477,7 @@ def fixed_ip_disassociate(context, address): fixed_ip_ref.save(session=session) -#@require_context +@require_context def fixed_ip_get_by_address(context, address, session=None): # TODO(devcamcar): Ensure floating ip belongs to user. # Only possible if it is associated with an instance. @@ -520,19 +497,19 @@ def fixed_ip_get_by_address(context, address, session=None): return result -#@require_context +@require_context def fixed_ip_get_instance(context, address): fixed_ip_ref = fixed_ip_get_by_address(context, address) return fixed_ip_ref.instance -#@require_admin_context +@require_admin_context def fixed_ip_get_network(context, address): fixed_ip_ref = fixed_ip_get_by_address(context, address) return fixed_ip_ref.network -#@require_context +@require_context def fixed_ip_update(context, address, values): session = get_session() with session.begin(): @@ -547,7 +524,7 @@ def fixed_ip_update(context, address, values): ################### -#@require_context +@require_context def instance_create(context, values): instance_ref = models.Instance() for (key, value) in values.iteritems(): @@ -563,7 +540,7 @@ def instance_create(context, values): return instance_ref -#@require_admin_context +@require_admin_context def instance_data_get_for_project(context, project_id): session = get_session() result = session.query(func.count(models.Instance.id), @@ -575,7 +552,7 @@ def instance_data_get_for_project(context, project_id): return (result[0] or 0, result[1] or 0) -#@require_context +@require_context def instance_destroy(context, instance_id): session = get_session() with session.begin(): @@ -583,7 +560,7 @@ def instance_destroy(context, instance_id): instance_ref.delete(session=session) -#@require_context +@require_context def instance_get(context, instance_id, session=None): if not session: session = get_session() @@ -606,11 +583,8 @@ def instance_get(context, instance_id, session=None): return result -#@require_admin_context +@require_admin_context def instance_get_all(context): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -618,11 +592,8 @@ def instance_get_all(context): ).all() -#@require_admin_context +@require_admin_context def instance_get_all_by_user(context, user_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -631,11 +602,9 @@ def instance_get_all_by_user(context, user_id): ).all() -#@require_context +@require_context def instance_get_all_by_project(context, project_id): - if is_user_context(context): - if context.project.id != project_id: - raise exception.NotAuthorized() + authorize_project_context(context, project_id) session = get_session() return session.query(models.Instance @@ -645,7 +614,7 @@ def instance_get_all_by_project(context, project_id): ).all() -#@require_context +@require_context def instance_get_all_by_reservation(context, reservation_id): session = get_session() @@ -664,7 +633,7 @@ def instance_get_all_by_reservation(context, reservation_id): ).all() -#@require_context +@require_context def instance_get_by_ec2_id(context, ec2_id): session = get_session() @@ -685,14 +654,14 @@ def instance_get_by_ec2_id(context, ec2_id): return result -#@require_context +@require_context def instance_ec2_id_exists(context, ec2_id, session=None): if not session: session = get_session() return session.query(exists().where(models.Instance.id==ec2_id)).one()[0] -#@require_context +@require_context def instance_get_fixed_address(context, instance_id): session = get_session() with session.begin(): @@ -702,7 +671,7 @@ def instance_get_fixed_address(context, instance_id): return instance_ref.fixed_ip['address'] -#@require_context +@require_context def instance_get_floating_address(context, instance_id): session = get_session() with session.begin(): @@ -715,20 +684,15 @@ def instance_get_floating_address(context, instance_id): return instance_ref.fixed_ip.floating_ips[0]['address'] -#@require_admin_context +@require_admin_context def instance_is_vpn(context, instance_id): - if not is_admin_context(context): - raise exception.NotAuthorized() # TODO(vish): Move this into image code somewhere instance_ref = instance_get(context, instance_id) return instance_ref['image_id'] == FLAGS.vpn_image_id -#@require_admin_context +@require_admin_context def instance_set_state(context, instance_id, state, description=None): - if not is_admin_context(context): - raise exception.NotAuthorized() - # TODO(devcamcar): Move this out of models and into driver from nova.compute import power_state if not description: @@ -739,7 +703,7 @@ def instance_set_state(context, instance_id, state, description=None): 'state_description': description}) -#@require_context +@require_context def instance_update(context, instance_id, values): session = get_session() with session.begin(): @@ -752,7 +716,7 @@ def instance_update(context, instance_id, values): ################### -#@require_context +@require_context def key_pair_create(context, values): key_pair_ref = models.KeyPair() for (key, value) in values.iteritems(): @@ -761,11 +725,9 @@ def key_pair_create(context, values): return key_pair_ref -#@require_context +@require_context def key_pair_destroy(context, user_id, name): - if is_user_context(context): - if context.user.id != user_id: - raise exception.NotAuthorized() + authorize_user_context(context, user_id) session = get_session() with session.begin(): @@ -773,11 +735,9 @@ def key_pair_destroy(context, user_id, name): key_pair_ref.delete(session=session) -#@require_context +@require_context def key_pair_destroy_all_by_user(context, user_id): - if is_user_context(context): - if context.user.id != user_id: - raise exception.NotAuthorized() + authorize_user_context(context, user_id) session = get_session() with session.begin(): @@ -786,11 +746,9 @@ def key_pair_destroy_all_by_user(context, user_id): {'id': user_id}) -#@require_context +@require_context def key_pair_get(context, user_id, name, session=None): - if is_user_context(context): - if context.user.id != user_id: - raise exception.NotAuthorized() + authorize_user_context(context, user_id) if not session: session = get_session() @@ -806,11 +764,9 @@ def key_pair_get(context, user_id, name, session=None): return result -#@require_context +@require_context def key_pair_get_all_by_user(context, user_id): - if is_user_context(context): - if context.user.id != user_id: - raise exception.NotAuthorized() + authorize_user_context(context, user_id) session = get_session() return session.query(models.KeyPair @@ -822,22 +778,16 @@ def key_pair_get_all_by_user(context, user_id): ################### -#@require_admin_context +@require_admin_context def network_count(context): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() return session.query(models.Network ).filter_by(deleted=_deleted(context) ).count() -#@require_admin_context +@require_admin_context def network_count_allocated_ips(context, network_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() return session.query(models.FixedIp ).filter_by(network_id=network_id @@ -846,11 +796,8 @@ def network_count_allocated_ips(context, network_id): ).count() -#@require_admin_context +@require_admin_context def network_count_available_ips(context, network_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() return session.query(models.FixedIp ).filter_by(network_id=network_id @@ -860,11 +807,8 @@ def network_count_available_ips(context, network_id): ).count() -#@require_admin_context +@require_admin_context def network_count_reserved_ips(context, network_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() return session.query(models.FixedIp ).filter_by(network_id=network_id @@ -873,11 +817,8 @@ def network_count_reserved_ips(context, network_id): ).count() -#@require_admin_context +@require_admin_context def network_create(context, values): - if not is_admin_context(context): - raise exception.NotAuthorized() - network_ref = models.Network() for (key, value) in values.iteritems(): network_ref[key] = value @@ -885,11 +826,8 @@ def network_create(context, values): return network_ref -#@require_admin_context +@require_admin_context def network_destroy(context, network_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): # TODO(vish): do we have to use sql here? @@ -907,7 +845,7 @@ def network_destroy(context, network_id): {'id': network_id}) -#@require_context +@require_context def network_get(context, network_id, session=None): if not session: session = get_session() @@ -933,11 +871,8 @@ def network_get(context, network_id, session=None): # NOTE(vish): pylint complains because of the long method name, but # it fits with the names of the rest of the methods # pylint: disable-msg=C0103 -#@require_admin_context +@require_admin_context def network_get_associated_fixed_ips(context, network_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() return session.query(models.FixedIp ).options(joinedload_all('instance') @@ -947,11 +882,8 @@ def network_get_associated_fixed_ips(context, network_id): ).all() -#@require_admin_context +@require_admin_context def network_get_by_bridge(context, bridge): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() result = session.query(models.Network ).filter_by(bridge=bridge @@ -964,11 +896,8 @@ def network_get_by_bridge(context, bridge): return result -#@require_admin_context +@require_admin_context def network_get_index(context, network_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): network_index = session.query(models.NetworkIndex @@ -988,22 +917,16 @@ def network_get_index(context, network_id): return network_index['index'] -#@require_admin_context +@require_admin_context def network_index_count(context): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() return session.query(models.NetworkIndex ).filter_by(deleted=_deleted(context) ).count() -#@require_admin_context +@require_admin_context def network_index_create_safe(context, values): - if not is_admin_context(context): - raise exception.NotAuthorized() - network_index_ref = models.NetworkIndex() for (key, value) in values.iteritems(): network_index_ref[key] = value @@ -1013,11 +936,8 @@ def network_index_create_safe(context, values): pass -#@require_admin_context +@require_admin_context def network_set_host(context, network_id, host_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): network_ref = session.query(models.Network @@ -1037,7 +957,7 @@ def network_set_host(context, network_id, host_id): return network_ref['host'] -#@require_context +@require_context def network_update(context, network_id, values): session = get_session() with session.begin(): @@ -1050,11 +970,8 @@ def network_update(context, network_id, values): ################### -#@require_context +@require_context def project_get_network(context, project_id): - if not is_admin_context(context) and not is_user_context(context): - raise error.NotAuthorized() - session = get_session() result= session.query(models.Network ).filter_by(project_id=project_id @@ -1078,22 +995,16 @@ def queue_get_for(_context, topic, physical_node_id): ################### -#@require_admin_context +@require_admin_context def export_device_count(context): - if not is_admin_context(context): - raise exception.notauthorized() - session = get_session() return session.query(models.ExportDevice ).filter_by(deleted=_deleted(context) ).count() -#@require_admin_context +@require_admin_context def export_device_create(context, values): - if not is_admin_context(context): - raise exception.notauthorized() - export_device_ref = models.ExportDevice() for (key, value) in values.iteritems(): export_device_ref[key] = value @@ -1127,11 +1038,8 @@ def auth_create_token(_context, token): ################### -#@require_admin_context +@require_admin_context def quota_get(context, project_id, session=None): - if not is_admin_context(context): - raise exception.NotAuthorized() - if not session: session = get_session() @@ -1145,11 +1053,8 @@ def quota_get(context, project_id, session=None): return result -#@require_admin_context +@require_admin_context def quota_create(context, values): - if not is_admin_context(context): - raise exception.NotAuthorized() - quota_ref = models.Quota() for (key, value) in values.iteritems(): quota_ref[key] = value @@ -1157,11 +1062,8 @@ def quota_create(context, values): return quota_ref -#@require_admin_context +@require_admin_context def quota_update(context, project_id, values): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): quota_ref = quota_get(context, project_id, session=session) @@ -1170,11 +1072,8 @@ def quota_update(context, project_id, values): quota_ref.save(session=session) -#@require_admin_context +@require_admin_context def quota_destroy(context, project_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): quota_ref = quota_get(context, project_id, session=session) @@ -1184,11 +1083,8 @@ def quota_destroy(context, project_id): ################### -#@require_admin_context +@require_admin_context def volume_allocate_shelf_and_blade(context, volume_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): export_device = session.query(models.ExportDevice @@ -1205,11 +1101,8 @@ def volume_allocate_shelf_and_blade(context, volume_id): return (export_device.shelf_id, export_device.blade_id) -#@require_admin_context +@require_admin_context def volume_attached(context, volume_id, instance_id, mountpoint): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): volume_ref = volume_get(context, volume_id, session=session) @@ -1220,7 +1113,7 @@ def volume_attached(context, volume_id, instance_id, mountpoint): volume_ref.save(session=session) -#@require_context +@require_context def volume_create(context, values): volume_ref = models.Volume() for (key, value) in values.iteritems(): @@ -1236,11 +1129,8 @@ def volume_create(context, values): return volume_ref -#@require_admin_context +@require_admin_context def volume_data_get_for_project(context, project_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() result = session.query(func.count(models.Volume.id), func.sum(models.Volume.size) @@ -1251,11 +1141,8 @@ def volume_data_get_for_project(context, project_id): return (result[0] or 0, result[1] or 0) -#@require_admin_context +@require_admin_context def volume_destroy(context, volume_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): # TODO(vish): do we have to use sql here? @@ -1266,11 +1153,8 @@ def volume_destroy(context, volume_id): {'id': volume_id}) -#@require_admin_context +@require_admin_context def volume_detached(context, volume_id): - if not is_admin_context(context): - raise exception.NotAuthorized() - session = get_session() with session.begin(): volume_ref = volume_get(context, volume_id, session=session) @@ -1281,7 +1165,7 @@ def volume_detached(context, volume_id): volume_ref.save(session=session) -#@require_context +@require_context def volume_get(context, volume_id, session=None): if not session: session = get_session() @@ -1304,22 +1188,15 @@ def volume_get(context, volume_id, session=None): return result -#@require_admin_context +@require_admin_context def volume_get_all(context): - if not is_admin_context(context): - raise exception.NotAuthorized() - return session.query(models.Volume ).filter_by(deleted=_deleted(context) ).all() -#@require_context +@require_context def volume_get_all_by_project(context, project_id): - if is_user_context(context): - if context.project.id != project_id: - raise exception.NotAuthorized() - elif not is_admin_context(context): - raise exception.NotAuthorized() + authorize_project_context(context, project_id) session = get_session() return session.query(models.Volume @@ -1328,7 +1205,7 @@ def volume_get_all_by_project(context, project_id): ).all() -#@require_context +@require_context def volume_get_by_ec2_id(context, ec2_id): session = get_session() result = None @@ -1353,18 +1230,17 @@ def volume_get_by_ec2_id(context, ec2_id): return result -#@require_context +@require_context def volume_ec2_id_exists(context, ec2_id, session=None): if not session: session = get_session() - if is_admin_context(context) or is_user_context(context): - return session.query(exists( - ).where(models.Volume.id==ec2_id) - ).one()[0] + return session.query(exists( + ).where(models.Volume.id==ec2_id) + ).one()[0] -#@require_context +@require_context def volume_get_instance(context, volume_id): session = get_session() result = None @@ -1390,7 +1266,7 @@ def volume_get_instance(context, volume_id): return result.instance -#@require_context +@require_context def volume_get_shelf_and_blade(context, volume_id): session = get_session() result = None @@ -1412,7 +1288,7 @@ def volume_get_shelf_and_blade(context, volume_id): return (result.shelf_id, result.blade_id) -#@require_context +@require_context def volume_update(context, volume_id, values): session = get_session() with session.begin(): -- cgit From 8c21cc52b1ba007fc12964ea5973290a3f660662 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Thu, 30 Sep 2010 10:21:44 +0100 Subject: Bug #651887: xenapi list_instances completely broken Don't just compute the result for list_instances -- return it! --- nova/virt/xenapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index 1c6de4403..0d06b1fce 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -103,8 +103,8 @@ class XenAPIConnection(object): self._conn.login_with_password(user, pw) def list_instances(self): - result = [self._conn.xenapi.VM.get_name_label(vm) \ - for vm in self._conn.xenapi.VM.get_all()] + return [self._conn.xenapi.VM.get_name_label(vm) \ + for vm in self._conn.xenapi.VM.get_all()] @defer.inlineCallbacks def spawn(self, instance): -- cgit From cf456bdb2a767644d95599aa1c8f580279959a4e Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Thu, 30 Sep 2010 02:47:05 -0700 Subject: Refactored APIRequestContext --- nova/api/context.py | 46 +++++++++++++++++++++++++++ nova/api/ec2/__init__.py | 8 ++--- nova/api/ec2/context.py | 33 -------------------- nova/db/sqlalchemy/api.py | 71 +++++++++++++++++++----------------------- nova/network/manager.py | 2 -- nova/tests/compute_unittest.py | 8 ++--- 6 files changed, 86 insertions(+), 82 deletions(-) create mode 100644 nova/api/context.py delete mode 100644 nova/api/ec2/context.py diff --git a/nova/api/context.py b/nova/api/context.py new file mode 100644 index 000000000..b66cfe468 --- /dev/null +++ b/nova/api/context.py @@ -0,0 +1,46 @@ +# 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, user, project): + self.user = user + self.project = project + self.request_id = ''.join( + [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-') + for x in xrange(20)] + ) + if user: + self.is_admin = user.is_admin() + else: + self.is_admin = False + self.read_deleted = False + + +def get_admin_context(user=None, read_deleted=False): + context_ref = APIRequestContext(user=user, project=None) + context_ref.is_admin = True + context_ref.read_deleted = read_deleted + return context_ref + diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 7a958f841..6b538a7f1 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -27,8 +27,8 @@ import webob.exc from nova import exception from nova import flags from nova import wsgi +from nova.api import context from nova.api.ec2 import apirequest -from nova.api.ec2 import context from nova.api.ec2 import admin from nova.api.ec2 import cloud from nova.auth import manager @@ -193,15 +193,15 @@ class Authorizer(wsgi.Middleware): return True if 'none' in roles: return False - return any(context.project.has_role(context.user.id, role) + return any(context.project.has_role(context.user.id, role) for role in roles) - + class Executor(wsgi.Application): """Execute an EC2 API request. - Executes 'ec2.action' upon 'ec2.controller', passing 'ec2.context' and + 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. """ diff --git a/nova/api/ec2/context.py b/nova/api/ec2/context.py deleted file mode 100644 index c53ba98d9..000000000 --- a/nova/api/ec2/context.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, user, project): - self.user = user - self.project = project - self.request_id = ''.join( - [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-') - for x in xrange(20)] - ) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 0e7d2e664..fc5ee2235 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -21,6 +21,7 @@ Implementation of SQLAlchemy backend import logging import sys +import warnings from nova import db from nova import exception @@ -36,28 +37,13 @@ from sqlalchemy.sql import exists, func FLAGS = flags.FLAGS -# NOTE(vish): disabling docstring pylint because the docstrings are -# in the interface definition -# pylint: disable-msg=C0111 -def _deleted(context): - """Calculates whether to include deleted objects based on context. - Currently just looks for a flag called deleted in the context dict. - """ - if is_user_context(context): - return False - if not hasattr(context, 'get'): - return False - return context.get('deleted', False) - - def is_admin_context(context): """Indicates if the request context is an administrator.""" if not context: - logging.warning('Use of empty request context is deprecated') - return True - if not context.user: + warnings.warn('Use of empty request context is deprecated', + DeprecationWarning) return True - return context.user.is_admin() + return context.is_admin def is_user_context(context): @@ -92,6 +78,13 @@ def authorize_user_context(context, user_id): raise exception.NotAuthorized() +def use_deleted(context): + """Indicates if the context has access to deleted objects.""" + if not context: + return False + return context.read_deleted + + def require_admin_context(f): """Decorator used to indicate that the method requires an administrator context. @@ -131,7 +124,7 @@ def service_get(context, service_id, session=None): result = session.query(models.Service ).filter_by(id=service_id - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).first() if not result: @@ -229,7 +222,7 @@ def service_get_by_args(context, host, binary): result = session.query(models.Service ).filter_by(host=host ).filter_by(binary=binary - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).first() if not result: @@ -398,7 +391,7 @@ def floating_ip_get_by_address(context, address, session=None): result = session.query(models.FloatingIp ).filter_by(address=address - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).first() if not result: raise exception.NotFound('No fixed ip for address %s' % address) @@ -487,7 +480,7 @@ def fixed_ip_get_by_address(context, address, session=None): result = session.query(models.FixedIp ).filter_by(address=address - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).options(joinedload('network') ).options(joinedload('instance') ).first() @@ -569,7 +562,7 @@ def instance_get(context, instance_id, session=None): if is_admin_context(context): result = session.query(models.Instance ).filter_by(id=instance_id - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).first() elif is_user_context(context): result = session.query(models.Instance @@ -588,7 +581,7 @@ def instance_get_all(context): session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).all() @@ -597,7 +590,7 @@ def instance_get_all_by_user(context, user_id): session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).filter_by(user_id=user_id ).all() @@ -610,7 +603,7 @@ def instance_get_all_by_project(context, project_id): return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') ).filter_by(project_id=project_id - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).all() @@ -622,7 +615,7 @@ def instance_get_all_by_reservation(context, reservation_id): return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') ).filter_by(reservation_id=reservation_id - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).all() elif is_user_context(context): return session.query(models.Instance @@ -640,7 +633,7 @@ def instance_get_by_ec2_id(context, ec2_id): if is_admin_context(context): result = session.query(models.Instance ).filter_by(ec2_id=ec2_id - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).first() elif is_user_context(context): result = session.query(models.Instance @@ -756,7 +749,7 @@ def key_pair_get(context, user_id, name, session=None): result = session.query(models.KeyPair ).filter_by(user_id=user_id ).filter_by(name=name - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).first() if not result: raise exception.NotFound('no keypair for user %s, name %s' % @@ -782,7 +775,7 @@ def key_pair_get_all_by_user(context, user_id): def network_count(context): session = get_session() return session.query(models.Network - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).count() @@ -854,7 +847,7 @@ def network_get(context, network_id, session=None): if is_admin_context(context): result = session.query(models.Network ).filter_by(id=network_id - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).first() elif is_user_context(context): result = session.query(models.Network @@ -921,7 +914,7 @@ def network_get_index(context, network_id): def network_index_count(context): session = get_session() return session.query(models.NetworkIndex - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).count() @@ -999,7 +992,7 @@ def queue_get_for(_context, topic, physical_node_id): def export_device_count(context): session = get_session() return session.query(models.ExportDevice - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).count() @@ -1045,7 +1038,7 @@ def quota_get(context, project_id, session=None): result = session.query(models.Quota ).filter_by(project_id=project_id - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).first() if not result: raise exception.NotFound('No quota for project_id %s' % project_id) @@ -1174,7 +1167,7 @@ def volume_get(context, volume_id, session=None): if is_admin_context(context): result = session.query(models.Volume ).filter_by(id=volume_id - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).first() elif is_user_context(context): result = session.query(models.Volume @@ -1191,7 +1184,7 @@ def volume_get(context, volume_id, session=None): @require_admin_context def volume_get_all(context): return session.query(models.Volume - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).all() @require_context @@ -1201,7 +1194,7 @@ def volume_get_all_by_project(context, project_id): session = get_session() return session.query(models.Volume ).filter_by(project_id=project_id - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).all() @@ -1213,7 +1206,7 @@ def volume_get_by_ec2_id(context, ec2_id): if is_admin_context(context): result = session.query(models.Volume ).filter_by(ec2_id=ec2_id - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).first() elif is_user_context(context): result = session.query(models.Volume @@ -1248,7 +1241,7 @@ def volume_get_instance(context, volume_id): if is_admin_context(context): result = session.query(models.Volume ).filter_by(id=volume_id - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=use_deleted(context) ).options(joinedload('instance') ).first() elif is_user_context(context): diff --git a/nova/network/manager.py b/nova/network/manager.py index ecf2fa2c2..265c0d742 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -88,7 +88,6 @@ class NetworkManager(manager.Manager): # TODO(vish): can we minimize db access by just getting the # id here instead of the ref? network_id = network_ref['id'] - # TODO(devcamcar): Replace with system context host = self.db.network_set_host(None, network_id, self.host) @@ -233,7 +232,6 @@ class VlanManager(NetworkManager): address = network_ref['vpn_private_address'] self.db.fixed_ip_associate(context, address, instance_id) else: - # TODO(devcamcar) Pass system context here. address = self.db.fixed_ip_associate_pool(None, network_ref['id'], instance_id) diff --git a/nova/tests/compute_unittest.py b/nova/tests/compute_unittest.py index e705c2552..1e2bb113b 100644 --- a/nova/tests/compute_unittest.py +++ b/nova/tests/compute_unittest.py @@ -30,7 +30,7 @@ from nova import flags from nova import test from nova import utils from nova.auth import manager - +from nova.api import context FLAGS = flags.FLAGS @@ -96,9 +96,9 @@ class ComputeTestCase(test.TrialTestCase): self.assertEqual(instance_ref['deleted_at'], None) terminate = datetime.datetime.utcnow() yield self.compute.terminate_instance(self.context, instance_id) - # TODO(devcamcar): Pass deleted in using system context. - # context.read_deleted ? - instance_ref = db.instance_get({'deleted': True}, instance_id) + self.context = context.get_admin_context(user=self.user, + read_deleted=True) + instance_ref = db.instance_get(self.context, instance_id) self.assert_(instance_ref['launched_at'] < terminate) self.assert_(instance_ref['deleted_at'] > terminate) -- cgit From ab948224a5c6ea976def30927ac7668dd765dbca Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Thu, 30 Sep 2010 03:13:47 -0700 Subject: Cleaned up db/api.py --- nova/db/api.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index 4cfdd788c..290b460a6 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -175,10 +175,6 @@ def floating_ip_get_by_address(context, address): return IMPL.floating_ip_get_by_address(context, address) - """Get an instance for a floating ip by address.""" - return IMPL.floating_ip_get_instance(context, address) - - #################### -- cgit From 75c5ba6aae6a57a61771ed78b6797c90f7da6940 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 30 Sep 2010 09:22:46 -0500 Subject: Grabbed the wrong copyright info --- nova/api/rackspace/context.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nova/api/rackspace/context.py b/nova/api/rackspace/context.py index 924ee151e..77394615b 100644 --- a/nova/api/rackspace/context.py +++ b/nova/api/rackspace/context.py @@ -1,7 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. +# Copyright 2010 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may -- cgit From 76d9510ac496051697f7cdf0d693d5146e83283a Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Thu, 30 Sep 2010 16:00:53 +0100 Subject: Bug #652103: NameError in exception handler in sqlalchemy API layer Fix reference to NoResultFound. --- nova/db/sqlalchemy/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 9c3caf9af..e63d3b18e 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -30,6 +30,7 @@ from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import joinedload_all +from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.sql import exists, func FLAGS = flags.FLAGS @@ -348,7 +349,7 @@ def fixed_ip_get_by_address(_context, address): ).filter_by(address=address ).filter_by(deleted=False ).one() - except exc.NoResultFound: + except NoResultFound: new_exc = exception.NotFound("No model for address %s" % address) raise new_exc.__class__, new_exc, sys.exc_info()[2] -- cgit From b40696640b13e0974a29c23240f7faa79ad00912 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Fri, 1 Oct 2010 00:42:09 +0200 Subject: Add a DB backend for auth manager. --- nova/auth/dbdriver.py | 236 +++++++++++++++++++++++++++++++++++++++++++ nova/auth/manager.py | 2 +- nova/db/api.py | 113 +++++++++++++++++++++ nova/db/sqlalchemy/api.py | 199 ++++++++++++++++++++++++++++++++++++ nova/db/sqlalchemy/models.py | 73 ++++++++++++- nova/tests/auth_unittest.py | 9 +- nova/tests/fake_flags.py | 2 +- 7 files changed, 629 insertions(+), 5 deletions(-) create mode 100644 nova/auth/dbdriver.py diff --git a/nova/auth/dbdriver.py b/nova/auth/dbdriver.py new file mode 100644 index 000000000..09d15018b --- /dev/null +++ b/nova/auth/dbdriver.py @@ -0,0 +1,236 @@ +# 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. + +""" +Auth driver using the DB as its backend. +""" + +import logging +import sys + +from nova import exception +from nova import db + + +class DbDriver(object): + """DB Auth driver + + Defines enter and exit and therefore supports the with/as syntax. + """ + + def __init__(self): + """Imports the LDAP module""" + pass + db + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def get_user(self, uid): + """Retrieve user by id""" + return self._db_user_to_auth_user(db.user_get({}, uid)) + + def get_user_from_access_key(self, access): + """Retrieve user by access key""" + return self._db_user_to_auth_user(db.user_get_by_access_key({}, access)) + + def get_project(self, pid): + """Retrieve project by id""" + return self._db_project_to_auth_projectuser(db.project_get({}, pid)) + + def get_users(self): + """Retrieve list of users""" + return [self._db_user_to_auth_user(user) for user in db.user_get_all({})] + + def get_projects(self, uid=None): + """Retrieve list of projects""" + if uid: + result = db.project_get_by_user({}, uid) + else: + result = db.project_get_all({}) + return [self._db_project_to_auth_projectuser(proj) for proj in result] + + def create_user(self, name, access_key, secret_key, is_admin): + """Create a user""" + values = { 'id' : name, + 'access_key' : access_key, + 'secret_key' : secret_key, + 'is_admin' : is_admin + } + try: + user_ref = db.user_create({}, values) + return self._db_user_to_auth_user(user_ref) + except exception.Duplicate, e: + raise exception.Duplicate('User %s already exists' % name) + + def _db_user_to_auth_user(self, user_ref): + return { 'id' : user_ref['id'], + 'name' : user_ref['id'], + 'access' : user_ref['access_key'], + 'secret' : user_ref['secret_key'], + 'admin' : user_ref['is_admin'] } + + def _db_project_to_auth_projectuser(self, project_ref): + return { 'id' : project_ref['id'], + 'name' : project_ref['name'], + 'project_manager_id' : project_ref['project_manager'], + 'description' : project_ref['description'], + 'member_ids' : [member['id'] for member in project_ref['members']] } + + def create_project(self, name, manager_uid, + description=None, member_uids=None): + """Create a project""" + manager = db.user_get({}, manager_uid) + if not manager: + raise exception.NotFound("Project can't be created because " + "manager %s doesn't exist" % manager_uid) + + # description is a required attribute + if description is None: + description = name + + # First, we ensure that all the given users exist before we go + # on to create the project. This way we won't have to destroy + # the project again because a user turns out to be invalid. + members = set([manager]) + if member_uids != None: + for member_uid in member_uids: + member = db.user_get({}, member_uid) + if not member: + raise exception.NotFound("Project can't be created " + "because user %s doesn't exist" + % member_uid) + members.add(member) + + values = { 'id' : name, + 'name' : name, + 'project_manager' : manager['id'], + 'description': description } + + try: + project = db.project_create({}, values) + except exception.Duplicate: + raise exception.Duplicate("Project can't be created because " + "project %s already exists" % name) + + for member in members: + db.project_add_member({}, project['id'], member['id']) + + # This looks silly, but ensures that the members element has been + # correctly populated + project_ref = db.project_get({}, project['id']) + return self._db_project_to_auth_projectuser(project_ref) + + def modify_project(self, project_id, manager_uid=None, description=None): + """Modify an existing project""" + if not manager_uid and not description: + return + values = {} + if manager_uid: + manager = db.user_get({}, manager_uid) + if not manager: + raise exception.NotFound("Project can't be modified because " + "manager %s doesn't exist" % + manager_uid) + values['project_manager'] = manager['id'] + if description: + values['description'] = description + + db.project_update({}, project_id, values) + + def add_to_project(self, uid, project_id): + """Add user to project""" + user, project = self._validate_user_and_project(uid, project_id) + db.project_add_member({}, project['id'], user['id']) + + def remove_from_project(self, uid, project_id): + """Remove user from project""" + user, project = self._validate_user_and_project(uid, project_id) + db.project_remove_member({}, project['id'], user['id']) + + def is_in_project(self, uid, project_id): + """Check if user is in project""" + user, project = self._validate_user_and_project(uid, project_id) + return user in project.members + + def has_role(self, uid, role, project_id=None): + """Check if user has role + + If project is specified, it checks for local role, otherwise it + checks for global role + """ + + return role in self.get_user_roles(uid, project_id) + + def add_role(self, uid, role, project_id=None): + """Add role for user (or user and project)""" + if not project_id: + db.user_add_role({}, uid, role) + return + db.user_add_project_role({}, uid, project_id, role) + + def remove_role(self, uid, role, project_id=None): + """Remove role for user (or user and project)""" + if not project_id: + db.user_remove_role({}, uid, role) + return + db.user_remove_project_role({}, uid, project_id, role) + + def get_user_roles(self, uid, project_id=None): + """Retrieve list of roles for user (or user and project)""" + if project_id is None: + roles = db.user_get_roles({}, uid) + return roles + else: + roles = db.user_get_roles_for_project({}, uid, project_id) + return roles + + def delete_user(self, id): + """Delete a user""" + user = db.user_get({}, id) + db.user_delete({}, user['id']) + + def delete_project(self, project_id): + """Delete a project""" + db.project_delete({}, project_id) + + def modify_user(self, uid, access_key=None, secret_key=None, admin=None): + """Modify an existing user""" + if not access_key and not secret_key and admin is None: + return + values = {} + if access_key: + values['access_key'] = access_key + if secret_key: + values['secret_key'] = secret_key + if admin is not None: + values['is_admin'] = admin + db.user_update({}, uid, values) + + def _validate_user_and_project(self, user_id, project_id): + user = db.user_get({}, user_id) + if not user: + raise exception.NotFound('User "%s" not found' % user_id) + project = db.project_get({}, project_id) + if not project: + raise exception.NotFound('Project "%s" not found' % project_id) + return user, project + diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 0bc12c80f..ce8a294df 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -69,7 +69,7 @@ flags.DEFINE_string('credential_cert_subject', '/C=US/ST=California/L=MountainView/O=AnsoLabs/' 'OU=NovaDev/CN=%s-%s', 'Subject for certificate for users') -flags.DEFINE_string('auth_driver', 'nova.auth.ldapdriver.FakeLdapDriver', +flags.DEFINE_string('auth_driver', 'nova.auth.dbdriver.DbDriver', 'Driver that auth manager uses') diff --git a/nova/db/api.py b/nova/db/api.py index b68a0fe8f..703936002 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -565,3 +565,116 @@ def volume_update(context, volume_id, values): """ return IMPL.volume_update(context, volume_id, values) + + +################### + + +def user_get(context, id): + """Get user by id""" + return IMPL.user_get(context, id) + + +def user_get_by_uid(context, uid): + """Get user by uid""" + return IMPL.user_get_by_uid(context, uid) + + +def user_get_by_access_key(context, access_key): + """Get user by access key""" + return IMPL.user_get_by_access_key(context, access_key) + + +def user_create(context, values): + """Create a new user""" + return IMPL.user_create(context, values) + + +def user_delete(context, id): + """Delete a user""" + return IMPL.user_delete(context, id) + + +def user_get_all(context): + """Create a new user""" + return IMPL.user_get_all(context) + + +def user_add_role(context, user_id, role): + """Add another global role for user""" + return IMPL.user_add_role(context, user_id, role) + + +def user_remove_role(context, user_id, role): + """Remove global role from user""" + return IMPL.user_remove_role(context, user_id, role) + + +def user_get_roles(context, user_id): + """Get global roles for user""" + return IMPL.user_get_roles(context, user_id) + + +def user_add_project_role(context, user_id, project_id, role): + """Add project role for user""" + return IMPL.user_add_project_role(context, user_id, project_id, role) + + +def user_remove_project_role(context, user_id, project_id, role): + """Remove project role from user""" + return IMPL.user_remove_project_role(context, user_id, project_id, role) + + +def user_get_roles_for_project(context, user_id, project_id): + """Return list of roles a user holds on project""" + return IMPL.user_get_roles_for_project(context, user_id, project_id) + + +def user_update(context, user_id, values): + """Update user""" + return IMPL.user_update(context, user_id, values) + + +def project_get(context, id): + """Get project by id""" + return IMPL.project_get(context, id) + + +#def project_get_by_uid(context, uid): +# """Get project by uid""" +# return IMPL.project_get_by_uid(context, uid) +# + +def project_create(context, values): + """Create a new project""" + return IMPL.project_create(context, values) + + +def project_add_member(context, project_id, user_id): + """Add user to project""" + return IMPL.project_add_member(context, project_id, user_id) + + +def project_get_all(context): + """Get all projects""" + return IMPL.project_get_all(context) + + +def project_get_by_user(context, user_id): + """Get all projects of which the given user is a member""" + return IMPL.project_get_by_user(context, user_id) + + +def project_remove_member(context, project_id, user_id): + """Remove the given user from the given project""" + return IMPL.project_remove_member(context, project_id, user_id) + + +def project_update(context, project_id, values): + """Update Remove the given user from the given project""" + return IMPL.project_update(context, project_id, values) + + +def project_delete(context, project_id): + """Delete project""" + return IMPL.project_delete(context, project_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 9c3caf9af..bd5c285d8 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -925,3 +925,202 @@ def volume_update(_context, volume_id, values): for (key, value) in values.iteritems(): volume_ref[key] = value volume_ref.save(session=session) + + +################### + + +def user_get(context, id): + return models.User.find(id, deleted=_deleted(context)) + + +def user_get_by_access_key(context, access_key): + session = get_session() + return session.query(models.User + ).filter_by(access_key=access_key + ).filter_by(deleted=_deleted(context) + ).first() + + +def user_create(_context, values): + user_ref = models.User() + for (key, value) in values.iteritems(): + user_ref[key] = value + user_ref.save() + return user_ref + + +def user_delete(context, id): + session = get_session() + with session.begin(): + session.execute('delete from user_project_association where user_id=:id', + {'id': id}) + session.execute('delete from user_role_association where user_id=:id', + {'id': id}) + session.execute('delete from user_project_role_association where user_id=:id', + {'id': id}) + user_ref = models.User.find(id, session=session) + session.delete(user_ref) + + +def user_get_all(context): + session = get_session() + return session.query(models.User + ).filter_by(deleted=_deleted(context) + ).all() + + +def project_create(_context, values): + project_ref = models.Project() + for (key, value) in values.iteritems(): + project_ref[key] = value + project_ref.save() + return project_ref + + +def project_add_member(context, project_id, user_id): + session = get_session() + with session.begin(): + project_ref = models.Project.find(project_id, session=session) + user_ref = models.User.find(user_id, session=session) + + project_ref.members += [user_ref] + project_ref.save(session=session) + + +def project_get(context, id): + session = get_session() + result = session.query(models.Project + ).filter_by(deleted=False + ).filter_by(id=id + ).options(joinedload_all('members') + ).first() + if not result: + raise exception.NotFound("No project with id %s" % id) + return result + + +def project_get_by_uid(context, uid): + session = get_session() + return session.query(models.Project + ).filter_by(uid=uid + ).filter_by(deleted=_deleted(context) + ).first() + + +def project_get_all(context): + session = get_session() + return session.query(models.Project + ).filter_by(deleted=_deleted(context) + ).options(joinedload_all('members') + ).all() + + +def project_get_by_user(context, user_id): + session = get_session() + user = session.query(models.User + ).filter_by(deleted=_deleted(context) + ).options(joinedload_all('projects') + ).first() + return user.projects + + +def project_remove_member(context, project_id, user_id): + session = get_session() + project = models.Project.find(project_id, session=session) + user = models.User.find(user_id, session=session) + if not project: + raise exception.NotFound('Project id "%s" not found' % (project_id,)) + + if not user: + raise exception.NotFound('User id "%s" not found' % (user_id,)) + + if user in project.members: + project.members.remove(user) + project.save(session=session) + + +def user_update(_context, user_id, values): + session = get_session() + with session.begin(): + user_ref = models.User.find(user_id, session=session) + for (key, value) in values.iteritems(): + user_ref[key] = value + user_ref.save(session=session) + + +def project_update(_context, project_id, values): + session = get_session() + with session.begin(): + project_ref = models.Project.find(project_id, session=session) + for (key, value) in values.iteritems(): + project_ref[key] = value + project_ref.save(session=session) + + +def project_delete(context, id): + session = get_session() + with session.begin(): + session.execute('delete from user_project_association where project_id=:id', + {'id': id}) + session.execute('delete from user_project_role_association where project_id=:id', + {'id': id}) + project_ref = models.Project.find(id, session=session) + session.delete(project_ref) + + +def user_get_roles(context, user_id): + session = get_session() + with session.begin(): + user_ref = models.User.find(user_id, session=session) + return [role.role for role in user_ref['roles']] + + +def user_get_roles_for_project(context, user_id, project_id): + session = get_session() + with session.begin(): + res = session.query(models.UserProjectRoleAssociation + ).filter_by(user_id=user_id + ).filter_by(project_id=project_id + ).all() + return [association.role for association in res] + +def user_remove_project_role(context, user_id, project_id, role): + session = get_session() + with session.begin(): + session.execute('delete from user_project_role_association where ' + \ + 'user_id=:user_id and project_id=:project_id and ' + \ + 'role=:role', { 'user_id' : user_id, + 'project_id' : project_id, + 'role' : role }) + + +def user_remove_role(context, user_id, role): + session = get_session() + with session.begin(): + res = session.query(models.UserRoleAssociation + ).filter_by(user_id=user_id + ).filter_by(role=role + ).all() + for role in res: + session.delete(role) + + +def user_add_role(context, user_id, role): + session = get_session() + with session.begin(): + user_ref = models.User.find(user_id, session=session) + models.UserRoleAssociation(user=user_ref, role=role).save(session=session) + + +def user_add_project_role(context, user_id, project_id, role): + session = get_session() + with session.begin(): + user_ref = models.User.find(user_id, session=session) + project_ref = models.Project.find(project_id, session=session) + models.UserProjectRoleAssociation(user_id=user_ref['id'], + project_id=project_ref['id'], + role=role).save(session=session) + + +################### diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 01e58b05e..b247eb416 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -27,7 +27,9 @@ import datetime from sqlalchemy.orm import relationship, backref, exc, object_mapper from sqlalchemy import Column, Integer, String from sqlalchemy import ForeignKey, DateTime, Boolean, Text +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import ForeignKeyConstraint from nova.db.sqlalchemy.session import get_session @@ -98,7 +100,13 @@ class NovaBase(object): if not session: session = get_session() session.add(self) - session.flush() + try: + session.flush() + except IntegrityError, e: + if str(e).endswith('is not unique'): + raise Exception.Duplicate(str(e)) + else: + raise def delete(self, session=None): """Delete this object""" @@ -456,6 +464,67 @@ class FixedIp(BASE, NovaBase): raise new_exc.__class__, new_exc, sys.exc_info()[2] +class User(BASE, NovaBase): + """Represents a user""" + __tablename__ = 'users' + id = Column(String(255), primary_key=True) + + name = Column(String(255)) + access_key = Column(String(255)) + secret_key = Column(String(255)) + + is_admin = Column(Boolean) + + +class Project(BASE, NovaBase): + """Represents a project""" + __tablename__ = 'projects' + id = Column(String(255), primary_key=True) + name = Column(String(255)) + description = Column(String(255)) + + project_manager = Column(String(255), ForeignKey(User.id)) + + members = relationship(User, + secondary='user_project_association', + backref='projects') + + +class UserProjectRoleAssociation(BASE, NovaBase): + __tablename__ = 'user_project_role_association' + user_id = Column(String(255), primary_key=True) + user = relationship(User, + primaryjoin=user_id==User.id, + foreign_keys=[User.id], + uselist=False) + + project_id = Column(String(255), primary_key=True) + project = relationship(Project, + primaryjoin=project_id==Project.id, + foreign_keys=[Project.id], + uselist=False) + + role = Column(String(255), primary_key=True) + ForeignKeyConstraint(['user_id', + 'project_id'], + ['user_project_association.user_id', + 'user_project_association.project_id']) + + +class UserRoleAssociation(BASE, NovaBase): + __tablename__ = 'user_role_association' + user_id = Column(String(255), ForeignKey('users.id'), primary_key=True) + user = relationship(User, backref='roles') + role = Column(String(255), primary_key=True) + + +class UserProjectAssociation(BASE, NovaBase): + __tablename__ = 'user_project_association' + user_id = Column(String(255), ForeignKey(User.id), primary_key=True) + project_id = Column(String(255), ForeignKey(Project.id), primary_key=True) + + + class FloatingIp(BASE, NovaBase): """Represents a floating ip that dynamically forwards to a fixed ip""" __tablename__ = 'floating_ips' @@ -486,7 +555,7 @@ def register_models(): from sqlalchemy import create_engine models = (Service, Instance, Volume, ExportDevice, FixedIp, FloatingIp, Network, NetworkIndex, - AuthToken) # , Image, Host) + AuthToken, UserProjectAssociation, User, Project) # , Image, Host) engine = create_engine(FLAGS.sql_connection, echo=False) for model in models: model.metadata.create_all(engine) diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index 1955bb417..99f7ab599 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -75,8 +75,9 @@ class user_and_project_generator(object): self.manager.delete_user(self.user) self.manager.delete_project(self.project) -class AuthManagerTestCase(test.TrialTestCase): +class AuthManagerTestCase(object): def setUp(self): + FLAGS.auth_driver = self.auth_driver super(AuthManagerTestCase, self).setUp() self.flags(connection_type='fake') self.manager = manager.AuthManager() @@ -320,6 +321,12 @@ class AuthManagerTestCase(test.TrialTestCase): self.assertEqual('secret', user.secret) self.assertTrue(user.is_admin()) +class AuthManagerLdapTestCase(AuthManagerTestCase, test.TrialTestCase): + auth_driver = 'nova.auth.ldapdriver.FakeLdapDriver' + +class AuthManagerDbTestCase(AuthManagerTestCase, test.TrialTestCase): + auth_driver = 'nova.auth.dbdriver.DbDriver' + if __name__ == "__main__": # TODO: Implement use_fake as an option diff --git a/nova/tests/fake_flags.py b/nova/tests/fake_flags.py index 8f4754650..4bbef8832 100644 --- a/nova/tests/fake_flags.py +++ b/nova/tests/fake_flags.py @@ -24,7 +24,7 @@ flags.DECLARE('volume_driver', 'nova.volume.manager') FLAGS.volume_driver = 'nova.volume.driver.FakeAOEDriver' FLAGS.connection_type = 'fake' FLAGS.fake_rabbit = True -FLAGS.auth_driver = 'nova.auth.ldapdriver.FakeLdapDriver' +FLAGS.auth_driver = 'nova.auth.dbdriver.DbDriver' flags.DECLARE('network_size', 'nova.network.manager') flags.DECLARE('num_networks', 'nova.network.manager') flags.DECLARE('fake_network', 'nova.network.manager') -- cgit From ae55b04a4b25c6756311f001e36b5bbd76675e8c Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Thu, 30 Sep 2010 18:48:45 -0400 Subject: Mongo bad, swift good. --- nova/objectstore/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/objectstore/__init__.py b/nova/objectstore/__init__.py index b8890ac03..ecad9be7c 100644 --- a/nova/objectstore/__init__.py +++ b/nova/objectstore/__init__.py @@ -22,7 +22,7 @@ .. automodule:: nova.objectstore :platform: Unix - :synopsis: Currently a trivial file-based system, getting extended w/ mongo. + :synopsis: Currently a trivial file-based system, getting extended w/ swift. .. moduleauthor:: Jesse Andrews .. moduleauthor:: Devin Carlen .. moduleauthor:: Vishvananda Ishaya -- cgit From c9e14d6257f0b488bd892c09d284091c0f612dd7 Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Fri, 1 Oct 2010 01:44:17 -0700 Subject: Locked down fixed ips and improved network tests --- nova/db/sqlalchemy/api.py | 98 ++++++++++++++++-------------------------- nova/tests/network_unittest.py | 44 ++++++++++--------- 2 files changed, 60 insertions(+), 82 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index fc5ee2235..860723516 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -78,7 +78,7 @@ def authorize_user_context(context, user_id): raise exception.NotAuthorized() -def use_deleted(context): +def can_read_deleted(context): """Indicates if the context has access to deleted objects.""" if not context: return False @@ -124,7 +124,7 @@ def service_get(context, service_id, session=None): result = session.query(models.Service ).filter_by(id=service_id - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).first() if not result: @@ -222,9 +222,8 @@ def service_get_by_args(context, host, binary): result = session.query(models.Service ).filter_by(host=host ).filter_by(binary=binary - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).first() - if not result: raise exception.NotFound('No service for %s, %s' % (host, binary)) @@ -256,7 +255,6 @@ def service_update(context, service_id, values): @require_context def floating_ip_allocate_address(context, host, project_id): authorize_project_context(context, project_id) - session = get_session() with session.begin(): floating_ip_ref = session.query(models.FloatingIp @@ -287,7 +285,6 @@ def floating_ip_create(context, values): @require_context def floating_ip_count_by_project(context, project_id): authorize_project_context(context, project_id) - session = get_session() return session.query(models.FloatingIp ).filter_by(project_id=project_id @@ -374,7 +371,6 @@ def floating_ip_get_all_by_host(context, host): @require_context def floating_ip_get_all_by_project(context, project_id): authorize_project_context(context, project_id) - session = get_session() return session.query(models.FloatingIp ).options(joinedload_all('fixed_ip.instance') @@ -391,7 +387,7 @@ def floating_ip_get_by_address(context, address, session=None): result = session.query(models.FloatingIp ).filter_by(address=address - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).first() if not result: raise exception.NotFound('No fixed ip for address %s' % address) @@ -406,6 +402,7 @@ def floating_ip_get_by_address(context, address, session=None): def fixed_ip_associate(context, address, instance_id): session = get_session() with session.begin(): + instance = instance_get(context, instance_id, session=session) fixed_ip_ref = session.query(models.FixedIp ).filter_by(address=address ).filter_by(deleted=False @@ -416,9 +413,7 @@ def fixed_ip_associate(context, address, instance_id): # then this has concurrency issues if not fixed_ip_ref: raise db.NoMoreAddresses() - fixed_ip_ref.instance = instance_get(context, - instance_id, - session=session) + fixed_ip_ref.instance = instance session.add(fixed_ip_ref) @@ -472,21 +467,21 @@ def fixed_ip_disassociate(context, address): @require_context def fixed_ip_get_by_address(context, address, session=None): - # TODO(devcamcar): Ensure floating ip belongs to user. - # Only possible if it is associated with an instance. - # May have to use system context for this always. if not session: session = get_session() result = session.query(models.FixedIp ).filter_by(address=address - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).options(joinedload('network') ).options(joinedload('instance') ).first() if not result: raise exception.NotFound('No floating ip for address %s' % address) + if is_user_context(context): + authorize_project_context(context, result.instance.project_id) + return result @@ -562,7 +557,7 @@ def instance_get(context, instance_id, session=None): if is_admin_context(context): result = session.query(models.Instance ).filter_by(id=instance_id - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).first() elif is_user_context(context): result = session.query(models.Instance @@ -581,7 +576,7 @@ def instance_get_all(context): session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).all() @@ -590,7 +585,7 @@ def instance_get_all_by_user(context, user_id): session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).filter_by(user_id=user_id ).all() @@ -603,7 +598,7 @@ def instance_get_all_by_project(context, project_id): return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') ).filter_by(project_id=project_id - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).all() @@ -615,7 +610,7 @@ def instance_get_all_by_reservation(context, reservation_id): return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') ).filter_by(reservation_id=reservation_id - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).all() elif is_user_context(context): return session.query(models.Instance @@ -633,7 +628,7 @@ def instance_get_by_ec2_id(context, ec2_id): if is_admin_context(context): result = session.query(models.Instance ).filter_by(ec2_id=ec2_id - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).first() elif is_user_context(context): result = session.query(models.Instance @@ -749,7 +744,7 @@ def key_pair_get(context, user_id, name, session=None): result = session.query(models.KeyPair ).filter_by(user_id=user_id ).filter_by(name=name - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).first() if not result: raise exception.NotFound('no keypair for user %s, name %s' % @@ -775,7 +770,7 @@ def key_pair_get_all_by_user(context, user_id): def network_count(context): session = get_session() return session.query(models.Network - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).count() @@ -847,7 +842,7 @@ def network_get(context, network_id, session=None): if is_admin_context(context): result = session.query(models.Network ).filter_by(id=network_id - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).first() elif is_user_context(context): result = session.query(models.Network @@ -914,7 +909,7 @@ def network_get_index(context, network_id): def network_index_count(context): session = get_session() return session.query(models.NetworkIndex - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).count() @@ -992,7 +987,7 @@ def queue_get_for(_context, topic, physical_node_id): def export_device_count(context): session = get_session() return session.query(models.ExportDevice - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).count() @@ -1038,7 +1033,7 @@ def quota_get(context, project_id, session=None): result = session.query(models.Quota ).filter_by(project_id=project_id - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).first() if not result: raise exception.NotFound('No quota for project_id %s' % project_id) @@ -1167,7 +1162,7 @@ def volume_get(context, volume_id, session=None): if is_admin_context(context): result = session.query(models.Volume ).filter_by(id=volume_id - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).first() elif is_user_context(context): result = session.query(models.Volume @@ -1184,7 +1179,7 @@ def volume_get(context, volume_id, session=None): @require_admin_context def volume_get_all(context): return session.query(models.Volume - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).all() @require_context @@ -1194,7 +1189,7 @@ def volume_get_all_by_project(context, project_id): session = get_session() return session.query(models.Volume ).filter_by(project_id=project_id - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).all() @@ -1206,7 +1201,7 @@ def volume_get_by_ec2_id(context, ec2_id): if is_admin_context(context): result = session.query(models.Volume ).filter_by(ec2_id=ec2_id - ).filter_by(deleted=use_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).first() elif is_user_context(context): result = session.query(models.Volume @@ -1233,47 +1228,26 @@ def volume_ec2_id_exists(context, ec2_id, session=None): ).one()[0] -@require_context +@require_admin_context def volume_get_instance(context, volume_id): session = get_session() - result = None - - if is_admin_context(context): - result = session.query(models.Volume - ).filter_by(id=volume_id - ).filter_by(deleted=use_deleted(context) - ).options(joinedload('instance') - ).first() - elif is_user_context(context): - result = session.query(models.Volume - ).filter_by(project_id=context.project.id - ).filter_by(deleted=False - ).options(joinedload('instance') - ).first() - else: - raise exception.NotAuthorized() - + result = session.query(models.Volume + ).filter_by(id=volume_id + ).filter_by(deleted=can_read_deleted(context) + ).options(joinedload('instance') + ).first() if not result: raise exception.NotFound('Volume %s not found' % ec2_id) return result.instance -@require_context +@require_admin_context def volume_get_shelf_and_blade(context, volume_id): session = get_session() - result = None - - if is_admin_context(context): - result = session.query(models.ExportDevice - ).filter_by(volume_id=volume_id - ).first() - elif is_user_context(context): - result = session.query(models.ExportDevice - ).join(models.Volume - ).filter(models.Volume.project_id==context.project.id - ).filter_by(volume_id=volume_id - ).first() + result = session.query(models.ExportDevice + ).filter_by(volume_id=volume_id + ).first() if not result: raise exception.NotFound('No export device found for volume %s' % volume_id) diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index e01d7cff9..e601c480c 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -49,7 +49,6 @@ class NetworkTestCase(test.TrialTestCase): self.user = self.manager.create_user('netuser', 'netuser', 'netuser') self.projects = [] self.network = utils.import_object(FLAGS.network_manager) - # TODO(devcamcar): Passing project=None is Bad(tm). self.context = context.APIRequestContext(project=None, user=self.user) for i in range(5): name = 'project%s' % i @@ -60,11 +59,9 @@ class NetworkTestCase(test.TrialTestCase): user_context = context.APIRequestContext(project=self.projects[i], user=self.user) self.network.set_network_host(user_context, self.projects[i].id) - instance_ref = db.instance_create(None, - {'mac_address': utils.generate_mac()}) + instance_ref = self._create_instance(0) self.instance_id = instance_ref['id'] - instance_ref = db.instance_create(None, - {'mac_address': utils.generate_mac()}) + instance_ref = self._create_instance(1) self.instance2_id = instance_ref['id'] def tearDown(self): # pylint: disable-msg=C0103 @@ -77,6 +74,15 @@ class NetworkTestCase(test.TrialTestCase): self.manager.delete_project(project) self.manager.delete_user(self.user) + def _create_instance(self, project_num, mac=None): + if not mac: + mac = utils.generate_mac() + project = self.projects[project_num] + self.context.project = project + return db.instance_create(self.context, + {'project_id': project.id, + 'mac_address': mac}) + def _create_address(self, project_num, instance_id=None): """Create an address in given project num""" if instance_id is None: @@ -84,6 +90,11 @@ class NetworkTestCase(test.TrialTestCase): self.context.project = self.projects[project_num] return self.network.allocate_fixed_ip(self.context, instance_id) + def _deallocate_address(self, project_num, address): + self.context.project = self.projects[project_num] + self.network.deallocate_fixed_ip(self.context, address) + + def test_public_network_association(self): """Makes sure that we can allocaate a public ip""" # TODO(vish): better way of adding floating ips @@ -134,14 +145,14 @@ class NetworkTestCase(test.TrialTestCase): lease_ip(address) lease_ip(address2) - self.network.deallocate_fixed_ip(self.context, address) + self._deallocate_address(0, address) release_ip(address) self.assertFalse(is_allocated_in_project(address, self.projects[0].id)) # First address release shouldn't affect the second self.assertTrue(is_allocated_in_project(address2, self.projects[1].id)) - self.network.deallocate_fixed_ip(self.context, address2) + self._deallocate_address(1, address2) release_ip(address2) self.assertFalse(is_allocated_in_project(address2, self.projects[1].id)) @@ -152,24 +163,19 @@ class NetworkTestCase(test.TrialTestCase): lease_ip(first) instance_ids = [] for i in range(1, 5): - mac = utils.generate_mac() - instance_ref = db.instance_create(None, - {'mac_address': mac}) + instance_ref = self._create_instance(i, mac=utils.generate_mac()) instance_ids.append(instance_ref['id']) address = self._create_address(i, instance_ref['id']) - mac = utils.generate_mac() - instance_ref = db.instance_create(None, - {'mac_address': mac}) + instance_ref = self._create_instance(i, mac=utils.generate_mac()) instance_ids.append(instance_ref['id']) address2 = self._create_address(i, instance_ref['id']) - mac = utils.generate_mac() - instance_ref = db.instance_create(None, - {'mac_address': mac}) + instance_ref = self._create_instance(i, mac=utils.generate_mac()) instance_ids.append(instance_ref['id']) address3 = self._create_address(i, instance_ref['id']) lease_ip(address) lease_ip(address2) lease_ip(address3) + self.context.project = self.projects[i] self.assertFalse(is_allocated_in_project(address, self.projects[0].id)) self.assertFalse(is_allocated_in_project(address2, @@ -185,7 +191,7 @@ class NetworkTestCase(test.TrialTestCase): for instance_id in instance_ids: db.instance_destroy(None, instance_id) release_ip(first) - self.network.deallocate_fixed_ip(self.context, first) + self._deallocate_address(0, first) def test_vpn_ip_and_port_looks_valid(self): """Ensure the vpn ip and port are reasonable""" @@ -246,9 +252,7 @@ class NetworkTestCase(test.TrialTestCase): addresses = [] instance_ids = [] for i in range(num_available_ips): - mac = utils.generate_mac() - instance_ref = db.instance_create(None, - {'mac_address': mac}) + instance_ref = self._create_instance(0) instance_ids.append(instance_ref['id']) address = self._create_address(0, instance_ref['id']) addresses.append(address) -- cgit From 7e020e743c138d542e957c24ea53c1ca7fbc757c Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Fri, 1 Oct 2010 13:03:57 +0200 Subject: Address a few comments from Todd. --- nova/db/api.py | 5 ----- nova/db/sqlalchemy/api.py | 8 -------- nova/db/sqlalchemy/models.py | 2 +- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index 703936002..eb4eee782 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -640,11 +640,6 @@ def project_get(context, id): return IMPL.project_get(context, id) -#def project_get_by_uid(context, uid): -# """Get project by uid""" -# return IMPL.project_get_by_uid(context, uid) -# - def project_create(context, values): """Create a new project""" return IMPL.project_create(context, values) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index bd5c285d8..5cd2d6d51 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1000,14 +1000,6 @@ def project_get(context, id): return result -def project_get_by_uid(context, uid): - session = get_session() - return session.query(models.Project - ).filter_by(uid=uid - ).filter_by(deleted=_deleted(context) - ).first() - - def project_get_all(context): session = get_session() return session.query(models.Project diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index b247eb416..92a68ab68 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -104,7 +104,7 @@ class NovaBase(object): session.flush() except IntegrityError, e: if str(e).endswith('is not unique'): - raise Exception.Duplicate(str(e)) + raise exception.Duplicate(str(e)) else: raise -- cgit From 033c464882c3d74ecd863abde767f37e7ad6a956 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Sat, 2 Oct 2010 12:39:47 +0200 Subject: Make _dhcp_file ensure the existence of the directory containing the files it returns. --- nova/network/linux_net.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 95f7fe2d0..37f9c8253 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -274,6 +274,9 @@ def _stop_dnsmasq(network): def _dhcp_file(vlan, kind): """Return path to a pid, leases or conf file for a vlan""" + if not os.path.exists(FLAGS.networks_path): + os.makedirs(FLAGS.networks_path) + return os.path.abspath("%s/nova-%s.%s" % (FLAGS.networks_path, vlan, kind)) -- cgit From 50fc372c1f4b5924b73de5c25100ce42166c4f12 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Sat, 2 Oct 2010 12:56:54 +0200 Subject: Adjust db api usage according to recent refactoring. --- nova/db/sqlalchemy/api.py | 89 +++++++++++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index b70e7dd4a..49d015716 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1278,18 +1278,39 @@ def volume_update(context, volume_id, values): ################### -def user_get(context, id): - return models.User.find(id, deleted=_deleted(context)) +@require_admin_context +def user_get(context, id, session=None): + if not session: + session = get_session() + + result = session.query(models.User + ).filter_by(id=id + ).filter_by(deleted=can_read_deleted(context) + ).first() + if not result: + raise exception.NotFound('No user for id %s' % id) -def user_get_by_access_key(context, access_key): - session = get_session() - return session.query(models.User + return result + + +@require_admin_context +def user_get_by_access_key(context, access_key, session=None): + if not session: + session = get_session() + + result = session.query(models.User ).filter_by(access_key=access_key - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).first() + if not result: + raise exception.NotFound('No user for id %s' % id) + + return result + +@require_admin_context def user_create(_context, values): user_ref = models.User() for (key, value) in values.iteritems(): @@ -1298,6 +1319,7 @@ def user_create(_context, values): return user_ref +@require_admin_context def user_delete(context, id): session = get_session() with session.begin(): @@ -1307,14 +1329,14 @@ def user_delete(context, id): {'id': id}) session.execute('delete from user_project_role_association where user_id=:id', {'id': id}) - user_ref = models.User.find(id, session=session) + user_ref = user_get(context, id, session=session) session.delete(user_ref) def user_get_all(context): session = get_session() return session.query(models.User - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).all() @@ -1329,29 +1351,33 @@ def project_create(_context, values): def project_add_member(context, project_id, user_id): session = get_session() with session.begin(): - project_ref = models.Project.find(project_id, session=session) - user_ref = models.User.find(user_id, session=session) + project_ref = project_get(context, project_id, session=session) + user_ref = user_get(context, user_id, session=session) project_ref.members += [user_ref] project_ref.save(session=session) -def project_get(context, id): - session = get_session() +def project_get(context, id, session=None): + if not session: + session = get_session() + result = session.query(models.Project ).filter_by(deleted=False ).filter_by(id=id ).options(joinedload_all('members') ).first() + if not result: raise exception.NotFound("No project with id %s" % id) + return result def project_get_all(context): session = get_session() return session.query(models.Project - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).options(joinedload_all('members') ).all() @@ -1359,7 +1385,7 @@ def project_get_all(context): def project_get_by_user(context, user_id): session = get_session() user = session.query(models.User - ).filter_by(deleted=_deleted(context) + ).filter_by(deleted=can_read_deleted(context) ).options(joinedload_all('projects') ).first() return user.projects @@ -1367,32 +1393,27 @@ def project_get_by_user(context, user_id): def project_remove_member(context, project_id, user_id): session = get_session() - project = models.Project.find(project_id, session=session) - user = models.User.find(user_id, session=session) - if not project: - raise exception.NotFound('Project id "%s" not found' % (project_id,)) - - if not user: - raise exception.NotFound('User id "%s" not found' % (user_id,)) + project = project_get(context, project_id, session=session) + user = user_get(context, user_id, session=session) if user in project.members: project.members.remove(user) project.save(session=session) -def user_update(_context, user_id, values): +def user_update(context, user_id, values): session = get_session() with session.begin(): - user_ref = models.User.find(user_id, session=session) + user_ref = user_get(context, user_id, session=session) for (key, value) in values.iteritems(): user_ref[key] = value user_ref.save(session=session) -def project_update(_context, project_id, values): +def project_update(context, project_id, values): session = get_session() with session.begin(): - project_ref = models.Project.find(project_id, session=session) + project_ref = project_get(context, project_id, session=session) for (key, value) in values.iteritems(): project_ref[key] = value project_ref.save(session=session) @@ -1405,17 +1426,17 @@ def project_delete(context, id): {'id': id}) session.execute('delete from user_project_role_association where project_id=:id', {'id': id}) - project_ref = models.Project.find(id, session=session) + project_ref = project_get(context, id, session=session) session.delete(project_ref) def user_get_roles(context, user_id): session = get_session() with session.begin(): - user_ref = models.User.find(user_id, session=session) + user_ref = user_get(context, user_id, session=session) return [role.role for role in user_ref['roles']] - + def user_get_roles_for_project(context, user_id, project_id): session = get_session() with session.begin(): @@ -1434,7 +1455,7 @@ def user_remove_project_role(context, user_id, project_id, role): 'project_id' : project_id, 'role' : role }) - + def user_remove_role(context, user_id, role): session = get_session() with session.begin(): @@ -1449,18 +1470,18 @@ def user_remove_role(context, user_id, role): def user_add_role(context, user_id, role): session = get_session() with session.begin(): - user_ref = models.User.find(user_id, session=session) + user_ref = user_get(context, user_id, session=session) models.UserRoleAssociation(user=user_ref, role=role).save(session=session) - + def user_add_project_role(context, user_id, project_id, role): session = get_session() with session.begin(): - user_ref = models.User.find(user_id, session=session) - project_ref = models.Project.find(project_id, session=session) + user_ref = user_get(context, user_id, session=session) + project_ref = project_get(context, project_id, session=session) models.UserProjectRoleAssociation(user_id=user_ref['id'], project_id=project_ref['id'], role=role).save(session=session) - + ################### -- cgit From 5945291281f239bd928cea1833ee5a5b6c3df523 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sat, 2 Oct 2010 12:42:09 +0100 Subject: Bug #653534: NameError on session_get in sqlalchemy.api.service_update Fix function call: session_get was meant to be service_get. --- nova/db/sqlalchemy/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 7f72f66b9..9a7c71a70 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -240,7 +240,7 @@ def service_create(context, values): def service_update(context, service_id, values): session = get_session() with session.begin(): - service_ref = session_get(context, service_id, session=session) + service_ref = service_get(context, service_id, session=session) for (key, value) in values.iteritems(): service_ref[key] = value service_ref.save(session=session) -- cgit From c66d550d208544799fdaf4646a846e9f9c0b6bc5 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sat, 2 Oct 2010 13:11:33 +0100 Subject: Bug #653560: AttributeError in VlanManager.periodic_tasks Pass the correct context to db.fixed_ip_disassociate_all_by_timeout. --- nova/network/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/network/manager.py b/nova/network/manager.py index ef1d01138..9580479e5 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -230,7 +230,7 @@ class VlanManager(NetworkManager): now = datetime.datetime.utcnow() timeout = FLAGS.fixed_ip_disassociate_timeout time = now - datetime.timedelta(seconds=timeout) - num = self.db.fixed_ip_disassociate_all_by_timeout(self, + num = self.db.fixed_ip_disassociate_all_by_timeout(context, self.host, time) if num: -- cgit From 12e43d9deb3984d2b7ccc91490ffa4c13eedbe2b Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sat, 2 Oct 2010 16:55:57 +0100 Subject: Bug #653651: XenAPI support completely broken by orm-refactor merge Matches changes in the database / model layer with corresponding fixes to nova.virt.xenapi. --- nova/virt/xenapi.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index 0d06b1fce..118e0b687 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -42,10 +42,12 @@ from twisted.internet import defer from twisted.internet import reactor from twisted.internet import task +from nova import db from nova import flags from nova import process from nova import utils from nova.auth.manager import AuthManager +from nova.compute import instance_types from nova.compute import power_state from nova.virt import images @@ -113,32 +115,24 @@ class XenAPIConnection(object): raise Exception('Attempted to create non-unique name %s' % instance.name) - if 'bridge_name' in instance.datamodel: - network_ref = \ - yield self._find_network_with_bridge( - instance.datamodel['bridge_name']) - else: - network_ref = None - - if 'mac_address' in instance.datamodel: - mac_address = instance.datamodel['mac_address'] - else: - mac_address = '' + network = db.project_get_network(None, instance.project_id) + network_ref = \ + yield self._find_network_with_bridge(network.bridge) - user = AuthManager().get_user(instance.datamodel['user_id']) - project = AuthManager().get_project(instance.datamodel['project_id']) + user = AuthManager().get_user(instance.user_id) + project = AuthManager().get_project(instance.project_id) vdi_uuid = yield self._fetch_image( - instance.datamodel['image_id'], user, project, True) + instance.image_id, user, project, True) kernel = yield self._fetch_image( - instance.datamodel['kernel_id'], user, project, False) + instance.kernel_id, user, project, False) ramdisk = yield self._fetch_image( - instance.datamodel['ramdisk_id'], user, project, False) + instance.ramdisk_id, user, project, False) vdi_ref = yield self._call_xenapi('VDI.get_by_uuid', vdi_uuid) vm_ref = yield self._create_vm(instance, kernel, ramdisk) yield self._create_vbd(vm_ref, vdi_ref, 0, True) if network_ref: - yield self._create_vif(vm_ref, network_ref, mac_address) + yield self._create_vif(vm_ref, network_ref, instance.mac_address) logging.debug('Starting VM %s...', vm_ref) yield self._call_xenapi('VM.start', vm_ref, False, False) logging.info('Spawning VM %s created %s.', instance.name, vm_ref) @@ -148,8 +142,9 @@ class XenAPIConnection(object): """Create a VM record. Returns a Deferred that gives the new VM reference.""" - mem = str(long(instance.datamodel['memory_kb']) * 1024) - vcpus = str(instance.datamodel['vcpus']) + instance_type = instance_types.INSTANCE_TYPES[instance.instance_type] + mem = str(long(instance_type['memory_mb']) * 1024 * 1024) + vcpus = str(instance_type['vcpus']) rec = { 'name_label': instance.name, 'name_description': '', -- cgit From 65e2bbc31a7e4ea5d8f9456c2ea5b54715305d11 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 3 Oct 2010 12:41:07 +0100 Subject: Bug #654023: nova-manage vpn commands broken, resulting in erroneous "Wrong number of arguments supplied" message Add a context of None to the call to db.instance_get_all. This is deprecated, but it's what all the other calls in this file do, and it's better than exploding, so it will do for now. --- bin/nova-manage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/nova-manage b/bin/nova-manage index bf3c67612..0b5869dfd 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -114,7 +114,7 @@ class VpnCommands(object): def _vpn_for(self, project_id): """Get the VPN instance for a project ID.""" - for instance in db.instance_get_all(): + for instance in db.instance_get_all(None): if (instance['image_id'] == FLAGS.vpn_image_id and not instance['state_description'] in ['shutting_down', 'shutdown'] -- cgit From a0498717e470eb6fd52a4f26101c3513d90a3974 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 3 Oct 2010 13:17:20 +0100 Subject: Bug #654034: nova-manage doesn't honour --verbose flag Honour the --verbose flag by setting the logging level to DEBUG. --- bin/nova-manage | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/nova-manage b/bin/nova-manage index bf3c67612..ce87b9437 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -52,6 +52,7 @@ CLI interface for nova management. """ +import logging import os import sys import time @@ -417,6 +418,10 @@ def main(): """Parse options and call the appropriate class/method.""" utils.default_flagfile('/etc/nova/nova-manage.conf') argv = FLAGS(sys.argv) + + if FLAGS.verbose: + logging.getLogger().setLevel(logging.DEBUG) + script_name = argv.pop(0) if len(argv) < 1: print script_name + " category action []" -- cgit From 4e45f9472a95207153d32c88df8396c633c67a5d Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Sun, 3 Oct 2010 20:22:35 +0200 Subject: s/APIRequestContext/get_admin_context/ <-- sudo for request contexts. --- nova/tests/network_unittest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index 5370966d2..59b0a36e4 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -56,8 +56,8 @@ class NetworkTestCase(test.TrialTestCase): 'netuser', name)) # create the necessary network data for the project - user_context = context.APIRequestContext(project=self.projects[i], - user=self.user) + user_context = context.get_admin_context(user=self.user) + self.network.set_network_host(user_context, self.projects[i].id) instance_ref = self._create_instance(0) self.instance_id = instance_ref['id'] -- cgit From 077c008546123291dbc89ac31b492df6d176e339 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 4 Oct 2010 11:53:27 +0200 Subject: Move manager_class instantiation and db.service_* calls out of nova.service.Service.__init__ into a new nova.service.Service.startService method which gets called by twisted. This delays opening db connections (and thus sqlite file creation) until after privileges have been shed by twisted. --- nova/service.py | 12 +++++++++--- nova/tests/scheduler_unittest.py | 10 ++++++++++ nova/tests/service_unittest.py | 3 +++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/nova/service.py b/nova/service.py index a6c186896..dadef3c48 100644 --- a/nova/service.py +++ b/nova/service.py @@ -52,11 +52,17 @@ class Service(object, service.Service): self.host = host self.binary = binary self.topic = topic - manager_class = utils.import_class(manager) - self.manager = manager_class(host=host, *args, **kwargs) + self.manager_class_name = manager + super(Service, self).__init__(*args, **kwargs) + self.saved_args, self.saved_kwargs = args, kwargs + + + def startService(self): + manager_class = utils.import_class(self.manager_class_name) + self.manager = manager_class(host=self.host, *self.saved_args, + **self.saved_kwargs) self.manager.init_host() self.model_disconnected = False - super(Service, self).__init__(*args, **kwargs) try: service_ref = db.service_get_by_args(None, self.host, diff --git a/nova/tests/scheduler_unittest.py b/nova/tests/scheduler_unittest.py index fde30f81e..53a8be144 100644 --- a/nova/tests/scheduler_unittest.py +++ b/nova/tests/scheduler_unittest.py @@ -117,10 +117,12 @@ class SimpleDriverTestCase(test.TrialTestCase): 'nova-compute', 'compute', FLAGS.compute_manager) + compute1.startService() compute2 = service.Service('host2', 'nova-compute', 'compute', FLAGS.compute_manager) + compute2.startService() hosts = self.scheduler.driver.hosts_up(self.context, 'compute') self.assertEqual(len(hosts), 2) compute1.kill() @@ -132,10 +134,12 @@ class SimpleDriverTestCase(test.TrialTestCase): 'nova-compute', 'compute', FLAGS.compute_manager) + compute1.startService() compute2 = service.Service('host2', 'nova-compute', 'compute', FLAGS.compute_manager) + compute2.startService() instance_id1 = self._create_instance() compute1.run_instance(self.context, instance_id1) instance_id2 = self._create_instance() @@ -153,10 +157,12 @@ class SimpleDriverTestCase(test.TrialTestCase): 'nova-compute', 'compute', FLAGS.compute_manager) + compute1.startService() compute2 = service.Service('host2', 'nova-compute', 'compute', FLAGS.compute_manager) + compute2.startService() instance_ids1 = [] instance_ids2 = [] for index in xrange(FLAGS.max_cores): @@ -184,10 +190,12 @@ class SimpleDriverTestCase(test.TrialTestCase): 'nova-volume', 'volume', FLAGS.volume_manager) + volume1.startService() volume2 = service.Service('host2', 'nova-volume', 'volume', FLAGS.volume_manager) + volume2.startService() volume_id1 = self._create_volume() volume1.create_volume(self.context, volume_id1) volume_id2 = self._create_volume() @@ -205,10 +213,12 @@ class SimpleDriverTestCase(test.TrialTestCase): 'nova-volume', 'volume', FLAGS.volume_manager) + volume1.startService() volume2 = service.Service('host2', 'nova-volume', 'volume', FLAGS.volume_manager) + volume2.startService() volume_ids1 = [] volume_ids2 = [] for index in xrange(FLAGS.max_gigabytes): diff --git a/nova/tests/service_unittest.py b/nova/tests/service_unittest.py index 06f80e82c..6afeec377 100644 --- a/nova/tests/service_unittest.py +++ b/nova/tests/service_unittest.py @@ -22,6 +22,8 @@ Unit Tests for remote procedure calls using queue import mox +from twisted.application.app import startApplication + from nova import exception from nova import flags from nova import rpc @@ -96,6 +98,7 @@ class ServiceTestCase(test.BaseTestCase): self.mox.ReplayAll() app = service.Service.create(host=host, binary=binary) + startApplication(app, False) self.assert_(app) # We're testing sort of weird behavior in how report_state decides -- cgit From 3fe309b6f1e8a592d7b2948f4c1cdc51a62d0ff4 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 4 Oct 2010 21:01:31 +0200 Subject: Add pylint thingamajig for startService (name defined by Twisted). --- nova/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/service.py b/nova/service.py index dadef3c48..115e0ff32 100644 --- a/nova/service.py +++ b/nova/service.py @@ -57,7 +57,7 @@ class Service(object, service.Service): self.saved_args, self.saved_kwargs = args, kwargs - def startService(self): + def startService(self): # pylint: disable-msg C0103 manager_class = utils.import_class(self.manager_class_name) self.manager = manager_class(host=self.host, *self.saved_args, **self.saved_kwargs) -- cgit