From 47bc72e5ec27bec349dcfc9468af6325f0a51019 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Wed, 12 Jan 2011 12:10:26 -0600 Subject: Start to add rescue/unrescue support --- nova/api/openstack/__init__.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index f96e2af91..a9b01548a 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -95,6 +95,8 @@ class APIRouter(wsgi.Router): server_members["actions"] = "GET" server_members['suspend'] = 'POST' server_members['resume'] = 'POST' + server_members['rescue'] = 'POST' + server_members['unrescue'] = 'POST' mapper.resource("server", "servers", controller=servers.Controller(), collection={'detail': 'GET'}, -- cgit From 7f2a4fdf5e43620081e163fc46f2ca4fdefd18f3 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Wed, 12 Jan 2011 15:07:51 -0600 Subject: Make rescue/unrescue available to API --- nova/api/openstack/servers.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 29af82533..3a6c61a3a 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -283,6 +283,28 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + def rescue(self, req, id): + """Permit users to rescue the server.""" + context = req.environ["nova.context"] + try: + self.compute_api.rescue(context, id) + except: + readable = traceback.format_exc() + LOG.exception(_("compute.api::rescue %s"), readable) + return faults.Fault(exc.HTTPUnprocessableEntity()) + return exc.HTTPAccepted() + + def unrescue(self, req, id): + """Permit users to unrescue the server.""" + context = req.environ["nova.context"] + try: + self.compute_api.unrescue(context, id) + except: + readable = traceback.format_exc() + LOG.exception(_("compute.api::unrescue %s"), readable) + return faults.Fault(exc.HTTPUnprocessableEntity()) + return exc.HTTPAccepted() + def get_ajax_console(self, req, id): """ Returns a url to an instance's ajaxterm console. """ try: -- cgit From 2c0f1d78927c14f1d155e617a066b09a00acb100 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 18 Jan 2011 17:40:36 -0600 Subject: Basic stubbing throughout the stack --- nova/api/openstack/servers.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 8cbcebed2..06104b37e 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -182,9 +182,44 @@ class Controller(wsgi.Controller): return exc.HTTPNoContent() def action(self, req, id): - """ Multi-purpose method used to reboot, rebuild, and + """ Multi-purpose method used to reboot, rebuild, or resize a server """ + + actions = { + 'reboot':self._action_reboot, + 'resize':self._action_resize, + 'confirmResize':self._action_confirm_resize, + 'revertResize':self._action_revert_resize, + 'rebuild':self._action_rebuild + } + input_dict = self._deserialize(req.body, req) + for key in actions.keys(): + if key in input_dict: + return actions[key](input_dict, req, id) + return faults.Fault(exc.HTTPNotImplemented()) + + def _action_confirm_resize(self, input_dict, req, id): + return fault.Fault(exc.HTTPNotImplemented()) + + def _action_revert_resize(self, input_dict, req, id): + return fault.Fault(exc.HTTPNotImplemented()) + + def _action_rebuild(self, input_dict, req, id): + return fault.Fault(exc.HTTPNotImplemented()) + + def _action_resize(self, input_dict, req, id): + """ Resizes a given instance to the flavor size requested """ + try: + resize_flavor = input_dict['resize']['flavorId'] + self.compute_api.resize(req.environ['nova.context'], id, + flavor_id) + except: + return faults.Fault(exc.HTTPUnprocessableEntity()) + return fault.Fault(exc.HTTPAccepted()) + + + def _action_reboot(self, input_dict, req, id): #TODO(sandy): rebuild/resize not supported. try: reboot_type = input_dict['reboot']['type'] -- cgit From 9be0770208b0e75c7d93ba10165b82d5be11be27 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Thu, 3 Feb 2011 17:57:46 -0800 Subject: flagged all INSTANCE_TYPES usage with FIXME comment. Added basic usage to nova-manage (needs formatting). created api methods. --- nova/api/ec2/admin.py | 1 + nova/api/openstack/flavors.py | 2 ++ 2 files changed, 3 insertions(+) (limited to 'nova/api') diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index d7e899d12..55cca1041 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -79,6 +79,7 @@ class AdminController(object): def __str__(self): return 'AdminController' + # FIX-ME(kpepple) for dynamic flavors def describe_instance_types(self, _context, **_kwargs): return {'instanceTypeSet': [instance_dict(n, v) for n, v in instance_types.INSTANCE_TYPES.iteritems()]} diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index f620d4107..1f5185134 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -45,6 +45,7 @@ class Controller(wsgi.Controller): def show(self, req, id): """Return data about the given flavor id.""" + # FIX-ME(kpepple) for dynamic flavors for name, val in instance_types.INSTANCE_TYPES.iteritems(): if val['flavorid'] == int(id): item = dict(ram=val['memory_mb'], disk=val['local_gb'], @@ -54,4 +55,5 @@ class Controller(wsgi.Controller): def _all_ids(self): """Return the list of all flavorids.""" + # FIX-ME(kpepple) for dynamic flavors return [i['flavorid'] for i in instance_types.INSTANCE_TYPES.values()] -- cgit From 25a5afbb783e28bd5303853bf09e4b254c938302 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Sat, 5 Feb 2011 01:14:45 -0800 Subject: added FIXME(kpepple) comments for all constant usage of INSTANCE_TYPES. updated api/ec2/admin.py to use the new instance_types db table --- nova/api/ec2/admin.py | 19 ++++++++++++++----- nova/api/openstack/flavors.py | 4 ++-- 2 files changed, 16 insertions(+), 7 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index 55cca1041..5a9e22bf7 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -63,8 +63,8 @@ def host_dict(host): return {} -def instance_dict(name, inst): - return {'name': name, +def instance_dict(inst): + return {'name': inst['name'], 'memory_mb': inst['memory_mb'], 'vcpus': inst['vcpus'], 'disk_gb': inst['local_gb'], @@ -79,10 +79,19 @@ class AdminController(object): def __str__(self): return 'AdminController' - # FIX-ME(kpepple) for dynamic flavors + # FIXME(kpepple) this is untested code path. def describe_instance_types(self, _context, **_kwargs): - return {'instanceTypeSet': [instance_dict(n, v) for n, v in - instance_types.INSTANCE_TYPES.iteritems()]} + """Returns all active instance types data (vcpus, memory, etc.)""" + # return {'instanceTypeSet': [instance_dict(n, v) for n, v in + # instance_types.INSTANCE_TYPES.iteritems()]} + return {'instanceTypeSet': + [for i in db.instance_type_get_all(): instance_dict(i)]} + + # FIXME(kpepple) this is untested code path. + def describe_instance_type(self, _context, name, **_kwargs): + """Returns a specific active instance types data""" + return {'instanceTypeSet': + [instance_dict(db.instance_type_get_by_name(name))]} def describe_user(self, _context, name, **_kwargs): """Returns user data, including access and secret keys.""" diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index 1f5185134..2416088f9 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -45,7 +45,7 @@ class Controller(wsgi.Controller): def show(self, req, id): """Return data about the given flavor id.""" - # FIX-ME(kpepple) for dynamic flavors + # FIXME(kpepple) for dynamic flavors for name, val in instance_types.INSTANCE_TYPES.iteritems(): if val['flavorid'] == int(id): item = dict(ram=val['memory_mb'], disk=val['local_gb'], @@ -55,5 +55,5 @@ class Controller(wsgi.Controller): def _all_ids(self): """Return the list of all flavorids.""" - # FIX-ME(kpepple) for dynamic flavors + # FIXME(kpepple) for dynamic flavors return [i['flavorid'] for i in instance_types.INSTANCE_TYPES.values()] -- cgit From 79ea4533df3bd8c58b96177c2979fab2987a842a Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Sat, 5 Feb 2011 02:45:53 -0800 Subject: converted openstack flavors over to use instance_types table. a few pep changes. --- nova/api/openstack/flavors.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index 2416088f9..7440af0b4 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -17,6 +17,8 @@ from webob import exc +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 @@ -45,15 +47,19 @@ class Controller(wsgi.Controller): def show(self, req, id): """Return data about the given flavor id.""" - # FIXME(kpepple) for dynamic flavors - for name, val in instance_types.INSTANCE_TYPES.iteritems(): - if val['flavorid'] == int(id): - item = dict(ram=val['memory_mb'], disk=val['local_gb'], - id=val['flavorid'], name=name) - return dict(flavor=item) + # FIXME(kpepple) do we need admin context here ? + ctxt = context.get_admin_context() + val = db.instance_type_get_by_flavor_id(ctxt, id) + item = dict(ram=val['memory_mb'], disk=val['local_gb'], + id=val['flavorid'], name=val['name']) + return dict(flavor=item) raise faults.Fault(exc.HTTPNotFound()) def _all_ids(self): """Return the list of all flavorids.""" - # FIXME(kpepple) for dynamic flavors - return [i['flavorid'] for i in instance_types.INSTANCE_TYPES.values()] + # FIXME(kpepple) do we need admin context here ? + ctxt = context.get_admin_context() + flavor_ids = [] + for i in db.instance_type_get_all(ctxt): + flavor_ids.append(i['flavorid']) + return flavor_ids -- cgit From fcd0a7b245470054718c94adf0da6a528a01f173 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Sat, 5 Feb 2011 13:49:38 -0800 Subject: corrected db.instance_types to return expect dict instead of lists. updated openstack flavors to expect dicts instead of lists. added deleted column to returned dict. --- nova/api/openstack/flavors.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index 7440af0b4..da38dd34d 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -17,7 +17,7 @@ from webob import exc -from nova import db +from nova import db from nova import context from nova.api.openstack import faults from nova.api.openstack import common @@ -50,8 +50,9 @@ class Controller(wsgi.Controller): # FIXME(kpepple) do we need admin context here ? ctxt = context.get_admin_context() val = db.instance_type_get_by_flavor_id(ctxt, id) - item = dict(ram=val['memory_mb'], disk=val['local_gb'], - id=val['flavorid'], name=val['name']) + v = val.values()[0] + item = dict(ram=v['memory_mb'], disk=v['local_gb'], + id=v['flavorid'], name=val.keys()[0]) return dict(flavor=item) raise faults.Fault(exc.HTTPNotFound()) @@ -60,6 +61,7 @@ class Controller(wsgi.Controller): # FIXME(kpepple) do we need admin context here ? ctxt = context.get_admin_context() flavor_ids = [] - for i in db.instance_type_get_all(ctxt): - flavor_ids.append(i['flavorid']) + inst_types = db.instance_type_get_all(ctxt) + for i in inst_types.keys(): + flavor_ids.append(inst_types[i]['flavorid']) return flavor_ids -- cgit From ea5271ed69d72dcab8189c3bfc66220c7ff60862 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Sun, 6 Feb 2011 10:56:05 -0800 Subject: refactor to remove ugly code in flavors --- nova/api/openstack/flavors.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index da38dd34d..3124c26b2 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -47,9 +47,10 @@ class Controller(wsgi.Controller): def show(self, req, id): """Return data about the given flavor id.""" - # FIXME(kpepple) do we need admin context here ? + # FIXME(kpepple) do we really need admin context here ? ctxt = context.get_admin_context() val = db.instance_type_get_by_flavor_id(ctxt, id) + # FIXME(kpepple) refactor db call to return dict v = val.values()[0] item = dict(ram=v['memory_mb'], disk=v['local_gb'], id=v['flavorid'], name=val.keys()[0]) @@ -58,10 +59,8 @@ class Controller(wsgi.Controller): def _all_ids(self): """Return the list of all flavorids.""" - # FIXME(kpepple) do we need admin context here ? + # FIXME(kpepple) do we really need admin context here ? ctxt = context.get_admin_context() - flavor_ids = [] inst_types = db.instance_type_get_all(ctxt) - for i in inst_types.keys(): - flavor_ids.append(inst_types[i]['flavorid']) + flavor_ids = [inst_types[i]['flavorid'] for i in inst_types.keys()] return flavor_ids -- cgit From 7dcdbcc546248c3384bd15975a721413e1d1f507 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Sun, 6 Feb 2011 13:28:07 -0800 Subject: simplified instance_types db calls to return entire row - we may need these extra columns for some features and there seems to be little downside in including them. still need to fix testing calls. --- nova/api/ec2/admin.py | 5 +++-- nova/api/openstack/flavors.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index 5a9e22bf7..4c0628e21 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -84,8 +84,9 @@ class AdminController(object): """Returns all active instance types data (vcpus, memory, etc.)""" # return {'instanceTypeSet': [instance_dict(n, v) for n, v in # instance_types.INSTANCE_TYPES.iteritems()]} - return {'instanceTypeSet': - [for i in db.instance_type_get_all(): instance_dict(i)]} + # return {'instanceTypeSet': + # [for i in db.instance_type_get_all(): instance_dict(i)]} + return {'instanceTypeSet': [db.instance_type_get_all(_context)]} # FIXME(kpepple) this is untested code path. def describe_instance_type(self, _context, name, **_kwargs): diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index 3124c26b2..9b674afbd 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -49,12 +49,12 @@ class Controller(wsgi.Controller): """Return data about the given flavor id.""" # FIXME(kpepple) do we really need admin context here ? ctxt = context.get_admin_context() - val = db.instance_type_get_by_flavor_id(ctxt, id) + values = db.instance_type_get_by_flavor_id(ctxt, id) # FIXME(kpepple) refactor db call to return dict - v = val.values()[0] - item = dict(ram=v['memory_mb'], disk=v['local_gb'], - id=v['flavorid'], name=val.keys()[0]) - return dict(flavor=item) + # v = val.values()[0] + # item = dict(ram=v['memory_mb'], disk=v['local_gb'], + # id=v['flavorid'], name=val.keys()[0]) + return dict(flavor=values) raise faults.Fault(exc.HTTPNotFound()) def _all_ids(self): -- cgit From a70ac6609713f2b610923a7ae382208f4d46b74a Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 10 Feb 2011 15:01:38 -0600 Subject: Typo fixes and some stupidity about the models --- nova/api/openstack/servers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 61dd3be36..06a40e92c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -207,27 +207,26 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotImplemented()) def _action_confirm_resize(self, input_dict, req, id): - return fault.Fault(exc.HTTPNotImplemented()) + return faults.Fault(exc.HTTPNotImplemented()) def _action_revert_resize(self, input_dict, req, id): - return fault.Fault(exc.HTTPNotImplemented()) + return faults.Fault(exc.HTTPNotImplemented()) def _action_rebuild(self, input_dict, req, id): - return fault.Fault(exc.HTTPNotImplemented()) + return faults.Fault(exc.HTTPNotImplemented()) def _action_resize(self, input_dict, req, id): """ Resizes a given instance to the flavor size requested """ try: - resize_flavor = input_dict['resize']['flavorId'] + flavor_id = input_dict['resize']['flavorId'] self.compute_api.resize(req.environ['nova.context'], id, flavor_id) except: return faults.Fault(exc.HTTPUnprocessableEntity()) - return fault.Fault(exc.HTTPAccepted()) + return faults.Fault(exc.HTTPAccepted()) def _action_reboot(self, input_dict, req, id): - #TODO(sandy): rebuild/resize not supported. try: reboot_type = input_dict['reboot']['type'] except Exception: -- cgit From 41e4e18a0324593c0076c3936d63bb6dcca487cb Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Mon, 14 Feb 2011 23:12:34 +0000 Subject: First cut on XenServer unified-images --- nova/api/openstack/servers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 17c5519a1..1c9dcd9a6 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -140,7 +140,11 @@ class Controller(wsgi.Controller): image_id = str(image_id) image = self._image_service.show(req.environ['nova.context'], image_id) - return lookup('kernel_id'), lookup('ramdisk_id') + disk_format = image['properties'].get('disk_format', None) + if disk_format == "vhd": + return None, None + else: + return lookup('kernel_id'), lookup('ramdisk_id') def create(self, req): """ Creates a new server for a given user """ -- cgit From 0020f14f43aa6f024d9aab7dc67c79caaaeb8257 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Tue, 15 Feb 2011 11:15:59 -0800 Subject: zone/info works --- nova/api/openstack/__init__.py | 6 +++--- nova/api/openstack/zones.py | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 33d040ab3..95fce7f84 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -76,13 +76,13 @@ class APIRouter(wsgi.Router): LOG.debug(_("Including admin operations in API.")) server_members['pause'] = 'POST' server_members['unpause'] = 'POST' - server_members["diagnostics"] = "GET" - server_members["actions"] = "GET" + server_members['diagnostics'] = 'GET' + server_members['actions'] = 'GET' server_members['suspend'] = 'POST' server_members['resume'] = 'POST' mapper.resource("zone", "zones", controller=zones.Controller(), - collection={'detail': 'GET'}) + collection={'detail': 'GET', 'info': 'GET'}), mapper.resource("server", "servers", controller=servers.Controller(), collection={'detail': 'GET'}, diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index 830464ffd..16e5e366b 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -42,7 +42,7 @@ class Controller(wsgi.Controller): _serialization_metadata = { 'application/xml': { "attributes": { - "zone": ["id", "api_url"]}}} + "zone": ["id", "api_url", "name", "capabilities"]}}} def index(self, req): """Return all zones in brief""" @@ -55,6 +55,11 @@ class Controller(wsgi.Controller): """Return all zones in detail""" return self.index(req) + def info(self, req): + """Return name and capabilities for this zone.""" + return dict(zone=dict(name=FLAGS.zone_name, + capabilities=FLAGS.zone_capabilities)) + def show(self, req, id): """Return data about the given zone id""" zone_id = int(id) -- cgit From 1d72b9d3ddc835d788ba1fec1a937c2788e94b38 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Tue, 15 Feb 2011 21:36:13 +0000 Subject: Using Nova style nokernel --- nova/api/openstack/servers.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 1c9dcd9a6..cd0cea235 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -138,13 +138,7 @@ class Controller(wsgi.Controller): _("%(param)s property not found for image %(_image_id)s") % locals()) - image_id = str(image_id) - image = self._image_service.show(req.environ['nova.context'], image_id) - disk_format = image['properties'].get('disk_format', None) - if disk_format == "vhd": - return None, None - else: - return lookup('kernel_id'), lookup('ramdisk_id') + return lookup('kernel_id'), lookup('ramdisk_id') def create(self, req): """ Creates a new server for a given user """ -- cgit From c33378fbbe0fd76e807530522715ba4175af18d8 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Tue, 15 Feb 2011 21:54:42 +0000 Subject: Fixing typo --- nova/api/openstack/servers.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index cd0cea235..c15e499a0 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -129,8 +129,12 @@ class Controller(wsgi.Controller): Machine images are associated with Kernels and Ramdisk images via metadata stored in Glance as 'image_properties' """ - def lookup(param): - _image_id = image_id + # FIXME(sirp): Currently Nova requires us to specify the `null_kernel` + # identifier ('nokernel') to indicate a RAW (or VHD) image. It would + # be better if we could omit the kernel_id and ramdisk_id properties + # on the image + def lookup(image, param): + _image_id = image.id try: return image['properties'][param] except KeyError: @@ -138,7 +142,8 @@ class Controller(wsgi.Controller): _("%(param)s property not found for image %(_image_id)s") % locals()) - return lookup('kernel_id'), lookup('ramdisk_id') + image = self._image_service.show(req.environ['nova.context'], image_id) + return lookup(image, 'kernel_id'), lookup(image, 'ramdisk_id') def create(self, req): """ Creates a new server for a given user """ -- cgit From 98b038c6878772f6b272cb169b1c74bd7c9838b8 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Tue, 15 Feb 2011 23:56:00 -0600 Subject: Foo --- nova/api/openstack/servers.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 06a40e92c..83b421127 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -207,10 +207,18 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotImplemented()) def _action_confirm_resize(self, input_dict, req, id): - return faults.Fault(exc.HTTPNotImplemented()) + try: + self.compute_api.confirm_resize(req.environ['nova.context'], id) + except: + return faults.Fault(exc.HTTPBadRequest()) + return exc.HTTPNoContent() def _action_revert_resize(self, input_dict, req, id): - return faults.Fault(exc.HTTPNotImplemented()) + try: + self.compute_api.confirm_resize(req.environ['nova.context'], id) + except: + return faults.Fault(exc.HTTPBadRequest()) + return exc.HTTPAccepted() def _action_rebuild(self, input_dict, req, id): return faults.Fault(exc.HTTPNotImplemented()) -- cgit From 163e81ac2bc2f9945273b0659ceb473767e5b19f Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Wed, 16 Feb 2011 11:53:50 -0500 Subject: This implements the blueprint 'Openstack API support for hostId': https://blueprints.launchpad.net/nova/+spec/openstack-api-hostid Now instances will have a unique hostId which for now is just a hash of the host. If the instance does not have a host yet, the hostId will be ''. --- nova/api/openstack/servers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 17c5519a1..58eda53b9 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import hashlib import json import traceback @@ -65,7 +66,11 @@ def _translate_detail_keys(inst): inst_dict['status'] = power_mapping[inst_dict['status']] inst_dict['addresses'] = dict(public=[], private=[]) inst_dict['metadata'] = {} - inst_dict['hostId'] = '' + + if inst['host']: + inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() + else: + inst_dict['hostId'] = '' return dict(server=inst_dict) -- cgit From 4375069b6635d6ccd87231cb7d9f5b17708ffb1a Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Wed, 16 Feb 2011 11:11:49 -0600 Subject: Stubbed out flavor create/delete API calls --- nova/api/openstack/flavors.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index 9b674afbd..215f0b8a6 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -42,7 +42,6 @@ class Controller(wsgi.Controller): def detail(self, req): """Return all flavors in detail.""" items = [self.show(req, id)['flavor'] for id in self._all_ids()] - items = common.limited(items, req) return dict(flavors=items) def show(self, req, id): @@ -57,6 +56,14 @@ class Controller(wsgi.Controller): return dict(flavor=values) raise faults.Fault(exc.HTTPNotFound()) + def create(self, req): + """Create a flavor.""" + print "CREATE! %s" % req + + def delete(self, req, id): + """Delete a flavor.""" + print "DELETE! %s %s" % (req, id) + def _all_ids(self): """Return the list of all flavorids.""" # FIXME(kpepple) do we really need admin context here ? -- cgit From 8f206774ee75c2d96c15dd2c604ae5da9601d91f Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 16 Feb 2011 17:02:57 -0600 Subject: Better exceptions --- nova/api/openstack/servers.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 83b421127..2fc105d07 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -209,15 +209,17 @@ class Controller(wsgi.Controller): def _action_confirm_resize(self, input_dict, req, id): try: self.compute_api.confirm_resize(req.environ['nova.context'], id) - except: - return faults.Fault(exc.HTTPBadRequest()) + except Exception, e: + LOG.exception(_("Error in confirm-resize %s"), e) + return faults.Fault(exc.HTTPBadRequest(e)) return exc.HTTPNoContent() def _action_revert_resize(self, input_dict, req, id): try: self.compute_api.confirm_resize(req.environ['nova.context'], id) - except: - return faults.Fault(exc.HTTPBadRequest()) + except Exception, e: + LOG.exception(_("Error in revert-resize %s"), e) + return faults.Fault(exc.HTTPBadRequest(e)) return exc.HTTPAccepted() def _action_rebuild(self, input_dict, req, id): @@ -229,8 +231,9 @@ class Controller(wsgi.Controller): flavor_id = input_dict['resize']['flavorId'] self.compute_api.resize(req.environ['nova.context'], id, flavor_id) - except: - return faults.Fault(exc.HTTPUnprocessableEntity()) + except Exception, e: + LOG.exception(_("Error in resize %s"), e) + return faults.Fault(exc.HTTPUnprocessableEntity(e)) return faults.Fault(exc.HTTPAccepted()) -- cgit From ce847afcc1e24463d7aa522f227a08193c72fcc0 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Wed, 16 Feb 2011 19:12:44 -0500 Subject: Moved definition of return_servers_with_host stub to inside the test_get_all_server_details_with_host test. --- 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 58eda53b9..323e6fda6 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -67,10 +67,9 @@ def _translate_detail_keys(inst): inst_dict['addresses'] = dict(public=[], private=[]) inst_dict['metadata'] = {} + inst_dict['hostId'] = '' if inst['host']: inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() - else: - inst_dict['hostId'] = '' return dict(server=inst_dict) -- cgit From 9d056b6fadcefed9ef9573bd89125b00af5e2726 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 17 Feb 2011 10:50:49 -0600 Subject: More testing --- nova/api/openstack/__init__.py | 1 + nova/api/openstack/flavors.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 056c7dd27..1fafebe37 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -73,6 +73,7 @@ class APIRouter(wsgi.Router): server_members = {'action': 'POST'} if FLAGS.allow_admin_api: LOG.debug(_("Including admin operations in API.")) + server_members['pause'] = 'POST' server_members['unpause'] = 'POST' server_members["diagnostics"] = "GET" diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index 215f0b8a6..a3a664506 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -58,11 +58,21 @@ class Controller(wsgi.Controller): def create(self, req): """Create a flavor.""" + instance_types.create( + name, + memory, + vcpus, + local_gb, + flavor_id, + swap, + rxtx_quota, + rxtx_cap) print "CREATE! %s" % req - def delete(self, req, id): + def delete(self, req, name): """Delete a flavor.""" - print "DELETE! %s %s" % (req, id) + instance_type.destroy(name) + print "DELETE! %s %s" % (req, name) def _all_ids(self): """Return the list of all flavorids.""" -- cgit From e67927c181a1f24df35a6df5663e397e260979cf Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 17 Feb 2011 13:28:39 -0600 Subject: Foo --- 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 2fc105d07..5f369463d 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -216,7 +216,7 @@ class Controller(wsgi.Controller): def _action_revert_resize(self, input_dict, req, id): try: - self.compute_api.confirm_resize(req.environ['nova.context'], id) + self.compute_api.revert_resize(req.environ['nova.context'], id) except Exception, e: LOG.exception(_("Error in revert-resize %s"), e) return faults.Fault(exc.HTTPBadRequest(e)) -- cgit From aa71a25c9f9bf5df3aea781138fa8d69654f06d9 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Thu, 17 Feb 2011 12:12:19 -0800 Subject: zone list now comes from scheduler zonemanager --- nova/api/openstack/zones.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index 16e5e366b..bd2c488d9 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -19,6 +19,7 @@ import logging from nova import flags from nova import wsgi from nova import db +from nova import rpc FLAGS = flags.FLAGS @@ -33,6 +34,10 @@ def _filter_keys(item, keys): return dict((k, v) for k, v in item.iteritems() if k in keys) +def _exclude_keys(item, keys): + return dict((k, v) for k, v in item.iteritems() if k not in keys) + + def _scrub_zone(zone): return _filter_keys(zone, ('id', 'api_url')) @@ -44,11 +49,34 @@ class Controller(wsgi.Controller): "attributes": { "zone": ["id", "api_url", "name", "capabilities"]}}} + def _call_scheduler(self, method, context, params=None): + """Generic handler for RPC calls to the scheduler. + + :param params: Optional dictionary of arguments to be passed to the + scheduler worker + + :retval: Result returned by scheduler worker + """ + if not params: + params = {} + queue = FLAGS.scheduler_topic + kwargs = {'method': method, 'args': params} + return rpc.call(context, queue, kwargs) + def index(self, req): """Return all zones in brief""" - items = db.zone_get_all(req.environ['nova.context']) + # Ask the ZoneManager in the Scheduler for most recent data. + items = self._call_scheduler('get_zone_list', + req.environ['nova.context']) + for item in items: + item['api_url'] = item['api_url'].replace('\\/', '/') + + # Or fall-back to the database ... + if len(items) == 0: + items = db.zone_get_all(req.environ['nova.context']) items = common.limited(items, req) - items = [_scrub_zone(item) for item in items] + items = [_exclude_keys(item, ['username', 'password']) + for item in items] return dict(zones=items) def detail(self, req): -- cgit From e77f8751dd59e5d650d047a6711c3d137947dda7 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Thu, 17 Feb 2011 16:18:03 -0400 Subject: fixup --- nova/api/openstack/zones.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index bd2c488d9..f75176824 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -66,7 +66,7 @@ class Controller(wsgi.Controller): def index(self, req): """Return all zones in brief""" # Ask the ZoneManager in the Scheduler for most recent data. - items = self._call_scheduler('get_zone_list', + items = self._call_scheduler('get_zone_list', req.environ['nova.context']) for item in items: item['api_url'] = item['api_url'].replace('\\/', '/') @@ -75,7 +75,7 @@ class Controller(wsgi.Controller): if len(items) == 0: items = db.zone_get_all(req.environ['nova.context']) items = common.limited(items, req) - items = [_exclude_keys(item, ['username', 'password']) + items = [_exclude_keys(item, ['username', 'password']) for item in items] return dict(zones=items) @@ -85,7 +85,7 @@ class Controller(wsgi.Controller): def info(self, req): """Return name and capabilities for this zone.""" - return dict(zone=dict(name=FLAGS.zone_name, + return dict(zone=dict(name=FLAGS.zone_name, capabilities=FLAGS.zone_capabilities)) def show(self, req, id): -- cgit From aa53c9476ed37f0a1359413d4a710eb08c997b06 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 17 Feb 2011 14:42:01 -0600 Subject: Finished flavor OS API stubs --- nova/api/openstack/flavors.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index a3a664506..375e12b18 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -58,21 +58,23 @@ class Controller(wsgi.Controller): def create(self, req): """Create a flavor.""" - instance_types.create( - name, - memory, - vcpus, - local_gb, - flavor_id, - swap, - rxtx_quota, - rxtx_cap) - print "CREATE! %s" % req + #TODO(jk0): Finish this later + #instance_types.create( + # name, + # memory, + # vcpus, + # local_gb, + # flavor_id, + # swap, + # rxtx_quota, + # rxtx_cap) + return "CREATE! %s" % req - def delete(self, req, name): + def delete(self, req, id): """Delete a flavor.""" - instance_type.destroy(name) - print "DELETE! %s %s" % (req, name) + #TODO(jk0): Finish this later + #instance_type.destroy(name) + return "DELETE! %s %s" % (req, id) def _all_ids(self): """Return the list of all flavorids.""" -- cgit From 3f3dddee0245cb143004dfb8c20204c511bec658 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 17 Feb 2011 16:52:31 -0600 Subject: a few changes and a bunch of unit tests --- nova/api/openstack/servers.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index fd6b10d5b..a719f5e15 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -227,7 +227,7 @@ class Controller(wsgi.Controller): self.compute_api.confirm_resize(req.environ['nova.context'], id) except Exception, e: LOG.exception(_("Error in confirm-resize %s"), e) - return faults.Fault(exc.HTTPBadRequest(e)) + return faults.Fault(exc.HTTPBadRequest()) return exc.HTTPNoContent() def _action_revert_resize(self, input_dict, req, id): @@ -235,7 +235,7 @@ class Controller(wsgi.Controller): self.compute_api.revert_resize(req.environ['nova.context'], id) except Exception, e: LOG.exception(_("Error in revert-resize %s"), e) - return faults.Fault(exc.HTTPBadRequest(e)) + return faults.Fault(exc.HTTPBadRequest()) return exc.HTTPAccepted() def _action_rebuild(self, input_dict, req, id): @@ -244,12 +244,16 @@ class Controller(wsgi.Controller): def _action_resize(self, input_dict, req, id): """ Resizes a given instance to the flavor size requested """ try: - flavor_id = input_dict['resize']['flavorId'] - self.compute_api.resize(req.environ['nova.context'], id, - flavor_id) + if 'resize' in input_dict and 'flavorId' in input_dict['resize']: + flavor_id = input_dict['resize']['flavorId'] + self.compute_api.resize(req.environ['nova.context'], id, + flavor_id) + else: + LOG.exception(_("Missing arguments for resize")) + return faults.Fault(exc.HTTPUnprocessableEntity()) except Exception, e: LOG.exception(_("Error in resize %s"), e) - return faults.Fault(exc.HTTPUnprocessableEntity(e)) + return faults.Fault(exc.HTTPBadRequest()) return faults.Fault(exc.HTTPAccepted()) -- cgit From c884064e7a9af04b2ebdbbb9ee32318a00716412 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 18 Feb 2011 12:08:35 -0400 Subject: fixups backed on merge comments --- nova/api/openstack/zones.py | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index f75176824..24a4444f7 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -1,4 +1,4 @@ -# Copyright 2010 OpenStack LLC. +# Copyright 2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -20,6 +20,7 @@ from nova import flags from nova import wsgi from nova import db from nova import rpc +from nova.scheduler.api import API FLAGS = flags.FLAGS @@ -49,31 +50,14 @@ class Controller(wsgi.Controller): "attributes": { "zone": ["id", "api_url", "name", "capabilities"]}}} - def _call_scheduler(self, method, context, params=None): - """Generic handler for RPC calls to the scheduler. - - :param params: Optional dictionary of arguments to be passed to the - scheduler worker - - :retval: Result returned by scheduler worker - """ - if not params: - params = {} - queue = FLAGS.scheduler_topic - kwargs = {'method': method, 'args': params} - return rpc.call(context, queue, kwargs) - def index(self, req): """Return all zones in brief""" - # Ask the ZoneManager in the Scheduler for most recent data. - items = self._call_scheduler('get_zone_list', - req.environ['nova.context']) - for item in items: - item['api_url'] = item['api_url'].replace('\\/', '/') - - # Or fall-back to the database ... - if len(items) == 0: + # 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] -- cgit From 18e573a14414838f11e772edca3eb5510f852c94 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 18 Feb 2011 17:45:57 -0400 Subject: sandy y u no read hacking guide and import classes? --- 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 24a4444f7..99be0ba02 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -19,8 +19,7 @@ import logging from nova import flags from nova import wsgi from nova import db -from nova import rpc -from nova.scheduler.api import API +from nova.scheduler import api FLAGS = flags.FLAGS @@ -54,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().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']) -- cgit From a43c5929de7ebf58eb9ecb8416ce3cf4194c176a Mon Sep 17 00:00:00 2001 From: Cerberus Date: Fri, 18 Feb 2011 16:13:34 -0600 Subject: Pep8 cleanup --- nova/api/openstack/servers.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index a719f5e15..f68c97323 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -209,12 +209,12 @@ class Controller(wsgi.Controller): resize a server """ actions = { - 'reboot':self._action_reboot, - 'resize':self._action_resize, - 'confirmResize':self._action_confirm_resize, - 'revertResize':self._action_revert_resize, - 'rebuild':self._action_rebuild - } + 'reboot': self._action_reboot, + 'resize': self._action_resize, + 'confirmResize': self._action_confirm_resize, + 'revertResize': self._action_revert_resize, + 'rebuild': self._action_rebuild, + } input_dict = self._deserialize(req.body, req) for key in actions.keys(): @@ -256,7 +256,6 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPBadRequest()) return faults.Fault(exc.HTTPAccepted()) - def _action_reboot(self, input_dict, req, id): try: reboot_type = input_dict['reboot']['type'] -- cgit 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 e3d6dc70a6b77d80afcf87473bc79549540ac4ce Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Fri, 25 Feb 2011 02:51:14 +0000 Subject: Removing unecessary nokernel stuff --- nova/api/openstack/servers.py | 51 ++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 22 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index c8c94f1fd..f51da0cdd 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -136,28 +136,6 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() - def _get_kernel_ramdisk_from_image(self, req, image_id): - """ - Machine images are associated with Kernels and Ramdisk images via - metadata stored in Glance as 'image_properties' - """ - # FIXME(sirp): Currently Nova requires us to specify the `null_kernel` - # identifier ('nokernel') to indicate a RAW (or VHD) image. It would - # be better if we could omit the kernel_id and ramdisk_id properties - # on the image - def lookup(image, param): - _image_id = image['id'] - try: - return image['properties'][param] - except KeyError: - LOG.debug( - _("%(param)s property not found for image %(_image_id)s") % - locals()) - return None - - image = self._image_service.show(req.environ['nova.context'], image_id) - return lookup(image, 'kernel_id'), lookup(image, 'ramdisk_id') - def create(self, req): """ Creates a new server for a given user """ env = self._deserialize(req.body, req) @@ -367,3 +345,32 @@ class Controller(wsgi.Controller): action=item.action, error=item.error)) 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. + """ + # 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) + + if image['type'] != 'machine': + return None, None + + try: + kernel_id = image['properties']['kernel_id'] + except KeyError: + raise exception.NotFound( + _("Kernel not found for image %(image_id)s") % locals()) + + try: + ramdisk_id = image['properties']['ramdisk_id'] + except KeyError: + raise exception.NotFound( + _("Ramdisk not found for image %(image_id)s") % locals()) + + return kernel_id, ramdisk_id -- cgit From 2218cb025adca1ded3e6596acc182b88742e3a51 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Thu, 24 Feb 2011 21:59:36 -0500 Subject: Rename auth_token db methods to follow standard. --- nova/api/openstack/auth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 1dfdd5318..c844c6231 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -103,11 +103,11 @@ class AuthMiddleware(wsgi.Middleware): 2 days ago. """ ctxt = context.get_admin_context() - token = self.db.auth_get_token(ctxt, token_hash) + token = self.db.auth_token_get(ctxt, token_hash) if token: delta = datetime.datetime.now() - token.created_at if delta.days >= 2: - self.db.auth_destroy_token(ctxt, token) + self.db.auth_token_destroy(ctxt, token) else: return self.auth.get_user(token.user_id) return None @@ -131,6 +131,6 @@ class AuthMiddleware(wsgi.Middleware): token_dict['server_management_url'] = req.url token_dict['storage_url'] = '' token_dict['user_id'] = user.id - token = self.db.auth_create_token(ctxt, token_dict) + token = self.db.auth_token_create(ctxt, token_dict) return token, user return None, None -- cgit From 865c3d57f8b84dfcc493ecead12816874b160e35 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Thu, 24 Feb 2011 23:51:17 -0500 Subject: Pass id of token to be deleted to the db api, not the actual object. --- nova/api/openstack/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index c844c6231..dff69a7f2 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -107,7 +107,7 @@ class AuthMiddleware(wsgi.Middleware): if token: delta = datetime.datetime.now() - token.created_at if delta.days >= 2: - self.db.auth_token_destroy(ctxt, token) + self.db.auth_token_destroy(ctxt, token.id) else: return self.auth.get_user(token.user_id) return None -- cgit From 079b532a1080da9fe5d99e90fa9c60d16506de06 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Fri, 25 Feb 2011 16:24:51 +0000 Subject: Verify status of image is active --- nova/api/openstack/servers.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index f51da0cdd..bf5663f60 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -358,6 +358,11 @@ class Controller(wsgi.Controller): # kernel_id and ramdisk_id image = self._image_service.show(req.environ['nova.context'], image_id) + if image['status'] != 'active': + raise exception.Invalid( + _("Cannot build from image %(image_id)s, status not active") % + locals()) + if image['type'] != 'machine': return None, None -- cgit From 4453021476fac599c0cee126b6eaa426d4878145 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Fri, 18 Mar 2011 02:09:46 +0000 Subject: Copy over to current trunk my tests, the 401/500 fix, and a couple of fixes to the committed fix which was actually brittle around the edges... --- nova/api/openstack/auth.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index dff69a7f2..6011e6115 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -26,6 +26,7 @@ import webob.dec from nova import auth from nova import context from nova import db +from nova import exception from nova import flags from nova import manager from nova import utils @@ -103,11 +104,14 @@ class AuthMiddleware(wsgi.Middleware): 2 days ago. """ ctxt = context.get_admin_context() - token = self.db.auth_token_get(ctxt, token_hash) + try: + token = self.db.auth_token_get(ctxt, token_hash) + except exception.NotFound: + return None if token: delta = datetime.datetime.now() - token.created_at if delta.days >= 2: - self.db.auth_token_destroy(ctxt, token.id) + self.db.auth_token_destroy(ctxt, token.token_hash) else: return self.auth.get_user(token.user_id) return None -- cgit From 05a96b320cf1d6b911b0edb11df0ed408a894e77 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 28 Feb 2011 14:49:03 -0500 Subject: Edited `nova.api.openstack.common:limited` method to raise an HTTPBadRequest exception if a negative limit or offset is given. I'm not confident that this is the correct approach, because I guess this method could be called out of an API/WSGI context, but the method *is* located in the OpenStack API module and is currently only used in WSGI-capable methods, so we should be safe. --- nova/api/openstack/common.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 1dc3767e2..9f85c5c8a 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +import webob.exc + from nova import exception @@ -27,7 +29,8 @@ def limited(items, request, max_limit=1000): GET variables. 'offset' is where to start in the list, and 'limit' is the maximum number of items to return. If 'limit' is not specified, 0, or > max_limit, we default - to max_limit. + to max_limit. Negative values for either offset or limit + will cause exc.HTTPBadRequest() exceptions to be raised. @kwarg max_limit: The maximum number of items to return from 'items' """ try: @@ -40,6 +43,9 @@ def limited(items, request, max_limit=1000): except ValueError: limit = max_limit + if offset < 0 or limit < 0: + raise webob.exc.HTTPBadRequest() + limit = min(max_limit, limit or max_limit) range_end = offset + limit return items[offset:range_end] -- cgit From 4572ffcf734b734870b90497063fc27e7642f67c Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Mon, 28 Feb 2011 19:56:46 -0500 Subject: No reason to initialize metadata twice. --- nova/api/openstack/servers.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 7d20f681b..69273ad7b 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -72,8 +72,6 @@ def _translate_detail_keys(inst): public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') inst_dict['addresses']['public'] = public_ips - inst_dict['metadata'] = {} - # Return the metadata as a dictionary metadata = {} for item in inst['metadata']: -- cgit From 69779dcdc5584fafa95974f263cef14912a12ed7 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Mon, 28 Feb 2011 17:06:35 -0800 Subject: refactored adminclient --- nova/api/ec2/admin.py | 12 ------------ 1 file changed, 12 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index d867e5da9..51a06bb26 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -29,7 +29,6 @@ from nova import flags from nova import log as logging from nova import utils from nova.auth import manager -from nova.compute import instance_types FLAGS = flags.FLAGS @@ -115,21 +114,10 @@ class AdminController(object): def __str__(self): return 'AdminController' - # FIXME(kpepple) this is untested code path. def describe_instance_types(self, _context, **_kwargs): """Returns all active instance types data (vcpus, memory, etc.)""" - # return {'instanceTypeSet': [instance_dict(n, v) for n, v in - # instance_types.INSTANCE_TYPES.iteritems()]} - # return {'instanceTypeSet': - # [for i in db.instance_type_get_all(): instance_dict(i)]} return {'instanceTypeSet': [db.instance_type_get_all(_context)]} - # FIXME(kpepple) this is untested code path. - def describe_instance_type(self, _context, name, **_kwargs): - """Returns a specific active instance types data""" - return {'instanceTypeSet': - [instance_dict(db.instance_type_get_by_name(name))]} - def describe_user(self, _context, name, **_kwargs): """Returns user data, including access and secret keys.""" return user_dict(manager.AuthManager().get_user(name)) -- cgit From f9d08c16d5c620c711d962a78be3a94b99364f14 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Tue, 1 Mar 2011 13:56:33 -0500 Subject: support adding a single personality in the osapi --- nova/api/openstack/servers.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 73c7bfe17..92e5c9024 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -80,7 +80,6 @@ def _translate_detail_keys(inst): return dict(server=inst_dict) - def _translate_keys(inst): """ Coerces into dictionary format, excluding all model attributes save for id and name """ @@ -154,6 +153,22 @@ class Controller(wsgi.Controller): image = self._image_service.show(req.environ['nova.context'], image_id) return lookup('kernel_id'), lookup('ramdisk_id') + + def _get_onset_files_from_personality_attr(self, personality_attr): + """ + Create a list of onset files from the personality request attribute + + At this time, onset_files must be formatted as a list of + (file_path, file_content) pairs for compatibility with the + underlying compute service. + """ + onset_files = [] + for personality in personality_attr: + path = personality['path'] + contents = personality['contents'] + onset_files.append((path, contents)) + return onset_files + def create(self, req): """ Creates a new server for a given user """ env = self._deserialize(req.body, req) @@ -181,6 +196,9 @@ class Controller(wsgi.Controller): for k, v in env['server']['metadata'].items(): metadata.append({'key': k, 'value': v}) + personality = env['server'].get('personality', []) + onset_files = self._get_onset_files_from_personality_attr(personality) + instances = self.compute_api.create( context, instance_types.get_by_flavor_id(env['server']['flavorId']), @@ -192,7 +210,7 @@ class Controller(wsgi.Controller): key_name=key_pair['name'], key_data=key_pair['public_key'], metadata=metadata, - onset_files=env.get('onset_files', [])) + onset_files=onset_files) return _translate_keys(instances[0]) def update(self, req, id): -- cgit From 94e42c3002f9043fc3c5b90a1cb5ad0c50ba261b Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Tue, 1 Mar 2011 16:21:37 -0500 Subject: ensure personality contents are b64 encoded --- nova/api/openstack/servers.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 8491fe697..8908bbdca 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import base64 import hashlib import json import traceback @@ -138,7 +139,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() - def _get_onset_files_from_personality_attr(self, personality_attr): + def _get_onset_files_from_personality(self, personality): """ Create a list of onset files from the personality request attribute @@ -147,9 +148,13 @@ class Controller(wsgi.Controller): underlying compute service. """ onset_files = [] - for personality in personality_attr: - path = personality['path'] - contents = personality['contents'] + for item in personality: + path = item['path'] + try: + contents = base64.b64decode(item['contents']) + except TypeError: + raise exc.HTTPBadRequest(explanation= + 'Personality content for %s cannot be decoded' % path) onset_files.append((path, contents)) return onset_files @@ -181,7 +186,7 @@ class Controller(wsgi.Controller): metadata.append({'key': k, 'value': v}) personality = env['server'].get('personality', []) - onset_files = self._get_onset_files_from_personality_attr(personality) + onset_files = self._get_onset_files_from_personality(personality) instances = self.compute_api.create( context, -- 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 58ac632f8e08b248d234deffdb56fe3a33a25130 Mon Sep 17 00:00:00 2001 From: Masanori Itoh Date: Thu, 3 Mar 2011 00:12:48 +0900 Subject: Port Todd's lp720157 fix to the current trunk, rev 752. --- nova/api/ec2/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'nova/api') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 5adc2c075..5a63dc8da 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -198,6 +198,12 @@ class Requestify(wsgi.Middleware): try: # Raise KeyError if omitted action = req.params['Action'] + # Fix bug lp:720157 for older (version 1) clients + version = req.params['SignatureVersion'] + if int(version) == 1: + non_args.remove('SignatureMethod') + if 'SignatureMethod' in args: + args.pop('SignatureMethod') for non_arg in non_args: # Remove, but raise KeyError if omitted args.pop(non_arg) -- cgit From f617fc087367a3d65bd4b826bf735f65fec9d2fd Mon Sep 17 00:00:00 2001 From: Masanori Itoh Date: Thu, 3 Mar 2011 00:28:04 +0900 Subject: Change DescribeKeyPairs response tag from keypairsSet to keySet, and fix lp720133. --- 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 844ccbe5e..c6309f03c 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -298,7 +298,7 @@ class CloudController(object): 'keyFingerprint': key_pair['fingerprint'], }) - return {'keypairsSet': result} + return {'keySet': result} def create_key_pair(self, context, key_name, **kwargs): LOG.audit(_("Create key pair %s"), key_name, context=context) -- cgit From 077a77a1ab6fbec468b36e2975c1e185235c17ff Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Wed, 2 Mar 2011 15:49:51 -0800 Subject: removed create and delete method (and corresponding tests) from flavors.py --- nova/api/openstack/flavors.py | 20 -------------------- 1 file changed, 20 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index 375e12b18..4db812c2d 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -56,26 +56,6 @@ class Controller(wsgi.Controller): return dict(flavor=values) raise faults.Fault(exc.HTTPNotFound()) - def create(self, req): - """Create a flavor.""" - #TODO(jk0): Finish this later - #instance_types.create( - # name, - # memory, - # vcpus, - # local_gb, - # flavor_id, - # swap, - # rxtx_quota, - # rxtx_cap) - return "CREATE! %s" % req - - def delete(self, req, id): - """Delete a flavor.""" - #TODO(jk0): Finish this later - #instance_type.destroy(name) - return "DELETE! %s %s" % (req, id) - def _all_ids(self): """Return the list of all flavorids.""" # FIXME(kpepple) do we really need admin context here ? -- cgit From 0bf74ef365688476b2b3a44e353c0062989d33b5 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Wed, 2 Mar 2011 16:12:22 -0800 Subject: fixed _context typo --- nova/api/ec2/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index 51a06bb26..d9a4ef999 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -114,9 +114,9 @@ class AdminController(object): def __str__(self): return 'AdminController' - def describe_instance_types(self, _context, **_kwargs): + 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': [db.instance_type_get_all(context)]} def describe_user(self, _context, name, **_kwargs): """Returns user data, including access and secret keys.""" -- cgit From 86aed7edae3dd90741d0da704a99460701b8bcc7 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Wed, 2 Mar 2011 16:32:09 -0800 Subject: added in req.environ for context --- nova/api/openstack/flavors.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index 4db812c2d..f3d040ba3 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -41,25 +41,19 @@ class Controller(wsgi.Controller): def detail(self, req): """Return all flavors in detail.""" - items = [self.show(req, id)['flavor'] for id in self._all_ids()] + items = [self.show(req, id)['flavor'] for id in self._all_ids(req)] return dict(flavors=items) def show(self, req, id): """Return data about the given flavor id.""" - # FIXME(kpepple) do we really need admin context here ? - ctxt = context.get_admin_context() + ctxt = req.environ['nova.context'] values = db.instance_type_get_by_flavor_id(ctxt, id) - # FIXME(kpepple) refactor db call to return dict - # v = val.values()[0] - # item = dict(ram=v['memory_mb'], disk=v['local_gb'], - # id=v['flavorid'], name=val.keys()[0]) return dict(flavor=values) raise faults.Fault(exc.HTTPNotFound()) - def _all_ids(self): + def _all_ids(self, req): """Return the list of all flavorids.""" - # FIXME(kpepple) do we really need admin context here ? - ctxt = context.get_admin_context() + ctxt = req.environ['nova.context'] inst_types = db.instance_type_get_all(ctxt) flavor_ids = [inst_types[i]['flavorid'] for i in inst_types.keys()] - return flavor_ids + return sorted(flavor_ids) -- cgit From 668cdc96b3f6fb412b9d1d4a3780744d6b2340b1 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Thu, 3 Mar 2011 00:59:09 -0500 Subject: more rigorous testing and error handling for os api personality --- nova/api/openstack/servers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 8908bbdca..73c787828 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -149,9 +149,13 @@ class Controller(wsgi.Controller): """ onset_files = [] for item in personality: - path = item['path'] try: - contents = base64.b64decode(item['contents']) + path = item['path'] + contents = item['contents'] + except TypeError: + raise exc.HTTPBadRequest(explanation='Bad personality format') + try: + contents = base64.b64decode(contents) except TypeError: raise exc.HTTPBadRequest(explanation= 'Personality content for %s cannot be decoded' % path) -- cgit From 6797c5acc47fb5111ef821d6b074cb635692a9fb Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Thu, 3 Mar 2011 15:41:45 +0000 Subject: Add in multi-tenant support in openstack api. --- nova/api/openstack/__init__.py | 25 +++++++++ nova/api/openstack/accounts.py | 73 ++++++++++++++++++++++++++ nova/api/openstack/auth.py | 54 +++++++++++++++++--- nova/api/openstack/backup_schedules.py | 6 +-- nova/api/openstack/consoles.py | 10 ++-- nova/api/openstack/flavors.py | 6 +-- nova/api/openstack/images.py | 12 ++--- nova/api/openstack/servers.py | 38 +++++++------- nova/api/openstack/shared_ip_groups.py | 12 ++--- nova/api/openstack/users.py | 93 ++++++++++++++++++++++++++++++++++ nova/api/openstack/zones.py | 12 ++--- 11 files changed, 286 insertions(+), 55 deletions(-) create mode 100644 nova/api/openstack/accounts.py create mode 100644 nova/api/openstack/users.py (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index b1b38ed2d..73d52192e 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -27,6 +27,7 @@ import webob.exc from nova import flags from nova import log as logging from nova import wsgi +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 @@ -34,6 +35,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 users from nova.api.openstack import zones @@ -71,6 +73,18 @@ class APIRouter(wsgi.Router): def __init__(self): mapper = routes.Mapper() + accounts_controller = accounts.Controller() + mapper.connect("account", "/{id}", + controller=accounts_controller, action="show", + conditions=dict(method=["GET"])) + if FLAGS.allow_admin_api: + mapper.connect("/{id}", + controller=accounts_controller, action="update", + conditions=dict(method=["PUT"])) + mapper.connect("/{id}", + controller=accounts_controller, action="delete", + conditions=dict(method=["DELETE"])) + server_members = {'action': 'POST'} if FLAGS.allow_admin_api: LOG.debug(_("Including admin operations in API.")) @@ -84,27 +98,38 @@ class APIRouter(wsgi.Router): server_members['inject_network_info'] = 'POST' mapper.resource("zone", "zones", controller=zones.Controller(), + path_prefix="{account_id}/", + collection={'detail': 'GET'}) + + mapper.resource("user", "users", controller=users.Controller(), + path_prefix="{account_id}/", collection={'detail': 'GET'}) mapper.resource("server", "servers", controller=servers.Controller(), collection={'detail': 'GET'}, + path_prefix="{account_id}/", member=server_members) mapper.resource("backup_schedule", "backup_schedule", controller=backup_schedules.Controller(), + path_prefix="{account_id}/servers/{server_id}/", parent_resource=dict(member_name='server', collection_name='servers')) mapper.resource("console", "consoles", controller=consoles.Controller(), + path_prefix="{account_id}/servers/{server_id}/", parent_resource=dict(member_name='server', collection_name='servers')) mapper.resource("image", "images", controller=images.Controller(), + path_prefix="{account_id}/", collection={'detail': 'GET'}) mapper.resource("flavor", "flavors", controller=flavors.Controller(), + path_prefix="{account_id}/", collection={'detail': 'GET'}) mapper.resource("shared_ip_group", "shared_ip_groups", + path_prefix="{account_id}/", collection={'detail': 'GET'}, controller=shared_ip_groups.Controller()) diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py new file mode 100644 index 000000000..264fdab99 --- /dev/null +++ b/nova/api/openstack/accounts.py @@ -0,0 +1,73 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import common + +from nova import exception +from nova import flags +from nova import log as logging +from nova import wsgi + +from nova.auth import manager + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.api.openstack') + + +def _translate_keys(account): + return dict(id=account.id, + name=account.name, + description=account.description, + manager=account.project_manager_id) + + +class Controller(wsgi.Controller): + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "account": ["id", "name", "description", "manager"]}}} + + def __init__(self): + self.manager = manager.AuthManager() + + def _check_admin(self, context): + """ We cannot depend on the db layer to check for admin access + for the auth manager, so we do it here """ + if not context.is_admin: + raise exception.NotAuthorized("Not admin user.") + + def show(self, req, id): + """Return data about the given account id""" + account = self.manager.get_project(id) + return dict(account=_translate_keys(account)) + + def delete(self, req, id): + self._check_admin(req.environ['nova.context']) + self.manager.delete_project(id) + return {} + + def update(self, req, id): + """ This is really create or update. """ + self._check_admin(req.environ['nova.context']) + env = self._deserialize(req.body, req) + description = env['account'].get('description') + manager = env['account'].get('manager') + try: + account = self.manager.get_project(id) + self.manager.modify_project(id, manager, description) + except exception.NotFound: + account = self.manager.create_project(id, manager, description) + return dict(account=_translate_keys(account)) diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 6011e6115..e77910fed 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -28,11 +28,13 @@ from nova import context from nova import db from nova import exception from nova import flags +from nova import log as logging from nova import manager from nova import utils from nova import wsgi from nova.api.openstack import faults +LOG = logging.getLogger('nova.api.openstack') FLAGS = flags.FLAGS @@ -50,14 +52,27 @@ class AuthMiddleware(wsgi.Middleware): def __call__(self, req): if not self.has_authentication(req): return self.authenticate(req) - user = self.get_user_by_authentication(req) + account_name = req.path_info_peek() if not user: return faults.Fault(webob.exc.HTTPUnauthorized()) - project = self.auth.get_project(FLAGS.default_project) - req.environ['nova.context'] = context.RequestContext(user, project) + if not account_name: + if self.auth.is_admin(user): + account_name = FLAGS.default_project + else: + return faults.Fault(webob.exc.HTTPUnauthorized()) + try: + account = self.auth.get_project(account_name) + except exception.NotFound: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + if not self.auth.is_admin(user) and \ + not self.auth.is_project_member(user, account): + return faults.Fault(webob.exc.HTTPUnauthorized()) + + req.environ['nova.context'] = context.RequestContext(user, account) return self.application def has_authentication(self, req): @@ -70,6 +85,7 @@ class AuthMiddleware(wsgi.Middleware): # Unless the request is explicitly made against // don't # honor it path_info = req.path_info + account_name = None if len(path_info) > 1: return faults.Fault(webob.exc.HTTPUnauthorized()) @@ -79,7 +95,10 @@ class AuthMiddleware(wsgi.Middleware): except KeyError: return faults.Fault(webob.exc.HTTPUnauthorized()) - token, user = self._authorize_user(username, key, req) + if ':' in username: + account_name, username = username.rsplit(':', 1) + + token, user = self._authorize_user(username, account_name, key, req) if user and token: res = webob.Response() res.headers['X-Auth-Token'] = token.token_hash @@ -116,23 +135,44 @@ class AuthMiddleware(wsgi.Middleware): return self.auth.get_user(token.user_id) return None - def _authorize_user(self, username, key, req): + def _authorize_user(self, username, account_name, key, req): """Generates a new token and assigns it to a user. username - string + account_name - string key - string API key req - webob.Request object """ ctxt = context.get_admin_context() user = self.auth.get_user_from_access_key(key) + if account_name: + try: + account = self.auth.get_project(account_name) + except exception.NotFound: + return None, None + else: + # (dragondm) punt and try to determine account. + # this is something of a hack, but a user on 1 account is a + # common case, and is the way the current RS code works. + accounts = self.auth.get_projects(user=user) + if len(accounts) == 1: + account = accounts[0] + else: + #we can't tell what account they are logging in for. + return None, None + if user and user.name == username: token_hash = hashlib.sha1('%s%s%f' % (username, key, time.time())).hexdigest() token_dict = {} token_dict['token_hash'] = token_hash token_dict['cdn_management_url'] = '' - # Same as auth url, e.g. http://foo.org:8774/baz/v1.0 - token_dict['server_management_url'] = req.url + # auth url + project (account) id, e.g. + # http://foo.org:8774/baz/v1.0/myacct/ + os_url = '%s%s%s/' % (req.url, + '' if req.url.endswith('/') else '/', + account.id) + token_dict['server_management_url'] = os_url token_dict['storage_url'] = '' token_dict['user_id'] = user.id token = self.db.auth_token_create(ctxt, token_dict) diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py index 7abb5f884..a4d5939df 100644 --- a/nova/api/openstack/backup_schedules.py +++ b/nova/api/openstack/backup_schedules.py @@ -40,15 +40,15 @@ class Controller(wsgi.Controller): def __init__(self): pass - def index(self, req, server_id): + def index(self, req, server_id, **kw): """ Returns the list of backup schedules for a given instance """ return _translate_keys({}) - def create(self, req, server_id): + def create(self, req, server_id, **kw): """ No actual update method required, since the existing API allows both create and update through a POST """ return faults.Fault(exc.HTTPNotImplemented()) - def delete(self, req, server_id, id): + def delete(self, req, server_id, id, **kw): """ Deletes an existing backup schedule """ return faults.Fault(exc.HTTPNotImplemented()) diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py index 9ebdbe710..85b2a4140 100644 --- a/nova/api/openstack/consoles.py +++ b/nova/api/openstack/consoles.py @@ -55,7 +55,7 @@ class Controller(wsgi.Controller): self.console_api = console.API() super(Controller, self).__init__() - def index(self, req, server_id): + def index(self, req, server_id, **kw): """Returns a list of consoles for this instance""" consoles = self.console_api.get_consoles( req.environ['nova.context'], @@ -63,14 +63,14 @@ class Controller(wsgi.Controller): return dict(consoles=[_translate_keys(console) for console in consoles]) - def create(self, req, server_id): + def create(self, req, server_id, **kw): """Creates a new console""" #info = self._deserialize(req.body, req) self.console_api.create_console( req.environ['nova.context'], int(server_id)) - def show(self, req, server_id, id): + def show(self, req, server_id, id, **kw): """Shows in-depth information on a specific console""" try: console = self.console_api.get_console( @@ -81,11 +81,11 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return _translate_detail_keys(console) - def update(self, req, server_id, id): + def update(self, req, server_id, id, **kw): """You can't update a console""" raise faults.Fault(exc.HTTPNotImplemented()) - def delete(self, req, server_id, id): + def delete(self, req, server_id, id, **kw): """Deletes a console""" try: self.console_api.delete_console(req.environ['nova.context'], diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index f620d4107..79c3e1ab3 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -32,18 +32,18 @@ class Controller(wsgi.Controller): "attributes": { "flavor": ["id", "name", "ram", "disk"]}}} - def index(self, req): + def index(self, req, **kw): """Return all flavors in brief.""" return dict(flavors=[dict(id=flavor['id'], name=flavor['name']) for flavor in self.detail(req)['flavors']]) - def detail(self, req): + def detail(self, req, **kw): """Return all flavors in detail.""" items = [self.show(req, id)['flavor'] for id in self._all_ids()] items = common.limited(items, req) return dict(flavors=items) - def show(self, req, id): + def show(self, req, id, **kw): """Return data about the given flavor id.""" for name, val in instance_types.INSTANCE_TYPES.iteritems(): if val['flavorid'] == int(id): diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index cf85a496f..5bc5b9978 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -115,14 +115,14 @@ class Controller(wsgi.Controller): def __init__(self): self._service = utils.import_object(FLAGS.image_service) - def index(self, req): + def index(self, req, **kw): """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) - def detail(self, req): + def detail(self, req, **kw): """Return all public images in detail""" try: items = self._service.detail(req.environ['nova.context']) @@ -136,7 +136,7 @@ class Controller(wsgi.Controller): items = [_translate_status(item) for item in items] return dict(images=items) - def show(self, req, id): + def show(self, req, id, **kw): """Return data about the given image id""" image_id = common.get_image_id_from_image_hash(self._service, req.environ['nova.context'], id) @@ -145,11 +145,11 @@ class Controller(wsgi.Controller): _convert_image_id_to_hash(image) return dict(image=image) - def delete(self, req, id): + def delete(self, req, id, **kw): # Only public images are supported for now. raise faults.Fault(exc.HTTPNotFound()) - def create(self, req): + def create(self, req, **kw): context = req.environ['nova.context'] env = self._deserialize(req.body, req) instance_id = env["image"]["serverId"] @@ -160,7 +160,7 @@ class Controller(wsgi.Controller): return dict(image=image_meta) - def update(self, req, id): + def update(self, req, id, **kw): # Users may not modify public images, and that's all that # we support for now. raise faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 69273ad7b..426de92be 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -105,11 +105,11 @@ class Controller(wsgi.Controller): self._image_service = utils.import_object(FLAGS.image_service) super(Controller, self).__init__() - def index(self, req): + def index(self, req, **kw): """ Returns a list of server names and ids for a given user """ return self._items(req, entity_maker=_translate_keys) - def detail(self, req): + def detail(self, req, **kw): """ Returns a list of server details for a given user """ return self._items(req, entity_maker=_translate_detail_keys) @@ -123,7 +123,7 @@ class Controller(wsgi.Controller): res = [entity_maker(inst)['server'] for inst in limited_list] return dict(servers=res) - def show(self, req, id): + def show(self, req, id, **kw): """ Returns server details by server id """ try: instance = self.compute_api.get(req.environ['nova.context'], id) @@ -131,7 +131,7 @@ class Controller(wsgi.Controller): except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) - def delete(self, req, id): + def delete(self, req, id, **kw): """ Destroys a server """ try: self.compute_api.delete(req.environ['nova.context'], id) @@ -139,7 +139,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() - def create(self, req): + def create(self, req, **kw): """ Creates a new server for a given user """ env = self._deserialize(req.body, req) if not env: @@ -180,7 +180,7 @@ class Controller(wsgi.Controller): onset_files=env.get('onset_files', [])) return _translate_keys(instances[0]) - def update(self, req, id): + def update(self, req, id, **kw): """ Updates the server name or password """ inst_dict = self._deserialize(req.body, req) if not inst_dict: @@ -202,7 +202,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPNoContent() - def action(self, req, id): + def action(self, req, id, **kw): """ Multi-purpose method used to reboot, rebuild, and resize a server """ input_dict = self._deserialize(req.body, req) @@ -219,7 +219,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def lock(self, req, id): + def lock(self, req, id, **kw): """ lock the instance with id admin only operation @@ -234,7 +234,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def unlock(self, req, id): + def unlock(self, req, id, **kw): """ unlock the instance with id admin only operation @@ -249,7 +249,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def get_lock(self, req, id): + def get_lock(self, req, id, **kw): """ return the boolean state of (instance with id)'s lock @@ -263,7 +263,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def reset_network(self, req, id): + def reset_network(self, req, id, **kw): """ Reset networking on an instance (admin only). @@ -277,7 +277,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def inject_network_info(self, req, id): + def inject_network_info(self, req, id, **kw): """ Inject network info for an instance (admin only). @@ -291,7 +291,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def pause(self, req, id): + def pause(self, req, id, **kw): """ Permit Admins to Pause the server. """ ctxt = req.environ['nova.context'] try: @@ -302,7 +302,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def unpause(self, req, id): + def unpause(self, req, id, **kw): """ Permit Admins to Unpause the server. """ ctxt = req.environ['nova.context'] try: @@ -313,7 +313,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def suspend(self, req, id): + def suspend(self, req, id, **kw): """permit admins to suspend the server""" context = req.environ['nova.context'] try: @@ -324,7 +324,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def resume(self, req, id): + def resume(self, req, id, **kw): """permit admins to resume the server from suspend""" context = req.environ['nova.context'] try: @@ -335,7 +335,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def get_ajax_console(self, req, id): + def get_ajax_console(self, req, id, **kw): """ Returns a url to an instance's ajaxterm console. """ try: self.compute_api.get_ajax_console(req.environ['nova.context'], @@ -344,12 +344,12 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() - def diagnostics(self, req, id): + def diagnostics(self, req, id, **kw): """Permit Admins to retrieve server diagnostics.""" ctxt = req.environ["nova.context"] return self.compute_api.get_diagnostics(ctxt, id) - def actions(self, req, id): + def actions(self, req, id, **kw): """Permit Admins to retrieve server actions.""" ctxt = req.environ["nova.context"] items = self.compute_api.get_actions(ctxt, id) diff --git a/nova/api/openstack/shared_ip_groups.py b/nova/api/openstack/shared_ip_groups.py index 5d78f9377..e3c917749 100644 --- a/nova/api/openstack/shared_ip_groups.py +++ b/nova/api/openstack/shared_ip_groups.py @@ -40,26 +40,26 @@ class Controller(wsgi.Controller): 'attributes': { 'sharedIpGroup': []}}} - def index(self, req): + def index(self, req, **kw): """ Returns a list of Shared IP Groups for the user """ return dict(sharedIpGroups=[]) - def show(self, req, id): + def show(self, req, id, **kw): """ Shows in-depth information on a specific Shared IP Group """ return _translate_keys({}) - def update(self, req, id): + def update(self, req, id, **kw): """ You can't update a Shared IP Group """ raise faults.Fault(exc.HTTPNotImplemented()) - def delete(self, req, id): + def delete(self, req, id, **kw): """ Deletes a Shared IP Group """ raise faults.Fault(exc.HTTPNotImplemented()) - def detail(self, req): + def detail(self, req, **kw): """ Returns a complete list of Shared IP Groups """ return _translate_detail_keys({}) - def create(self, req): + def create(self, req, **kw): """ Creates a new Shared IP group """ raise faults.Fault(exc.HTTPNotImplemented()) diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py new file mode 100644 index 000000000..c0b7544f9 --- /dev/null +++ b/nova/api/openstack/users.py @@ -0,0 +1,93 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import common + +from nova import exception +from nova import flags +from nova import log as logging +from nova import wsgi + +from nova.auth import manager + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.api.openstack') + + +def _translate_keys(user): + return dict(id=user.id, + name=user.name, + access=user.access, + secret=user.secret, + admin=user.admin) + + +class Controller(wsgi.Controller): + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "user": ["id", "name", "access", "secret", "admin"]}}} + + def __init__(self): + self.manager = manager.AuthManager() + + def _check_admin(self, context): + """ We cannot depend on the db layer to check for admin access + for the auth manager, so we do it here """ + if not context.is_admin: + raise exception.NotAuthorized("Not admin user") + + def index(self, req, **kw): + """Return all users in brief""" + users = self.manager.get_users() + users = common.limited(users, req) + users = [_translate_keys(user) for user in users] + return dict(users=users) + + def detail(self, req, **kw): + """Return all users in detail""" + return self.index(req) + + def show(self, req, id, **kw): + """Return data about the given user id""" + user = self.manager.get_user(id) + return dict(user=_translate_keys(user)) + + def delete(self, req, id, **kw): + self._check_admin(req.environ['nova.context']) + self.manager.delete_user(id) + return {} + + def create(self, req, **kw): + self._check_admin(req.environ['nova.context']) + env = self._deserialize(req.body, req) + is_admin = env['user'].get('admin') in ('T', 'True', True) + name = env['user'].get('name') + access = env['user'].get('access') + secret = env['user'].get('secret') + user = self.manager.create_user(name, access, secret, is_admin) + return dict(user=_translate_keys(user)) + + def update(self, req, id, **kw): + self._check_admin(req.environ['nova.context']) + env = self._deserialize(req.body, req) + is_admin = env['user'].get('admin') + if is_admin is not None: + is_admin = is_admin in ('T', 'True', True) + access = env['user'].get('access') + secret = env['user'].get('secret') + self.manager.modify_user(id, access, secret, is_admin) + return dict(user=_translate_keys(self.manager.get_user(id))) diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index d5206da20..30bf2b67b 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -43,35 +43,35 @@ class Controller(wsgi.Controller): "attributes": { "zone": ["id", "api_url"]}}} - def index(self, req): + def index(self, req, **kw): """Return all zones in brief""" items = db.zone_get_all(req.environ['nova.context']) items = common.limited(items, req) items = [_scrub_zone(item) for item in items] return dict(zones=items) - def detail(self, req): + def detail(self, req, **kw): """Return all zones in detail""" return self.index(req) - def show(self, req, id): + def show(self, req, id, **kw): """Return data about the given zone id""" zone_id = int(id) zone = db.zone_get(req.environ['nova.context'], zone_id) return dict(zone=_scrub_zone(zone)) - def delete(self, req, id): + def delete(self, req, id, **kw): zone_id = int(id) db.zone_delete(req.environ['nova.context'], zone_id) return {} - def create(self, req): + def create(self, req, **kw): context = req.environ['nova.context'] env = self._deserialize(req.body, req) zone = db.zone_create(context, env["zone"]) return dict(zone=_scrub_zone(zone)) - def update(self, req, id): + def update(self, req, id, **kw): context = req.environ['nova.context'] env = self._deserialize(req.body, req) zone_id = int(id) -- cgit From a62e603e8b1cedd89ca0c71f1cdc928d19c68a4d Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 3 Mar 2011 11:04:33 -0500 Subject: adding wsgi.Request class to add custom best_match; adding new class to wsgify decorators; replacing all references to webob.Request in non-test code to wsgi.Request --- nova/api/direct.py | 4 ++-- nova/api/ec2/__init__.py | 14 +++++++------- nova/api/ec2/metadatarequesthandler.py | 2 +- nova/api/openstack/__init__.py | 4 ++-- nova/api/openstack/auth.py | 4 ++-- nova/api/openstack/common.py | 2 +- nova/api/openstack/faults.py | 2 +- nova/api/openstack/ratelimiting/__init__.py | 4 ++-- 8 files changed, 18 insertions(+), 18 deletions(-) (limited to 'nova/api') diff --git a/nova/api/direct.py b/nova/api/direct.py index 208b6d086..cd237f649 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -187,7 +187,7 @@ class ServiceWrapper(wsgi.Controller): def __init__(self, service_handle): self.service_handle = service_handle - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): arg_dict = req.environ['wsgiorg.routing_args'][1] action = arg_dict['action'] @@ -218,7 +218,7 @@ class Proxy(object): self.prefix = prefix def __do_request(self, path, context, **kwargs): - req = webob.Request.blank(path) + req = wsgi.Request.blank(path) req.method = 'POST' req.body = urllib.urlencode({'json': utils.dumps(kwargs)}) req.environ['openstack.context'] = context diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 5adc2c075..58b1ecf03 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -53,7 +53,7 @@ flags.DEFINE_list('lockout_memcached_servers', None, class RequestLogging(wsgi.Middleware): """Access-Log akin logging for all EC2 API requests.""" - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): start = utils.utcnow() rv = req.get_response(self.application) @@ -112,7 +112,7 @@ class Lockout(wsgi.Middleware): debug=0) super(Lockout, self).__init__(application) - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): access_key = str(req.params['AWSAccessKeyId']) failures_key = "authfailures-%s" % access_key @@ -141,7 +141,7 @@ class Authenticate(wsgi.Middleware): """Authenticate an EC2 request and add 'ec2.context' to WSGI environ.""" - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): # Read request signature and access id. try: @@ -190,7 +190,7 @@ class Requestify(wsgi.Middleware): super(Requestify, self).__init__(app) self.controller = utils.import_class(controller)() - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): non_args = ['Action', 'Signature', 'AWSAccessKeyId', 'SignatureMethod', 'SignatureVersion', 'Version', 'Timestamp'] @@ -269,7 +269,7 @@ class Authorizer(wsgi.Middleware): }, } - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): context = req.environ['ec2.context'] controller = req.environ['ec2.request'].controller.__class__.__name__ @@ -303,7 +303,7 @@ class Executor(wsgi.Application): response, or a 400 upon failure. """ - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): context = req.environ['ec2.context'] api_request = req.environ['ec2.request'] @@ -365,7 +365,7 @@ class Executor(wsgi.Application): class Versions(wsgi.Application): - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): """Respond to a request for all EC2 versions.""" # available api versions diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py index 6fb441656..28f99b0ef 100644 --- a/nova/api/ec2/metadatarequesthandler.py +++ b/nova/api/ec2/metadatarequesthandler.py @@ -65,7 +65,7 @@ class MetadataRequestHandler(wsgi.Application): data = data[item] return data - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): cc = cloud.CloudController() remote_address = req.remote_addr diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 274330e3b..b5439788d 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -47,7 +47,7 @@ flags.DEFINE_bool('allow_admin_api', class FaultWrapper(wsgi.Middleware): """Calls down the middleware stack, making exceptions into faults.""" - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): try: return req.get_response(self.application) @@ -115,7 +115,7 @@ class APIRouter(wsgi.Router): class Versions(wsgi.Application): - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): """Respond to a request for all OpenStack API versions.""" response = { diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 6011e6115..de8905f46 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -46,7 +46,7 @@ class AuthMiddleware(wsgi.Middleware): self.auth = auth.manager.AuthManager() super(AuthMiddleware, self).__init__(application) - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): if not self.has_authentication(req): return self.authenticate(req) @@ -121,7 +121,7 @@ class AuthMiddleware(wsgi.Middleware): username - string key - string API key - req - webob.Request object + req - wsgi.Request object """ ctxt = context.get_admin_context() user = self.auth.get_user_from_access_key(key) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 9f85c5c8a..49b970d75 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -25,7 +25,7 @@ def limited(items, request, max_limit=1000): Return a slice of items according to requested offset and limit. @param items: A sliceable entity - @param request: `webob.Request` possibly containing 'offset' and 'limit' + @param request: `wsgi.Request` possibly containing 'offset' and 'limit' GET variables. 'offset' is where to start in the list, and 'limit' is the maximum number of items to return. If 'limit' is not specified, 0, or > max_limit, we default diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 224a7ef0b..c70b00fa3 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -42,7 +42,7 @@ class Fault(webob.exc.HTTPException): """Create a Fault for the given webob.exc.exception.""" self.wrapped_exc = exception - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): """Generate a WSGI response based on the exception passed to ctor.""" # Replace the body with fault details. diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py index cbb4b897e..88ffc3246 100644 --- a/nova/api/openstack/ratelimiting/__init__.py +++ b/nova/api/openstack/ratelimiting/__init__.py @@ -57,7 +57,7 @@ class RateLimitingMiddleware(wsgi.Middleware): self.limiter = WSGIAppProxy(service_host) super(RateLimitingMiddleware, self).__init__(application) - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): """Rate limit the request. @@ -183,7 +183,7 @@ class WSGIApp(object): """Create the WSGI application using the given Limiter instance.""" self.limiter = limiter - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): parts = req.path_info.split('/') # format: /limiter// -- cgit From 9cfe8ff2e8e66952c3202b852a88ee6fca6fb736 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Thu, 3 Mar 2011 16:31:01 -0500 Subject: pep8 --- nova/api/openstack/servers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 73c787828..ea13116fa 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -85,6 +85,7 @@ def _translate_detail_keys(inst): return dict(server=inst_dict) + def _translate_keys(inst): """ Coerces into dictionary format, excluding all model attributes save for id and name """ @@ -143,8 +144,8 @@ class Controller(wsgi.Controller): """ Create a list of onset files from the personality request attribute - At this time, onset_files must be formatted as a list of - (file_path, file_content) pairs for compatibility with the + At this time, onset_files must be formatted as a list of + (file_path, file_content) pairs for compatibility with the underlying compute service. """ onset_files = [] @@ -157,8 +158,8 @@ class Controller(wsgi.Controller): try: contents = base64.b64decode(contents) except TypeError: - raise exc.HTTPBadRequest(explanation= - 'Personality content for %s cannot be decoded' % path) + msg = 'Personality content for %s cannot be decoded' % path + raise exc.HTTPBadRequest(explanation=msg) onset_files.append((path, contents)) return onset_files -- cgit From 848aced747a60c47d76efcb2147041339df4a628 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Thu, 3 Mar 2011 17:21:21 -0500 Subject: Refactor wsgi.Serializer away from handling Requests directly; now require Content-Type in all requests; fix tests according to new code --- nova/api/direct.py | 2 +- nova/api/openstack/__init__.py | 4 +++- nova/api/openstack/consoles.py | 2 +- nova/api/openstack/faults.py | 5 +++-- nova/api/openstack/images.py | 2 +- nova/api/openstack/servers.py | 9 ++++++--- nova/api/openstack/zones.py | 4 ++-- 7 files changed, 17 insertions(+), 11 deletions(-) (limited to 'nova/api') diff --git a/nova/api/direct.py b/nova/api/direct.py index cd237f649..1d699f947 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -206,7 +206,7 @@ class ServiceWrapper(wsgi.Controller): 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) + return self._serialize(result, req.best_match()) else: return result diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index b5439788d..6e1a2a06c 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -124,4 +124,6 @@ class Versions(wsgi.Application): metadata = { "application/xml": { "attributes": dict(version=["status", "id"])}} - return wsgi.Serializer(req.environ, metadata).to_content_type(response) + + content_type = req.best_match() + return wsgi.Serializer(metadata).serialize(response, content_type) diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py index 9ebdbe710..8c291c2eb 100644 --- a/nova/api/openstack/consoles.py +++ b/nova/api/openstack/consoles.py @@ -65,7 +65,7 @@ class Controller(wsgi.Controller): def create(self, req, server_id): """Creates a new console""" - #info = self._deserialize(req.body, req) + #info = self._deserialize(req.body, req.get_content_type()) self.console_api.create_console( req.environ['nova.context'], int(server_id)) diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index c70b00fa3..075fdb997 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -57,6 +57,7 @@ class Fault(webob.exc.HTTPException): fault_data[fault_name]['retryAfter'] = retry # 'code' is an attribute on the fault tag itself metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} - serializer = wsgi.Serializer(req.environ, metadata) - self.wrapped_exc.body = serializer.to_content_type(fault_data) + serializer = wsgi.Serializer(metadata) + content_type = req.best_match() + self.wrapped_exc.body = serializer.serialize(fault_data, content_type) return self.wrapped_exc diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index cf85a496f..98f0dd96b 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -151,7 +151,7 @@ class Controller(wsgi.Controller): def create(self, req): context = req.environ['nova.context'] - env = self._deserialize(req.body, req) + env = self._deserialize(req.body, req.get_content_type()) instance_id = env["image"]["serverId"] name = env["image"]["name"] diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 08b95b46a..24d2826af 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -141,7 +141,7 @@ class Controller(wsgi.Controller): def create(self, req): """ Creates a new server for a given user """ - env = self._deserialize(req.body, req) + env = self._deserialize(req.body, req.get_content_type()) if not env: return faults.Fault(exc.HTTPUnprocessableEntity()) @@ -182,7 +182,10 @@ class Controller(wsgi.Controller): def update(self, req, id): """ Updates the server name or password """ - inst_dict = self._deserialize(req.body, req) + if len(req.body) == 0: + raise exc.HTTPUnprocessableEntity() + + inst_dict = self._deserialize(req.body, req.get_content_type()) if not inst_dict: return faults.Fault(exc.HTTPUnprocessableEntity()) @@ -205,7 +208,7 @@ class Controller(wsgi.Controller): def action(self, req, id): """ Multi-purpose method used to reboot, rebuild, and resize a server """ - input_dict = self._deserialize(req.body, req) + input_dict = self._deserialize(req.body, req.get_content_type()) #TODO(sandy): rebuild/resize not supported. try: reboot_type = input_dict['reboot']['type'] diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index d5206da20..cf6cd789f 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -67,13 +67,13 @@ class Controller(wsgi.Controller): def create(self, req): context = req.environ['nova.context'] - env = self._deserialize(req.body, req) + env = self._deserialize(req.body, req.get_content_type()) zone = db.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) + env = self._deserialize(req.body, req.get_content_type()) zone_id = int(id) zone = db.zone_update(context, zone_id, env["zone"]) return dict(zone=_scrub_zone(zone)) -- cgit From 417f6ca5c54878a6bea4d545126f93ecb6a043b4 Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Thu, 3 Mar 2011 22:22:00 +0000 Subject: localize a few error messages. --- nova/api/openstack/accounts.py | 2 +- nova/api/openstack/users.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py index 264fdab99..3b90d2776 100644 --- a/nova/api/openstack/accounts.py +++ b/nova/api/openstack/accounts.py @@ -47,7 +47,7 @@ class Controller(wsgi.Controller): """ We cannot depend on the db layer to check for admin access for the auth manager, so we do it here """ if not context.is_admin: - raise exception.NotAuthorized("Not admin user.") + raise exception.NotAuthorized(_("Not admin user.")) def show(self, req, id): """Return data about the given account id""" diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py index c0b7544f9..ae3bf7791 100644 --- a/nova/api/openstack/users.py +++ b/nova/api/openstack/users.py @@ -48,7 +48,7 @@ class Controller(wsgi.Controller): """ We cannot depend on the db layer to check for admin access for the auth manager, so we do it here """ if not context.is_admin: - raise exception.NotAuthorized("Not admin user") + raise exception.NotAuthorized(_("Not admin user")) def index(self, req, **kw): """Return all users in brief""" -- cgit From c5bfab9a0d213cee549371f05e74747cfcd8f998 Mon Sep 17 00:00:00 2001 From: Ryan Lane Date: Thu, 3 Mar 2011 23:05:00 +0000 Subject: Changing output of status from showing the user as the owner, to showing the project --- 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 b1917e9ea..cadda97db 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -562,7 +562,7 @@ class CloudController(object): if context.is_admin: v['status'] = '%s (%s, %s, %s, %s)' % ( volume['status'], - volume['user_id'], + volume['project_id'], volume['host'], instance_data, volume['mountpoint']) -- cgit From 5ae13551990be67e3509ddcd10d1872a91634d83 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Thu, 3 Mar 2011 18:27:57 -0500 Subject: rename onset_files to personality_files all the way down to compute manager --- nova/api/openstack/servers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index ea13116fa..8f6d8de66 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -140,15 +140,15 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() - def _get_onset_files_from_personality(self, personality): + def _get_personality_files(self, personality): """ - Create a list of onset files from the personality request attribute + Create a list of personality files from the personality attribute - At this time, onset_files must be formatted as a list of + At this time, personality_files must be formatted as a list of (file_path, file_content) pairs for compatibility with the underlying compute service. """ - onset_files = [] + personality_files = [] for item in personality: try: path = item['path'] @@ -160,8 +160,8 @@ class Controller(wsgi.Controller): except TypeError: msg = 'Personality content for %s cannot be decoded' % path raise exc.HTTPBadRequest(explanation=msg) - onset_files.append((path, contents)) - return onset_files + personality_files.append((path, contents)) + return personality_files def create(self, req): """ Creates a new server for a given user """ @@ -191,7 +191,7 @@ class Controller(wsgi.Controller): metadata.append({'key': k, 'value': v}) personality = env['server'].get('personality', []) - onset_files = self._get_onset_files_from_personality(personality) + personality_files = self._get_personality_files(personality) instances = self.compute_api.create( context, @@ -204,7 +204,7 @@ class Controller(wsgi.Controller): key_name=key_pair['name'], key_data=key_pair['public_key'], metadata=metadata, - onset_files=onset_files) + personality_files=personality_files) return _translate_keys(instances[0]) def update(self, req, id): -- cgit From a433ddeda77aaa4462694661ecdca71eed6db669 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 4 Mar 2011 02:36:55 +0000 Subject: Replace objectstore images with S3 image service backending to glance or local --- nova/api/ec2/cloud.py | 127 +++++++++++++++++++++++++++----------------------- 1 file changed, 68 insertions(+), 59 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 844ccbe5e..8c2e77d86 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -39,7 +39,9 @@ from nova import log as logging from nova import network from nova import utils from nova import volume +from nova.api.ec2 import ec2utils from nova.compute import instance_types +from nova.image import s3 FLAGS = flags.FLAGS @@ -73,30 +75,19 @@ def _gen_key(context, user_id, key_name): return {'private_key': private_key, 'fingerprint': fingerprint} -def ec2_id_to_id(ec2_id): - """Convert an ec2 ID (i-[base 16 number]) to an instance id (int)""" - return int(ec2_id.split('-')[-1], 16) - - -def id_to_ec2_id(instance_id, template='i-%08x'): - """Convert an instance ID (int) to an ec2 ID (i-[base 16 number])""" - return template % instance_id - - class CloudController(object): """ CloudController provides the critical dispatch between inbound API calls through the endpoint and messages sent to the other nodes. """ def __init__(self): - self.image_service = utils.import_object(FLAGS.image_service) + self.image_service = s3.S3ImageService() self.network_api = network.API() self.volume_api = volume.API() self.compute_api = compute.API( network_api=self.network_api, - image_service=self.image_service, volume_api=self.volume_api, - hostname_factory=id_to_ec2_id) + hostname_factory=ec2utils.id_to_ec2_id) self.setup() def __str__(self): @@ -154,7 +145,7 @@ class CloudController(object): availability_zone = self._get_availability_zone_by_host(ctxt, host) floating_ip = db.instance_get_floating_address(ctxt, instance_ref['id']) - ec2_id = id_to_ec2_id(instance_ref['id']) + ec2_id = ec2utils.id_to_ec2_id(instance_ref['id']) data = { 'user-data': base64.b64decode(instance_ref['user_data']), 'meta-data': { @@ -525,7 +516,7 @@ class CloudController(object): ec2_id = instance_id[0] else: ec2_id = instance_id - instance_id = ec2_id_to_id(ec2_id) + instance_id = ec2utils.ec2_id_to_id(ec2_id) output = self.compute_api.get_console_output( context, instance_id=instance_id) now = datetime.datetime.utcnow() @@ -535,7 +526,7 @@ class CloudController(object): def get_ajax_console(self, context, instance_id, **kwargs): ec2_id = instance_id[0] - instance_id = ec2_id_to_id(ec2_id) + instance_id = ec2utils.ec2_id_to_id(ec2_id) return self.compute_api.get_ajax_console(context, instance_id=instance_id) @@ -543,7 +534,7 @@ class CloudController(object): if volume_id: volumes = [] for ec2_id in volume_id: - internal_id = ec2_id_to_id(ec2_id) + internal_id = ec2utils.ec2_id_to_id(ec2_id) volume = self.volume_api.get(context, internal_id) volumes.append(volume) else: @@ -556,11 +547,11 @@ class CloudController(object): instance_data = None if volume.get('instance', None): instance_id = volume['instance']['id'] - instance_ec2_id = id_to_ec2_id(instance_id) + instance_ec2_id = ec2utils.id_to_ec2_id(instance_id) instance_data = '%s[%s]' % (instance_ec2_id, volume['instance']['host']) v = {} - v['volumeId'] = id_to_ec2_id(volume['id'], 'vol-%08x') + v['volumeId'] = ec2utils.id_to_ec2_id(volume['id'], 'vol-%08x') v['status'] = volume['status'] v['size'] = volume['size'] v['availabilityZone'] = volume['availability_zone'] @@ -578,8 +569,7 @@ class CloudController(object): 'device': volume['mountpoint'], 'instanceId': instance_ec2_id, 'status': 'attached', - 'volumeId': id_to_ec2_id(volume['id'], - 'vol-%08x')}] + 'volumeId': v['volumeId']}] else: v['attachmentSet'] = [{}] @@ -598,12 +588,12 @@ class CloudController(object): return {'volumeSet': [self._format_volume(context, dict(volume))]} def delete_volume(self, context, volume_id, **kwargs): - volume_id = ec2_id_to_id(volume_id) + volume_id = ec2utils.ec2_id_to_id(volume_id) self.volume_api.delete(context, volume_id=volume_id) return True def update_volume(self, context, volume_id, **kwargs): - volume_id = ec2_id_to_id(volume_id) + volume_id = ec2utils.ec2_id_to_id(volume_id) updatable_fields = ['display_name', 'display_description'] changes = {} for field in updatable_fields: @@ -614,8 +604,8 @@ class CloudController(object): return True def attach_volume(self, context, volume_id, instance_id, device, **kwargs): - volume_id = ec2_id_to_id(volume_id) - instance_id = ec2_id_to_id(instance_id) + volume_id = ec2utils.ec2_id_to_id(volume_id) + instance_id = ec2utils.ec2_id_to_id(instance_id) msg = _("Attach volume %(volume_id)s to instance %(instance_id)s" " at %(device)s") % locals() LOG.audit(msg, context=context) @@ -626,22 +616,22 @@ class CloudController(object): volume = self.volume_api.get(context, volume_id) return {'attachTime': volume['attach_time'], 'device': volume['mountpoint'], - 'instanceId': id_to_ec2_id(instance_id), + 'instanceId': ec2utils.id_to_ec2_id(instance_id), 'requestId': context.request_id, 'status': volume['attach_status'], - 'volumeId': id_to_ec2_id(volume_id, 'vol-%08x')} + 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')} def detach_volume(self, context, volume_id, **kwargs): - volume_id = ec2_id_to_id(volume_id) + 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) instance = self.compute_api.detach_volume(context, volume_id=volume_id) return {'attachTime': volume['attach_time'], 'device': volume['mountpoint'], - 'instanceId': id_to_ec2_id(instance['id']), + 'instanceId': ec2utils.id_to_ec2_id(instance['id']), 'requestId': context.request_id, 'status': volume['attach_status'], - 'volumeId': id_to_ec2_id(volume_id, 'vol-%08x')} + 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')} def _convert_to_set(self, lst, label): if lst == None or lst == []: @@ -675,7 +665,7 @@ class CloudController(object): if instance_id: instances = [] for ec2_id in instance_id: - internal_id = ec2_id_to_id(ec2_id) + internal_id = ec2utils.ec2_id_to_id(ec2_id) instance = self.compute_api.get(context, instance_id=internal_id) instances.append(instance) @@ -687,7 +677,7 @@ class CloudController(object): continue i = {} instance_id = instance['id'] - ec2_id = id_to_ec2_id(instance_id) + ec2_id = ec2utils.id_to_ec2_id(instance_id) i['instanceId'] = ec2_id i['imageId'] = instance['image_id'] i['instanceState'] = { @@ -755,7 +745,7 @@ class CloudController(object): if (floating_ip_ref['fixed_ip'] and floating_ip_ref['fixed_ip']['instance']): instance_id = floating_ip_ref['fixed_ip']['instance']['id'] - ec2_id = id_to_ec2_id(instance_id) + ec2_id = ec2utils.id_to_ec2_id(instance_id) address_rv = {'public_ip': address, 'instance_id': ec2_id} if context.is_admin: @@ -778,7 +768,7 @@ class CloudController(object): def associate_address(self, context, instance_id, public_ip, **kwargs): LOG.audit(_("Associate address %(public_ip)s to" " instance %(instance_id)s") % locals(), context=context) - instance_id = ec2_id_to_id(instance_id) + instance_id = ec2utils.ec2_id_to_id(instance_id) self.compute_api.associate_floating_ip(context, instance_id=instance_id, address=public_ip) @@ -791,13 +781,17 @@ class CloudController(object): def run_instances(self, context, **kwargs): max_count = int(kwargs.get('max_count', 1)) + if kwargs.get('kernel_id'): + kwargs['kernel_id'] = ec2utils.ec2_id_to_id(kwargs['kernel_id']) + if kwargs.get('ramdisk_id'): + kwargs['ramdisk_id'] = ec2utils.ec2_id_to_id(kwargs['ramdisk_id']) instances = self.compute_api.create(context, instance_type=instance_types.get_by_type( kwargs.get('instance_type', None)), - image_id=kwargs['image_id'], + image_id=ec2utils.ec2_id_to_id(kwargs['image_id']), min_count=int(kwargs.get('min_count', max_count)), max_count=max_count, - kernel_id=kwargs.get('kernel_id', None), + kernel_id=kwargs.get('kernel_id'), ramdisk_id=kwargs.get('ramdisk_id'), display_name=kwargs.get('display_name'), display_description=kwargs.get('display_description'), @@ -814,7 +808,7 @@ class CloudController(object): instance_id is a kwarg so its name cannot be modified.""" LOG.debug(_("Going to start terminating instances")) for ec2_id in instance_id: - instance_id = ec2_id_to_id(ec2_id) + instance_id = ec2utils.ec2_id_to_id(ec2_id) self.compute_api.delete(context, instance_id=instance_id) return True @@ -822,19 +816,19 @@ class CloudController(object): """instance_id is a list of instance ids""" LOG.audit(_("Reboot instance %r"), instance_id, context=context) for ec2_id in instance_id: - instance_id = ec2_id_to_id(ec2_id) + instance_id = ec2utils.ec2_id_to_id(ec2_id) self.compute_api.reboot(context, instance_id=instance_id) return True def rescue_instance(self, context, instance_id, **kwargs): """This is an extension to the normal ec2_api""" - instance_id = ec2_id_to_id(instance_id) + instance_id = ec2utils.ec2_id_to_id(instance_id) self.compute_api.rescue(context, instance_id=instance_id) return True def unrescue_instance(self, context, instance_id, **kwargs): """This is an extension to the normal ec2_api""" - instance_id = ec2_id_to_id(instance_id) + instance_id = ec2utils.ec2_id_to_id(instance_id) self.compute_api.unrescue(context, instance_id=instance_id) return True @@ -845,41 +839,50 @@ class CloudController(object): if field in kwargs: changes[field] = kwargs[field] if changes: - instance_id = ec2_id_to_id(ec2_id) + instance_id = ec2utils.ec2_id_to_id(ec2_id) self.compute_api.update(context, instance_id=instance_id, **kwargs) return True - def _format_image(self, context, image): + def _format_image(self, image): """Convert from format defined by BaseImageService to S3 format.""" i = {} i['imageId'] = image.get('id') - i['kernelId'] = image.get('kernel_id') - i['ramdiskId'] = image.get('ramdisk_id') - i['imageOwnerId'] = image.get('owner_id') - i['imageLocation'] = image.get('location') - i['imageState'] = image.get('status') + i['kernelId'] = image['properties'].get('kernel_id') + i['ramdiskId'] = image['properties'].get('ramdisk_id') + i['imageOwnerId'] = image['properties'].get('owner_id') + i['imageLocation'] = image['properties'].get('image_location') + i['imageState'] = image['properties'].get('image_state') i['type'] = image.get('type') - i['isPublic'] = image.get('is_public') - i['architecture'] = image.get('architecture') + i['isPublic'] = image['properties'].get('is_public') == 'True' + i['architecture'] = image['properties'].get('architecture') return i def describe_images(self, context, image_id=None, **kwargs): # NOTE: image_id is a list! - images = self.image_service.index(context) if image_id: - images = filter(lambda x: x['id'] in image_id, images) - images = [self._format_image(context, i) for i in images] + images = [] + for ec2_id in image_id: + try: + image = self.image_service.show(context, ec2_id) + except exception.NotFound: + raise exception.NotFound(_('Image %s not found') % + ec2_id) + images.append(image) + else: + images = self.image_service.detail(context) + images = [self._format_image(i) for i in images] return {'imagesSet': images} def deregister_image(self, context, image_id, **kwargs): LOG.audit(_("De-registering image %s"), image_id, context=context) - self.image_service.deregister(context, image_id) + self.image_service.delete(context, image_id) return {'imageId': image_id} def register_image(self, context, image_location=None, **kwargs): if image_location is None and 'name' in kwargs: image_location = kwargs['name'] - image_id = self.image_service.register(context, image_location) + image = {"image_location": image_location} + image_id = self.image_service.create(context, image) msg = _("Registered image %(image_location)s with" " id %(image_id)s") % locals() LOG.audit(msg, context=context) @@ -890,11 +893,10 @@ class CloudController(object): raise exception.ApiError(_('attribute not supported: %s') % attribute) try: - image = self._format_image(context, - self.image_service.show(context, + image = self._format_image(self.image_service.show(context, image_id)) - except IndexError: - raise exception.ApiError(_('invalid id: %s') % image_id) + except (IndexError, exception.NotFound): + raise exception.NotFound(_('Image %s not found') % image_id) result = {'image_id': image_id, 'launchPermission': []} if image['isPublic']: result['launchPermission'].append({'group': 'all'}) @@ -913,7 +915,14 @@ class CloudController(object): if not operation_type in ['add', 'remove']: raise exception.ApiError(_('operation_type must be add or remove')) LOG.audit(_("Updating image %s publicity"), image_id, context=context) - return self.image_service.modify(context, image_id, operation_type) + + try: + metadata = self.image_service.show(context, image_id) + except exception.NotFound: + raise exception.NotFound(_('Image %s not found') % image_id) + del(metadata['id']) + metadata['properties']['is_public'] = (operation_type == 'add') + return self.image_service.update(context, image_id, metadata) def update_image(self, context, image_id, **kwargs): result = self.image_service.update(context, image_id, dict(kwargs)) -- cgit From 13307e02258a5a08bedb1ed933a107668aac6457 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 4 Mar 2011 05:04:49 +0000 Subject: make local image service work --- 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 8c2e77d86..aa1dcbe33 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -853,7 +853,7 @@ class CloudController(object): i['imageLocation'] = image['properties'].get('image_location') i['imageState'] = image['properties'].get('image_state') i['type'] = image.get('type') - i['isPublic'] = image['properties'].get('is_public') == 'True' + i['isPublic'] = str(image['properties'].get('is_public', '')) == 'True' i['architecture'] = image['properties'].get('architecture') return i -- cgit From 517a571f8905c32efd45f7b3410fb263ad705545 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 4 Mar 2011 05:58:49 +0000 Subject: add the ec2utils file i forgot --- nova/api/ec2/ec2utils.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 nova/api/ec2/ec2utils.py (limited to 'nova/api') diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py new file mode 100644 index 000000000..0ea22c0e6 --- /dev/null +++ b/nova/api/ec2/ec2utils.py @@ -0,0 +1,27 @@ +# 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. + + +def ec2_id_to_id(ec2_id): + """Convert an ec2 ID (i-[base 16 number]) to an instance id (int)""" + return int(ec2_id.split('-')[-1], 16) + + +def id_to_ec2_id(instance_id, template='i-%08x'): + """Convert an instance ID (int) to an ec2 ID (i-[base 16 number])""" + return template % instance_id -- cgit From 1d8914fc752f7182f942cdd40f2ba18baedeed0c Mon Sep 17 00:00:00 2001 From: Cerberus Date: Fri, 4 Mar 2011 11:19:35 -0600 Subject: More fixes --- 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 ceb17c9e4..c2bf42b72 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -203,8 +203,8 @@ class Controller(wsgi.Controller): return exc.HTTPNoContent() def action(self, req, id): - """ Multi-purpose method used to reboot, rebuild, or - resize a server """ + """Multi-purpose method used to reboot, rebuild, or + resize a server""" actions = { 'reboot': self._action_reboot, -- 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 831f398653cc99253bfeeb232165d3f9c043bd0b Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Fri, 4 Mar 2011 14:01:25 -0500 Subject: Fix renaming of instance fields using update_instance method. --- nova/api/ec2/apirequest.py | 18 +++++++++++++++++- nova/api/ec2/cloud.py | 4 ++-- 2 files changed, 19 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py index 2b1acba5a..d7ad08d2f 100644 --- a/nova/api/ec2/apirequest.py +++ b/nova/api/ec2/apirequest.py @@ -52,7 +52,23 @@ def _database_to_isoformat(datetimeobj): def _try_convert(value): - """Return a non-string if possible""" + """Return a non-string from a string or unicode, if possible. + + ============= ===================================================== + When value is returns + ============= ===================================================== + zero-length '' + 'None' None + 'True' True + 'False' False + '0', '-0' 0 + 0xN, -0xN int from hex (postitive) (N is any number) + 0bN, -0bN int from binary (positive) (N is any number) + * try conversion to int, float, complex, fallback value + + """ + if len(value) == 0: + return '' if value == 'None': return None if value == 'True': diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index c6309f03c..0d22a3f46 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -838,14 +838,14 @@ class CloudController(object): self.compute_api.unrescue(context, instance_id=instance_id) return True - def update_instance(self, context, ec2_id, **kwargs): + def update_instance(self, context, instance_id, **kwargs): updatable_fields = ['display_name', 'display_description'] changes = {} for field in updatable_fields: if field in kwargs: changes[field] = kwargs[field] if changes: - instance_id = ec2_id_to_id(ec2_id) + instance_id = ec2_id_to_id(instance_id) self.compute_api.update(context, instance_id=instance_id, **kwargs) return True -- cgit From f3c1c99ca0f6f3164430b33f46772ef8bdc87b70 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 4 Mar 2011 19:54:19 +0000 Subject: move the id wrapping into cloud layer instead of image_service --- nova/api/ec2/cloud.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index aa1dcbe33..3ea3fa07e 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -843,10 +843,19 @@ class CloudController(object): self.compute_api.update(context, instance_id=instance_id, **kwargs) return True + _type_prefix_map = {'machine': 'ami', + 'kernel': 'aki', + 'ramdisk': 'ari'} + + def _image_ec2_id(self, image_id, image_type): + prefix = self._type_prefix_map[image_type] + template = prefix + '-%08x' + return ec2utils.id_to_ec2_id(int(image_id), template=template) + def _format_image(self, image): """Convert from format defined by BaseImageService to S3 format.""" i = {} - i['imageId'] = image.get('id') + i['imageId'] = self._image_ec2_id(image.get('id'), image.get('type')) i['kernelId'] = image['properties'].get('kernel_id') i['ramdiskId'] = image['properties'].get('ramdisk_id') i['imageOwnerId'] = image['properties'].get('owner_id') @@ -863,7 +872,8 @@ class CloudController(object): images = [] for ec2_id in image_id: try: - image = self.image_service.show(context, ec2_id) + internal_id = ec2utils.ec2_id_to_id(ec2_id) + image = self.image_service.show(context, internal_id) except exception.NotFound: raise exception.NotFound(_('Image %s not found') % ec2_id) @@ -875,14 +885,16 @@ class CloudController(object): def deregister_image(self, context, image_id, **kwargs): LOG.audit(_("De-registering image %s"), image_id, context=context) - self.image_service.delete(context, image_id) + internal_id = ec2utils.ec2_id_to_id(image_id) + self.image_service.delete(context, internal_id) return {'imageId': image_id} def register_image(self, context, image_location=None, **kwargs): if image_location is None and 'name' in kwargs: image_location = kwargs['name'] - image = {"image_location": image_location} - image_id = self.image_service.create(context, image) + metadata = {"image_location": image_location} + image = self.image_service.create(context, metadata) + image_id = self._image_ec2_id(image['id'], image['type']) msg = _("Registered image %(image_location)s with" " id %(image_id)s") % locals() LOG.audit(msg, context=context) @@ -893,8 +905,9 @@ class CloudController(object): raise exception.ApiError(_('attribute not supported: %s') % attribute) try: + internal_id = ec2utils.ec2_id_to_id(image_id) image = self._format_image(self.image_service.show(context, - image_id)) + internal_id)) except (IndexError, exception.NotFound): raise exception.NotFound(_('Image %s not found') % image_id) result = {'image_id': image_id, 'launchPermission': []} @@ -917,13 +930,15 @@ class CloudController(object): LOG.audit(_("Updating image %s publicity"), image_id, context=context) try: - metadata = self.image_service.show(context, image_id) + internal_id = ec2utils.ec2_id_to_id(image_id) + metadata = self.image_service.show(context, internal_id) except exception.NotFound: raise exception.NotFound(_('Image %s not found') % image_id) del(metadata['id']) metadata['properties']['is_public'] = (operation_type == 'add') - return self.image_service.update(context, image_id, metadata) + return self.image_service.update(context, internal_id, metadata) def update_image(self, context, image_id, **kwargs): - result = self.image_service.update(context, image_id, dict(kwargs)) + internal_id = ec2utils.ec2_id_to_id(image_id) + result = self.image_service.update(context, internal_id, dict(kwargs)) return result -- cgit From 10668b87f46a1fb5d039f6e7d7a7a55b89d7602a Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Fri, 4 Mar 2011 17:04:41 -0500 Subject: respond well if personality attribute is incomplete --- nova/api/openstack/servers.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 8b7b20b92..7c620dbc6 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -153,6 +153,9 @@ class Controller(wsgi.Controller): try: path = item['path'] contents = item['contents'] + except KeyError, key: + expl = 'Bad personality format: missing %s' % key + raise exc.HTTPBadRequest(explanation=expl) except TypeError: raise exc.HTTPBadRequest(explanation='Bad personality format') try: -- 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 7af17cbef6f3e1c5b052133e40e0edbd8ca9ffb3 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Sun, 6 Mar 2011 10:41:24 -0500 Subject: select cleanups --- 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 7c620dbc6..93f504f91 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -153,7 +153,7 @@ class Controller(wsgi.Controller): try: path = item['path'] contents = item['contents'] - except KeyError, key: + except KeyError as key: expl = 'Bad personality format: missing %s' % key raise exc.HTTPBadRequest(explanation=expl) except TypeError: -- cgit From a5bee00af4d6ec3eed6ed0abd866948f4510f041 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 7 Mar 2011 01:25:01 +0000 Subject: make compute get the new images properly, fix a bunch of tests, and provide conversion commands --- nova/api/ec2/cloud.py | 74 ++++++++++++++++++++++++++++++------------------ nova/api/ec2/ec2utils.py | 6 +++- 2 files changed, 52 insertions(+), 28 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 3ea3fa07e..496e944fe 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -146,10 +146,13 @@ class CloudController(object): floating_ip = db.instance_get_floating_address(ctxt, instance_ref['id']) ec2_id = ec2utils.id_to_ec2_id(instance_ref['id']) + image_ec2_id = self._image_ec2_id(instance_ref['image_id'], 'machine') + k_ec2_id = self._image_ec2_id(instance_ref['kernel_id'], 'kernel') + r_ec2_id = self._image_ec2_id(instance_ref['ramdisk_id'], 'ramdisk') data = { 'user-data': base64.b64decode(instance_ref['user_data']), 'meta-data': { - 'ami-id': instance_ref['image_id'], + 'ami-id': image_ec2_id, 'ami-launch-index': instance_ref['launch_index'], 'ami-manifest-path': 'FIXME', 'block-device-mapping': { @@ -164,12 +167,12 @@ class CloudController(object): 'instance-type': instance_ref['instance_type'], 'local-hostname': hostname, 'local-ipv4': address, - 'kernel-id': instance_ref['kernel_id'], + 'kernel-id': k_ec2_id, + 'ramdisk-id': r_ec2_id, 'placement': {'availability-zone': availability_zone}, 'public-hostname': hostname, 'public-ipv4': floating_ip or '', 'public-keys': keys, - 'ramdisk-id': instance_ref['ramdisk_id'], 'reservation-id': instance_ref['reservation_id'], 'security-groups': '', 'mpi': mpi}} @@ -679,7 +682,7 @@ class CloudController(object): instance_id = instance['id'] ec2_id = ec2utils.id_to_ec2_id(instance_id) i['instanceId'] = ec2_id - i['imageId'] = instance['image_id'] + i['imageId'] = self._image_ec2_id(instance['image_id']) i['instanceState'] = { 'code': instance['state'], 'name': instance['state_description']} @@ -782,13 +785,15 @@ class CloudController(object): def run_instances(self, context, **kwargs): max_count = int(kwargs.get('max_count', 1)) if kwargs.get('kernel_id'): - kwargs['kernel_id'] = ec2utils.ec2_id_to_id(kwargs['kernel_id']) + kernel = self._get_image(context, kwargs['kernel_id']) + kwargs['kernel_id'] = kernel['id'] if kwargs.get('ramdisk_id'): - kwargs['ramdisk_id'] = ec2utils.ec2_id_to_id(kwargs['ramdisk_id']) + ramdisk = self._get_image(context, kwargs['ramdisk_id']) + kwargs['ramdisk_id'] = ramdisk['id'] instances = self.compute_api.create(context, instance_type=instance_types.get_by_type( kwargs.get('instance_type', None)), - image_id=ec2utils.ec2_id_to_id(kwargs['image_id']), + image_id=self._get_image(context, kwargs['image_id'])['id'], min_count=int(kwargs.get('min_count', max_count)), max_count=max_count, kernel_id=kwargs.get('kernel_id'), @@ -847,17 +852,34 @@ class CloudController(object): 'kernel': 'aki', 'ramdisk': 'ari'} - def _image_ec2_id(self, image_id, image_type): + def _image_ec2_id(self, image_id, image_type='machine'): prefix = self._type_prefix_map[image_type] template = prefix + '-%08x' return ec2utils.id_to_ec2_id(int(image_id), template=template) + def _get_image(self, context, ec2_id): + try: + internal_id = ec2utils.ec2_id_to_id(ec2_id) + return self.image_service.show(context, internal_id) + except exception.NotFound: + return self.image_service.show_by_name(context, ec2_id) + + def _format_image(self, image): """Convert from format defined by BaseImageService to S3 format.""" i = {} - i['imageId'] = self._image_ec2_id(image.get('id'), image.get('type')) - i['kernelId'] = image['properties'].get('kernel_id') - i['ramdiskId'] = image['properties'].get('ramdisk_id') + ec2_id = self._image_ec2_id(image.get('id'), image.get('type')) + name = image.get('name') + if name: + i['imageId'] = "%s (%s)" % (ec2_id, name) + else: + i['imageId'] = ec2_id + kernel_id = image['properties'].get('kernel_id') + if kernel_id: + i['kernelId'] = self._image_ec2_id(kernel_id, 'kernel') + ramdisk_id = image['properties'].get('ramdisk_id') + if ramdisk_id: + i['ramdiskId'] = self._image_ec2_id(ramdisk_id, 'ramdisk') i['imageOwnerId'] = image['properties'].get('owner_id') i['imageLocation'] = image['properties'].get('image_location') i['imageState'] = image['properties'].get('image_state') @@ -872,8 +894,7 @@ class CloudController(object): images = [] for ec2_id in image_id: try: - internal_id = ec2utils.ec2_id_to_id(ec2_id) - image = self.image_service.show(context, internal_id) + image = self._get_image(context, ec2_id) except exception.NotFound: raise exception.NotFound(_('Image %s not found') % ec2_id) @@ -885,16 +906,17 @@ class CloudController(object): def deregister_image(self, context, image_id, **kwargs): LOG.audit(_("De-registering image %s"), image_id, context=context) - internal_id = ec2utils.ec2_id_to_id(image_id) + image = self._get_image(context, image_id) + internal_id = image['id'] self.image_service.delete(context, internal_id) return {'imageId': image_id} def register_image(self, context, image_location=None, **kwargs): if image_location is None and 'name' in kwargs: image_location = kwargs['name'] - metadata = {"image_location": image_location} + metadata = {'properties': {'image_location': image_location}} image = self.image_service.create(context, metadata) - image_id = self._image_ec2_id(image['id'], image['type']) + image_id = self._image_ec2_id(image['id'], image['type']) msg = _("Registered image %(image_location)s with" " id %(image_id)s") % locals() LOG.audit(msg, context=context) @@ -905,13 +927,11 @@ class CloudController(object): raise exception.ApiError(_('attribute not supported: %s') % attribute) try: - internal_id = ec2utils.ec2_id_to_id(image_id) - image = self._format_image(self.image_service.show(context, - internal_id)) - except (IndexError, exception.NotFound): + image = self._get_image(context, image_id) + except exception.NotFound: raise exception.NotFound(_('Image %s not found') % image_id) - result = {'image_id': image_id, 'launchPermission': []} - if image['isPublic']: + result = {'imageId': image_id, 'launchPermission': []} + if image['properties']['is_public']: result['launchPermission'].append({'group': 'all'}) return result @@ -930,13 +950,13 @@ class CloudController(object): LOG.audit(_("Updating image %s publicity"), image_id, context=context) try: - internal_id = ec2utils.ec2_id_to_id(image_id) - metadata = self.image_service.show(context, internal_id) + image = self._get_image(context, image_id) except exception.NotFound: raise exception.NotFound(_('Image %s not found') % image_id) - del(metadata['id']) - metadata['properties']['is_public'] = (operation_type == 'add') - return self.image_service.update(context, internal_id, metadata) + internal_id = image['id'] + del(image['id']) + image['properties']['is_public'] = (operation_type == 'add') + return self.image_service.update(context, internal_id, image) def update_image(self, context, image_id, **kwargs): internal_id = ec2utils.ec2_id_to_id(image_id) diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index 0ea22c0e6..e4df80cf8 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -16,10 +16,14 @@ # License for the specific language governing permissions and limitations # under the License. +from nova import exception def ec2_id_to_id(ec2_id): """Convert an ec2 ID (i-[base 16 number]) to an instance id (int)""" - return int(ec2_id.split('-')[-1], 16) + try: + return int(ec2_id.split('-')[-1], 16) + except ValueError: + raise exception.NotFound(_("Id %s Not Found") % ec2_id) def id_to_ec2_id(instance_id, template='i-%08x'): -- cgit From b3d3366b8fd4eaf81bb9e03ad808c1a139e5b5b0 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Mon, 7 Mar 2011 12:07:23 -0500 Subject: Generate 'adminPass' and call set_password when creating servers. --- nova/api/openstack/servers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 08b95b46a..6cd8bb451 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -84,13 +84,11 @@ def _translate_detail_keys(inst): return dict(server=inst_dict) - def _translate_keys(inst): """ Coerces into dictionary format, excluding all model attributes save for id and name """ return dict(server=dict(id=inst['id'], name=inst['display_name'])) - class Controller(wsgi.Controller): """ The Server API controller for the OpenStack API """ @@ -178,7 +176,13 @@ class Controller(wsgi.Controller): key_data=key_pair['public_key'], metadata=metadata, onset_files=env.get('onset_files', [])) - return _translate_keys(instances[0]) + + server = _translate_keys(instances[0]) + password = "%s%s" % (server['server']['name'][:4], + utils.generate_password(12)) + server['server']['adminPass'] = password + self.compute_api.set_admin_password(context, server['server']['id']) + return server def update(self, req, id): """ Updates the server name or password """ -- cgit From bcb18ee3d0d095b616c0909c92a151a599d4e17f Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Mon, 7 Mar 2011 15:05:07 -0500 Subject: Invalid values for offset and limit params in http requests now return a 400 response with a useful message in the body. Also added and updated tests. --- nova/api/openstack/common.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 9f85c5c8a..f7a9cc3f0 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -36,15 +36,18 @@ def limited(items, request, max_limit=1000): try: offset = int(request.GET.get('offset', 0)) except ValueError: - offset = 0 + raise webob.exc.HTTPBadRequest(_('offset param must be an integer')) try: limit = int(request.GET.get('limit', max_limit)) except ValueError: - limit = max_limit + raise webob.exc.HTTPBadRequest(_('limit param must be an integer')) - if offset < 0 or limit < 0: - raise webob.exc.HTTPBadRequest() + if limit < 0: + raise webob.exc.HTTPBadRequest(_('limit param must be positive')) + + if offset < 0: + raise webob.exc.HTTPBadRequest(_('offset param must be positive')) limit = min(max_limit, limit or max_limit) range_end = offset + limit -- cgit From fd95523689b80f53972c59c3738e6b786a7160ff Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 7 Mar 2011 21:22:06 +0000 Subject: pep8 --- nova/api/ec2/cloud.py | 1 - nova/api/ec2/ec2utils.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 496e944fe..6479c9445 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -864,7 +864,6 @@ class CloudController(object): except exception.NotFound: return self.image_service.show_by_name(context, ec2_id) - def _format_image(self, image): """Convert from format defined by BaseImageService to S3 format.""" i = {} diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index e4df80cf8..3b34f6ea5 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -18,6 +18,7 @@ from nova import exception + def ec2_id_to_id(ec2_id): """Convert an ec2 ID (i-[base 16 number]) to an instance id (int)""" try: -- cgit From cbc2956a4e863c1bc952c7cef6045c39d293818d Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Tue, 8 Mar 2011 17:18:13 +0000 Subject: Remove addition of account to service url. --- nova/api/openstack/__init__.py | 24 ++++------------------ nova/api/openstack/auth.py | 46 +++++++++--------------------------------- 2 files changed, 13 insertions(+), 57 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 005d330a6..a655b1c85 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -73,18 +73,6 @@ class APIRouter(wsgi.Router): def __init__(self): mapper = routes.Mapper() - accounts_controller = accounts.Controller() - mapper.connect("account", "/{id}", - controller=accounts_controller, action="show", - conditions=dict(method=["GET"])) - if FLAGS.allow_admin_api: - mapper.connect("/{id}", - controller=accounts_controller, action="update", - conditions=dict(method=["PUT"])) - mapper.connect("/{id}", - controller=accounts_controller, action="delete", - conditions=dict(method=["DELETE"])) - server_members = {'action': 'POST'} if FLAGS.allow_admin_api: LOG.debug(_("Including admin operations in API.")) @@ -101,38 +89,34 @@ class APIRouter(wsgi.Router): server_members['inject_network_info'] = 'POST' mapper.resource("zone", "zones", controller=zones.Controller(), - path_prefix="{account_id}/", collection={'detail': 'GET'}) mapper.resource("user", "users", controller=users.Controller(), - path_prefix="{account_id}/", collection={'detail': 'GET'}) + mapper.resource("account", "accounts", + controller=accounts.Controller(), + collection={'detail': 'GET'}) + mapper.resource("server", "servers", controller=servers.Controller(), collection={'detail': 'GET'}, - path_prefix="{account_id}/", member=server_members) mapper.resource("backup_schedule", "backup_schedule", controller=backup_schedules.Controller(), - path_prefix="{account_id}/servers/{server_id}/", parent_resource=dict(member_name='server', collection_name='servers')) mapper.resource("console", "consoles", controller=consoles.Controller(), - path_prefix="{account_id}/servers/{server_id}/", parent_resource=dict(member_name='server', collection_name='servers')) mapper.resource("image", "images", controller=images.Controller(), - path_prefix="{account_id}/", collection={'detail': 'GET'}) mapper.resource("flavor", "flavors", controller=flavors.Controller(), - path_prefix="{account_id}/", collection={'detail': 'GET'}) mapper.resource("shared_ip_group", "shared_ip_groups", - path_prefix="{account_id}/", collection={'detail': 'GET'}, controller=shared_ip_groups.Controller()) diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index e77910fed..e71fc69e3 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -53,19 +53,15 @@ class AuthMiddleware(wsgi.Middleware): if not self.has_authentication(req): return self.authenticate(req) user = self.get_user_by_authentication(req) - account_name = req.path_info_peek() - + accounts = self.auth.get_projects(user=user) if not user: return faults.Fault(webob.exc.HTTPUnauthorized()) - if not account_name: - if self.auth.is_admin(user): - account_name = FLAGS.default_project - else: - return faults.Fault(webob.exc.HTTPUnauthorized()) - try: - account = self.auth.get_project(account_name) - except exception.NotFound: + if accounts: + #we are punting on this til auth is settled, + #and possibly til api v1.1 (mdragon) + account = accounts[0] + else: return faults.Fault(webob.exc.HTTPUnauthorized()) if not self.auth.is_admin(user) and \ @@ -85,7 +81,6 @@ class AuthMiddleware(wsgi.Middleware): # Unless the request is explicitly made against // don't # honor it path_info = req.path_info - account_name = None if len(path_info) > 1: return faults.Fault(webob.exc.HTTPUnauthorized()) @@ -95,10 +90,7 @@ class AuthMiddleware(wsgi.Middleware): except KeyError: return faults.Fault(webob.exc.HTTPUnauthorized()) - if ':' in username: - account_name, username = username.rsplit(':', 1) - - token, user = self._authorize_user(username, account_name, key, req) + token, user = self._authorize_user(username, key, req) if user and token: res = webob.Response() res.headers['X-Auth-Token'] = token.token_hash @@ -135,31 +127,15 @@ class AuthMiddleware(wsgi.Middleware): return self.auth.get_user(token.user_id) return None - def _authorize_user(self, username, account_name, key, req): + def _authorize_user(self, username, key, req): """Generates a new token and assigns it to a user. username - string - account_name - string key - string API key req - webob.Request object """ ctxt = context.get_admin_context() user = self.auth.get_user_from_access_key(key) - if account_name: - try: - account = self.auth.get_project(account_name) - except exception.NotFound: - return None, None - else: - # (dragondm) punt and try to determine account. - # this is something of a hack, but a user on 1 account is a - # common case, and is the way the current RS code works. - accounts = self.auth.get_projects(user=user) - if len(accounts) == 1: - account = accounts[0] - else: - #we can't tell what account they are logging in for. - return None, None if user and user.name == username: token_hash = hashlib.sha1('%s%s%f' % (username, key, @@ -167,11 +143,7 @@ class AuthMiddleware(wsgi.Middleware): token_dict = {} token_dict['token_hash'] = token_hash token_dict['cdn_management_url'] = '' - # auth url + project (account) id, e.g. - # http://foo.org:8774/baz/v1.0/myacct/ - os_url = '%s%s%s/' % (req.url, - '' if req.url.endswith('/') else '/', - account.id) + os_url = req.url token_dict['server_management_url'] = os_url token_dict['storage_url'] = '' token_dict['user_id'] = user.id -- cgit From 23d3be4b6f28359211e29212867157daeac9e142 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 8 Mar 2011 20:25:05 +0000 Subject: update code to work with new container and disk formats from glance --- nova/api/ec2/cloud.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 6479c9445..6b79f7f53 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -867,7 +867,8 @@ class CloudController(object): def _format_image(self, image): """Convert from format defined by BaseImageService to S3 format.""" i = {} - ec2_id = self._image_ec2_id(image.get('id'), image.get('type')) + image_type = image['properties'].get('type') + ec2_id = self._image_ec2_id(image.get('id'), image_type) name = image.get('name') if name: i['imageId'] = "%s (%s)" % (ec2_id, name) @@ -882,7 +883,7 @@ 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['type'] = image.get('type') + i['type'] = image_type i['isPublic'] = str(image['properties'].get('is_public', '')) == 'True' i['architecture'] = image['properties'].get('architecture') return i @@ -915,7 +916,8 @@ class CloudController(object): image_location = kwargs['name'] metadata = {'properties': {'image_location': image_location}} image = self.image_service.create(context, metadata) - image_id = self._image_ec2_id(image['id'], image['type']) + image_id = self._image_ec2_id(image['id'], + image['properties']['type']) msg = _("Registered image %(image_location)s with" " id %(image_id)s") % locals() LOG.audit(msg, context=context) @@ -954,6 +956,7 @@ class CloudController(object): raise exception.NotFound(_('Image %s not found') % image_id) internal_id = image['id'] del(image['id']) + raise Exception(image) image['properties']['is_public'] = (operation_type == 'add') return self.image_service.update(context, internal_id, image) -- cgit From dd2f0019297d01fe5d6b3dae4efc72946191be75 Mon Sep 17 00:00:00 2001 From: Rick Harris Date: Tue, 8 Mar 2011 22:14:25 +0000 Subject: Use disk_format and container_format instead of image type --- 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 c2bf42b72..85999764f 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -450,7 +450,7 @@ class Controller(wsgi.Controller): _("Cannot build from image %(image_id)s, status not active") % locals()) - if image['type'] != 'machine': + if image['disk_format'] != 'ami': return None, None try: -- cgit From a320b5df9f916adf8422ed312306c77570d392c2 Mon Sep 17 00:00:00 2001 From: Eric Windisch Date: Wed, 9 Mar 2011 00:30:05 -0500 Subject: execvp: almost passes tests --- 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 0d22a3f46..b7d72d1c1 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -115,7 +115,7 @@ class CloudController(object): start = os.getcwd() os.chdir(FLAGS.ca_path) # TODO(vish): Do this with M2Crypto instead - utils.runthis(_("Generating root CA: %s"), "sh genrootca.sh") + utils.runthis(_("Generating root CA: %s"), "sh", "genrootca.sh") os.chdir(start) def _get_mpi_data(self, context, project_id): -- cgit From 0f45b59ca6f9502a3ae6578e2fca5a7d9575ae5e Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Wed, 9 Mar 2011 10:37:21 -0500 Subject: Added 'adminPass' to the serialization_metadata. --- 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 9581b8477..bbedd7c63 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -96,7 +96,7 @@ class Controller(wsgi.Controller): 'application/xml': { "attributes": { "server": ["id", "imageId", "name", "flavorId", "hostId", - "status", "progress"]}}} + "status", "progress", "adminPass"]}}} def __init__(self): self.compute_api = compute.API() -- cgit From f1dea606a64c9144fb723be0e5b86806891380f8 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Wed, 9 Mar 2011 11:54:21 -0500 Subject: add override to handle xml deserialization for server instance creation --- nova/api/openstack/servers.py | 68 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 93f504f91..4a6282204 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -17,6 +17,7 @@ import base64 import hashlib import json import traceback +from xml.dom import minidom from webob import exc @@ -166,9 +167,16 @@ class Controller(wsgi.Controller): personality_files.append((path, contents)) return personality_files + def _deserialize_create(self, request): + if request.content_type == "application/xml": + deserializer = ServerCreateRequestXMLDeserializer() + return deserializer.deserialize(request.body) + else: + return self._deserialize(request.body, request) + def create(self, req): """ Creates a new server for a given user """ - env = self._deserialize(req.body, req) + env = self._deserialize_create(req) if not env: return faults.Fault(exc.HTTPUnprocessableEntity()) @@ -448,3 +456,61 @@ class Controller(wsgi.Controller): _("Ramdisk not found for image %(image_id)s") % locals()) return kernel_id, ramdisk_id + + +class ServerCreateRequestXMLDeserializer(object): + + def deserialize(self, string): + dom = minidom.parseString(string) + server = self._extract_server(dom) + return {'server': server} + + def _extract_server(self, node): + server = {} + server_node = self._find_first_child_named(node, 'server') + for attr in ["name", "imageId", "flavorId"]: + server[attr] = server_node.getAttribute(attr) + metadata = self._extract_metadata(server_node) + if metadata is not None: + server["metadata"] = metadata + personality = self._extract_personality(server_node) + if personality is not None: + server["personality"] = personality + return server + + def _extract_metadata(self, server_node): + metadata_node = self._find_first_child_named(server_node, "metadata") + if metadata_node is None: + return None + metadata = {} + for meta_node in metadata_node.childNodes: + key = meta_node.getAttribute("key") + metadata[key] = self._extract_text(meta_node) + return metadata + + def _extract_personality(self, server_node): + personality_node = \ + self._find_first_child_named(server_node, "personality") + if personality_node is None: + return None + personality = [] + for file_node in personality_node.childNodes: + item = {} + if file_node.hasAttribute("path"): + item["path"] = file_node.getAttribute("path") + item["contents"] = self._extract_text(file_node) + personality.append(item) + return personality + + def _find_first_child_named(self, parent, name): + for node in parent.childNodes: + if node.nodeName == name: + return node + return None + + def _extract_text(self, node): + if len(node.childNodes) == 1: + child = node.childNodes[0] + if child.nodeType == child.TEXT_NODE: + return child.nodeValue + return "" -- cgit From eadce208c55513ddbab550898e641b8ee55a67ec Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Wed, 9 Mar 2011 12:32:15 -0500 Subject: Fix spacing. --- 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 bbedd7c63..7222285e0 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -84,11 +84,13 @@ def _translate_detail_keys(inst): return dict(server=inst_dict) + def _translate_keys(inst): """ Coerces into dictionary format, excluding all model attributes save for id and name """ return dict(server=dict(id=inst['id'], name=inst['display_name'])) + class Controller(wsgi.Controller): """ The Server API controller for the OpenStack API """ -- cgit From 429fdb1ee733a62052c67f4e42c62447fc716ec0 Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Wed, 9 Mar 2011 18:10:45 +0000 Subject: removed uneeded **kw args leftover from removed account-in-url changes. --- nova/api/openstack/backup_schedules.py | 6 ++--- nova/api/openstack/consoles.py | 10 ++++---- nova/api/openstack/flavors.py | 6 ++--- nova/api/openstack/images.py | 12 +++++----- nova/api/openstack/servers.py | 42 +++++++++++++++++----------------- nova/api/openstack/shared_ip_groups.py | 12 +++++----- nova/api/openstack/users.py | 12 +++++----- nova/api/openstack/zones.py | 12 +++++----- 8 files changed, 56 insertions(+), 56 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py index a4d5939df..7abb5f884 100644 --- a/nova/api/openstack/backup_schedules.py +++ b/nova/api/openstack/backup_schedules.py @@ -40,15 +40,15 @@ class Controller(wsgi.Controller): def __init__(self): pass - def index(self, req, server_id, **kw): + def index(self, req, server_id): """ Returns the list of backup schedules for a given instance """ return _translate_keys({}) - def create(self, req, server_id, **kw): + def create(self, req, server_id): """ No actual update method required, since the existing API allows both create and update through a POST """ return faults.Fault(exc.HTTPNotImplemented()) - def delete(self, req, server_id, id, **kw): + def delete(self, req, server_id, id): """ Deletes an existing backup schedule """ return faults.Fault(exc.HTTPNotImplemented()) diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py index 85b2a4140..9ebdbe710 100644 --- a/nova/api/openstack/consoles.py +++ b/nova/api/openstack/consoles.py @@ -55,7 +55,7 @@ class Controller(wsgi.Controller): self.console_api = console.API() super(Controller, self).__init__() - def index(self, req, server_id, **kw): + def index(self, req, server_id): """Returns a list of consoles for this instance""" consoles = self.console_api.get_consoles( req.environ['nova.context'], @@ -63,14 +63,14 @@ class Controller(wsgi.Controller): return dict(consoles=[_translate_keys(console) for console in consoles]) - def create(self, req, server_id, **kw): + def create(self, req, server_id): """Creates a new console""" #info = self._deserialize(req.body, req) self.console_api.create_console( req.environ['nova.context'], int(server_id)) - def show(self, req, server_id, id, **kw): + def show(self, req, server_id, id): """Shows in-depth information on a specific console""" try: console = self.console_api.get_console( @@ -81,11 +81,11 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return _translate_detail_keys(console) - def update(self, req, server_id, id, **kw): + def update(self, req, server_id, id): """You can't update a console""" raise faults.Fault(exc.HTTPNotImplemented()) - def delete(self, req, server_id, id, **kw): + def delete(self, req, server_id, id): """Deletes a console""" try: self.console_api.delete_console(req.environ['nova.context'], diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index 1de67328b..f3d040ba3 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -34,17 +34,17 @@ class Controller(wsgi.Controller): "attributes": { "flavor": ["id", "name", "ram", "disk"]}}} - def index(self, req, **kw): + 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']]) - def detail(self, req, **kw): + def detail(self, req): """Return all flavors in detail.""" items = [self.show(req, id)['flavor'] for id in self._all_ids(req)] return dict(flavors=items) - def show(self, req, id, **kw): + def show(self, req, id): """Return data about the given flavor id.""" ctxt = req.environ['nova.context'] values = db.instance_type_get_by_flavor_id(ctxt, id) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 5bc5b9978..cf85a496f 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -115,14 +115,14 @@ class Controller(wsgi.Controller): def __init__(self): self._service = utils.import_object(FLAGS.image_service) - def index(self, req, **kw): + 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) - def detail(self, req, **kw): + def detail(self, req): """Return all public images in detail""" try: items = self._service.detail(req.environ['nova.context']) @@ -136,7 +136,7 @@ class Controller(wsgi.Controller): items = [_translate_status(item) for item in items] return dict(images=items) - def show(self, req, id, **kw): + 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) @@ -145,11 +145,11 @@ class Controller(wsgi.Controller): _convert_image_id_to_hash(image) return dict(image=image) - def delete(self, req, id, **kw): + def delete(self, req, id): # Only public images are supported for now. raise faults.Fault(exc.HTTPNotFound()) - def create(self, req, **kw): + def create(self, req): context = req.environ['nova.context'] env = self._deserialize(req.body, req) instance_id = env["image"]["serverId"] @@ -160,7 +160,7 @@ class Controller(wsgi.Controller): return dict(image=image_meta) - def update(self, req, id, **kw): + 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()) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 54060c2bb..c2bf42b72 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -105,11 +105,11 @@ class Controller(wsgi.Controller): self._image_service = utils.import_object(FLAGS.image_service) super(Controller, self).__init__() - def index(self, req, **kw): + def index(self, req): """ Returns a list of server names and ids for a given user """ return self._items(req, entity_maker=_translate_keys) - def detail(self, req, **kw): + def detail(self, req): """ Returns a list of server details for a given user """ return self._items(req, entity_maker=_translate_detail_keys) @@ -123,7 +123,7 @@ class Controller(wsgi.Controller): res = [entity_maker(inst)['server'] for inst in limited_list] return dict(servers=res) - def show(self, req, id, **kw): + def show(self, req, id): """ Returns server details by server id """ try: instance = self.compute_api.get(req.environ['nova.context'], id) @@ -131,7 +131,7 @@ class Controller(wsgi.Controller): except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) - def delete(self, req, id, **kw): + def delete(self, req, id): """ Destroys a server """ try: self.compute_api.delete(req.environ['nova.context'], id) @@ -139,7 +139,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() - def create(self, req, **kw): + def create(self, req): """ Creates a new server for a given user """ env = self._deserialize(req.body, req) if not env: @@ -180,7 +180,7 @@ class Controller(wsgi.Controller): onset_files=env.get('onset_files', [])) return _translate_keys(instances[0]) - def update(self, req, id, **kw): + def update(self, req, id): """ Updates the server name or password """ inst_dict = self._deserialize(req.body, req) if not inst_dict: @@ -202,7 +202,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPNoContent() - def action(self, req, id, **kw): + def action(self, req, id): """Multi-purpose method used to reboot, rebuild, or resize a server""" @@ -267,7 +267,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def lock(self, req, id, **kw): + def lock(self, req, id): """ lock the instance with id admin only operation @@ -282,7 +282,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def unlock(self, req, id, **kw): + def unlock(self, req, id): """ unlock the instance with id admin only operation @@ -297,7 +297,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def get_lock(self, req, id, **kw): + def get_lock(self, req, id): """ return the boolean state of (instance with id)'s lock @@ -311,7 +311,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def reset_network(self, req, id, **kw): + def reset_network(self, req, id): """ Reset networking on an instance (admin only). @@ -325,7 +325,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def inject_network_info(self, req, id, **kw): + def inject_network_info(self, req, id): """ Inject network info for an instance (admin only). @@ -339,7 +339,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def pause(self, req, id, **kw): + def pause(self, req, id): """ Permit Admins to Pause the server. """ ctxt = req.environ['nova.context'] try: @@ -350,7 +350,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def unpause(self, req, id, **kw): + def unpause(self, req, id): """ Permit Admins to Unpause the server. """ ctxt = req.environ['nova.context'] try: @@ -361,7 +361,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def suspend(self, req, id, **kw): + def suspend(self, req, id): """permit admins to suspend the server""" context = req.environ['nova.context'] try: @@ -372,7 +372,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def resume(self, req, id, **kw): + def resume(self, req, id): """permit admins to resume the server from suspend""" context = req.environ['nova.context'] try: @@ -383,7 +383,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def rescue(self, req, id, **kw): + def rescue(self, req, id): """Permit users to rescue the server.""" context = req.environ["nova.context"] try: @@ -394,7 +394,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def unrescue(self, req, id, **kw): + def unrescue(self, req, id): """Permit users to unrescue the server.""" context = req.environ["nova.context"] try: @@ -405,7 +405,7 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() - def get_ajax_console(self, req, id, **kw): + def get_ajax_console(self, req, id): """ Returns a url to an instance's ajaxterm console. """ try: self.compute_api.get_ajax_console(req.environ['nova.context'], @@ -414,12 +414,12 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() - def diagnostics(self, req, id, **kw): + def diagnostics(self, req, id): """Permit Admins to retrieve server diagnostics.""" ctxt = req.environ["nova.context"] return self.compute_api.get_diagnostics(ctxt, id) - def actions(self, req, id, **kw): + def actions(self, req, id): """Permit Admins to retrieve server actions.""" ctxt = req.environ["nova.context"] items = self.compute_api.get_actions(ctxt, id) diff --git a/nova/api/openstack/shared_ip_groups.py b/nova/api/openstack/shared_ip_groups.py index e3c917749..5d78f9377 100644 --- a/nova/api/openstack/shared_ip_groups.py +++ b/nova/api/openstack/shared_ip_groups.py @@ -40,26 +40,26 @@ class Controller(wsgi.Controller): 'attributes': { 'sharedIpGroup': []}}} - def index(self, req, **kw): + def index(self, req): """ Returns a list of Shared IP Groups for the user """ return dict(sharedIpGroups=[]) - def show(self, req, id, **kw): + def show(self, req, id): """ Shows in-depth information on a specific Shared IP Group """ return _translate_keys({}) - def update(self, req, id, **kw): + def update(self, req, id): """ You can't update a Shared IP Group """ raise faults.Fault(exc.HTTPNotImplemented()) - def delete(self, req, id, **kw): + def delete(self, req, id): """ Deletes a Shared IP Group """ raise faults.Fault(exc.HTTPNotImplemented()) - def detail(self, req, **kw): + def detail(self, req): """ Returns a complete list of Shared IP Groups """ return _translate_detail_keys({}) - def create(self, req, **kw): + def create(self, req): """ Creates a new Shared IP group """ raise faults.Fault(exc.HTTPNotImplemented()) diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py index ae3bf7791..83ebec964 100644 --- a/nova/api/openstack/users.py +++ b/nova/api/openstack/users.py @@ -50,28 +50,28 @@ class Controller(wsgi.Controller): if not context.is_admin: raise exception.NotAuthorized(_("Not admin user")) - def index(self, req, **kw): + def index(self, req): """Return all users in brief""" users = self.manager.get_users() users = common.limited(users, req) users = [_translate_keys(user) for user in users] return dict(users=users) - def detail(self, req, **kw): + def detail(self, req): """Return all users in detail""" return self.index(req) - def show(self, req, id, **kw): + def show(self, req, id): """Return data about the given user id""" user = self.manager.get_user(id) return dict(user=_translate_keys(user)) - def delete(self, req, id, **kw): + def delete(self, req, id): self._check_admin(req.environ['nova.context']) self.manager.delete_user(id) return {} - def create(self, req, **kw): + def create(self, req): self._check_admin(req.environ['nova.context']) env = self._deserialize(req.body, req) is_admin = env['user'].get('admin') in ('T', 'True', True) @@ -81,7 +81,7 @@ class Controller(wsgi.Controller): user = self.manager.create_user(name, access, secret, is_admin) return dict(user=_translate_keys(user)) - def update(self, req, id, **kw): + def update(self, req, id): self._check_admin(req.environ['nova.context']) env = self._deserialize(req.body, req) is_admin = env['user'].get('admin') diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index 30bf2b67b..d5206da20 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -43,35 +43,35 @@ class Controller(wsgi.Controller): "attributes": { "zone": ["id", "api_url"]}}} - def index(self, req, **kw): + def index(self, req): """Return all zones in brief""" items = db.zone_get_all(req.environ['nova.context']) items = common.limited(items, req) items = [_scrub_zone(item) for item in items] return dict(zones=items) - def detail(self, req, **kw): + def detail(self, req): """Return all zones in detail""" return self.index(req) - def show(self, req, id, **kw): + 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) return dict(zone=_scrub_zone(zone)) - def delete(self, req, id, **kw): + def delete(self, req, id): zone_id = int(id) db.zone_delete(req.environ['nova.context'], zone_id) return {} - def create(self, req, **kw): + def create(self, req): context = req.environ['nova.context'] env = self._deserialize(req.body, req) zone = db.zone_create(context, env["zone"]) return dict(zone=_scrub_zone(zone)) - def update(self, req, id, **kw): + def update(self, req, id): context = req.environ['nova.context'] env = self._deserialize(req.body, req) zone_id = int(id) -- cgit From 2c733d5365b753989b506d82d376d980cd701547 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Wed, 9 Mar 2011 14:38:34 -0500 Subject: rearrange functions and add docstrings --- nova/api/openstack/servers.py | 83 ++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 33 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 4a18d870c..419a001fb 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -141,39 +141,6 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() - def _get_personality_files(self, personality): - """ - Create a list of personality files from the personality attribute - - At this time, personality_files must be formatted as a list of - (file_path, file_content) pairs for compatibility with the - underlying compute service. - """ - personality_files = [] - for item in personality: - try: - path = item['path'] - contents = item['contents'] - except KeyError as key: - expl = 'Bad personality format: missing %s' % key - raise exc.HTTPBadRequest(explanation=expl) - except TypeError: - raise exc.HTTPBadRequest(explanation='Bad personality format') - try: - contents = base64.b64decode(contents) - except TypeError: - msg = 'Personality content for %s cannot be decoded' % path - raise exc.HTTPBadRequest(explanation=msg) - personality_files.append((path, contents)) - return personality_files - - def _deserialize_create(self, request): - if request.content_type == "application/xml": - deserializer = ServerCreateRequestXMLDeserializer() - return deserializer.deserialize(request.body) - else: - return self._deserialize(request.body, request) - def create(self, req): """ Creates a new server for a given user """ env = self._deserialize_create(req) @@ -218,6 +185,44 @@ class Controller(wsgi.Controller): personality_files=personality_files) return _translate_keys(instances[0]) + def _deserialize_create(self, request): + """ + Deserialize a create request + + Overrides normal behavior in the case of xml content + """ + if request.content_type == "application/xml": + deserializer = ServerCreateRequestXMLDeserializer() + return deserializer.deserialize(request.body) + else: + return self._deserialize(request.body, request) + + def _get_personality_files(self, personality): + """ + Create a list of personality files from the personality attribute + + At this time, personality_files must be formatted as a list of + (file_path, file_content) pairs for compatibility with the + underlying compute service. + """ + personality_files = [] + for item in personality: + try: + path = item['path'] + contents = item['contents'] + except KeyError as key: + expl = 'Bad personality format: missing %s' % key + raise exc.HTTPBadRequest(explanation=expl) + except TypeError: + raise exc.HTTPBadRequest(explanation='Bad personality format') + try: + contents = base64.b64decode(contents) + except TypeError: + msg = 'Personality content for %s cannot be decoded' % path + raise exc.HTTPBadRequest(explanation=msg) + personality_files.append((path, contents)) + return personality_files + def update(self, req, id): """ Updates the server name or password """ inst_dict = self._deserialize(req.body, req) @@ -507,13 +512,21 @@ class Controller(wsgi.Controller): class ServerCreateRequestXMLDeserializer(object): + """ + Deserializer to handle xml-formatted server create requests. + + Handles standard server attributes as well as optional metadata + and personality attributes + """ def deserialize(self, string): + """Deserialize an xml-formatted server create request""" dom = minidom.parseString(string) server = self._extract_server(dom) return {'server': server} def _extract_server(self, node): + """Marshal the server attribute of a parsed request""" server = {} server_node = self._find_first_child_named(node, 'server') for attr in ["name", "imageId", "flavorId"]: @@ -527,6 +540,7 @@ class ServerCreateRequestXMLDeserializer(object): return server def _extract_metadata(self, server_node): + """Marshal the metadata attribute of a parsed request""" metadata_node = self._find_first_child_named(server_node, "metadata") if metadata_node is None: return None @@ -537,6 +551,7 @@ class ServerCreateRequestXMLDeserializer(object): return metadata def _extract_personality(self, server_node): + """Marshal the personality attribute of a parsed request""" personality_node = \ self._find_first_child_named(server_node, "personality") if personality_node is None: @@ -551,12 +566,14 @@ class ServerCreateRequestXMLDeserializer(object): return personality def _find_first_child_named(self, parent, name): + """Search a nodes children for the first child with a given name""" for node in parent.childNodes: if node.nodeName == name: return node return None def _extract_text(self, node): + """Get the text field contained by the given node""" if len(node.childNodes) == 1: child = node.childNodes[0] if child.nodeType == child.TEXT_NODE: -- cgit From a9bd1b456332aaf5f6ab9942979485f2192b6f3e Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Wed, 9 Mar 2011 14:53:44 -0500 Subject: Add password parameter to the set_admin_password call in the compute api. Updated servers password to use this parameter. --- nova/api/openstack/servers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 7222285e0..41166f810 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -183,7 +183,8 @@ class Controller(wsgi.Controller): password = "%s%s" % (server['server']['name'][:4], utils.generate_password(12)) server['server']['adminPass'] = password - self.compute_api.set_admin_password(context, server['server']['id']) + self.compute_api.set_admin_password(context, server['server']['id'], + password) return server def update(self, req, id): -- cgit From 3f723bcf54b4d779c66373dc8f69f43923dd586a Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 9 Mar 2011 15:08:11 -0500 Subject: renaming wsgi.Request.best_match to best_match_content_type; correcting calls to that function in code from trunk --- nova/api/direct.py | 2 +- nova/api/openstack/__init__.py | 2 +- nova/api/openstack/faults.py | 2 +- nova/api/openstack/servers.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/direct.py b/nova/api/direct.py index 1d699f947..dfca250e0 100644 --- a/nova/api/direct.py +++ b/nova/api/direct.py @@ -206,7 +206,7 @@ class ServiceWrapper(wsgi.Controller): 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()) + return self._serialize(result, req.best_match_content_type()) else: return result diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 6e1a2a06c..197fcc619 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -125,5 +125,5 @@ class Versions(wsgi.Application): "application/xml": { "attributes": dict(version=["status", "id"])}} - content_type = req.best_match() + content_type = req.best_match_content_type() return wsgi.Serializer(metadata).serialize(response, content_type) diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 075fdb997..2fd733299 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -58,6 +58,6 @@ class Fault(webob.exc.HTTPException): # 'code' is an attribute on the fault tag itself metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} serializer = wsgi.Serializer(metadata) - content_type = req.best_match() + content_type = req.best_match_content_type() self.wrapped_exc.body = serializer.serialize(fault_data, content_type) return self.wrapped_exc diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 8dd078a31..25c667532 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -217,7 +217,7 @@ class Controller(wsgi.Controller): 'rebuild': self._action_rebuild, } - input_dict = self._deserialize(req.body, req) + input_dict = self._deserialize(req.body, req.get_content_type()) for key in actions.keys(): if key in input_dict: return actions[key](input_dict, req, id) -- cgit From e76aad24ce8a9b1b7de1b2f874c22c9995f3071f Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Thu, 10 Mar 2011 14:30:17 +0100 Subject: Only include ramdisk and kernel id if they are actually set. --- nova/api/ec2/cloud.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index b1917e9ea..1d2254225 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -147,8 +147,6 @@ class CloudController(object): instance_ref['id']) ec2_id = ec2utils.id_to_ec2_id(instance_ref['id']) image_ec2_id = self._image_ec2_id(instance_ref['image_id'], 'machine') - k_ec2_id = self._image_ec2_id(instance_ref['kernel_id'], 'kernel') - r_ec2_id = self._image_ec2_id(instance_ref['ramdisk_id'], 'ramdisk') data = { 'user-data': base64.b64decode(instance_ref['user_data']), 'meta-data': { @@ -167,8 +165,6 @@ class CloudController(object): 'instance-type': instance_ref['instance_type'], 'local-hostname': hostname, 'local-ipv4': address, - 'kernel-id': k_ec2_id, - 'ramdisk-id': r_ec2_id, 'placement': {'availability-zone': availability_zone}, 'public-hostname': hostname, 'public-ipv4': floating_ip or '', @@ -176,6 +172,13 @@ class CloudController(object): 'reservation-id': instance_ref['reservation_id'], 'security-groups': '', 'mpi': mpi}} + + for image_type in ['kernel', 'ramdisk']: + if '%s_id' % image_type in instance_ref: + ec2_id = self._image_ec2_id(instance_ref['%s_id' % image_type], + image_type) + data['meta-data']['%s-id' % image_type] = ec2_id + if False: # TODO(vish): store ancestor ids data['ancestor-ami-ids'] = [] if False: # TODO(vish): store product codes -- 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 be66b329d5b94ffbfb782355ef342eadbaed72a5 Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Thu, 10 Mar 2011 22:14:53 +0000 Subject: Fix a fer nits jaypipes found in review. --- nova/api/openstack/accounts.py | 18 +++++++++++++++--- nova/api/openstack/users.py | 4 ++-- 2 files changed, 17 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py index 3b90d2776..dd88c3390 100644 --- a/nova/api/openstack/accounts.py +++ b/nova/api/openstack/accounts.py @@ -21,6 +21,7 @@ from nova import log as logging from nova import wsgi from nova.auth import manager +from nova.api.openstack import faults FLAGS = flags.FLAGS LOG = logging.getLogger('nova.api.openstack') @@ -44,11 +45,17 @@ class Controller(wsgi.Controller): self.manager = manager.AuthManager() def _check_admin(self, context): - """ We cannot depend on the db layer to check for admin access - for the auth manager, so we do it here """ + """We cannot depend on the db layer to check for admin access + for the auth manager, so we do it here""" if not context.is_admin: raise exception.NotAuthorized(_("Not admin user.")) + def index(self, req): + raise faults.Fault(exc.HTTPNotImplemented()) + + def detail(self, req): + raise faults.Fault(exc.HTTPNotImplemented()) + def show(self, req, id): """Return data about the given account id""" account = self.manager.get_project(id) @@ -59,8 +66,13 @@ class Controller(wsgi.Controller): self.manager.delete_project(id) return {} + 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()) + def update(self, req, id): - """ This is really create or update. """ + """This is really create or update.""" self._check_admin(req.environ['nova.context']) env = self._deserialize(req.body, req) description = env['account'].get('description') diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py index 83ebec964..5bb20a718 100644 --- a/nova/api/openstack/users.py +++ b/nova/api/openstack/users.py @@ -45,8 +45,8 @@ class Controller(wsgi.Controller): self.manager = manager.AuthManager() def _check_admin(self, context): - """ We cannot depend on the db layer to check for admin access - for the auth manager, so we do it here """ + """We cannot depend on the db layer to check for admin access + for the auth manager, so we do it here""" if not context.is_admin: raise exception.NotAuthorized(_("Not admin user")) -- cgit From c967679fa8144af57d79d89666ee29a0241d38a9 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Thu, 10 Mar 2011 17:36:41 -0500 Subject: switch to a more consistent usage of onset_files variable names --- nova/api/openstack/servers.py | 59 ++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 20 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 7bef8eb32..adb5c5f99 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -169,20 +169,23 @@ class Controller(wsgi.Controller): metadata.append({'key': k, 'value': v}) personality = env['server'].get('personality', []) - personality_files = self._get_personality_files(personality) - - instances = self.compute_api.create( - context, - instance_types.get_by_flavor_id(env['server']['flavorId']), - image_id, - kernel_id=kernel_id, - ramdisk_id=ramdisk_id, - display_name=env['server']['name'], - display_description=env['server']['name'], - key_name=key_pair['name'], - key_data=key_pair['public_key'], - metadata=metadata, - personality_files=personality_files) + onset_files = self._get_onset_files(personality) + + try: + instances = self.compute_api.create( + context, + instance_types.get_by_flavor_id(env['server']['flavorId']), + image_id, + kernel_id=kernel_id, + ramdisk_id=ramdisk_id, + display_name=env['server']['name'], + display_description=env['server']['name'], + key_name=key_pair['name'], + key_data=key_pair['public_key'], + metadata=metadata, + onset_files=onset_files) + except QuotaError as error: + self._handle_quota_error(error) server = _translate_keys(instances[0]) password = "%s%s" % (server['server']['name'][:4], @@ -204,15 +207,15 @@ class Controller(wsgi.Controller): else: return self._deserialize(request.body, request.get_content_type()) - def _get_personality_files(self, personality): + def _get_onset_files(self, personality): """ - Create a list of personality files from the personality attribute + Create a list of onset files from the personality attribute - At this time, personality_files must be formatted as a list of + At this time, onset_files must be formatted as a list of (file_path, file_content) pairs for compatibility with the underlying compute service. """ - personality_files = [] + onset_files = [] for item in personality: try: path = item['path'] @@ -227,8 +230,24 @@ class Controller(wsgi.Controller): except TypeError: msg = 'Personality content for %s cannot be decoded' % path raise exc.HTTPBadRequest(explanation=msg) - personality_files.append((path, contents)) - return personality_files + onset_files.append((path, contents)) + return onset_files + + def _handle_quota_errors(self, error): + """ + Reraise quota errors as api-specific http exceptions + """ + if error.code == "OnsetFileLimitExceeded": + expl = "Personality file limit exceeded" + raise exc.HTTPBadRequest(explanation=expl) + if error.code == "OnsetFilePathLimitExceeded": + expl = "Personality file path too long" + raise exc.HTTPBadRequest(explanation=expl) + if error.code == "OnsetFileContentLimitExceeded": + expl = "Personality file content too long" + raise exc.HTTPBadRequest(explanation=expl) + # if the original error is okay, just reraise it + raise error def update(self, req, id): """ Updates the server name or password """ -- cgit From c540d6d7fef1da68a42c16ac1f9a44337661bb0d Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Thu, 10 Mar 2011 20:12:58 -0500 Subject: Added version attribute to RequestContext class. Set the version in the nova.context object at the middleware level. Prototyped how we can serialize ip addresses based on the version. --- nova/api/openstack/auth.py | 3 ++- nova/api/openstack/servers.py | 38 ++++++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 13 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index de8905f46..320443935 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -57,7 +57,8 @@ class AuthMiddleware(wsgi.Middleware): return faults.Fault(webob.exc.HTTPUnauthorized()) project = self.auth.get_project(FLAGS.default_project) - req.environ['nova.context'] = context.RequestContext(user, project) + req.environ['nova.context'] = context.RequestContext(user, project, + version=req.script_name.replace('/v', '')) return self.application def has_authentication(self, req): diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index dc28a0782..ec542bc92 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -39,7 +39,7 @@ LOG = logging.getLogger('server') FLAGS = flags.FLAGS -def _translate_detail_keys(inst): +def _translate_detail_keys(req, inst): """ Coerces into dictionary format, mapping everything to Rackspace-like attributes for return""" power_mapping = { @@ -54,6 +54,7 @@ def _translate_detail_keys(inst): power_state.CRASHED: 'error', power_state.FAILED: 'error'} inst_dict = {} + version = req.environ['nova.context'].version mapped_keys = dict(status='state', imageId='image_id', flavorId='instance_type', name='display_name', id='id') @@ -62,15 +63,7 @@ def _translate_detail_keys(inst): inst_dict[k] = inst[v] inst_dict['status'] = power_mapping[inst_dict['status']] - inst_dict['addresses'] = dict(public=[], private=[]) - - # grab single private fixed ip - private_ips = utils.get_from_path(inst, 'fixed_ip/address') - inst_dict['addresses']['private'] = private_ips - - # grab all public floating ips - public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') - inst_dict['addresses']['public'] = public_ips + inst_dict['addresses'] = _addresses_generator(version)(inst) # Return the metadata as a dictionary metadata = {} @@ -91,6 +84,27 @@ def _translate_keys(inst): return dict(server=dict(id=inst['id'], name=inst['display_name'])) +def _addresses_generator(version): + + def _gen_addresses_1_0(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) + + def _gen_addresses_1_1(inst): + private_ips = utils.get_from_path(inst, 'fixed_ip/address') + private_ips = [dict(version=4, addr=a) for a in private_ips] + public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') + public_ips = [dict(version=4, addr=a) for a in public_ips] + return dict(public=public_ips, private=private_ips) + + dispatch_table = { + '1.0': _gen_addresses_1_0, + '1.1': _gen_addresses_1_1, + } + + return dispatch_table[version] + class Controller(wsgi.Controller): """ The Server API controller for the OpenStack API """ @@ -120,14 +134,14 @@ class Controller(wsgi.Controller): """ instance_list = self.compute_api.get_all(req.environ['nova.context']) limited_list = common.limited(instance_list, req) - res = [entity_maker(inst)['server'] for inst in limited_list] + res = [entity_maker(req, inst)['server'] for inst in limited_list] return dict(servers=res) def show(self, req, id): """ Returns server details by server id """ try: instance = self.compute_api.get(req.environ['nova.context'], id) - return _translate_detail_keys(instance) + return _translate_detail_keys(req, instance) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) -- cgit From 76b5871adbb1bfc2e3d2a1151a00fa654c45953d Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Thu, 10 Mar 2011 20:35:37 -0500 Subject: _translate_keys now needs one more argument, the request object. --- 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 ec542bc92..bd317f995 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -78,7 +78,7 @@ def _translate_detail_keys(req, inst): return dict(server=inst_dict) -def _translate_keys(inst): +def _translate_keys(req, inst): """ Coerces into dictionary format, excluding all model attributes save for id and name """ return dict(server=dict(id=inst['id'], name=inst['display_name'])) @@ -193,7 +193,7 @@ class Controller(wsgi.Controller): metadata=metadata, onset_files=env.get('onset_files', [])) - server = _translate_keys(instances[0]) + server = _translate_keys(req, instances[0]) password = "%s%s" % (server['server']['name'][:4], utils.generate_password(12)) server['server']['adminPass'] = password -- cgit From a5415e8fc40eaa82761532e5ba83c3800cf9ed78 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Fri, 11 Mar 2011 12:45:53 -0500 Subject: Need to set version to '1.0' in the nova.context in test code for tests to be happy. --- nova/api/openstack/auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 320443935..8461a8059 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -57,8 +57,9 @@ class AuthMiddleware(wsgi.Middleware): return faults.Fault(webob.exc.HTTPUnauthorized()) project = self.auth.get_project(FLAGS.default_project) + version = req.path.split('/')[1].replace('v', '') req.environ['nova.context'] = context.RequestContext(user, project, - version=req.script_name.replace('/v', '')) + version=version) return self.application def has_authentication(self, req): -- 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 d03b169e2343fc13f37324f0136835ae54f85569 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Fri, 11 Mar 2011 16:04:54 -0500 Subject: Added support for ips resource: /servers/1/ips Refactored implmentation of how the servers response model is generated. --- nova/api/openstack/servers.py | 85 +++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 36 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index bd317f995..b486dfebb 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -39,9 +39,41 @@ LOG = logging.getLogger('server') FLAGS = flags.FLAGS -def _translate_detail_keys(req, inst): +def _translate_keys(req, inst): + """ Coerces into dictionary format, excluding all model attributes + save for id and name """ + return dict(server=dict(id=inst['id'], name=inst['display_name'])) + + +def _build_addresses_10(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) + + +def _build_addresses_11(inst): + private_ips = utils.get_from_path(inst, 'fixed_ip/address') + private_ips = [dict(version=4, addr=a) for a in private_ips] + public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') + public_ips = [dict(version=4, addr=a) for a in public_ips] + return dict(public=public_ips, private=private_ips) + + +def addresses_builder(req): + version = req.environ['nova.context'].version + if version == '1.1': + return _build_addresses_11 + else: + return _build_addresses_10 + + +def build_server(req, inst, is_detail): """ Coerces into dictionary format, mapping everything to Rackspace-like attributes for return""" + + if not is_detail: + return dict(server=dict(id=inst['id'], name=inst['display_name'])) + power_mapping = { None: 'build', power_state.NOSTATE: 'build', @@ -63,7 +95,7 @@ def _translate_detail_keys(req, inst): inst_dict[k] = inst[v] inst_dict['status'] = power_mapping[inst_dict['status']] - inst_dict['addresses'] = _addresses_generator(version)(inst) + inst_dict['addresses'] = addresses_builder(req)(inst) # Return the metadata as a dictionary metadata = {} @@ -78,33 +110,6 @@ def _translate_detail_keys(req, inst): return dict(server=inst_dict) -def _translate_keys(req, inst): - """ Coerces into dictionary format, excluding all model attributes - save for id and name """ - return dict(server=dict(id=inst['id'], name=inst['display_name'])) - - -def _addresses_generator(version): - - def _gen_addresses_1_0(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) - - def _gen_addresses_1_1(inst): - private_ips = utils.get_from_path(inst, 'fixed_ip/address') - private_ips = [dict(version=4, addr=a) for a in private_ips] - public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') - public_ips = [dict(version=4, addr=a) for a in public_ips] - return dict(public=public_ips, private=private_ips) - - dispatch_table = { - '1.0': _gen_addresses_1_0, - '1.1': _gen_addresses_1_1, - } - - return dispatch_table[version] - class Controller(wsgi.Controller): """ The Server API controller for the OpenStack API """ @@ -119,29 +124,37 @@ class Controller(wsgi.Controller): self._image_service = utils.import_object(FLAGS.image_service) super(Controller, self).__init__() + def ips(self, req, id): + try: + instance = self.compute_api.get(req.environ['nova.context'], id) + return addresses_builder(req)(instance) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + def index(self, req): """ Returns a list of server names and ids for a given user """ - return self._items(req, entity_maker=_translate_keys) + return self._items(req, is_detail=False) def detail(self, req): """ Returns a list of server details for a given user """ - return self._items(req, entity_maker=_translate_detail_keys) + return self._items(req, is_detail=True) - def _items(self, req, entity_maker): + def _items(self, req, is_detail): """Returns a list of servers for a given user. - entity_maker - either _translate_detail_keys or _translate_keys + builder - the response model builder """ instance_list = self.compute_api.get_all(req.environ['nova.context']) limited_list = common.limited(instance_list, req) - res = [entity_maker(req, inst)['server'] for inst in limited_list] + res = [build_server(req, inst, is_detail)['server'] + for inst in limited_list] return dict(servers=res) def show(self, req, id): """ Returns server details by server id """ try: instance = self.compute_api.get(req.environ['nova.context'], id) - return _translate_detail_keys(req, instance) + return build_server(req, instance, is_detail=True) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -193,7 +206,7 @@ class Controller(wsgi.Controller): metadata=metadata, onset_files=env.get('onset_files', [])) - server = _translate_keys(req, instances[0]) + server = build_server(req, instances[0], is_detail=False) password = "%s%s" % (server['server']['name'][:4], utils.generate_password(12)) server['server']['adminPass'] = password -- cgit From be9734b03bce871d32e21da2ba341dfa42aa020a Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Fri, 11 Mar 2011 17:19:14 -0500 Subject: Fixed lp732866 by catching relevant `exception.NotFound` exception. Tests did not uncover this vulnerability due to "incorrect" FakeAuthManager. I say "incorrect" because potentially different implementations (LDAP or Database driven) of AuthManager might return different errors from `get_user_from_access_key`. Also, removed all references to 'bacon', 'ham', 'herp', and 'derp' and replaced them with hopefully more helpful terms. --- nova/api/openstack/auth.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 4c6b58eff..f3a9bdeca 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -135,7 +135,11 @@ class AuthMiddleware(wsgi.Middleware): req - wsgi.Request object """ ctxt = context.get_admin_context() - user = self.auth.get_user_from_access_key(key) + + try: + user = self.auth.get_user_from_access_key(key) + except exception.NotFound: + user = None if user and user.name == username: token_hash = hashlib.sha1('%s%s%f' % (username, key, -- cgit From 87f7356e98dbb4d01305785ed8209f44b525ff2c Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Fri, 11 Mar 2011 19:21:34 -0500 Subject: Removed _translate_keys() functions since it is no longer used. Moved private top level functions to bottom of module. --- nova/api/openstack/servers.py | 148 ++++++++++++++++++++---------------------- 1 file changed, 71 insertions(+), 77 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index b486dfebb..940c2c47e 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -34,82 +34,9 @@ import nova.api.openstack LOG = logging.getLogger('server') - - FLAGS = flags.FLAGS -def _translate_keys(req, inst): - """ Coerces into dictionary format, excluding all model attributes - save for id and name """ - return dict(server=dict(id=inst['id'], name=inst['display_name'])) - - -def _build_addresses_10(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) - - -def _build_addresses_11(inst): - private_ips = utils.get_from_path(inst, 'fixed_ip/address') - private_ips = [dict(version=4, addr=a) for a in private_ips] - public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') - public_ips = [dict(version=4, addr=a) for a in public_ips] - return dict(public=public_ips, private=private_ips) - - -def addresses_builder(req): - version = req.environ['nova.context'].version - if version == '1.1': - return _build_addresses_11 - else: - return _build_addresses_10 - - -def build_server(req, inst, is_detail): - """ Coerces into dictionary format, mapping everything to Rackspace-like - attributes for return""" - - if not is_detail: - return dict(server=dict(id=inst['id'], name=inst['display_name'])) - - power_mapping = { - None: 'build', - power_state.NOSTATE: 'build', - power_state.RUNNING: 'active', - power_state.BLOCKED: 'active', - power_state.SUSPENDED: 'suspended', - power_state.PAUSED: 'paused', - power_state.SHUTDOWN: 'active', - power_state.SHUTOFF: 'active', - power_state.CRASHED: 'error', - power_state.FAILED: 'error'} - inst_dict = {} - version = req.environ['nova.context'].version - - mapped_keys = dict(status='state', imageId='image_id', - flavorId='instance_type', 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['addresses'] = addresses_builder(req)(inst) - - # Return the metadata as a dictionary - metadata = {} - for item in inst['metadata']: - metadata[item['key']] = item['value'] - inst_dict['metadata'] = metadata - - inst_dict['hostId'] = '' - if inst['host']: - inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() - - return dict(server=inst_dict) - - class Controller(wsgi.Controller): """ The Server API controller for the OpenStack API """ @@ -127,7 +54,7 @@ class Controller(wsgi.Controller): def ips(self, req, id): try: instance = self.compute_api.get(req.environ['nova.context'], id) - return addresses_builder(req)(instance) + return _addresses_builder(req)(instance) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -146,7 +73,7 @@ class Controller(wsgi.Controller): """ instance_list = self.compute_api.get_all(req.environ['nova.context']) limited_list = common.limited(instance_list, req) - res = [build_server(req, inst, is_detail)['server'] + res = [_build_server(req, inst, is_detail)['server'] for inst in limited_list] return dict(servers=res) @@ -154,7 +81,7 @@ class Controller(wsgi.Controller): """ Returns server details by server id """ try: instance = self.compute_api.get(req.environ['nova.context'], id) - return build_server(req, instance, is_detail=True) + return _build_server(req, instance, is_detail=True) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -206,7 +133,7 @@ class Controller(wsgi.Controller): metadata=metadata, onset_files=env.get('onset_files', [])) - server = build_server(req, instances[0], is_detail=False) + server = _build_server(req, instances[0], is_detail=False) password = "%s%s" % (server['server']['name'][:4], utils.generate_password(12)) server['server']['adminPass'] = password @@ -503,3 +430,70 @@ class Controller(wsgi.Controller): _("Ramdisk not found for image %(image_id)s") % locals()) return kernel_id, ramdisk_id + + +def _build_server(req, inst, is_detail): + """ Coerces into dictionary format, mapping everything to Rackspace-like + attributes for return""" + + if not is_detail: + return dict(server=dict(id=inst['id'], name=inst['display_name'])) + + power_mapping = { + None: 'build', + power_state.NOSTATE: 'build', + power_state.RUNNING: 'active', + power_state.BLOCKED: 'active', + power_state.SUSPENDED: 'suspended', + power_state.PAUSED: 'paused', + power_state.SHUTDOWN: 'active', + power_state.SHUTOFF: 'active', + power_state.CRASHED: 'error', + power_state.FAILED: 'error'} + inst_dict = {} + version = req.environ['nova.context'].version + + mapped_keys = dict(status='state', imageId='image_id', + flavorId='instance_type', 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['addresses'] = _addresses_builder(req)(inst) + + # Return the metadata as a dictionary + metadata = {} + for item in inst['metadata']: + metadata[item['key']] = item['value'] + inst_dict['metadata'] = metadata + + inst_dict['hostId'] = '' + if inst['host']: + inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() + + return dict(server=inst_dict) + + +def _addresses_builder(req): + version = req.environ['nova.context'].version + if version == '1.1': + return _build_addresses_11 + else: + return _build_addresses_10 + + +def _build_addresses_10(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) + + +def _build_addresses_11(inst): + private_ips = utils.get_from_path(inst, 'fixed_ip/address') + private_ips = [dict(version=4, addr=a) for a in private_ips] + public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') + public_ips = [dict(version=4, addr=a) for a in public_ips] + return dict(public=public_ips, private=private_ips) + + -- 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 266ea0bdd1da014a3cf23c7003f7fc932f447d35 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Mon, 14 Mar 2011 15:02:59 -0400 Subject: Create v1_0 and v1_1 packages for the openstack api. Added a servers module to each. Added tests to validate the structure of ip addresses for a 1.1 request. --- nova/api/openstack/servers.py | 28 +++++++--------------------- nova/api/openstack/v1_0/__init__.py | 0 nova/api/openstack/v1_0/servers.py | 6 ++++++ nova/api/openstack/v1_1/__init__.py | 0 nova/api/openstack/v1_1/servers.py | 8 ++++++++ 5 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 nova/api/openstack/v1_0/__init__.py create mode 100644 nova/api/openstack/v1_0/servers.py create mode 100644 nova/api/openstack/v1_1/__init__.py create mode 100644 nova/api/openstack/v1_1/servers.py (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 940c2c47e..0d36546d7 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -27,6 +27,8 @@ from nova import wsgi from nova import utils from nova.api.openstack import common from nova.api.openstack import faults +from nova.api.openstack.v1_0 import servers as v1_0 +from nova.api.openstack.v1_1 import servers as v1_1 from nova.auth import manager as auth_manager from nova.compute import instance_types from nova.compute import power_state @@ -54,7 +56,7 @@ class Controller(wsgi.Controller): def ips(self, req, id): try: instance = self.compute_api.get(req.environ['nova.context'], id) - return _addresses_builder(req)(instance) + return _get_addresses_builder(req)(instance) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -460,7 +462,7 @@ def _build_server(req, inst, is_detail): inst_dict[k] = inst[v] inst_dict['status'] = power_mapping[inst_dict['status']] - inst_dict['addresses'] = _addresses_builder(req)(inst) + inst_dict['addresses'] = _get_addresses_builder(req)(inst) # Return the metadata as a dictionary metadata = {} @@ -475,25 +477,9 @@ def _build_server(req, inst, is_detail): return dict(server=inst_dict) -def _addresses_builder(req): +def _get_addresses_builder(req): version = req.environ['nova.context'].version if version == '1.1': - return _build_addresses_11 + return v1_1.build_addresses else: - return _build_addresses_10 - - -def _build_addresses_10(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) - - -def _build_addresses_11(inst): - private_ips = utils.get_from_path(inst, 'fixed_ip/address') - private_ips = [dict(version=4, addr=a) for a in private_ips] - public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') - public_ips = [dict(version=4, addr=a) for a in public_ips] - return dict(public=public_ips, private=private_ips) - - + return v1_0.build_addresses diff --git a/nova/api/openstack/v1_0/__init__.py b/nova/api/openstack/v1_0/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nova/api/openstack/v1_0/servers.py b/nova/api/openstack/v1_0/servers.py new file mode 100644 index 000000000..d332b1378 --- /dev/null +++ b/nova/api/openstack/v1_0/servers.py @@ -0,0 +1,6 @@ +from nova import utils + +def build_addresses(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) diff --git a/nova/api/openstack/v1_1/__init__.py b/nova/api/openstack/v1_1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nova/api/openstack/v1_1/servers.py b/nova/api/openstack/v1_1/servers.py new file mode 100644 index 000000000..012fc3e5c --- /dev/null +++ b/nova/api/openstack/v1_1/servers.py @@ -0,0 +1,8 @@ +from nova import utils + +def build_addresses(inst): + private_ips = utils.get_from_path(inst, 'fixed_ip/address') + private_ips = [dict(version=4, addr=a) for a in private_ips] + public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') + public_ips = [dict(version=4, addr=a) for a in public_ips] + return dict(public=public_ips, private=private_ips) -- 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 a4e94971b696681a5ced189d8f4263c8f77cc531 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 14 Mar 2011 19:57:30 -0700 Subject: Make key_pair optional with OpenStack API --- nova/api/openstack/servers.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index dc28a0782..47ed254ec 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -146,10 +146,14 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) context = req.environ['nova.context'] + + key_name = None + key_data = None key_pairs = auth_manager.AuthManager.get_key_pairs(context) - if not key_pairs: - raise exception.NotFound(_("No keypairs defined")) - key_pair = key_pairs[0] + if key_pairs: + key_pair = key_pairs[0] + key_name = key_pair['name'] + key_data = key_pair['public_key'] image_id = common.get_image_id_from_image_hash(self._image_service, context, env['server']['imageId']) @@ -174,8 +178,8 @@ class Controller(wsgi.Controller): ramdisk_id=ramdisk_id, display_name=env['server']['name'], display_description=env['server']['name'], - key_name=key_pair['name'], - key_data=key_pair['public_key'], + key_name=key_name, + key_data=key_data, metadata=metadata, onset_files=env.get('onset_files', [])) -- cgit From d5b9391e2911ba2210a045a2af380dfc85d16919 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Mon, 14 Mar 2011 23:14:59 -0400 Subject: Added a views package and a views.servers module. For representing the response object before it is serialized. --- nova/api/openstack/servers.py | 65 +++++------------------------- nova/api/openstack/v1_0/__init__.py | 0 nova/api/openstack/v1_0/servers.py | 6 --- nova/api/openstack/v1_1/__init__.py | 0 nova/api/openstack/v1_1/servers.py | 8 ---- nova/api/openstack/views/__init__.py | 0 nova/api/openstack/views/servers.py | 76 ++++++++++++++++++++++++++++++++++++ 7 files changed, 86 insertions(+), 69 deletions(-) delete mode 100644 nova/api/openstack/v1_0/__init__.py delete mode 100644 nova/api/openstack/v1_0/servers.py delete mode 100644 nova/api/openstack/v1_1/__init__.py delete mode 100644 nova/api/openstack/v1_1/servers.py create mode 100644 nova/api/openstack/views/__init__.py create mode 100644 nova/api/openstack/views/servers.py (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 0d36546d7..ea8321e8d 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -27,8 +27,7 @@ from nova import wsgi from nova import utils from nova.api.openstack import common from nova.api.openstack import faults -from nova.api.openstack.v1_0 import servers as v1_0 -from nova.api.openstack.v1_1 import servers as v1_1 +from nova.api.openstack.views.servers import get_view_builder from nova.auth import manager as auth_manager from nova.compute import instance_types from nova.compute import power_state @@ -56,7 +55,8 @@ class Controller(wsgi.Controller): def ips(self, req, id): try: instance = self.compute_api.get(req.environ['nova.context'], id) - return _get_addresses_builder(req)(instance) + builder = get_view_builder(req) + return builder._build_addresses(instance) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -75,15 +75,17 @@ class Controller(wsgi.Controller): """ instance_list = self.compute_api.get_all(req.environ['nova.context']) limited_list = common.limited(instance_list, req) - res = [_build_server(req, inst, is_detail)['server'] + builder = get_view_builder(req) + servers = [builder.build(inst, is_detail)['server'] for inst in limited_list] - return dict(servers=res) + return dict(servers=servers) def show(self, req, id): """ Returns server details by server id """ try: instance = self.compute_api.get(req.environ['nova.context'], id) - return _build_server(req, instance, is_detail=True) + builder = get_view_builder(req) + return builder.build(instance, is_detail=True) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -135,7 +137,8 @@ class Controller(wsgi.Controller): metadata=metadata, onset_files=env.get('onset_files', [])) - server = _build_server(req, instances[0], is_detail=False) + builder = get_view_builder(req) + server = builder.build(instances[0], is_detail=False) password = "%s%s" % (server['server']['name'][:4], utils.generate_password(12)) server['server']['adminPass'] = password @@ -434,52 +437,4 @@ class Controller(wsgi.Controller): return kernel_id, ramdisk_id -def _build_server(req, inst, is_detail): - """ Coerces into dictionary format, mapping everything to Rackspace-like - attributes for return""" - if not is_detail: - return dict(server=dict(id=inst['id'], name=inst['display_name'])) - - power_mapping = { - None: 'build', - power_state.NOSTATE: 'build', - power_state.RUNNING: 'active', - power_state.BLOCKED: 'active', - power_state.SUSPENDED: 'suspended', - power_state.PAUSED: 'paused', - power_state.SHUTDOWN: 'active', - power_state.SHUTOFF: 'active', - power_state.CRASHED: 'error', - power_state.FAILED: 'error'} - inst_dict = {} - version = req.environ['nova.context'].version - - mapped_keys = dict(status='state', imageId='image_id', - flavorId='instance_type', 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['addresses'] = _get_addresses_builder(req)(inst) - - # Return the metadata as a dictionary - metadata = {} - for item in inst['metadata']: - metadata[item['key']] = item['value'] - inst_dict['metadata'] = metadata - - inst_dict['hostId'] = '' - if inst['host']: - inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() - - return dict(server=inst_dict) - - -def _get_addresses_builder(req): - version = req.environ['nova.context'].version - if version == '1.1': - return v1_1.build_addresses - else: - return v1_0.build_addresses diff --git a/nova/api/openstack/v1_0/__init__.py b/nova/api/openstack/v1_0/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nova/api/openstack/v1_0/servers.py b/nova/api/openstack/v1_0/servers.py deleted file mode 100644 index d332b1378..000000000 --- a/nova/api/openstack/v1_0/servers.py +++ /dev/null @@ -1,6 +0,0 @@ -from nova import utils - -def build_addresses(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) diff --git a/nova/api/openstack/v1_1/__init__.py b/nova/api/openstack/v1_1/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nova/api/openstack/v1_1/servers.py b/nova/api/openstack/v1_1/servers.py deleted file mode 100644 index 012fc3e5c..000000000 --- a/nova/api/openstack/v1_1/servers.py +++ /dev/null @@ -1,8 +0,0 @@ -from nova import utils - -def build_addresses(inst): - private_ips = utils.get_from_path(inst, 'fixed_ip/address') - private_ips = [dict(version=4, addr=a) for a in private_ips] - public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') - public_ips = [dict(version=4, addr=a) for a in public_ips] - return dict(public=public_ips, private=private_ips) diff --git a/nova/api/openstack/views/__init__.py b/nova/api/openstack/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py new file mode 100644 index 000000000..120cae4d4 --- /dev/null +++ b/nova/api/openstack/views/servers.py @@ -0,0 +1,76 @@ +import hashlib +from nova.compute import power_state +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 = req.environ['nova.context'].version + if version == '1.1': + return DataViewBuilder_1_1() + else: + return DataViewBuilder_1_0() + + +class DataViewBuilder(object): + ''' Models a server response as a python dictionary. ''' + + def build(self, inst, is_detail): + """ Coerces into dictionary format, mapping everything to Rackspace-like + attributes for return""" + + if not is_detail: + return dict(server=dict(id=inst['id'], name=inst['display_name'])) + + power_mapping = { + None: 'build', + power_state.NOSTATE: 'build', + power_state.RUNNING: 'active', + power_state.BLOCKED: 'active', + power_state.SUSPENDED: 'suspended', + power_state.PAUSED: 'paused', + power_state.SHUTDOWN: 'active', + power_state.SHUTOFF: 'active', + power_state.CRASHED: 'error', + power_state.FAILED: 'error'} + inst_dict = {} + + mapped_keys = dict(status='state', imageId='image_id', + flavorId='instance_type', 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['addresses'] = self._build_addresses(inst) + + # Return the metadata as a dictionary + metadata = {} + for item in inst['metadata']: + metadata[item['key']] = item['value'] + inst_dict['metadata'] = metadata + + inst_dict['hostId'] = '' + if inst['host']: + inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() + + return dict(server=inst_dict) + + +class DataViewBuilder_1_0(DataViewBuilder): + def _build_addresses(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 DataViewBuilder_1_1(DataViewBuilder): + def _build_addresses(self, inst): + private_ips = utils.get_from_path(inst, 'fixed_ip/address') + private_ips = [dict(version=4, addr=a) for a in private_ips] + public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') + public_ips = [dict(version=4, addr=a) for a in public_ips] + return dict(public=public_ips, private=private_ips) + -- cgit From c70e3777a488a63062c030e9949e9c16f2269f9c Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 14 Mar 2011 23:55:44 -0400 Subject: moving addresses views to new module; removing 'Data' from 'DataViewBuilder' --- nova/api/openstack/views/servers.py | 39 ++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 18 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 120cae4d4..3ccfd8dba 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -1,29 +1,40 @@ import hashlib from nova.compute import power_state +from nova.api.openstack.views import addresses as addresses_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 = req.environ['nova.context'].version + addresses_builder = addresses_view.get_view_builder(req) if version == '1.1': - return DataViewBuilder_1_1() + return ViewBuilder_1_1(addresses_builder) else: - return DataViewBuilder_1_0() + return ViewBuilder_1_0(addresses_builder) + +class ViewBuilder(object): + ''' Models a server response as a python dictionary.''' -class DataViewBuilder(object): - ''' Models a server response as a python dictionary. ''' + def __init__(self, addresses_builder): + self.addresses_builder = addresses_builder def build(self, inst, is_detail): """ Coerces into dictionary format, mapping everything to Rackspace-like attributes for return""" + if is_detail: + return self._build_detail(inst) + else: + return self._build_simple(inst) - if not is_detail: + def _build_simple(self, inst): return dict(server=dict(id=inst['id'], name=inst['display_name'])) + def _build_detail(self, inst): power_mapping = { None: 'build', power_state.NOSTATE: 'build', @@ -44,7 +55,7 @@ class DataViewBuilder(object): inst_dict[k] = inst[v] inst_dict['status'] = power_mapping[inst_dict['status']] - inst_dict['addresses'] = self._build_addresses(inst) + inst_dict['addresses'] = self.addresses_builder.build(inst) # Return the metadata as a dictionary metadata = {} @@ -59,18 +70,10 @@ class DataViewBuilder(object): return dict(server=inst_dict) -class DataViewBuilder_1_0(DataViewBuilder): - def _build_addresses(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_0(ViewBuilder): + pass -class DataViewBuilder_1_1(DataViewBuilder): - def _build_addresses(self, inst): - private_ips = utils.get_from_path(inst, 'fixed_ip/address') - private_ips = [dict(version=4, addr=a) for a in private_ips] - public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') - public_ips = [dict(version=4, addr=a) for a in public_ips] - return dict(public=public_ips, private=private_ips) +class ViewBuilder_1_1(ViewBuilder): + pass -- cgit From 18cd549ba8d7aa4c688a7f7a5e940acbaaa03acc Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 15 Mar 2011 00:49:20 -0400 Subject: adding flavors and images barebones view code; adding flavorRef and imageRef to v1.1 servers --- nova/api/openstack/servers.py | 13 ++++++------ nova/api/openstack/views/servers.py | 42 ++++++++++++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 11 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index ea8321e8d..7bb7250ba 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -27,7 +27,8 @@ 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.servers import get_view_builder +from nova.api.openstack.views import servers as servers_views +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 @@ -55,8 +56,8 @@ class Controller(wsgi.Controller): def ips(self, req, id): try: instance = self.compute_api.get(req.environ['nova.context'], id) - builder = get_view_builder(req) - return builder._build_addresses(instance) + builder = addresses_views.get_view_builder(req) + return builder.build(instance) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -75,7 +76,7 @@ class Controller(wsgi.Controller): """ instance_list = self.compute_api.get_all(req.environ['nova.context']) limited_list = common.limited(instance_list, req) - builder = get_view_builder(req) + builder = servers_views.get_view_builder(req) servers = [builder.build(inst, is_detail)['server'] for inst in limited_list] return dict(servers=servers) @@ -84,7 +85,7 @@ class Controller(wsgi.Controller): """ Returns server details by server id """ try: instance = self.compute_api.get(req.environ['nova.context'], id) - builder = get_view_builder(req) + builder = servers_views.get_view_builder(req) return builder.build(instance, is_detail=True) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -137,7 +138,7 @@ class Controller(wsgi.Controller): metadata=metadata, onset_files=env.get('onset_files', [])) - builder = get_view_builder(req) + builder = servers_views.get_view_builder(req) server = builder.build(instances[0], is_detail=False) password = "%s%s" % (server['server']['name'][:4], utils.generate_password(12)) diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 3ccfd8dba..950662747 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -1,6 +1,8 @@ import hashlib from nova.compute import power_state from nova.api.openstack.views import addresses as addresses_view +from nova.api.openstack.views import flavors as flavors_view +from nova.api.openstack.views import images as images_view from nova import utils @@ -12,7 +14,9 @@ def get_view_builder(req): version = req.environ['nova.context'].version addresses_builder = addresses_view.get_view_builder(req) if version == '1.1': - return ViewBuilder_1_1(addresses_builder) + 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) @@ -48,8 +52,10 @@ 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', 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] @@ -67,13 +73,39 @@ class ViewBuilder(object): if inst['host']: inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() + inst_dict = self._decorate_response(inst_dict, inst) + return dict(server=inst_dict) + def _build_image_data(self, response, inst): + raise NotImplementedError() + class ViewBuilder_1_0(ViewBuilder): - pass + def _decorate_response(self, response, inst): + response["imageId"] = inst["image_id"] + response["flavorId"] = inst["instance_type"] + return response class ViewBuilder_1_1(ViewBuilder): - pass + 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 _decorate_response(self, response, inst): + response = self._build_image_ref(response, inst) + response = self._build_flavor_ref(response, inst) + return response + + def _build_image_ref(self, response, inst): + image_id = inst["image_id"] + response["imageRef"] = self.image_builder.generate_href(image_id) + return response + + def _build_flavor_ref(self, response, inst): + flavor_id = inst["instance_type"] + response["flavorRef"]= self.flavor_builder.generate_href(flavor_id) + return response -- cgit From 354f5e61c4bfb32ad8c2bc3389678f19db5fdb56 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 15 Mar 2011 00:55:52 -0400 Subject: pep8 fixes --- nova/api/openstack/servers.py | 3 --- nova/api/openstack/views/servers.py | 12 +++++++----- 2 files changed, 7 insertions(+), 8 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 7bb7250ba..de67cbc4a 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -436,6 +436,3 @@ class Controller(wsgi.Controller): _("Ramdisk not found for image %(image_id)s") % locals()) return kernel_id, ramdisk_id - - - diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 950662747..15ac9964c 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -16,7 +16,8 @@ def 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) + return ViewBuilder_1_1(addresses_builder, flavor_builder, + image_builder) else: return ViewBuilder_1_0(addresses_builder) @@ -28,8 +29,10 @@ 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""" + """ + Coerces into dictionary format, mapping everything to + Rackspace-like attributes for return + """ if is_detail: return self._build_detail(inst) else: @@ -106,6 +109,5 @@ class ViewBuilder_1_1(ViewBuilder): def _build_flavor_ref(self, response, inst): flavor_id = inst["instance_type"] - response["flavorRef"]= self.flavor_builder.generate_href(flavor_id) + response["flavorRef"] = self.flavor_builder.generate_href(flavor_id) return response - -- 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 e161b00349a7478ac9f51f087c9f16cd345bc2d2 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 15 Mar 2011 13:23:42 -0400 Subject: adding missing view modules; modifying a couple of servers tests to use enumerate --- nova/api/openstack/views/addresses.py | 38 ++++++++++++++++++++++++++++++++++ nova/api/openstack/views/flavors.py | 39 +++++++++++++++++++++++++++++++++++ nova/api/openstack/views/images.py | 39 +++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 nova/api/openstack/views/addresses.py create mode 100644 nova/api/openstack/views/flavors.py create mode 100644 nova/api/openstack/views/images.py (limited to 'nova/api') diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py new file mode 100644 index 000000000..d764e5229 --- /dev/null +++ b/nova/api/openstack/views/addresses.py @@ -0,0 +1,38 @@ +import hashlib +from nova.compute import power_state +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 = req.environ['nova.context'].version + 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.''' + + def build(self, inst): + raise NotImplementedError() + + +class ViewBuilder_1_0(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): + 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] + public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') + public_ips = [dict(version=4, addr=a) for a in public_ips] + return dict(public=public_ips, private=private_ips) diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py new file mode 100644 index 000000000..c6b6c10bb --- /dev/null +++ b/nova/api/openstack/views/flavors.py @@ -0,0 +1,39 @@ + + +def get_view_builder(req): + ''' + A factory method that returns the correct builder based on the version of + the api requested. + ''' + version = req.environ['nova.context'].version + 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): + pass + + def build(self, flavor_obj): + raise NotImplementedError() + + def _decorate_response(self, response, flavor_obj): + return response + + +class ViewBuilder_1_1(ViewBuilder): + def __init__(self, base_url): + self.base_url = base_url + + def _decorate_response(self, response, flavor_obj): + raise NotImplementedError() + + def generate_href(self, flavor_id): + return "{0}/flavors/{1}".format(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 new file mode 100644 index 000000000..c80713250 --- /dev/null +++ b/nova/api/openstack/views/images.py @@ -0,0 +1,39 @@ + + +def get_view_builder(req): + ''' + A factory method that returns the correct builder based on the version of + the api requested. + ''' + version = req.environ['nova.context'].version + 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): + pass + + def build(self, image_obj): + raise NotImplementedError() + + def _decorate_response(self, response, image_obj): + return response + + +class ViewBuilder_1_1(ViewBuilder): + def __init__(self, base_url): + self.base_url = base_url + + def _decorate_response(self, response, image_obj): + raise NotImplementedError() + + def generate_href(self, image_id): + return "{0}/images/{1}".format(self.base_url, image_id) + + +class ViewBuilder_1_0(ViewBuilder): + pass -- cgit From 937c135ec0c8b557b22ad30c400c75c713f660e1 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Tue, 15 Mar 2011 14:31:48 -0400 Subject: Code clean up. Removing _decorate_response methods. Replaced them with more explicit methods, _build_image, and _build_flavor. --- nova/api/openstack/views/flavors.py | 6 ------ nova/api/openstack/views/images.py | 6 ------ nova/api/openstack/views/servers.py | 30 ++++++++++++++++-------------- 3 files changed, 16 insertions(+), 26 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index c6b6c10bb..dfcc2644c 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -20,17 +20,11 @@ class ViewBuilder(object): def build(self, flavor_obj): raise NotImplementedError() - def _decorate_response(self, response, flavor_obj): - return response - class ViewBuilder_1_1(ViewBuilder): def __init__(self, base_url): self.base_url = base_url - def _decorate_response(self, response, flavor_obj): - raise NotImplementedError() - def generate_href(self, flavor_id): return "{0}/flavors/{1}".format(self.base_url, flavor_id) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index c80713250..cd61ed656 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -20,17 +20,11 @@ class ViewBuilder(object): def build(self, image_obj): raise NotImplementedError() - def _decorate_response(self, response, image_obj): - return response - class ViewBuilder_1_1(ViewBuilder): def __init__(self, base_url): self.base_url = base_url - def _decorate_response(self, response, image_obj): - raise NotImplementedError() - def generate_href(self, image_id): return "{0}/images/{1}".format(self.base_url, image_id) diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 15ac9964c..708c74b4e 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -23,7 +23,10 @@ def get_view_builder(req): class ViewBuilder(object): - ''' Models a server response as a python dictionary.''' + ''' + Models a server response as a python dictionary. + Abstract methods: _build_image, _build_flavor + ''' def __init__(self, addresses_builder): self.addresses_builder = addresses_builder @@ -76,19 +79,24 @@ class ViewBuilder(object): if inst['host']: inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() - inst_dict = self._decorate_response(inst_dict, inst) + self._build_image(inst_dict, inst) + self._build_flavor(inst_dict, inst) return dict(server=inst_dict) - def _build_image_data(self, response, inst): + def _build_image(self, response, inst): + raise NotImplementedError() + + def _build_flavor(self, response, inst): raise NotImplementedError() class ViewBuilder_1_0(ViewBuilder): - def _decorate_response(self, response, inst): + def _build_image(self, response, inst): response["imageId"] = inst["image_id"] + + def _build_flavor(self, response, inst): response["flavorId"] = inst["instance_type"] - return response class ViewBuilder_1_1(ViewBuilder): @@ -97,17 +105,11 @@ class ViewBuilder_1_1(ViewBuilder): self.flavor_builder = flavor_builder self.image_builder = image_builder - def _decorate_response(self, response, inst): - response = self._build_image_ref(response, inst) - response = self._build_flavor_ref(response, inst) - return response - - def _build_image_ref(self, response, inst): + def _build_image(self, response, inst): image_id = inst["image_id"] response["imageRef"] = self.image_builder.generate_href(image_id) - return response - def _build_flavor_ref(self, response, inst): + def _build_flavor(self, response, inst): flavor_id = inst["instance_type"] response["flavorRef"] = self.flavor_builder.generate_href(flavor_id) - return response + -- cgit From cc25d277755f0e103ff09144d1d490536ab9acec Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 15 Mar 2011 15:56:54 -0400 Subject: modifying paste config to support v1.1; adding v1.1 entry in versions resource ( GET /) --- nova/api/openstack/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index ce3cff337..0244bc93c 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -128,8 +128,11 @@ class Versions(wsgi.Application): def __call__(self, req): """Respond to a request for all OpenStack API versions.""" response = { - "versions": [ - dict(status="CURRENT", id="v1.0")]} + "versions": [ + dict(status="DEPRECATED", id="v1.0"), + dict(status="CURRENT", id="v1.1"), + ], + } metadata = { "application/xml": { "attributes": dict(version=["status", "id"])}} -- cgit From 4b49df7e7232dfd3e187faac52b9eb72773be360 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Tue, 15 Mar 2011 16:49:19 -0400 Subject: Major cosmetic changes to limits, but little-to-no functional changes. MUCH better testability now, no more relying on system time to tick by for limit testing. --- nova/api/openstack/__init__.py | 6 + nova/api/openstack/faults.py | 21 +++ nova/api/openstack/limits.py | 342 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 nova/api/openstack/limits.py (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index ce3cff337..db2dff8d5 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 limits from nova.api.openstack import servers from nova.api.openstack import shared_ip_groups from nova.api.openstack import users @@ -114,12 +115,17 @@ 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()) + _limits = limits.LimitsController() + mapper.resource("limit", "limits", controller=_limits) + super(APIRouter, self).__init__(mapper) diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 2fd733299..6ed9322de 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -61,3 +61,24 @@ class Fault(webob.exc.HTTPException): content_type = req.best_match_content_type() self.wrapped_exc.body = serializer.serialize(fault_data, content_type) return self.wrapped_exc + + +class OverLimitFault(webob.exc.HTTPException): + """ + Rate-limited request response. + """ + + wrapped_exc = webob.exc.HTTPForbidden() + + def __init__(self, message, details, retry_time): + """ + Initialize new `OverLimitFault` with relevant information. + """ + self.message = message + self.details = details + self.retry_time = retry_time + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, request): + """Currently just return the wrapped exception.""" + return self.wrapped_exc diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py new file mode 100644 index 000000000..4f64cab3c --- /dev/null +++ b/nova/api/openstack/limits.py @@ -0,0 +1,342 @@ +# 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 + +""" +Module dedicated functions/classes dealing with rate limiting requests. +""" + +import copy +import httplib +import json +import math +import re +import time +import urllib +import webob.exc + +from collections import defaultdict + +from webob.dec import wsgify + +from nova import wsgi +from nova.api.openstack import faults +from nova.wsgi import Controller +from nova.wsgi import Middleware + + +# Convenience constants for the limits dictionary passed to Limiter(). +PER_SECOND = 1 +PER_MINUTE = 60 +PER_HOUR = 60 * 60 +PER_DAY = 60 * 60 * 24 + + +class LimitsController(Controller): + """ + Controller for accessing limits in the OpenStack API. + """ + + def index(self, req): + """ + Return all global and rate limit information. + """ + abs_limits = {} + rate_limits = req.environ.get("nova.limits", {}) + + return { + "limits" : { + "rate" : rate_limits, + "absolute" : abs_limits, + }, + } + + +class Limit(object): + """ + Stores information about a limit for HTTP requets. + """ + + UNITS = { + 1 : "SECOND", + 60 : "MINUTE", + 60 * 60 : "HOUR", + 60 * 60 * 24 : "DAY", + } + + def __init__(self, verb, uri, regex, value, unit): + """ + Initialize a new `Limit`. + + @param verb: HTTP verb (POST, PUT, etc.) + @param uri: Human-readable URI + @param regex: Regular expression format for this limit + @param value: Integer number of requests which can be made + @param unit: Unit of measure for the value parameter + """ + self.verb = verb + self.uri = uri + self.regex = regex + self.value = int(value) + self.unit = unit + self.remaining = int(value) + + if value <= 0: + raise ValueError("Limit value must be > 0") + + self.last_request = None + self.next_request = None + + self.water_level = 0 + self.capacity = float(self.unit) + self.request_value = float(self.capacity) / float(self.value) + + def __call__(self, verb, url): + """ + Represents a call to this limit from a relevant request. + + @param verb: string http verb (POST, GET, etc.) + @param url: string URL + """ + if self.verb != verb or not re.match(self.regex, url): + return + + now = self._get_time() + + if self.last_request is None: + self.last_request = now + + leak_value = now - self.last_request + + self.water_level -= leak_value + self.water_level = max(self.water_level, 0) + self.water_level += self.request_value + + difference = self.water_level - self.capacity + + self.last_request = now + + if difference > 0: + self.water_level -= self.request_value + self.next_request = now + difference + return difference + + cap = self.capacity + water = self.water_level + val = self.value + + self.remaining = math.floor((cap - water) / cap * val) + print "Remaining:", self.remaining + self.next_request = now + + def _get_time(self): + """Retrieve the current time. Broken out for testability.""" + return time.time() + + def display_unit(self): + """Display the string name of the unit.""" + return self.UNITS.get(self.unit, "UNKNOWN") + + def display(self): + """Return a useful representation of this class.""" + return { + "verb" : self.verb, + "uri" : self.uri, + "regex" : self.regex, + "value" : self.value, + "remaining" : int(self.remaining), + "unit" : self.display_unit(), + "resetTime" : int(self.next_request or self._get_time()), + } + + + +# "Limit" format is a dictionary with the HTTP verb, human-readable URI, +# a regular-expression to match, value and unit of measure (PER_DAY, etc.) + +DEFAULT_LIMITS = [ + Limit("POST", "*", ".*", 10, PER_MINUTE), + Limit("POST", "*/servers", "^/servers", 50, PER_DAY), + Limit("PUT", "*", ".*", 10, PER_MINUTE), + Limit("GET", "*changes-since*", ".*changes-since.*", 3, PER_MINUTE), + Limit("DELETE", "*", ".*", 100, PER_MINUTE), +] + + +class RateLimitingMiddleware(Middleware): + """ + Rate-limits requests passing through this middleware. All limit information + is stored in memory for this implementation. + """ + + def __init__(self, application, limits=None): + """ + Initialize new `RateLimitingMiddleware`, which wraps the given WSGI + application and sets up the given limits. + + @param application: WSGI application to wrap + @param limits: List of dictionaries describing limits + """ + Middleware.__init__(self, application) + self._limiter = Limiter(limits or DEFAULT_LIMITS) + + @wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + """ + Represents a single call through this middleware. We should record the + request if we have a limit relevant to it. If no limit is relevant to + the request, ignore it. + + If the request should be rate limited, return a fault telling the user + they are over the limit and need to retry later. + """ + verb = req.method + url = req.url + username = req.environ["nova.context"].user_id + + delay = self._limiter.check_for_delay(verb, url, username) + + if delay: + msg = "This request was rate-limited." + details = "Error details." + retry = time.time() + delay + return faults.OverLimitFault(msg, details, retry) + + req.environ["nova.limits"] = self._limiter.get_limits(username) + + return self.application + + +class Limiter(object): + """ + Rate-limit checking class which handles limits in memory. + """ + + def __init__(self, limits): + """ + Initialize the new `Limiter`. + + @param limits: List of `Limit` objects + """ + self.limits = copy.deepcopy(limits) + self.levels = defaultdict(lambda: copy.deepcopy(limits)) + + def get_limits(self, username=None): + """ + Return the limits for a given user. + """ + return [limit.display() for limit in self.levels[username]] + + def check_for_delay(self, verb, url, username=None): + """ + Check the given verb/user/user triplet for limit. + """ + def _get_delay_list(): + """Yield limit delays.""" + for limit in self.levels[username]: + delay = limit(verb, url) + if delay: + yield delay + + delays = list(_get_delay_list()) + + if delays: + delays.sort() + return delays[0] + + +class WsgiLimiter(object): + """ + Rate-limit checking from a WSGI application. Uses an in-memory `Limiter`. + + To use: + POST / with JSON data such as: + { + "verb" : GET, + "path" : "/servers" + } + + and receive a 204 No Content, or a 403 Forbidden with an X-Wait-Seconds + header containing the number of seconds to wait before the action would + succeed. + """ + + def __init__(self, limits=None): + """ + Initialize the new `WsgiLimiter`. + + @param limits: List of `Limit` objects + """ + self._limiter = Limiter(limits or DEFAULT_LIMITS) + + @wsgify(RequestClass=wsgi.Request) + def __call__(self, request): + """ + Handles a call to this application. Returns 204 if the request is + acceptable to the limiter, else a 403 is returned with a relevant + header indicating when the request *will* succeed. + """ + if request.method != "POST": + raise webob.exc.HTTPMethodNotAllowed() + + try: + info = dict(json.loads(request.body)) + except ValueError: + raise webob.exc.HTTPBadRequest() + + username = request.path_info_pop() + verb = info.get("verb") + path = info.get("path") + + delay = self._limiter.check_for_delay(verb, path, username) + + if delay: + headers = {"X-Wait-Seconds": "%.2f" % delay} + return webob.exc.HTTPForbidden(headers=headers) + else: + return webob.exc.HTTPNoContent() + + +class WsgiLimiterProxy(object): + """ + Rate-limit requests based on answers from a remote source. + """ + + def __init__(self, limiter_address): + """ + Initialize the new `WsgiLimiterProxy`. + + @param limiter_address: IP/port combination of where to request limit + """ + self.limiter_address = limiter_address + + def check_for_delay(self, verb, path, username=None): + body = json.dumps({"verb":verb,"path":path}) + headers = {"Content-Type" : "application/json"} + + conn = httplib.HTTPConnection(self.limiter_address) + + if username: + conn.request("POST", "/%s" % (username), body, headers) + else: + conn.request("POST", "/", body, headers) + + resp = conn.getresponse() + + if 200 >= resp.status < 300: + return None + + return resp.getheader("X-Wait-Seconds") -- cgit From 1b477c2225816ea8f05595a8812932d516828e01 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Tue, 15 Mar 2011 17:47:34 -0400 Subject: pep8 fixes --- nova/api/openstack/limits.py | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index 4f64cab3c..b1e633330 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -58,12 +58,12 @@ class LimitsController(Controller): rate_limits = req.environ.get("nova.limits", {}) return { - "limits" : { - "rate" : rate_limits, - "absolute" : abs_limits, + "limits": { + "rate": rate_limits, + "absolute": abs_limits, }, } - + class Limit(object): """ @@ -71,10 +71,10 @@ class Limit(object): """ UNITS = { - 1 : "SECOND", - 60 : "MINUTE", - 60 * 60 : "HOUR", - 60 * 60 * 24 : "DAY", + 1: "SECOND", + 60: "MINUTE", + 60 * 60: "HOUR", + 60 * 60 * 24: "DAY", } def __init__(self, verb, uri, regex, value, unit): @@ -137,14 +137,13 @@ class Limit(object): cap = self.capacity water = self.water_level val = self.value - + self.remaining = math.floor((cap - water) / cap * val) - print "Remaining:", self.remaining self.next_request = now def _get_time(self): """Retrieve the current time. Broken out for testability.""" - return time.time() + return time.time() def display_unit(self): """Display the string name of the unit.""" @@ -153,17 +152,15 @@ class Limit(object): def display(self): """Return a useful representation of this class.""" return { - "verb" : self.verb, - "uri" : self.uri, - "regex" : self.regex, - "value" : self.value, - "remaining" : int(self.remaining), - "unit" : self.display_unit(), - "resetTime" : int(self.next_request or self._get_time()), + "verb": self.verb, + "uri": self.uri, + "regex": self.regex, + "value": self.value, + "remaining": int(self.remaining), + "unit": self.display_unit(), + "resetTime": int(self.next_request or self._get_time()), } - - # "Limit" format is a dictionary with the HTTP verb, human-readable URI, # a regular-expression to match, value and unit of measure (PER_DAY, etc.) @@ -324,11 +321,11 @@ class WsgiLimiterProxy(object): self.limiter_address = limiter_address def check_for_delay(self, verb, path, username=None): - body = json.dumps({"verb":verb,"path":path}) - headers = {"Content-Type" : "application/json"} + body = json.dumps({"verb": verb, "path": path}) + headers = {"Content-Type": "application/json"} conn = httplib.HTTPConnection(self.limiter_address) - + if username: conn.request("POST", "/%s" % (username), body, headers) else: -- cgit From e9ef6e04786a40d20f8022bec5d23d2e4503ce3a Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Tue, 15 Mar 2011 17:56:00 -0400 Subject: s/onset_files/injected_files/g --- nova/api/openstack/servers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index adb5c5f99..42fe13619 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -169,7 +169,7 @@ class Controller(wsgi.Controller): metadata.append({'key': k, 'value': v}) personality = env['server'].get('personality', []) - onset_files = self._get_onset_files(personality) + injected_files = self._get_injected_files(personality) try: instances = self.compute_api.create( @@ -183,7 +183,7 @@ class Controller(wsgi.Controller): key_name=key_pair['name'], key_data=key_pair['public_key'], metadata=metadata, - onset_files=onset_files) + injected_files=injected_files) except QuotaError as error: self._handle_quota_error(error) @@ -207,15 +207,15 @@ class Controller(wsgi.Controller): else: return self._deserialize(request.body, request.get_content_type()) - def _get_onset_files(self, personality): + def _get_injected_files(self, personality): """ - Create a list of onset files from the personality attribute + Create a list of injected files from the personality attribute - At this time, onset_files must be formatted as a list of + At this time, injected_files must be formatted as a list of (file_path, file_content) pairs for compatibility with the underlying compute service. """ - onset_files = [] + injected_files = [] for item in personality: try: path = item['path'] @@ -230,8 +230,8 @@ class Controller(wsgi.Controller): except TypeError: msg = 'Personality content for %s cannot be decoded' % path raise exc.HTTPBadRequest(explanation=msg) - onset_files.append((path, contents)) - return onset_files + injected_files.append((path, contents)) + return injected_files def _handle_quota_errors(self, error): """ -- cgit From 74068a7b504a95dc8e0339faa04c8c5520417f32 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Tue, 15 Mar 2011 18:10:25 -0400 Subject: Per Eric Day's suggest, the verson is not store in the request environ instead of the nova.context. --- nova/api/openstack/auth.py | 4 ++-- nova/api/openstack/views/addresses.py | 2 +- nova/api/openstack/views/flavors.py | 2 +- nova/api/openstack/views/images.py | 2 +- nova/api/openstack/views/servers.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index e33a9faf5..c820a5963 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -68,9 +68,9 @@ class AuthMiddleware(wsgi.Middleware): not self.auth.is_project_member(user, account): return faults.Fault(webob.exc.HTTPUnauthorized()) + req.environ['nova.context'] = context.RequestContext(user, account) version = req.path.split('/')[1].replace('v', '') - req.environ['nova.context'] = context.RequestContext(user, account, - version=version) + req.environ['version'] = version return self.application def has_authentication(self, req): diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index d764e5229..65c24dbd7 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -8,7 +8,7 @@ def get_view_builder(req): A factory method that returns the correct builder based on the version of the api requested. ''' - version = req.environ['nova.context'].version + version = req.environ['version'] if version == '1.1': return ViewBuilder_1_1() else: diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index dfcc2644c..f945f9f8f 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -5,7 +5,7 @@ def get_view_builder(req): A factory method that returns the correct builder based on the version of the api requested. ''' - version = req.environ['nova.context'].version + version = req.environ['version'] base_url = req.application_url if version == '1.1': return ViewBuilder_1_1(base_url) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index cd61ed656..a59d4a557 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -5,7 +5,7 @@ def get_view_builder(req): A factory method that returns the correct builder based on the version of the api requested. ''' - version = req.environ['nova.context'].version + version = req.environ['version'] base_url = req.application_url if version == '1.1': return ViewBuilder_1_1(base_url) diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 7ca2b2427..2549cc11c 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -11,7 +11,7 @@ def get_view_builder(req): A factory method that returns the correct builder based on the version of the api requested. ''' - version = req.environ['nova.context'].version + version = req.environ['version'] addresses_builder = addresses_view.get_view_builder(req) if version == '1.1': flavor_builder = flavors_view.get_view_builder(req) -- cgit From 3cc78174e023b3f848b9c4b30468d356ee575ea6 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Tue, 15 Mar 2011 18:11:54 -0400 Subject: internationalization --- nova/api/openstack/servers.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 42fe13619..f618c31a0 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -221,15 +221,16 @@ class Controller(wsgi.Controller): path = item['path'] contents = item['contents'] except KeyError as key: - expl = 'Bad personality format: missing %s' % key + expl = _('Bad personality format: missing %s') % key raise exc.HTTPBadRequest(explanation=expl) except TypeError: - raise exc.HTTPBadRequest(explanation='Bad personality format') + expl = _('Bad personality format') + raise exc.HTTPBadRequest(explanation=expl) try: contents = base64.b64decode(contents) except TypeError: - msg = 'Personality content for %s cannot be decoded' % path - raise exc.HTTPBadRequest(explanation=msg) + expl = _('Personality content for %s cannot be decoded') % path + raise exc.HTTPBadRequest(explanation=expl) injected_files.append((path, contents)) return injected_files @@ -238,13 +239,13 @@ class Controller(wsgi.Controller): Reraise quota errors as api-specific http exceptions """ if error.code == "OnsetFileLimitExceeded": - expl = "Personality file limit exceeded" + expl = _("Personality file limit exceeded") raise exc.HTTPBadRequest(explanation=expl) if error.code == "OnsetFilePathLimitExceeded": - expl = "Personality file path too long" + expl = _("Personality file path too long") raise exc.HTTPBadRequest(explanation=expl) if error.code == "OnsetFileContentLimitExceeded": - expl = "Personality file content too long" + expl = _("Personality file content too long") raise exc.HTTPBadRequest(explanation=expl) # if the original error is okay, just reraise it raise error -- cgit From e237b4a5653384688b16f7fd2c0708eaec4b9ec7 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Tue, 15 Mar 2011 19:11:21 -0400 Subject: ignore differently-named nodes in personality and metadata parsing --- nova/api/openstack/servers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index f618c31a0..ea88f1fdc 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -575,7 +575,7 @@ class ServerCreateRequestXMLDeserializer(object): if metadata_node is None: return None metadata = {} - for meta_node in metadata_node.childNodes: + for meta_node in self._find_children_named(metadata_node, "meta"): key = meta_node.getAttribute("key") metadata[key] = self._extract_text(meta_node) return metadata @@ -587,7 +587,7 @@ class ServerCreateRequestXMLDeserializer(object): if personality_node is None: return None personality = [] - for file_node in personality_node.childNodes: + for file_node in self._find_children_named(personality_node, "file"): item = {} if file_node.hasAttribute("path"): item["path"] = file_node.getAttribute("path") @@ -602,6 +602,12 @@ class ServerCreateRequestXMLDeserializer(object): return node return None + def _find_children_named(self, parent, name): + """Return all of a nodes children who have the given name""" + for node in parent.childNodes: + if node.nodeName == name: + yield node + def _extract_text(self, node): """Get the text field contained by the given node""" if len(node.childNodes) == 1: -- cgit From bee1951ac78688e49939aee4e2285ef0ff89adb2 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Tue, 15 Mar 2011 19:55:13 -0400 Subject: As suggested by Eric Day: * changed request.environ version key to more descriptive 'api.version' * removed python3 string formatting * added licenses to headers on new files --- nova/api/openstack/auth.py | 2 +- nova/api/openstack/common.py | 3 +++ nova/api/openstack/views/addresses.py | 22 +++++++++++++++++++--- nova/api/openstack/views/flavors.py | 23 ++++++++++++++++++++--- nova/api/openstack/views/images.py | 23 ++++++++++++++++++++--- nova/api/openstack/views/servers.py | 20 +++++++++++++++++++- 6 files changed, 82 insertions(+), 11 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index c820a5963..7ae285019 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['version'] = version + req.environ['nova.api.openstack.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 74ac21024..d94969ff5 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -74,3 +74,6 @@ 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') diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index 65c24dbd7..9d392aace 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -1,6 +1,22 @@ -import hashlib -from nova.compute import power_state +# 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. + from nova import utils +from nova.api.openstack import common def get_view_builder(req): @@ -8,7 +24,7 @@ def get_view_builder(req): A factory method that returns the correct builder based on the version of the api requested. ''' - version = req.environ['version'] + version = common.get_api_version(req) if version == '1.1': return ViewBuilder_1_1() else: diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index f945f9f8f..aa3c2aeb2 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -1,11 +1,28 @@ - +# 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. + +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 = req.environ['version'] + version = common.get_api_version(req) base_url = req.application_url if version == '1.1': return ViewBuilder_1_1(base_url) @@ -26,7 +43,7 @@ class ViewBuilder_1_1(ViewBuilder): self.base_url = base_url def generate_href(self, flavor_id): - return "{0}/flavors/{1}".format(self.base_url, flavor_id) + return "%s/flavors/%s" % (self.base_url, flavor_id) class ViewBuilder_1_0(ViewBuilder): diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index a59d4a557..930b464b0 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -1,11 +1,28 @@ - +# 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. + +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 = req.environ['version'] + version = common.get_api_version(req) base_url = req.application_url if version == '1.1': return ViewBuilder_1_1(base_url) @@ -26,7 +43,7 @@ class ViewBuilder_1_1(ViewBuilder): self.base_url = base_url def generate_href(self, image_id): - return "{0}/images/{1}".format(self.base_url, image_id) + return "%s/images/%s" % (self.base_url, image_id) class ViewBuilder_1_0(ViewBuilder): diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 2549cc11c..261acfed0 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -1,5 +1,23 @@ +# 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. + import hashlib from nova.compute import power_state +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 from nova.api.openstack.views import images as images_view @@ -11,7 +29,7 @@ def get_view_builder(req): A factory method that returns the correct builder based on the version of the api requested. ''' - version = req.environ['version'] + 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) -- cgit From c42d79b58eccaebab14274adf09128d890e920f7 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 15 Mar 2011 20:37:37 -0400 Subject: adding imageRef and flavorRef attributes to servers serialization metadata --- nova/api/openstack/servers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index de67cbc4a..dc62882eb 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -46,7 +46,8 @@ class Controller(wsgi.Controller): 'application/xml': { "attributes": { "server": ["id", "imageId", "name", "flavorId", "hostId", - "status", "progress", "adminPass"]}}} + "status", "progress", "adminPass", "flavorRef", + "imageRef"]}}} def __init__(self): self.compute_api = compute.API() -- cgit From 5e45d0ba921566e98817cb9e62e383f84c30c5f6 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Tue, 15 Mar 2011 20:51:17 -0400 Subject: Limits controller and testing with XML and JSON serialization. --- nova/api/openstack/limits.py | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index b1e633330..57e6bfcc2 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -50,12 +50,24 @@ class LimitsController(Controller): Controller for accessing limits in the OpenStack API. """ + _serialization_metadata = { + "application/xml": { + "attributes": { + "limit": ["verb", "URI", "regex", "value", "unit", + "resetTime", "remaining", "name"], + }, + "plurals" : { + "rate" : "limit", + }, + }, + } + def index(self, req): """ Return all global and rate limit information. """ abs_limits = {} - rate_limits = req.environ.get("nova.limits", {}) + rate_limits = req.environ.get("nova.limits", []) return { "limits": { @@ -92,6 +104,7 @@ class Limit(object): self.regex = regex self.value = int(value) self.unit = unit + self.unit_string = self.display_unit().lower() self.remaining = int(value) if value <= 0: @@ -101,8 +114,10 @@ class Limit(object): self.next_request = None self.water_level = 0 - self.capacity = float(self.unit) + self.capacity = self.unit self.request_value = float(self.capacity) / float(self.value) + self.error_message = _("Only %(value)s %(verb)s request(s) can be "\ + "made to %(uri)s every %(unit_string)s." % self.__dict__) def __call__(self, verb, url): """ @@ -153,7 +168,7 @@ class Limit(object): """Return a useful representation of this class.""" return { "verb": self.verb, - "uri": self.uri, + "URI": self.uri, "regex": self.regex, "value": self.value, "remaining": int(self.remaining), @@ -204,13 +219,12 @@ class RateLimitingMiddleware(Middleware): url = req.url username = req.environ["nova.context"].user_id - delay = self._limiter.check_for_delay(verb, url, username) + delay, error = self._limiter.check_for_delay(verb, url, username) if delay: msg = "This request was rate-limited." - details = "Error details." retry = time.time() + delay - return faults.OverLimitFault(msg, details, retry) + return faults.OverLimitFault(msg, error, retry) req.environ["nova.limits"] = self._limiter.get_limits(username) @@ -240,13 +254,15 @@ class Limiter(object): def check_for_delay(self, verb, url, username=None): """ Check the given verb/user/user triplet for limit. + + @return: Tuple of delay (in seconds) and error message (or None, None) """ def _get_delay_list(): """Yield limit delays.""" for limit in self.levels[username]: delay = limit(verb, url) if delay: - yield delay + yield delay, limit.error_message delays = list(_get_delay_list()) @@ -254,6 +270,8 @@ class Limiter(object): delays.sort() return delays[0] + return None, None + class WsgiLimiter(object): """ @@ -298,11 +316,11 @@ class WsgiLimiter(object): verb = info.get("verb") path = info.get("path") - delay = self._limiter.check_for_delay(verb, path, username) + delay, error = self._limiter.check_for_delay(verb, path, username) if delay: headers = {"X-Wait-Seconds": "%.2f" % delay} - return webob.exc.HTTPForbidden(headers=headers) + return webob.exc.HTTPForbidden(headers=headers, explanation=error) else: return webob.exc.HTTPNoContent() @@ -333,7 +351,9 @@ class WsgiLimiterProxy(object): resp = conn.getresponse() + print resp + if 200 >= resp.status < 300: - return None + return None, None - return resp.getheader("X-Wait-Seconds") + return resp.getheader("X-Wait-Seconds"), resp.read() or None -- cgit From 5ba3e21875d3cf3b71082477311902891706eee4 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Tue, 15 Mar 2011 21:09:26 -0400 Subject: Removed VIM specific stuff and changed copyright from 2010 to 2011. --- nova/api/openstack/limits.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index 57e6bfcc2..3ecd46377 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.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 -- cgit From 6911123fda88c9793a70ea4b03d0352c9c38f938 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Tue, 15 Mar 2011 21:26:45 -0400 Subject: Adding newlines for pep8. --- nova/api/openstack/common.py | 1 + nova/api/openstack/views/flavors.py | 1 + nova/api/openstack/views/images.py | 1 + 3 files changed, 3 insertions(+) (limited to 'nova/api') 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/views/flavors.py b/nova/api/openstack/views/flavors.py index aa3c2aeb2..dd2e75a7a 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -17,6 +17,7 @@ from nova.api.openstack import common + def get_view_builder(req): ''' A factory method that returns the correct builder based on the version of diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 930b464b0..2369a8f9d 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -17,6 +17,7 @@ from nova.api.openstack import common + def get_view_builder(req): ''' A factory method that returns the correct builder based on the version of -- 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 be9a218e2e4b01fe19722fb0073731d8ae6a7eea Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Tue, 15 Mar 2011 23:13:05 -0400 Subject: Added tests back for RateLimitingMiddleware which now throw correctly serialized errors with correct error codes. Removed some error printing, and simplified some other parts of the code with suggestions from teammates. --- nova/api/openstack/faults.py | 22 ++++++++++++++++++---- nova/api/openstack/limits.py | 25 +++++++++++++------------ 2 files changed, 31 insertions(+), 16 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 6ed9322de..d05c61fc7 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -68,17 +68,31 @@ class OverLimitFault(webob.exc.HTTPException): Rate-limited request response. """ - wrapped_exc = webob.exc.HTTPForbidden() + _serialization_metadata = { + "application/xml": { + "attributes": { + "overLimitFault": "code" + } + } + } def __init__(self, message, details, retry_time): """ Initialize new `OverLimitFault` with relevant information. """ - self.message = message - self.details = details - self.retry_time = retry_time + self.wrapped_exc = webob.exc.HTTPForbidden() + self.content = { + "overLimitFault": { + "code": self.wrapped_exc.status_int, + "message": message, + "details": details, + }, + } @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, request): """Currently just return the wrapped exception.""" + serializer = wsgi.Serializer(self._serialization_metadata) + content_type = request.best_match_content_type() + self.wrapped_exc.body = serializer.serialize(self.content, content_type) return self.wrapped_exc diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index 3ecd46377..c4e04e9d9 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -54,8 +54,8 @@ class LimitsController(Controller): "limit": ["verb", "URI", "regex", "value", "unit", "resetTime", "remaining", "name"], }, - "plurals" : { - "rate" : "limit", + "plurals": { + "rate": "limit", }, }, } @@ -215,7 +215,12 @@ class RateLimitingMiddleware(Middleware): """ verb = req.method url = req.url - username = req.environ["nova.context"].user_id + context = req.environ.get("nova.context") + + if context: + username = context.user_id + else: + username = None delay, error = self._limiter.check_for_delay(verb, url, username) @@ -255,14 +260,12 @@ class Limiter(object): @return: Tuple of delay (in seconds) and error message (or None, None) """ - def _get_delay_list(): - """Yield limit delays.""" - for limit in self.levels[username]: - delay = limit(verb, url) - if delay: - yield delay, limit.error_message + delays = [] - delays = list(_get_delay_list()) + for limit in self.levels[username]: + delay = limit(verb, url) + if delay: + delays.append((delay, limit.error_message)) if delays: delays.sort() @@ -349,8 +352,6 @@ class WsgiLimiterProxy(object): resp = conn.getresponse() - print resp - if 200 >= resp.status < 300: return None, None -- 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 659cb8bd43e2091c61f44dacf21274a677ea3146 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 15 Mar 2011 23:35:44 -0400 Subject: openstack api 1.0 flavors resource now implemented; adding flavors request value testing --- nova/api/openstack/flavors.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index f3d040ba3..c99b945fb 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -22,6 +22,7 @@ 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 wsgi import nova.api.openstack @@ -47,13 +48,18 @@ class Controller(wsgi.Controller): def show(self, req, id): """Return data about the given flavor id.""" ctxt = req.environ['nova.context'] - values = db.instance_type_get_by_flavor_id(ctxt, id) + 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"], + } return dict(flavor=values) - raise faults.Fault(exc.HTTPNotFound()) def _all_ids(self, req): """Return the list of all flavorids.""" ctxt = req.environ['nova.context'] - inst_types = db.instance_type_get_all(ctxt) + 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) -- cgit From 78542ad1de6476a8962fa0c3b273c4a272410a83 Mon Sep 17 00:00:00 2001 From: Naveed Massjouni Date: Wed, 16 Mar 2011 00:38:47 -0400 Subject: req envirom param 'nova.api.openstack.version' should be 'api.version' --- nova/api/openstack/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') 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): -- 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 a586714557e38116b6b4f473aa21ac54ff0223e7 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 16 Mar 2011 10:10:58 -0400 Subject: Added i18n to error message. --- nova/api/openstack/limits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index c4e04e9d9..1fe519f8a 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -225,7 +225,7 @@ class RateLimitingMiddleware(Middleware): delay, error = self._limiter.check_for_delay(verb, url, username) if delay: - msg = "This request was rate-limited." + msg = _("This request was rate-limited.") retry = time.time() + delay return faults.OverLimitFault(msg, error, retry) -- 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 5473f3a47c1b11c6625960e1ed73c28c7b061fcb Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 16 Mar 2011 13:41:00 -0400 Subject: moving code out of try/except that would never trigger NotFound --- 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 dc62882eb..818dd825f 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -57,11 +57,12 @@ 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) - return builder.build(instance) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) + builder = addresses_views.get_view_builder(req) + return builder.build(instance) + def index(self, req): """ Returns a list of server names and ids for a given user """ return self._items(req, is_detail=False) -- 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 7de1ef791296d547c2691454d5cb5451087cd76b Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Wed, 16 Mar 2011 12:15:57 -0700 Subject: User ids are strings, and are not necessarily == name. Also fix so that non-existent user gives a 404, not a 500. --- nova/api/openstack/users.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py index ebd0f4512..d3ab3d553 100644 --- a/nova/api/openstack/users.py +++ b/nova/api/openstack/users.py @@ -13,13 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. -import common +from webob import exc from nova import exception from nova import flags from nova import log as logging from nova import wsgi - +from nova.api.openstack import common +from nova.api.openstack import faults from nova.auth import manager FLAGS = flags.FLAGS @@ -63,7 +64,17 @@ class Controller(wsgi.Controller): def show(self, req, id): """Return data about the given user id""" - user = self.manager.get_user(id) + + #NOTE(justinsb): The drivers are a little inconsistent in how they + # deal with "NotFound" - some throw, some return None. + try: + user = self.manager.get_user(id) + except exception.NotFound: + user = None + + if user is None: + raise faults.Fault(exc.HTTPNotFound()) + return dict(user=_translate_keys(user)) def delete(self, req, id): -- cgit From 85bae497aa803914d329f2872d343a9982dc370e Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 16 Mar 2011 14:35:04 -0500 Subject: Changes --- nova/api/openstack/flavors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index f3d040ba3..b9e40371d 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -36,7 +36,7 @@ class Controller(wsgi.Controller): def index(self, req): """Return all flavors in brief.""" - return dict(flavors=[dict(id=flavor['id'], name=flavor['name']) + return dict(flavors=[dict(id=flavor['flavorid'], name=flavor['name']) for flavor in self.detail(req)['flavors']]) def detail(self, req): @@ -48,6 +48,7 @@ class Controller(wsgi.Controller): """Return data about the given flavor id.""" ctxt = req.environ['nova.context'] values = db.instance_type_get_by_flavor_id(ctxt, id) + values.update({'id': values['flavorid']}) return dict(flavor=values) raise faults.Fault(exc.HTTPNotFound()) -- cgit From f17fb9370d4af42267837a36c937f213669b0291 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 16 Mar 2011 14:43:57 -0500 Subject: Dumb --- 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 b9e40371d..1c440b3a9 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -48,7 +48,7 @@ class Controller(wsgi.Controller): """Return data about the given flavor id.""" ctxt = req.environ['nova.context'] values = db.instance_type_get_by_flavor_id(ctxt, id) - values.update({'id': values['flavorid']}) + values['id'] = values['flavorid'] return dict(flavor=values) raise faults.Fault(exc.HTTPNotFound()) -- 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 a766b4111addad804e47b8be3e6dedb5f80a83c4 Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Thu, 17 Mar 2011 02:20:18 +0000 Subject: added in network qos support for xenserver. Pull qos settings from flavor, use when creating instance. --- nova/api/openstack/servers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 3ecd4fb01..2f26fa873 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -32,6 +32,7 @@ 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 +from nova.quota import QuotaError import nova.api.openstack @@ -189,7 +190,7 @@ class Controller(wsgi.Controller): metadata=metadata, injected_files=injected_files) except QuotaError as error: - self._handle_quota_error(error) + self._handle_quota_errors(error) server = _translate_keys(instances[0]) password = "%s%s" % (server['server']['name'][:4], -- 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 732633c93f8d8cf71875d2caf096c9efbcf9dbce Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 17 Mar 2011 09:55:41 -0400 Subject: Update the Openstack API to handle case where personality is set but null in the request to create a server. --- nova/api/openstack/servers.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 3ecd4fb01..bf21ed17f 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -220,6 +220,11 @@ class Controller(wsgi.Controller): underlying compute service. """ injected_files = [] + + # NOTE(dprince): handle case where 'personality: null' is in JSON req + if not personality: + return injected_files + for item in personality: try: path = item['path'] -- 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 f8aa9485fe2048ff916d9dd40478ef0b1486077f Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 17 Mar 2011 10:45:46 -0400 Subject: Switch back to 'is not None' for personality_files check. (makes mark happy) --- 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 bf21ed17f..6dd66a9a5 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -221,7 +221,6 @@ class Controller(wsgi.Controller): """ injected_files = [] - # NOTE(dprince): handle case where 'personality: null' is in JSON req if not personality: return injected_files -- cgit From 1d64d0a3d7f25448361ce54e32bba3de68c7afd1 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Thu, 17 Mar 2011 16:14:59 +0100 Subject: Remove unconditional raise, probably left over from debugging. --- 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 40a9da0e7..e257e44e7 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -959,7 +959,7 @@ class CloudController(object): raise exception.NotFound(_('Image %s not found') % image_id) internal_id = image['id'] del(image['id']) - raise Exception(image) + image['properties']['is_public'] = (operation_type == 'add') return self.image_service.update(context, internal_id, image) -- 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 d31e0f0ad048fbd0374170ea76968859a4c6df34 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 17 Mar 2011 12:39:09 -0400 Subject: Fixed pep8 violation. --- nova/api/openstack/faults.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index d05c61fc7..56f5b8e7e 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -94,5 +94,6 @@ class OverLimitFault(webob.exc.HTTPException): """Currently just return the wrapped exception.""" serializer = wsgi.Serializer(self._serialization_metadata) content_type = request.best_match_content_type() - self.wrapped_exc.body = serializer.serialize(self.content, content_type) + content = serializer.serialize(self.content, content_type) + self.wrapped_exc.body = content return self.wrapped_exc -- 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 f96dea3da633fc71f16de1bdb95e88249b316e29 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 17 Mar 2011 13:11:40 -0400 Subject: Pep8 error, oddly specific to pep8 v0.5 < x > v0.6 --- nova/api/openstack/faults.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 56f5b8e7e..ccccbd3d2 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -71,9 +71,9 @@ class OverLimitFault(webob.exc.HTTPException): _serialization_metadata = { "application/xml": { "attributes": { - "overLimitFault": "code" - } - } + "overLimitFault": "code", + }, + }, } def __init__(self, message, details, retry_time): -- 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 ca267d0e52ed721f1236dc4b6030433fe92d0d51 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 17 Mar 2011 15:27:20 -0400 Subject: Move the check for None personalities into the create method. --- nova/api/openstack/servers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 6dd66a9a5..c6422add4 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -172,8 +172,10 @@ class Controller(wsgi.Controller): for k, v in env['server']['metadata'].items(): metadata.append({'key': k, 'value': v}) - personality = env['server'].get('personality', []) - injected_files = self._get_injected_files(personality) + personality = env['server'].get('personality') + injected_files = [] + if personality: + injected_files = self._get_injected_files(personality) try: instances = self.compute_api.create( @@ -221,9 +223,6 @@ class Controller(wsgi.Controller): """ injected_files = [] - if not personality: - return injected_files - for item in personality: try: path = item['path'] -- 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 3628f50b4ecd2db0377fd9c158248d3b7e8e98ff Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Thu, 17 Mar 2011 16:26:52 -0400 Subject: Better comment for fault. Improved readability of two small sections. --- nova/api/openstack/faults.py | 5 ++++- nova/api/openstack/limits.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index ccccbd3d2..0e9c4b26f 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -91,7 +91,10 @@ class OverLimitFault(webob.exc.HTTPException): @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, request): - """Currently just return the wrapped exception.""" + """ + Return the wrapped exception with a serialized body conforming to our + error format. + """ serializer = wsgi.Serializer(self._serialization_metadata) content_type = request.best_match_content_type() content = serializer.serialize(self.content, content_type) diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index 1fe519f8a..efc7d193d 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -151,7 +151,7 @@ class Limit(object): water = self.water_level val = self.value - self.remaining = math.floor((cap - water) / cap * val) + self.remaining = math.floor(((cap - water) / cap) * val) self.next_request = now def _get_time(self): -- 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 a50deeb264ff721584d5b0a6ace749d8e2c44842 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 18 Mar 2011 10:10:46 -0400 Subject: Change cloud.id_to_ec2_id to ec2utils.id_to_ec2_id. Fixes EC2 API error handling when invalid instances and volume names are specified. --- nova/api/ec2/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index fccebca5d..20701cfa8 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -31,7 +31,7 @@ from nova import log as logging from nova import utils from nova import wsgi from nova.api.ec2 import apirequest -from nova.api.ec2 import cloud +from nova.api.ec2 import ec2utils from nova.auth import manager @@ -319,13 +319,13 @@ class Executor(wsgi.Application): except exception.InstanceNotFound as ex: LOG.info(_('InstanceNotFound raised: %s'), unicode(ex), context=context) - ec2_id = cloud.id_to_ec2_id(ex.instance_id) + ec2_id = ec2utils.id_to_ec2_id(ex.instance_id) message = _('Instance %s not found') % ec2_id return self._error(req, context, type(ex).__name__, message) except exception.VolumeNotFound as ex: LOG.info(_('VolumeNotFound raised: %s'), unicode(ex), context=context) - ec2_id = cloud.id_to_ec2_id(ex.volume_id, 'vol-%08x') + ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x') message = _('Volume %s not found') % ec2_id return self._error(req, context, type(ex).__name__, message) except exception.NotFound as ex: -- 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 d06bce4b64b57551a722688a4038a4eaffa34278 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 22 Mar 2011 16:34:02 -0400 Subject: typo fix. --- nova/api/ec2/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index d9a4ef999..f32d0804f 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -60,7 +60,7 @@ def project_dict(project): def host_dict(host, compute_service, instances, volume_service, volumes, now): """Convert a host model object to a result dict""" - rv = {'hostanme': host, 'instance_count': len(instances), + rv = {'hostname': host, 'instance_count': len(instances), 'volume_count': len(volumes)} if compute_service: latest = compute_service['updated_at'] or compute_service['created_at'] -- 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 48c04eb35fae704913e9ed05868d1334ee5458fa Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Wed, 23 Mar 2011 12:17:48 -0400 Subject: add changePassword action to os api v1.1 --- nova/api/openstack/servers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 73843f63e..90f709a47 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -256,6 +256,7 @@ class Controller(wsgi.Controller): resize a server""" actions = { + 'changePassword': self._action_change_password, 'reboot': self._action_reboot, 'resize': self._action_resize, 'confirmResize': self._action_confirm_resize, @@ -269,6 +270,9 @@ class Controller(wsgi.Controller): return actions[key](input_dict, req, id) return faults.Fault(exc.HTTPNotImplemented()) + def _action_change_password(self, input_dict, req, id): + return exc.HTTPNotImplemented() + def _action_confirm_resize(self, input_dict, req, id): try: self.compute_api.confirm_resize(req.environ['nova.context'], id) @@ -555,6 +559,15 @@ class ControllerV11(Controller): def _get_addresses_view_builder(self, req): return nova.api.openstack.views.addresses.ViewBuilderV11(req) + def _action_change_password(self, input_dict, req, id): + context = req.environ['nova.context'] + if not 'changePassword' in input_dict \ + or not 'adminPass' in input_dict['changePassword']: + return exc.HTTPBadRequest() + password = input_dict['changePassword']['adminPass'] + self.compute_api.set_admin_password(context, id, password) + return exc.HTTPAccepted() + class ServerCreateRequestXMLDeserializer(object): """ -- 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 5977a511ed202fcf396e7c60d713eb5329d6883b Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Mon, 28 Mar 2011 13:30:15 -0400 Subject: style changes --- nova/api/openstack/servers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index a8e3e7900..a98f81d98 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -267,11 +267,11 @@ class Controller(wsgi.Controller): actions = { 'changePassword': self._action_change_password, - 'reboot': self._action_reboot, - 'resize': self._action_resize, + 'reboot': self._action_reboot, + 'resize': self._action_resize, 'confirmResize': self._action_confirm_resize, - 'revertResize': self._action_revert_resize, - 'rebuild': self._action_rebuild, + 'revertResize': self._action_revert_resize, + 'rebuild': self._action_rebuild, } input_dict = self._deserialize(req.body, req.get_content_type()) @@ -595,8 +595,8 @@ class ControllerV11(Controller): def _action_change_password(self, input_dict, req, id): context = req.environ['nova.context'] - if not 'changePassword' in input_dict \ - or not 'adminPass' in input_dict['changePassword']: + if (not 'changePassword' in input_dict + or not 'adminPass' in input_dict['changePassword']): return exc.HTTPBadRequest() password = input_dict['changePassword']['adminPass'] self.compute_api.set_admin_password(context, id, password) -- cgit From 71347f2e9d6195a25cabff782c7058bed006e286 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Mon, 28 Mar 2011 13:40:16 -0400 Subject: lock down requirements for change password --- 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 a98f81d98..b5727a7e1 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -599,6 +599,8 @@ class ControllerV11(Controller): or not 'adminPass' in input_dict['changePassword']): return exc.HTTPBadRequest() password = input_dict['changePassword']['adminPass'] + if not isinstance(password, basestring) or password == '': + return exc.HTTPBadRequest() self.compute_api.set_admin_password(context, id, password) return exc.HTTPAccepted() -- cgit From 63747d35929a1df0a29792f41657b4821c5787a3 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Mon, 28 Mar 2011 13:50:24 -0400 Subject: pep8 whitespace --- 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 b5727a7e1..aaae17a39 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -595,7 +595,7 @@ class ControllerV11(Controller): def _action_change_password(self, input_dict, req, id): context = req.environ['nova.context'] - if (not 'changePassword' in input_dict + if (not 'changePassword' in input_dict or not 'adminPass' in input_dict['changePassword']): return exc.HTTPBadRequest() password = input_dict['changePassword']['adminPass'] -- 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 5c74862a08a82b7db3e11fbcbec63293ea903e00 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Mon, 28 Mar 2011 23:11:42 -0700 Subject: make all openstack status uppercase --- nova/api/openstack/views/servers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 4e7f62eb3..6b471a0f4 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -72,12 +72,12 @@ class ViewBuilder(object): 'id': int(inst['id']), 'name': inst['display_name'], 'addresses': self.addresses_builder.build(inst), - 'status': power_mapping[inst.get('state')]} + 'status': power_mapping[inst.get('state')].upper()} ctxt = nova.context.get_admin_context() compute_api = nova.compute.API() if compute_api.has_finished_migration(ctxt, inst['id']): - inst_dict['status'] = 'resize-confirm' + inst_dict['status'] = 'resize-confirm'.upper() # Return the metadata as a dictionary metadata = {} -- 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 c512bae72859b8583731886011e8f9a4310d69f8 Mon Sep 17 00:00:00 2001 From: Mark Washenberger Date: Tue, 29 Mar 2011 10:53:44 -0400 Subject: use informative error messages --- nova/api/openstack/servers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index aaae17a39..31b9bc2f1 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -597,10 +597,12 @@ class ControllerV11(Controller): context = req.environ['nova.context'] if (not 'changePassword' in input_dict or not 'adminPass' in input_dict['changePassword']): - return exc.HTTPBadRequest() + msg = _("No adminPass was specified") + return exc.HTTPBadRequest(msg) password = input_dict['changePassword']['adminPass'] if not isinstance(password, basestring) or password == '': - return exc.HTTPBadRequest() + msg = _("Invalid adminPass") + return exc.HTTPBadRequest(msg) self.compute_api.set_admin_password(context, id, password) return exc.HTTPAccepted() -- 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 2af6fb2a4d3659e9882a6f6d1c8e71bc8f040aba Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Tue, 29 Mar 2011 14:56:18 -0400 Subject: Added content_type to OSAPI faults. --- nova/api/openstack/faults.py | 1 + 1 file changed, 1 insertion(+) (limited to 'nova/api') diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 0e9c4b26f..940bd8771 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -60,6 +60,7 @@ class Fault(webob.exc.HTTPException): serializer = wsgi.Serializer(metadata) content_type = req.best_match_content_type() self.wrapped_exc.body = serializer.serialize(fault_data, content_type) + self.wrapped_exc.content_type = content_type return self.wrapped_exc -- 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 From 032acaab1fd9a3823e203ddaf9c50857acd25ac7 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Thu, 31 Mar 2011 15:32:31 -0500 Subject: Don't include first 4 chars of instance name in adminPass --- 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 f7696d918..d47ea5788 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -180,8 +180,7 @@ class Controller(wsgi.Controller): builder = self._get_view_builder(req) server = builder.build(inst, is_detail=True) - password = "%s%s" % (server['server']['name'][:4], - utils.generate_password(12)) + password = utils.generate_password(16) server['server']['adminPass'] = password self.compute_api.set_admin_password(context, server['server']['id'], password) -- cgit From d98c61d21f3a60e3368cc723fc6764c66b8176b4 Mon Sep 17 00:00:00 2001 From: Masanori Itoh Date: Sat, 2 Apr 2011 01:05:50 +0900 Subject: Add checking if the floating_ip is allocated or not before appending to result array. --- nova/api/ec2/cloud.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 7ba8dfbea..a6bdab808 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -757,19 +757,20 @@ class CloudController(object): iterator = db.floating_ip_get_all_by_project(context, context.project_id) for floating_ip_ref in iterator: - address = floating_ip_ref['address'] - ec2_id = None - if (floating_ip_ref['fixed_ip'] - and floating_ip_ref['fixed_ip']['instance']): - instance_id = floating_ip_ref['fixed_ip']['instance']['id'] - ec2_id = ec2utils.id_to_ec2_id(instance_id) - address_rv = {'public_ip': address, - 'instance_id': ec2_id} - if context.is_admin: - details = "%s (%s)" % (address_rv['instance_id'], - floating_ip_ref['project_id']) - address_rv['instance_id'] = details - addresses.append(address_rv) + if floating_ip_ref['project_id'] is not None: + address = floating_ip_ref['address'] + ec2_id = None + if (floating_ip_ref['fixed_ip'] + and floating_ip_ref['fixed_ip']['instance']): + instance_id = floating_ip_ref['fixed_ip']['instance']['id'] + ec2_id = ec2utils.id_to_ec2_id(instance_id) + address_rv = {'public_ip': address, + 'instance_id': ec2_id} + if context.is_admin: + details = "%s (%s)" % (address_rv['instance_id'], + floating_ip_ref['project_id']) + address_rv['instance_id'] = details + addresses.append(address_rv) return {'addressesSet': addresses} def allocate_address(self, context, **kwargs): -- cgit From ec3b3d6ae97cddce490c2cbeed2f8f9241704ed1 Mon Sep 17 00:00:00 2001 From: Masanori Itoh Date: Sat, 2 Apr 2011 01:44:12 +0900 Subject: Made the fix simpler. --- nova/api/ec2/cloud.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index a6bdab808..425784e8a 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -757,20 +757,21 @@ class CloudController(object): iterator = db.floating_ip_get_all_by_project(context, context.project_id) for floating_ip_ref in iterator: - if floating_ip_ref['project_id'] is not None: - address = floating_ip_ref['address'] - ec2_id = None - if (floating_ip_ref['fixed_ip'] - and floating_ip_ref['fixed_ip']['instance']): - instance_id = floating_ip_ref['fixed_ip']['instance']['id'] - ec2_id = ec2utils.id_to_ec2_id(instance_id) - address_rv = {'public_ip': address, - 'instance_id': ec2_id} - if context.is_admin: - details = "%s (%s)" % (address_rv['instance_id'], - floating_ip_ref['project_id']) - address_rv['instance_id'] = details - addresses.append(address_rv) + if floating_ip_ref['project_id'] is None: + continue + address = floating_ip_ref['address'] + ec2_id = None + if (floating_ip_ref['fixed_ip'] + and floating_ip_ref['fixed_ip']['instance']): + instance_id = floating_ip_ref['fixed_ip']['instance']['id'] + ec2_id = ec2utils.id_to_ec2_id(instance_id) + address_rv = {'public_ip': address, + 'instance_id': ec2_id} + if context.is_admin: + details = "%s (%s)" % (address_rv['instance_id'], + floating_ip_ref['project_id']) + address_rv['instance_id'] = details + addresses.append(address_rv) return {'addressesSet': addresses} def allocate_address(self, context, **kwargs): -- cgit From bd64c4f6bebb50528b87bf6e3f64d7d1cba053df Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 4 Apr 2011 10:59:44 -0400 Subject: Fixes error which occurs when no name is specified for an image. --- nova/api/openstack/views/images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 3807fa95f..d8578ebdd 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -61,7 +61,7 @@ class ViewBuilder(object): image = { "id": image_obj["id"], - "name": image_obj["name"], + "name": image_obj.get("name"), } if "instance_id" in properties: -- cgit From 59d46ada05f47bf477427b932a47c1cf1d91811e Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Mon, 4 Apr 2011 11:19:20 -0400 Subject: Ensure no errors for improper responses from image service. --- nova/api/openstack/views/images.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index d8578ebdd..16195b050 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -60,7 +60,7 @@ class ViewBuilder(object): self._format_status(image_obj) image = { - "id": image_obj["id"], + "id": image_obj.get("id"), "name": image_obj.get("name"), } @@ -72,9 +72,9 @@ class ViewBuilder(object): if detail: image.update({ - "created": image_obj["created_at"], - "updated": image_obj["updated_at"], - "status": image_obj["status"], + "created": image_obj.get("created_at"), + "updated": image_obj.get("updated_at"), + "status": image_obj.get("status"), }) if image["status"] == "SAVING": -- cgit From fc53de592fb903f8a7e3741fa98d03140aca9066 Mon Sep 17 00:00:00 2001 From: Ken Pepple Date: Mon, 4 Apr 2011 09:45:26 -0700 Subject: corrected capitalization of openstack api status and added tests --- nova/api/openstack/views/servers.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 6b471a0f4..d24c025be 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -57,27 +57,27 @@ class ViewBuilder(object): def _build_detail(self, inst): """Returns a detailed model of a server.""" power_mapping = { - None: 'build', - power_state.NOSTATE: 'build', - power_state.RUNNING: 'active', - power_state.BLOCKED: 'active', - power_state.SUSPENDED: 'suspended', - power_state.PAUSED: 'paused', - power_state.SHUTDOWN: 'active', - power_state.SHUTOFF: 'active', - power_state.CRASHED: 'error', - power_state.FAILED: 'error'} + None: 'BUILD', + power_state.NOSTATE: 'BUILD', + power_state.RUNNING: 'ACTIVE', + power_state.BLOCKED: 'ACTIVE', + power_state.SUSPENDED: 'SUSPENDED', + power_state.PAUSED: 'PAUSED', + power_state.SHUTDOWN: 'ACTIVE', + power_state.SHUTOFF: 'ACTIVE', + power_state.CRASHED: 'ERROR', + power_state.FAILED: 'ERROR'} inst_dict = { 'id': int(inst['id']), 'name': inst['display_name'], 'addresses': self.addresses_builder.build(inst), - 'status': power_mapping[inst.get('state')].upper()} + 'status': power_mapping[inst.get('state')]} ctxt = nova.context.get_admin_context() compute_api = nova.compute.API() if compute_api.has_finished_migration(ctxt, inst['id']): - inst_dict['status'] = 'resize-confirm'.upper() + inst_dict['status'] = 'RESIZE-CONFIRM' # Return the metadata as a dictionary metadata = {} -- cgit From 95fe2026e869e2da29196f8bf3e48ae2a2560e46 Mon Sep 17 00:00:00 2001 From: Masanori Itoh Date: Tue, 5 Apr 2011 02:04:58 +0900 Subject: Moved 'name' property from to , corrected and fixes bug # 750482. --- nova/api/ec2/cloud.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 425784e8a..ecf144452 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -886,10 +886,7 @@ class CloudController(object): image_type = image['properties'].get('type') ec2_id = self._image_ec2_id(image.get('id'), image_type) name = image.get('name') - if name: - i['imageId'] = "%s (%s)" % (ec2_id, name) - else: - i['imageId'] = ec2_id + i['imageId'] = ec2_id kernel_id = image['properties'].get('kernel_id') if kernel_id: i['kernelId'] = self._image_ec2_id(kernel_id, 'kernel') @@ -897,11 +894,15 @@ class CloudController(object): if ramdisk_id: i['ramdiskId'] = self._image_ec2_id(ramdisk_id, 'ramdisk') i['imageOwnerId'] = image['properties'].get('owner_id') - i['imageLocation'] = image['properties'].get('image_location') + if name: + i['imageLocation'] = "%s (%s)" % (image['properties']. + get('image_location'), name) + else: + i['imageLocation'] = image['properties'].get('image_location') i['imageState'] = image['properties'].get('image_state') - i['displayName'] = image.get('name') + i['displayName'] = name i['description'] = image.get('description') - i['type'] = image_type + i['imageType'] = image_type i['isPublic'] = str(image['properties'].get('is_public', '')) == 'True' i['architecture'] = image['properties'].get('architecture') return i -- cgit From ff23dd2a3b86c816da04eddc903de0c8c3141954 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Tue, 5 Apr 2011 11:42:14 +0200 Subject: Allow CA code and state to be separated, and make sure CA code gets installed by setup.py install. --- nova/api/ec2/cloud.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 425784e8a..f119bd75c 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -103,10 +103,16 @@ class CloudController(object): # Gen root CA, if we don't have one root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file) if not os.path.exists(root_ca_path): + genrootca_sh_path = os.path.join(os.path.dirname(__file__), + os.path.pardir, + os.path.pardir, + 'CA', + 'genrootca.sh') + start = os.getcwd() os.chdir(FLAGS.ca_path) # TODO(vish): Do this with M2Crypto instead - utils.runthis(_("Generating root CA: %s"), "sh", "genrootca.sh") + utils.runthis(_("Generating root CA: %s"), "sh", genrootca_sh_path) os.chdir(start) def _get_mpi_data(self, context, project_id): -- cgit From d7013c9617d0740976a78ba87b1214c2b15ee702 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Tue, 5 Apr 2011 13:16:12 +0200 Subject: Automatically create CA state dir, and make sure the CA scripts look for the templates in the right places. --- nova/api/ec2/cloud.py | 1 + 1 file changed, 1 insertion(+) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index f119bd75c..5d6d9537a 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -110,6 +110,7 @@ class CloudController(object): 'genrootca.sh') start = os.getcwd() + os.makedirs(FLAGS.ca_path) os.chdir(FLAGS.ca_path) # TODO(vish): Do this with M2Crypto instead utils.runthis(_("Generating root CA: %s"), "sh", genrootca_sh_path) -- cgit