From 65b9dfbea28f1607ef471e78b73ba77183d943f6 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Thu, 24 Feb 2011 01:53:01 -0800 Subject: capability aggregation working --- nova/api/openstack/zones.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index 989b3a235..c6c27dd4b 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -15,9 +15,10 @@ import common +from nova import db from nova import flags +from nova import log as logging from nova import wsgi -from nova import db from nova.scheduler import api @@ -67,8 +68,16 @@ class Controller(wsgi.Controller): def info(self, req): """Return name and capabilities for this zone.""" - return dict(zone=dict(name=FLAGS.zone_name, - capabilities=FLAGS.zone_capabilities)) + items = api.API().get_zone_capabilities(req.environ['nova.context']) + + zone = dict(name=FLAGS.zone_name) + caps = FLAGS.zone_capabilities.split(';') + for cap in caps: + key_values = cap.split(':') + zone[key_values[0]] = key_values[1] + for item, (min_value, max_value) in items.iteritems(): + zone[item] = "%s,%s" % (min_value, max_value) + return dict(zone=zone) def show(self, req, id): """Return data about the given zone id""" -- cgit From 47bbfaab52642f3ff79bcdefb8d705fb02b549f9 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Thu, 24 Feb 2011 15:23:15 -0800 Subject: new tests --- nova/api/openstack/zones.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index c6c27dd4b..fecbd6fa3 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -53,7 +53,7 @@ class Controller(wsgi.Controller): """Return all zones in brief""" # Ask the ZoneManager in the Scheduler for most recent data, # or fall-back to the database ... - items = api.API().get_zone_list(req.environ['nova.context']) + items = api.API.get_zone_list(req.environ['nova.context']) if not items: items = db.zone_get_all(req.environ['nova.context']) @@ -68,7 +68,7 @@ class Controller(wsgi.Controller): def info(self, req): """Return name and capabilities for this zone.""" - items = api.API().get_zone_capabilities(req.environ['nova.context']) + items = api.API.get_zone_capabilities(req.environ['nova.context']) zone = dict(name=FLAGS.zone_name) caps = FLAGS.zone_capabilities.split(';') -- cgit From 7825b7ce81dec97e997d296c3e30b5d143948abc Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 2 Mar 2011 01:21:54 -0800 Subject: initial commit of vnc support --- nova/api/ec2/cloud.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 844ccbe5e..aa9c6824e 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -539,6 +539,12 @@ class CloudController(object): return self.compute_api.get_ajax_console(context, instance_id=instance_id) + def get_vnc_console(self, context, instance_id, **kwargs): + ec2_id = instance_id + instance_id = ec2_id_to_id(ec2_id) + return self.compute_api.get_vnc_console(context, + instance_id=instance_id) + def describe_volumes(self, context, volume_id=None, **kwargs): if volume_id: volumes = [] -- cgit From 7afebad78de462918b89d61f5d8e0cee8bc11068 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Fri, 4 Mar 2011 13:45:43 -0500 Subject: Fix api logging to show proper path and controller:action. --- nova/api/ec2/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 5adc2c075..2493adc95 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -61,10 +61,13 @@ class RequestLogging(wsgi.Middleware): return rv def log_request_completion(self, response, request, start): - controller = request.environ.get('ec2.controller', None) - if controller: - controller = controller.__class__.__name__ - action = request.environ.get('ec2.action', None) + apireq = request.environ.get('ec2.request', None) + if apirequest: + controller = apireq.controller + action = apireq.action + else: + controller = None + action = None ctxt = request.environ.get('ec2.context', None) delta = utils.utcnow() - start seconds = delta.seconds @@ -75,7 +78,7 @@ class RequestLogging(wsgi.Middleware): microseconds, request.remote_addr, request.method, - request.path_info, + "%s%s" % (request.script_name, request.path_info), controller, action, response.status_int, -- cgit From ceccffaab6fb5fce3b0951b5a8eea65f523e8563 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Fri, 4 Mar 2011 19:13:27 -0500 Subject: apirequest -> apireq. --- nova/api/ec2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 2493adc95..b06289e67 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -62,7 +62,7 @@ class RequestLogging(wsgi.Middleware): def log_request_completion(self, response, request, start): apireq = request.environ.get('ec2.request', None) - if apirequest: + if apireq: controller = apireq.controller action = apireq.action else: -- cgit From 6b95c5133452ae26da2cb7f08267aa4cb056e7af Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 10 Mar 2011 15:05:04 -0500 Subject: Initial support fo extension resources. Tests. --- nova/api/openstack/__init__.py | 8 +++++++- nova/api/openstack/extensions.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 nova/api/openstack/extensions.py (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index ab9dbb780..28e2a1691 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -30,6 +30,7 @@ from nova import wsgi from nova.api.openstack import faults from nova.api.openstack import backup_schedules from nova.api.openstack import consoles +from nova.api.openstack import extensions from nova.api.openstack import flavors from nova.api.openstack import images from nova.api.openstack import servers @@ -68,7 +69,7 @@ class APIRouter(wsgi.Router): """Simple paste factory, :class:`nova.wsgi.Router` doesn't have one""" return cls() - def __init__(self): + def __init__(self, ext_manager=None): mapper = routes.Mapper() server_members = {'action': 'POST'} @@ -111,6 +112,11 @@ class APIRouter(wsgi.Router): collection={'detail': 'GET'}, controller=shared_ip_groups.Controller()) + if ext_manager is None: + ext_manager = extensions.ExtensionManager() + for resource in ext_manager.get_resources(): + resource.add_routes(mapper) + super(APIRouter, self).__init__(mapper) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py new file mode 100644 index 000000000..1c539c500 --- /dev/null +++ b/nova/api/openstack/extensions.py @@ -0,0 +1,29 @@ +# 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. + +class ExtensionManager(object): + + def get_resources(self): + """ + returns a list of ExtensionResource objects + """ + return [] + +class ExtensionResource(object): + + def add_routes(self, mapper): + pass -- cgit From b76b61dbec03455824b90c427eb816c15e284013 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Fri, 11 Mar 2011 10:32:09 -0800 Subject: Added volume api from previous megapatch --- nova/api/openstack/__init__.py | 6 ++ nova/api/openstack/volumes.py | 160 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 nova/api/openstack/volumes.py (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index ab9dbb780..a7b639669 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -34,6 +34,7 @@ from nova.api.openstack import flavors from nova.api.openstack import images from nova.api.openstack import servers from nova.api.openstack import shared_ip_groups +from nova.api.openstack import volumes from nova.api.openstack import zones @@ -111,6 +112,11 @@ class APIRouter(wsgi.Router): collection={'detail': 'GET'}, controller=shared_ip_groups.Controller()) + #NOTE(justinsb): volumes is not yet part of the official API + mapper.resource("volume", "volumes", + controller=volumes.Controller(), + collection={'detail': 'GET'}) + super(APIRouter, self).__init__(mapper) diff --git a/nova/api/openstack/volumes.py b/nova/api/openstack/volumes.py new file mode 100644 index 000000000..99300421e --- /dev/null +++ b/nova/api/openstack/volumes.py @@ -0,0 +1,160 @@ +# Copyright 2011 Justin Santa Barbara +# 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 webob import exc + +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + +FLAGS = flags.FLAGS + + +def _translate_detail_view(context, inst): + """ Maps keys for details view""" + + inst_dict = _translate_summary_view(context, inst) + + # No additional data / lookups at the moment + + return inst_dict + + +def _translate_summary_view(context, volume): + """ Maps keys for summary view""" + v = {} + + instance_id = None + # instance_data = None + attached_to = volume.get('instance') + if attached_to: + instance_id = attached_to['id'] + # instance_data = '%s[%s]' % (instance_ec2_id, + # attached_to['host']) + v['id'] = volume['id'] + v['status'] = volume['status'] + v['size'] = volume['size'] + v['availabilityZone'] = volume['availability_zone'] + v['createdAt'] = volume['created_at'] + # if context.is_admin: + # v['status'] = '%s (%s, %s, %s, %s)' % ( + # volume['status'], + # volume['user_id'], + # volume['host'], + # instance_data, + # volume['mountpoint']) + if volume['attach_status'] == 'attached': + v['attachments'] = [{'attachTime': volume['attach_time'], + 'deleteOnTermination': False, + 'mountpoint': volume['mountpoint'], + 'instanceId': instance_id, + 'status': 'attached', + 'volumeId': volume['id']}] + else: + v['attachments'] = [{}] + + v['displayName'] = volume['display_name'] + v['displayDescription'] = volume['display_description'] + return v + + +class Controller(wsgi.Controller): + """ The Volumes API controller for the OpenStack API """ + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "volume": [ + "id", + "status", + "size", + "availabilityZone", + "createdAt", + "displayName", + "displayDescription", + ]}}} + + def __init__(self): + self.volume_api = volume.API() + super(Controller, self).__init__() + + def show(self, req, id): + """Return data about the given volume""" + context = req.environ['nova.context'] + + try: + volume = self.volume_api.get(context, id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + return {'volume': _translate_detail_view(context, volume)} + + def delete(self, req, id): + """ Delete a volume """ + context = req.environ['nova.context'] + + LOG.audit(_("Delete volume with id: %s"), id, context=context) + + try: + self.volume_api.delete(context, volume_id=id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + + def index(self, req): + """ Returns a summary list of volumes""" + return self._items(req, entity_maker=_translate_summary_view) + + def detail(self, req): + """ Returns a detailed list of volumes """ + return self._items(req, entity_maker=_translate_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of volumes, transformed through entity_maker""" + context = req.environ['nova.context'] + + volumes = self.volume_api.get_all(context) + limited_list = common.limited(volumes, req) + res = [entity_maker(context, inst) for inst in limited_list] + return {'volumes': res} + + def create(self, req): + """Creates a new volume""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + vol = env['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + volume = self.volume_api.create(context, size, + vol.get('display_name'), + vol.get('display_description')) + + # Work around problem that instance is lazy-loaded... + volume['instance'] = None + + retval = _translate_detail_view(context, volume) + + return {'volume': retval} -- cgit From 7d4eae131f2f844f368aa5ff79c68191756775b6 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 11 Mar 2011 14:33:12 -0500 Subject: Add config for osapi_extensions_path. Update the ExtensionManager so that it loads extensions in the osapi_extensions_path. --- nova/api/openstack/__init__.py | 9 +++++---- nova/api/openstack/extensions.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 28e2a1691..8f6076511 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -69,7 +69,7 @@ class APIRouter(wsgi.Router): """Simple paste factory, :class:`nova.wsgi.Router` doesn't have one""" return cls() - def __init__(self, ext_manager=None): + def __init__(self, ext_mgr=None): mapper = routes.Mapper() server_members = {'action': 'POST'} @@ -112,9 +112,10 @@ class APIRouter(wsgi.Router): collection={'detail': 'GET'}, controller=shared_ip_groups.Controller()) - if ext_manager is None: - ext_manager = extensions.ExtensionManager() - for resource in ext_manager.get_resources(): + if ext_mgr is None: + ext_mgr = extensions.ExtensionManager(FLAGS.osapi_extensions_path) + for resource in ext_mgr.get_resources(): + print resource resource.add_routes(mapper) super(APIRouter, self).__init__(mapper) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 1c539c500..24846d9cd 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -15,15 +15,45 @@ # License for the specific language governing permissions and limitations # under the License. +import imp +import os +import sys + + class ExtensionManager(object): + def __init__(self, path): + + self.path = path + self.extensions = [] + self._load_extensions() + def get_resources(self): """ returns a list of ExtensionResource objects """ - return [] + resources = [] + for ext in self.extensions: + resources.append(ext.get_resources()) + return resources + + def _load_extensions(self): + if not os.path.exists(self.path): + return + + for f in os.listdir(self.path): + mod_name, file_ext = os.path.splitext(os.path.split(f)[-1]) + ext_path = os.path.join(self.path, f) + if file_ext.lower() == '.py': + mod = imp.load_source(mod_name, ext_path) + self.extensions.append(getattr(mod, 'get_extension')()) + class ExtensionResource(object): + """ + Example ExtensionResource object. All ExtensionResource objects should + adhere to this interface. + """ def add_routes(self, mapper): pass -- cgit From 2bfa7b29c7882da559041cea771b9243555828fa Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Sun, 13 Mar 2011 13:51:42 -0400 Subject: The extension name is constructed from the camel cased module_name + 'Extension'. --- nova/api/openstack/extensions.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 24846d9cd..13789863b 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -38,6 +38,12 @@ class ExtensionManager(object): return resources def _load_extensions(self): + """ + Load extensions from the configured path. The extension name is + constructed from the camel cased module_name + 'Extension'. If your + extension module was named widgets.py the extension class within that + module should be 'WidgetsExtension'. + """ if not os.path.exists(self.path): return @@ -46,7 +52,8 @@ class ExtensionManager(object): ext_path = os.path.join(self.path, f) if file_ext.lower() == '.py': mod = imp.load_source(mod_name, ext_path) - self.extensions.append(getattr(mod, 'get_extension')()) + ext_name = mod_name[0].upper() + mod_name[1:] + 'Extension' + self.extensions.append(getattr(mod, ext_name)()) class ExtensionResource(object): -- cgit From 229c5bc3324d5df39ca959d71a540a806bc5ad3e Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Mon, 14 Mar 2011 16:58:03 -0400 Subject: Implement action extensions. --- nova/api/openstack/__init__.py | 1 - nova/api/openstack/extensions.py | 106 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 9b7b76a91..8a458eea1 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -124,7 +124,6 @@ class APIRouter(wsgi.Router): if ext_mgr is None: ext_mgr = extensions.ExtensionManager(FLAGS.osapi_extensions_path) for resource in ext_mgr.get_resources(): - print resource resource.add_routes(mapper) super(APIRouter, self).__init__(mapper) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 13789863b..e41de3120 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -18,11 +18,101 @@ import imp import os import sys +import routes +import webob.dec +import webob.exc + +from nova import flags +from nova import log as logging +from nova import wsgi + + +LOG = logging.getLogger('extensions') + + +FLAGS = flags.FLAGS + + +class ExtensionActionController(wsgi.Controller): + + def __init__(self, application, action_name, handler): + + self.application = application + self.action_name = action_name + self.handler = handler + + def action(self, req, id): + + input_dict = self._deserialize(req.body, req.get_content_type()) + if self.action_name in input_dict: + return self.handler(input_dict, req, id) + # no action handler found (bump to downstream application) + res = self.application + return res + + +class ExtensionMiddleware(wsgi.Middleware): + """ + Extensions middleware that intercepts configured routes for extensions. + """ + @classmethod + def factory(cls, global_config, **local_config): + """ paste factory """ + def _factory(app): + return cls(app, **local_config) + return _factory + + def __init__(self, application, ext_mgr=None): + mapper = routes.Mapper() + + if ext_mgr is None: + ext_mgr = ExtensionManager(FLAGS.osapi_extensions_path) + + # create custom mapper connections for extended actions + for action in ext_mgr.get_actions(): + controller = ExtensionActionController(application, action.name, + action.handler) + mapper.connect("/%s/{id}/action.:(format)" % action.collection, + action='action', + controller=controller, + conditions=dict(method=['POST'])) + mapper.connect("/%s/{id}/action" % action.collection, + action='action', + controller=controller, + conditions=dict(method=['POST'])) + + self._router = routes.middleware.RoutesMiddleware(self._dispatch, + mapper) + + super(ExtensionMiddleware, self).__init__(application) + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + """ + Route the incoming request with router. + """ + req.environ['extended.app'] = self.application + return self._router + + @staticmethod + @webob.dec.wsgify(RequestClass=wsgi.Request) + def _dispatch(req): + """ + Called by self._router after matching the incoming request to a route + and putting the information into req.environ. Either returns the + routed WSGI app's response or defers to the extended application. + """ + match = req.environ['wsgiorg.routing_args'][1] + if not match: + return req.environ['extended.app'] + app = match['controller'] + return app class ExtensionManager(object): def __init__(self, path): + LOG.audit(_('Initializing extension manager.')) self.path = path self.extensions = [] @@ -37,6 +127,12 @@ class ExtensionManager(object): resources.append(ext.get_resources()) return resources + def get_actions(self): + actions = [] + for ext in self.extensions: + actions.extend(ext.get_actions()) + return actions + def _load_extensions(self): """ Load extensions from the configured path. The extension name is @@ -48,6 +144,7 @@ class ExtensionManager(object): return for f in os.listdir(self.path): + LOG.audit(_('Loading extension file: %s'), f) mod_name, file_ext = os.path.splitext(os.path.split(f)[-1]) ext_path = os.path.join(self.path, f) if file_ext.lower() == '.py': @@ -56,6 +153,15 @@ class ExtensionManager(object): self.extensions.append(getattr(mod, ext_name)()) +class ExtensionAction(object): + + def __init__(self, member, collection, name, handler): + self.member = member + self.collection = collection + self.name = name + self.handler = handler + + class ExtensionResource(object): """ Example ExtensionResource object. All ExtensionResource objects should -- cgit From a56a973e9d839df5bcd956126300afd7df4c2fe9 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Tue, 15 Mar 2011 00:37:13 +0000 Subject: Fixing API per spec, to get unit-tests to pass --- nova/api/openstack/images.py | 118 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 10 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 98f0dd96b..7b3800429 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -15,10 +15,13 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime + from webob import exc from nova import compute from nova import flags +from nova import log from nova import utils from nova import wsgi import nova.api.openstack @@ -27,6 +30,8 @@ from nova.api.openstack import faults import nova.image.service +LOG = log.getLogger('nova.api.openstack.images') + FLAGS = flags.FLAGS @@ -84,8 +89,6 @@ def _translate_status(item): # S3ImageService pass - return item - def _filter_keys(item, keys): """ @@ -104,6 +107,89 @@ def _convert_image_id_to_hash(image): image['id'] = image_id +def _translate_s3_like_images(image_metadata): + """Work-around for leaky S3ImageService abstraction""" + api_metadata = image_metadata.copy() + _convert_image_id_to_hash(api_metadata) + api_metadata = _translate_keys(api_metadata) + _translate_status(api_metadata) + return api_metadata + + +def _translate_metadata_for_api_detail(image_metadata): + """Translate from ImageService to OpenStack API style attribute names + + This involves 3 steps: + + 1. Translating required keys + + 2. Translating optional keys (ex. progress, serverId) + + 3. Formatting values according to API spec (for example dates must + look like "2010-08-10T12:00:00Z") + """ + service_metadata = image_metadata.copy() + api_metadata = {} + + # 1. Translate required keys + required_image_service2api = { + 'id': 'id', + 'name': 'name', + 'updated_at': 'updated', + 'created_at': 'created', + 'status': 'status'} + for service_attr, api_attr in required_image_service2api.items(): + api_metadata[api_attr] = service_metadata[service_attr] + + # 2. Translate optional keys + optional_image_service2api = {'instance_id': 'serverId'} + for service_attr, api_attr in optional_image_service2api.items(): + if service_attr in service_metadata: + api_metadata[api_attr] = service_metadata[service_attr] + + # 2a. Progress special case + # TODO(sirp): ImageService doesn't have a notion of progress yet, so for + # now just fake it + if service_metadata['status'] == 'saving': + api_metadata['progress'] = 0 + + # 3. Format values + + # 3a. Format Image Status (API requires uppercase) + status_service2api = {'queued': 'QUEUED', + 'preparing': 'PREPARING', + 'saving': 'SAVING', + 'active': 'ACTIVE', + 'killed': 'FAILED'} + api_metadata['status'] = status_service2api[api_metadata['status']] + + # 3b. Format timestamps + def _format_timestamp(dt_str): + """Return a timestamp formatted for OpenStack API + + NOTE(sirp): + + ImageService (specifically GlanceImageService) is currently + returning timestamps as strings. This should probably be datetime + objects. In the mean time, we work around this by using strptime() to + create datetime objects. + """ + if dt_str is None: + return None + + service_timestamp_fmt = "%Y-%m-%dT%H:%M:%S" + api_timestamp_fmt = "%Y-%m-%dT%H:%M:%SZ" + dt = datetime.datetime.strptime(dt_str, service_timestamp_fmt) + return dt.strftime(api_timestamp_fmt) + + for ts_attr in ('created', 'updated'): + if ts_attr in api_metadata: + formatted_timestamp = _format_timestamp(api_metadata[ts_attr]) + api_metadata[ts_attr] = formatted_timestamp + + return api_metadata + + class Controller(wsgi.Controller): _serialization_metadata = { @@ -125,16 +211,28 @@ class Controller(wsgi.Controller): def detail(self, req): """Return all public images in detail""" try: - items = self._service.detail(req.environ['nova.context']) + service_image_metas = self._service.detail( + req.environ['nova.context']) except NotImplementedError: - items = self._service.index(req.environ['nova.context']) - for image in items: - _convert_image_id_to_hash(image) + service_image_metas = self._service.index( + req.environ['nova.context']) - items = common.limited(items, req) - items = [_translate_keys(item) for item in items] - items = [_translate_status(item) for item in items] - return dict(images=items) + service_image_metas = common.limited(service_image_metas, req) + + # FIXME(sirp): The S3ImageService appears to be leaking implementation + # details, including its internal attribute names, and internal + # `status` values. Working around it for now. + s3_like_image = (service_image_metas and + ('imageId' in service_image_metas[0])) + if s3_like_image: + translate = _translate_s3_like_images + else: + translate = _translate_metadata_for_api_detail + + api_image_metas = [translate(service_image_meta) + for service_image_meta in service_image_metas] + + return dict(images=api_image_metas) def show(self, req, id): """Return data about the given image id""" -- cgit From f0141b1616e1b1fc9e52e33b37cc3a1091c57587 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Mon, 14 Mar 2011 22:24:34 -0400 Subject: Moved extended resource code into the extensions.py module. --- nova/api/openstack/__init__.py | 6 ------ nova/api/openstack/extensions.py | 29 +++++++++++++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 8a458eea1..ff91c77cf 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -31,7 +31,6 @@ from nova.api.openstack import accounts from nova.api.openstack import faults from nova.api.openstack import backup_schedules from nova.api.openstack import consoles -from nova.api.openstack import extensions from nova.api.openstack import flavors from nova.api.openstack import images from nova.api.openstack import servers @@ -121,11 +120,6 @@ class APIRouter(wsgi.Router): collection={'detail': 'GET'}, controller=shared_ip_groups.Controller()) - if ext_mgr is None: - ext_mgr = extensions.ExtensionManager(FLAGS.osapi_extensions_path) - for resource in ext_mgr.get_resources(): - resource.add_routes(mapper) - super(APIRouter, self).__init__(mapper) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index e41de3120..f32471051 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -68,7 +68,15 @@ class ExtensionMiddleware(wsgi.Middleware): if ext_mgr is None: ext_mgr = ExtensionManager(FLAGS.osapi_extensions_path) - # create custom mapper connections for extended actions + # extended resources + for resource in ext_mgr.get_resources(): + mapper.resource(resource.member, resource.collection, + controller=resource.controller, + collection=resource.collection_actions, + member=resource.member_actions, + parent_resource=resource.parent) + + # extended actions for action in ext_mgr.get_actions(): controller = ExtensionActionController(application, action.name, action.handler) @@ -124,10 +132,13 @@ class ExtensionManager(object): """ resources = [] for ext in self.extensions: - resources.append(ext.get_resources()) + resources.extend(ext.get_resources()) return resources def get_actions(self): + """ + returns a list of ExtensionAction objects + """ actions = [] for ext in self.extensions: actions.extend(ext.get_actions()) @@ -163,10 +174,12 @@ class ExtensionAction(object): class ExtensionResource(object): - """ - Example ExtensionResource object. All ExtensionResource objects should - adhere to this interface. - """ - def add_routes(self, mapper): - pass + def __init__(self, member, collection, controller, + parent=None, collection_actions={}, member_actions={}): + self.member = member + self.collection = collection + self.controller = controller + self.parent = parent + self.collection_actions = collection_actions + self.member_actions = member_actions -- cgit From ad6f82909060cd4d1d99a1b1a9f33aa2788d8c94 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Tue, 15 Mar 2011 05:37:08 +0000 Subject: serverId returned as int per spec --- nova/api/openstack/images.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 7b3800429..0c56b5f0d 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -116,7 +116,7 @@ def _translate_s3_like_images(image_metadata): return api_metadata -def _translate_metadata_for_api_detail(image_metadata): +def _translate_from_image_service_to_api(image_metadata): """Translate from ImageService to OpenStack API style attribute names This involves 3 steps: @@ -227,7 +227,7 @@ class Controller(wsgi.Controller): if s3_like_image: translate = _translate_s3_like_images else: - translate = _translate_metadata_for_api_detail + translate = _translate_from_image_service_to_api api_image_metas = [translate(service_image_meta) for service_image_meta in service_image_metas] -- cgit From f91d7925761f8204fdd46435ff57d74ae17483cf Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 15 Mar 2011 18:29:26 -0700 Subject: first pass openstack redirect working --- nova/api/openstack/servers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 85999764f..ffcbe628c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -343,6 +343,7 @@ class Controller(wsgi.Controller): """ Permit Admins to Pause the server. """ ctxt = req.environ['nova.context'] try: + LOG.debug(_("*** Compute.api::pause %s"), id) self.compute_api.pause(ctxt, id) except: readable = traceback.format_exc() -- cgit From 0053b776276e9cac617c812931c248be7e49fea2 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 15 Mar 2011 23:00:09 -0400 Subject: Add ResponseExtensions. --- nova/api/openstack/extensions.py | 144 ++++++++++++++++++++++++++++++++------- 1 file changed, 118 insertions(+), 26 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index f32471051..8b8806c8a 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -33,24 +33,43 @@ LOG = logging.getLogger('extensions') FLAGS = flags.FLAGS -class ExtensionActionController(wsgi.Controller): +class ActionExtensionController(wsgi.Controller): - def __init__(self, application, action_name, handler): + def __init__(self, application): self.application = application - self.action_name = action_name - self.handler = handler + self.action_handlers = {} + + def add_action(self, action_name, handler): + self.action_handlers[action_name] = handler def action(self, req, id): input_dict = self._deserialize(req.body, req.get_content_type()) - if self.action_name in input_dict: - return self.handler(input_dict, req, id) + for action_name, handler in self.action_handlers.iteritems(): + if action_name in input_dict: + return handler(input_dict, req, id) # no action handler found (bump to downstream application) res = self.application return res +class ResponseExtensionController(wsgi.Controller): + + def __init__(self, application): + self.application = application + self.handlers = [] + + def add_handler(self, handler): + self.handlers.append(handler) + + def process(self, req, *args, **kwargs): + res = req.get_response(self.application) + # currently response handlers are un-ordered + for handler in self.handlers: + return handler(res) + + class ExtensionMiddleware(wsgi.Middleware): """ Extensions middleware that intercepts configured routes for extensions. @@ -62,33 +81,80 @@ class ExtensionMiddleware(wsgi.Middleware): return cls(app, **local_config) return _factory + def _actions_by_collection(self, application, ext_mgr): + """ + Return a dict of ActionExtensionController objects by collection + """ + action_controllers = {} + for action in ext_mgr.get_actions(): + if not action.collection in action_controllers.keys(): + controller = ActionExtensionController(application) + action_controllers[action.collection] = controller + return action_controllers + + def _responses_by_collection(self, application, ext_mgr): + """ + Return a dict of ResponseExtensionController objects by collection + """ + response_ext_controllers = {} + for resp_ext in ext_mgr.get_response_extensions(): + if not resp_ext.url_route in response_ext_controllers.keys(): + controller = ResponseExtensionController(application) + response_ext_controllers[resp_ext.url_route] = controller + return response_ext_controllers + def __init__(self, application, ext_mgr=None): - mapper = routes.Mapper() if ext_mgr is None: ext_mgr = ExtensionManager(FLAGS.osapi_extensions_path) + self.ext_mgr = ext_mgr + + mapper = routes.Mapper() # extended resources for resource in ext_mgr.get_resources(): - mapper.resource(resource.member, resource.collection, + LOG.debug(_('Extended resource: %s'), + resource.collection) + mapper.resource(resource.collection, resource.collection, controller=resource.controller, collection=resource.collection_actions, member=resource.member_actions, parent_resource=resource.parent) # extended actions + action_controllers = self._actions_by_collection(application, ext_mgr) for action in ext_mgr.get_actions(): - controller = ExtensionActionController(application, action.name, - action.handler) - mapper.connect("/%s/{id}/action.:(format)" % action.collection, + LOG.debug(_('Extended collection/action: %s/%s'), + action.collection, + action.action_name) + controller = action_controllers[action.collection] + controller.add_action(action.action_name, action.handler) + + mapper.connect("/%s/:(id)/action.:(format)" % action.collection, action='action', controller=controller, conditions=dict(method=['POST'])) - mapper.connect("/%s/{id}/action" % action.collection, + mapper.connect("/%s/:(id)/action" % action.collection, action='action', controller=controller, conditions=dict(method=['POST'])) + # extended responses + resp_controllers = self._responses_by_collection(application, ext_mgr) + for response_ext in ext_mgr.get_response_extensions(): + LOG.debug(_('Extended response: %s'), response_ext.url_route) + controller = resp_controllers[response_ext.url_route] + controller.add_handler(response_ext.handler) + mapper.connect(response_ext.url_route + '.:(format)', + action='process', + controller=controller, + conditions=response_ext.conditions) + + mapper.connect(response_ext.url_route, + action='process', + controller=controller, + conditions=response_ext.conditions) + self._router = routes.middleware.RoutesMiddleware(self._dispatch, mapper) @@ -106,9 +172,8 @@ class ExtensionMiddleware(wsgi.Middleware): @webob.dec.wsgify(RequestClass=wsgi.Request) def _dispatch(req): """ - Called by self._router after matching the incoming request to a route - and putting the information into req.environ. Either returns the - routed WSGI app's response or defers to the extended application. + Returns the routed WSGI app's response or defers to the extended + application. """ match = req.environ['wsgiorg.routing_args'][1] if not match: @@ -128,7 +193,7 @@ class ExtensionManager(object): def get_resources(self): """ - returns a list of ExtensionResource objects + returns a list of ResourceExtension objects """ resources = [] for ext in self.extensions: @@ -137,13 +202,22 @@ class ExtensionManager(object): def get_actions(self): """ - returns a list of ExtensionAction objects + returns a list of ActionExtension objects """ actions = [] for ext in self.extensions: actions.extend(ext.get_actions()) return actions + def get_response_extensions(self): + """ + returns a list of ResponseExtension objects + """ + response_exts = [] + for ext in self.extensions: + response_exts.extend(ext.get_response_extensions()) + return response_exts + def _load_extensions(self): """ Load extensions from the configured path. The extension name is @@ -160,24 +234,42 @@ class ExtensionManager(object): ext_path = os.path.join(self.path, f) if file_ext.lower() == '.py': mod = imp.load_source(mod_name, ext_path) - ext_name = mod_name[0].upper() + mod_name[1:] + 'Extension' + ext_name = mod_name[0].upper() + mod_name[1:] self.extensions.append(getattr(mod, ext_name)()) -class ExtensionAction(object): +class ResponseExtension(object): + """ + ResponseExtension objects can be used to add data to responses from + core nova OpenStack API controllers. + """ - def __init__(self, member, collection, name, handler): - self.member = member + def __init__(self, url_route, method, handler): + self.url_route = url_route + self.conditions = dict(method=[method]) + self.handler = handler + + +class ActionExtension(object): + """ + ActionExtension objects can be used to add custom actions to core nova + nova OpenStack API controllers. + """ + + def __init__(self, collection, action_name, handler): self.collection = collection - self.name = name + self.action_name = action_name self.handler = handler -class ExtensionResource(object): +class ResourceExtension(object): + """ + ResourceExtension objects can be used to add add top level resources + to the OpenStack API in nova. + """ - def __init__(self, member, collection, controller, - parent=None, collection_actions={}, member_actions={}): - self.member = member + def __init__(self, collection, controller, parent=None, + collection_actions={}, member_actions={}): self.collection = collection self.controller = controller self.parent = parent -- cgit From 7a61965908ebfc076ad3b1d9cdc5773ade50bf75 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 15 Mar 2011 20:30:27 -0700 Subject: response working --- nova/api/zone_redirect.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 nova/api/zone_redirect.py (limited to 'nova/api') diff --git a/nova/api/zone_redirect.py b/nova/api/zone_redirect.py new file mode 100644 index 000000000..5e40a82dd --- /dev/null +++ b/nova/api/zone_redirect.py @@ -0,0 +1,100 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License.import datetime + +"""Reroutes calls to child zones on ZoneRouteException's.""" + +import httplib +import re +import webob +import webob.dec +import webob.exc +import urlparse +import urllib + +from nova import exception +from nova import log as logging +from nova import wsgi + +import novaclient.client as client + +try: + import json +except ImportError: + import simplejson as json + + +LOG = logging.getLogger('server') + + +class ZoneRedirectMiddleware(wsgi.Middleware): + """Catches Zone Routing exceptions and delegates the call + to child zones.""" + + @webob.dec.wsgify + def __call__(self, req): + try: + return req.get_response(self.application) + except exception.ZoneRouteException as e: + if len(e.zones) == 0: + exc = webob.exc.HTTPInternalServerError(explanation= + _("No zones to reroute to.")) + return faults.Fault(exc) + + zone = e.zones[0] + # Todo(sandy): This only works for OpenStack API currently. + # Needs to be broken out into a driver. + url = zone.api_url + LOG.info(_("Zone redirect to:[url:%(api_url)s, username:%(username)s]" + % dict(api_url=zone.api_url, username=zone.username))) + + LOG.info(_("Zone Initial Req: %s"), req) + nova = client.OpenStackClient(zone.username, zone.password, + zone.api_url) + nova.authenticate() + new_req = req.copy() + #m = re.search('(https?://.+)/(v\d+\.\d+)/', url) + + scheme, netloc, path, query, frag = urlparse.urlsplit(new_req.path_qs) + query = urlparse.parse_qsl(query) + LOG.debug("**** QUERY=%s^%s^%s", path, query, frag) + query = [(key, value) for key, value in query if key != 'fresh'] + query = urllib.urlencode(query) + url = urlparse.urlunsplit((scheme, netloc, path, query, frag)) + + m = re.search('/(v\d+\.\d+)/(.+)', url) + version = m.group(1) + resource = m.group(2) + + LOG.info(_("New Request Data: %s"), new_req.body) + #LOG.info(_("New Request Headers: %s"), new_req.headers) + LOG.info(_("New Request Path: %s"), resource) + if req.method == 'GET': + response, body = nova.get(resource, body=new_req.body) + elif req.method == 'POST': + response, body = nova.post(resource, body=new_req.body) + elif req.method == 'PUT': + response, body = nova.put(resource, body=new_req.body) + elif req.method == 'DELETE': + response, body = nova.delete(resource, body=new_req.body) + #response, body = nova.request(req.path_qs, headers=new_req.headers, body=new_req.body) + LOG.info(_("Zone Response: %s / %s"), response, body) + res = webob.Response() + res.status = response['status'] + res.content_type = response['content-type'] + res.body = json.dumps(body) + LOG.info(_("Zone WebOb Response: %s"), res) + return res -- cgit From ddeede35a5036aa3c80742fde69468aedbc74892 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 16 Mar 2011 06:09:00 -0700 Subject: Error codes handled properly now --- nova/api/zone_redirect.py | 94 ++++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 41 deletions(-) (limited to 'nova/api') diff --git a/nova/api/zone_redirect.py b/nova/api/zone_redirect.py index 5e40a82dd..ad47a6216 100644 --- a/nova/api/zone_redirect.py +++ b/nova/api/zone_redirect.py @@ -30,6 +30,7 @@ from nova import log as logging from nova import wsgi import novaclient.client as client +import novaclient.exceptions as osexceptions try: import json @@ -49,52 +50,63 @@ class ZoneRedirectMiddleware(wsgi.Middleware): try: return req.get_response(self.application) except exception.ZoneRouteException as e: - if len(e.zones) == 0: + if not e.zones: exc = webob.exc.HTTPInternalServerError(explanation= _("No zones to reroute to.")) return faults.Fault(exc) - zone = e.zones[0] # Todo(sandy): This only works for OpenStack API currently. # Needs to be broken out into a driver. - url = zone.api_url - LOG.info(_("Zone redirect to:[url:%(api_url)s, username:%(username)s]" - % dict(api_url=zone.api_url, username=zone.username))) - - LOG.info(_("Zone Initial Req: %s"), req) - nova = client.OpenStackClient(zone.username, zone.password, - zone.api_url) - nova.authenticate() - new_req = req.copy() - #m = re.search('(https?://.+)/(v\d+\.\d+)/', url) - - scheme, netloc, path, query, frag = urlparse.urlsplit(new_req.path_qs) - query = urlparse.parse_qsl(query) - LOG.debug("**** QUERY=%s^%s^%s", path, query, frag) - query = [(key, value) for key, value in query if key != 'fresh'] - query = urllib.urlencode(query) - url = urlparse.urlunsplit((scheme, netloc, path, query, frag)) - - m = re.search('/(v\d+\.\d+)/(.+)', url) - version = m.group(1) - resource = m.group(2) - - LOG.info(_("New Request Data: %s"), new_req.body) - #LOG.info(_("New Request Headers: %s"), new_req.headers) - LOG.info(_("New Request Path: %s"), resource) - if req.method == 'GET': - response, body = nova.get(resource, body=new_req.body) - elif req.method == 'POST': - response, body = nova.post(resource, body=new_req.body) - elif req.method == 'PUT': - response, body = nova.put(resource, body=new_req.body) - elif req.method == 'DELETE': - response, body = nova.delete(resource, body=new_req.body) - #response, body = nova.request(req.path_qs, headers=new_req.headers, body=new_req.body) - LOG.info(_("Zone Response: %s / %s"), response, body) + for zone in e.zones: + url = zone.api_url + LOG.info(_("Zone redirect to:[url:%(api_url)s, " + "username:%(username)s]" + % dict(api_url=zone.api_url, + username=zone.username))) + + nova = client.OpenStackClient(zone.username, zone.password, + zone.api_url) + nova.authenticate() + new_req = req.copy() + + scheme, netloc, path, query, frag = \ + urlparse.urlsplit(new_req.path_qs) + query = urlparse.parse_qsl(query) + query = [(key, value) for key, value in query if key != 'fresh'] + query = urllib.urlencode(query) + url = urlparse.urlunsplit((scheme, netloc, path, query, frag)) + + m = re.search('/(v\d+\.\d+)/(.+)', url) + version = m.group(1) + resource = m.group(2) + + #LOG.info(_("New Request Data: %s"), new_req.body) + #LOG.info(_("New Request Path: %s"), resource) + try: + if req.method == 'GET': + response, body = nova.get(resource, body=new_req.body) + elif req.method == 'POST': + response, body = nova.post(resource, body=new_req.body) + elif req.method == 'PUT': + response, body = nova.put(resource, body=new_req.body) + elif req.method == 'DELETE': + response, body = nova.delete(resource, + body=new_req.body) + except osexceptions.OpenStackException, e: + LOG.info(_("Zone returned error: %s ('%s', '%s')"), + e.code, e.message, e.details) + continue + + LOG.info(_("Zone Response: %s [%s]/ %s"), response, + response.status, body) + if response.status == 200: + res = webob.Response() + res.status = response['status'] + res.content_type = response['content-type'] + res.body = json.dumps(body) + return res + + LOG.info(_("Returning 404 ...")) res = webob.Response() - res.status = response['status'] - res.content_type = response['content-type'] - res.body = json.dumps(body) - LOG.info(_("Zone WebOb Response: %s"), res) + res.status = "404" return res -- cgit From 8dffae687e78a1fa2a8cf0d321d64ee95a35cc1f Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 16 Mar 2011 06:47:27 -0700 Subject: Checks locally before routing --- nova/api/zone_redirect.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) (limited to 'nova/api') diff --git a/nova/api/zone_redirect.py b/nova/api/zone_redirect.py index ad47a6216..fec1b1af3 100644 --- a/nova/api/zone_redirect.py +++ b/nova/api/zone_redirect.py @@ -57,9 +57,20 @@ class ZoneRedirectMiddleware(wsgi.Middleware): # Todo(sandy): This only works for OpenStack API currently. # Needs to be broken out into a driver. + new_req = req.copy() + + scheme, netloc, path, query, frag = \ + urlparse.urlsplit(new_req.path_qs) + query = urlparse.parse_qsl(query) + query = [(key, value) for key, value in query if key != 'fresh'] + query = urllib.urlencode(query) + url = urlparse.urlunsplit((scheme, netloc, path, query, frag)) + + m = re.search('/v\d+\.\d+/(.+)', url) + resource = m.group(1) + for zone in e.zones: - url = zone.api_url - LOG.info(_("Zone redirect to:[url:%(api_url)s, " + LOG.debug(_("Zone redirect to:[url:%(api_url)s, " "username:%(username)s]" % dict(api_url=zone.api_url, username=zone.username))) @@ -67,21 +78,6 @@ class ZoneRedirectMiddleware(wsgi.Middleware): nova = client.OpenStackClient(zone.username, zone.password, zone.api_url) nova.authenticate() - new_req = req.copy() - - scheme, netloc, path, query, frag = \ - urlparse.urlsplit(new_req.path_qs) - query = urlparse.parse_qsl(query) - query = [(key, value) for key, value in query if key != 'fresh'] - query = urllib.urlencode(query) - url = urlparse.urlunsplit((scheme, netloc, path, query, frag)) - - m = re.search('/(v\d+\.\d+)/(.+)', url) - version = m.group(1) - resource = m.group(2) - - #LOG.info(_("New Request Data: %s"), new_req.body) - #LOG.info(_("New Request Path: %s"), resource) try: if req.method == 'GET': response, body = nova.get(resource, body=new_req.body) @@ -97,7 +93,7 @@ class ZoneRedirectMiddleware(wsgi.Middleware): e.code, e.message, e.details) continue - LOG.info(_("Zone Response: %s [%s]/ %s"), response, + LOG.debug(_("Zone Response: %s [%s]/ %s"), response, response.status, body) if response.status == 200: res = webob.Response() @@ -106,7 +102,7 @@ class ZoneRedirectMiddleware(wsgi.Middleware): res.body = json.dumps(body) return res - LOG.info(_("Returning 404 ...")) + LOG.debug(_("Zone Redirect Middleware returning 404 ...")) res = webob.Response() res.status = "404" return res -- cgit From d714df5ed4a6a1d4f1c0f7680c2fbb6a6abb81a5 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Wed, 16 Mar 2011 11:02:22 -0400 Subject: Implement top level extensions. --- nova/api/openstack/extensions.py | 94 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 8 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 8b8806c8a..66ddd8078 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -25,6 +25,7 @@ import webob.exc from nova import flags from nova import log as logging from nova import wsgi +from nova.api.openstack import faults LOG = logging.getLogger('extensions') @@ -70,6 +71,42 @@ class ResponseExtensionController(wsgi.Controller): return handler(res) +class ExtensionController(wsgi.Controller): + + def __init__(self, extension_manager): + self.extension_manager = extension_manager + + def _translate(self, ext): + ext_data = {} + ext_data['name'] = ext.get_name() + ext_data['alias'] = ext.get_alias() + ext_data['description'] = ext.get_description() + ext_data['namespace'] = ext.get_namespace() + ext_data['updated'] = ext.get_updated() + ext_data['links'] = [] # TODO: implement extension links + return ext_data + + def index(self, req): + extensions = [] + for alias, ext in self.extension_manager.extensions.iteritems(): + extensions.append(self._translate(ext)) + return dict(extensions=extensions) + + def show(self, req, id): + # NOTE: the extensions alias is used as the 'id' for show + ext = self.extension_manager.extensions[id] + return self._translate(ext) + + def delete(self, req, id): + raise faults.Fault(exc.HTTPNotFound()) + + def create(self, req): + raise faults.Fault(exc.HTTPNotFound()) + + def delete(self, req, id): + raise faults.Fault(exc.HTTPNotFound()) + + class ExtensionMiddleware(wsgi.Middleware): """ Extensions middleware that intercepts configured routes for extensions. @@ -183,12 +220,17 @@ class ExtensionMiddleware(wsgi.Middleware): class ExtensionManager(object): + """ + Load extensions from the configured extension path. + See nova/tests/api/openstack/extensions/foxinsocks.py for an example + extension implementation. + """ def __init__(self, path): LOG.audit(_('Initializing extension manager.')) self.path = path - self.extensions = [] + self.extensions = {} self._load_extensions() def get_resources(self): @@ -196,8 +238,14 @@ class ExtensionManager(object): returns a list of ResourceExtension objects """ resources = [] - for ext in self.extensions: - resources.extend(ext.get_resources()) + resources.append(ResourceExtension('extensions', + ExtensionController(self))) + for alias, ext in self.extensions.iteritems(): + try: + resources.extend(ext.get_resources()) + except AttributeError: + # NOTE: Extension aren't required to have resource extensions + pass return resources def get_actions(self): @@ -205,8 +253,12 @@ class ExtensionManager(object): returns a list of ActionExtension objects """ actions = [] - for ext in self.extensions: - actions.extend(ext.get_actions()) + for alias, ext in self.extensions.iteritems(): + try: + actions.extend(ext.get_actions()) + except AttributeError: + # NOTE: Extension aren't required to have action extensions + pass return actions def get_response_extensions(self): @@ -214,16 +266,36 @@ class ExtensionManager(object): returns a list of ResponseExtension objects """ response_exts = [] - for ext in self.extensions: - response_exts.extend(ext.get_response_extensions()) + for alias, ext in self.extensions.iteritems(): + try: + response_exts.extend(ext.get_response_extensions()) + except AttributeError: + # NOTE: Extension aren't required to have response extensions + pass return response_exts + def _check_extension(self, extension): + """ + Checks for required methods in extension objects. + """ + try: + LOG.debug(_('Ext name: %s'), extension.get_name()) + LOG.debug(_('Ext alias: %s'), extension.get_alias()) + LOG.debug(_('Ext description: %s'), extension.get_description()) + LOG.debug(_('Ext namespace: %s'), extension.get_namespace()) + LOG.debug(_('Ext updated: %s'), extension.get_updated()) + except AttributeError as ex: + LOG.exception(_("Exception loading extension: %s"), unicode(ex)) + def _load_extensions(self): """ Load extensions from the configured path. The extension name is constructed from the camel cased module_name + 'Extension'. If your extension module was named widgets.py the extension class within that module should be 'WidgetsExtension'. + + See nova/tests/api/openstack/extensions/foxinsocks.py for an example + extension implementation. """ if not os.path.exists(self.path): return @@ -235,7 +307,13 @@ class ExtensionManager(object): if file_ext.lower() == '.py': mod = imp.load_source(mod_name, ext_path) ext_name = mod_name[0].upper() + mod_name[1:] - self.extensions.append(getattr(mod, ext_name)()) + try: + new_ext = getattr(mod, ext_name)() + self._check_extension(new_ext) + self.extensions[new_ext.get_alias()] = new_ext + except AttributeError as ex: + LOG.exception(_("Exception loading extension: %s"), + unicode(ex)) class ResponseExtension(object): -- cgit From b2456e983178b97ad94f48c77ef210000d6d6ca4 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Wed, 16 Mar 2011 14:03:38 -0400 Subject: Move mapper code into the _action_ext_controllers and _response_ext_controllers methods. --- nova/api/openstack/extensions.py | 65 +++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 31 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 66ddd8078..4adfcfc5b 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -118,7 +118,7 @@ class ExtensionMiddleware(wsgi.Middleware): return cls(app, **local_config) return _factory - def _actions_by_collection(self, application, ext_mgr): + def _action_ext_controllers(self, application, ext_mgr, mapper): """ Return a dict of ActionExtensionController objects by collection """ @@ -126,18 +126,38 @@ class ExtensionMiddleware(wsgi.Middleware): for action in ext_mgr.get_actions(): if not action.collection in action_controllers.keys(): controller = ActionExtensionController(application) + mapper.connect("/%s/:(id)/action.:(format)" % + action.collection, + action='action', + controller=controller, + conditions=dict(method=['POST'])) + mapper.connect("/%s/:(id)/action" % action.collection, + action='action', + controller=controller, + conditions=dict(method=['POST'])) action_controllers[action.collection] = controller + return action_controllers - def _responses_by_collection(self, application, ext_mgr): + def _response_ext_controllers(self, application, ext_mgr, mapper): """ Return a dict of ResponseExtensionController objects by collection """ response_ext_controllers = {} for resp_ext in ext_mgr.get_response_extensions(): - if not resp_ext.url_route in response_ext_controllers.keys(): + if not resp_ext.key in response_ext_controllers.keys(): controller = ResponseExtensionController(application) - response_ext_controllers[resp_ext.url_route] = controller + mapper.connect(resp_ext.url_route + '.:(format)', + action='process', + controller=controller, + conditions=resp_ext.conditions) + + mapper.connect(resp_ext.url_route, + action='process', + controller=controller, + conditions=resp_ext.conditions) + response_ext_controllers[resp_ext.key] = controller + return response_ext_controllers def __init__(self, application, ext_mgr=None): @@ -159,38 +179,20 @@ class ExtensionMiddleware(wsgi.Middleware): parent_resource=resource.parent) # extended actions - action_controllers = self._actions_by_collection(application, ext_mgr) + action_controllers = self._action_ext_controllers(application, ext_mgr, + mapper) for action in ext_mgr.get_actions(): - LOG.debug(_('Extended collection/action: %s/%s'), - action.collection, - action.action_name) + LOG.debug(_('Extended action: %s'), action.action_name) controller = action_controllers[action.collection] controller.add_action(action.action_name, action.handler) - mapper.connect("/%s/:(id)/action.:(format)" % action.collection, - action='action', - controller=controller, - conditions=dict(method=['POST'])) - mapper.connect("/%s/:(id)/action" % action.collection, - action='action', - controller=controller, - conditions=dict(method=['POST'])) - # extended responses - resp_controllers = self._responses_by_collection(application, ext_mgr) + resp_controllers = self._response_ext_controllers(application, ext_mgr, + mapper) for response_ext in ext_mgr.get_response_extensions(): - LOG.debug(_('Extended response: %s'), response_ext.url_route) - controller = resp_controllers[response_ext.url_route] + LOG.debug(_('Extended response: %s'), response_ext.key) + controller = resp_controllers[response_ext.key] controller.add_handler(response_ext.handler) - mapper.connect(response_ext.url_route + '.:(format)', - action='process', - controller=controller, - conditions=response_ext.conditions) - - mapper.connect(response_ext.url_route, - action='process', - controller=controller, - conditions=response_ext.conditions) self._router = routes.middleware.RoutesMiddleware(self._dispatch, mapper) @@ -322,10 +324,11 @@ class ResponseExtension(object): core nova OpenStack API controllers. """ - def __init__(self, url_route, method, handler): + def __init__(self, method, url_route, handler): self.url_route = url_route - self.conditions = dict(method=[method]) self.handler = handler + self.conditions = dict(method=[method]) + self.key = "%s-%s" % (method, url_route) class ActionExtension(object): -- cgit From f91cd67c403d7de54600eea0d91c223af0493788 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Wed, 16 Mar 2011 14:09:29 -0400 Subject: Comment update. --- nova/api/openstack/extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 4adfcfc5b..557b12fd9 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -345,7 +345,7 @@ class ActionExtension(object): class ResourceExtension(object): """ - ResourceExtension objects can be used to add add top level resources + ResourceExtension objects can be used to add top level resources to the OpenStack API in nova. """ -- cgit From 4d057c9c2df77816ead6f30fa2795148aa8148d3 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 16 Mar 2011 11:44:40 -0700 Subject: Refactored ZoneRedirect into ZoneChildHelper so ZoneManager can use this too. --- nova/api/zone_redirect.py | 79 ++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 35 deletions(-) (limited to 'nova/api') diff --git a/nova/api/zone_redirect.py b/nova/api/zone_redirect.py index fec1b1af3..0adf94046 100644 --- a/nova/api/zone_redirect.py +++ b/nova/api/zone_redirect.py @@ -28,6 +28,7 @@ import urllib from nova import exception from nova import log as logging from nova import wsgi +from nova.scheduler import api import novaclient.client as client import novaclient.exceptions as osexceptions @@ -41,6 +42,43 @@ except ImportError: LOG = logging.getLogger('server') +class RequestForwarder(api.ChildZoneHelper): + + def __init__(self, resource, method, body): + self.resource = resource + self.method = method + self.body = body + + def process(self, client, zone): + api_url = zone.api_url + LOG.debug(_("Zone redirect to: %(api_url)s, " % locals())) + try: + if self.method == 'GET': + response, body = client.get(self.resource, body=self.body) + elif self.method == 'POST': + response, body = client.post(self.resource, body=self.body) + elif self.method == 'PUT': + response, body = client.put(self.resource, body=self.body) + elif self.method == 'DELETE': + response, body = client.delete(self.resource, body=self.body) + except osexceptions.OpenStackException, e: + LOG.info(_("Zone returned error: %s ('%s', '%s')"), + e.code, e.message, e.details) + res = webob.Response() + res.status = "404" + return res + + status = response.status + LOG.debug(_("Zone %(api_url)s response: " + "%(response)s [%(status)s]/ %(body)s") % + locals()) + res = webob.Response() + res.status = response['status'] + res.content_type = response['content-type'] + res.body = json.dumps(body) + return res + + class ZoneRedirectMiddleware(wsgi.Middleware): """Catches Zone Routing exceptions and delegates the call to child zones.""" @@ -57,10 +95,8 @@ class ZoneRedirectMiddleware(wsgi.Middleware): # Todo(sandy): This only works for OpenStack API currently. # Needs to be broken out into a driver. - new_req = req.copy() - scheme, netloc, path, query, frag = \ - urlparse.urlsplit(new_req.path_qs) + urlparse.urlsplit(req.path_qs) query = urlparse.parse_qsl(query) query = [(key, value) for key, value in query if key != 'fresh'] query = urllib.urlencode(query) @@ -69,38 +105,11 @@ class ZoneRedirectMiddleware(wsgi.Middleware): m = re.search('/v\d+\.\d+/(.+)', url) resource = m.group(1) - for zone in e.zones: - LOG.debug(_("Zone redirect to:[url:%(api_url)s, " - "username:%(username)s]" - % dict(api_url=zone.api_url, - username=zone.username))) - - nova = client.OpenStackClient(zone.username, zone.password, - zone.api_url) - nova.authenticate() - try: - if req.method == 'GET': - response, body = nova.get(resource, body=new_req.body) - elif req.method == 'POST': - response, body = nova.post(resource, body=new_req.body) - elif req.method == 'PUT': - response, body = nova.put(resource, body=new_req.body) - elif req.method == 'DELETE': - response, body = nova.delete(resource, - body=new_req.body) - except osexceptions.OpenStackException, e: - LOG.info(_("Zone returned error: %s ('%s', '%s')"), - e.code, e.message, e.details) - continue - - LOG.debug(_("Zone Response: %s [%s]/ %s"), response, - response.status, body) - if response.status == 200: - res = webob.Response() - res.status = response['status'] - res.content_type = response['content-type'] - res.body = json.dumps(body) - return res + forwarder = RequestForwarder(resource, req.method, req.body) + for result in forwarder.start(e.zones): + # Todo(sandy): We need to aggregate multiple successes. + if result.status_int == 200: + return result LOG.debug(_("Zone Redirect Middleware returning 404 ...")) res = webob.Response() -- cgit From 77a48cdd8a22cc84ed67a6b3d1c3793dd93e44a8 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 16 Mar 2011 16:15:56 -0400 Subject: expanding osapi flavors tests; rewriting flavors resource with view builders; adding 1.1 specific links to flavors resources --- nova/api/openstack/flavors.py | 48 ++++++++++++++++++------------------- nova/api/openstack/views/flavors.py | 45 ++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 27 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index c99b945fb..bc61e8d1a 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -15,16 +15,12 @@ # License for the specific language governing permissions and limitations # under the License. -from webob import exc +import webob from nova import db -from nova import context -from nova.api.openstack import faults -from nova.api.openstack import common -from nova.compute import instance_types -from nova.api.openstack.views import flavors as flavors_views +from nova import exception from nova import wsgi -import nova.api.openstack +from nova.api.openstack.views import flavors as flavors_views class Controller(wsgi.Controller): @@ -37,29 +33,31 @@ class Controller(wsgi.Controller): def index(self, req): """Return all flavors in brief.""" - return dict(flavors=[dict(id=flavor['id'], name=flavor['name']) - for flavor in self.detail(req)['flavors']]) + items = self._get_flavors(req, False) + return dict(flavors=items) def detail(self, req): """Return all flavors in detail.""" - items = [self.show(req, id)['flavor'] for id in self._all_ids(req)] + items = self._get_flavors(req, True) return dict(flavors=items) + def _get_flavors(self, req, is_detail): + """Helper function that returns a list of flavor dicts.""" + ctxt = req.environ['nova.context'] + flavors = db.api.instance_type_get_all(ctxt) + builder = flavors_views.get_view_builder(req) + items = [builder.build(flavor, is_detail=is_detail) \ + for flavor in flavors.values()] + return items + def show(self, req, id): """Return data about the given flavor id.""" - ctxt = req.environ['nova.context'] - flavor = db.api.instance_type_get_by_flavor_id(ctxt, id) - values = { - "id": flavor["flavorid"], - "name": flavor["name"], - "ram": flavor["memory_mb"], - "disk": flavor["local_gb"], - } + try: + ctxt = req.environ['nova.context'] + flavor = db.api.instance_type_get_by_flavor_id(ctxt, id) + except exception.NotFound: + return webob.exc.HTTPNotFound() + + builder = flavors_views.get_view_builder(req) + values = builder.build(flavor, is_detail=True) return dict(flavor=values) - - def _all_ids(self, req): - """Return the list of all flavorids.""" - ctxt = req.environ['nova.context'] - inst_types = db.api.instance_type_get_all(ctxt) - flavor_ids = [inst_types[i]['flavorid'] for i in inst_types.keys()] - return sorted(flavor_ids) diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index dd2e75a7a..19ac8f114 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -35,14 +35,55 @@ class ViewBuilder(object): def __init__(self): pass - def build(self, flavor_obj): - raise NotImplementedError() + def build(self, flavor_obj, is_detail=False): + if is_detail: + flavor = self._build_detail(flavor_obj) + else: + flavor = self._build_simple(flavor_obj) + + full_flavor = self._build_extra(flavor) + + return full_flavor + + def _build_simple(self, flavor_obj): + return { + "id": flavor_obj["flavorid"], + "name": flavor_obj["name"], + } + + def _build_detail(self, flavor_obj): + simple = self._build_simple(flavor_obj) + + detail = { + "ram": flavor_obj["memory_mb"], + "disk": flavor_obj["local_gb"], + } + + detail.update(simple) + + return detail + + def _build_extra(self, flavor_obj): + return flavor_obj class ViewBuilder_1_1(ViewBuilder): def __init__(self, base_url): self.base_url = base_url + def _build_extra(self, flavor_obj): + flavor_obj["links"] = self._build_links(flavor_obj) + return flavor_obj + + def _build_links(self, flavor_obj): + links = [ + { + "rel": "self", + "href": self.generate_href(flavor_obj["id"]), + }, + ] + return links + def generate_href(self, flavor_id): return "%s/flavors/%s" % (self.base_url, flavor_id) -- cgit From bb606c7ba42fc567f2e9989e0f560783743e5ddd Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 16 Mar 2011 16:58:38 -0400 Subject: adding bookmarks links to 1.1 flavor entities --- nova/api/openstack/views/flavors.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index 19ac8f114..be7e68763 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -76,10 +76,21 @@ class ViewBuilder_1_1(ViewBuilder): return flavor_obj def _build_links(self, flavor_obj): + href = self.generate_href(flavor_obj["id"]) links = [ { "rel": "self", - "href": self.generate_href(flavor_obj["id"]), + "href": href, + }, + { + "rel": "bookmark", + "type": "application/json", + "href": href, + }, + { + "rel": "bookmark", + "type": "application/xml", + "href": href, }, ] return links -- cgit From 05ccc91bdb3ad47ffecee29d21835ded17f65816 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 16 Mar 2011 17:24:32 -0400 Subject: pep8 --- nova/api/openstack/views/flavors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index be7e68763..7d75c0aa2 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -61,8 +61,8 @@ class ViewBuilder(object): detail.update(simple) - return detail - + return detail + def _build_extra(self, flavor_obj): return flavor_obj -- cgit From 703e680aa6d0da1953ec6f8ae3a6aa66dc9fad7e Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Wed, 16 Mar 2011 16:13:24 -0700 Subject: Fix the errors that pylint was reporting on this file This was meant more as a test of whether pylint was now returning false-positives. It looks like the bugs it's reporting are at least partially real. --- nova/api/openstack/servers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 3ecd4fb01..dfaf35128 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -25,8 +25,9 @@ from nova import compute from nova import exception from nova import flags from nova import log as logging -from nova import wsgi +from nova import quota from nova import utils +from nova import wsgi from nova.api.openstack import common from nova.api.openstack import faults from nova.auth import manager as auth_manager @@ -188,7 +189,7 @@ class Controller(wsgi.Controller): key_data=key_data, metadata=metadata, injected_files=injected_files) - except QuotaError as error: + except quota.QuotaError as error: self._handle_quota_error(error) server = _translate_keys(instances[0]) @@ -238,7 +239,7 @@ class Controller(wsgi.Controller): injected_files.append((path, contents)) return injected_files - def _handle_quota_errors(self, error): + def _handle_quota_error(self, error): """ Reraise quota errors as api-specific http exceptions """ -- cgit From a1e2959312b51757653447de3e8c9e92029da6fd Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Wed, 16 Mar 2011 16:23:31 -0700 Subject: Fix a few of the more obvious non-errors while we're in here --- nova/api/openstack/servers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index dfaf35128..42cf693de 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -15,11 +15,10 @@ import base64 import hashlib -import json import traceback -from xml.dom import minidom from webob import exc +from xml.dom import minidom from nova import compute from nova import exception @@ -33,7 +32,6 @@ from nova.api.openstack import faults from nova.auth import manager as auth_manager from nova.compute import instance_types from nova.compute import power_state -import nova.api.openstack LOG = logging.getLogger('server') @@ -270,7 +268,7 @@ class Controller(wsgi.Controller): update_dict['admin_pass'] = inst_dict['server']['adminPass'] try: self.compute_api.set_admin_password(ctxt, id) - except exception.TimeoutException, e: + except exception.TimeoutException: return exc.HTTPRequestTimeout() if 'name' in inst_dict['server']: update_dict['display_name'] = inst_dict['server']['name'] -- cgit From c9158dfcf4efd2cf22df9aed7b1bb01e037e8eb2 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 16 Mar 2011 19:04:27 -0700 Subject: moved scheduler API check into db.api decorator --- nova/api/zone_redirect.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/zone_redirect.py b/nova/api/zone_redirect.py index 0adf94046..4fe255c99 100644 --- a/nova/api/zone_redirect.py +++ b/nova/api/zone_redirect.py @@ -43,7 +43,7 @@ LOG = logging.getLogger('server') class RequestForwarder(api.ChildZoneHelper): - + """Worker for sending an OpenStack Request to each child zone.""" def __init__(self, resource, method, body): self.resource = resource self.method = method @@ -98,10 +98,13 @@ class ZoneRedirectMiddleware(wsgi.Middleware): scheme, netloc, path, query, frag = \ urlparse.urlsplit(req.path_qs) query = urlparse.parse_qsl(query) + # Remove any cache busters from old novaclient calls ... query = [(key, value) for key, value in query if key != 'fresh'] query = urllib.urlencode(query) url = urlparse.urlunsplit((scheme, netloc, path, query, frag)) + # Strip off the API version, since this is given when the + # child zone was added. m = re.search('/v\d+\.\d+/(.+)', url) resource = m.group(1) -- cgit From cfe77c1236b68aa96dd85503582e08a07a23f77f Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 16 Mar 2011 19:21:32 -0700 Subject: cleanup --- nova/api/openstack/servers.py | 1 - 1 file changed, 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index ffcbe628c..85999764f 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -343,7 +343,6 @@ class Controller(wsgi.Controller): """ Permit Admins to Pause the server. """ ctxt = req.environ['nova.context'] try: - LOG.debug(_("*** Compute.api::pause %s"), id) self.compute_api.pause(ctxt, id) except: readable = traceback.format_exc() -- cgit From 609a912fa8a816c1f47140489dcc1131356cd67c Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 16 Mar 2011 19:26:54 -0700 Subject: pep8 --- nova/api/zone_redirect.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/zone_redirect.py b/nova/api/zone_redirect.py index 4fe255c99..7ebae1401 100644 --- a/nova/api/zone_redirect.py +++ b/nova/api/zone_redirect.py @@ -48,7 +48,7 @@ class RequestForwarder(api.ChildZoneHelper): self.resource = resource self.method = method self.body = body - + def process(self, client, zone): api_url = zone.api_url LOG.debug(_("Zone redirect to: %(api_url)s, " % locals())) @@ -89,12 +89,12 @@ class ZoneRedirectMiddleware(wsgi.Middleware): return req.get_response(self.application) except exception.ZoneRouteException as e: if not e.zones: - exc = webob.exc.HTTPInternalServerError(explanation= - _("No zones to reroute to.")) + exc = webob.exc.HTTPInternalServerError(explanation=_( + "No zones to reroute to.")) return faults.Fault(exc) # Todo(sandy): This only works for OpenStack API currently. - # Needs to be broken out into a driver. + # Needs to be broken out into a driver. scheme, netloc, path, query, frag = \ urlparse.urlsplit(req.path_qs) query = urlparse.parse_qsl(query) -- cgit From 11698a131fe6b99bfd91a977a975b07bcd4c2b2b Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Thu, 17 Mar 2011 03:21:09 -0400 Subject: Added mechanism for versioned controllers for openstack api versions 1.0/1.1. Create servers in the 1.1 api now supports imageRef/flavorRef instead of imageId/flavorId. --- nova/api/openstack/__init__.py | 29 ++++++++++++---- nova/api/openstack/auth.py | 2 +- nova/api/openstack/common.py | 1 + nova/api/openstack/servers.py | 64 +++++++++++++++++++++++++++++------ nova/api/openstack/views/addresses.py | 16 ++------- nova/api/openstack/views/flavors.py | 18 +--------- nova/api/openstack/views/images.py | 18 +--------- nova/api/openstack/views/servers.py | 49 ++++++++++----------------- 8 files changed, 100 insertions(+), 97 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 0244bc93c..0b50d17d0 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -71,9 +71,14 @@ class APIRouter(wsgi.Router): return cls() def __init__(self): + self.server_members = {} mapper = routes.Mapper() + self._setup_routes(mapper) + super(APIRouter, self).__init__(mapper) - server_members = {'action': 'POST'} + def _setup_routes(self, mapper): + server_members = self.server_members + server_members['action'] = 'POST' if FLAGS.allow_admin_api: LOG.debug(_("Including admin operations in API.")) @@ -98,10 +103,6 @@ class APIRouter(wsgi.Router): controller=accounts.Controller(), collection={'detail': 'GET'}) - mapper.resource("server", "servers", controller=servers.Controller(), - collection={'detail': 'GET'}, - member=server_members) - mapper.resource("backup_schedule", "backup_schedule", controller=backup_schedules.Controller(), parent_resource=dict(member_name='server', @@ -120,7 +121,23 @@ class APIRouter(wsgi.Router): collection={'detail': 'GET'}, controller=shared_ip_groups.Controller()) - super(APIRouter, self).__init__(mapper) + +class APIRouterV10(APIRouter): + def _setup_routes(self, mapper): + APIRouter._setup_routes(self, mapper) + mapper.resource("server", "servers", + controller=servers.ControllerV10(), + collection={'detail': 'GET'}, + member=self.server_members) + + +class APIRouterV11(APIRouter): + def _setup_routes(self, mapper): + APIRouter._setup_routes(self, mapper) + mapper.resource("server", "servers", + controller=servers.ControllerV11(), + collection={'detail': 'GET'}, + member=self.server_members) class Versions(wsgi.Application): diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 7ae285019..6f1cf5e63 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -70,7 +70,7 @@ class AuthMiddleware(wsgi.Middleware): req.environ['nova.context'] = context.RequestContext(user, account) version = req.path.split('/')[1].replace('v', '') - req.environ['nova.api.openstack.version'] = version + req.environ['api.version'] = version return self.application def has_authentication(self, req): diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index d94969ff5..d6679de01 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -75,5 +75,6 @@ def get_image_id_from_image_hash(image_service, context, image_hash): return image_id raise exception.NotFound(image_hash) + def get_api_version(req): return req.environ.get('api.version') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index dc62882eb..9ce0caa46 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -27,8 +27,9 @@ from nova import wsgi from nova import utils from nova.api.openstack import common from nova.api.openstack import faults -from nova.api.openstack.views import servers as servers_views -from nova.api.openstack.views import addresses as addresses_views +import nova.api.openstack.views.addresses +import nova.api.openstack.views.flavors +import nova.api.openstack.views.servers from nova.auth import manager as auth_manager from nova.compute import instance_types from nova.compute import power_state @@ -57,7 +58,7 @@ class Controller(wsgi.Controller): def ips(self, req, id): try: instance = self.compute_api.get(req.environ['nova.context'], id) - builder = addresses_views.get_view_builder(req) + builder = self._get_addresses_view_builder(req) return builder.build(instance) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -77,7 +78,7 @@ class Controller(wsgi.Controller): """ instance_list = self.compute_api.get_all(req.environ['nova.context']) limited_list = common.limited(instance_list, req) - builder = servers_views.get_view_builder(req) + builder = self._get_view_builder(req) servers = [builder.build(inst, is_detail)['server'] for inst in limited_list] return dict(servers=servers) @@ -86,7 +87,7 @@ class Controller(wsgi.Controller): """ Returns server details by server id """ try: instance = self.compute_api.get(req.environ['nova.context'], id) - builder = servers_views.get_view_builder(req) + builder = self._get_view_builder(req) return builder.build(instance, is_detail=True) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -111,8 +112,9 @@ class Controller(wsgi.Controller): raise exception.NotFound(_("No keypairs defined")) key_pair = key_pairs[0] + requested_image_id = self._image_id_from_req_data(env) image_id = common.get_image_id_from_image_hash(self._image_service, - context, env['server']['imageId']) + context, requested_image_id) kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image( req, image_id) @@ -126,9 +128,10 @@ class Controller(wsgi.Controller): for k, v in env['server']['metadata'].items(): metadata.append({'key': k, 'value': v}) - instances = self.compute_api.create( + flavor_id = self._flavor_id_from_req_data(env) + (inst,) = self.compute_api.create( context, - instance_types.get_by_flavor_id(env['server']['flavorId']), + instance_types.get_by_flavor_id(flavor_id), image_id, kernel_id=kernel_id, ramdisk_id=ramdisk_id, @@ -138,9 +141,11 @@ class Controller(wsgi.Controller): key_data=key_pair['public_key'], metadata=metadata, onset_files=env.get('onset_files', [])) + inst['instance_type'] = flavor_id + inst['image_id'] = requested_image_id - builder = servers_views.get_view_builder(req) - server = builder.build(instances[0], is_detail=False) + builder = self._get_view_builder(req) + server = builder.build(inst, is_detail=True) password = "%s%s" % (server['server']['name'][:4], utils.generate_password(12)) server['server']['adminPass'] = password @@ -437,3 +442,42 @@ class Controller(wsgi.Controller): _("Ramdisk not found for image %(image_id)s") % locals()) return kernel_id, ramdisk_id + + +class ControllerV10(Controller): + def _image_id_from_req_data(self, data): + return data['server']['imageId'] + + def _flavor_id_from_req_data(self, data): + return data['server']['flavorId'] + + def _get_view_builder(self, req): + addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV10() + return nova.api.openstack.views.servers.ViewBuilderV10( + addresses_builder) + + def _get_addresses_view_builder(self, req): + return nova.api.openstack.views.addresses.ViewBuilderV10(req) + + +class ControllerV11(Controller): + def _image_id_from_req_data(self, data): + href = data['server']['imageRef'] + return href.split('/')[-1] + + def _flavor_id_from_req_data(self, data): + href = data['server']['flavorRef'] + return href.split('/')[-1] + + def _get_view_builder(self, req): + base_url = req.application_url + flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11( + base_url) + image_builder = nova.api.openstack.views.images.ViewBuilderV11( + base_url) + addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11() + return nova.api.openstack.views.servers.ViewBuilderV11( + addresses_builder, flavor_builder, image_builder) + + def _get_addresses_view_builder(self, req): + return nova.api.openstack.views.addresses.ViewBuilderV11(req) diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index 9d392aace..90c77855b 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -19,18 +19,6 @@ from nova import utils from nova.api.openstack import common -def get_view_builder(req): - ''' - A factory method that returns the correct builder based on the version of - the api requested. - ''' - version = common.get_api_version(req) - if version == '1.1': - return ViewBuilder_1_1() - else: - return ViewBuilder_1_0() - - class ViewBuilder(object): ''' Models a server addresses response as a python dictionary.''' @@ -38,14 +26,14 @@ class ViewBuilder(object): raise NotImplementedError() -class ViewBuilder_1_0(ViewBuilder): +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') return dict(public=public_ips, private=private_ips) -class ViewBuilder_1_1(ViewBuilder): +class ViewBuilderV11(ViewBuilder): def build(self, inst): private_ips = utils.get_from_path(inst, 'fixed_ip/address') private_ips = [dict(version=4, addr=a) for a in private_ips] diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index aa3c2aeb2..18bd779c0 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -17,18 +17,6 @@ from nova.api.openstack import common -def get_view_builder(req): - ''' - A factory method that returns the correct builder based on the version of - the api requested. - ''' - version = common.get_api_version(req) - base_url = req.application_url - if version == '1.1': - return ViewBuilder_1_1(base_url) - else: - return ViewBuilder_1_0() - class ViewBuilder(object): def __init__(self): @@ -38,13 +26,9 @@ class ViewBuilder(object): raise NotImplementedError() -class ViewBuilder_1_1(ViewBuilder): +class ViewBuilderV11(ViewBuilder): def __init__(self, base_url): self.base_url = base_url def generate_href(self, flavor_id): return "%s/flavors/%s" % (self.base_url, flavor_id) - - -class ViewBuilder_1_0(ViewBuilder): - pass diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 930b464b0..a6c6ad7d1 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -17,18 +17,6 @@ from nova.api.openstack import common -def get_view_builder(req): - ''' - A factory method that returns the correct builder based on the version of - the api requested. - ''' - version = common.get_api_version(req) - base_url = req.application_url - if version == '1.1': - return ViewBuilder_1_1(base_url) - else: - return ViewBuilder_1_0() - class ViewBuilder(object): def __init__(self): @@ -38,13 +26,9 @@ class ViewBuilder(object): raise NotImplementedError() -class ViewBuilder_1_1(ViewBuilder): +class ViewBuilderV11(ViewBuilder): def __init__(self, base_url): self.base_url = base_url def generate_href(self, image_id): return "%s/images/%s" % (self.base_url, image_id) - - -class ViewBuilder_1_0(ViewBuilder): - pass diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 261acfed0..8d47ac757 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -24,22 +24,6 @@ from nova.api.openstack.views import images as images_view from nova import utils -def get_view_builder(req): - ''' - A factory method that returns the correct builder based on the version of - the api requested. - ''' - version = common.get_api_version(req) - addresses_builder = addresses_view.get_view_builder(req) - if version == '1.1': - flavor_builder = flavors_view.get_view_builder(req) - image_builder = images_view.get_view_builder(req) - return ViewBuilder_1_1(addresses_builder, flavor_builder, - image_builder) - else: - return ViewBuilder_1_0(addresses_builder) - - class ViewBuilder(object): ''' Models a server response as a python dictionary. @@ -76,25 +60,20 @@ class ViewBuilder(object): power_state.FAILED: 'error'} inst_dict = {} - #mapped_keys = dict(status='state', imageId='image_id', - # flavorId='instance_type', name='display_name', id='id') - - mapped_keys = dict(status='state', name='display_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['id'] = int(inst['id']) + inst_dict['name'] = inst['display_name'] + inst_dict['status'] = power_mapping[inst.get('state')] inst_dict['addresses'] = self.addresses_builder.build(inst) # Return the metadata as a dictionary metadata = {} - for item in inst['metadata']: - metadata[item['key']] = item['value'] + if 'metadata' in inst: + for item in inst['metadata']: + metadata[item['key']] = item['value'] inst_dict['metadata'] = metadata inst_dict['hostId'] = '' - if inst['host']: + if inst.get('host'): inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() self._build_image(inst_dict, inst) @@ -109,24 +88,30 @@ class ViewBuilder(object): raise NotImplementedError() -class ViewBuilder_1_0(ViewBuilder): +class ViewBuilderV10(ViewBuilder): def _build_image(self, response, inst): - response["imageId"] = inst["image_id"] + if inst.get('image_id') != None: + response['imageId'] = inst['image_id'] def _build_flavor(self, response, inst): - response["flavorId"] = inst["instance_type"] + if inst.get('instance_type') != None: + response['flavorId'] = inst['instance_type'] -class ViewBuilder_1_1(ViewBuilder): +class ViewBuilderV11(ViewBuilder): def __init__(self, addresses_builder, flavor_builder, image_builder): ViewBuilder.__init__(self, addresses_builder) self.flavor_builder = flavor_builder self.image_builder = image_builder def _build_image(self, response, inst): + if inst.get('image_id') == None: + return image_id = inst["image_id"] response["imageRef"] = self.image_builder.generate_href(image_id) def _build_flavor(self, response, inst): + if inst.get('instance_type') == None: + return flavor_id = inst["instance_type"] response["flavorRef"] = self.flavor_builder.generate_href(flavor_id) -- cgit From 05ca6e24d4a3cf64bbe371f1c9c74088110eba68 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Thu, 17 Mar 2011 04:32:24 -0400 Subject: Setting the api verion in the request in the auth middle is no longer needed. Also, common.get_api_version is no longer needed. As Eric Day noted, having versioned controllers will make that unnecessary. --- nova/api/openstack/auth.py | 2 -- nova/api/openstack/common.py | 4 ---- 2 files changed, 6 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 6f1cf5e63..4c6b58eff 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -69,8 +69,6 @@ class AuthMiddleware(wsgi.Middleware): return faults.Fault(webob.exc.HTTPUnauthorized()) req.environ['nova.context'] = context.RequestContext(user, account) - version = req.path.split('/')[1].replace('v', '') - req.environ['api.version'] = version return self.application def has_authentication(self, req): diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index d6679de01..74ac21024 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -74,7 +74,3 @@ def get_image_id_from_image_hash(image_service, context, image_hash): if abs(hash(image_id)) == int(image_hash): return image_id raise exception.NotFound(image_hash) - - -def get_api_version(req): - return req.environ.get('api.version') -- cgit From abc6c82449dfc46a33dcd8190840e51f44b5b930 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Thu, 17 Mar 2011 07:30:22 -0700 Subject: Replaced capability flags with List --- nova/api/openstack/zones.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index 547920901..ebfc7743c 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -71,9 +71,9 @@ class Controller(wsgi.Controller): items = api.API.get_zone_capabilities(req.environ['nova.context']) zone = dict(name=FLAGS.zone_name) - caps = FLAGS.zone_capabilities.split(';') + caps = FLAGS.zone_capabilities for cap in caps: - key_values = cap.split(':') + key_values = cap.split('=') zone[key_values[0]] = key_values[1] for item, (min_value, max_value) in items.iteritems(): zone[item] = "%s,%s" % (min_value, max_value) -- cgit From 41c097000c1eeb4f1532b22f136c383b8174e6cc Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 17 Mar 2011 11:35:32 -0400 Subject: Add tests and code to handle multiple ResponseExtension objects. --- nova/api/openstack/extensions.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 557b12fd9..23181d2a6 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -66,9 +66,20 @@ class ResponseExtensionController(wsgi.Controller): def process(self, req, *args, **kwargs): res = req.get_response(self.application) + content_type = req.best_match_content_type() # currently response handlers are un-ordered for handler in self.handlers: - return handler(res) + res=handler(res) + try: + body = res.body + headers = res.headers + except AttributeError: + body = self._serialize(res, content_type) + headers={"Content-Type": content_type} + res = webob.Response() + res.body = body + res.headers = headers + return res class ExtensionController(wsgi.Controller): -- cgit From c1f7df80d22b718bc96332c2f52354627d11700d Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 17 Mar 2011 12:31:16 -0400 Subject: adding comments; removing returns from build_extra; removing unnecessary backslash --- nova/api/openstack/flavors.py | 2 +- nova/api/openstack/views/flavors.py | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index bc61e8d1a..6eba0f9b8 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -46,7 +46,7 @@ class Controller(wsgi.Controller): ctxt = req.environ['nova.context'] flavors = db.api.instance_type_get_all(ctxt) builder = flavors_views.get_view_builder(req) - items = [builder.build(flavor, is_detail=is_detail) \ + items = [builder.build(flavor, is_detail=is_detail) for flavor in flavors.values()] return items diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index 7d75c0aa2..92003a19f 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -32,26 +32,27 @@ def get_view_builder(req): class ViewBuilder(object): - def __init__(self): - pass def build(self, flavor_obj, is_detail=False): + """Generic method used to generate a flavor entity.""" if is_detail: flavor = self._build_detail(flavor_obj) else: flavor = self._build_simple(flavor_obj) - full_flavor = self._build_extra(flavor) + self._build_extra(flavor) - return full_flavor + return flavor def _build_simple(self, flavor_obj): + """Build a minimal representation of a flavor.""" return { "id": flavor_obj["flavorid"], "name": flavor_obj["name"], } def _build_detail(self, flavor_obj): + """Build a more complete representation of a flavor.""" simple = self._build_simple(flavor_obj) detail = { @@ -64,19 +65,26 @@ class ViewBuilder(object): return detail def _build_extra(self, flavor_obj): - return flavor_obj + """Hook for version-specific changes to newly created flavor object.""" + pass class ViewBuilder_1_1(ViewBuilder): + """Openstack API v1.1 flavors view builder.""" + def __init__(self, base_url): + """ + :param base_url: url of the root wsgi application + """ self.base_url = base_url def _build_extra(self, flavor_obj): flavor_obj["links"] = self._build_links(flavor_obj) - return flavor_obj def _build_links(self, flavor_obj): + """Generate a container of links that refer to the provided flavor.""" href = self.generate_href(flavor_obj["id"]) + links = [ { "rel": "self", @@ -93,11 +101,17 @@ class ViewBuilder_1_1(ViewBuilder): "href": href, }, ] + return links def generate_href(self, flavor_id): + """Create an url that refers to a specific flavor id.""" return "%s/flavors/%s" % (self.base_url, flavor_id) class ViewBuilder_1_0(ViewBuilder): + """ + Openstack API v1.0 flavors view builder. Currently, there + are no 1.0-specific attributes of a flavor. + """ pass -- cgit From 686e113188aaf8195aed7bea8bf70c21b6bff498 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 17 Mar 2011 12:04:49 -0500 Subject: Mapping the resize status --- nova/api/openstack/servers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 47ed254ec..59234b0de 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -61,7 +61,13 @@ def _translate_detail_keys(inst): for k, v in mapped_keys.iteritems(): inst_dict[k] = inst[v] - inst_dict['status'] = power_mapping[inst_dict['status']] + context = req.environ['nova.context'].elevated() + migration = self.db.migrate_get_by_instance_and_status(context, + inst['id'], 'finished') + if migration: + inst_dict['status'] = 'resize-confirm' + else + inst_dict['status'] = power_mapping[inst_dict['status']] inst_dict['addresses'] = dict(public=[], private=[]) # grab single private fixed ip -- cgit From 3afeb8466fa9f005edc9da182b1e0af6ffb00ade Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 17 Mar 2011 12:05:43 -0500 Subject: Mapping the resize status --- nova/api/openstack/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 59234b0de..fd835c247 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -66,7 +66,7 @@ def _translate_detail_keys(inst): inst['id'], 'finished') if migration: inst_dict['status'] = 'resize-confirm' - else + else: inst_dict['status'] = power_mapping[inst_dict['status']] inst_dict['addresses'] = dict(public=[], private=[]) -- cgit From 99a3899291ef14149bee0581ad7615e07dfc55c1 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 17 Mar 2011 14:07:25 -0400 Subject: adding serialization_metadata to encode links on flavors --- nova/api/openstack/flavors.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index 6eba0f9b8..f0936332c 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -29,7 +29,11 @@ class Controller(wsgi.Controller): _serialization_metadata = { 'application/xml': { "attributes": { - "flavor": ["id", "name", "ram", "disk"]}}} + "flavor": ["id", "name", "ram", "disk"], + "link": ["rel","type","href"], + } + } + } def index(self, req): """Return all flavors in brief.""" -- cgit From 5a141466db962e184ced0a57efb6bfe94ff5a246 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 17 Mar 2011 14:09:44 -0400 Subject: pep8 --- nova/api/openstack/flavors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index f0936332c..e9ee9d012 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -30,7 +30,7 @@ class Controller(wsgi.Controller): 'application/xml': { "attributes": { "flavor": ["id", "name", "ram", "disk"], - "link": ["rel","type","href"], + "link": ["rel", "type", "href"], } } } -- cgit From 25c407b6ade499dd0bdd470e7fd46682c34a98b7 Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Thu, 17 Mar 2011 19:13:09 +0000 Subject: Get the migration out --- nova/api/openstack/servers.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index fd835c247..601a68508 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -20,6 +20,8 @@ import traceback from webob import exc from nova import compute +from nova import context +from nova import db from nova import exception from nova import flags from nova import log as logging @@ -61,12 +63,12 @@ def _translate_detail_keys(inst): for k, v in mapped_keys.iteritems(): inst_dict[k] = inst[v] - context = req.environ['nova.context'].elevated() - migration = self.db.migrate_get_by_instance_and_status(context, - inst['id'], 'finished') - if migration: + ctxt = context.get_admin_context() + try: + migration = db.migration_get_by_instance_and_status(ctxt, + inst['id'], 'finished') inst_dict['status'] = 'resize-confirm' - else: + except Exception, e: inst_dict['status'] = power_mapping[inst_dict['status']] inst_dict['addresses'] = dict(public=[], private=[]) -- cgit From 35bd58bd9dc6441f5620b262d1f65b852f56c67c Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 17 Mar 2011 15:43:56 -0400 Subject: moving Versions app out of __init__.py into its own module; adding openstack versions tests; adding links to version entities --- nova/api/openstack/__init__.py | 18 ------------ nova/api/openstack/versions.py | 55 ++++++++++++++++++++++++++++++++++ nova/api/openstack/views/versions.py | 57 ++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 nova/api/openstack/versions.py create mode 100644 nova/api/openstack/views/versions.py (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 0244bc93c..eb0402962 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -121,21 +121,3 @@ class APIRouter(wsgi.Router): controller=shared_ip_groups.Controller()) super(APIRouter, self).__init__(mapper) - - -class Versions(wsgi.Application): - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - """Respond to a request for all OpenStack API versions.""" - response = { - "versions": [ - dict(status="DEPRECATED", id="v1.0"), - dict(status="CURRENT", id="v1.1"), - ], - } - metadata = { - "application/xml": { - "attributes": dict(version=["status", "id"])}} - - content_type = req.best_match_content_type() - return wsgi.Serializer(metadata).serialize(response, content_type) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py new file mode 100644 index 000000000..f0f2c484c --- /dev/null +++ b/nova/api/openstack/versions.py @@ -0,0 +1,55 @@ +# 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. + +import webob.dec +import webob.exc + +from nova import wsgi +import nova.api.openstack.views.versions + + +class Versions(wsgi.Application): + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + """Respond to a request for all OpenStack API versions.""" + version_objs = [ + { + "id": "v1.1", + "status": "CURRENT", + }, + { + "id": "v1.0", + "status": "DEPRECATED", + }, + ] + + builder = nova.api.openstack.views.versions.get_view_builder(req) + versions = [builder.build(version) for version in version_objs] + response = dict(versions=versions) + + metadata = { + "application/xml": { + "attributes": { + "version": ["status", "id"], + "link": ["rel", "href"], + } + } + } + + content_type = req.best_match_content_type() + return wsgi.Serializer(metadata).serialize(response, content_type) diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py new file mode 100644 index 000000000..555d58d5c --- /dev/null +++ b/nova/api/openstack/views/versions.py @@ -0,0 +1,57 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-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. + + +def get_view_builder(req): + base_url = req.application_url + return ViewBuilder(base_url) + + +class ViewBuilder(object): + + def __init__(self, base_url): + """ + :param base_url: url of the root wsgi application + """ + self.base_url = base_url + + def build(self, version_data): + """Generic method used to generate a version entity.""" + version = { + "id": version_data["id"], + "status": version_data["status"], + "links": self._build_links(version_data), + } + + return version + + def _build_links(self, version_data): + """Generate a container of links that refer to the provided version.""" + href = self.generate_href(version_data["id"]) + + links = [ + { + "rel": "self", + "href": href, + }, + ] + + return links + + def generate_href(self, version_number): + """Create an url that refers to a specific version_number.""" + return "%s/%s" % (self.base_url, version_number) -- cgit From e138e0836922ee0608feefbff5e4e5d03ad14197 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Thu, 17 Mar 2011 16:02:37 -0400 Subject: Now returns a 400 for a create server request with invalid hrefs for imageRef/flavorRef values. Also added tests. --- nova/api/openstack/common.py | 12 ++++++++++-- nova/api/openstack/servers.py | 5 ++--- 2 files changed, 12 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 74ac21024..b224cbfb4 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -15,9 +15,10 @@ # License for the specific language governing permissions and limitations # under the License. -import webob.exc - +import re from nova import exception +from webob import exc +import webob.exc def limited(items, request, max_limit=1000): @@ -74,3 +75,10 @@ def get_image_id_from_image_hash(image_service, context, image_hash): if abs(hash(image_id)) == int(image_hash): return image_id raise exception.NotFound(image_hash) + + +def get_id_from_href(href): + m = re.match(r'http.+/.+/(\d)+$', href) + if not m: + raise exc.HTTPBadRequest(_('could not parse id from href')) + return int(m.group(1)) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index f03225b55..6f25d10bd 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -512,7 +512,6 @@ class Controller(wsgi.Controller): return kernel_id, ramdisk_id - class ControllerV10(Controller): def _image_id_from_req_data(self, data): return data['server']['imageId'] @@ -532,11 +531,11 @@ class ControllerV10(Controller): class ControllerV11(Controller): def _image_id_from_req_data(self, data): href = data['server']['imageRef'] - return href.split('/')[-1] + return common.get_id_from_href(href) def _flavor_id_from_req_data(self, data): href = data['server']['flavorRef'] - return href.split('/')[-1] + return common.get_id_from_href(href) def _get_view_builder(self, req): base_url = req.application_url -- cgit From f7d5dea09568c6440918264d97ecdbcc316c0ec4 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 17 Mar 2011 15:31:48 -0500 Subject: pep8 --- nova/api/openstack/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index b0e355232..050450457 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -69,7 +69,7 @@ def _translate_detail_keys(inst): ctxt = context.get_admin_context() try: migration = db.migration_get_by_instance_and_status(ctxt, - inst['id'], 'finished') + inst['id'], 'finished') inst_dict['status'] = 'resize-confirm' except Exception, e: inst_dict['status'] = power_mapping[inst_dict['status']] -- cgit From af67fba36436feeede4dcc5720e51d2b66c3094a Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 17 Mar 2011 22:30:34 -0400 Subject: Images now v1.1 supported...mostly. --- nova/api/openstack/__init__.py | 11 +- nova/api/openstack/images.py | 214 ++++++++++++++++--------------------- nova/api/openstack/views/images.py | 63 +++++++++-- 3 files changed, 151 insertions(+), 137 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 0b50d17d0..0ac67fdba 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -113,8 +113,6 @@ class APIRouter(wsgi.Router): 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("shared_ip_group", "shared_ip_groups", @@ -130,6 +128,10 @@ class APIRouterV10(APIRouter): collection={'detail': 'GET'}, member=self.server_members) + mapper.resource("image", "images", + controller=images.Controller_v1_0(), + collection={'detail': 'GET'}) + class APIRouterV11(APIRouter): def _setup_routes(self, mapper): @@ -139,6 +141,11 @@ class APIRouterV11(APIRouter): collection={'detail': 'GET'}, member=self.server_members) + mapper.resource("image", "images", + controller=images.Controller_v1_1(), + collection={'detail': 'GET'}) + + class Versions(wsgi.Application): @webob.dec.wsgify(RequestClass=wsgi.Request) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 98f0dd96b..2357bfd3d 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -1,6 +1,4 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. +# Copyright 2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -19,92 +17,19 @@ from webob import exc from nova import compute from nova import flags +from nova import log from nova import utils from nova import wsgi -import nova.api.openstack -from nova.api.openstack import common -from nova.api.openstack import faults -import nova.image.service - - -FLAGS = flags.FLAGS - - -def _translate_keys(item): - """ - Maps key names to Rackspace-like attributes for return - also pares down attributes to those we want - item is a dict - - Note: should be removed when the set of keys expected by the api - and the set of keys returned by the image service are equivalent - - """ - # TODO(tr3buchet): this map is specific to s3 object store, - # replace with a list of keys for _filter_keys later - mapped_keys = {'status': 'imageState', - 'id': 'imageId', - 'name': 'imageLocation'} - - mapped_item = {} - # TODO(tr3buchet): - # this chunk of code works with s3 and the local image service/glance - # when we switch to glance/local image service it can be replaced with - # a call to _filter_keys, and mapped_keys can be changed to a list - try: - for k, v in mapped_keys.iteritems(): - # map s3 fields - mapped_item[k] = item[v] - except KeyError: - # return only the fields api expects - mapped_item = _filter_keys(item, mapped_keys.keys()) - - return mapped_item - - -def _translate_status(item): - """ - Translates status of image to match current Rackspace api bindings - item is a dict +from nova.api.openstack.views import images as images_view - Note: should be removed when the set of statuses expected by the api - and the set of statuses returned by the image service are equivalent +class Controller(wsgi.Controller): """ - status_mapping = { - 'pending': 'queued', - 'decrypting': 'preparing', - 'untarring': 'saving', - 'available': 'active'} - try: - item['status'] = status_mapping[item['status']] - except KeyError: - # TODO(sirp): Performing translation of status (if necessary) here for - # now. Perhaps this should really be done in EC2 API and - # S3ImageService - pass - - return item - - -def _filter_keys(item, keys): - """ - Filters all model attributes except for keys - item is a dict - + Base `wsgi.Controller` for retrieving and displaying images in the + OpenStack API. Version-inspecific code goes here. """ - return dict((k, v) for k, v in item.iteritems() if k in keys) - - -def _convert_image_id_to_hash(image): - if 'imageId' in image: - # Convert EC2-style ID (i-blah) to Rackspace-style (int) - image_id = abs(hash(image['imageId'])) - image['imageId'] = image_id - image['id'] = image_id - -class Controller(wsgi.Controller): + _builder = images_view.Builder_v1_0() _serialization_metadata = { 'application/xml': { @@ -112,55 +37,96 @@ class Controller(wsgi.Controller): "image": ["id", "name", "updated", "created", "status", "serverId", "progress"]}}} - def __init__(self): - self._service = utils.import_object(FLAGS.image_service) + def __init__(self, image_service=None, compute_service=None): + """ + Initialize new `ImageController`. + + @param compute_service: `nova.compute.api:API` + @param image_service: `nova.image.service:BaseImageService` + """ + _default_service = utils.import_object(flags.FLAGS.image_service) + + self.__compute = compute_service or compute.API() + self.__image = image_service or _default_service + self.__log = log.getLogger(self.__class__.__name__) def index(self, req): - """Return all public images in brief""" - items = self._service.index(req.environ['nova.context']) - items = common.limited(items, req) - items = [_filter_keys(item, ('id', 'name')) for item in items] - return dict(images=items) + """ + Return an index listing of images available to the request. + + @param req: `webob.Request` object + """ + context = req.environ['nova.context'] + images = self.__image.index(context) + build = self._builder.build + return dict(images=[build(req, image, False) for image in images]) def detail(self, req): - """Return all public images in detail""" - try: - items = self._service.detail(req.environ['nova.context']) - except NotImplementedError: - items = self._service.index(req.environ['nova.context']) - for image in items: - _convert_image_id_to_hash(image) - - items = common.limited(items, req) - items = [_translate_keys(item) for item in items] - items = [_translate_status(item) for item in items] - return dict(images=items) - - def show(self, req, id): - """Return data about the given image id""" - image_id = common.get_image_id_from_image_hash(self._service, - req.environ['nova.context'], id) - - image = self._service.show(req.environ['nova.context'], image_id) - _convert_image_id_to_hash(image) - return dict(image=image) - - def delete(self, req, id): - # Only public images are supported for now. - raise faults.Fault(exc.HTTPNotFound()) + """ + Return a detailed index listing of images available to the request. + + @param req: `webob.Request` object. + """ + context = req.environ['nova.context'] + images = self.__image.detail(context) + build = self._builder.build + return dict(images=[build(req, image, True) for image in images]) + + def show(self, req, image_id): + """ + Return detailed information about a specific image. + + @param req: `webob.Request` object + @param image_id: Image identifier (integer) + """ + context = req.environ['nova.context'] + image = self.__image.show(context, image_id) + return self._builder.build(req, image, True) + + def delete(self, req, image_id): + """ + Delete an image, if allowed. + + @param req: `webob.Request` object + @param image_id: Image identifier (integer) + """ + context = req.environ['nova.context'] + self.__image.delete(context, image_id) + return exc.HTTPNoContent() def create(self, req): + """ + Snapshot a server instance and save the image. + + @param req: `webob.Request` object + """ context = req.environ['nova.context'] - env = self._deserialize(req.body, req.get_content_type()) - instance_id = env["image"]["serverId"] - name = env["image"]["name"] + body = req.body + content_type = req.get_content_type() + image = self._deserialize(body, content_type) + + if not image: + raise exc.HTTPBadRequest() + + try: + server_id = image["serverId"] + image_name = image["name"] + except KeyError: + raise exc.HTTPBadRequest() + + image = self.__compute.snapshot(context, server_id, image_name) + return self._builder.build(req, image, True) - image_meta = compute.API().snapshot( - context, instance_id, name) - return dict(image=image_meta) +class Controller_v1_0(Controller): + """ + Version 1.0 specific controller logic. + """ + _builder = images_view.Builder_v1_0() + - def update(self, req, id): - # Users may not modify public images, and that's all that - # we support for now. - raise faults.Fault(exc.HTTPNotFound()) +class Controller_v1_1(Controller): + """ + Version 1.1 specific controller logic. + """ + _builder = images_view.Builder_v1_1() diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index a6c6ad7d1..1631d1fe3 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -15,20 +15,61 @@ # License for the specific language governing permissions and limitations # under the License. -from nova.api.openstack import common +class Builder(object): + """ + Base class for generating responses to OpenStack API requests for + information about images. + """ -class ViewBuilder(object): - def __init__(self): - pass + def build(self, request, image_obj, detail=False): + """ + Return a standardized image structure for display by the API. + """ + image = { + "id": image_obj["id"], + "name": image_obj["name"], + } - def build(self, image_obj): - raise NotImplementedError() + if detail: + image.update({ + "created": image_obj["created_at"], + "updated": image_obj["updated_at"], + "status": image_obj["status"], + }) + return image -class ViewBuilderV11(ViewBuilder): - def __init__(self, base_url): - self.base_url = base_url - def generate_href(self, image_id): - return "%s/images/%s" % (self.base_url, image_id) +class Builder_v1_0(Builder): + pass + + +class Builder_v1_1(Builder): + """ + OpenStack API v1.1 Image Builder + """ + + def build(self, request, image_obj, detail=False): + """ + Return a standardized image structure for display by the API. + """ + image = Builder.build(self, request, image_obj, detail) + href = "%s/images/%s" % (request.application_url, image_obj["id"]) + + image["links"] = [{ + "rel": "self", + "href": href, + }, + { + "rel": "bookmark", + "type": "application/json", + "href": href, + }, + { + "rel": "bookmark", + "type": "application/xml", + "href": href, + }] + + return image -- cgit From 47bec2abb39f76d5b3ea634dbb7012d55d7f99ce Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 17 Mar 2011 23:07:40 -0400 Subject: adding servers container to openstack api v1.1 servers entities --- nova/api/openstack/servers.py | 11 ++++++-- nova/api/openstack/views/servers.py | 56 +++++++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 12 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index e3141934b..de35aca8d 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -47,11 +47,15 @@ class Controller(wsgi.Controller): """ The Server API controller for the OpenStack API """ _serialization_metadata = { - 'application/xml': { + "application/xml": { "attributes": { "server": ["id", "imageId", "name", "flavorId", "hostId", "status", "progress", "adminPass", "flavorRef", - "imageRef"]}}} + "imageRef"], + "link": ["rel", "type", "href"], + }, + }, + } def __init__(self): self.compute_api = compute.API() @@ -513,6 +517,7 @@ class Controller(wsgi.Controller): return kernel_id, ramdisk_id + class ControllerV10(Controller): def _image_id_from_req_data(self, data): return data['server']['imageId'] @@ -546,7 +551,7 @@ class ControllerV11(Controller): base_url) addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11() return nova.api.openstack.views.servers.ViewBuilderV11( - addresses_builder, flavor_builder, image_builder) + addresses_builder, flavor_builder, image_builder, base_url) def _get_addresses_view_builder(self, req): return nova.api.openstack.views.addresses.ViewBuilderV11(req) diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 8d47ac757..477f3caa0 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -39,12 +39,16 @@ class ViewBuilder(object): Rackspace-like attributes for return """ if is_detail: - return self._build_detail(inst) + server = self._build_detail(inst) else: - return self._build_simple(inst) + server = self._build_simple(inst) + + self._build_extra(server, inst) + + return dict(server=server) def _build_simple(self, inst): - return dict(server=dict(id=inst['id'], name=inst['display_name'])) + return dict(id=inst['id'], name=inst['display_name']) def _build_detail(self, inst): power_mapping = { @@ -79,7 +83,7 @@ class ViewBuilder(object): self._build_image(inst_dict, inst) self._build_flavor(inst_dict, inst) - return dict(server=inst_dict) + return inst_dict def _build_image(self, response, inst): raise NotImplementedError() @@ -87,6 +91,9 @@ class ViewBuilder(object): def _build_flavor(self, response, inst): raise NotImplementedError() + def _build_extra(self, response, inst): + pass + class ViewBuilderV10(ViewBuilder): def _build_image(self, response, inst): @@ -99,19 +106,50 @@ class ViewBuilderV10(ViewBuilder): class ViewBuilderV11(ViewBuilder): - def __init__(self, addresses_builder, flavor_builder, image_builder): + def __init__(self, addresses_builder, flavor_builder, image_builder, + base_url): ViewBuilder.__init__(self, addresses_builder) self.flavor_builder = flavor_builder self.image_builder = image_builder + self.base_url = base_url def _build_image(self, response, inst): - if inst.get('image_id') == None: + image_id = inst.get("image_id", None) + if image_id == None: return - image_id = inst["image_id"] response["imageRef"] = self.image_builder.generate_href(image_id) def _build_flavor(self, response, inst): - if inst.get('instance_type') == None: + flavor_id = inst.get("instance_type", None) + if flavor_id == None: return - flavor_id = inst["instance_type"] response["flavorRef"] = self.flavor_builder.generate_href(flavor_id) + + def _build_extra(self, response, inst): + self._build_links(response, inst) + + def _build_links(self, response, inst): + href = self.generate_href(inst["id"]) + + links = [ + { + "rel": "self", + "href": href, + }, + { + "rel": "bookmark", + "type": "application/json", + "href": href, + }, + { + "rel": "bookmark", + "type": "application/xml", + "href": href, + }, + ] + + response["links"] = links + + def generate_href(self, server_id): + """Create an url that refers to a specific server id.""" + return "%s/servers/%s" % (self.base_url, server_id) -- cgit From febbfd45a0c1dbd16093ab38897e94ddb331e9ea Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 17 Mar 2011 23:34:12 -0400 Subject: Updated naming, removed some prints, and removed some invalid tests. --- nova/api/openstack/__init__.py | 4 ++-- nova/api/openstack/images.py | 20 +++++++++++--------- nova/api/openstack/views/images.py | 8 ++++---- 3 files changed, 17 insertions(+), 15 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 0ac67fdba..1ec943f55 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -129,7 +129,7 @@ class APIRouterV10(APIRouter): member=self.server_members) mapper.resource("image", "images", - controller=images.Controller_v1_0(), + controller=images.ControllerV10(), collection={'detail': 'GET'}) @@ -142,7 +142,7 @@ class APIRouterV11(APIRouter): member=self.server_members) mapper.resource("image", "images", - controller=images.Controller_v1_1(), + controller=images.ControllerV11(), collection={'detail': 'GET'}) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 2357bfd3d..bc7338699 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -29,13 +29,15 @@ class Controller(wsgi.Controller): OpenStack API. Version-inspecific code goes here. """ - _builder = images_view.Builder_v1_0() - _serialization_metadata = { 'application/xml': { "attributes": { "image": ["id", "name", "updated", "created", "status", - "serverId", "progress"]}}} + "serverId", "progress"], + "link": ["rel", "type", "href"], + }, + }, + } def __init__(self, image_service=None, compute_service=None): """ @@ -109,8 +111,8 @@ class Controller(wsgi.Controller): raise exc.HTTPBadRequest() try: - server_id = image["serverId"] - image_name = image["name"] + server_id = image["image"]["serverId"] + image_name = image["image"]["name"] except KeyError: raise exc.HTTPBadRequest() @@ -118,15 +120,15 @@ class Controller(wsgi.Controller): return self._builder.build(req, image, True) -class Controller_v1_0(Controller): +class ControllerV10(Controller): """ Version 1.0 specific controller logic. """ - _builder = images_view.Builder_v1_0() + _builder = images_view.ViewBuilderV10() -class Controller_v1_1(Controller): +class ControllerV11(Controller): """ Version 1.1 specific controller logic. """ - _builder = images_view.Builder_v1_1() + _builder = images_view.ViewBuilderV11() diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 1631d1fe3..c41e60546 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -16,7 +16,7 @@ # under the License. -class Builder(object): +class ViewBuilder(object): """ Base class for generating responses to OpenStack API requests for information about images. @@ -41,11 +41,11 @@ class Builder(object): return image -class Builder_v1_0(Builder): +class ViewBuilderV10(ViewBuilder): pass -class Builder_v1_1(Builder): +class ViewBuilderV11(ViewBuilder): """ OpenStack API v1.1 Image Builder """ @@ -54,7 +54,7 @@ class Builder_v1_1(Builder): """ Return a standardized image structure for display by the API. """ - image = Builder.build(self, request, image_obj, detail) + image = ViewBuilder.build(self, request, image_obj, detail) href = "%s/images/%s" % (request.application_url, image_obj["id"]) image["links"] = [{ -- cgit From 0845d7081bc912e7eefa2d98e8c53033d872ef5e Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 17 Mar 2011 23:58:23 -0400 Subject: pep8 --- nova/api/openstack/views/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 477f3caa0..5f4c0f740 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -44,7 +44,7 @@ class ViewBuilder(object): server = self._build_simple(inst) self._build_extra(server, inst) - + return dict(server=server) def _build_simple(self, inst): -- cgit From fce6b6f8f39378f5f31b8aa432922374c744544e Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Fri, 18 Mar 2011 00:05:58 -0400 Subject: Become compatible with ironcamel and bcwaldon's implementations for standardness. --- nova/api/openstack/images.py | 31 +++++++++++++++++++++++-------- nova/api/openstack/views/images.py | 18 +++++++++++++++--- 2 files changed, 38 insertions(+), 11 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index bc7338699..a192883fc 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -17,9 +17,9 @@ from webob import exc from nova import compute 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.views import images as images_view @@ -39,6 +39,11 @@ class Controller(wsgi.Controller): }, } + _builder_dispatch = { + "1.0": images_view.ViewBuilderV10, + "1.1": images_view.ViewBuilderV11, + } + def __init__(self, image_service=None, compute_service=None): """ Initialize new `ImageController`. @@ -50,7 +55,17 @@ class Controller(wsgi.Controller): self.__compute = compute_service or compute.API() self.__image = image_service or _default_service - self.__log = log.getLogger(self.__class__.__name__) + + def get_builder(self, request): + """ + Property to get the ViewBuilder class we need to use. + """ + version = common.get_api_version(request) + base_url = request.application_url + try: + return self._builder_dispatch[version](base_url) + except KeyError: + raise exc.HTTPNotFound() def index(self, req): """ @@ -60,7 +75,7 @@ class Controller(wsgi.Controller): """ context = req.environ['nova.context'] images = self.__image.index(context) - build = self._builder.build + build = self.get_builder(req).build return dict(images=[build(req, image, False) for image in images]) def detail(self, req): @@ -71,7 +86,7 @@ class Controller(wsgi.Controller): """ context = req.environ['nova.context'] images = self.__image.detail(context) - build = self._builder.build + build = self.get_builder(req).build return dict(images=[build(req, image, True) for image in images]) def show(self, req, image_id): @@ -83,7 +98,7 @@ class Controller(wsgi.Controller): """ context = req.environ['nova.context'] image = self.__image.show(context, image_id) - return self._builder.build(req, image, True) + return self.get_builder(req).build(req, image, True) def delete(self, req, image_id): """ @@ -117,18 +132,18 @@ class Controller(wsgi.Controller): raise exc.HTTPBadRequest() image = self.__compute.snapshot(context, server_id, image_name) - return self._builder.build(req, image, True) + return self.get_builder(req).build(req, image, True) class ControllerV10(Controller): """ Version 1.0 specific controller logic. """ - _builder = images_view.ViewBuilderV10() + pass class ControllerV11(Controller): """ Version 1.1 specific controller logic. """ - _builder = images_view.ViewBuilderV11() + pass diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index c41e60546..313ba2eba 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -22,7 +22,19 @@ class ViewBuilder(object): information about images. """ - def build(self, request, image_obj, detail=False): + def __init__(self, base_url): + """ + Initialize new `ViewBuilder`. + """ + self._url = base_url + + def generate_href(self, image_id): + """ + Return an href string pointing to this object. + """ + return "%s/images/%s" % (self._url, image_id) + + def build(self, image_obj, detail=False): """ Return a standardized image structure for display by the API. """ @@ -50,12 +62,12 @@ class ViewBuilderV11(ViewBuilder): OpenStack API v1.1 Image Builder """ - def build(self, request, image_obj, detail=False): + def build(self, image_obj, detail=False): """ Return a standardized image structure for display by the API. """ image = ViewBuilder.build(self, request, image_obj, detail) - href = "%s/images/%s" % (request.application_url, image_obj["id"]) + href = self.generate_url(image_obj["id"]) image["links"] = [{ "rel": "self", -- cgit From acb7a7355055e04b9bb05fbba5f6590e57d681fa Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Fri, 18 Mar 2011 00:18:55 -0400 Subject: Support for markers for pagination as defined in the 1.1 spec. --- nova/api/openstack/common.py | 28 ++++++++++++++++++++++++++++ nova/api/openstack/servers.py | 8 +++++++- 2 files changed, 35 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index b224cbfb4..8106f841b 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -55,6 +55,34 @@ def limited(items, request, max_limit=1000): return items[offset:range_end] +def limited_by_marker(items, request, max_limit=1000): + ''' Return a slice of items according to requested marker and limit. ''' + + marker = request.GET.get('marker') + + try: + limit = int(request.GET.get('limit', max_limit)) + except ValueError: + raise webob.exc.HTTPBadRequest(_('limit param must be an integer')) + + if limit < 0: + raise webob.exc.HTTPBadRequest(_('limit param must be positive')) + + limit = min(max_limit, limit or max_limit) + start_index = 0 + if marker != None: + found_it = False + for i, item in enumerate(items): + if str(item['id']) == marker: + start_index = i + found_it = True + break + if not found_it: + raise webob.exc.HTTPBadRequest(_('marker not found')) + range_end = start_index + limit + return items[start_index:range_end] + + def get_image_id_from_image_hash(image_service, context, image_hash): """Given an Image ID Hash, return an objectstore Image ID. diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index e3141934b..461bf5989 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -81,7 +81,7 @@ class Controller(wsgi.Controller): builder - the response model builder """ instance_list = self.compute_api.get_all(req.environ['nova.context']) - limited_list = common.limited(instance_list, req) + limited_list = self._limit_items(instance_list, req) builder = self._get_view_builder(req) servers = [builder.build(inst, is_detail)['server'] for inst in limited_list] @@ -528,6 +528,9 @@ class ControllerV10(Controller): def _get_addresses_view_builder(self, req): return nova.api.openstack.views.addresses.ViewBuilderV10(req) + def _limit_items(self, items, req): + return common.limited(items, req) + class ControllerV11(Controller): def _image_id_from_req_data(self, data): @@ -551,6 +554,9 @@ class ControllerV11(Controller): def _get_addresses_view_builder(self, req): return nova.api.openstack.views.addresses.ViewBuilderV11(req) + def _limit_items(self, items, req): + return common.limited_by_marker(items, req) + class ServerCreateRequestXMLDeserializer(object): """ -- cgit From c28ec048a56a3ead96dc7528ca50865945d40646 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Fri, 18 Mar 2011 00:19:53 -0400 Subject: Final touches and bug/pep8 fixes. --- nova/api/openstack/__init__.py | 1 - nova/api/openstack/images.py | 35 ++++++++++++++++++----------------- nova/api/openstack/servers.py | 1 + nova/api/openstack/views/images.py | 4 ++-- 4 files changed, 21 insertions(+), 20 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 1ec943f55..14cb6b331 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -146,7 +146,6 @@ class APIRouterV11(APIRouter): collection={'detail': 'GET'}) - class Versions(wsgi.Application): @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index a192883fc..4cd989054 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -56,17 +56,6 @@ class Controller(wsgi.Controller): self.__compute = compute_service or compute.API() self.__image = image_service or _default_service - def get_builder(self, request): - """ - Property to get the ViewBuilder class we need to use. - """ - version = common.get_api_version(request) - base_url = request.application_url - try: - return self._builder_dispatch[version](base_url) - except KeyError: - raise exc.HTTPNotFound() - def index(self, req): """ Return an index listing of images available to the request. @@ -76,7 +65,7 @@ class Controller(wsgi.Controller): context = req.environ['nova.context'] images = self.__image.index(context) build = self.get_builder(req).build - return dict(images=[build(req, image, False) for image in images]) + return dict(images=[build(image, False) for image in images]) def detail(self, req): """ @@ -87,7 +76,7 @@ class Controller(wsgi.Controller): context = req.environ['nova.context'] images = self.__image.detail(context) build = self.get_builder(req).build - return dict(images=[build(req, image, True) for image in images]) + return dict(images=[build(image, True) for image in images]) def show(self, req, image_id): """ @@ -98,7 +87,7 @@ class Controller(wsgi.Controller): """ context = req.environ['nova.context'] image = self.__image.show(context, image_id) - return self.get_builder(req).build(req, image, True) + return self.get_builder().build(req, image, True) def delete(self, req, image_id): """ @@ -132,18 +121,30 @@ class Controller(wsgi.Controller): raise exc.HTTPBadRequest() image = self.__compute.snapshot(context, server_id, image_name) - return self.get_builder(req).build(req, image, True) + return self.get_builder(req).build(image, True) class ControllerV10(Controller): """ Version 1.0 specific controller logic. """ - pass + + def get_builder(self, request): + """ + Property to get the ViewBuilder class we need to use. + """ + base_url = request.application_url + return images_view.ViewBuilderV10(base_url) class ControllerV11(Controller): """ Version 1.1 specific controller logic. """ - pass + + def get_builder(self, request): + """ + Property to get the ViewBuilder class we need to use. + """ + base_url = request.application_url + return images_view.ViewBuilderV11(base_url) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 5f6fbd96c..73843f63e 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -516,6 +516,7 @@ class Controller(wsgi.Controller): return kernel_id, ramdisk_id + class ControllerV10(Controller): def _image_id_from_req_data(self, data): return data['server']['imageId'] diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 313ba2eba..7daa6fe26 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -66,8 +66,8 @@ class ViewBuilderV11(ViewBuilder): """ Return a standardized image structure for display by the API. """ - image = ViewBuilder.build(self, request, image_obj, detail) - href = self.generate_url(image_obj["id"]) + image = ViewBuilder.build(self, image_obj, detail) + href = self.generate_href(image_obj["id"]) image["links"] = [{ "rel": "self", -- cgit From 1abcdbea89e69013c193d2eb0b4b7a0bc2e2fa58 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 18 Mar 2011 02:14:36 -0400 Subject: Implement metadata resource for Openstack API v1.1. Includes: -GET /servers/id/meta -POST /servers/id/meta -GET /servers/id/meta/key -PUT /servers/id/meta/key -DELETE /servers/id/meta/key --- nova/api/openstack/__init__.py | 5 +++ nova/api/openstack/server_metadata.py | 75 +++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 nova/api/openstack/server_metadata.py (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 0b50d17d0..4beb84dcd 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 servers +from nova.api.openstack import server_metadata from nova.api.openstack import shared_ip_groups from nova.api.openstack import users from nova.api.openstack import zones @@ -138,6 +139,10 @@ class APIRouterV11(APIRouter): controller=servers.ControllerV11(), collection={'detail': 'GET'}, member=self.server_members) + mapper.resource("server_meta", "meta", + controller=server_metadata.Controller(), + parent_resource=dict(member_name='server', + collection_name='servers')) class Versions(wsgi.Application): diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py new file mode 100644 index 000000000..1408f59a6 --- /dev/null +++ b/nova/api/openstack/server_metadata.py @@ -0,0 +1,75 @@ +# 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. + +from webob import exc + +from nova import compute +from nova import wsgi +from nova.api.openstack import faults + + +class Controller(wsgi.Controller): + """ The server metadata API controller for the Openstack API """ + + def __init__(self): + self.compute_api = compute.API() + super(Controller, self).__init__() + + def _get_metadata(self, context, server_id): + metadata = self.compute_api.get_instance_metadata(context, server_id) + meta_dict = {} + for key, value in metadata.iteritems(): + meta_dict[key] = value + return dict(metadata=meta_dict) + + def index(self, req, server_id): + """ Returns the list of metadata for a given instance """ + context = req.environ['nova.context'] + return self._get_metadata(context, server_id) + + 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']) + return req.body + + def update(self, req, server_id, id): + context = req.environ['nova.context'] + body = self._deserialize(req.body, req.get_content_type()) + if not id in body: + expl = _('Request body and URI mismatch') + raise exc.HTTPBadRequest(explanation=expl) + self.compute_api.update_or_create_instance_metadata(context, + server_id, + body) + return req.body + + def show(self, req, server_id, id): + """ Return a single metadata item """ + context = req.environ['nova.context'] + data = self._get_metadata(context, server_id) + if id in data['metadata']: + return {id: data['metadata'][id]} + else: + return faults.Fault(exc.HTTPNotFound()) + + def delete(self, req, server_id, id): + """ Deletes an existing metadata """ + context = req.environ['nova.context'] + self.compute_api.delete_instance_metadata(context, server_id, id) -- cgit From 70e8b431334989ad067f0a5543aea408b7186c5c Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Fri, 18 Mar 2011 10:34:08 -0400 Subject: Fixed 'Undefined variable' errors generated by pylint (E0602). --- nova/api/openstack/accounts.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py index 2510ffb61..86066fa20 100644 --- a/nova/api/openstack/accounts.py +++ b/nova/api/openstack/accounts.py @@ -14,6 +14,7 @@ # under the License. import common +import webob.exc from nova import exception from nova import flags @@ -51,10 +52,10 @@ class Controller(wsgi.Controller): raise exception.NotAuthorized(_("Not admin user.")) def index(self, req): - raise faults.Fault(exc.HTTPNotImplemented()) + raise faults.Fault(webob.exc.HTTPNotImplemented()) def detail(self, req): - raise faults.Fault(exc.HTTPNotImplemented()) + raise faults.Fault(webob.exc.HTTPNotImplemented()) def show(self, req, id): """Return data about the given account id""" @@ -69,7 +70,7 @@ class Controller(wsgi.Controller): def create(self, req): """We use update with create-or-update semantics because the id comes from an external source""" - raise faults.Fault(exc.HTTPNotImplemented()) + raise faults.Fault(webob.exc.HTTPNotImplemented()) def update(self, req, id): """This is really create or update.""" -- cgit From ef33d6bde27276fb4c93ed6bbcb972977f03a370 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 18 Mar 2011 09:21:08 -0700 Subject: results --- nova/api/openstack/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 9f14a6b82..49f714d47 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -130,7 +130,7 @@ class Controller(wsgi.Controller): try: LOG.debug(_("***SHOW")) instance = self.compute_api.routing_get(req.environ['nova.context'], id) - LOG.debug(_("***SHOW")) + LOG.debug(_("***SHOW OUT %s" % instance)) return _translate_detail_keys(instance) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) -- cgit From 705020cc4acded862633aa5e02d5bb46c88dbc51 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 18 Mar 2011 11:46:27 -0700 Subject: api decorator --- nova/api/openstack/servers.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 49f714d47..17d620562 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -33,6 +33,7 @@ from nova.auth import manager as auth_manager from nova.compute import instance_types from nova.compute import power_state import nova.api.openstack +from nova.scheduler import api as scheduler_api LOG = logging.getLogger('server') @@ -125,6 +126,7 @@ class Controller(wsgi.Controller): res = [entity_maker(inst)['server'] for inst in limited_list] return dict(servers=res) + @scheduler_api.redirect_handler def show(self, req, id): """ Returns server details by server id """ try: -- cgit From feb5c82e29303285d3f914c37116a59538fec28f Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 18 Mar 2011 12:23:57 -0700 Subject: fix ups --- nova/api/openstack/servers.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 17d620562..86414fab2 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -137,6 +137,7 @@ class Controller(wsgi.Controller): except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) + @scheduler_api.redirect_handler def delete(self, req, id): """ Destroys a server """ try: @@ -258,6 +259,7 @@ class Controller(wsgi.Controller): # if the original error is okay, just reraise it raise error + @scheduler_api.redirect_handler def update(self, req, id): """ Updates the server name or password """ if len(req.body) == 0: @@ -283,6 +285,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPNoContent() + @scheduler_api.redirect_handler def action(self, req, id): """Multi-purpose method used to reboot, rebuild, or resize a server""" @@ -348,6 +351,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def lock(self, req, id): """ lock the instance with id @@ -363,6 +367,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def unlock(self, req, id): """ unlock the instance with id @@ -378,6 +383,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def get_lock(self, req, id): """ return the boolean state of (instance with id)'s lock @@ -392,6 +398,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def reset_network(self, req, id): """ Reset networking on an instance (admin only). @@ -406,6 +413,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def inject_network_info(self, req, id): """ Inject network info for an instance (admin only). @@ -420,6 +428,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def pause(self, req, id): """ Permit Admins to Pause the server. """ ctxt = req.environ['nova.context'] @@ -431,6 +440,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def unpause(self, req, id): """ Permit Admins to Unpause the server. """ ctxt = req.environ['nova.context'] @@ -442,6 +452,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def suspend(self, req, id): """permit admins to suspend the server""" context = req.environ['nova.context'] @@ -453,6 +464,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def resume(self, req, id): """permit admins to resume the server from suspend""" context = req.environ['nova.context'] @@ -464,6 +476,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def rescue(self, req, id): """Permit users to rescue the server.""" context = req.environ["nova.context"] @@ -475,6 +488,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def unrescue(self, req, id): """Permit users to unrescue the server.""" context = req.environ["nova.context"] @@ -486,6 +500,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def get_ajax_console(self, req, id): """ Returns a url to an instance's ajaxterm console. """ try: @@ -495,6 +510,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler def diagnostics(self, req, id): """Permit Admins to retrieve server diagnostics.""" ctxt = req.environ["nova.context"] -- cgit From 2f4c1802c7e482a447d348f049ff429b3d1a640c Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Fri, 18 Mar 2011 16:06:43 -0400 Subject: fix date formatting in images controller show --- nova/api/openstack/images.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 98f0dd96b..94e05823e 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -143,6 +143,7 @@ class Controller(wsgi.Controller): image = self._service.show(req.environ['nova.context'], image_id) _convert_image_id_to_hash(image) + self._format_image_dates(image) return dict(image=image) def delete(self, req, id): @@ -164,3 +165,8 @@ class Controller(wsgi.Controller): # Users may not modify public images, and that's all that # we support for now. raise faults.Fault(exc.HTTPNotFound()) + + def _format_image_dates(self, image): + for attr in ['created_at', 'updated_at', 'deleted_at']: + if image[attr] is not None: + image[attr] = image[attr].strftime('%Y-%m-%dT%H:%M:%SZ') -- cgit From 8f0b60f598c28b2f558f3ecdaa2f9604926393e6 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Mon, 21 Mar 2011 07:49:58 -0700 Subject: remove scheduler.api.API. naming changes. --- nova/api/openstack/zones.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index ebfc7743c..d129cf34f 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -53,7 +53,7 @@ class Controller(wsgi.Controller): """Return all zones in brief""" # Ask the ZoneManager in the Scheduler for most recent data, # or fall-back to the database ... - items = api.API.get_zone_list(req.environ['nova.context']) + items = api.get_zone_list(req.environ['nova.context']) if not items: items = db.zone_get_all(req.environ['nova.context']) @@ -68,7 +68,7 @@ class Controller(wsgi.Controller): def info(self, req): """Return name and capabilities for this zone.""" - items = api.API.get_zone_capabilities(req.environ['nova.context']) + items = api.get_zone_capabilities(req.environ['nova.context']) zone = dict(name=FLAGS.zone_name) caps = FLAGS.zone_capabilities -- cgit From f988df6c6f29d6c885d44c6768297aaf489faf34 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Mon, 21 Mar 2011 13:58:39 -0400 Subject: Fix pep8 issues in nova/api/openstack/extensions.py. --- nova/api/openstack/extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 23181d2a6..f881dbde7 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -69,13 +69,13 @@ class ResponseExtensionController(wsgi.Controller): content_type = req.best_match_content_type() # currently response handlers are un-ordered for handler in self.handlers: - res=handler(res) + res = handler(res) try: body = res.body headers = res.headers except AttributeError: body = self._serialize(res, content_type) - headers={"Content-Type": content_type} + headers = {"Content-Type": content_type} res = webob.Response() res.body = body res.headers = headers -- cgit From ff2d6dc656c03b8aeab5e50c5d39ca9dcde9b9b1 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Mon, 21 Mar 2011 14:00:39 -0400 Subject: Updated comment per the extension naming convention we actually use. --- nova/api/openstack/extensions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index f881dbde7..9d98d849a 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -303,9 +303,9 @@ class ExtensionManager(object): def _load_extensions(self): """ Load extensions from the configured path. The extension name is - constructed from the camel cased module_name + 'Extension'. If your - extension module was named widgets.py the extension class within that - module should be 'WidgetsExtension'. + constructed from the module_name. If your extension module was named + widgets.py the extension class within that module should be + 'Widgets'. See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. -- cgit From 85f50cf496e2c193ddc715f3019b4a4769ab5bd9 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 21 Mar 2011 15:14:24 -0400 Subject: pep8; various fixes --- nova/api/openstack/servers.py | 1 + nova/api/openstack/views/servers.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index e3141934b..dafc096ba 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -513,6 +513,7 @@ class Controller(wsgi.Controller): return kernel_id, ramdisk_id + class ControllerV10(Controller): def _image_id_from_req_data(self, data): return data['server']['imageId'] diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 8d47ac757..078d5d484 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -67,9 +67,8 @@ class ViewBuilder(object): # Return the metadata as a dictionary metadata = {} - if 'metadata' in inst: - for item in inst['metadata']: - metadata[item['key']] = item['value'] + for item in inst.get('metadata', []): + metadata[item['key']] = item['value'] inst_dict['metadata'] = metadata inst_dict['hostId'] = '' -- cgit From 39783f386a473ed28c786bb72a29e8403503c40c Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Mon, 21 Mar 2011 17:09:53 -0400 Subject: make bcwaldon happy --- nova/api/openstack/images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 94e05823e..99c14275a 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -168,5 +168,5 @@ class Controller(wsgi.Controller): def _format_image_dates(self, image): for attr in ['created_at', 'updated_at', 'deleted_at']: - if image[attr] is not None: + if image.get(attr) is not None: image[attr] = image[attr].strftime('%Y-%m-%dT%H:%M:%SZ') -- cgit From 4b8ed5afd1fd3e616eda0015f9bf16c7097f5476 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 22 Mar 2011 03:13:12 -0400 Subject: vpn changes --- nova/api/ec2/admin.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index d9a4ef999..208fe5c4f 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -28,6 +28,7 @@ from nova import exception from nova import flags from nova import log as logging from nova import utils +from nova.api.ec2 import ec2utils from nova.auth import manager @@ -92,15 +93,18 @@ def vpn_dict(project, vpn_instance): 'public_ip': project.vpn_ip, 'public_port': project.vpn_port} if vpn_instance: - rv['instance_id'] = vpn_instance['ec2_id'] + rv['instance_id'] = ec2utils.id_to_ec2_id(vpn_instance['id']) rv['created_at'] = utils.isotime(vpn_instance['created_at']) address = vpn_instance.get('fixed_ip', None) if address: rv['internal_ip'] = address['address'] - if utils.vpn_ping(project.vpn_ip, project.vpn_port): - rv['state'] = 'running' + if project.vpn_ip and project.vpn_port: + if utils.vpn_ping(project.vpn_ip, project.vpn_port): + rv['state'] = 'running' + else: + rv['state'] = 'down' else: - rv['state'] = 'down' + rv['state'] = 'down - invalid project vpn config' else: rv['state'] = 'pending' return rv @@ -279,7 +283,7 @@ class AdminController(object): ", ensure it isn't running, and try " "again in a few minutes") instance = self._vpn_for(context, project) - return {'instance_id': instance['ec2_id']} + return {'instance_id': ec2utils.id_to_ec2_id(instance['id'])} def describe_vpns(self, context): vpns = [] -- cgit From 7aa027b2005ff24f7308e1ec23eddb44bf352628 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 22 Mar 2011 10:01:18 -0400 Subject: Add unit test and code updates to ensure that a PUT requests to create/update server metadata only contain a single key. --- nova/api/openstack/server_metadata.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index 1408f59a6..45bbac99d 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -55,6 +55,9 @@ class Controller(wsgi.Controller): if not id in body: expl = _('Request body and URI mismatch') raise exc.HTTPBadRequest(explanation=expl) + 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) -- cgit From 1abd4e6d592fb41d86fa32c3f77fd0e0a43ca5d3 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 22 Mar 2011 10:23:33 -0400 Subject: making Controller._get_flavors is_detail a keyword argument --- nova/api/openstack/flavors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index e9ee9d012..f7b5b722f 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -37,15 +37,15 @@ class Controller(wsgi.Controller): def index(self, req): """Return all flavors in brief.""" - items = self._get_flavors(req, False) + items = self._get_flavors(req, is_detail=False) return dict(flavors=items) def detail(self, req): """Return all flavors in detail.""" - items = self._get_flavors(req, True) + items = self._get_flavors(req, is_detail=True) return dict(flavors=items) - def _get_flavors(self, req, is_detail): + def _get_flavors(self, req, is_detail=True): """Helper function that returns a list of flavor dicts.""" ctxt = req.environ['nova.context'] flavors = db.api.instance_type_get_all(ctxt) -- cgit From b82d548d0357f73ff446f5bf24e27fbefd98e4b3 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 22 Mar 2011 10:58:31 -0400 Subject: adding view builder tests --- nova/api/openstack/views/versions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py index 555d58d5c..d0145c94a 100644 --- a/nova/api/openstack/views/versions.py +++ b/nova/api/openstack/views/versions.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +import os + def get_view_builder(req): base_url = req.application_url @@ -54,4 +56,4 @@ class ViewBuilder(object): def generate_href(self, version_number): """Create an url that refers to a specific version_number.""" - return "%s/%s" % (self.base_url, version_number) + return os.path.join(self.base_url, version_number) -- cgit From 4e33ab9fc16d580fbcf57da8e6e2228ad27cc1af Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Tue, 22 Mar 2011 11:46:00 -0400 Subject: Adding more docstrings. image_id and instance_type fields of an instance will always exist, so no reason to check if keys exist. --- nova/api/openstack/__init__.py | 4 ++++ nova/api/openstack/views/servers.py | 25 ++++++++++++------------- 2 files changed, 16 insertions(+), 13 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 35b04f863..21d354f1c 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -131,6 +131,8 @@ class APIRouter(wsgi.Router): class APIRouterV10(APIRouter): + ''' Defines routes specific to OpenStack API V1.0 ''' + def _setup_routes(self, mapper): APIRouter._setup_routes(self, mapper) mapper.resource("server", "servers", @@ -140,6 +142,8 @@ class APIRouterV10(APIRouter): class APIRouterV11(APIRouter): + ''' Defines routes specific to OpenStack API V1.1 ''' + def _setup_routes(self, mapper): APIRouter._setup_routes(self, mapper) mapper.resource("server", "servers", diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 078d5d484..3100c46b5 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -34,19 +34,18 @@ class ViewBuilder(object): self.addresses_builder = addresses_builder def build(self, inst, is_detail): - """ - Coerces into dictionary format, mapping everything to - Rackspace-like attributes for return - """ + ''' Returns a dict that represenst a server ''' if is_detail: return self._build_detail(inst) else: return self._build_simple(inst) def _build_simple(self, inst): - return dict(server=dict(id=inst['id'], name=inst['display_name'])) + ''' Returns a simple model of a server ''' + return dict(server=dict(id=inst['id'], name=inst['display_name'])) def _build_detail(self, inst): + ''' Returns a detailed model of a server ''' power_mapping = { None: 'build', power_state.NOSTATE: 'build', @@ -81,36 +80,36 @@ class ViewBuilder(object): return dict(server=inst_dict) def _build_image(self, response, inst): + ''' Returns the image sub-resource of a server ''' raise NotImplementedError() def _build_flavor(self, response, inst): + ''' Returns the flavor sub-resource of a server ''' raise NotImplementedError() class ViewBuilderV10(ViewBuilder): + ''' Models an Openstack API V1.0 server response ''' + def _build_image(self, response, inst): - if inst.get('image_id') != None: - response['imageId'] = inst['image_id'] + response['imageId'] = inst['image_id'] def _build_flavor(self, response, inst): - if inst.get('instance_type') != None: - response['flavorId'] = inst['instance_type'] + response['flavorId'] = inst['instance_type'] class ViewBuilderV11(ViewBuilder): + ''' Models an Openstack API V1.0 server response ''' + def __init__(self, addresses_builder, flavor_builder, image_builder): ViewBuilder.__init__(self, addresses_builder) self.flavor_builder = flavor_builder self.image_builder = image_builder def _build_image(self, response, inst): - if inst.get('image_id') == None: - return image_id = inst["image_id"] response["imageRef"] = self.image_builder.generate_href(image_id) def _build_flavor(self, response, inst): - if inst.get('instance_type') == None: - return flavor_id = inst["instance_type"] response["flavorRef"] = self.flavor_builder.generate_href(flavor_id) -- cgit From f4dee61638db068c03edd7fe0ab3488ac4670d89 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 22 Mar 2011 11:56:07 -0400 Subject: pep8 fix. --- nova/api/openstack/servers.py | 1 + 1 file changed, 1 insertion(+) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 5f6fbd96c..73843f63e 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -516,6 +516,7 @@ class Controller(wsgi.Controller): return kernel_id, ramdisk_id + class ControllerV10(Controller): def _image_id_from_req_data(self, data): return data['server']['imageId'] -- cgit From 493e87976b7eb273f4115d46c91ad73671abb796 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Tue, 22 Mar 2011 13:18:08 -0400 Subject: Now using urlparse to parse a url to grab id out of it. --- nova/api/openstack/common.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index b224cbfb4..99fba8fef 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -17,6 +17,7 @@ import re from nova import exception +from urlparse import urlparse from webob import exc import webob.exc @@ -78,7 +79,7 @@ def get_image_id_from_image_hash(image_service, context, image_hash): def get_id_from_href(href): - m = re.match(r'http.+/.+/(\d)+$', href) - if not m: + try: + return int(urlparse(href).path.split('/')[-1]) + except: raise exc.HTTPBadRequest(_('could not parse id from href')) - return int(m.group(1)) -- cgit From 97e8f300af824145c8b92949ccbdfe81c0d7ca95 Mon Sep 17 00:00:00 2001 From: Josh Kleinpeter Date: Tue, 22 Mar 2011 12:33:34 -0500 Subject: Changed default for disabled on service_get_all to None. Changed calls to service_get_all so that the results should still be as they previously were. --- nova/api/ec2/admin.py | 2 +- nova/api/ec2/cloud.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index d9a4ef999..3ae29d8ce 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -299,7 +299,7 @@ class AdminController(object): * Volume (up, down, None) * Volume Count """ - services = db.service_get_all(context) + services = db.service_get_all(context, False) now = datetime.datetime.utcnow() hosts = [] rv = [] diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index e257e44e7..2afcea77c 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -196,7 +196,7 @@ class CloudController(object): def _describe_availability_zones(self, context, **kwargs): ctxt = context.elevated() - enabled_services = db.service_get_all(ctxt) + enabled_services = db.service_get_all(ctxt, False) disabled_services = db.service_get_all(ctxt, True) available_zones = [] for zone in [service.availability_zone for service @@ -221,7 +221,7 @@ class CloudController(object): rv = {'availabilityZoneInfo': [{'zoneName': 'nova', 'zoneState': 'available'}]} - services = db.service_get_all(context) + services = db.service_get_all(context, False) now = datetime.datetime.utcnow() hosts = [] for host in [service['host'] for service in services]: -- cgit From 2a38aa7583be37ece6c42ba9307c2db0232dbed3 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 22 Mar 2011 10:37:56 -0700 Subject: Whoops --- nova/api/openstack/servers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index be423c572..199d89c6d 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -92,9 +92,10 @@ class Controller(wsgi.Controller): """ Returns server details by server id """ try: LOG.debug(_("***SHOW")) - instance = self.compute_api.get(req.environ['nova.context'], id) - builder = servers_views.get_view_builder(req) + instance = self.compute_api.routing_get( + req.environ['nova.context'], id) LOG.debug(_("***SHOW OUT %s" % instance)) + builder = servers_views.get_view_builder(req) return builder.build(instance, is_detail=True) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) -- cgit From 116c0d52d21ebd6ed55a61467aac5d8c06a4b086 Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Tue, 22 Mar 2011 17:46:17 +0000 Subject: Merge stuff --- nova/api/openstack/servers.py | 4 ++-- nova/api/openstack/views/servers.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index db5942e92..f3367e118 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -36,7 +36,7 @@ from nova.api.openstack.views import addresses as addresses_views from nova.auth import manager as auth_manager from nova.compute import instance_types from nova.compute import power_state -prom nova.quota import QuotaError +from nova.quota import QuotaError import nova.api.openstack @@ -44,7 +44,7 @@ LOG = logging.getLogger('server') FLAGS = flags.FLAGS -plass Controller(wsgi.Controller): +class Controller(wsgi.Controller): """ The Server API controller for the OpenStack API """ _serialization_metadata = { diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 6d54a7a7e..9fd25999a 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -89,8 +89,9 @@ class ViewBuilder(object): migration = db.migration_get_by_instance_and_status(ctxt, inst['id'], 'finished') inst_dict['status'] = 'resize-confirm' - except Exception, e: - inst_dict['status'] = power_mapping[inst_dict['status']] + except: + pass + inst_dict['addresses'] = self.addresses_builder.build(inst) # Return the metadata as a dictionary -- cgit From 186fab6781265b2dc92cb6049c11b390cb38b969 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 22 Mar 2011 14:09:23 -0400 Subject: fixing copyright --- nova/api/openstack/versions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 4fa04ebb3..33f1dd628 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -1,7 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may -- cgit From 8792383dfbd630388e6a51a76910e73203a3793f Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Tue, 22 Mar 2011 18:24:00 +0000 Subject: Tweak --- nova/api/openstack/views/servers.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 9fd25999a..709052f22 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -16,7 +16,10 @@ # under the License. import hashlib + from nova.compute import power_state +import nova.context +from nova import db from nova.api.openstack import common from nova.api.openstack.views import addresses as addresses_view from nova.api.openstack.views import flavors as flavors_view @@ -86,6 +89,7 @@ class ViewBuilder(object): inst_dict['status'] = power_mapping[inst_dict['status']] try: + ctxt = nova.context.get_admin_context() migration = db.migration_get_by_instance_and_status(ctxt, inst['id'], 'finished') inst_dict['status'] = 'resize-confirm' -- cgit From ca37b31d64f9c5cf32ca7e6015176ef36e702dce Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Tue, 22 Mar 2011 16:04:27 -0400 Subject: Updating doc strings in accordance with PEP 257. Fixing order of imports in common.py. --- nova/api/openstack/__init__.py | 4 ++-- nova/api/openstack/common.py | 16 +++++++++++----- nova/api/openstack/views/servers.py | 22 ++++++++++++---------- 3 files changed, 25 insertions(+), 17 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 21d354f1c..5f9648210 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -131,7 +131,7 @@ class APIRouter(wsgi.Router): class APIRouterV10(APIRouter): - ''' Defines routes specific to OpenStack API V1.0 ''' + """Define routes specific to OpenStack API V1.0.""" def _setup_routes(self, mapper): APIRouter._setup_routes(self, mapper) @@ -142,7 +142,7 @@ class APIRouterV10(APIRouter): class APIRouterV11(APIRouter): - ''' Defines routes specific to OpenStack API V1.1 ''' + """Define routes specific to OpenStack API V1.1.""" def _setup_routes(self, mapper): APIRouter._setup_routes(self, mapper) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 99fba8fef..21ceec45e 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -15,11 +15,11 @@ # License for the specific language governing permissions and limitations # under the License. -import re -from nova import exception from urlparse import urlparse -from webob import exc -import webob.exc + +import webob + +from nova import exception def limited(items, request, max_limit=1000): @@ -79,7 +79,13 @@ def get_image_id_from_image_hash(image_service, context, image_hash): def get_id_from_href(href): + """Return the id portion of a url. + + Given: http://www.foo.com/bar/123?q=4 + Returns: 4 + + """ try: return int(urlparse(href).path.split('/')[-1]) except: - raise exc.HTTPBadRequest(_('could not parse id from href')) + raise webob.exc.HTTPBadRequest(_('could not parse id from href')) diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 3100c46b5..fad361bd4 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -25,27 +25,29 @@ from nova import utils class ViewBuilder(object): - ''' - Models a server response as a python dictionary. + """Model a server response as a python dictionary. + + Public methods: build Abstract methods: _build_image, _build_flavor - ''' + + """ def __init__(self, addresses_builder): self.addresses_builder = addresses_builder def build(self, inst, is_detail): - ''' Returns a dict that represenst a server ''' + """Return a dict that represenst a server.""" if is_detail: return self._build_detail(inst) else: return self._build_simple(inst) def _build_simple(self, inst): - ''' Returns a simple model of a server ''' + """Return a simple model of a server.""" return dict(server=dict(id=inst['id'], name=inst['display_name'])) def _build_detail(self, inst): - ''' Returns a detailed model of a server ''' + """Returns a detailed model of a server.""" power_mapping = { None: 'build', power_state.NOSTATE: 'build', @@ -80,16 +82,16 @@ class ViewBuilder(object): return dict(server=inst_dict) def _build_image(self, response, inst): - ''' Returns the image sub-resource of a server ''' + """Return the image sub-resource of a server.""" raise NotImplementedError() def _build_flavor(self, response, inst): - ''' Returns the flavor sub-resource of a server ''' + """Return the flavor sub-resource of a server.""" raise NotImplementedError() class ViewBuilderV10(ViewBuilder): - ''' Models an Openstack API V1.0 server response ''' + """Model an Openstack API V1.0 server response.""" def _build_image(self, response, inst): response['imageId'] = inst['image_id'] @@ -99,7 +101,7 @@ class ViewBuilderV10(ViewBuilder): class ViewBuilderV11(ViewBuilder): - ''' Models an Openstack API V1.0 server response ''' + """Model an Openstack API V1.0 server response.""" def __init__(self, addresses_builder, flavor_builder, image_builder): ViewBuilder.__init__(self, addresses_builder) -- cgit From 789fcb46915dce5fa533357ac462040ec6aa8968 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Tue, 22 Mar 2011 20:26:45 +0000 Subject: Adding BASE_IMAGE_ATTRS to ImageService --- nova/api/openstack/images.py | 57 +++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 27 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 0c56b5f0d..97e62c22d 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -119,43 +119,46 @@ def _translate_s3_like_images(image_metadata): def _translate_from_image_service_to_api(image_metadata): """Translate from ImageService to OpenStack API style attribute names - This involves 3 steps: + This involves 4 steps: - 1. Translating required keys + 1. Filter out attributes that the OpenStack API doesn't need - 2. Translating optional keys (ex. progress, serverId) + 2. Translate from base image attributes from names used by + BaseImageService to names used by OpenStack API - 3. Formatting values according to API spec (for example dates must + 3. Add in any image properties + + 4. Format values according to API spec (for example dates must look like "2010-08-10T12:00:00Z") """ service_metadata = image_metadata.copy() - api_metadata = {} - - # 1. Translate required keys - required_image_service2api = { - 'id': 'id', - 'name': 'name', - 'updated_at': 'updated', - 'created_at': 'created', - 'status': 'status'} - for service_attr, api_attr in required_image_service2api.items(): - api_metadata[api_attr] = service_metadata[service_attr] - - # 2. Translate optional keys - optional_image_service2api = {'instance_id': 'serverId'} - for service_attr, api_attr in optional_image_service2api.items(): - if service_attr in service_metadata: - api_metadata[api_attr] = service_metadata[service_attr] - - # 2a. Progress special case + properties = service_metadata.pop('properties', {}) + + # 1. Filter out unecessary attributes + api_keys = ['id', 'name', 'updated_at', 'created_at', 'status'] + api_metadata = utils.partition_dict(service_metadata, api_keys)[0] + + # 2. Translate base image attributes + api_map = {'updated_at': 'updated', 'created_at': 'created'} + api_metadata = utils.map_dict_keys(api_metadata, api_map) + + # 3. Add in any image properties + # 3a. serverId is used for backups and snapshots + try: + api_metadata['serverId'] = int(properties['instance_id']) + except KeyError: + pass # skip if it's not present + except ValueError: + pass # skip if it's not an integer + + # 3b. Progress special case # TODO(sirp): ImageService doesn't have a notion of progress yet, so for # now just fake it if service_metadata['status'] == 'saving': api_metadata['progress'] = 0 - # 3. Format values - - # 3a. Format Image Status (API requires uppercase) + # 4. Format values + # 4a. Format Image Status (API requires uppercase) status_service2api = {'queued': 'QUEUED', 'preparing': 'PREPARING', 'saving': 'SAVING', @@ -163,7 +166,7 @@ def _translate_from_image_service_to_api(image_metadata): 'killed': 'FAILED'} api_metadata['status'] = status_service2api[api_metadata['status']] - # 3b. Format timestamps + # 4b. Format timestamps def _format_timestamp(dt_str): """Return a timestamp formatted for OpenStack API -- cgit From 7d97f700811468303c21159454db64e84f71a2a0 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Tue, 22 Mar 2011 23:49:24 +0000 Subject: Refactored out _safe_translate code --- nova/api/openstack/images.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 97e62c22d..2d2f67fe1 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -193,6 +193,21 @@ def _translate_from_image_service_to_api(image_metadata): return api_metadata +def _safe_translate(image_metadata): + """Translate attributes for OpenStack API, temporary workaround for + S3ImageService attribute leakage. + """ + # FIXME(sirp): The S3ImageService appears to be leaking implementation + # details, including its internal attribute names, and internal + # `status` values. Working around it for now. + s3_like_image = ('imageId' in image_metadata) + if s3_like_image: + translate = _translate_s3_like_images + else: + translate = _translate_from_image_service_to_api + return translate(image_metadata) + + class Controller(wsgi.Controller): _serialization_metadata = { @@ -221,30 +236,18 @@ class Controller(wsgi.Controller): req.environ['nova.context']) service_image_metas = common.limited(service_image_metas, req) - - # FIXME(sirp): The S3ImageService appears to be leaking implementation - # details, including its internal attribute names, and internal - # `status` values. Working around it for now. - s3_like_image = (service_image_metas and - ('imageId' in service_image_metas[0])) - if s3_like_image: - translate = _translate_s3_like_images - else: - translate = _translate_from_image_service_to_api - - api_image_metas = [translate(service_image_meta) + api_image_metas = [_safe_translate(service_image_meta) for service_image_meta in service_image_metas] - return dict(images=api_image_metas) def show(self, req, id): """Return data about the given image id""" image_id = common.get_image_id_from_image_hash(self._service, req.environ['nova.context'], id) - - image = self._service.show(req.environ['nova.context'], image_id) - _convert_image_id_to_hash(image) - return dict(image=image) + service_image_meta = self._service.show( + req.environ['nova.context'], image_id) + api_image_meta = _safe_translate(service_image_meta) + return dict(image=api_image_meta) def delete(self, req, id): # Only public images are supported for now. @@ -255,11 +258,10 @@ class Controller(wsgi.Controller): env = self._deserialize(req.body, req.get_content_type()) instance_id = env["image"]["serverId"] name = env["image"]["name"] - - image_meta = compute.API().snapshot( + service_image_meta = compute.API().snapshot( context, instance_id, name) - - return dict(image=image_meta) + api_image_meta = _safe_translate(service_image_meta) + return dict(image=api_image_meta) def update(self, req, id): # Users may not modify public images, and that's all that -- cgit From 209da18033a49062bbcfaf7739db5959be87b142 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 22 Mar 2011 20:36:49 -0700 Subject: pep8 and fixed up zone-list --- nova/api/openstack/servers.py | 2 -- nova/api/openstack/zones.py | 18 +++++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 199d89c6d..db6a1de97 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -91,10 +91,8 @@ class Controller(wsgi.Controller): def show(self, req, id): """ Returns server details by server id """ try: - LOG.debug(_("***SHOW")) instance = self.compute_api.routing_get( req.environ['nova.context'], id) - LOG.debug(_("***SHOW OUT %s" % instance)) builder = servers_views.get_view_builder(req) return builder.build(instance, is_detail=True) except exception.NotFound: diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index d129cf34f..6ce27e9a9 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -15,7 +15,6 @@ import common -from nova import db from nova import flags from nova import log as logging from nova import wsgi @@ -39,7 +38,8 @@ def _exclude_keys(item, keys): def _scrub_zone(zone): - return _filter_keys(zone, ('id', 'api_url')) + return _exclude_keys(zone, ('username', 'password', 'created_at', + 'deleted', 'deleted_at', 'updated_at')) class Controller(wsgi.Controller): @@ -54,12 +54,8 @@ class Controller(wsgi.Controller): # Ask the ZoneManager in the Scheduler for most recent data, # or fall-back to the database ... items = api.get_zone_list(req.environ['nova.context']) - if not items: - items = db.zone_get_all(req.environ['nova.context']) - items = common.limited(items, req) - items = [_exclude_keys(item, ['username', 'password']) - for item in items] + items = [_scrub_zone(item) for item in items] return dict(zones=items) def detail(self, req): @@ -82,23 +78,23 @@ class Controller(wsgi.Controller): def show(self, req, id): """Return data about the given zone id""" zone_id = int(id) - zone = db.zone_get(req.environ['nova.context'], zone_id) + zone = api.zone_get(req.environ['nova.context'], zone_id) return dict(zone=_scrub_zone(zone)) def delete(self, req, id): zone_id = int(id) - db.zone_delete(req.environ['nova.context'], zone_id) + api.zone_delete(req.environ['nova.context'], zone_id) return {} def create(self, req): context = req.environ['nova.context'] env = self._deserialize(req.body, req.get_content_type()) - zone = db.zone_create(context, env["zone"]) + zone = api.zone_create(context, env["zone"]) return dict(zone=_scrub_zone(zone)) def update(self, req, id): context = req.environ['nova.context'] env = self._deserialize(req.body, req.get_content_type()) zone_id = int(id) - zone = db.zone_update(context, zone_id, env["zone"]) + zone = api.zone_update(context, zone_id, env["zone"]) return dict(zone=_scrub_zone(zone)) -- cgit From 846b09925da07c2858052143d5fff4766a782cf1 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 22 Mar 2011 22:54:34 -0700 Subject: Fix for lp740742 - format describe_instance_output correctly to prevent errors in dashboard --- nova/api/ec2/admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index 037184b40..d8d90ad83 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -120,7 +120,8 @@ class AdminController(object): def describe_instance_types(self, context, **_kwargs): """Returns all active instance types data (vcpus, memory, etc.)""" - return {'instanceTypeSet': [db.instance_type_get_all(context)]} + return {'instanceTypeSet': [instance_dict(v) for v in + db.instance_type_get_all(context).values()]} def describe_user(self, _context, name, **_kwargs): """Returns user data, including access and secret keys.""" -- cgit From ac475d05e6807804a74bca665563c7260523a733 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Wed, 23 Mar 2011 06:00:04 +0000 Subject: Small cleanup of openstack/images.py --- nova/api/openstack/images.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 2d2f67fe1..d914f5196 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -221,32 +221,28 @@ class Controller(wsgi.Controller): def index(self, req): """Return all public images in brief""" - items = self._service.index(req.environ['nova.context']) - items = common.limited(items, req) - items = [_filter_keys(item, ('id', 'name')) for item in items] - return dict(images=items) + context = req.environ['nova.context'] + image_metas = self._service.index(context) + image_metas = common.limited(image_metas, req) + return dict(images=image_metas) def detail(self, req): """Return all public images in detail""" - try: - service_image_metas = self._service.detail( - req.environ['nova.context']) - except NotImplementedError: - service_image_metas = self._service.index( - req.environ['nova.context']) - - service_image_metas = common.limited(service_image_metas, req) - api_image_metas = [_safe_translate(service_image_meta) - for service_image_meta in service_image_metas] + context = req.environ['nova.context'] + image_metas = self._service.detail(context) + image_metas = common.limited(image_metas, req) + api_image_metas = [_safe_translate(image_meta) + for image_meta in image_metas] return dict(images=api_image_metas) def show(self, req, id): """Return data about the given image id""" - image_id = common.get_image_id_from_image_hash(self._service, - req.environ['nova.context'], id) - service_image_meta = self._service.show( - req.environ['nova.context'], image_id) - api_image_meta = _safe_translate(service_image_meta) + context = req.environ['nova.context'] + image_id = common.get_image_id_from_image_hash( + self._service, req.environ['nova.context'], id) + + image_meta = self._service.show(context, image_id) + api_image_meta = _safe_translate(image_meta) return dict(image=api_image_meta) def delete(self, req, id): @@ -258,9 +254,9 @@ class Controller(wsgi.Controller): env = self._deserialize(req.body, req.get_content_type()) instance_id = env["image"]["serverId"] name = env["image"]["name"] - service_image_meta = compute.API().snapshot( + image_meta = compute.API().snapshot( context, instance_id, name) - api_image_meta = _safe_translate(service_image_meta) + api_image_meta = _safe_translate(image_meta) return dict(image=api_image_meta) def update(self, req, id): -- cgit From 32e1c38ef9539be6f914adc69f30e409b159a9e6 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Wed, 23 Mar 2011 06:55:28 +0000 Subject: Adding tests for owned and non-existent images --- nova/api/openstack/images.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index d914f5196..ab286bb45 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -20,6 +20,7 @@ import datetime from webob import exc from nova import compute +from nova import exception from nova import flags from nova import log from nova import utils @@ -238,8 +239,11 @@ class Controller(wsgi.Controller): def show(self, req, id): """Return data about the given image id""" context = req.environ['nova.context'] - image_id = common.get_image_id_from_image_hash( - self._service, req.environ['nova.context'], id) + try: + image_id = common.get_image_id_from_image_hash( + self._service, context, id) + except exception.NotFound: + raise faults.Fault(exc.HTTPNotFound()) image_meta = self._service.show(context, image_id) api_image_meta = _safe_translate(image_meta) -- cgit From 07af0c9653863575600986158b89ff6afa48996e Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Wed, 23 Mar 2011 07:30:01 +0000 Subject: Use subset_dict --- nova/api/openstack/images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index ab286bb45..19edcf194 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -137,7 +137,7 @@ def _translate_from_image_service_to_api(image_metadata): # 1. Filter out unecessary attributes api_keys = ['id', 'name', 'updated_at', 'created_at', 'status'] - api_metadata = utils.partition_dict(service_metadata, api_keys)[0] + api_metadata = utils.subset_dict(service_metadata, api_keys) # 2. Translate base image attributes api_map = {'updated_at': 'updated', 'created_at': 'created'} -- cgit From ff9e29e3ef56ec8b28f28d328ca010ce25f0c7b0 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 23 Mar 2011 09:47:22 -0400 Subject: Removed some un-needed code, and started adding tests for show(), which I forgot\! --- nova/api/openstack/images.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 4cd989054..d8606e3c2 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -13,9 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. -from webob import exc +import webob.exc from nova import compute +from nova import exception from nova import flags from nova import utils from nova import wsgi @@ -39,11 +40,6 @@ class Controller(wsgi.Controller): }, } - _builder_dispatch = { - "1.0": images_view.ViewBuilderV10, - "1.1": images_view.ViewBuilderV11, - } - def __init__(self, image_service=None, compute_service=None): """ Initialize new `ImageController`. @@ -60,7 +56,7 @@ class Controller(wsgi.Controller): """ Return an index listing of images available to the request. - @param req: `webob.Request` object + @param req: `wsgi.Request` object """ context = req.environ['nova.context'] images = self.__image.index(context) @@ -71,31 +67,38 @@ class Controller(wsgi.Controller): """ Return a detailed index listing of images available to the request. - @param req: `webob.Request` object. + @param req: `wsgi.Request` object. """ context = req.environ['nova.context'] images = self.__image.detail(context) build = self.get_builder(req).build return dict(images=[build(image, True) for image in images]) - def show(self, req, image_id): + def show(self, req, id): """ Return detailed information about a specific image. - @param req: `webob.Request` object - @param image_id: Image identifier (integer) + @param req: `wsgi.Request` object + @param id: Image identifier (integer) """ + image_id = id context = req.environ['nova.context'] - image = self.__image.show(context, image_id) - return self.get_builder().build(req, image, True) - def delete(self, req, image_id): + try: + image = self.__image.show(context, image_id) + except exception.NotFound: + raise webob.exc.HTTPNotFound + + return self.get_builder(req).build(image, True) + + def delete(self, req, id): """ Delete an image, if allowed. - @param req: `webob.Request` object - @param image_id: Image identifier (integer) + @param req: `wsgi.Request` object + @param id: Image identifier (integer) """ + image_id = id context = req.environ['nova.context'] self.__image.delete(context, image_id) return exc.HTTPNoContent() @@ -104,7 +107,7 @@ class Controller(wsgi.Controller): """ Snapshot a server instance and save the image. - @param req: `webob.Request` object + @param req: `wsgi.Request` object """ context = req.environ['nova.context'] body = req.body -- cgit From 572b6d30c809af6e117d96de9a5a2d845c1eeda0 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 23 Mar 2011 11:52:43 -0400 Subject: Testing of XML and JSON for show(), and conformance to API spec for JSON. --- nova/api/openstack/images.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index d8606e3c2..38c8a7306 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -21,6 +21,7 @@ from nova import flags 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 @@ -87,9 +88,10 @@ class Controller(wsgi.Controller): try: image = self.__image.show(context, image_id) except exception.NotFound: - raise webob.exc.HTTPNotFound + ex = webob.exc.HTTPNotFound(explanation="Image not found.") + raise faults.Fault(ex) - return self.get_builder(req).build(image, True) + return dict(image=self.get_builder(req).build(image, True)) def delete(self, req, id): """ -- cgit From 1f90c7c6555e042cda1371a22c9891713a3f6430 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Wed, 23 Mar 2011 13:01:59 -0400 Subject: Implement v1.1 image metadata. --- nova/api/openstack/__init__.py | 6 ++- nova/api/openstack/image_metadata.py | 95 ++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 nova/api/openstack/image_metadata.py (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 21d354f1c..c12aa7e89 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -33,6 +33,7 @@ from nova.api.openstack import backup_schedules 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 limits from nova.api.openstack import servers from nova.api.openstack import shared_ip_groups @@ -150,7 +151,10 @@ class APIRouterV11(APIRouter): controller=servers.ControllerV11(), collection={'detail': 'GET'}, member=self.server_members) - + mapper.resource("image_meta", "meta", + controller=image_metadata.Controller(), + parent_resource=dict(member_name='image', + collection_name='images')) class Versions(wsgi.Application): @webob.dec.wsgify(RequestClass=wsgi.Request) diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py new file mode 100644 index 000000000..5ca349af1 --- /dev/null +++ b/nova/api/openstack/image_metadata.py @@ -0,0 +1,95 @@ +# 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. + +from webob import exc + +from nova import flags +from nova import utils +from nova import wsgi +from nova.api.openstack import faults + + +FLAGS = flags.FLAGS + + +class Controller(wsgi.Controller): + """ The image metadata API controller for the Openstack API """ + + def __init__(self): + self.image_service = utils.import_object(FLAGS.image_service) + super(Controller, self).__init__() + + def _get_metadata(self, context, image_id, image=None): + if not image: + image = self.image_service.show(context, image_id) + metadata = {} + if 'properties' in image: + metadata = image['properties'] + return metadata + + def index(self, req, image_id): + """ Returns the list of metadata for a given instance """ + context = req.environ['nova.context'] + metadata = self._get_metadata(context, image_id) + return dict(metadata=metadata) + + def show(self, req, image_id, id): + context = req.environ['nova.context'] + metadata = self._get_metadata(context, image_id) + if id in metadata: + return {id: metadata[id]} + else: + return faults.Fault(exc.HTTPNotFound()) + + def create(self, req, image_id): + context = req.environ['nova.context'] + body = self._deserialize(req.body, req.get_content_type()) + img = self.image_service.show(context, image_id) + metadata = self._get_metadata(context, image_id, img) + if 'metadata' in body: + for key, value in body['metadata'].iteritems(): + metadata[key] = value + img['properties'] = metadata + self.image_service.update(context, image_id, img, None) + return dict(metadata=metadata) + + def update(self, req, image_id, id): + context = req.environ['nova.context'] + body = self._deserialize(req.body, req.get_content_type()) + if not id in body: + expl = _('Request body and URI mismatch') + raise exc.HTTPBadRequest(explanation=expl) + if len(body) > 1: + expl = _('Request body contains too many items') + raise exc.HTTPBadRequest(explanation=expl) + img = self.image_service.show(context, image_id) + metadata = self._get_metadata(context, image_id, img) + metadata[id] = body[id] + img['properties'] = metadata + self.image_service.update(context, image_id, img, None) + + return req.body + + def delete(self, req, image_id, id): + context = req.environ['nova.context'] + img = self.image_service.show(context, image_id) + metadata = self._get_metadata(context, image_id) + if not id in metadata: + return faults.Fault(exc.HTTPNotFound()) + metadata.pop(id) + img['properties'] = metadata + self.image_service.update(context, image_id, img, None) -- cgit From b49ac333df4de61ca632666cca85f6e9baf788b0 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Wed, 23 Mar 2011 13:04:44 -0400 Subject: pep8 fix. --- nova/api/openstack/__init__.py | 1 + 1 file changed, 1 insertion(+) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index c12aa7e89..efb10eb1b 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -156,6 +156,7 @@ class APIRouterV11(APIRouter): parent_resource=dict(member_name='image', collection_name='images')) + class Versions(wsgi.Application): @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): -- cgit From ea92a88b727814698dbc4ebf5dc705677d636445 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Wed, 23 Mar 2011 14:05:21 -0400 Subject: Using super to call parent _setup_routes in APIRouter subclasses. --- nova/api/openstack/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 5f9648210..e68110bc4 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -134,7 +134,7 @@ class APIRouterV10(APIRouter): """Define routes specific to OpenStack API V1.0.""" def _setup_routes(self, mapper): - APIRouter._setup_routes(self, mapper) + super(APIRouterV10, self)._setup_routes(mapper) mapper.resource("server", "servers", controller=servers.ControllerV10(), collection={'detail': 'GET'}, @@ -145,7 +145,7 @@ class APIRouterV11(APIRouter): """Define routes specific to OpenStack API V1.1.""" def _setup_routes(self, mapper): - APIRouter._setup_routes(self, mapper) + super(APIRouterV11, self)._setup_routes(mapper) mapper.resource("server", "servers", controller=servers.ControllerV11(), collection={'detail': 'GET'}, -- cgit From c3d47689a762bfa4aa38c7d4700bb1969d37d1d1 Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Wed, 23 Mar 2011 18:56:23 +0000 Subject: merge prop changes --- nova/api/openstack/views/servers.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 709052f22..a21a6e7ff 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -18,6 +18,7 @@ import hashlib from nova.compute import power_state +import nova.compute.api import nova.context from nova import db from nova.api.openstack import common @@ -87,14 +88,10 @@ class ViewBuilder(object): for k, v in mapped_keys.iteritems(): inst_dict[k] = inst[v] + ctxt = nova.context.get_admin_context() inst_dict['status'] = power_mapping[inst_dict['status']] - try: - ctxt = nova.context.get_admin_context() - migration = db.migration_get_by_instance_and_status(ctxt, - inst['id'], 'finished') + if nova.compute.api.has_finished_migration(ctxt, inst['id']): inst_dict['status'] = 'resize-confirm' - except: - pass inst_dict['addresses'] = self.addresses_builder.build(inst) -- cgit From 5a5c7d22e7a00c9a3b34f8c08db70b644eee2d92 Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Wed, 23 Mar 2011 19:16:03 +0000 Subject: Unit test cleanup --- nova/api/openstack/views/servers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index a21a6e7ff..18d31a29d 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -18,7 +18,7 @@ import hashlib from nova.compute import power_state -import nova.compute.api +import nova.compute import nova.context from nova import db from nova.api.openstack import common @@ -90,7 +90,8 @@ class ViewBuilder(object): ctxt = nova.context.get_admin_context() inst_dict['status'] = power_mapping[inst_dict['status']] - if nova.compute.api.has_finished_migration(ctxt, inst['id']): + compute_api = nova.compute.API() + if compute_api.has_finished_migration(ctxt, inst['id']): inst_dict['status'] = 'resize-confirm' inst_dict['addresses'] = self.addresses_builder.build(inst) -- cgit From db6aaa666dc1deaeead7f32fd22a4f6b2d40ed25 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 23 Mar 2011 15:17:34 -0400 Subject: fixing some dictionary get calls --- nova/api/openstack/views/servers.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 5f4c0f740..6b5dc7c16 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -71,7 +71,7 @@ class ViewBuilder(object): # Return the metadata as a dictionary metadata = {} - if 'metadata' in inst: + if 'metadata' in dict(inst): for item in inst['metadata']: metadata[item['key']] = item['value'] inst_dict['metadata'] = metadata @@ -97,11 +97,11 @@ class ViewBuilder(object): class ViewBuilderV10(ViewBuilder): def _build_image(self, response, inst): - if inst.get('image_id') != None: + if 'image_id' in dict(inst): response['imageId'] = inst['image_id'] def _build_flavor(self, response, inst): - if inst.get('instance_type') != None: + if 'instance_type' in dict(inst): response['flavorId'] = inst['instance_type'] @@ -114,16 +114,15 @@ class ViewBuilderV11(ViewBuilder): self.base_url = base_url def _build_image(self, response, inst): - image_id = inst.get("image_id", None) - if image_id == None: - return - response["imageRef"] = self.image_builder.generate_href(image_id) + if "image_id" in dict(inst): + image_id = inst.get("image_id") + response["imageRef"] = self.image_builder.generate_href(image_id) def _build_flavor(self, response, inst): - flavor_id = inst.get("instance_type", None) - if flavor_id == None: - return - response["flavorRef"] = self.flavor_builder.generate_href(flavor_id) + if "instance_type" in dict(inst): + flavor_id = inst["instance_type"] + flavor_ref = self.flavor_builder.generate_href(flavor_id) + response["flavorRef"] = flavor_ref def _build_extra(self, response, inst): self._build_links(response, inst) -- cgit From 05e6f82aa971606f7d33fb1de8f2c1c170d030de Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Wed, 23 Mar 2011 12:31:15 -0700 Subject: indenting cleanup --- nova/api/openstack/zones.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index d129cf34f..d4a59993b 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -17,7 +17,6 @@ import common from nova import db from nova import flags -from nova import log as logging from nova import wsgi from nova.scheduler import api @@ -73,8 +72,8 @@ class Controller(wsgi.Controller): zone = dict(name=FLAGS.zone_name) caps = FLAGS.zone_capabilities for cap in caps: - key_values = cap.split('=') - zone[key_values[0]] = key_values[1] + key, value = cap.split('=') + zone[key] = value for item, (min_value, max_value) in items.iteritems(): zone[item] = "%s,%s" % (min_value, max_value) return dict(zone=zone) -- cgit From 8eab4f6ecaf51221b335e76d9e532a1f159c2f2d Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Wed, 23 Mar 2011 19:44:32 +0000 Subject: Forgot extraneous module import --- nova/api/openstack/servers.py | 1 - 1 file changed, 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index f3367e118..d392ab57f 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -23,7 +23,6 @@ from webob import exc from nova import compute from nova import context -from nova import db from nova import exception from nova import flags from nova import log as logging -- cgit From 0218a11bb1d5275d5b99c98aea1edba0f45f56e2 Mon Sep 17 00:00:00 2001 From: "matt.dietz@rackspace.com" <> Date: Wed, 23 Mar 2011 19:48:26 +0000 Subject: Forgot extraneous module import again --- nova/api/openstack/views/servers.py | 1 - 1 file changed, 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 18d31a29d..68f712e56 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -20,7 +20,6 @@ import hashlib from nova.compute import power_state import nova.compute import nova.context -from nova import db from nova.api.openstack import common from nova.api.openstack.views import addresses as addresses_view from nova.api.openstack.views import flavors as flavors_view -- cgit From 0d677a9b63ed9b4612379494bf8a58af1c090331 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Wed, 23 Mar 2011 16:51:30 -0400 Subject: Should not call super __init__ twice in APIRouter --- nova/api/openstack/__init__.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index e68110bc4..143b1d2b2 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -127,8 +127,6 @@ class APIRouter(wsgi.Router): _limits = limits.LimitsController() mapper.resource("limit", "limits", controller=_limits) - super(APIRouter, self).__init__(mapper) - class APIRouterV10(APIRouter): """Define routes specific to OpenStack API V1.0.""" -- cgit From f52a2a8a440b303e5289815ab4f6c2d24bfdc59f Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Thu, 24 Mar 2011 01:41:38 -0400 Subject: Fixed the docstring for common.get_id_from_href --- nova/api/openstack/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 21ceec45e..bff050347 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -79,10 +79,10 @@ def get_image_id_from_image_hash(image_service, context, image_hash): def get_id_from_href(href): - """Return the id portion of a url. + """Return the id portion of a url as an int. Given: http://www.foo.com/bar/123?q=4 - Returns: 4 + Returns: 123 """ try: -- cgit From 1894937e1ef6769a5f76c0a382931480e2547ce8 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 01:03:41 -0700 Subject: Added volume_attachments --- nova/api/openstack/__init__.py | 6 ++ nova/api/openstack/volume_attachments.py | 154 +++++++++++++++++++++++++++++++ nova/api/openstack/volumes.py | 60 ++++++------ 3 files changed, 190 insertions(+), 30 deletions(-) create mode 100644 nova/api/openstack/volume_attachments.py (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 474c1d0e6..af3f8c5ce 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -38,6 +38,7 @@ from nova.api.openstack import servers from nova.api.openstack import shared_ip_groups from nova.api.openstack import users from nova.api.openstack import volumes +from nova.api.openstack import volume_attachments from nova.api.openstack import zones @@ -109,6 +110,11 @@ class APIRouter(wsgi.Router): parent_resource=dict(member_name='server', collection_name='servers')) + mapper.resource("volume_attachment", "volume_attachment", + controller=volume_attachments.Controller(), + parent_resource=dict(member_name='server', + collection_name='servers')) + mapper.resource("console", "consoles", controller=consoles.Controller(), parent_resource=dict(member_name='server', diff --git a/nova/api/openstack/volume_attachments.py b/nova/api/openstack/volume_attachments.py new file mode 100644 index 000000000..fbcec7c29 --- /dev/null +++ b/nova/api/openstack/volume_attachments.py @@ -0,0 +1,154 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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 webob import exc + +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + +FLAGS = flags.FLAGS + + +def _translate_detail_view(context, volume): + """ Maps keys for details view""" + + v = _translate_summary_view(context, volume) + + # No additional data / lookups at the moment + + return v + + +def _translate_summary_view(context, volume): + """ Maps keys for summary view""" + v = {} + + volume_id = volume['id'] + + # NOTE(justinsb): We use the volume id as the id of the attachment object + v['id'] = volume_id + + v['volumeId'] = volume_id + v['serverId'] = volume['instance_id'] + v['device'] = volume['mountpoint'] + + return v + + +class Controller(wsgi.Controller): + """ The volume attachment API controller for the Openstack API + + A child resource of the server. Note that we use the volume id + as the ID of the attachment (though this is not guaranteed externally)""" + + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'volumeAttachment': [ 'id', + 'serverId', + 'volumeId', + 'device' ]}}} + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(Controller, self).__init__() + + def index(self, req, server_id): + """ Returns the list of volume attachments for a given instance """ + return self._items(req, server_id, + entity_maker=_translate_summary_view) + + def show(self, req, id): + """Return data about the given volume""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get(context, id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + return {'volume': _translate_detail_view(context, vol)} + + def create(self, req, server_id): + """ Attach a volume to an instance """ + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + instance_id = server_id + volume_id = env['volumeAttachment']['volumeId'] + device = env['volumeAttachment']['device'] + + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" + " at %(device)s") % locals() + LOG.audit(msg, context=context) + + self.compute_api.attach_volume(context, + instance_id=instance_id, + volume_id=volume_id, + device=device) + vol = self.volume_api.get(context, volume_id) + + retval = _translate_detail_view(context, vol) + + return {'volumeAttachment': retval} + + def update(self, _req, _server_id, _id): + """ Update a volume attachment. We don't currently support this.""" + return faults.Fault(exc.HTTPBadRequest()) + + def delete(self, req, server_id, id): + """ Detach a volume from an instance """ + context = req.environ['nova.context'] + + volume_id = id + LOG.audit(_("Detach volume %s"), volume_id, context=context) + + vol = self.volume_api.get(context, volume_id) + if vol['instance_id'] != server_id: + return faults.Fault(exc.HTTPNotFound()) + + self.compute_api.detach_volume(context, + volume_id=volume_id) + + return exc.HTTPAccepted() + + def _items(self, req, server_id, entity_maker): + """Returns a list of attachments, transformed through entity_maker""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.get(context, server_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + volumes = instance['volumes'] + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumeAttachments': res} diff --git a/nova/api/openstack/volumes.py b/nova/api/openstack/volumes.py index 99300421e..ea2dc4aab 100644 --- a/nova/api/openstack/volumes.py +++ b/nova/api/openstack/volumes.py @@ -29,52 +29,52 @@ LOG = logging.getLogger("nova.api.volumes") FLAGS = flags.FLAGS -def _translate_detail_view(context, inst): +def _translate_detail_view(context, vol): """ Maps keys for details view""" - inst_dict = _translate_summary_view(context, inst) + d = _translate_summary_view(context, vol) # No additional data / lookups at the moment - return inst_dict + return d -def _translate_summary_view(context, volume): +def _translate_summary_view(_context, vol): """ Maps keys for summary view""" - v = {} + d = {} instance_id = None # instance_data = None - attached_to = volume.get('instance') + attached_to = vol.get('instance') if attached_to: instance_id = attached_to['id'] # instance_data = '%s[%s]' % (instance_ec2_id, # attached_to['host']) - v['id'] = volume['id'] - v['status'] = volume['status'] - v['size'] = volume['size'] - v['availabilityZone'] = volume['availability_zone'] - v['createdAt'] = volume['created_at'] + d['id'] = vol['id'] + d['status'] = vol['status'] + d['size'] = vol['size'] + d['availabilityZone'] = vol['availability_zone'] + d['createdAt'] = vol['created_at'] # if context.is_admin: # v['status'] = '%s (%s, %s, %s, %s)' % ( - # volume['status'], - # volume['user_id'], - # volume['host'], + # vol['status'], + # vol['user_id'], + # vol['host'], # instance_data, - # volume['mountpoint']) - if volume['attach_status'] == 'attached': - v['attachments'] = [{'attachTime': volume['attach_time'], + # vol['mountpoint']) + if vol['attach_status'] == 'attached': + d['attachments'] = [{'attachTime': vol['attach_time'], 'deleteOnTermination': False, - 'mountpoint': volume['mountpoint'], + 'mountpoint': vol['mountpoint'], 'instanceId': instance_id, 'status': 'attached', - 'volumeId': volume['id']}] + 'volumeId': vol['id']}] else: - v['attachments'] = [{}] + d['attachments'] = [{}] - v['displayName'] = volume['display_name'] - v['displayDescription'] = volume['display_description'] - return v + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d class Controller(wsgi.Controller): @@ -102,11 +102,11 @@ class Controller(wsgi.Controller): context = req.environ['nova.context'] try: - volume = self.volume_api.get(context, id) + vol = self.volume_api.get(context, id) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) - return {'volume': _translate_detail_view(context, volume)} + return {'volume': _translate_detail_view(context, vol)} def delete(self, req, id): """ Delete a volume """ @@ -134,7 +134,7 @@ class Controller(wsgi.Controller): volumes = self.volume_api.get_all(context) limited_list = common.limited(volumes, req) - res = [entity_maker(context, inst) for inst in limited_list] + res = [entity_maker(context, vol) for vol in limited_list] return {'volumes': res} def create(self, req): @@ -148,13 +148,13 @@ class Controller(wsgi.Controller): vol = env['volume'] size = vol['size'] LOG.audit(_("Create volume of %s GB"), size, context=context) - volume = self.volume_api.create(context, size, - vol.get('display_name'), - vol.get('display_description')) + new_volume = self.volume_api.create(context, size, + vol.get('display_name'), + vol.get('display_description')) # Work around problem that instance is lazy-loaded... volume['instance'] = None - retval = _translate_detail_view(context, volume) + retval = _translate_detail_view(context, new_volume) return {'volume': retval} -- cgit From 699adb4311fdd86525fae022f4119401fd1c0168 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 01:37:14 -0700 Subject: Added simple nova volume tests --- nova/api/openstack/volume_attachments.py | 2 +- nova/api/openstack/volumes.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/volume_attachments.py b/nova/api/openstack/volume_attachments.py index fbcec7c29..1cb2c9494 100644 --- a/nova/api/openstack/volume_attachments.py +++ b/nova/api/openstack/volume_attachments.py @@ -97,7 +97,7 @@ class Controller(wsgi.Controller): """ Attach a volume to an instance """ context = req.environ['nova.context'] - env = self._deserialize(req.body, req) + env = self._deserialize(req.body, req.get_content_type()) if not env: return faults.Fault(exc.HTTPUnprocessableEntity()) diff --git a/nova/api/openstack/volumes.py b/nova/api/openstack/volumes.py index ea2dc4aab..ec3b9a6c8 100644 --- a/nova/api/openstack/volumes.py +++ b/nova/api/openstack/volumes.py @@ -141,7 +141,7 @@ class Controller(wsgi.Controller): """Creates a new volume""" context = req.environ['nova.context'] - env = self._deserialize(req.body, req) + env = self._deserialize(req.body, req.get_content_type()) if not env: return faults.Fault(exc.HTTPUnprocessableEntity()) @@ -153,7 +153,7 @@ class Controller(wsgi.Controller): vol.get('display_description')) # Work around problem that instance is lazy-loaded... - volume['instance'] = None + new_volume['instance'] = None retval = _translate_detail_view(context, new_volume) -- cgit From 230d07e9002371bdb0030c9199df35fc6360a0a2 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 03:26:32 -0700 Subject: Test for attach / detach (and associated fixes) --- nova/api/openstack/__init__.py | 2 +- nova/api/openstack/volume_attachments.py | 77 +++++++++++++++++++++----------- 2 files changed, 53 insertions(+), 26 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 54d8a738d..e8aa4821b 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -134,7 +134,7 @@ class APIRouter(wsgi.Router): controller=volumes.Controller(), collection={'detail': 'GET'}) - mapper.resource("volume_attachment", "volume_attachment", + mapper.resource("volume_attachment", "volume_attachments", controller=volume_attachments.Controller(), parent_resource=dict(member_name='server', collection_name='servers')) diff --git a/nova/api/openstack/volume_attachments.py b/nova/api/openstack/volume_attachments.py index 1cb2c9494..2ce681e19 100644 --- a/nova/api/openstack/volume_attachments.py +++ b/nova/api/openstack/volume_attachments.py @@ -35,27 +35,29 @@ FLAGS = flags.FLAGS def _translate_detail_view(context, volume): """ Maps keys for details view""" - v = _translate_summary_view(context, volume) + d = _translate_summary_view(context, volume) # No additional data / lookups at the moment - return v + return d -def _translate_summary_view(context, volume): +def _translate_summary_view(context, vol): """ Maps keys for summary view""" - v = {} + d = {} + + volume_id = vol['id'] - volume_id = volume['id'] - # NOTE(justinsb): We use the volume id as the id of the attachment object - v['id'] = volume_id - - v['volumeId'] = volume_id - v['serverId'] = volume['instance_id'] - v['device'] = volume['mountpoint'] + d['id'] = volume_id - return v + d['volumeId'] = volume_id + if vol.get('instance_id'): + d['serverId'] = vol['instance_id'] + if vol.get('mountpoint'): + d['device'] = vol['mountpoint'] + + return d class Controller(wsgi.Controller): @@ -82,16 +84,22 @@ class Controller(wsgi.Controller): return self._items(req, server_id, entity_maker=_translate_summary_view) - def show(self, req, id): + def show(self, req, server_id, id): """Return data about the given volume""" context = req.environ['nova.context'] + volume_id = id try: - vol = self.volume_api.get(context, id) + vol = self.volume_api.get(context, volume_id) except exception.NotFound: + LOG.debug("volume_id not found") return faults.Fault(exc.HTTPNotFound()) - return {'volume': _translate_detail_view(context, vol)} + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + return {'volumeAttachment': _translate_detail_view(context, vol)} def create(self, req, server_id): """ Attach a volume to an instance """ @@ -109,15 +117,29 @@ class Controller(wsgi.Controller): " at %(device)s") % locals() LOG.audit(msg, context=context) - self.compute_api.attach_volume(context, - instance_id=instance_id, - volume_id=volume_id, - device=device) - vol = self.volume_api.get(context, volume_id) + try: + self.compute_api.attach_volume(context, + instance_id=instance_id, + volume_id=volume_id, + device=device) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + # The attach is async + attachment = {} + attachment['id'] = volume_id + attachment['volumeId'] = volume_id - retval = _translate_detail_view(context, vol) + # NOTE(justinsb): And now, we have a problem... + # The attach is async, so there's a window in which we don't see + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. + # For now, we'll probably have to rely on libraries being smart - return {'volumeAttachment': retval} + # TODO: How do I return "accepted" here?? + return {'volumeAttachment': attachment} def update(self, _req, _server_id, _id): """ Update a volume attachment. We don't currently support this.""" @@ -130,10 +152,15 @@ class Controller(wsgi.Controller): volume_id = id LOG.audit(_("Detach volume %s"), volume_id, context=context) - vol = self.volume_api.get(context, volume_id) - if vol['instance_id'] != server_id: + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") return faults.Fault(exc.HTTPNotFound()) - + self.compute_api.detach_volume(context, volume_id=volume_id) -- cgit From d49219f8b6dd626b868b99bee8a22c4ac5495af1 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 24 Mar 2011 03:28:59 -0700 Subject: pep8 fixes --- nova/api/openstack/__init__.py | 1 + nova/api/openstack/volume_attachments.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index e8aa4821b..030974482 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -141,6 +141,7 @@ class APIRouter(wsgi.Router): super(APIRouter, self).__init__(mapper) + class APIRouterV10(APIRouter): """Define routes specific to OpenStack API V1.0.""" diff --git a/nova/api/openstack/volume_attachments.py b/nova/api/openstack/volume_attachments.py index 2ce681e19..58a9a727b 100644 --- a/nova/api/openstack/volume_attachments.py +++ b/nova/api/openstack/volume_attachments.py @@ -62,17 +62,17 @@ def _translate_summary_view(context, vol): class Controller(wsgi.Controller): """ The volume attachment API controller for the Openstack API - + A child resource of the server. Note that we use the volume id as the ID of the attachment (though this is not guaranteed externally)""" _serialization_metadata = { 'application/xml': { 'attributes': { - 'volumeAttachment': [ 'id', - 'serverId', - 'volumeId', - 'device' ]}}} + 'volumeAttachment': ['id', + 'serverId', + 'volumeId', + 'device']}}} def __init__(self): self.compute_api = compute.API() -- cgit From 3b8f1f54136a67ba4c306e47b25b686328ec23b5 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 24 Mar 2011 10:15:50 -0400 Subject: making servers.generate_href more robust --- nova/api/openstack/views/servers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 08b53177b..4e7f62eb3 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -16,6 +16,7 @@ # under the License. import hashlib +import os from nova.compute import power_state import nova.compute @@ -164,4 +165,4 @@ class ViewBuilderV11(ViewBuilder): def generate_href(self, server_id): """Create an url that refers to a specific server id.""" - return "%s/servers/%s" % (self.base_url, server_id) + return os.path.join(self.base_url, "servers", str(server_id)) -- cgit From f900a3354e8a4d9925d1a28780942eee12efe91e Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 24 Mar 2011 12:15:33 -0400 Subject: Renamed __image and __compute to better describe their purposes. Use os.path.join to create href as per suggestion. Added base get_builder as per pychecker suggestion. --- nova/api/openstack/images.py | 24 ++++++++++++++---------- nova/api/openstack/views/images.py | 4 +++- 2 files changed, 17 insertions(+), 11 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index dc07348e4..b01d991e8 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -50,8 +50,8 @@ class Controller(wsgi.Controller): """ _default_service = utils.import_object(flags.FLAGS.image_service) - self.__compute = compute_service or compute.API() - self.__image = image_service or _default_service + self._compute_service = compute_service or compute.API() + self._image_service = image_service or _default_service def index(self, req): """ @@ -60,7 +60,7 @@ class Controller(wsgi.Controller): @param req: `wsgi.Request` object """ context = req.environ['nova.context'] - images = self.__image.index(context) + images = self._image_service.index(context) build = self.get_builder(req).build return dict(images=[build(image, False) for image in images]) @@ -71,7 +71,7 @@ class Controller(wsgi.Controller): @param req: `wsgi.Request` object. """ context = req.environ['nova.context'] - images = self.__image.detail(context) + images = self._image_service.detail(context) build = self.get_builder(req).build return dict(images=[build(image, True) for image in images]) @@ -86,7 +86,7 @@ class Controller(wsgi.Controller): context = req.environ['nova.context'] try: - image = self.__image.show(context, image_id) + image = self._image_service.show(context, image_id) except exception.NotFound: ex = webob.exc.HTTPNotFound(explanation="Image not found.") raise faults.Fault(ex) @@ -102,8 +102,8 @@ class Controller(wsgi.Controller): """ image_id = id context = req.environ['nova.context'] - self.__image.delete(context, image_id) - return exc.HTTPNoContent() + self._image_service.delete(context, image_id) + return webob.exc.HTTPNoContent() def create(self, req): """ @@ -116,17 +116,21 @@ class Controller(wsgi.Controller): image = self._deserialize(req.body, content_type) if not image: - raise exc.HTTPBadRequest() + raise webob.exc.HTTPBadRequest() try: server_id = image["image"]["serverId"] image_name = image["image"]["name"] except KeyError: - raise exc.HTTPBadRequest() + raise webob.exc.HTTPBadRequest() - image = self.__compute.snapshot(context, server_id, image_name) + image = self._compute_service.snapshot(context, server_id, image_name) return self.get_builder(req).build(image, True) + def get_builder(self, request): + """Indicates that you must use a Controller subclass.""" + raise NotImplementedError + class ControllerV10(Controller): """ diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 9d0bad095..bab1f0bac 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +import os.path + class ViewBuilder(object): """ @@ -37,7 +39,7 @@ class ViewBuilder(object): """ Return an href string pointing to this object. """ - return "%s/images/%s" % (self._url, image_id) + return os.path.join(self._url, "images", str(image_id)) def build(self, image_obj, detail=False): """ -- cgit From f5b2167c3a18097a0de0c5b26a63baad7c1904a1 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 24 Mar 2011 12:16:06 -0400 Subject: removing old Versions application and correcting fakes to use new controller --- nova/api/openstack/__init__.py | 18 ------------------ 1 file changed, 18 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 143b1d2b2..a9dbde6ce 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -148,21 +148,3 @@ class APIRouterV11(APIRouter): controller=servers.ControllerV11(), collection={'detail': 'GET'}, member=self.server_members) - - -class Versions(wsgi.Application): - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - """Respond to a request for all OpenStack API versions.""" - response = { - "versions": [ - dict(status="DEPRECATED", id="v1.0"), - dict(status="CURRENT", id="v1.1"), - ], - } - metadata = { - "application/xml": { - "attributes": dict(version=["status", "id"])}} - - content_type = req.best_match_content_type() - return wsgi.Serializer(metadata).serialize(response, content_type) -- cgit From fbb8291263ae49521bbe02aa7f75c000c7f2db8d Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 24 Mar 2011 12:46:39 -0400 Subject: adding versioned controllers --- nova/api/openstack/__init__.py | 11 ++++++++--- nova/api/openstack/flavors.py | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 143b1d2b2..f47422359 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -117,9 +117,6 @@ class APIRouter(wsgi.Router): mapper.resource("image", "images", controller=images.Controller(), collection={'detail': 'GET'}) - mapper.resource("flavor", "flavors", controller=flavors.Controller(), - collection={'detail': 'GET'}) - mapper.resource("shared_ip_group", "shared_ip_groups", collection={'detail': 'GET'}, controller=shared_ip_groups.Controller()) @@ -138,6 +135,10 @@ class APIRouterV10(APIRouter): collection={'detail': 'GET'}, member=self.server_members) + mapper.resource("flavor", "flavors", + controller=flavors.ControllerV10(), + collection={'detail': 'GET'}) + class APIRouterV11(APIRouter): """Define routes specific to OpenStack API V1.1.""" @@ -149,6 +150,10 @@ class APIRouterV11(APIRouter): collection={'detail': 'GET'}, member=self.server_members) + mapper.resource("flavor", "flavors", + controller=flavors.ControllerV11(), + collection={'detail': 'GET'}) + class Versions(wsgi.Application): @webob.dec.wsgify(RequestClass=wsgi.Request) diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index f7b5b722f..5b99b5a6f 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -20,7 +20,7 @@ import webob from nova import db from nova import exception from nova import wsgi -from nova.api.openstack.views import flavors as flavors_views +from nova.api.openstack import views class Controller(wsgi.Controller): @@ -49,7 +49,7 @@ class Controller(wsgi.Controller): """Helper function that returns a list of flavor dicts.""" ctxt = req.environ['nova.context'] flavors = db.api.instance_type_get_all(ctxt) - builder = flavors_views.get_view_builder(req) + builder = self._get_view_builder(req) items = [builder.build(flavor, is_detail=is_detail) for flavor in flavors.values()] return items @@ -62,6 +62,17 @@ class Controller(wsgi.Controller): except exception.NotFound: return webob.exc.HTTPNotFound() - builder = flavors_views.get_view_builder(req) + builder = self._get_view_builder(req) values = builder.build(flavor, is_detail=True) return dict(flavor=values) + + +class ControllerV10(Controller): + def _get_view_builder(self, req): + return views.flavors.ViewBuilder() + + +class ControllerV11(Controller): + def _get_view_builder(self, req): + base_url = req.application_url + return views.flavors.ViewBuilderV11(base_url) -- cgit From a69f6ef093805d74832a9dd531e55dd614dfa71c Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 24 Mar 2011 12:57:49 -0400 Subject: Docstring fixes. --- nova/api/openstack/image_metadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py index 5ca349af1..e09952967 100644 --- a/nova/api/openstack/image_metadata.py +++ b/nova/api/openstack/image_metadata.py @@ -27,7 +27,7 @@ FLAGS = flags.FLAGS class Controller(wsgi.Controller): - """ The image metadata API controller for the Openstack API """ + """The image metadata API controller for the Openstack API""" def __init__(self): self.image_service = utils.import_object(FLAGS.image_service) @@ -42,7 +42,7 @@ class Controller(wsgi.Controller): return metadata def index(self, req, image_id): - """ Returns the list of metadata for a given instance """ + """Returns the list of metadata for a given instance""" context = req.environ['nova.context'] metadata = self._get_metadata(context, image_id) return dict(metadata=metadata) -- cgit From 1ad0faf980ac89e904a246f1dfeddf51a21fd740 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Thu, 24 Mar 2011 13:48:04 -0400 Subject: Paginated results should not include the item starting at marker. Improved implementation of common.limited_by_marker as suggested by Matt Dietz. Added flag osapi_max_limit. --- nova/api/openstack/common.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index f85d1df9d..f598ac824 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -20,9 +20,11 @@ from urlparse import urlparse import webob from nova import exception +from nova import flags +FLAGS = flags.FLAGS -def limited(items, request, max_limit=1000): +def limited(items, request, max_limit=FLAGS.osapi_max_limit): """ Return a slice of items according to requested offset and limit. @@ -56,10 +58,13 @@ def limited(items, request, max_limit=1000): return items[offset:range_end] -def limited_by_marker(items, request, max_limit=1000): - ''' Return a slice of items according to requested marker and limit. ''' +def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit): + """Return a slice of items according to the requested marker and limit.""" - marker = request.GET.get('marker') + try: + marker = int(request.GET.get('marker', 0)) + except ValueError: + raise webob.exc.HTTPBadRequest(_('marker param must be an integer')) try: limit = int(request.GET.get('limit', max_limit)) @@ -69,17 +74,16 @@ def limited_by_marker(items, request, max_limit=1000): if limit < 0: raise webob.exc.HTTPBadRequest(_('limit param must be positive')) - limit = min(max_limit, limit or max_limit) + limit = min(max_limit, limit) start_index = 0 - if marker != None: - found_it = False + if marker: + start_index = -1 for i, item in enumerate(items): - if str(item['id']) == marker: - start_index = i - found_it = True + if item['id'] == marker: + start_index = i + 1 break - if not found_it: - raise webob.exc.HTTPBadRequest(_('marker not found')) + if start_index < 0: + raise webob.exc.HTTPBadRequest(_('marker [%s] not found' % marker)) range_end = start_index + limit return items[start_index:range_end] -- cgit From c7ccbd7a16a546cbd0717427772691ce7d8b4da6 Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 12:42:46 -0700 Subject: support volume and network in the direct api --- nova/api/ec2/cloud.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 2afcea77c..5d31d71d3 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -541,7 +541,7 @@ class CloudController(object): volumes = [] for ec2_id in volume_id: internal_id = ec2utils.ec2_id_to_id(ec2_id) - volume = self.volume_api.get(context, internal_id) + volume = self.volume_api.get(context, volume_id=internal_id) volumes.append(volume) else: volumes = self.volume_api.get_all(context) @@ -585,9 +585,11 @@ class CloudController(object): def create_volume(self, context, size, **kwargs): LOG.audit(_("Create volume of %s GB"), size, context=context) - volume = self.volume_api.create(context, size, - kwargs.get('display_name'), - kwargs.get('display_description')) + volume = self.volume_api.create( + context, + size=size, + name=kwargs.get('display_name'), + description=kwargs.get('display_description')) # TODO(vish): Instance should be None at db layer instead of # trying to lazy load, but for now we turn it into # a dict to avoid an error. @@ -606,7 +608,7 @@ class CloudController(object): if field in kwargs: changes[field] = kwargs[field] if changes: - self.volume_api.update(context, volume_id, kwargs) + self.volume_api.update(context, volume_id=volume_id, fields=changes) return True def attach_volume(self, context, volume_id, instance_id, device, **kwargs): @@ -619,7 +621,7 @@ class CloudController(object): instance_id=instance_id, volume_id=volume_id, device=device) - volume = self.volume_api.get(context, volume_id) + volume = self.volume_api.get(context, volume_id=volume_id) return {'attachTime': volume['attach_time'], 'device': volume['mountpoint'], 'instanceId': ec2utils.id_to_ec2_id(instance_id), @@ -630,7 +632,7 @@ class CloudController(object): def detach_volume(self, context, volume_id, **kwargs): volume_id = ec2utils.ec2_id_to_id(volume_id) LOG.audit(_("Detach volume %s"), volume_id, context=context) - volume = self.volume_api.get(context, volume_id) + volume = self.volume_api.get(context, volume_id=volume_id) instance = self.compute_api.detach_volume(context, volume_id=volume_id) return {'attachTime': volume['attach_time'], 'device': volume['mountpoint'], @@ -768,7 +770,7 @@ class CloudController(object): def release_address(self, context, public_ip, **kwargs): LOG.audit(_("Release address %s"), public_ip, context=context) - self.network_api.release_floating_ip(context, public_ip) + self.network_api.release_floating_ip(context, address=public_ip) return {'releaseResponse': ["Address released."]} def associate_address(self, context, instance_id, public_ip, **kwargs): @@ -782,7 +784,7 @@ class CloudController(object): def disassociate_address(self, context, public_ip, **kwargs): LOG.audit(_("Disassociate address %s"), public_ip, context=context) - self.network_api.disassociate_floating_ip(context, public_ip) + self.network_api.disassociate_floating_ip(context, address=public_ip) return {'disassociateResponse': ["Address disassociated."]} def run_instances(self, context, **kwargs): -- cgit From ef5c9e11595a00de468783adbb60cfbc2cbbf13d Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 12:42:46 -0700 Subject: add Limited, an API limiting/versioning wrapper --- nova/api/direct.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) (limited to 'nova/api') diff --git a/nova/api/direct.py b/nova/api/direct.py index dfca250e0..1011091a6 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -211,6 +211,42 @@ class ServiceWrapper(wsgi.Controller): return result +class Limited(object): + __notdoc = """Limit the available methods on a given object. + + (Not a docstring so that the docstring can be conditionally overriden.) + + Useful when defining a public API that only exposes a subset of an + internal API. + + Expected usage of this class is to define a subclass that lists the allowed + methods in the 'allowed' variable. + + Additionally where appropriate methods can be added or overwritten, for + example to provide backwards compatibility. + + """ + + _allowed = None + + def __init__(self, proxy): + self._proxy = proxy + if not self.__doc__: + self.__doc__ = proxy.__doc__ + if not self._allowed: + self._allowed = [] + + def __getattr__(self, key): + """Only return methods that are named in self._allowed.""" + if key not in self._allowed: + raise AttributeError() + return getattr(self._proxy, key) + + def __dir__(self): + """Only return methods that are named in self._allowed.""" + return [x for x in dir(self._proxy) if x in self._allowed] + + class Proxy(object): """Pretend a Direct API endpoint is an object.""" def __init__(self, app, prefix=None): -- cgit From 5c03ade2ee82350d845c8306d5aab9eda3073137 Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 12:42:47 -0700 Subject: add some more docs to direct.py --- nova/api/direct.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'nova/api') diff --git a/nova/api/direct.py b/nova/api/direct.py index 1011091a6..2e158e89e 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -225,6 +225,10 @@ class Limited(object): Additionally where appropriate methods can be added or overwritten, for example to provide backwards compatibility. + The wrapping approach has been chosen so that the wrapped API can maintain + its own internal consistency, for example if it calls "self.create" it + should get its own create method rather than anything we do here. + """ _allowed = None -- cgit From 4a6db815b01c71076bae96c155396e5adbe8af90 Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 12:42:47 -0700 Subject: better error handling and serialization --- nova/api/direct.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/direct.py b/nova/api/direct.py index 2e158e89e..bb2ace1c9 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -38,6 +38,7 @@ import routes import webob from nova import context +from nova import exception from nova import flags from nova import utils from nova import wsgi @@ -205,10 +206,12 @@ 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 type(result) is dict or type(result) is list: - return self._serialize(result, req.best_match_content_type()) - else: + 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()) + except: + raise exception.Error("returned non-serializable type: %s" % result) class Limited(object): -- cgit From c5cbec20d2785d3060d57b55a264fbf936709500 Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 24 Mar 2011 13:20:15 -0700 Subject: pep8 cleanups --- nova/api/direct.py | 3 ++- nova/api/ec2/cloud.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/direct.py b/nova/api/direct.py index bb2ace1c9..e5f33cee4 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -211,7 +211,8 @@ class ServiceWrapper(wsgi.Controller): try: return self._serialize(result, req.best_match_content_type()) except: - raise exception.Error("returned non-serializable type: %s" % result) + raise exception.Error("returned non-serializable type: %s" + % result) class Limited(object): diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 5d31d71d3..0da642318 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -608,7 +608,9 @@ class CloudController(object): if field in kwargs: changes[field] = kwargs[field] if changes: - self.volume_api.update(context, volume_id=volume_id, fields=changes) + self.volume_api.update(context, + volume_id=volume_id, + fields=changes) return True def attach_volume(self, context, volume_id, instance_id, device, **kwargs): -- cgit From 3d06c636537374557ee6ff77b7c0bc7718eafcdb Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 24 Mar 2011 18:59:11 -0400 Subject: Added detail keywork and i18n as per suggestions. --- nova/api/openstack/images.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index b01d991e8..be33bb656 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -62,7 +62,7 @@ class Controller(wsgi.Controller): context = req.environ['nova.context'] images = self._image_service.index(context) build = self.get_builder(req).build - return dict(images=[build(image, False) for image in images]) + return dict(images=[build(image, detail=False) for image in images]) def detail(self, req): """ @@ -73,7 +73,7 @@ class Controller(wsgi.Controller): context = req.environ['nova.context'] images = self._image_service.detail(context) build = self.get_builder(req).build - return dict(images=[build(image, True) for image in images]) + return dict(images=[build(image, detail=True) for image in images]) def show(self, req, id): """ @@ -88,10 +88,10 @@ class Controller(wsgi.Controller): try: image = self._image_service.show(context, image_id) except exception.NotFound: - ex = webob.exc.HTTPNotFound(explanation="Image not found.") + ex = webob.exc.HTTPNotFound(explanation=_("Image not found.")) raise faults.Fault(ex) - return dict(image=self.get_builder(req).build(image, True)) + return dict(image=self.get_builder(req).build(image, detail=True)) def delete(self, req, id): """ @@ -125,7 +125,7 @@ class Controller(wsgi.Controller): raise webob.exc.HTTPBadRequest() image = self._compute_service.snapshot(context, server_id, image_name) - return self.get_builder(req).build(image, True) + return self.get_builder(req).build(image, detail=True) def get_builder(self, request): """Indicates that you must use a Controller subclass.""" -- cgit From f2f08a5b0309876bb312c9124e75bd89331c4816 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 24 Mar 2011 17:04:55 -0700 Subject: make everything work with trunk again --- nova/api/ec2/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 6b08f98c2..eb0428c2c 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -538,7 +538,7 @@ class CloudController(object): def get_vnc_console(self, context, instance_id, **kwargs): ec2_id = instance_id - instance_id = ec2_id_to_id(ec2_id) + instance_id = ec2utils.ec2_id_to_id(ec2_id) return self.compute_api.get_vnc_console(context, instance_id=instance_id) -- cgit From 06c0eff8ec7eef33933da9bd8adbf7b70a977889 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 24 Mar 2011 17:44:27 -0700 Subject: add hook for osapi --- nova/api/ec2/cloud.py | 1 + nova/api/openstack/servers.py | 10 ++++++++++ 2 files changed, 11 insertions(+) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index eb0428c2c..fa4624ff1 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -537,6 +537,7 @@ class CloudController(object): instance_id=instance_id) def get_vnc_console(self, context, instance_id, **kwargs): + """Returns vnc browser url to the dashboard.""" ec2_id = instance_id instance_id = ec2utils.ec2_id_to_id(ec2_id) return self.compute_api.get_vnc_console(context, diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 0dad46268..88cc790c1 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -481,6 +481,16 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() + @scheduler_api.redirect_handler + def get_vnc_console(self, req, id): + """ Returns a url to an instance's ajaxterm console. """ + try: + self.compute_api.get_vnc_console(req.environ['nova.context'], + int(id)) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + @scheduler_api.redirect_handler def diagnostics(self, req, id): """Permit Admins to retrieve server diagnostics.""" -- cgit From e722803067e6386e98f29aa867d4cf98ce6e0cc2 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 24 Mar 2011 18:38:28 -0700 Subject: clarify comment --- nova/api/ec2/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index fa4624ff1..e5a957b83 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -537,7 +537,7 @@ class CloudController(object): instance_id=instance_id) def get_vnc_console(self, context, instance_id, **kwargs): - """Returns vnc browser url to the dashboard.""" + """Returns vnc browser url. Used by OS dashboard.""" ec2_id = instance_id instance_id = ec2utils.ec2_id_to_id(ec2_id) return self.compute_api.get_vnc_console(context, -- cgit From ccf4727ca16d7a67c6a35950ab378ab4615dbdad Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Fri, 25 Mar 2011 04:41:47 +0000 Subject: disk_format is now an ImageService property --- nova/api/openstack/servers.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 144d14536..ac9e29f07 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -502,33 +502,41 @@ class Controller(wsgi.Controller): return dict(actions=actions) def _get_kernel_ramdisk_from_image(self, req, image_id): - """Retrevies kernel and ramdisk IDs from Glance - - Only 'machine' (ami) type use kernel and ramdisk outside of the - image. + """Fetch an image from the ImageService, then if present, return the + associated kernel and ramdisk image IDs. """ - # FIXME(sirp): Since we're retrieving the kernel_id from an - # image_property, this means only Glance is supported. - # The BaseImageService needs to expose a consistent way of accessing - # kernel_id and ramdisk_id - image = self._image_service.show(req.environ['nova.context'], image_id) + context = req.environ['nova.context'] + image_meta = self._image_service.show(context, image_id) + # NOTE(sirp): extracted to a separate method to aid unit-testing, the + # new method doesn't need a request obj or an ImageService stub + kernel_id, ramdisk_id = self._do_get_kernel_ramdisk_from_image( + image_meta) + return kernel_id, ramdisk_id - if image['status'] != 'active': + @staticmethod + def _do_get_kernel_ramdisk_from_image(image_meta): + """Given an ImageService image_meta, return kernel and ramdisk image + ids if present. + + This is only valid for `ami` style images. + """ + image_id = image_meta['id'] + if image_meta['status'] != 'active': raise exception.Invalid( _("Cannot build from image %(image_id)s, status not active") % locals()) - if image['disk_format'] != 'ami': + if image_meta['properties']['disk_format'] != 'ami': return None, None try: - kernel_id = image['properties']['kernel_id'] + kernel_id = image_meta['properties']['kernel_id'] except KeyError: raise exception.NotFound( _("Kernel not found for image %(image_id)s") % locals()) try: - ramdisk_id = image['properties']['ramdisk_id'] + ramdisk_id = image_meta['properties']['ramdisk_id'] except KeyError: raise exception.NotFound( _("Ramdisk not found for image %(image_id)s") % locals()) -- cgit From 51e8841b7cd818e5a3e0fa6bf023561b0160717d Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 25 Mar 2011 10:07:42 -0400 Subject: Use metadata = image.get('properties', {}). --- nova/api/openstack/image_metadata.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py index e09952967..c9d6ac532 100644 --- a/nova/api/openstack/image_metadata.py +++ b/nova/api/openstack/image_metadata.py @@ -36,9 +36,7 @@ class Controller(wsgi.Controller): def _get_metadata(self, context, image_id, image=None): if not image: image = self.image_service.show(context, image_id) - metadata = {} - if 'properties' in image: - metadata = image['properties'] + metadata = image.get('properties', {}) return metadata def index(self, req, image_id): -- cgit From cd1bac4deff367131d43f87cdfbc3b6b34bbdc1e Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Fri, 25 Mar 2011 16:39:43 -0700 Subject: Initial extensification of volumes --- nova/api/openstack/__init__.py | 10 -- nova/api/openstack/extensions.py | 151 ++++++++++++++--- nova/api/openstack/incubator/__init__.py | 20 +++ nova/api/openstack/incubator/volumes/__init__.py | 18 ++ .../incubator/volumes/volume_attachments.py | 181 +++++++++++++++++++++ nova/api/openstack/incubator/volumes/volumes.py | 160 ++++++++++++++++++ .../api/openstack/incubator/volumes/volumes_ext.py | 55 +++++++ nova/api/openstack/volume_attachments.py | 181 --------------------- nova/api/openstack/volumes.py | 160 ------------------ 9 files changed, 565 insertions(+), 371 deletions(-) create mode 100644 nova/api/openstack/incubator/__init__.py create mode 100644 nova/api/openstack/incubator/volumes/__init__.py create mode 100644 nova/api/openstack/incubator/volumes/volume_attachments.py create mode 100644 nova/api/openstack/incubator/volumes/volumes.py create mode 100644 nova/api/openstack/incubator/volumes/volumes_ext.py delete mode 100644 nova/api/openstack/volume_attachments.py delete mode 100644 nova/api/openstack/volumes.py (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 0e5b2a071..731e16a58 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -128,16 +128,6 @@ class APIRouter(wsgi.Router): _limits = limits.LimitsController() mapper.resource("limit", "limits", controller=_limits) - #NOTE(justinsb): volumes is not yet part of the official API - mapper.resource("volume", "volumes", - controller=volumes.Controller(), - collection={'detail': 'GET'}) - - mapper.resource("volume_attachment", "volume_attachments", - controller=volume_attachments.Controller(), - parent_resource=dict(member_name='server', - collection_name='servers')) - super(APIRouter, self).__init__(mapper) diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 9d98d849a..6a8ce9669 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2011 OpenStack LLC. +# Copyright 2011 Justin Santa Barbara # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,12 +17,14 @@ # under the License. import imp +import inspect import os import sys import routes import webob.dec import webob.exc +from nova import exception from nova import flags from nova import log as logging from nova import wsgi @@ -34,6 +37,63 @@ LOG = logging.getLogger('extensions') FLAGS = flags.FLAGS +class ExtensionDescriptor(object): + """This is the base class that defines the contract for extensions""" + + def get_name(self): + """The name of the extension + + e.g. 'Fox In Socks' """ + raise NotImplementedError() + + def get_alias(self): + """The alias for the extension + + e.g. 'FOXNSOX'""" + raise NotImplementedError() + + def get_description(self): + """Friendly description for the extension + + e.g. 'The Fox In Socks Extension'""" + raise NotImplementedError() + + def get_namespace(self): + """The XML namespace for the extension + + e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'""" + raise NotImplementedError() + + def get_updated(self): + """The timestamp when the extension was last updated + + e.g. '2011-01-22T13:25:27-06:00'""" + #NOTE(justinsb): Huh? Isn't this defined by the namespace? + raise NotImplementedError() + + def get_resources(self): + """List of extensions.ResourceExtension extension objects + + Resources define new nouns, and are accessible through URLs""" + resources = [] + return resources + + def get_actions(self): + """List of extensions.ActionExtension extension objects + + Actions are verbs callable from the API""" + actions = [] + return actions + + def get_response_extensions(self): + """List of extensions.ResponseExtension extension objects + + Response extensions are used to insert information into existing + response data""" + response_exts = [] + return response_exts + + class ActionExtensionController(wsgi.Controller): def __init__(self, application): @@ -109,13 +169,10 @@ class ExtensionController(wsgi.Controller): return self._translate(ext) def delete(self, req, id): - raise faults.Fault(exc.HTTPNotFound()) + raise faults.Fault(webob.exc.HTTPNotFound()) def create(self, req): - raise faults.Fault(exc.HTTPNotFound()) - - def delete(self, req, id): - raise faults.Fault(exc.HTTPNotFound()) + raise faults.Fault(webob.exc.HTTPNotFound()) class ExtensionMiddleware(wsgi.Middleware): @@ -235,16 +292,19 @@ class ExtensionMiddleware(wsgi.Middleware): class ExtensionManager(object): """ Load extensions from the configured extension path. - See nova/tests/api/openstack/extensions/foxinsocks.py for an example - extension implementation. + + See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an + example extension implementation. """ def __init__(self, path): LOG.audit(_('Initializing extension manager.')) + self.super_verbose = False + self.path = path self.extensions = {} - self._load_extensions() + self._load_all_extensions() def get_resources(self): """ @@ -300,7 +360,7 @@ class ExtensionManager(object): except AttributeError as ex: LOG.exception(_("Exception loading extension: %s"), unicode(ex)) - def _load_extensions(self): + def _load_all_extensions(self): """ Load extensions from the configured path. The extension name is constructed from the module_name. If your extension module was named @@ -310,23 +370,74 @@ class ExtensionManager(object): See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. """ - if not os.path.exists(self.path): + self._load_extensions_under_path(self.path) + + incubator_path = os.path.join(os.path.dirname(__file__), "incubator") + self._load_extensions_under_path(incubator_path) + + def _load_extensions_under_path(self, path): + if not os.path.isdir(path): + LOG.warning(_('Extensions directory not found: %s') % path) return - for f in os.listdir(self.path): - LOG.audit(_('Loading extension file: %s'), f) + LOG.debug(_('Looking for extensions in: %s') % path) + + for child in os.listdir(path): + child_path = os.path.join(path, child) + if not os.path.isdir(child_path): + continue + self._load_extension(child_path) + + def _load_extension(self, path): + if not os.path.isdir(path): + return + + for f in os.listdir(path): mod_name, file_ext = os.path.splitext(os.path.split(f)[-1]) - ext_path = os.path.join(self.path, f) - if file_ext.lower() == '.py': - mod = imp.load_source(mod_name, ext_path) - ext_name = mod_name[0].upper() + mod_name[1:] + if file_ext.startswith('_'): + continue + if file_ext.lower() != '.py': + continue + + ext_path = os.path.join(path, f) + if self.super_verbose: + LOG.debug(_('Checking extension file: %s'), ext_path) + + mod = imp.load_source(mod_name, ext_path) + for _name, cls in inspect.getmembers(mod): try: - new_ext = getattr(mod, ext_name)() - self._check_extension(new_ext) - self.extensions[new_ext.get_alias()] = new_ext + if not inspect.isclass(cls): + continue + + #NOTE(justinsb): It seems that python modules aren't great + # If you have two identically named modules, the classes + # from both are mixed in. So name your extension based + # on the alias, not 'extension.py'! + #TODO(justinsb): Any way to work around this? + + if self.super_verbose: + LOG.debug(_('Checking class: %s'), cls) + + if not ExtensionDescriptor in cls.__bases__: + if self.super_verbose: + LOG.debug(_('Not a ExtensionDescriptor: %s'), cls) + continue + + obj = cls() + self._add_extension(obj) except AttributeError as ex: LOG.exception(_("Exception loading extension: %s"), - unicode(ex)) + unicode(ex)) + + def _add_extension(self, ext): + alias = ext.get_alias() + LOG.audit(_('Loaded extension: %s'), alias) + + self._check_extension(ext) + + if alias in self.extensions: + raise exception.Error("Found duplicate extension: %s" % alias) + self.extensions[alias] = ext class ResponseExtension(object): diff --git a/nova/api/openstack/incubator/__init__.py b/nova/api/openstack/incubator/__init__.py new file mode 100644 index 000000000..cded38174 --- /dev/null +++ b/nova/api/openstack/incubator/__init__.py @@ -0,0 +1,20 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License.import datetime + +"""Incubator contains extensions that are shipped with nova. + +It can't be called 'extensions' because that causes namespacing problems.""" diff --git a/nova/api/openstack/incubator/volumes/__init__.py b/nova/api/openstack/incubator/volumes/__init__.py new file mode 100644 index 000000000..2a9c93210 --- /dev/null +++ b/nova/api/openstack/incubator/volumes/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License.import datetime + +"""The volumes extension adds volumes and attachments to the API.""" diff --git a/nova/api/openstack/incubator/volumes/volume_attachments.py b/nova/api/openstack/incubator/volumes/volume_attachments.py new file mode 100644 index 000000000..58a9a727b --- /dev/null +++ b/nova/api/openstack/incubator/volumes/volume_attachments.py @@ -0,0 +1,181 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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 webob import exc + +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + +FLAGS = flags.FLAGS + + +def _translate_detail_view(context, volume): + """ Maps keys for details view""" + + d = _translate_summary_view(context, volume) + + # No additional data / lookups at the moment + + return d + + +def _translate_summary_view(context, vol): + """ Maps keys for summary view""" + d = {} + + volume_id = vol['id'] + + # NOTE(justinsb): We use the volume id as the id of the attachment object + d['id'] = volume_id + + d['volumeId'] = volume_id + if vol.get('instance_id'): + d['serverId'] = vol['instance_id'] + if vol.get('mountpoint'): + d['device'] = vol['mountpoint'] + + return d + + +class Controller(wsgi.Controller): + """ The volume attachment API controller for the Openstack API + + A child resource of the server. Note that we use the volume id + as the ID of the attachment (though this is not guaranteed externally)""" + + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'volumeAttachment': ['id', + 'serverId', + 'volumeId', + 'device']}}} + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(Controller, self).__init__() + + def index(self, req, server_id): + """ Returns the list of volume attachments for a given instance """ + return self._items(req, server_id, + entity_maker=_translate_summary_view) + + def show(self, req, server_id, id): + """Return data about the given volume""" + context = req.environ['nova.context'] + + volume_id = id + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + LOG.debug("volume_id not found") + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + return {'volumeAttachment': _translate_detail_view(context, vol)} + + def create(self, req, server_id): + """ Attach a volume to an instance """ + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + instance_id = server_id + volume_id = env['volumeAttachment']['volumeId'] + device = env['volumeAttachment']['device'] + + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" + " at %(device)s") % locals() + LOG.audit(msg, context=context) + + try: + self.compute_api.attach_volume(context, + instance_id=instance_id, + volume_id=volume_id, + device=device) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + # The attach is async + attachment = {} + attachment['id'] = volume_id + attachment['volumeId'] = volume_id + + # NOTE(justinsb): And now, we have a problem... + # The attach is async, so there's a window in which we don't see + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. + # For now, we'll probably have to rely on libraries being smart + + # TODO: How do I return "accepted" here?? + return {'volumeAttachment': attachment} + + def update(self, _req, _server_id, _id): + """ Update a volume attachment. We don't currently support this.""" + return faults.Fault(exc.HTTPBadRequest()) + + def delete(self, req, server_id, id): + """ Detach a volume from an instance """ + context = req.environ['nova.context'] + + volume_id = id + LOG.audit(_("Detach volume %s"), volume_id, context=context) + + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + self.compute_api.detach_volume(context, + volume_id=volume_id) + + return exc.HTTPAccepted() + + def _items(self, req, server_id, entity_maker): + """Returns a list of attachments, transformed through entity_maker""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.get(context, server_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + volumes = instance['volumes'] + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumeAttachments': res} diff --git a/nova/api/openstack/incubator/volumes/volumes.py b/nova/api/openstack/incubator/volumes/volumes.py new file mode 100644 index 000000000..ec3b9a6c8 --- /dev/null +++ b/nova/api/openstack/incubator/volumes/volumes.py @@ -0,0 +1,160 @@ +# Copyright 2011 Justin Santa Barbara +# 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 webob import exc + +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + +FLAGS = flags.FLAGS + + +def _translate_detail_view(context, vol): + """ Maps keys for details view""" + + d = _translate_summary_view(context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_summary_view(_context, vol): + """ Maps keys for summary view""" + d = {} + + instance_id = None + # instance_data = None + attached_to = vol.get('instance') + if attached_to: + instance_id = attached_to['id'] + # instance_data = '%s[%s]' % (instance_ec2_id, + # attached_to['host']) + d['id'] = vol['id'] + d['status'] = vol['status'] + d['size'] = vol['size'] + d['availabilityZone'] = vol['availability_zone'] + d['createdAt'] = vol['created_at'] + # if context.is_admin: + # v['status'] = '%s (%s, %s, %s, %s)' % ( + # vol['status'], + # vol['user_id'], + # vol['host'], + # instance_data, + # vol['mountpoint']) + if vol['attach_status'] == 'attached': + d['attachments'] = [{'attachTime': vol['attach_time'], + 'deleteOnTermination': False, + 'mountpoint': vol['mountpoint'], + 'instanceId': instance_id, + 'status': 'attached', + 'volumeId': vol['id']}] + else: + d['attachments'] = [{}] + + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +class Controller(wsgi.Controller): + """ The Volumes API controller for the OpenStack API """ + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "volume": [ + "id", + "status", + "size", + "availabilityZone", + "createdAt", + "displayName", + "displayDescription", + ]}}} + + def __init__(self): + self.volume_api = volume.API() + super(Controller, self).__init__() + + def show(self, req, id): + """Return data about the given volume""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get(context, id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + return {'volume': _translate_detail_view(context, vol)} + + def delete(self, req, id): + """ Delete a volume """ + context = req.environ['nova.context'] + + LOG.audit(_("Delete volume with id: %s"), id, context=context) + + try: + self.volume_api.delete(context, volume_id=id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + + def index(self, req): + """ Returns a summary list of volumes""" + return self._items(req, entity_maker=_translate_summary_view) + + def detail(self, req): + """ Returns a detailed list of volumes """ + return self._items(req, entity_maker=_translate_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of volumes, transformed through entity_maker""" + context = req.environ['nova.context'] + + volumes = self.volume_api.get_all(context) + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumes': res} + + def create(self, req): + """Creates a new volume""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + vol = env['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + new_volume = self.volume_api.create(context, size, + vol.get('display_name'), + vol.get('display_description')) + + # Work around problem that instance is lazy-loaded... + new_volume['instance'] = None + + retval = _translate_detail_view(context, new_volume) + + return {'volume': retval} diff --git a/nova/api/openstack/incubator/volumes/volumes_ext.py b/nova/api/openstack/incubator/volumes/volumes_ext.py new file mode 100644 index 000000000..87a57320d --- /dev/null +++ b/nova/api/openstack/incubator/volumes/volumes_ext.py @@ -0,0 +1,55 @@ +# Copyright 2011 Justin Santa Barbara +# 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.api.openstack import extensions +from nova.api.openstack.incubator.volumes import volumes +from nova.api.openstack.incubator.volumes import volume_attachments + + +class VolumesExtension(extensions.ExtensionDescriptor): + def get_name(self): + return "Volumes" + + def get_alias(self): + return "VOLUMES" + + def get_description(self): + return "Volumes support" + + def get_namespace(self): + return "http://docs.openstack.org/ext/volumes/api/v1.1" + + def get_updated(self): + return "2011-03-25T00:00:00+00:00" + + def get_resources(self): + resources = [] + + #NOTE(justinsb): No way to provide singular name ('volume') + # Does this matter? + res = extensions.ResourceExtension('volumes', + volumes.Controller(), + collection_actions={'detail': 'GET'} + ) + resources.append(res) + + res = extensions.ResourceExtension('volume_attachments', + volume_attachments.Controller(), + parent=dict( + member_name='server', + collection_name='servers')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/volume_attachments.py b/nova/api/openstack/volume_attachments.py deleted file mode 100644 index 58a9a727b..000000000 --- a/nova/api/openstack/volume_attachments.py +++ /dev/null @@ -1,181 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# 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 webob import exc - -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova import wsgi -from nova.api.openstack import common -from nova.api.openstack import faults - - -LOG = logging.getLogger("nova.api.volumes") - -FLAGS = flags.FLAGS - - -def _translate_detail_view(context, volume): - """ Maps keys for details view""" - - d = _translate_summary_view(context, volume) - - # No additional data / lookups at the moment - - return d - - -def _translate_summary_view(context, vol): - """ Maps keys for summary view""" - d = {} - - volume_id = vol['id'] - - # NOTE(justinsb): We use the volume id as the id of the attachment object - d['id'] = volume_id - - d['volumeId'] = volume_id - if vol.get('instance_id'): - d['serverId'] = vol['instance_id'] - if vol.get('mountpoint'): - d['device'] = vol['mountpoint'] - - return d - - -class Controller(wsgi.Controller): - """ The volume attachment API controller for the Openstack API - - A child resource of the server. Note that we use the volume id - as the ID of the attachment (though this is not guaranteed externally)""" - - _serialization_metadata = { - 'application/xml': { - 'attributes': { - 'volumeAttachment': ['id', - 'serverId', - 'volumeId', - 'device']}}} - - def __init__(self): - self.compute_api = compute.API() - self.volume_api = volume.API() - super(Controller, self).__init__() - - def index(self, req, server_id): - """ Returns the list of volume attachments for a given instance """ - return self._items(req, server_id, - entity_maker=_translate_summary_view) - - def show(self, req, server_id, id): - """Return data about the given volume""" - context = req.environ['nova.context'] - - volume_id = id - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - LOG.debug("volume_id not found") - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - return {'volumeAttachment': _translate_detail_view(context, vol)} - - def create(self, req, server_id): - """ Attach a volume to an instance """ - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - instance_id = server_id - volume_id = env['volumeAttachment']['volumeId'] - device = env['volumeAttachment']['device'] - - msg = _("Attach volume %(volume_id)s to instance %(server_id)s" - " at %(device)s") % locals() - LOG.audit(msg, context=context) - - try: - self.compute_api.attach_volume(context, - instance_id=instance_id, - volume_id=volume_id, - device=device) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - # The attach is async - attachment = {} - attachment['id'] = volume_id - attachment['volumeId'] = volume_id - - # NOTE(justinsb): And now, we have a problem... - # The attach is async, so there's a window in which we don't see - # the attachment (until the attachment completes). We could also - # get problems with concurrent requests. I think we need an - # attachment state, and to write to the DB here, but that's a bigger - # change. - # For now, we'll probably have to rely on libraries being smart - - # TODO: How do I return "accepted" here?? - return {'volumeAttachment': attachment} - - def update(self, _req, _server_id, _id): - """ Update a volume attachment. We don't currently support this.""" - return faults.Fault(exc.HTTPBadRequest()) - - def delete(self, req, server_id, id): - """ Detach a volume from an instance """ - context = req.environ['nova.context'] - - volume_id = id - LOG.audit(_("Detach volume %s"), volume_id, context=context) - - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - self.compute_api.detach_volume(context, - volume_id=volume_id) - - return exc.HTTPAccepted() - - def _items(self, req, server_id, entity_maker): - """Returns a list of attachments, transformed through entity_maker""" - context = req.environ['nova.context'] - - try: - instance = self.compute_api.get(context, server_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - volumes = instance['volumes'] - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumeAttachments': res} diff --git a/nova/api/openstack/volumes.py b/nova/api/openstack/volumes.py deleted file mode 100644 index ec3b9a6c8..000000000 --- a/nova/api/openstack/volumes.py +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright 2011 Justin Santa Barbara -# 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 webob import exc - -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova import wsgi -from nova.api.openstack import common -from nova.api.openstack import faults - - -LOG = logging.getLogger("nova.api.volumes") - -FLAGS = flags.FLAGS - - -def _translate_detail_view(context, vol): - """ Maps keys for details view""" - - d = _translate_summary_view(context, vol) - - # No additional data / lookups at the moment - - return d - - -def _translate_summary_view(_context, vol): - """ Maps keys for summary view""" - d = {} - - instance_id = None - # instance_data = None - attached_to = vol.get('instance') - if attached_to: - instance_id = attached_to['id'] - # instance_data = '%s[%s]' % (instance_ec2_id, - # attached_to['host']) - d['id'] = vol['id'] - d['status'] = vol['status'] - d['size'] = vol['size'] - d['availabilityZone'] = vol['availability_zone'] - d['createdAt'] = vol['created_at'] - # if context.is_admin: - # v['status'] = '%s (%s, %s, %s, %s)' % ( - # vol['status'], - # vol['user_id'], - # vol['host'], - # instance_data, - # vol['mountpoint']) - if vol['attach_status'] == 'attached': - d['attachments'] = [{'attachTime': vol['attach_time'], - 'deleteOnTermination': False, - 'mountpoint': vol['mountpoint'], - 'instanceId': instance_id, - 'status': 'attached', - 'volumeId': vol['id']}] - else: - d['attachments'] = [{}] - - d['displayName'] = vol['display_name'] - d['displayDescription'] = vol['display_description'] - return d - - -class Controller(wsgi.Controller): - """ The Volumes API controller for the OpenStack API """ - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "volume": [ - "id", - "status", - "size", - "availabilityZone", - "createdAt", - "displayName", - "displayDescription", - ]}}} - - def __init__(self): - self.volume_api = volume.API() - super(Controller, self).__init__() - - def show(self, req, id): - """Return data about the given volume""" - context = req.environ['nova.context'] - - try: - vol = self.volume_api.get(context, id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - return {'volume': _translate_detail_view(context, vol)} - - def delete(self, req, id): - """ Delete a volume """ - context = req.environ['nova.context'] - - LOG.audit(_("Delete volume with id: %s"), id, context=context) - - try: - self.volume_api.delete(context, volume_id=id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() - - def index(self, req): - """ Returns a summary list of volumes""" - return self._items(req, entity_maker=_translate_summary_view) - - def detail(self, req): - """ Returns a detailed list of volumes """ - return self._items(req, entity_maker=_translate_detail_view) - - def _items(self, req, entity_maker): - """Returns a list of volumes, transformed through entity_maker""" - context = req.environ['nova.context'] - - volumes = self.volume_api.get_all(context) - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumes': res} - - def create(self, req): - """Creates a new volume""" - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - vol = env['volume'] - size = vol['size'] - LOG.audit(_("Create volume of %s GB"), size, context=context) - new_volume = self.volume_api.create(context, size, - vol.get('display_name'), - vol.get('display_description')) - - # Work around problem that instance is lazy-loaded... - new_volume['instance'] = None - - retval = _translate_detail_view(context, new_volume) - - return {'volume': retval} -- cgit From 5936449d99b852897fddbbb140465db0ad9a330c Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Fri, 25 Mar 2011 17:48:59 -0700 Subject: Now that it's an extension, it has to be v1.1. Also fixed up all the things that changed in v1.1 --- nova/api/openstack/__init__.py | 2 -- nova/api/openstack/common.py | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 731e16a58..7f2bb1155 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -39,8 +39,6 @@ from nova.api.openstack import servers from nova.api.openstack import server_metadata from nova.api.openstack import shared_ip_groups from nova.api.openstack import users -from nova.api.openstack import volumes -from nova.api.openstack import volume_attachments from nova.api.openstack import zones diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 8cad1273a..4ab6b7a81 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -21,6 +21,10 @@ import webob from nova import exception from nova import flags +from nova import log as logging + + +LOG = logging.getLogger('common') FLAGS = flags.FLAGS @@ -121,4 +125,5 @@ def get_id_from_href(href): try: return int(urlparse(href).path.split('/')[-1]) except: + LOG.debug(_("Error extracting id from href: %s") % href) raise webob.exc.HTTPBadRequest(_('could not parse id from href')) -- cgit From b3f8e9fb546c621946563af0908e43cb01c50431 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Sun, 27 Mar 2011 18:48:32 -0700 Subject: Bunch of style fixes --- nova/api/openstack/common.py | 1 + nova/api/openstack/extensions.py | 2 +- .../incubator/volumes/volume_attachments.py | 21 +++++++++++---------- nova/api/openstack/incubator/volumes/volumes.py | 13 +++++++------ 4 files changed, 20 insertions(+), 17 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 4ab6b7a81..75aeb0a5f 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -26,6 +26,7 @@ from nova import log as logging LOG = logging.getLogger('common') + FLAGS = flags.FLAGS diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 6a8ce9669..e81ffb3d3 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -68,7 +68,7 @@ class ExtensionDescriptor(object): """The timestamp when the extension was last updated e.g. '2011-01-22T13:25:27-06:00'""" - #NOTE(justinsb): Huh? Isn't this defined by the namespace? + #NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS raise NotImplementedError() def get_resources(self): diff --git a/nova/api/openstack/incubator/volumes/volume_attachments.py b/nova/api/openstack/incubator/volumes/volume_attachments.py index 58a9a727b..93786d1c7 100644 --- a/nova/api/openstack/incubator/volumes/volume_attachments.py +++ b/nova/api/openstack/incubator/volumes/volume_attachments.py @@ -29,11 +29,12 @@ from nova.api.openstack import faults LOG = logging.getLogger("nova.api.volumes") + FLAGS = flags.FLAGS def _translate_detail_view(context, volume): - """ Maps keys for details view""" + """Maps keys for details view""" d = _translate_summary_view(context, volume) @@ -43,12 +44,12 @@ def _translate_detail_view(context, volume): def _translate_summary_view(context, vol): - """ Maps keys for summary view""" + """Maps keys for summary view""" d = {} volume_id = vol['id'] - # NOTE(justinsb): We use the volume id as the id of the attachment object + #NOTE(justinsb): We use the volume id as the id of the attachment object d['id'] = volume_id d['volumeId'] = volume_id @@ -61,7 +62,7 @@ def _translate_summary_view(context, vol): class Controller(wsgi.Controller): - """ The volume attachment API controller for the Openstack API + """The volume attachment API controller for the Openstack API A child resource of the server. Note that we use the volume id as the ID of the attachment (though this is not guaranteed externally)""" @@ -80,7 +81,7 @@ class Controller(wsgi.Controller): super(Controller, self).__init__() def index(self, req, server_id): - """ Returns the list of volume attachments for a given instance """ + """Returns the list of volume attachments for a given instance """ return self._items(req, server_id, entity_maker=_translate_summary_view) @@ -102,7 +103,7 @@ class Controller(wsgi.Controller): return {'volumeAttachment': _translate_detail_view(context, vol)} def create(self, req, server_id): - """ Attach a volume to an instance """ + """Attach a volume to an instance """ context = req.environ['nova.context'] env = self._deserialize(req.body, req.get_content_type()) @@ -130,7 +131,7 @@ class Controller(wsgi.Controller): attachment['id'] = volume_id attachment['volumeId'] = volume_id - # NOTE(justinsb): And now, we have a problem... + #NOTE(justinsb): And now, we have a problem... # The attach is async, so there's a window in which we don't see # the attachment (until the attachment completes). We could also # get problems with concurrent requests. I think we need an @@ -138,15 +139,15 @@ class Controller(wsgi.Controller): # change. # For now, we'll probably have to rely on libraries being smart - # TODO: How do I return "accepted" here?? + #TODO(justinsb): How do I return "accepted" here? return {'volumeAttachment': attachment} def update(self, _req, _server_id, _id): - """ Update a volume attachment. We don't currently support this.""" + """Update a volume attachment. We don't currently support this.""" return faults.Fault(exc.HTTPBadRequest()) def delete(self, req, server_id, id): - """ Detach a volume from an instance """ + """Detach a volume from an instance """ context = req.environ['nova.context'] volume_id = id diff --git a/nova/api/openstack/incubator/volumes/volumes.py b/nova/api/openstack/incubator/volumes/volumes.py index ec3b9a6c8..e122bb465 100644 --- a/nova/api/openstack/incubator/volumes/volumes.py +++ b/nova/api/openstack/incubator/volumes/volumes.py @@ -26,11 +26,12 @@ from nova.api.openstack import faults LOG = logging.getLogger("nova.api.volumes") + FLAGS = flags.FLAGS def _translate_detail_view(context, vol): - """ Maps keys for details view""" + """Maps keys for details view""" d = _translate_summary_view(context, vol) @@ -40,7 +41,7 @@ def _translate_detail_view(context, vol): def _translate_summary_view(_context, vol): - """ Maps keys for summary view""" + """Maps keys for summary view""" d = {} instance_id = None @@ -78,7 +79,7 @@ def _translate_summary_view(_context, vol): class Controller(wsgi.Controller): - """ The Volumes API controller for the OpenStack API """ + """The Volumes API controller for the OpenStack API""" _serialization_metadata = { 'application/xml': { @@ -109,7 +110,7 @@ class Controller(wsgi.Controller): return {'volume': _translate_detail_view(context, vol)} def delete(self, req, id): - """ Delete a volume """ + """Delete a volume """ context = req.environ['nova.context'] LOG.audit(_("Delete volume with id: %s"), id, context=context) @@ -121,11 +122,11 @@ class Controller(wsgi.Controller): return exc.HTTPAccepted() def index(self, req): - """ Returns a summary list of volumes""" + """Returns a summary list of volumes""" return self._items(req, entity_maker=_translate_summary_view) def detail(self, req): - """ Returns a detailed list of volumes """ + """Returns a detailed list of volumes """ return self._items(req, entity_maker=_translate_detail_view) def _items(self, req, entity_maker): -- cgit From 7cdc3add34b109e3f956f785b60a5aa5cf273e53 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 28 Mar 2011 12:24:41 +0200 Subject: Do not load extensions that start with a "_" --- nova/api/openstack/extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 9d98d849a..439612faa 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -317,7 +317,7 @@ class ExtensionManager(object): LOG.audit(_('Loading extension file: %s'), f) mod_name, file_ext = os.path.splitext(os.path.split(f)[-1]) ext_path = os.path.join(self.path, f) - if file_ext.lower() == '.py': + if file_ext.lower() == '.py' and not mod_name.startswith('_'): mod = imp.load_source(mod_name, ext_path) ext_name = mod_name[0].upper() + mod_name[1:] try: -- cgit From d25968ab494f65ed90981e440169e31a7488befe Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 28 Mar 2011 15:21:53 +0200 Subject: Add friendlier message if an extension fails to include a correctly named class or factory. --- nova/api/openstack/extensions.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 439612faa..4a7236863 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -321,7 +321,14 @@ class ExtensionManager(object): mod = imp.load_source(mod_name, ext_path) ext_name = mod_name[0].upper() + mod_name[1:] try: - new_ext = getattr(mod, ext_name)() + new_ext_class = getattr(mod, ext_name, None) + if not new_ext_class: + LOG.warning(_('Did not find expected name ' + '"%(ext_name)" in %(file)s'), + { 'ext_name': ext_name, + 'file': ext_path }) + continue + new_ext = new_ext_class() self._check_extension(new_ext) self.extensions[new_ext.get_alias()] = new_ext except AttributeError as ex: -- cgit From 9786a19ec0bc5176cc01b56d473a977b85800977 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 28 Mar 2011 15:34:20 +0200 Subject: Spell "warn" correctly. --- nova/api/openstack/extensions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 4a7236863..259d24a7d 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -323,10 +323,10 @@ class ExtensionManager(object): try: new_ext_class = getattr(mod, ext_name, None) if not new_ext_class: - LOG.warning(_('Did not find expected name ' - '"%(ext_name)" in %(file)s'), - { 'ext_name': ext_name, - 'file': ext_path }) + LOG.warn(_('Did not find expected name ' + '"%(ext_name)" in %(file)s'), + { 'ext_name': ext_name, + 'file': ext_path }) continue new_ext = new_ext_class() self._check_extension(new_ext) -- cgit From 14337b0a31c8f04d8044e234eb295b41a9a9c5ce Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 28 Mar 2011 14:02:53 -0400 Subject: adding shared_ip_groups testing; replacing all shared_ip_groups contoller code with HTTPNotImplemented; moving shared_ip_groups controller to APIRouterV10 --- nova/api/openstack/__init__.py | 8 ++++---- nova/api/openstack/shared_ip_groups.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 8fabbce8e..cf53ffcd6 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -119,10 +119,6 @@ class APIRouter(wsgi.Router): mapper.resource("image", "images", controller=images.Controller(), collection={'detail': 'GET'}) - mapper.resource("shared_ip_group", "shared_ip_groups", - collection={'detail': 'GET'}, - controller=shared_ip_groups.Controller()) - _limits = limits.LimitsController() mapper.resource("limit", "limits", controller=_limits) @@ -141,6 +137,10 @@ class APIRouterV10(APIRouter): controller=flavors.ControllerV10(), collection={'detail': 'GET'}) + mapper.resource("shared_ip_group", "shared_ip_groups", + collection={'detail': 'GET'}, + controller=shared_ip_groups.Controller()) + class APIRouterV11(APIRouter): """Define routes specific to OpenStack API V1.1.""" diff --git a/nova/api/openstack/shared_ip_groups.py b/nova/api/openstack/shared_ip_groups.py index 5d78f9377..ee7991d7f 100644 --- a/nova/api/openstack/shared_ip_groups.py +++ b/nova/api/openstack/shared_ip_groups.py @@ -42,11 +42,11 @@ class Controller(wsgi.Controller): def index(self, req): """ Returns a list of Shared IP Groups for the user """ - return dict(sharedIpGroups=[]) + raise faults.Fault(exc.HTTPNotImplemented()) def show(self, req, id): """ Shows in-depth information on a specific Shared IP Group """ - return _translate_keys({}) + raise faults.Fault(exc.HTTPNotImplemented()) def update(self, req, id): """ You can't update a Shared IP Group """ @@ -58,7 +58,7 @@ class Controller(wsgi.Controller): def detail(self, req): """ Returns a complete list of Shared IP Groups """ - return _translate_detail_keys({}) + raise faults.Fault(exc.HTTPNotImplemented()) def create(self, req): """ Creates a new Shared IP group """ -- cgit From b6df504c33cfa0fe02e31962578b77d841e1e6d8 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 28 Mar 2011 14:31:12 -0400 Subject: backup_schedule tests corrected; controller moved to APIRouterV10; making controller fully HTTPNotImplemented --- nova/api/openstack/__init__.py | 10 +++++----- nova/api/openstack/backup_schedules.py | 6 +++++- 2 files changed, 10 insertions(+), 6 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 8fabbce8e..149abfeb8 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -106,11 +106,6 @@ class APIRouter(wsgi.Router): controller=accounts.Controller(), collection={'detail': 'GET'}) - mapper.resource("backup_schedule", "backup_schedule", - controller=backup_schedules.Controller(), - parent_resource=dict(member_name='server', - collection_name='servers')) - mapper.resource("console", "consoles", controller=consoles.Controller(), parent_resource=dict(member_name='server', @@ -141,6 +136,11 @@ class APIRouterV10(APIRouter): controller=flavors.ControllerV10(), collection={'detail': 'GET'}) + mapper.resource("backup_schedule", "backup_schedule", + controller=backup_schedules.Controller(), + 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/backup_schedules.py b/nova/api/openstack/backup_schedules.py index 7abb5f884..f2d2d86e8 100644 --- a/nova/api/openstack/backup_schedules.py +++ b/nova/api/openstack/backup_schedules.py @@ -42,7 +42,11 @@ class Controller(wsgi.Controller): def index(self, req, server_id): """ Returns the list of backup schedules for a given instance """ - return _translate_keys({}) + return faults.Fault(exc.HTTPNotImplemented()) + + def show(self, req, server_id, id): + """ Returns a single backup schedule for a given instance """ + return faults.Fault(exc.HTTPNotImplemented()) def create(self, req, server_id): """ No actual update method required, since the existing API allows -- cgit From dea3af64186ff204de7d5ca9852af267e648823e Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 28 Mar 2011 20:36:07 +0200 Subject: Remove now useless try/except block. --- nova/api/openstack/extensions.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 259d24a7d..e2f833d57 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -320,20 +320,16 @@ class ExtensionManager(object): if file_ext.lower() == '.py' and not mod_name.startswith('_'): mod = imp.load_source(mod_name, ext_path) ext_name = mod_name[0].upper() + mod_name[1:] - try: - new_ext_class = getattr(mod, ext_name, None) - if not new_ext_class: - LOG.warn(_('Did not find expected name ' - '"%(ext_name)" in %(file)s'), - { 'ext_name': ext_name, - 'file': ext_path }) - continue - new_ext = new_ext_class() - self._check_extension(new_ext) - self.extensions[new_ext.get_alias()] = new_ext - except AttributeError as ex: - LOG.exception(_("Exception loading extension: %s"), - unicode(ex)) + new_ext_class = getattr(mod, ext_name, None) + if not new_ext_class: + LOG.warn(_('Did not find expected name ' + '"%(ext_name)" in %(file)s'), + { 'ext_name': ext_name, + 'file': ext_path }) + continue + new_ext = new_ext_class() + self._check_extension(new_ext) + self.extensions[new_ext.get_alias()] = new_ext class ResponseExtension(object): -- cgit From 9fdf9967234d8553c3548ad03fc3b2691285fa7d Mon Sep 17 00:00:00 2001 From: Devin Carlen Date: Mon, 28 Mar 2011 11:56:19 -0700 Subject: Added image name and description mapping to ec2 api --- nova/api/ec2/cloud.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 0da642318..9e34d3317 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -890,6 +890,8 @@ class CloudController(object): i['imageOwnerId'] = image['properties'].get('owner_id') i['imageLocation'] = image['properties'].get('image_location') i['imageState'] = image['properties'].get('image_state') + i['displayName'] = image.get('name') + i['description'] = image.get('description') i['type'] = image_type i['isPublic'] = str(image['properties'].get('is_public', '')) == 'True' i['architecture'] = image['properties'].get('architecture') -- cgit From 7040eadcc7e86d063c5c69391dedafa181711913 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 28 Mar 2011 22:21:18 +0200 Subject: pep8 --- nova/api/openstack/extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index e2f833d57..b9b7f998d 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -324,8 +324,8 @@ class ExtensionManager(object): if not new_ext_class: LOG.warn(_('Did not find expected name ' '"%(ext_name)" in %(file)s'), - { 'ext_name': ext_name, - 'file': ext_path }) + {'ext_name': ext_name, + 'file': ext_path}) continue new_ext = new_ext_class() self._check_extension(new_ext) -- cgit From 633917f56200cc11ef26717b8305ef0ccbe76569 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 28 Mar 2011 16:42:09 -0400 Subject: Made param descriptions sphinx compatible. --- nova/api/openstack/images.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 54e08438a..5fd1f0163 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -51,8 +51,8 @@ class Controller(wsgi.Controller): """ Initialize new `ImageController`. - @param compute_service: `nova.compute.api:API` - @param image_service: `nova.image.service:BaseImageService` + :param compute_service: `nova.compute.api:API` + :param image_service: `nova.image.service:BaseImageService` """ _default_service = utils.import_object(flags.FLAGS.image_service) @@ -63,7 +63,7 @@ class Controller(wsgi.Controller): """ Return an index listing of images available to the request. - @param req: `wsgi.Request` object + :param req: `wsgi.Request` object """ context = req.environ['nova.context'] images = self._image_service.index(context) @@ -75,7 +75,7 @@ class Controller(wsgi.Controller): """ Return a detailed index listing of images available to the request. - @param req: `wsgi.Request` object. + :param req: `wsgi.Request` object. """ context = req.environ['nova.context'] images = self._image_service.detail(context) @@ -87,8 +87,8 @@ class Controller(wsgi.Controller): """ Return detailed information about a specific image. - @param req: `wsgi.Request` object - @param id: Image identifier (integer) + :param req: `wsgi.Request` object + :param id: Image identifier (integer) """ context = req.environ['nova.context'] @@ -110,8 +110,8 @@ class Controller(wsgi.Controller): """ Delete an image, if allowed. - @param req: `wsgi.Request` object - @param id: Image identifier (integer) + :param req: `wsgi.Request` object + :param id: Image identifier (integer) """ image_id = id context = req.environ['nova.context'] @@ -122,7 +122,7 @@ class Controller(wsgi.Controller): """ Snapshot a server instance and save the image. - @param req: `wsgi.Request` object + :param req: `wsgi.Request` object """ context = req.environ['nova.context'] content_type = req.get_content_type() -- cgit From 45e3deee1581580bed56d1bdfaaf9f4814fe7963 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 28 Mar 2011 17:13:37 -0400 Subject: Updated docstrings to satisfy. --- nova/api/openstack/images.py | 40 ++++++++++++---------------------------- 1 file changed, 12 insertions(+), 28 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 5fd1f0163..ad748b4ca 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -32,10 +32,8 @@ FLAGS = flags.FLAGS class Controller(wsgi.Controller): - """ - Base `wsgi.Controller` for retrieving and displaying images in the - OpenStack API. Version-inspecific code goes here. - """ + """Base `wsgi.Controller` for retrieving and displaying images in the + OpenStack API. Version-inspecific code goes here.""" _serialization_metadata = { 'application/xml': { @@ -48,8 +46,7 @@ class Controller(wsgi.Controller): } def __init__(self, image_service=None, compute_service=None): - """ - Initialize new `ImageController`. + """Initialize new `ImageController`. :param compute_service: `nova.compute.api:API` :param image_service: `nova.image.service:BaseImageService` @@ -60,8 +57,7 @@ class Controller(wsgi.Controller): self._image_service = image_service or _default_service def index(self, req): - """ - Return an index listing of images available to the request. + """Return an index listing of images available to the request. :param req: `wsgi.Request` object """ @@ -72,8 +68,7 @@ class Controller(wsgi.Controller): return dict(images=[builder(image, detail=False) for image in images]) def detail(self, req): - """ - Return a detailed index listing of images available to the request. + """Return a detailed index listing of images available to the request. :param req: `wsgi.Request` object. """ @@ -84,8 +79,7 @@ class Controller(wsgi.Controller): return dict(images=[builder(image, detail=True) for image in images]) def show(self, req, id): - """ - Return detailed information about a specific image. + """Return detailed information about a specific image. :param req: `wsgi.Request` object :param id: Image identifier (integer) @@ -107,8 +101,7 @@ class Controller(wsgi.Controller): return dict(image=self.get_builder(req).build(image, detail=True)) def delete(self, req, id): - """ - Delete an image, if allowed. + """Delete an image, if allowed. :param req: `wsgi.Request` object :param id: Image identifier (integer) @@ -119,8 +112,7 @@ class Controller(wsgi.Controller): return webob.exc.HTTPNoContent() def create(self, req): - """ - Snapshot a server instance and save the image. + """Snapshot a server instance and save the image. :param req: `wsgi.Request` object """ @@ -146,26 +138,18 @@ class Controller(wsgi.Controller): class ControllerV10(Controller): - """ - Version 1.0 specific controller logic. - """ + """Version 1.0 specific controller logic.""" def get_builder(self, request): - """ - Property to get the ViewBuilder class we need to use. - """ + """Property to get the ViewBuilder class we need to use.""" base_url = request.application_url return images_view.ViewBuilderV10(base_url) class ControllerV11(Controller): - """ - Version 1.1 specific controller logic. - """ + """Version 1.1 specific controller logic.""" def get_builder(self, request): - """ - Property to get the ViewBuilder class we need to use. - """ + """Property to get the ViewBuilder class we need to use.""" base_url = request.application_url return images_view.ViewBuilderV11(base_url) -- cgit From 6efd9dc30870008750c9754de4672d3eea656cce Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 28 Mar 2011 17:15:54 -0400 Subject: Updated docstrings to satisfy. --- nova/api/openstack/views/images.py | 35 ++++++++++------------------------- 1 file changed, 10 insertions(+), 25 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 9db73bd4b..8f5568f6c 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -19,29 +19,21 @@ import os.path class ViewBuilder(object): - """ - Base class for generating responses to OpenStack API requests for - information about images. - """ + """Base class for generating responses to OpenStack API requests for + information about images.""" def __init__(self, base_url): - """ - Initialize new `ViewBuilder`. - """ + """Initialize new `ViewBuilder`.""" self._url = base_url def _format_dates(self, image): - """ - Update all date fields to ensure standardized formatting. - """ + """Update all date fields to ensure standardized formatting.""" for attr in ['created_at', 'updated_at', 'deleted_at']: if image.get(attr) is not None: image[attr] = image[attr].strftime('%Y-%m-%dT%H:%M:%SZ') def _format_status(self, image): - """ - Update the status field to standardize format. - """ + """Update the status field to standardize format.""" status_mapping = { 'pending': 'queued', 'decrypting': 'preparing', @@ -56,15 +48,11 @@ class ViewBuilder(object): image['status'] = image['status'].upper() def generate_href(self, image_id): - """ - Return an href string pointing to this object. - """ + """Return an href string pointing to this object.""" return os.path.join(self._url, "images", str(image_id)) def build(self, image_obj, detail=False): - """ - Return a standardized image structure for display by the API. - """ + """Return a standardized image structure for display by the API.""" properties = image_obj.get("properties", {}) self._format_dates(image_obj) @@ -97,18 +85,15 @@ class ViewBuilder(object): class ViewBuilderV10(ViewBuilder): + """OpenStack API v1.0 Image Builder""" pass class ViewBuilderV11(ViewBuilder): - """ - OpenStack API v1.1 Image Builder - """ + """OpenStack API v1.1 Image Builder""" def build(self, image_obj, detail=False): - """ - Return a standardized image structure for display by the API. - """ + """Return a standardized image structure for display by the API.""" image = ViewBuilder.build(self, image_obj, detail) href = self.generate_href(image_obj["id"]) -- cgit From c439309fddb7e6ebc14ab6b82ac9960f459c5aed Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 28 Mar 2011 17:40:45 -0400 Subject: osapi servers update tests actually assert now; enforcing server name being a string of length > 0; moving server update adminPass support to be v1.0-specific --- nova/api/openstack/servers.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 75a305a14..80617cf1c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -246,20 +246,27 @@ class Controller(wsgi.Controller): ctxt = req.environ['nova.context'] update_dict = {} - if 'adminPass' in inst_dict['server']: - update_dict['admin_pass'] = inst_dict['server']['adminPass'] - try: - self.compute_api.set_admin_password(ctxt, id) - except exception.TimeoutException: - return exc.HTTPRequestTimeout() + if 'name' in inst_dict['server']: - update_dict['display_name'] = inst_dict['server']['name'] + name = inst_dict['server']['name'] + + if not isinstance(name, basestring) or name == '': + return exc.HTTPBadRequest() + + update_dict['display_name'] = name + + self._parse_update(ctxt, id, inst_dict, update_dict) + try: self.compute_api.update(ctxt, id, **update_dict) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPNoContent() + def _parse_update(self, context, id, inst_dict, update_dict): + pass + @scheduler_api.redirect_handler def action(self, req, id): """Multi-purpose method used to reboot, rebuild, or @@ -566,6 +573,15 @@ class ControllerV10(Controller): def _limit_items(self, items, req): return common.limited(items, req) + def _parse_update(self, context, server_id, inst_dict, update_dict): + if 'adminPass' in inst_dict['server']: + update_dict['admin_pass'] = inst_dict['server']['adminPass'] + try: + self.compute_api.set_admin_password(context, server_id) + except exception.TimeoutException: + return exc.HTTPRequestTimeout() + + class ControllerV11(Controller): def _image_id_from_req_data(self, data): -- cgit From f460d75ae355ee76b6c51d884162f00076140716 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 28 Mar 2011 17:45:48 -0400 Subject: pep8 --- nova/api/openstack/servers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 80617cf1c..e9bc0a797 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -253,7 +253,7 @@ class Controller(wsgi.Controller): if not isinstance(name, basestring) or name == '': return exc.HTTPBadRequest() - update_dict['display_name'] = name + update_dict['display_name'] = name self._parse_update(ctxt, id, inst_dict, update_dict) @@ -582,7 +582,6 @@ class ControllerV10(Controller): return exc.HTTPRequestTimeout() - class ControllerV11(Controller): def _image_id_from_req_data(self, data): href = data['server']['imageRef'] -- cgit From 94092e3d896732fa1a97627f0fa504c3af70b3c5 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Mon, 28 Mar 2011 15:38:09 -0700 Subject: address some of termie's recommendations --- nova/api/openstack/servers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index c0fba4bb9..822342149 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -473,7 +473,7 @@ class Controller(wsgi.Controller): @scheduler_api.redirect_handler def get_ajax_console(self, req, id): - """ Returns a url to an instance's ajaxterm console. """ + """Returns a url to an instance's ajaxterm console.""" try: self.compute_api.get_ajax_console(req.environ['nova.context'], int(id)) @@ -483,7 +483,7 @@ class Controller(wsgi.Controller): @scheduler_api.redirect_handler def get_vnc_console(self, req, id): - """ Returns a url to an instance's ajaxterm console. """ + """Returns a url to an instance's ajaxterm console.""" try: self.compute_api.get_vnc_console(req.environ['nova.context'], int(id)) -- cgit From 57a4864e30df604612a347ba069ccc8499b04f1f Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 28 Mar 2011 16:30:31 -0700 Subject: Code cleanup to keep the termie-bot happy --- nova/api/openstack/extensions.py | 131 +++++++++++++++------------------------ 1 file changed, 50 insertions(+), 81 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index e81ffb3d3..9566d3250 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -38,55 +38,55 @@ FLAGS = flags.FLAGS class ExtensionDescriptor(object): - """This is the base class that defines the contract for extensions""" + """This is the base class that defines the contract for extensions.""" def get_name(self): - """The name of the extension + """The name of the extension. e.g. 'Fox In Socks' """ raise NotImplementedError() def get_alias(self): - """The alias for the extension + """The alias for the extension. e.g. 'FOXNSOX'""" raise NotImplementedError() def get_description(self): - """Friendly description for the extension + """Friendly description for the extension. e.g. 'The Fox In Socks Extension'""" raise NotImplementedError() def get_namespace(self): - """The XML namespace for the extension + """The XML namespace for the extension. e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'""" raise NotImplementedError() def get_updated(self): - """The timestamp when the extension was last updated + """The timestamp when the extension was last updated. e.g. '2011-01-22T13:25:27-06:00'""" - #NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS + # NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS raise NotImplementedError() def get_resources(self): - """List of extensions.ResourceExtension extension objects + """List of extensions.ResourceExtension extension objects. Resources define new nouns, and are accessible through URLs""" resources = [] return resources def get_actions(self): - """List of extensions.ActionExtension extension objects + """List of extensions.ActionExtension extension objects. Actions are verbs callable from the API""" actions = [] return actions def get_response_extensions(self): - """List of extensions.ResponseExtension extension objects + """List of extensions.ResponseExtension extension objects. Response extensions are used to insert information into existing response data""" @@ -154,17 +154,17 @@ class ExtensionController(wsgi.Controller): ext_data['description'] = ext.get_description() ext_data['namespace'] = ext.get_namespace() ext_data['updated'] = ext.get_updated() - ext_data['links'] = [] # TODO: implement extension links + ext_data['links'] = [] # TODO(dprince): implement extension links return ext_data def index(self, req): extensions = [] - for alias, ext in self.extension_manager.extensions.iteritems(): + for _alias, ext in self.extension_manager.extensions.iteritems(): extensions.append(self._translate(ext)) return dict(extensions=extensions) def show(self, req, id): - # NOTE: the extensions alias is used as the 'id' for show + # NOTE(dprince): the extensions alias is used as the 'id' for show ext = self.extension_manager.extensions[id] return self._translate(ext) @@ -176,20 +176,16 @@ class ExtensionController(wsgi.Controller): class ExtensionMiddleware(wsgi.Middleware): - """ - Extensions middleware that intercepts configured routes for extensions. - """ + """Extensions middleware for WSGI.""" @classmethod def factory(cls, global_config, **local_config): - """ paste factory """ + """Paste factory.""" def _factory(app): return cls(app, **local_config) return _factory def _action_ext_controllers(self, application, ext_mgr, mapper): - """ - Return a dict of ActionExtensionController objects by collection - """ + """Return a dict of ActionExtensionController-s by collection.""" action_controllers = {} for action in ext_mgr.get_actions(): if not action.collection in action_controllers.keys(): @@ -208,9 +204,7 @@ class ExtensionMiddleware(wsgi.Middleware): return action_controllers def _response_ext_controllers(self, application, ext_mgr, mapper): - """ - Return a dict of ResponseExtensionController objects by collection - """ + """Returns a dict of ResponseExtensionController-s by collection.""" response_ext_controllers = {} for resp_ext in ext_mgr.get_response_extensions(): if not resp_ext.key in response_ext_controllers.keys(): @@ -269,16 +263,15 @@ class ExtensionMiddleware(wsgi.Middleware): @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): - """ - Route the incoming request with router. - """ + """Route the incoming request with router.""" req.environ['extended.app'] = self.application return self._router @staticmethod @webob.dec.wsgify(RequestClass=wsgi.Request) def _dispatch(req): - """ + """Dispatch the request. + Returns the routed WSGI app's response or defers to the extended application. """ @@ -290,8 +283,7 @@ class ExtensionMiddleware(wsgi.Middleware): class ExtensionManager(object): - """ - Load extensions from the configured extension path. + """Load extensions from the configured extension path. See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an example extension implementation. @@ -307,50 +299,30 @@ class ExtensionManager(object): self._load_all_extensions() def get_resources(self): - """ - returns a list of ResourceExtension objects - """ + """Returns a list of ResourceExtension objects.""" resources = [] resources.append(ResourceExtension('extensions', ExtensionController(self))) - for alias, ext in self.extensions.iteritems(): - try: - resources.extend(ext.get_resources()) - except AttributeError: - # NOTE: Extension aren't required to have resource extensions - pass + for _alias, ext in self.extensions.iteritems(): + resources.extend(ext.get_resources()) return resources def get_actions(self): - """ - returns a list of ActionExtension objects - """ + """Returns a list of ActionExtension objects.""" actions = [] - for alias, ext in self.extensions.iteritems(): - try: - actions.extend(ext.get_actions()) - except AttributeError: - # NOTE: Extension aren't required to have action extensions - pass + for _alias, ext in self.extensions.iteritems(): + actions.extend(ext.get_actions()) return actions def get_response_extensions(self): - """ - returns a list of ResponseExtension objects - """ + """Returns a list of ResponseExtension objects.""" response_exts = [] - for alias, ext in self.extensions.iteritems(): - try: - response_exts.extend(ext.get_response_extensions()) - except AttributeError: - # NOTE: Extension aren't required to have response extensions - pass + for _alias, ext in self.extensions.iteritems(): + response_exts.extend(ext.get_response_extensions()) return response_exts def _check_extension(self, extension): - """ - Checks for required methods in extension objects. - """ + """Checks for required methods in extension objects.""" try: LOG.debug(_('Ext name: %s'), extension.get_name()) LOG.debug(_('Ext alias: %s'), extension.get_alias()) @@ -361,11 +333,17 @@ class ExtensionManager(object): LOG.exception(_("Exception loading extension: %s"), unicode(ex)) def _load_all_extensions(self): - """ - Load extensions from the configured path. The extension name is - constructed from the module_name. If your extension module was named - widgets.py the extension class within that module should be - 'Widgets'. + """Load extensions from the configured path. + + An extension consists of a directory of related files, with a class + that defines a class that inherits from ExtensionDescriptor. + + Because of some oddities involving identically named modules, it's + probably best to name your file after the name of your extension, + rather than something likely to clash like 'extension.py'. + + The name of your directory should be the same as the alias your + extension uses, for everyone's sanity. See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. @@ -409,11 +387,11 @@ class ExtensionManager(object): if not inspect.isclass(cls): continue - #NOTE(justinsb): It seems that python modules aren't great - # If you have two identically named modules, the classes - # from both are mixed in. So name your extension based - # on the alias, not 'extension.py'! - #TODO(justinsb): Any way to work around this? + # NOTE(justinsb): It seems that python modules are odd. + # If you have two identically named modules, the classes + # from both are mixed in. So name your extension based + # on the alias, not 'extension.py'! + # TODO(justinsb): Any way to work around this? if self.super_verbose: LOG.debug(_('Checking class: %s'), cls) @@ -441,10 +419,7 @@ class ExtensionManager(object): class ResponseExtension(object): - """ - ResponseExtension objects can be used to add data to responses from - core nova OpenStack API controllers. - """ + """Add data to responses from core nova OpenStack API controllers.""" def __init__(self, method, url_route, handler): self.url_route = url_route @@ -454,10 +429,7 @@ class ResponseExtension(object): class ActionExtension(object): - """ - ActionExtension objects can be used to add custom actions to core nova - nova OpenStack API controllers. - """ + """Add custom actions to core nova OpenStack API controllers.""" def __init__(self, collection, action_name, handler): self.collection = collection @@ -466,10 +438,7 @@ class ActionExtension(object): class ResourceExtension(object): - """ - ResourceExtension objects can be used to add top level resources - to the OpenStack API in nova. - """ + """Add top level resources to the OpenStack API in nova.""" def __init__(self, collection, controller, parent=None, collection_actions={}, member_actions={}): -- cgit From a6e8c83196cb4b2d8a292a99cb1feb22ed9b21db Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 28 Mar 2011 16:36:25 -0700 Subject: Cleaned up images/fake.py, including move to Duplicate exception --- .../incubator/volumes/volume_attachments.py | 30 +++++++++++----------- nova/api/openstack/incubator/volumes/volumes.py | 18 ++++++------- .../api/openstack/incubator/volumes/volumes_ext.py | 4 +-- 3 files changed, 26 insertions(+), 26 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/incubator/volumes/volume_attachments.py b/nova/api/openstack/incubator/volumes/volume_attachments.py index 93786d1c7..1e260b34d 100644 --- a/nova/api/openstack/incubator/volumes/volume_attachments.py +++ b/nova/api/openstack/incubator/volumes/volume_attachments.py @@ -34,7 +34,7 @@ FLAGS = flags.FLAGS def _translate_detail_view(context, volume): - """Maps keys for details view""" + """Maps keys for details view.""" d = _translate_summary_view(context, volume) @@ -44,12 +44,12 @@ def _translate_detail_view(context, volume): def _translate_summary_view(context, vol): - """Maps keys for summary view""" + """Maps keys for summary view.""" d = {} volume_id = vol['id'] - #NOTE(justinsb): We use the volume id as the id of the attachment object + # NOTE(justinsb): We use the volume id as the id of the attachment object d['id'] = volume_id d['volumeId'] = volume_id @@ -62,7 +62,7 @@ def _translate_summary_view(context, vol): class Controller(wsgi.Controller): - """The volume attachment API controller for the Openstack API + """The volume attachment API controller for the Openstack API. A child resource of the server. Note that we use the volume id as the ID of the attachment (though this is not guaranteed externally)""" @@ -81,12 +81,12 @@ class Controller(wsgi.Controller): super(Controller, self).__init__() def index(self, req, server_id): - """Returns the list of volume attachments for a given instance """ + """Returns the list of volume attachments for a given instance.""" return self._items(req, server_id, entity_maker=_translate_summary_view) def show(self, req, server_id, id): - """Return data about the given volume""" + """Return data about the given volume.""" context = req.environ['nova.context'] volume_id = id @@ -103,7 +103,7 @@ class Controller(wsgi.Controller): return {'volumeAttachment': _translate_detail_view(context, vol)} def create(self, req, server_id): - """Attach a volume to an instance """ + """Attach a volume to an instance.""" context = req.environ['nova.context'] env = self._deserialize(req.body, req.get_content_type()) @@ -131,15 +131,15 @@ class Controller(wsgi.Controller): attachment['id'] = volume_id attachment['volumeId'] = volume_id - #NOTE(justinsb): And now, we have a problem... + # NOTE(justinsb): And now, we have a problem... # The attach is async, so there's a window in which we don't see - # the attachment (until the attachment completes). We could also - # get problems with concurrent requests. I think we need an - # attachment state, and to write to the DB here, but that's a bigger - # change. + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. # For now, we'll probably have to rely on libraries being smart - #TODO(justinsb): How do I return "accepted" here? + # TODO(justinsb): How do I return "accepted" here? return {'volumeAttachment': attachment} def update(self, _req, _server_id, _id): @@ -147,7 +147,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPBadRequest()) def delete(self, req, server_id, id): - """Detach a volume from an instance """ + """Detach a volume from an instance.""" context = req.environ['nova.context'] volume_id = id @@ -168,7 +168,7 @@ class Controller(wsgi.Controller): return exc.HTTPAccepted() def _items(self, req, server_id, entity_maker): - """Returns a list of attachments, transformed through entity_maker""" + """Returns a list of attachments, transformed through entity_maker.""" context = req.environ['nova.context'] try: diff --git a/nova/api/openstack/incubator/volumes/volumes.py b/nova/api/openstack/incubator/volumes/volumes.py index e122bb465..a7d5fbaa6 100644 --- a/nova/api/openstack/incubator/volumes/volumes.py +++ b/nova/api/openstack/incubator/volumes/volumes.py @@ -31,7 +31,7 @@ FLAGS = flags.FLAGS def _translate_detail_view(context, vol): - """Maps keys for details view""" + """Maps keys for details view.""" d = _translate_summary_view(context, vol) @@ -41,7 +41,7 @@ def _translate_detail_view(context, vol): def _translate_summary_view(_context, vol): - """Maps keys for summary view""" + """Maps keys for summary view.""" d = {} instance_id = None @@ -79,7 +79,7 @@ def _translate_summary_view(_context, vol): class Controller(wsgi.Controller): - """The Volumes API controller for the OpenStack API""" + """The Volumes API controller for the OpenStack API.""" _serialization_metadata = { 'application/xml': { @@ -99,7 +99,7 @@ class Controller(wsgi.Controller): super(Controller, self).__init__() def show(self, req, id): - """Return data about the given volume""" + """Return data about the given volume.""" context = req.environ['nova.context'] try: @@ -110,7 +110,7 @@ class Controller(wsgi.Controller): return {'volume': _translate_detail_view(context, vol)} def delete(self, req, id): - """Delete a volume """ + """Delete a volume.""" context = req.environ['nova.context'] LOG.audit(_("Delete volume with id: %s"), id, context=context) @@ -122,15 +122,15 @@ class Controller(wsgi.Controller): return exc.HTTPAccepted() def index(self, req): - """Returns a summary list of volumes""" + """Returns a summary list of volumes.""" return self._items(req, entity_maker=_translate_summary_view) def detail(self, req): - """Returns a detailed list of volumes """ + """Returns a detailed list of volumes.""" return self._items(req, entity_maker=_translate_detail_view) def _items(self, req, entity_maker): - """Returns a list of volumes, transformed through entity_maker""" + """Returns a list of volumes, transformed through entity_maker.""" context = req.environ['nova.context'] volumes = self.volume_api.get_all(context) @@ -139,7 +139,7 @@ class Controller(wsgi.Controller): return {'volumes': res} def create(self, req): - """Creates a new volume""" + """Creates a new volume.""" context = req.environ['nova.context'] env = self._deserialize(req.body, req.get_content_type()) diff --git a/nova/api/openstack/incubator/volumes/volumes_ext.py b/nova/api/openstack/incubator/volumes/volumes_ext.py index 87a57320d..6a3bb0265 100644 --- a/nova/api/openstack/incubator/volumes/volumes_ext.py +++ b/nova/api/openstack/incubator/volumes/volumes_ext.py @@ -37,8 +37,8 @@ class VolumesExtension(extensions.ExtensionDescriptor): def get_resources(self): resources = [] - #NOTE(justinsb): No way to provide singular name ('volume') - # Does this matter? + # NOTE(justinsb): No way to provide singular name ('volume') + # Does this matter? res = extensions.ResourceExtension('volumes', volumes.Controller(), collection_actions={'detail': 'GET'} -- cgit From 87bc3bca7904135656ed3a99efc19952be95dcbf Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 28 Mar 2011 16:54:17 -0700 Subject: Multi-line comments should end in a blankline --- nova/api/openstack/incubator/volumes/volume_attachments.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/incubator/volumes/volume_attachments.py b/nova/api/openstack/incubator/volumes/volume_attachments.py index 1e260b34d..aec4ea8f3 100644 --- a/nova/api/openstack/incubator/volumes/volume_attachments.py +++ b/nova/api/openstack/incubator/volumes/volume_attachments.py @@ -65,7 +65,9 @@ class Controller(wsgi.Controller): """The volume attachment API controller for the Openstack API. A child resource of the server. Note that we use the volume id - as the ID of the attachment (though this is not guaranteed externally)""" + as the ID of the attachment (though this is not guaranteed externally) + + """ _serialization_metadata = { 'application/xml': { -- cgit From 55b801db77b9d631e746e79bd84a7866d1877fb2 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 28 Mar 2011 17:09:39 -0700 Subject: More style changes --- nova/api/openstack/extensions.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 1c39dd0e2..d0b4d378a 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -43,45 +43,59 @@ class ExtensionDescriptor(object): def get_name(self): """The name of the extension. - e.g. 'Fox In Socks' """ + e.g. 'Fox In Socks' + + """ raise NotImplementedError() def get_alias(self): """The alias for the extension. - e.g. 'FOXNSOX'""" + e.g. 'FOXNSOX' + + """ raise NotImplementedError() def get_description(self): """Friendly description for the extension. - e.g. 'The Fox In Socks Extension'""" + e.g. 'The Fox In Socks Extension' + + """ raise NotImplementedError() def get_namespace(self): """The XML namespace for the extension. - e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'""" + e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0' + + """ raise NotImplementedError() def get_updated(self): """The timestamp when the extension was last updated. - e.g. '2011-01-22T13:25:27-06:00'""" + e.g. '2011-01-22T13:25:27-06:00' + + """ # NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS raise NotImplementedError() def get_resources(self): """List of extensions.ResourceExtension extension objects. - Resources define new nouns, and are accessible through URLs""" + Resources define new nouns, and are accessible through URLs. + + """ resources = [] return resources def get_actions(self): """List of extensions.ActionExtension extension objects. - Actions are verbs callable from the API""" + Actions are verbs callable from the API. + + """ actions = [] return actions @@ -89,7 +103,9 @@ class ExtensionDescriptor(object): """List of extensions.ResponseExtension extension objects. Response extensions are used to insert information into existing - response data""" + response data. + + """ response_exts = [] return response_exts @@ -274,6 +290,7 @@ class ExtensionMiddleware(wsgi.Middleware): Returns the routed WSGI app's response or defers to the extended application. + """ match = req.environ['wsgiorg.routing_args'][1] if not match: @@ -287,6 +304,7 @@ class ExtensionManager(object): See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an example extension implementation. + """ def __init__(self, path): @@ -347,6 +365,7 @@ class ExtensionManager(object): See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. + """ self._load_extensions_under_path(self.path) -- cgit From c4c9c0e5b70305ac06494bde35fcd18fdf60798e Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Tue, 29 Mar 2011 09:51:02 -0400 Subject: Tweaking docstrings just in case. --- nova/api/openstack/images.py | 3 +-- nova/api/openstack/views/images.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index ad748b4ca..d672e3a0e 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -32,8 +32,7 @@ FLAGS = flags.FLAGS class Controller(wsgi.Controller): - """Base `wsgi.Controller` for retrieving and displaying images in the - OpenStack API. Version-inspecific code goes here.""" + """Base `wsgi.Controller` for retrieving/displaying images.""" _serialization_metadata = { 'application/xml': { diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 8f5568f6c..3807fa95f 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -19,8 +19,7 @@ import os.path class ViewBuilder(object): - """Base class for generating responses to OpenStack API requests for - information about images.""" + """Base class for generating responses to OpenStack API image requests.""" def __init__(self, base_url): """Initialize new `ViewBuilder`.""" -- cgit From 793de5cef9fb539a4fb3ba976d83adde38a589c1 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 29 Mar 2011 10:40:35 -0400 Subject: adding more tests; making name checks more robust --- nova/api/openstack/servers.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index e9bc0a797..29c491716 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -250,8 +250,15 @@ class Controller(wsgi.Controller): if 'name' in inst_dict['server']: name = inst_dict['server']['name'] - if not isinstance(name, basestring) or name == '': - return exc.HTTPBadRequest() + if not isinstance(name, basestring): + msg = _("Server name is not a string or unicode") + return exc.HTTPBadRequest(msg) + + name = name.strip() + + if name == '': + msg = _("Server name is an empty string") + return exc.HTTPBadRequest(msg) update_dict['display_name'] = name -- cgit From f624d2e35dab0d87a289a346999c0cb01ed0aa55 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 29 Mar 2011 11:11:57 -0400 Subject: adding server name validation to create method; adding tests --- nova/api/openstack/servers.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 29c491716..d564b37c1 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -150,6 +150,15 @@ class Controller(wsgi.Controller): injected_files = self._get_injected_files(personality) flavor_id = self._flavor_id_from_req_data(env) + + if not 'name' in env['server']: + msg = _("Server name is not defined") + return exc.HTTPBadRequest(msg) + + name = env['server']['name'] + self._validate_server_name(name) + name = name.strip() + try: (inst,) = self.compute_api.create( context, @@ -157,8 +166,8 @@ class Controller(wsgi.Controller): image_id, kernel_id=kernel_id, ramdisk_id=ramdisk_id, - display_name=env['server']['name'], - display_description=env['server']['name'], + display_name=name, + display_description=name, key_name=key_name, key_data=key_data, metadata=metadata, @@ -249,18 +258,8 @@ class Controller(wsgi.Controller): if 'name' in inst_dict['server']: name = inst_dict['server']['name'] - - if not isinstance(name, basestring): - msg = _("Server name is not a string or unicode") - return exc.HTTPBadRequest(msg) - - name = name.strip() - - if name == '': - msg = _("Server name is an empty string") - return exc.HTTPBadRequest(msg) - - update_dict['display_name'] = name + self._validate_server_name(name) + update_dict['display_name'] = name.strip() self._parse_update(ctxt, id, inst_dict, update_dict) @@ -271,6 +270,15 @@ class Controller(wsgi.Controller): return exc.HTTPNoContent() + def _validate_server_name(self, value): + if not isinstance(value, basestring): + msg = _("Server name is not a string or unicode") + raise exc.HTTPBadRequest(msg) + + if value.strip() == '': + msg = _("Server name is an empty string") + raise exc.HTTPBadRequest(msg) + def _parse_update(self, context, id, inst_dict, update_dict): pass -- cgit From a070b8861ccc01b485b109855f44a36cd6ebdbd6 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 29 Mar 2011 11:16:42 -0400 Subject: pep8 --- nova/api/openstack/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index d564b37c1..4400a68a1 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -154,7 +154,7 @@ class Controller(wsgi.Controller): if not 'name' in env['server']: msg = _("Server name is not defined") return exc.HTTPBadRequest(msg) - + name = env['server']['name'] self._validate_server_name(name) name = name.strip() -- cgit From d1ef69edb8da18c5c7e56b6006e22022d55d6664 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 29 Mar 2011 11:41:33 -0400 Subject: adding code to explicitly set the content-type in versions controller; updating test --- nova/api/openstack/versions.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 33f1dd628..3f9d91934 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -15,8 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +import webob import webob.dec -import webob.exc from nova import wsgi import nova.api.openstack.views.versions @@ -51,4 +51,10 @@ class Versions(wsgi.Application): } content_type = req.best_match_content_type() - return wsgi.Serializer(metadata).serialize(response, content_type) + body = wsgi.Serializer(metadata).serialize(response, content_type) + + response = webob.Response() + response.content_type = content_type + response.body = body + + return response -- cgit From 2f89d5541aa11b8654b197ffe24d3fd13e945da6 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Tue, 29 Mar 2011 12:12:26 -0400 Subject: Import order. --- nova/api/openstack/images.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index d672e3a0e..e77100d7b 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -13,9 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. -import webob.exc import datetime +import webob.exc + from nova import compute from nova import exception from nova import flags -- cgit From f5c072de1edddc4ddab89be8146a81d361397c45 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 29 Mar 2011 14:53:38 -0700 Subject: incorporate feedback from termie --- nova/api/openstack/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 822342149..8170ab4a1 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -486,7 +486,7 @@ class Controller(wsgi.Controller): """Returns a url to an instance's ajaxterm console.""" try: self.compute_api.get_vnc_console(req.environ['nova.context'], - int(id)) + int(id)) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() -- cgit From e86f58261ee6acb8705106d3de61be0de488d94b Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 15:37:45 -0700 Subject: Reverted extension loading tweaks --- nova/api/openstack/extensions.py | 116 +++---- nova/api/openstack/incubator/__init__.py | 4 +- nova/api/openstack/incubator/volumes.py | 355 +++++++++++++++++++++ nova/api/openstack/incubator/volumes/__init__.py | 18 -- .../incubator/volumes/volume_attachments.py | 184 ----------- nova/api/openstack/incubator/volumes/volumes.py | 161 ---------- .../api/openstack/incubator/volumes/volumes_ext.py | 55 ---- 7 files changed, 407 insertions(+), 486 deletions(-) create mode 100644 nova/api/openstack/incubator/volumes.py delete mode 100644 nova/api/openstack/incubator/volumes/__init__.py delete mode 100644 nova/api/openstack/incubator/volumes/volume_attachments.py delete mode 100644 nova/api/openstack/incubator/volumes/volumes.py delete mode 100644 nova/api/openstack/incubator/volumes/volumes_ext.py (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index d0b4d378a..d1d479313 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -38,7 +38,10 @@ FLAGS = flags.FLAGS class ExtensionDescriptor(object): - """This is the base class that defines the contract for extensions.""" + """Base class that defines the contract for extensions. + + Note that you don't have to derive from this class to have a valid + extension; it is purely a convenience.""" def get_name(self): """The name of the extension. @@ -321,22 +324,37 @@ class ExtensionManager(object): resources = [] resources.append(ResourceExtension('extensions', ExtensionController(self))) - for _alias, ext in self.extensions.iteritems(): - resources.extend(ext.get_resources()) + for alias, ext in self.extensions.iteritems(): + try: + resources.extend(ext.get_resources()) + except AttributeError: + # NOTE(dprince): Extension aren't required to have resource + # extensions + pass return resources def get_actions(self): """Returns a list of ActionExtension objects.""" actions = [] - for _alias, ext in self.extensions.iteritems(): - actions.extend(ext.get_actions()) + for alias, ext in self.extensions.iteritems(): + try: + actions.extend(ext.get_actions()) + except AttributeError: + # NOTE(dprince): Extension aren't required to have action + # extensions + pass return actions def get_response_extensions(self): """Returns a list of ResponseExtension objects.""" response_exts = [] - for _alias, ext in self.extensions.iteritems(): - response_exts.extend(ext.get_response_extensions()) + for alias, ext in self.extensions.iteritems(): + try: + response_exts.extend(ext.get_response_extensions()) + except AttributeError: + # NOTE(dprince): Extension aren't required to have response + # extensions + pass return response_exts def _check_extension(self, extension): @@ -353,78 +371,42 @@ class ExtensionManager(object): def _load_all_extensions(self): """Load extensions from the configured path. - An extension consists of a directory of related files, with a class - that defines a class that inherits from ExtensionDescriptor. + Load extensions from the configured path. The extension name is + constructed from the module_name. If your extension module was named + widgets.py the extension class within that module should be + 'Widgets'. - Because of some oddities involving identically named modules, it's - probably best to name your file after the name of your extension, - rather than something likely to clash like 'extension.py'. - - The name of your directory should be the same as the alias your - extension uses, for everyone's sanity. + In addition, extensions are loaded from the 'incubator' directory. See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. """ - self._load_extensions_under_path(self.path) + if os.path.exists(self.path): + self._load_all_extensions_from_path(self.path) incubator_path = os.path.join(os.path.dirname(__file__), "incubator") - self._load_extensions_under_path(incubator_path) - - def _load_extensions_under_path(self, path): - if not os.path.isdir(path): - LOG.warning(_('Extensions directory not found: %s') % path) - return - - LOG.debug(_('Looking for extensions in: %s') % path) - - for child in os.listdir(path): - child_path = os.path.join(path, child) - if not os.path.isdir(child_path): - continue - self._load_extension(child_path) - - def _load_extension(self, path): - if not os.path.isdir(path): - return + if os.path.exists(incubator_path): + self._load_all_extensions_from_path(incubator_path) + def _load_all_extensions_from_path(self, path): for f in os.listdir(path): + LOG.audit(_('Loading extension file: %s'), f) mod_name, file_ext = os.path.splitext(os.path.split(f)[-1]) - if mod_name.startswith('_'): - continue - if file_ext.lower() != '.py': - continue - ext_path = os.path.join(path, f) - if self.super_verbose: - LOG.debug(_('Checking extension file: %s'), ext_path) - - mod = imp.load_source(mod_name, ext_path) - for _name, cls in inspect.getmembers(mod): - try: - if not inspect.isclass(cls): - continue - - # NOTE(justinsb): It seems that python modules are odd. - # If you have two identically named modules, the classes - # from both are mixed in. So name your extension based - # on the alias, not 'extension.py'! - # TODO(justinsb): Any way to work around this? - - if self.super_verbose: - LOG.debug(_('Checking class: %s'), cls) - - if not ExtensionDescriptor in cls.__bases__: - if self.super_verbose: - LOG.debug(_('Not a ExtensionDescriptor: %s'), cls) - continue - - obj = cls() - self._add_extension(obj) - except AttributeError as ex: - LOG.exception(_("Exception loading extension: %s"), - unicode(ex)) + if file_ext.lower() == '.py' and not mod_name.startswith('_'): + mod = imp.load_source(mod_name, ext_path) + ext_name = mod_name[0].upper() + mod_name[1:] + new_ext_class = getattr(mod, ext_name, None) + if not new_ext_class: + LOG.warn(_('Did not find expected name ' + '"%(ext_name)s" in %(file)s'), + {'ext_name': ext_name, + 'file': ext_path}) + continue + new_ext = new_ext_class() + self._check_extension(new_ext) + self._add_extension(new_ext) def _add_extension(self, ext): alias = ext.get_alias() diff --git a/nova/api/openstack/incubator/__init__.py b/nova/api/openstack/incubator/__init__.py index cded38174..e115f1ab9 100644 --- a/nova/api/openstack/incubator/__init__.py +++ b/nova/api/openstack/incubator/__init__.py @@ -17,4 +17,6 @@ """Incubator contains extensions that are shipped with nova. -It can't be called 'extensions' because that causes namespacing problems.""" +It can't be called 'extensions' because that causes namespacing problems. + +""" diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py new file mode 100644 index 000000000..7a5b2c1d0 --- /dev/null +++ b/nova/api/openstack/incubator/volumes.py @@ -0,0 +1,355 @@ +# Copyright 2011 Justin Santa Barbara +# 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. + +"""The volumes extension.""" + +from webob import exc + +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + + +FLAGS = flags.FLAGS + + +def _translate_volume_detail_view(context, vol): + """Maps keys for volumes details view.""" + + d = _translate_volume_summary_view(context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_volume_summary_view(_context, vol): + """Maps keys for volumes summary view.""" + d = {} + + instance_id = None + # instance_data = None + attached_to = vol.get('instance') + if attached_to: + instance_id = attached_to['id'] + # instance_data = '%s[%s]' % (instance_ec2_id, + # attached_to['host']) + d['id'] = vol['id'] + d['status'] = vol['status'] + d['size'] = vol['size'] + d['availabilityZone'] = vol['availability_zone'] + d['createdAt'] = vol['created_at'] + # if context.is_admin: + # v['status'] = '%s (%s, %s, %s, %s)' % ( + # vol['status'], + # vol['user_id'], + # vol['host'], + # instance_data, + # vol['mountpoint']) + if vol['attach_status'] == 'attached': + d['attachments'] = [{'attachTime': vol['attach_time'], + 'deleteOnTermination': False, + 'mountpoint': vol['mountpoint'], + 'instanceId': instance_id, + 'status': 'attached', + 'volumeId': vol['id']}] + else: + d['attachments'] = [{}] + + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +class VolumeController(wsgi.Controller): + """The Volumes API controller for the OpenStack API.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "volume": [ + "id", + "status", + "size", + "availabilityZone", + "createdAt", + "displayName", + "displayDescription", + ]}}} + + def __init__(self): + self.volume_api = volume.API() + super(VolumeController, self).__init__() + + def show(self, req, id): + """Return data about the given volume.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get(context, id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + return {'volume': _translate_volume_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a volume.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete volume with id: %s"), id, context=context) + + try: + self.volume_api.delete(context, volume_id=id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + + def index(self, req): + """Returns a summary list of volumes.""" + return self._items(req, entity_maker=_translate_volume_summary_view) + + def detail(self, req): + """Returns a detailed list of volumes.""" + return self._items(req, entity_maker=_translate_volume_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of volumes, transformed through entity_maker.""" + context = req.environ['nova.context'] + + volumes = self.volume_api.get_all(context) + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumes': res} + + def create(self, req): + """Creates a new volume.""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + vol = env['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + new_volume = self.volume_api.create(context, size, + vol.get('display_name'), + vol.get('display_description')) + + # Work around problem that instance is lazy-loaded... + new_volume['instance'] = None + + retval = _translate_volume_detail_view(context, new_volume) + + return {'volume': retval} + + +def _translate_attachment_detail_view(_context, vol): + """Maps keys for attachment details view.""" + + d = _translate_attachment_summary_view(_context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_attachment_summary_view(_context, vol): + """Maps keys for attachment summary view.""" + d = {} + + volume_id = vol['id'] + + # NOTE(justinsb): We use the volume id as the id of the attachment object + d['id'] = volume_id + + d['volumeId'] = volume_id + if vol.get('instance_id'): + d['serverId'] = vol['instance_id'] + if vol.get('mountpoint'): + d['device'] = vol['mountpoint'] + + return d + + +class VolumeAttachmentController(wsgi.Controller): + """The volume attachment API controller for the Openstack API. + + A child resource of the server. Note that we use the volume id + as the ID of the attachment (though this is not guaranteed externally) + + """ + + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'volumeAttachment': ['id', + 'serverId', + 'volumeId', + 'device']}}} + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(VolumeAttachmentController, self).__init__() + + def index(self, req, server_id): + """Returns the list of volume attachments for a given instance.""" + return self._items(req, server_id, + entity_maker=_translate_attachment_summary_view) + + def show(self, req, server_id, id): + """Return data about the given volume.""" + context = req.environ['nova.context'] + + volume_id = id + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + LOG.debug("volume_id not found") + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + return {'volumeAttachment': _translate_attachment_detail_view(context, + vol)} + + def create(self, req, server_id): + """Attach a volume to an instance.""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + instance_id = server_id + volume_id = env['volumeAttachment']['volumeId'] + device = env['volumeAttachment']['device'] + + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" + " at %(device)s") % locals() + LOG.audit(msg, context=context) + + try: + self.compute_api.attach_volume(context, + instance_id=instance_id, + volume_id=volume_id, + device=device) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + # The attach is async + attachment = {} + attachment['id'] = volume_id + attachment['volumeId'] = volume_id + + # NOTE(justinsb): And now, we have a problem... + # The attach is async, so there's a window in which we don't see + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. + # For now, we'll probably have to rely on libraries being smart + + # TODO(justinsb): How do I return "accepted" here? + return {'volumeAttachment': attachment} + + def update(self, _req, _server_id, _id): + """Update a volume attachment. We don't currently support this.""" + return faults.Fault(exc.HTTPBadRequest()) + + def delete(self, req, server_id, id): + """Detach a volume from an instance.""" + context = req.environ['nova.context'] + + volume_id = id + LOG.audit(_("Detach volume %s"), volume_id, context=context) + + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + self.compute_api.detach_volume(context, + volume_id=volume_id) + + return exc.HTTPAccepted() + + def _items(self, req, server_id, entity_maker): + """Returns a list of attachments, transformed through entity_maker.""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.get(context, server_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + volumes = instance['volumes'] + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumeAttachments': res} + + +class Volumes(extensions.ExtensionDescriptor): + def get_name(self): + return "Volumes" + + def get_alias(self): + return "VOLUMES" + + def get_description(self): + return "Volumes support" + + def get_namespace(self): + return "http://docs.openstack.org/ext/volumes/api/v1.1" + + def get_updated(self): + return "2011-03-25T00:00:00+00:00" + + def get_resources(self): + resources = [] + + # NOTE(justinsb): No way to provide singular name ('volume') + # Does this matter? + res = extensions.ResourceExtension('volumes', + VolumeController(), + collection_actions= + {'detail': 'GET'} + ) + resources.append(res) + + res = extensions.ResourceExtension('volume_attachments', + VolumeAttachmentController(), + parent=dict( + member_name='server', + collection_name='servers')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/incubator/volumes/__init__.py b/nova/api/openstack/incubator/volumes/__init__.py deleted file mode 100644 index 2a9c93210..000000000 --- a/nova/api/openstack/incubator/volumes/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License.import datetime - -"""The volumes extension adds volumes and attachments to the API.""" diff --git a/nova/api/openstack/incubator/volumes/volume_attachments.py b/nova/api/openstack/incubator/volumes/volume_attachments.py deleted file mode 100644 index aec4ea8f3..000000000 --- a/nova/api/openstack/incubator/volumes/volume_attachments.py +++ /dev/null @@ -1,184 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# 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 webob import exc - -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova import wsgi -from nova.api.openstack import common -from nova.api.openstack import faults - - -LOG = logging.getLogger("nova.api.volumes") - - -FLAGS = flags.FLAGS - - -def _translate_detail_view(context, volume): - """Maps keys for details view.""" - - d = _translate_summary_view(context, volume) - - # No additional data / lookups at the moment - - return d - - -def _translate_summary_view(context, vol): - """Maps keys for summary view.""" - d = {} - - volume_id = vol['id'] - - # NOTE(justinsb): We use the volume id as the id of the attachment object - d['id'] = volume_id - - d['volumeId'] = volume_id - if vol.get('instance_id'): - d['serverId'] = vol['instance_id'] - if vol.get('mountpoint'): - d['device'] = vol['mountpoint'] - - return d - - -class Controller(wsgi.Controller): - """The volume attachment API controller for the Openstack API. - - A child resource of the server. Note that we use the volume id - as the ID of the attachment (though this is not guaranteed externally) - - """ - - _serialization_metadata = { - 'application/xml': { - 'attributes': { - 'volumeAttachment': ['id', - 'serverId', - 'volumeId', - 'device']}}} - - def __init__(self): - self.compute_api = compute.API() - self.volume_api = volume.API() - super(Controller, self).__init__() - - def index(self, req, server_id): - """Returns the list of volume attachments for a given instance.""" - return self._items(req, server_id, - entity_maker=_translate_summary_view) - - def show(self, req, server_id, id): - """Return data about the given volume.""" - context = req.environ['nova.context'] - - volume_id = id - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - LOG.debug("volume_id not found") - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - return {'volumeAttachment': _translate_detail_view(context, vol)} - - def create(self, req, server_id): - """Attach a volume to an instance.""" - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - instance_id = server_id - volume_id = env['volumeAttachment']['volumeId'] - device = env['volumeAttachment']['device'] - - msg = _("Attach volume %(volume_id)s to instance %(server_id)s" - " at %(device)s") % locals() - LOG.audit(msg, context=context) - - try: - self.compute_api.attach_volume(context, - instance_id=instance_id, - volume_id=volume_id, - device=device) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - # The attach is async - attachment = {} - attachment['id'] = volume_id - attachment['volumeId'] = volume_id - - # NOTE(justinsb): And now, we have a problem... - # The attach is async, so there's a window in which we don't see - # the attachment (until the attachment completes). We could also - # get problems with concurrent requests. I think we need an - # attachment state, and to write to the DB here, but that's a bigger - # change. - # For now, we'll probably have to rely on libraries being smart - - # TODO(justinsb): How do I return "accepted" here? - return {'volumeAttachment': attachment} - - def update(self, _req, _server_id, _id): - """Update a volume attachment. We don't currently support this.""" - return faults.Fault(exc.HTTPBadRequest()) - - def delete(self, req, server_id, id): - """Detach a volume from an instance.""" - context = req.environ['nova.context'] - - volume_id = id - LOG.audit(_("Detach volume %s"), volume_id, context=context) - - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - self.compute_api.detach_volume(context, - volume_id=volume_id) - - return exc.HTTPAccepted() - - def _items(self, req, server_id, entity_maker): - """Returns a list of attachments, transformed through entity_maker.""" - context = req.environ['nova.context'] - - try: - instance = self.compute_api.get(context, server_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - volumes = instance['volumes'] - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumeAttachments': res} diff --git a/nova/api/openstack/incubator/volumes/volumes.py b/nova/api/openstack/incubator/volumes/volumes.py deleted file mode 100644 index a7d5fbaa6..000000000 --- a/nova/api/openstack/incubator/volumes/volumes.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright 2011 Justin Santa Barbara -# 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 webob import exc - -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova import wsgi -from nova.api.openstack import common -from nova.api.openstack import faults - - -LOG = logging.getLogger("nova.api.volumes") - - -FLAGS = flags.FLAGS - - -def _translate_detail_view(context, vol): - """Maps keys for details view.""" - - d = _translate_summary_view(context, vol) - - # No additional data / lookups at the moment - - return d - - -def _translate_summary_view(_context, vol): - """Maps keys for summary view.""" - d = {} - - instance_id = None - # instance_data = None - attached_to = vol.get('instance') - if attached_to: - instance_id = attached_to['id'] - # instance_data = '%s[%s]' % (instance_ec2_id, - # attached_to['host']) - d['id'] = vol['id'] - d['status'] = vol['status'] - d['size'] = vol['size'] - d['availabilityZone'] = vol['availability_zone'] - d['createdAt'] = vol['created_at'] - # if context.is_admin: - # v['status'] = '%s (%s, %s, %s, %s)' % ( - # vol['status'], - # vol['user_id'], - # vol['host'], - # instance_data, - # vol['mountpoint']) - if vol['attach_status'] == 'attached': - d['attachments'] = [{'attachTime': vol['attach_time'], - 'deleteOnTermination': False, - 'mountpoint': vol['mountpoint'], - 'instanceId': instance_id, - 'status': 'attached', - 'volumeId': vol['id']}] - else: - d['attachments'] = [{}] - - d['displayName'] = vol['display_name'] - d['displayDescription'] = vol['display_description'] - return d - - -class Controller(wsgi.Controller): - """The Volumes API controller for the OpenStack API.""" - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "volume": [ - "id", - "status", - "size", - "availabilityZone", - "createdAt", - "displayName", - "displayDescription", - ]}}} - - def __init__(self): - self.volume_api = volume.API() - super(Controller, self).__init__() - - def show(self, req, id): - """Return data about the given volume.""" - context = req.environ['nova.context'] - - try: - vol = self.volume_api.get(context, id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - return {'volume': _translate_detail_view(context, vol)} - - def delete(self, req, id): - """Delete a volume.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete volume with id: %s"), id, context=context) - - try: - self.volume_api.delete(context, volume_id=id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() - - def index(self, req): - """Returns a summary list of volumes.""" - return self._items(req, entity_maker=_translate_summary_view) - - def detail(self, req): - """Returns a detailed list of volumes.""" - return self._items(req, entity_maker=_translate_detail_view) - - def _items(self, req, entity_maker): - """Returns a list of volumes, transformed through entity_maker.""" - context = req.environ['nova.context'] - - volumes = self.volume_api.get_all(context) - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumes': res} - - def create(self, req): - """Creates a new volume.""" - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - vol = env['volume'] - size = vol['size'] - LOG.audit(_("Create volume of %s GB"), size, context=context) - new_volume = self.volume_api.create(context, size, - vol.get('display_name'), - vol.get('display_description')) - - # Work around problem that instance is lazy-loaded... - new_volume['instance'] = None - - retval = _translate_detail_view(context, new_volume) - - return {'volume': retval} diff --git a/nova/api/openstack/incubator/volumes/volumes_ext.py b/nova/api/openstack/incubator/volumes/volumes_ext.py deleted file mode 100644 index 6a3bb0265..000000000 --- a/nova/api/openstack/incubator/volumes/volumes_ext.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2011 Justin Santa Barbara -# 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.api.openstack import extensions -from nova.api.openstack.incubator.volumes import volumes -from nova.api.openstack.incubator.volumes import volume_attachments - - -class VolumesExtension(extensions.ExtensionDescriptor): - def get_name(self): - return "Volumes" - - def get_alias(self): - return "VOLUMES" - - def get_description(self): - return "Volumes support" - - def get_namespace(self): - return "http://docs.openstack.org/ext/volumes/api/v1.1" - - def get_updated(self): - return "2011-03-25T00:00:00+00:00" - - def get_resources(self): - resources = [] - - # NOTE(justinsb): No way to provide singular name ('volume') - # Does this matter? - res = extensions.ResourceExtension('volumes', - volumes.Controller(), - collection_actions={'detail': 'GET'} - ) - resources.append(res) - - res = extensions.ResourceExtension('volume_attachments', - volume_attachments.Controller(), - parent=dict( - member_name='server', - collection_name='servers')) - resources.append(res) - - return resources -- cgit From 034a841cbac8e73c55e9525df7360a068fe9d892 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 15:43:38 -0700 Subject: pep8 fixes --- nova/api/openstack/incubator/volumes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py index 7a5b2c1d0..47b86216e 100644 --- a/nova/api/openstack/incubator/volumes.py +++ b/nova/api/openstack/incubator/volumes.py @@ -339,9 +339,8 @@ class Volumes(extensions.ExtensionDescriptor): # NOTE(justinsb): No way to provide singular name ('volume') # Does this matter? res = extensions.ResourceExtension('volumes', - VolumeController(), - collection_actions= - {'detail': 'GET'} + VolumeController(), + collection_actions={'detail': 'GET'} ) resources.append(res) -- cgit From 11d258e1d8a4a78a699aa564b5f8139bf0b73db2 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 15:52:04 -0700 Subject: Added missing blank line at end of multiline docstring --- nova/api/openstack/extensions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index d1d479313..631275235 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -41,7 +41,9 @@ class ExtensionDescriptor(object): """Base class that defines the contract for extensions. Note that you don't have to derive from this class to have a valid - extension; it is purely a convenience.""" + extension; it is purely a convenience. + + """ def get_name(self): """The name of the extension. -- cgit From ded3416d48980c32eb20f95665f281ffc2927517 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 17:10:40 -0700 Subject: Removed commented-out EC2 code from volumes.py --- nova/api/openstack/incubator/volumes.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py index 47b86216e..0e6a1d0e9 100644 --- a/nova/api/openstack/incubator/volumes.py +++ b/nova/api/openstack/incubator/volumes.py @@ -49,24 +49,16 @@ def _translate_volume_summary_view(_context, vol): d = {} instance_id = None - # instance_data = None attached_to = vol.get('instance') if attached_to: instance_id = attached_to['id'] - # instance_data = '%s[%s]' % (instance_ec2_id, - # attached_to['host']) + d['id'] = vol['id'] d['status'] = vol['status'] d['size'] = vol['size'] d['availabilityZone'] = vol['availability_zone'] d['createdAt'] = vol['created_at'] - # if context.is_admin: - # v['status'] = '%s (%s, %s, %s, %s)' % ( - # vol['status'], - # vol['user_id'], - # vol['host'], - # instance_data, - # vol['mountpoint']) + if vol['attach_status'] == 'attached': d['attachments'] = [{'attachTime': vol['attach_time'], 'deleteOnTermination': False, -- cgit From 27b92e509c71a8b79dc6240aecdf598bf9d608f1 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 17:13:40 -0700 Subject: Change volume so that it returns attachments in the same format as is used for the attachment object --- nova/api/openstack/incubator/volumes.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py index 0e6a1d0e9..f96e20ed4 100644 --- a/nova/api/openstack/incubator/volumes.py +++ b/nova/api/openstack/incubator/volumes.py @@ -44,15 +44,10 @@ def _translate_volume_detail_view(context, vol): return d -def _translate_volume_summary_view(_context, vol): +def _translate_volume_summary_view(context, vol): """Maps keys for volumes summary view.""" d = {} - instance_id = None - attached_to = vol.get('instance') - if attached_to: - instance_id = attached_to['id'] - d['id'] = vol['id'] d['status'] = vol['status'] d['size'] = vol['size'] @@ -60,12 +55,7 @@ def _translate_volume_summary_view(_context, vol): d['createdAt'] = vol['created_at'] if vol['attach_status'] == 'attached': - d['attachments'] = [{'attachTime': vol['attach_time'], - 'deleteOnTermination': False, - 'mountpoint': vol['mountpoint'], - 'instanceId': instance_id, - 'status': 'attached', - 'volumeId': vol['id']}] + d['attachments'] = [_translate_attachment_detail_view(context, vol)] else: d['attachments'] = [{}] -- cgit From 86914566436d778cdae2244cb9b277e25e21cb21 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 17:22:20 -0700 Subject: Fix a docstring --- nova/api/openstack/incubator/volumes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py index f96e20ed4..6efacce52 100644 --- a/nova/api/openstack/incubator/volumes.py +++ b/nova/api/openstack/incubator/volumes.py @@ -202,7 +202,7 @@ class VolumeAttachmentController(wsgi.Controller): entity_maker=_translate_attachment_summary_view) def show(self, req, server_id, id): - """Return data about the given volume.""" + """Return data about the given volume attachment.""" context = req.environ['nova.context'] volume_id = id -- cgit From 9c8300c98239c181cc66740bf18717f0488a0743 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 18:08:17 -0700 Subject: Renamed incubator => contrib --- nova/api/openstack/contrib/__init__.py | 22 ++ nova/api/openstack/contrib/volumes.py | 336 +++++++++++++++++++++++++++++++ nova/api/openstack/extensions.py | 4 +- nova/api/openstack/incubator/__init__.py | 22 -- nova/api/openstack/incubator/volumes.py | 336 ------------------------------- 5 files changed, 360 insertions(+), 360 deletions(-) create mode 100644 nova/api/openstack/contrib/__init__.py create mode 100644 nova/api/openstack/contrib/volumes.py delete mode 100644 nova/api/openstack/incubator/__init__.py delete mode 100644 nova/api/openstack/incubator/volumes.py (limited to 'nova/api') diff --git a/nova/api/openstack/contrib/__init__.py b/nova/api/openstack/contrib/__init__.py new file mode 100644 index 000000000..e115f1ab9 --- /dev/null +++ b/nova/api/openstack/contrib/__init__.py @@ -0,0 +1,22 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License.import datetime + +"""Incubator contains extensions that are shipped with nova. + +It can't be called 'extensions' because that causes namespacing problems. + +""" diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py new file mode 100644 index 000000000..6efacce52 --- /dev/null +++ b/nova/api/openstack/contrib/volumes.py @@ -0,0 +1,336 @@ +# Copyright 2011 Justin Santa Barbara +# 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. + +"""The volumes extension.""" + +from webob import exc + +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + + +FLAGS = flags.FLAGS + + +def _translate_volume_detail_view(context, vol): + """Maps keys for volumes details view.""" + + d = _translate_volume_summary_view(context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_volume_summary_view(context, vol): + """Maps keys for volumes summary view.""" + d = {} + + d['id'] = vol['id'] + d['status'] = vol['status'] + d['size'] = vol['size'] + d['availabilityZone'] = vol['availability_zone'] + d['createdAt'] = vol['created_at'] + + if vol['attach_status'] == 'attached': + d['attachments'] = [_translate_attachment_detail_view(context, vol)] + else: + d['attachments'] = [{}] + + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +class VolumeController(wsgi.Controller): + """The Volumes API controller for the OpenStack API.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "volume": [ + "id", + "status", + "size", + "availabilityZone", + "createdAt", + "displayName", + "displayDescription", + ]}}} + + def __init__(self): + self.volume_api = volume.API() + super(VolumeController, self).__init__() + + def show(self, req, id): + """Return data about the given volume.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get(context, id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + return {'volume': _translate_volume_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a volume.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete volume with id: %s"), id, context=context) + + try: + self.volume_api.delete(context, volume_id=id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + + def index(self, req): + """Returns a summary list of volumes.""" + return self._items(req, entity_maker=_translate_volume_summary_view) + + def detail(self, req): + """Returns a detailed list of volumes.""" + return self._items(req, entity_maker=_translate_volume_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of volumes, transformed through entity_maker.""" + context = req.environ['nova.context'] + + volumes = self.volume_api.get_all(context) + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumes': res} + + def create(self, req): + """Creates a new volume.""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + vol = env['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + new_volume = self.volume_api.create(context, size, + vol.get('display_name'), + vol.get('display_description')) + + # Work around problem that instance is lazy-loaded... + new_volume['instance'] = None + + retval = _translate_volume_detail_view(context, new_volume) + + return {'volume': retval} + + +def _translate_attachment_detail_view(_context, vol): + """Maps keys for attachment details view.""" + + d = _translate_attachment_summary_view(_context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_attachment_summary_view(_context, vol): + """Maps keys for attachment summary view.""" + d = {} + + volume_id = vol['id'] + + # NOTE(justinsb): We use the volume id as the id of the attachment object + d['id'] = volume_id + + d['volumeId'] = volume_id + if vol.get('instance_id'): + d['serverId'] = vol['instance_id'] + if vol.get('mountpoint'): + d['device'] = vol['mountpoint'] + + return d + + +class VolumeAttachmentController(wsgi.Controller): + """The volume attachment API controller for the Openstack API. + + A child resource of the server. Note that we use the volume id + as the ID of the attachment (though this is not guaranteed externally) + + """ + + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'volumeAttachment': ['id', + 'serverId', + 'volumeId', + 'device']}}} + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(VolumeAttachmentController, self).__init__() + + def index(self, req, server_id): + """Returns the list of volume attachments for a given instance.""" + return self._items(req, server_id, + entity_maker=_translate_attachment_summary_view) + + def show(self, req, server_id, id): + """Return data about the given volume attachment.""" + context = req.environ['nova.context'] + + volume_id = id + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + LOG.debug("volume_id not found") + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + return {'volumeAttachment': _translate_attachment_detail_view(context, + vol)} + + def create(self, req, server_id): + """Attach a volume to an instance.""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + instance_id = server_id + volume_id = env['volumeAttachment']['volumeId'] + device = env['volumeAttachment']['device'] + + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" + " at %(device)s") % locals() + LOG.audit(msg, context=context) + + try: + self.compute_api.attach_volume(context, + instance_id=instance_id, + volume_id=volume_id, + device=device) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + # The attach is async + attachment = {} + attachment['id'] = volume_id + attachment['volumeId'] = volume_id + + # NOTE(justinsb): And now, we have a problem... + # The attach is async, so there's a window in which we don't see + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. + # For now, we'll probably have to rely on libraries being smart + + # TODO(justinsb): How do I return "accepted" here? + return {'volumeAttachment': attachment} + + def update(self, _req, _server_id, _id): + """Update a volume attachment. We don't currently support this.""" + return faults.Fault(exc.HTTPBadRequest()) + + def delete(self, req, server_id, id): + """Detach a volume from an instance.""" + context = req.environ['nova.context'] + + volume_id = id + LOG.audit(_("Detach volume %s"), volume_id, context=context) + + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + self.compute_api.detach_volume(context, + volume_id=volume_id) + + return exc.HTTPAccepted() + + def _items(self, req, server_id, entity_maker): + """Returns a list of attachments, transformed through entity_maker.""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.get(context, server_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + volumes = instance['volumes'] + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumeAttachments': res} + + +class Volumes(extensions.ExtensionDescriptor): + def get_name(self): + return "Volumes" + + def get_alias(self): + return "VOLUMES" + + def get_description(self): + return "Volumes support" + + def get_namespace(self): + return "http://docs.openstack.org/ext/volumes/api/v1.1" + + def get_updated(self): + return "2011-03-25T00:00:00+00:00" + + def get_resources(self): + resources = [] + + # NOTE(justinsb): No way to provide singular name ('volume') + # Does this matter? + res = extensions.ResourceExtension('volumes', + VolumeController(), + collection_actions={'detail': 'GET'} + ) + resources.append(res) + + res = extensions.ResourceExtension('volume_attachments', + VolumeAttachmentController(), + parent=dict( + member_name='server', + collection_name='servers')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 631275235..cba151fb6 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -378,7 +378,7 @@ class ExtensionManager(object): widgets.py the extension class within that module should be 'Widgets'. - In addition, extensions are loaded from the 'incubator' directory. + In addition, extensions are loaded from the 'contrib' directory. See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. @@ -387,7 +387,7 @@ class ExtensionManager(object): if os.path.exists(self.path): self._load_all_extensions_from_path(self.path) - incubator_path = os.path.join(os.path.dirname(__file__), "incubator") + incubator_path = os.path.join(os.path.dirname(__file__), "contrib") if os.path.exists(incubator_path): self._load_all_extensions_from_path(incubator_path) diff --git a/nova/api/openstack/incubator/__init__.py b/nova/api/openstack/incubator/__init__.py deleted file mode 100644 index e115f1ab9..000000000 --- a/nova/api/openstack/incubator/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License.import datetime - -"""Incubator contains extensions that are shipped with nova. - -It can't be called 'extensions' because that causes namespacing problems. - -""" diff --git a/nova/api/openstack/incubator/volumes.py b/nova/api/openstack/incubator/volumes.py deleted file mode 100644 index 6efacce52..000000000 --- a/nova/api/openstack/incubator/volumes.py +++ /dev/null @@ -1,336 +0,0 @@ -# Copyright 2011 Justin Santa Barbara -# 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. - -"""The volumes extension.""" - -from webob import exc - -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova import wsgi -from nova.api.openstack import common -from nova.api.openstack import extensions -from nova.api.openstack import faults - - -LOG = logging.getLogger("nova.api.volumes") - - -FLAGS = flags.FLAGS - - -def _translate_volume_detail_view(context, vol): - """Maps keys for volumes details view.""" - - d = _translate_volume_summary_view(context, vol) - - # No additional data / lookups at the moment - - return d - - -def _translate_volume_summary_view(context, vol): - """Maps keys for volumes summary view.""" - d = {} - - d['id'] = vol['id'] - d['status'] = vol['status'] - d['size'] = vol['size'] - d['availabilityZone'] = vol['availability_zone'] - d['createdAt'] = vol['created_at'] - - if vol['attach_status'] == 'attached': - d['attachments'] = [_translate_attachment_detail_view(context, vol)] - else: - d['attachments'] = [{}] - - d['displayName'] = vol['display_name'] - d['displayDescription'] = vol['display_description'] - return d - - -class VolumeController(wsgi.Controller): - """The Volumes API controller for the OpenStack API.""" - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "volume": [ - "id", - "status", - "size", - "availabilityZone", - "createdAt", - "displayName", - "displayDescription", - ]}}} - - def __init__(self): - self.volume_api = volume.API() - super(VolumeController, self).__init__() - - def show(self, req, id): - """Return data about the given volume.""" - context = req.environ['nova.context'] - - try: - vol = self.volume_api.get(context, id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - return {'volume': _translate_volume_detail_view(context, vol)} - - def delete(self, req, id): - """Delete a volume.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete volume with id: %s"), id, context=context) - - try: - self.volume_api.delete(context, volume_id=id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() - - def index(self, req): - """Returns a summary list of volumes.""" - return self._items(req, entity_maker=_translate_volume_summary_view) - - def detail(self, req): - """Returns a detailed list of volumes.""" - return self._items(req, entity_maker=_translate_volume_detail_view) - - def _items(self, req, entity_maker): - """Returns a list of volumes, transformed through entity_maker.""" - context = req.environ['nova.context'] - - volumes = self.volume_api.get_all(context) - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumes': res} - - def create(self, req): - """Creates a new volume.""" - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - vol = env['volume'] - size = vol['size'] - LOG.audit(_("Create volume of %s GB"), size, context=context) - new_volume = self.volume_api.create(context, size, - vol.get('display_name'), - vol.get('display_description')) - - # Work around problem that instance is lazy-loaded... - new_volume['instance'] = None - - retval = _translate_volume_detail_view(context, new_volume) - - return {'volume': retval} - - -def _translate_attachment_detail_view(_context, vol): - """Maps keys for attachment details view.""" - - d = _translate_attachment_summary_view(_context, vol) - - # No additional data / lookups at the moment - - return d - - -def _translate_attachment_summary_view(_context, vol): - """Maps keys for attachment summary view.""" - d = {} - - volume_id = vol['id'] - - # NOTE(justinsb): We use the volume id as the id of the attachment object - d['id'] = volume_id - - d['volumeId'] = volume_id - if vol.get('instance_id'): - d['serverId'] = vol['instance_id'] - if vol.get('mountpoint'): - d['device'] = vol['mountpoint'] - - return d - - -class VolumeAttachmentController(wsgi.Controller): - """The volume attachment API controller for the Openstack API. - - A child resource of the server. Note that we use the volume id - as the ID of the attachment (though this is not guaranteed externally) - - """ - - _serialization_metadata = { - 'application/xml': { - 'attributes': { - 'volumeAttachment': ['id', - 'serverId', - 'volumeId', - 'device']}}} - - def __init__(self): - self.compute_api = compute.API() - self.volume_api = volume.API() - super(VolumeAttachmentController, self).__init__() - - def index(self, req, server_id): - """Returns the list of volume attachments for a given instance.""" - return self._items(req, server_id, - entity_maker=_translate_attachment_summary_view) - - def show(self, req, server_id, id): - """Return data about the given volume attachment.""" - context = req.environ['nova.context'] - - volume_id = id - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - LOG.debug("volume_id not found") - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - return {'volumeAttachment': _translate_attachment_detail_view(context, - vol)} - - def create(self, req, server_id): - """Attach a volume to an instance.""" - context = req.environ['nova.context'] - - env = self._deserialize(req.body, req.get_content_type()) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - instance_id = server_id - volume_id = env['volumeAttachment']['volumeId'] - device = env['volumeAttachment']['device'] - - msg = _("Attach volume %(volume_id)s to instance %(server_id)s" - " at %(device)s") % locals() - LOG.audit(msg, context=context) - - try: - self.compute_api.attach_volume(context, - instance_id=instance_id, - volume_id=volume_id, - device=device) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - # The attach is async - attachment = {} - attachment['id'] = volume_id - attachment['volumeId'] = volume_id - - # NOTE(justinsb): And now, we have a problem... - # The attach is async, so there's a window in which we don't see - # the attachment (until the attachment completes). We could also - # get problems with concurrent requests. I think we need an - # attachment state, and to write to the DB here, but that's a bigger - # change. - # For now, we'll probably have to rely on libraries being smart - - # TODO(justinsb): How do I return "accepted" here? - return {'volumeAttachment': attachment} - - def update(self, _req, _server_id, _id): - """Update a volume attachment. We don't currently support this.""" - return faults.Fault(exc.HTTPBadRequest()) - - def delete(self, req, server_id, id): - """Detach a volume from an instance.""" - context = req.environ['nova.context'] - - volume_id = id - LOG.audit(_("Detach volume %s"), volume_id, context=context) - - try: - vol = self.volume_api.get(context, volume_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - if str(vol['instance_id']) != server_id: - LOG.debug("instance_id != server_id") - return faults.Fault(exc.HTTPNotFound()) - - self.compute_api.detach_volume(context, - volume_id=volume_id) - - return exc.HTTPAccepted() - - def _items(self, req, server_id, entity_maker): - """Returns a list of attachments, transformed through entity_maker.""" - context = req.environ['nova.context'] - - try: - instance = self.compute_api.get(context, server_id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - volumes = instance['volumes'] - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumeAttachments': res} - - -class Volumes(extensions.ExtensionDescriptor): - def get_name(self): - return "Volumes" - - def get_alias(self): - return "VOLUMES" - - def get_description(self): - return "Volumes support" - - def get_namespace(self): - return "http://docs.openstack.org/ext/volumes/api/v1.1" - - def get_updated(self): - return "2011-03-25T00:00:00+00:00" - - def get_resources(self): - resources = [] - - # NOTE(justinsb): No way to provide singular name ('volume') - # Does this matter? - res = extensions.ResourceExtension('volumes', - VolumeController(), - collection_actions={'detail': 'GET'} - ) - resources.append(res) - - res = extensions.ResourceExtension('volume_attachments', - VolumeAttachmentController(), - parent=dict( - member_name='server', - collection_name='servers')) - resources.append(res) - - return resources -- cgit From be8bf22f90e322823cb3cf4963f5c7313ef727ec Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 18:08:36 -0700 Subject: Removed unused super_verbose argument left over from previous code --- nova/api/openstack/extensions.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index cba151fb6..6813d85c9 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -315,8 +315,6 @@ class ExtensionManager(object): def __init__(self, path): LOG.audit(_('Initializing extension manager.')) - self.super_verbose = False - self.path = path self.extensions = {} self._load_all_extensions() -- cgit From 2315682856f420ff0b781bead142e1aff82071a4 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Tue, 29 Mar 2011 18:16:09 -0700 Subject: "Incubator" is no more. Long live "contrib" --- nova/api/openstack/contrib/__init__.py | 2 +- nova/api/openstack/extensions.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/contrib/__init__.py b/nova/api/openstack/contrib/__init__.py index e115f1ab9..b42a1d89d 100644 --- a/nova/api/openstack/contrib/__init__.py +++ b/nova/api/openstack/contrib/__init__.py @@ -15,7 +15,7 @@ # License for the specific language governing permissions and limitations # under the License.import datetime -"""Incubator contains extensions that are shipped with nova. +"""Contrib contains extensions that are shipped with nova. It can't be called 'extensions' because that causes namespacing problems. diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index 6813d85c9..fb1dccb28 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -385,9 +385,9 @@ class ExtensionManager(object): if os.path.exists(self.path): self._load_all_extensions_from_path(self.path) - incubator_path = os.path.join(os.path.dirname(__file__), "contrib") - if os.path.exists(incubator_path): - self._load_all_extensions_from_path(incubator_path) + contrib_path = os.path.join(os.path.dirname(__file__), "contrib") + if os.path.exists(contrib_path): + self._load_all_extensions_from_path(contrib_path) def _load_all_extensions_from_path(self, path): for f in os.listdir(path): -- cgit