diff options
| author | Ken Pepple <ken.pepple@gmail.com> | 2011-04-21 10:29:11 -0700 |
|---|---|---|
| committer | Ken Pepple <ken.pepple@gmail.com> | 2011-04-21 10:29:11 -0700 |
| commit | f205dcf659697adaae0d85a042ea2ea7ffe5c1c7 (patch) | |
| tree | b2da90f0cb240450bc30b9d6ebc3c703145b9bb6 /nova/api | |
| parent | dfcdafde2dc202aa3325cd1cea8d809f56fdde57 (diff) | |
| parent | c796920198305e101c75bcbf4e027ba9e81975d7 (diff) | |
rebase trunk
Diffstat (limited to 'nova/api')
26 files changed, 396 insertions, 114 deletions
diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 0fedbbfad..747015af5 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -15,5 +15,3 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - -"""No-op __init__ for directory full of api goodies.""" diff --git a/nova/api/direct.py b/nova/api/direct.py index e5f33cee4..8ceae299c 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -44,14 +44,33 @@ from nova import utils from nova import wsgi +# Global storage for registering modules. ROUTES = {} def register_service(path, handle): + """Register a service handle at a given path. + + Services registered in this way will be made available to any instances of + nova.api.direct.Router. + + :param path: `routes` path, can be a basic string like "/path" + :param handle: an object whose methods will be made available via the api + + """ ROUTES[path] = handle class Router(wsgi.Router): + """A simple WSGI router configured via `register_service`. + + This is a quick way to attach multiple services to a given endpoint. + It will automatically load the routes registered in the `ROUTES` global. + + TODO(termie): provide a paste-deploy version of this. + + """ + def __init__(self, mapper=None): if mapper is None: mapper = routes.Mapper() @@ -66,6 +85,24 @@ class Router(wsgi.Router): class DelegatedAuthMiddleware(wsgi.Middleware): + """A simple and naive authentication middleware. + + Designed mostly to provide basic support for alternative authentication + schemes, this middleware only desires the identity of the user and will + generate the appropriate nova.context.RequestContext for the rest of the + application. This allows any middleware above it in the stack to + authenticate however it would like while only needing to conform to a + minimal interface. + + Expects two headers to determine identity: + - X-OpenStack-User + - X-OpenStack-Project + + This middleware is tied to identity management and will need to be kept + in sync with any changes to the way identity is dealt with internally. + + """ + def process_request(self, request): os_user = request.headers['X-OpenStack-User'] os_project = request.headers['X-OpenStack-Project'] @@ -74,6 +111,20 @@ class DelegatedAuthMiddleware(wsgi.Middleware): class JsonParamsMiddleware(wsgi.Middleware): + """Middleware to allow method arguments to be passed as serialized JSON. + + Accepting arguments as JSON is useful for accepting data that may be more + complex than simple primitives. + + In this case we accept it as urlencoded data under the key 'json' as in + json=<urlencoded_json> but this could be extended to accept raw JSON + in the POST body. + + Filters out the parameters `self`, `context` and anything beginning with + an underscore. + + """ + def process_request(self, request): if 'json' not in request.params: return @@ -92,6 +143,13 @@ class JsonParamsMiddleware(wsgi.Middleware): class PostParamsMiddleware(wsgi.Middleware): + """Middleware to allow method arguments to be passed as POST parameters. + + Filters out the parameters `self`, `context` and anything beginning with + an underscore. + + """ + def process_request(self, request): params_parsed = request.params params = {} @@ -106,12 +164,21 @@ class PostParamsMiddleware(wsgi.Middleware): class Reflection(object): - """Reflection methods to list available methods.""" + """Reflection methods to list available methods. + + This is an object that expects to be registered via register_service. + These methods allow the endpoint to be self-describing. They introspect + the exposed methods and provide call signatures and documentation for + them allowing quick experimentation. + + """ + def __init__(self): self._methods = {} self._controllers = {} def _gather_methods(self): + """Introspect available methods and generate documentation for them.""" methods = {} controllers = {} for route, handler in ROUTES.iteritems(): @@ -185,6 +252,16 @@ class Reflection(object): class ServiceWrapper(wsgi.Controller): + """Wrapper to dynamically povide a WSGI controller for arbitrary objects. + + With lightweight introspection allows public methods on the object to + be accesed via simple WSGI routing and parameters and serializes the + return values. + + Automatically used be nova.api.direct.Router to wrap registered instances. + + """ + def __init__(self, service_handle): self.service_handle = service_handle @@ -206,10 +283,14 @@ class ServiceWrapper(wsgi.Controller): # NOTE(vish): make sure we have no unicode keys for py2.6. params = dict([(str(k), v) for (k, v) in params.iteritems()]) result = method(context, **params) + if result is None or type(result) is str or type(result) is unicode: return result + try: - return self._serialize(result, req.best_match_content_type()) + content_type = req.best_match_content_type() + default_xmlns = self.get_default_xmlns(req) + return self._serialize(result, content_type, default_xmlns) except: raise exception.Error("returned non-serializable type: %s" % result) @@ -256,7 +337,16 @@ class Limited(object): class Proxy(object): - """Pretend a Direct API endpoint is an object.""" + """Pretend a Direct API endpoint is an object. + + This is mostly useful in testing at the moment though it should be easily + extendable to provide a basic API library functionality. + + In testing we use this to stub out internal objects to verify that results + from the API are serializable. + + """ + def __init__(self, app, prefix=None): self.app = app self.prefix = prefix diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index d7ad08d2f..6672e60bb 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -196,7 +196,7 @@ class APIRequest(object): elif isinstance(data, datetime.datetime): data_el.appendChild( xml.createTextNode(_database_to_isoformat(data))) - elif data != None: + elif data is not None: data_el.appendChild(xml.createTextNode(str(data))) return data_el diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 99520b302..4785d812a 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -110,7 +110,8 @@ class CloudController(object): 'genrootca.sh') start = os.getcwd() - os.makedirs(FLAGS.ca_path) + if not os.path.exists(FLAGS.ca_path): + os.makedirs(FLAGS.ca_path) os.chdir(FLAGS.ca_path) # TODO(vish): Do this with M2Crypto instead utils.runthis(_("Generating root CA: %s"), "sh", genrootca_sh_path) @@ -141,6 +142,11 @@ class CloudController(object): instance_ref = self.compute_api.get_all(ctxt, fixed_ip=address) if instance_ref is None: return None + + # This ensures that all attributes of the instance + # are populated. + instance_ref = db.instance_get(ctxt, instance_ref['id']) + mpi = self._get_mpi_data(ctxt, instance_ref['project_id']) if instance_ref['key_name']: keys = {'0': {'_name': instance_ref['key_name'], @@ -153,7 +159,7 @@ class CloudController(object): floating_ip = db.instance_get_floating_address(ctxt, instance_ref['id']) ec2_id = ec2utils.id_to_ec2_id(instance_ref['id']) - image_ec2_id = self._image_ec2_id(instance_ref['image_id'], 'machine') + image_ec2_id = self._image_ec2_id(instance_ref['image_id'], 'ami') data = { 'user-data': base64.b64decode(instance_ref['user_data']), 'meta-data': { @@ -181,9 +187,9 @@ class CloudController(object): 'mpi': mpi}} for image_type in ['kernel', 'ramdisk']: - if '%s_id' % image_type in instance_ref: + if instance_ref.get('%s_id' % image_type): ec2_id = self._image_ec2_id(instance_ref['%s_id' % image_type], - image_type) + self._image_type(image_type)) data['meta-data']['%s-id' % image_type] = ec2_id if False: # TODO(vish): store ancestor ids @@ -436,7 +442,7 @@ class CloudController(object): group_name) criteria = self._revoke_rule_args_to_dict(context, **kwargs) - if criteria == None: + if criteria is None: raise exception.ApiError(_("Not enough parameters to build a " "valid rule.")) @@ -658,7 +664,7 @@ class CloudController(object): 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')} def _convert_to_set(self, lst, label): - if lst == None or lst == []: + if lst is None or lst == []: return None if not isinstance(lst, list): lst = [lst] @@ -729,7 +735,10 @@ class CloudController(object): instance['project_id'], instance['host']) i['productCodesSet'] = self._convert_to_set([], 'product_codes') - i['instanceType'] = instance['instance_type'] + if instance['instance_type']: + i['instanceType'] = instance['instance_type'].get('name') + else: + i['instanceType'] = None i['launchTime'] = instance['created_at'] i['amiLaunchIndex'] = instance['launch_index'] i['displayName'] = instance['display_name'] @@ -784,7 +793,7 @@ class CloudController(object): def allocate_address(self, context, **kwargs): LOG.audit(_("Allocate address"), context=context) public_ip = self.network_api.allocate_floating_ip(context) - return {'addressSet': [{'publicIp': public_ip}]} + return {'publicIp': public_ip} def release_address(self, context, public_ip, **kwargs): LOG.audit(_("Release address %s"), public_ip, context=context) @@ -814,7 +823,7 @@ class CloudController(object): ramdisk = self._get_image(context, kwargs['ramdisk_id']) kwargs['ramdisk_id'] = ramdisk['id'] instances = self.compute_api.create(context, - instance_type=instance_types.get_by_type( + instance_type=instance_types.get_instance_type_by_name( kwargs.get('instance_type', None)), image_id=self._get_image(context, kwargs['image_id'])['id'], min_count=int(kwargs.get('min_count', max_count)), @@ -871,13 +880,27 @@ class CloudController(object): self.compute_api.update(context, instance_id=instance_id, **kwargs) return True - _type_prefix_map = {'machine': 'ami', - 'kernel': 'aki', - 'ramdisk': 'ari'} + @staticmethod + def _image_type(image_type): + """Converts to a three letter image type. - def _image_ec2_id(self, image_id, image_type='machine'): - prefix = self._type_prefix_map[image_type] - template = prefix + '-%08x' + aki, kernel => aki + ari, ramdisk => ari + anything else => ami + + """ + if image_type == 'kernel': + return 'aki' + if image_type == 'ramdisk': + return 'ari' + if image_type not in ['aki', 'ari']: + return 'ami' + return image_type + + @staticmethod + def _image_ec2_id(image_id, image_type='ami'): + """Returns image ec2_id using id and three letter type.""" + template = image_type + '-%08x' return ec2utils.id_to_ec2_id(int(image_id), template=template) def _get_image(self, context, ec2_id): @@ -885,32 +908,42 @@ class CloudController(object): internal_id = ec2utils.ec2_id_to_id(ec2_id) return self.image_service.show(context, internal_id) except exception.NotFound: - return self.image_service.show_by_name(context, ec2_id) + try: + return self.image_service.show_by_name(context, ec2_id) + except exception.NotFound: + raise exception.NotFound(_('Image %s not found') % ec2_id) def _format_image(self, image): """Convert from format defined by BaseImageService to S3 format.""" i = {} - image_type = image['properties'].get('type') + image_type = self._image_type(image.get('container_format')) ec2_id = self._image_ec2_id(image.get('id'), image_type) name = image.get('name') i['imageId'] = ec2_id kernel_id = image['properties'].get('kernel_id') if kernel_id: - i['kernelId'] = self._image_ec2_id(kernel_id, 'kernel') + i['kernelId'] = self._image_ec2_id(kernel_id, 'aki') ramdisk_id = image['properties'].get('ramdisk_id') if ramdisk_id: - i['ramdiskId'] = self._image_ec2_id(ramdisk_id, 'ramdisk') + i['ramdiskId'] = self._image_ec2_id(ramdisk_id, 'ari') i['imageOwnerId'] = image['properties'].get('owner_id') if name: i['imageLocation'] = "%s (%s)" % (image['properties']. get('image_location'), name) else: i['imageLocation'] = image['properties'].get('image_location') - i['imageState'] = image['properties'].get('image_state') + # NOTE(vish): fallback status if image_state isn't set + state = image.get('status') + if state == 'active': + state = 'available' + i['imageState'] = image['properties'].get('image_state', state) i['displayName'] = name i['description'] = image.get('description') - i['imageType'] = image_type - i['isPublic'] = str(image['properties'].get('is_public', '')) == 'True' + display_mapping = {'aki': 'kernel', + 'ari': 'ramdisk', + 'ami': 'machine'} + i['imageType'] = display_mapping.get(image_type) + i['isPublic'] = image.get('is_public') == True i['architecture'] = image['properties'].get('architecture') return i @@ -942,8 +975,9 @@ class CloudController(object): image_location = kwargs['name'] metadata = {'properties': {'image_location': image_location}} image = self.image_service.create(context, metadata) + image_type = self._image_type(image.get('container_format')) image_id = self._image_ec2_id(image['id'], - image['properties']['type']) + image_type) msg = _("Registered image %(image_location)s with" " id %(image_id)s") % locals() LOG.audit(msg, context=context) @@ -958,7 +992,7 @@ class CloudController(object): except exception.NotFound: raise exception.NotFound(_('Image %s not found') % image_id) result = {'imageId': image_id, 'launchPermission': []} - if image['properties']['is_public']: + if image['is_public']: result['launchPermission'].append({'group': 'all'}) return result @@ -983,7 +1017,7 @@ class CloudController(object): internal_id = image['id'] del(image['id']) - image['properties']['is_public'] = (operation_type == 'add') + image['is_public'] = (operation_type == 'add') return self.image_service.update(context, internal_id, image) def update_image(self, context, image_id, **kwargs): diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 7545eb0c9..5e76a06f7 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -34,6 +34,7 @@ from nova.api.openstack import consoles from nova.api.openstack import flavors from nova.api.openstack import images from nova.api.openstack import image_metadata +from nova.api.openstack import ips from nova.api.openstack import limits from nova.api.openstack import servers from nova.api.openstack import server_metadata @@ -144,6 +145,11 @@ class APIRouterV10(APIRouter): parent_resource=dict(member_name='server', collection_name='servers')) + mapper.resource("ip", "ips", controller=ips.Controller(), + collection=dict(public='GET', private='GET'), + parent_resource=dict(member_name='server', + collection_name='servers')) + class APIRouterV11(APIRouter): """Define routes specific to OpenStack API V1.1.""" diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py index 86066fa20..6e3763e47 100644 --- a/nova/api/openstack/accounts.py +++ b/nova/api/openstack/accounts.py @@ -13,15 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. -import common import webob.exc from nova import exception from nova import flags from nova import log as logging -from nova import wsgi from nova.auth import manager +from nova.api.openstack import common from nova.api.openstack import faults FLAGS = flags.FLAGS @@ -35,7 +34,7 @@ def _translate_keys(account): manager=account.project_manager_id) -class Controller(wsgi.Controller): +class Controller(common.OpenstackController): _serialization_metadata = { 'application/xml': { diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index f3a9bdeca..311e6bde9 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -55,6 +55,9 @@ class AuthMiddleware(wsgi.Middleware): user = self.get_user_by_authentication(req) accounts = self.auth.get_projects(user=user) if not user: + token = req.headers["X-Auth-Token"] + msg = _("%(user)s could not be found with token '%(token)s'") + LOG.warn(msg % locals()) return faults.Fault(webob.exc.HTTPUnauthorized()) if accounts: @@ -66,6 +69,8 @@ class AuthMiddleware(wsgi.Middleware): if not self.auth.is_admin(user) and \ not self.auth.is_project_member(user, account): + msg = _("%(user)s must be an admin or a member of %(account)s") + LOG.warn(msg % locals()) return faults.Fault(webob.exc.HTTPUnauthorized()) req.environ['nova.context'] = context.RequestContext(user, account) @@ -82,12 +87,16 @@ class AuthMiddleware(wsgi.Middleware): # honor it path_info = req.path_info if len(path_info) > 1: - return faults.Fault(webob.exc.HTTPUnauthorized()) + msg = _("Authentication requests must be made against a version " + "root (e.g. /v1.0 or /v1.1).") + LOG.warn(msg) + return faults.Fault(webob.exc.HTTPUnauthorized(explanation=msg)) try: username = req.headers['X-Auth-User'] key = req.headers['X-Auth-Key'] - except KeyError: + except KeyError as ex: + LOG.warn(_("Could not find %s in request.") % ex) return faults.Fault(webob.exc.HTTPUnauthorized()) token, user = self._authorize_user(username, key, req) @@ -100,6 +109,7 @@ class AuthMiddleware(wsgi.Middleware): res.headers['X-CDN-Management-Url'] = token.cdn_management_url res.content_type = 'text/plain' res.status = '204' + LOG.debug(_("Successfully authenticated '%s'") % username) return res else: return faults.Fault(webob.exc.HTTPUnauthorized()) @@ -139,6 +149,7 @@ class AuthMiddleware(wsgi.Middleware): try: user = self.auth.get_user_from_access_key(key) except exception.NotFound: + LOG.warn(_("User not found with provided API key.")) user = None if user and user.name == username: @@ -153,4 +164,9 @@ class AuthMiddleware(wsgi.Middleware): token_dict['user_id'] = user.id token = self.db.auth_token_create(ctxt, token_dict) return token, user + elif user and user.name != username: + msg = _("Provided API key is valid, but not for user " + "'%(username)s'") % locals() + LOG.warn(msg) + return None, None diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py index f2d2d86e8..4bf744046 100644 --- a/nova/api/openstack/backup_schedules.py +++ b/nova/api/openstack/backup_schedules.py @@ -19,7 +19,7 @@ import time from webob import exc -from nova import wsgi +from nova.api.openstack import common from nova.api.openstack import faults import nova.image.service @@ -29,7 +29,7 @@ def _translate_keys(inst): return dict(backupSchedule=inst) -class Controller(wsgi.Controller): +class Controller(common.OpenstackController): """ The backup schedule API controller for the Openstack API """ _serialization_metadata = { diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 75aeb0a5f..0b6dc944a 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -22,14 +22,19 @@ import webob from nova import exception from nova import flags from nova import log as logging +from nova import wsgi -LOG = logging.getLogger('common') +LOG = logging.getLogger('nova.api.openstack.common') FLAGS = flags.FLAGS +XML_NS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0' +XML_NS_V11 = 'http://docs.openstack.org/compute/api/v1.1' + + def limited(items, request, max_limit=FLAGS.osapi_max_limit): """ Return a slice of items according to requested offset and limit. @@ -111,8 +116,14 @@ def get_image_id_from_image_hash(image_service, context, image_hash): items = image_service.index(context) for image in items: image_id = image['id'] - if abs(hash(image_id)) == int(image_hash): - return image_id + try: + if abs(hash(image_id)) == int(image_hash): + return image_id + except ValueError: + msg = _("Requested image_id has wrong format: %s," + "should have numerical format") % image_id + LOG.error(msg) + raise Exception(msg) raise exception.NotFound(image_hash) @@ -128,3 +139,9 @@ def get_id_from_href(href): except: LOG.debug(_("Error extracting id from href: %s") % href) raise webob.exc.HTTPBadRequest(_('could not parse id from href')) + + +class OpenstackController(wsgi.Controller): + def get_default_xmlns(self, req): + # Use V10 by default + return XML_NS_V10 diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py index 8c291c2eb..1a77f25d7 100644 --- a/nova/api/openstack/consoles.py +++ b/nova/api/openstack/consoles.py @@ -19,7 +19,7 @@ from webob import exc from nova import console from nova import exception -from nova import wsgi +from nova.api.openstack import common from nova.api.openstack import faults @@ -43,7 +43,7 @@ def _translate_detail_keys(cons): return dict(console=info) -class Controller(wsgi.Controller): +class Controller(common.OpenstackController): """The Consoles Controller for the Openstack API""" _serialization_metadata = { diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py index 6efacce52..18de2ec71 100644 --- a/nova/api/openstack/contrib/volumes.py +++ b/nova/api/openstack/contrib/volumes.py @@ -322,8 +322,7 @@ class Volumes(extensions.ExtensionDescriptor): # Does this matter? res = extensions.ResourceExtension('volumes', VolumeController(), - collection_actions={'detail': 'GET'} - ) + collection_actions={'detail': 'GET'}) resources.append(res) res = extensions.ResourceExtension('volume_attachments', diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index fb1dccb28..7ea7afef6 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -28,6 +28,7 @@ from nova import exception from nova import flags from nova import log as logging from nova import wsgi +from nova.api.openstack import common from nova.api.openstack import faults @@ -115,7 +116,7 @@ class ExtensionDescriptor(object): return response_exts -class ActionExtensionController(wsgi.Controller): +class ActionExtensionController(common.OpenstackController): def __init__(self, application): @@ -136,7 +137,7 @@ class ActionExtensionController(wsgi.Controller): return res -class ResponseExtensionController(wsgi.Controller): +class ResponseExtensionController(common.OpenstackController): def __init__(self, application): self.application = application @@ -155,7 +156,8 @@ class ResponseExtensionController(wsgi.Controller): body = res.body headers = res.headers except AttributeError: - body = self._serialize(res, content_type) + default_xmlns = None + body = self._serialize(res, content_type, default_xmlns) headers = {"Content-Type": content_type} res = webob.Response() res.body = body @@ -163,7 +165,7 @@ class ResponseExtensionController(wsgi.Controller): return res -class ExtensionController(wsgi.Controller): +class ExtensionController(common.OpenstackController): def __init__(self, extension_manager): self.extension_manager = extension_manager diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 940bd8771..87118ce19 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -20,10 +20,10 @@ import webob.dec import webob.exc from nova import wsgi +from nova.api.openstack import common class Fault(webob.exc.HTTPException): - """An RS API fault response.""" _fault_names = { @@ -47,7 +47,7 @@ class Fault(webob.exc.HTTPException): """Generate a WSGI response based on the exception passed to ctor.""" # Replace the body with fault details. code = self.wrapped_exc.status_int - fault_name = self._fault_names.get(code, "computeFault") + fault_name = self._fault_names.get(code, "cloudServersFault") fault_data = { fault_name: { 'code': code, @@ -57,7 +57,8 @@ class Fault(webob.exc.HTTPException): 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(metadata) + default_xmlns = common.XML_NS_V10 + serializer = wsgi.Serializer(metadata, default_xmlns) content_type = req.best_match_content_type() self.wrapped_exc.body = serializer.serialize(fault_data, content_type) self.wrapped_exc.content_type = content_type diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index 5b99b5a6f..40787bd17 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -19,11 +19,11 @@ import webob from nova import db from nova import exception -from nova import wsgi +from nova.api.openstack import common from nova.api.openstack import views -class Controller(wsgi.Controller): +class Controller(common.OpenstackController): """Flavor controller for the OpenStack API.""" _serialization_metadata = { @@ -76,3 +76,6 @@ class ControllerV11(Controller): def _get_view_builder(self, req): base_url = req.application_url return views.flavors.ViewBuilderV11(base_url) + + def get_default_xmlns(self, req): + return common.XML_NS_V11 diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py index c9d6ac532..1eccc0174 100644 --- a/nova/api/openstack/image_metadata.py +++ b/nova/api/openstack/image_metadata.py @@ -18,15 +18,17 @@ from webob import exc from nova import flags +from nova import quota from nova import utils from nova import wsgi +from nova.api.openstack import common from nova.api.openstack import faults FLAGS = flags.FLAGS -class Controller(wsgi.Controller): +class Controller(common.OpenstackController): """The image metadata API controller for the Openstack API""" def __init__(self): @@ -39,6 +41,15 @@ class Controller(wsgi.Controller): metadata = image.get('properties', {}) return metadata + def _check_quota_limit(self, context, metadata): + if metadata is None: + return + num_metadata = len(metadata) + quota_metadata = quota.allowed_metadata_items(context, num_metadata) + if quota_metadata < num_metadata: + expl = _("Image metadata limit exceeded") + raise exc.HTTPBadRequest(explanation=expl) + def index(self, req, image_id): """Returns the list of metadata for a given instance""" context = req.environ['nova.context'] @@ -61,6 +72,7 @@ class Controller(wsgi.Controller): if 'metadata' in body: for key, value in body['metadata'].iteritems(): metadata[key] = value + self._check_quota_limit(context, metadata) img['properties'] = metadata self.image_service.update(context, image_id, img, None) return dict(metadata=metadata) @@ -77,6 +89,7 @@ class Controller(wsgi.Controller): img = self.image_service.show(context, image_id) metadata = self._get_metadata(context, image_id, img) metadata[id] = body[id] + self._check_quota_limit(context, metadata) img['properties'] = metadata self.image_service.update(context, image_id, img, None) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index e3f93f635..34d4c27fc 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -13,8 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import datetime - import webob.exc from nova import compute @@ -22,7 +20,6 @@ from nova import exception from nova import flags from nova import log from nova import utils -from nova import wsgi from nova.api.openstack import common from nova.api.openstack import faults from nova.api.openstack.views import images as images_view @@ -32,7 +29,7 @@ LOG = log.getLogger('nova.api.openstack.images') FLAGS = flags.FLAGS -class Controller(wsgi.Controller): +class Controller(common.OpenstackController): """Base `wsgi.Controller` for retrieving/displaying images.""" _serialization_metadata = { @@ -153,3 +150,6 @@ class ControllerV11(Controller): """Property to get the ViewBuilder class we need to use.""" base_url = request.application_url return images_view.ViewBuilderV11(base_url) + + def get_default_xmlns(self, req): + return common.XML_NS_V11 diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py new file mode 100644 index 000000000..778e9ba1a --- /dev/null +++ b/nova/api/openstack/ips.py @@ -0,0 +1,72 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import time + +from webob import exc + +import nova +import nova.api.openstack.views.addresses +from nova.api.openstack import common +from nova.api.openstack import faults + + +class Controller(common.OpenstackController): + """The servers addresses API controller for the Openstack API.""" + + _serialization_metadata = { + 'application/xml': { + 'list_collections': { + 'public': {'item_name': 'ip', 'item_key': 'addr'}, + 'private': {'item_name': 'ip', 'item_key': 'addr'}, + }, + }, + } + + def __init__(self): + self.compute_api = nova.compute.API() + self.builder = nova.api.openstack.views.addresses.ViewBuilderV10() + + def index(self, req, server_id): + try: + instance = self.compute_api.get(req.environ['nova.context'], id) + except nova.exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return {'addresses': self.builder.build(instance)} + + def public(self, req, server_id): + try: + instance = self.compute_api.get(req.environ['nova.context'], id) + except nova.exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return {'public': self.builder.build_public_parts(instance)} + + def private(self, req, server_id): + try: + instance = self.compute_api.get(req.environ['nova.context'], id) + except nova.exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return {'private': self.builder.build_private_parts(instance)} + + def show(self, req, server_id, id): + return faults.Fault(exc.HTTPNotImplemented()) + + def create(self, req, server_id): + return faults.Fault(exc.HTTPNotImplemented()) + + def delete(self, req, server_id, id): + return faults.Fault(exc.HTTPNotImplemented()) diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index efc7d193d..9877af191 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -31,8 +31,8 @@ from collections import defaultdict from webob.dec import wsgify from nova import wsgi +from nova.api.openstack import common from nova.api.openstack import faults -from nova.wsgi import Controller from nova.wsgi import Middleware @@ -43,7 +43,7 @@ PER_HOUR = 60 * 60 PER_DAY = 60 * 60 * 24 -class LimitsController(Controller): +class LimitsController(common.OpenstackController): """ Controller for accessing limits in the OpenStack API. """ diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index 45bbac99d..fd64ee4fb 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -18,11 +18,13 @@ from webob import exc from nova import compute +from nova import quota from nova import wsgi +from nova.api.openstack import common from nova.api.openstack import faults -class Controller(wsgi.Controller): +class Controller(common.OpenstackController): """ The server metadata API controller for the Openstack API """ def __init__(self): @@ -43,10 +45,14 @@ class Controller(wsgi.Controller): def create(self, req, server_id): context = req.environ['nova.context'] - body = self._deserialize(req.body, req.get_content_type()) - self.compute_api.update_or_create_instance_metadata(context, - server_id, - body['metadata']) + data = self._deserialize(req.body, req.get_content_type()) + metadata = data.get('metadata') + try: + self.compute_api.update_or_create_instance_metadata(context, + server_id, + metadata) + except quota.QuotaError as error: + self._handle_quota_error(error) return req.body def update(self, req, server_id, id): @@ -58,9 +64,13 @@ class Controller(wsgi.Controller): if len(body) > 1: expl = _('Request body contains too many items') raise exc.HTTPBadRequest(explanation=expl) - self.compute_api.update_or_create_instance_metadata(context, - server_id, - body) + try: + self.compute_api.update_or_create_instance_metadata(context, + server_id, + body) + except quota.QuotaError as error: + self._handle_quota_error(error) + return req.body def show(self, req, server_id, id): @@ -76,3 +86,9 @@ class Controller(wsgi.Controller): """ Deletes an existing metadata """ context = req.environ['nova.context'] self.compute_api.delete_instance_metadata(context, server_id, id) + + def _handle_quota_error(self, error): + """Reraise quota errors as api-specific http exceptions.""" + if error.code == "MetadataLimitExceeded": + raise exc.HTTPBadRequest(explanation=error.message) + raise error diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 6704a68ae..22a9c632c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -44,7 +44,7 @@ LOG = logging.getLogger('server') FLAGS = flags.FLAGS -class Controller(wsgi.Controller): +class Controller(common.OpenstackController): """ The Server API controller for the OpenStack API """ _serialization_metadata = { @@ -55,6 +55,13 @@ class Controller(wsgi.Controller): "imageRef"], "link": ["rel", "type", "href"], }, + "dict_collections": { + "metadata": {"item_name": "meta", "item_key": "key"}, + }, + "list_collections": { + "public": {"item_name": "ip", "item_key": "addr"}, + "private": {"item_name": "ip", "item_key": "addr"}, + }, }, } @@ -63,15 +70,6 @@ class Controller(wsgi.Controller): self._image_service = utils.import_object(FLAGS.image_service) super(Controller, self).__init__() - def ips(self, req, id): - try: - instance = self.compute_api.get(req.environ['nova.context'], id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - builder = self._get_addresses_view_builder(req) - return builder.build(instance) - def index(self, req): """ Returns a list of server names and ids for a given user """ return self._items(req, is_detail=False) @@ -120,6 +118,8 @@ class Controller(wsgi.Controller): context = req.environ['nova.context'] + password = self._get_server_admin_password(env['server']) + key_name = None key_data = None key_pairs = auth_manager.AuthManager.get_key_pairs(context) @@ -129,21 +129,16 @@ class Controller(wsgi.Controller): key_data = key_pair['public_key'] requested_image_id = self._image_id_from_req_data(env) - image_id = common.get_image_id_from_image_hash(self._image_service, - context, requested_image_id) + try: + image_id = common.get_image_id_from_image_hash(self._image_service, + context, requested_image_id) + except: + msg = _("Can not find requested image") + return faults.Fault(exc.HTTPBadRequest(msg)) + kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image( req, image_id) - # Metadata is a list, not a Dictionary, because we allow duplicate keys - # (even though JSON can't encode this) - # In future, we may not allow duplicate keys. - # However, the CloudServers API is not definitive on this front, - # and we want to be compatible. - metadata = [] - if env['server'].get('metadata'): - for k, v in env['server']['metadata'].items(): - metadata.append({'key': k, 'value': v}) - personality = env['server'].get('personality') injected_files = [] if personality: @@ -160,9 +155,11 @@ class Controller(wsgi.Controller): name = name.strip() try: + inst_type = \ + instance_types.get_instance_type_by_flavor_id(flavor_id) (inst,) = self.compute_api.create( context, - instance_types.get_by_flavor_id(flavor_id), + inst_type, image_id, kernel_id=kernel_id, ramdisk_id=ramdisk_id, @@ -170,17 +167,16 @@ class Controller(wsgi.Controller): display_description=name, key_name=key_name, key_data=key_data, - metadata=metadata, + metadata=env['server'].get('metadata', {}), injected_files=injected_files) except quota.QuotaError as error: self._handle_quota_error(error) - inst['instance_type'] = flavor_id + inst['instance_type'] = inst_type inst['image_id'] = requested_image_id builder = self._get_view_builder(req) server = builder.build(inst, is_detail=True) - password = utils.generate_password(16) server['server']['adminPass'] = password self.compute_api.set_admin_password(context, server['server']['id'], password) @@ -242,6 +238,10 @@ class Controller(wsgi.Controller): # if the original error is okay, just reraise it raise error + def _get_server_admin_password(self, server): + """ Determine the admin password for a server on creation """ + return utils.generate_password(16) + @scheduler_api.redirect_handler def update(self, req, id): """ Updates the server name or password """ @@ -565,7 +565,7 @@ class Controller(wsgi.Controller): _("Cannot build from image %(image_id)s, status not active") % locals()) - if image_meta['properties']['disk_format'] != 'ami': + if image_meta.get('container_format') != 'ami': return None, None try: @@ -648,6 +648,19 @@ class ControllerV11(Controller): def _limit_items(self, items, req): return common.limited_by_marker(items, req) + def _get_server_admin_password(self, server): + """ Determine the admin password for a server on creation """ + password = server.get('adminPass') + if password is None: + return utils.generate_password(16) + if not isinstance(password, basestring) or password == '': + msg = _("Invalid adminPass") + raise exc.HTTPBadRequest(msg) + return password + + def get_default_xmlns(self, req): + return common.XML_NS_V11 + class ServerCreateRequestXMLDeserializer(object): """ diff --git a/nova/api/openstack/shared_ip_groups.py b/nova/api/openstack/shared_ip_groups.py index ee7991d7f..996db3648 100644 --- a/nova/api/openstack/shared_ip_groups.py +++ b/nova/api/openstack/shared_ip_groups.py @@ -17,7 +17,7 @@ from webob import exc -from nova import wsgi +from nova.api.openstack import common from nova.api.openstack import faults @@ -32,7 +32,7 @@ def _translate_detail_keys(inst): return dict(sharedIpGroups=inst) -class Controller(wsgi.Controller): +class Controller(common.OpenstackController): """ The Shared IP Groups Controller for the Openstack API """ _serialization_metadata = { diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py index d3ab3d553..077ccfc79 100644 --- a/nova/api/openstack/users.py +++ b/nova/api/openstack/users.py @@ -18,7 +18,6 @@ from webob import exc from nova import exception from nova import flags from nova import log as logging -from nova import wsgi from nova.api.openstack import common from nova.api.openstack import faults from nova.auth import manager @@ -35,7 +34,7 @@ def _translate_keys(user): admin=user.admin) -class Controller(wsgi.Controller): +class Controller(common.OpenstackController): _serialization_metadata = { 'application/xml': { diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index 90c77855b..2810cce39 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -28,10 +28,16 @@ class ViewBuilder(object): class ViewBuilderV10(ViewBuilder): def build(self, inst): - private_ips = utils.get_from_path(inst, 'fixed_ip/address') - public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') + private_ips = self.build_private_parts(inst) + public_ips = self.build_public_parts(inst) return dict(public=public_ips, private=private_ips) + def build_public_parts(self, inst): + return utils.get_from_path(inst, 'fixed_ip/floating_ips/address') + + def build_private_parts(self, inst): + return utils.get_from_path(inst, 'fixed_ip/address') + class ViewBuilderV11(ViewBuilder): def build(self, inst): diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 16195b050..9dec8a355 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -34,11 +34,11 @@ class ViewBuilder(object): def _format_status(self, image): """Update the status field to standardize format.""" status_mapping = { - 'pending': 'queued', - 'decrypting': 'preparing', - 'untarring': 'saving', - 'available': 'active', - 'killed': 'failed', + 'pending': 'QUEUED', + 'decrypting': 'PREPARING', + 'untarring': 'SAVING', + 'available': 'ACTIVE', + 'killed': 'FAILED', } try: diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index d24c025be..e52bfaea3 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -82,7 +82,7 @@ class ViewBuilder(object): # Return the metadata as a dictionary metadata = {} for item in inst.get('metadata', []): - metadata[item['key']] = item['value'] + metadata[item['key']] = str(item['value']) inst_dict['metadata'] = metadata inst_dict['hostId'] = '' @@ -115,7 +115,7 @@ class ViewBuilderV10(ViewBuilder): def _build_flavor(self, response, inst): if 'instance_type' in dict(inst): - response['flavorId'] = inst['instance_type'] + response['flavorId'] = inst['instance_type']['flavorid'] class ViewBuilderV11(ViewBuilder): @@ -134,7 +134,7 @@ class ViewBuilderV11(ViewBuilder): def _build_flavor(self, response, inst): if "instance_type" in dict(inst): - flavor_id = inst["instance_type"] + flavor_id = inst["instance_type"]['flavorid'] flavor_ref = self.flavor_builder.generate_href(flavor_id) response["flavorRef"] = flavor_ref diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index 846cb48a1..227ffecdc 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -13,12 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. -import common - from nova import db from nova import flags from nova import log as logging -from nova import wsgi +from nova.api.openstack import common from nova.scheduler import api @@ -43,7 +41,7 @@ def _scrub_zone(zone): 'deleted', 'deleted_at', 'updated_at')) -class Controller(wsgi.Controller): +class Controller(common.OpenstackController): _serialization_metadata = { 'application/xml': { |
