From 60ff2e3b72b5a3c9200f8fc47aac01cdb610bdcf Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Mon, 9 Jan 2012 14:02:02 -0800 Subject: Implements blueprint separate-nova-volumeapi * Moves openstack/v2 directory to compute and fixes tests accordingly * Moves some code from api/openstack/compute to shared location, for use by volume api * Implements basic volume functionality for types, volumes, and snapshots * Changes service name from osapi to osapi_compute (and adds osapi_volume) * Renames nova-api-os to nova-api-os-compute, adds nove-api-os-volume * Separate extension mechanism for compute and volume ** Removes flag osapi_extension and replaces with osapi_compute_extension and osapi_volume_extension * Updates the paste config * Fixes setup.py to include nova-os-api-compute and nova-os-api-volume * Fix bug in volume version code that occurred as result of trunk merge * Update integrated/test_volumes.py to use new endpoint Change-Id: I4c2e57c3cafd4e1a9e2ff3ce201c8cf28326afcd --- nova/api/mapper.py | 76 ++ nova/api/openstack/__init__.py | 69 ++ nova/api/openstack/auth.py | 257 +++++ nova/api/openstack/compute/__init__.py | 146 +++ nova/api/openstack/compute/consoles.py | 131 +++ nova/api/openstack/compute/contrib/__init__.py | 32 + nova/api/openstack/compute/contrib/accounts.py | 107 ++ .../api/openstack/compute/contrib/admin_actions.py | 291 +++++ nova/api/openstack/compute/contrib/cloudpipe.py | 172 +++ .../openstack/compute/contrib/console_output.py | 73 ++ .../openstack/compute/contrib/createserverext.py | 60 ++ .../openstack/compute/contrib/deferred_delete.py | 77 ++ nova/api/openstack/compute/contrib/disk_config.py | 200 ++++ .../openstack/compute/contrib/extended_status.py | 116 ++ .../openstack/compute/contrib/flavorextradata.py | 37 + .../openstack/compute/contrib/flavorextraspecs.py | 127 +++ .../openstack/compute/contrib/floating_ip_dns.py | 227 ++++ .../openstack/compute/contrib/floating_ip_pools.py | 104 ++ nova/api/openstack/compute/contrib/floating_ips.py | 237 +++++ nova/api/openstack/compute/contrib/hosts.py | 187 ++++ nova/api/openstack/compute/contrib/keypairs.py | 163 +++ nova/api/openstack/compute/contrib/multinic.py | 106 ++ nova/api/openstack/compute/contrib/networks.py | 117 ++ nova/api/openstack/compute/contrib/quotas.py | 102 ++ nova/api/openstack/compute/contrib/rescue.py | 80 ++ .../openstack/compute/contrib/security_groups.py | 592 +++++++++++ .../compute/contrib/server_action_list.py | 77 ++ .../compute/contrib/server_diagnostics.py | 69 ++ .../compute/contrib/simple_tenant_usage.py | 265 +++++ nova/api/openstack/compute/contrib/users.py | 145 +++ .../compute/contrib/virtual_interfaces.py | 93 ++ .../compute/contrib/virtual_storage_arrays.py | 687 ++++++++++++ nova/api/openstack/compute/contrib/volumes.py | 550 ++++++++++ nova/api/openstack/compute/contrib/volumetypes.py | 237 +++++ nova/api/openstack/compute/contrib/zones.py | 239 +++++ nova/api/openstack/compute/extensions.py | 45 + nova/api/openstack/compute/flavors.py | 112 ++ nova/api/openstack/compute/image_metadata.py | 118 ++ nova/api/openstack/compute/images.py | 195 ++++ nova/api/openstack/compute/ips.py | 105 ++ nova/api/openstack/compute/limits.py | 477 +++++++++ .../api/openstack/compute/ratelimiting/__init__.py | 222 ++++ nova/api/openstack/compute/schemas/atom-link.rng | 141 +++ nova/api/openstack/compute/schemas/atom.rng | 597 +++++++++++ .../openstack/compute/schemas/v1.1/addresses.rng | 14 + .../openstack/compute/schemas/v1.1/extension.rng | 11 + .../openstack/compute/schemas/v1.1/extensions.rng | 6 + nova/api/openstack/compute/schemas/v1.1/flavor.rng | 13 + .../api/openstack/compute/schemas/v1.1/flavors.rng | 6 + .../compute/schemas/v1.1/flavors_index.rng | 12 + nova/api/openstack/compute/schemas/v1.1/image.rng | 36 + nova/api/openstack/compute/schemas/v1.1/images.rng | 6 + .../compute/schemas/v1.1/images_index.rng | 15 + nova/api/openstack/compute/schemas/v1.1/limits.rng | 28 + .../openstack/compute/schemas/v1.1/metadata.rng | 9 + nova/api/openstack/compute/schemas/v1.1/server.rng | 59 + .../api/openstack/compute/schemas/v1.1/servers.rng | 6 + .../compute/schemas/v1.1/servers_index.rng | 15 + .../api/openstack/compute/schemas/v1.1/version.rng | 17 + .../openstack/compute/schemas/v1.1/versions.rng | 11 + nova/api/openstack/compute/server_metadata.py | 175 +++ nova/api/openstack/compute/servers.py | 1123 ++++++++++++++++++++ nova/api/openstack/compute/versions.py | 236 ++++ nova/api/openstack/compute/views/__init__.py | 0 nova/api/openstack/compute/views/addresses.py | 52 + nova/api/openstack/compute/views/flavors.py | 62 ++ nova/api/openstack/compute/views/images.py | 139 +++ nova/api/openstack/compute/views/limits.py | 96 ++ nova/api/openstack/compute/views/servers.py | 193 ++++ nova/api/openstack/compute/views/versions.py | 94 ++ nova/api/openstack/extensions.py | 623 +++++++++++ nova/api/openstack/urlmap.py | 297 ++++++ nova/api/openstack/v2/__init__.py | 182 ---- nova/api/openstack/v2/auth.py | 257 ----- nova/api/openstack/v2/consoles.py | 131 --- nova/api/openstack/v2/contrib/__init__.py | 90 -- nova/api/openstack/v2/contrib/accounts.py | 107 -- nova/api/openstack/v2/contrib/admin_actions.py | 291 ----- nova/api/openstack/v2/contrib/cloudpipe.py | 172 --- nova/api/openstack/v2/contrib/console_output.py | 73 -- nova/api/openstack/v2/contrib/createserverext.py | 60 -- nova/api/openstack/v2/contrib/deferred_delete.py | 77 -- nova/api/openstack/v2/contrib/disk_config.py | 200 ---- nova/api/openstack/v2/contrib/extended_status.py | 116 -- nova/api/openstack/v2/contrib/flavorextradata.py | 37 - nova/api/openstack/v2/contrib/flavorextraspecs.py | 127 --- nova/api/openstack/v2/contrib/floating_ip_dns.py | 227 ---- nova/api/openstack/v2/contrib/floating_ip_pools.py | 104 -- nova/api/openstack/v2/contrib/floating_ips.py | 237 ----- nova/api/openstack/v2/contrib/hosts.py | 187 ---- nova/api/openstack/v2/contrib/keypairs.py | 163 --- nova/api/openstack/v2/contrib/multinic.py | 106 -- nova/api/openstack/v2/contrib/networks.py | 117 -- nova/api/openstack/v2/contrib/quotas.py | 102 -- nova/api/openstack/v2/contrib/rescue.py | 80 -- nova/api/openstack/v2/contrib/security_groups.py | 592 ----------- .../api/openstack/v2/contrib/server_action_list.py | 77 -- .../api/openstack/v2/contrib/server_diagnostics.py | 69 -- .../openstack/v2/contrib/simple_tenant_usage.py | 265 ----- nova/api/openstack/v2/contrib/users.py | 145 --- .../api/openstack/v2/contrib/virtual_interfaces.py | 92 -- .../openstack/v2/contrib/virtual_storage_arrays.py | 687 ------------ nova/api/openstack/v2/contrib/volumes.py | 550 ---------- nova/api/openstack/v2/contrib/volumetypes.py | 237 ----- nova/api/openstack/v2/contrib/zones.py | 239 ----- nova/api/openstack/v2/extensions.py | 575 ---------- nova/api/openstack/v2/flavors.py | 112 -- nova/api/openstack/v2/image_metadata.py | 118 -- nova/api/openstack/v2/images.py | 195 ---- nova/api/openstack/v2/ips.py | 105 -- nova/api/openstack/v2/limits.py | 477 --------- nova/api/openstack/v2/ratelimiting/__init__.py | 222 ---- nova/api/openstack/v2/schemas/atom-link.rng | 141 --- nova/api/openstack/v2/schemas/atom.rng | 597 ----------- nova/api/openstack/v2/schemas/v1.1/addresses.rng | 14 - nova/api/openstack/v2/schemas/v1.1/extension.rng | 11 - nova/api/openstack/v2/schemas/v1.1/extensions.rng | 6 - nova/api/openstack/v2/schemas/v1.1/flavor.rng | 13 - nova/api/openstack/v2/schemas/v1.1/flavors.rng | 6 - .../openstack/v2/schemas/v1.1/flavors_index.rng | 12 - nova/api/openstack/v2/schemas/v1.1/image.rng | 36 - nova/api/openstack/v2/schemas/v1.1/images.rng | 6 - .../api/openstack/v2/schemas/v1.1/images_index.rng | 15 - nova/api/openstack/v2/schemas/v1.1/limits.rng | 28 - nova/api/openstack/v2/schemas/v1.1/metadata.rng | 9 - nova/api/openstack/v2/schemas/v1.1/server.rng | 59 - nova/api/openstack/v2/schemas/v1.1/servers.rng | 6 - .../openstack/v2/schemas/v1.1/servers_index.rng | 15 - nova/api/openstack/v2/schemas/v1.1/version.rng | 17 - nova/api/openstack/v2/schemas/v1.1/versions.rng | 11 - nova/api/openstack/v2/server_metadata.py | 175 --- nova/api/openstack/v2/servers.py | 1123 -------------------- nova/api/openstack/v2/urlmap.py | 297 ------ nova/api/openstack/v2/versions.py | 236 ---- nova/api/openstack/v2/views/__init__.py | 0 nova/api/openstack/v2/views/addresses.py | 52 - nova/api/openstack/v2/views/flavors.py | 62 -- nova/api/openstack/v2/views/images.py | 139 --- nova/api/openstack/v2/views/limits.py | 96 -- nova/api/openstack/v2/views/servers.py | 193 ---- nova/api/openstack/v2/views/versions.py | 94 -- nova/api/openstack/volume/__init__.py | 99 ++ nova/api/openstack/volume/contrib/__init__.py | 32 + nova/api/openstack/volume/extensions.py | 44 + nova/api/openstack/volume/snapshots.py | 183 ++++ nova/api/openstack/volume/types.py | 89 ++ nova/api/openstack/volume/versions.py | 83 ++ nova/api/openstack/volume/views/__init__.py | 16 + nova/api/openstack/volume/views/versions.py | 37 + nova/api/openstack/volume/volumes.py | 254 +++++ nova/api/openstack/xmlutil.py | 4 +- 151 files changed, 12453 insertions(+), 11471 deletions(-) create mode 100644 nova/api/mapper.py create mode 100644 nova/api/openstack/auth.py create mode 100644 nova/api/openstack/compute/__init__.py create mode 100644 nova/api/openstack/compute/consoles.py create mode 100644 nova/api/openstack/compute/contrib/__init__.py create mode 100644 nova/api/openstack/compute/contrib/accounts.py create mode 100644 nova/api/openstack/compute/contrib/admin_actions.py create mode 100644 nova/api/openstack/compute/contrib/cloudpipe.py create mode 100644 nova/api/openstack/compute/contrib/console_output.py create mode 100644 nova/api/openstack/compute/contrib/createserverext.py create mode 100644 nova/api/openstack/compute/contrib/deferred_delete.py create mode 100644 nova/api/openstack/compute/contrib/disk_config.py create mode 100644 nova/api/openstack/compute/contrib/extended_status.py create mode 100644 nova/api/openstack/compute/contrib/flavorextradata.py create mode 100644 nova/api/openstack/compute/contrib/flavorextraspecs.py create mode 100644 nova/api/openstack/compute/contrib/floating_ip_dns.py create mode 100644 nova/api/openstack/compute/contrib/floating_ip_pools.py create mode 100644 nova/api/openstack/compute/contrib/floating_ips.py create mode 100644 nova/api/openstack/compute/contrib/hosts.py create mode 100644 nova/api/openstack/compute/contrib/keypairs.py create mode 100644 nova/api/openstack/compute/contrib/multinic.py create mode 100644 nova/api/openstack/compute/contrib/networks.py create mode 100644 nova/api/openstack/compute/contrib/quotas.py create mode 100644 nova/api/openstack/compute/contrib/rescue.py create mode 100644 nova/api/openstack/compute/contrib/security_groups.py create mode 100644 nova/api/openstack/compute/contrib/server_action_list.py create mode 100644 nova/api/openstack/compute/contrib/server_diagnostics.py create mode 100644 nova/api/openstack/compute/contrib/simple_tenant_usage.py create mode 100644 nova/api/openstack/compute/contrib/users.py create mode 100644 nova/api/openstack/compute/contrib/virtual_interfaces.py create mode 100644 nova/api/openstack/compute/contrib/virtual_storage_arrays.py create mode 100644 nova/api/openstack/compute/contrib/volumes.py create mode 100644 nova/api/openstack/compute/contrib/volumetypes.py create mode 100644 nova/api/openstack/compute/contrib/zones.py create mode 100644 nova/api/openstack/compute/extensions.py create mode 100644 nova/api/openstack/compute/flavors.py create mode 100644 nova/api/openstack/compute/image_metadata.py create mode 100644 nova/api/openstack/compute/images.py create mode 100644 nova/api/openstack/compute/ips.py create mode 100644 nova/api/openstack/compute/limits.py create mode 100644 nova/api/openstack/compute/ratelimiting/__init__.py create mode 100644 nova/api/openstack/compute/schemas/atom-link.rng create mode 100644 nova/api/openstack/compute/schemas/atom.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/addresses.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/extension.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/extensions.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/flavor.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/flavors.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/flavors_index.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/image.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/images.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/images_index.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/limits.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/metadata.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/server.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/servers.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/servers_index.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/version.rng create mode 100644 nova/api/openstack/compute/schemas/v1.1/versions.rng create mode 100644 nova/api/openstack/compute/server_metadata.py create mode 100644 nova/api/openstack/compute/servers.py create mode 100644 nova/api/openstack/compute/versions.py create mode 100644 nova/api/openstack/compute/views/__init__.py create mode 100644 nova/api/openstack/compute/views/addresses.py create mode 100644 nova/api/openstack/compute/views/flavors.py create mode 100644 nova/api/openstack/compute/views/images.py create mode 100644 nova/api/openstack/compute/views/limits.py create mode 100644 nova/api/openstack/compute/views/servers.py create mode 100644 nova/api/openstack/compute/views/versions.py create mode 100644 nova/api/openstack/extensions.py create mode 100644 nova/api/openstack/urlmap.py delete mode 100644 nova/api/openstack/v2/__init__.py delete mode 100644 nova/api/openstack/v2/auth.py delete mode 100644 nova/api/openstack/v2/consoles.py delete mode 100644 nova/api/openstack/v2/contrib/__init__.py delete mode 100644 nova/api/openstack/v2/contrib/accounts.py delete mode 100644 nova/api/openstack/v2/contrib/admin_actions.py delete mode 100644 nova/api/openstack/v2/contrib/cloudpipe.py delete mode 100644 nova/api/openstack/v2/contrib/console_output.py delete mode 100644 nova/api/openstack/v2/contrib/createserverext.py delete mode 100644 nova/api/openstack/v2/contrib/deferred_delete.py delete mode 100644 nova/api/openstack/v2/contrib/disk_config.py delete mode 100644 nova/api/openstack/v2/contrib/extended_status.py delete mode 100644 nova/api/openstack/v2/contrib/flavorextradata.py delete mode 100644 nova/api/openstack/v2/contrib/flavorextraspecs.py delete mode 100644 nova/api/openstack/v2/contrib/floating_ip_dns.py delete mode 100644 nova/api/openstack/v2/contrib/floating_ip_pools.py delete mode 100644 nova/api/openstack/v2/contrib/floating_ips.py delete mode 100644 nova/api/openstack/v2/contrib/hosts.py delete mode 100644 nova/api/openstack/v2/contrib/keypairs.py delete mode 100644 nova/api/openstack/v2/contrib/multinic.py delete mode 100644 nova/api/openstack/v2/contrib/networks.py delete mode 100644 nova/api/openstack/v2/contrib/quotas.py delete mode 100644 nova/api/openstack/v2/contrib/rescue.py delete mode 100644 nova/api/openstack/v2/contrib/security_groups.py delete mode 100644 nova/api/openstack/v2/contrib/server_action_list.py delete mode 100644 nova/api/openstack/v2/contrib/server_diagnostics.py delete mode 100644 nova/api/openstack/v2/contrib/simple_tenant_usage.py delete mode 100644 nova/api/openstack/v2/contrib/users.py delete mode 100644 nova/api/openstack/v2/contrib/virtual_interfaces.py delete mode 100644 nova/api/openstack/v2/contrib/virtual_storage_arrays.py delete mode 100644 nova/api/openstack/v2/contrib/volumes.py delete mode 100644 nova/api/openstack/v2/contrib/volumetypes.py delete mode 100644 nova/api/openstack/v2/contrib/zones.py delete mode 100644 nova/api/openstack/v2/extensions.py delete mode 100644 nova/api/openstack/v2/flavors.py delete mode 100644 nova/api/openstack/v2/image_metadata.py delete mode 100644 nova/api/openstack/v2/images.py delete mode 100644 nova/api/openstack/v2/ips.py delete mode 100644 nova/api/openstack/v2/limits.py delete mode 100644 nova/api/openstack/v2/ratelimiting/__init__.py delete mode 100644 nova/api/openstack/v2/schemas/atom-link.rng delete mode 100644 nova/api/openstack/v2/schemas/atom.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/addresses.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/extension.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/extensions.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/flavor.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/flavors.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/flavors_index.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/image.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/images.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/images_index.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/limits.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/metadata.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/server.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/servers.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/servers_index.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/version.rng delete mode 100644 nova/api/openstack/v2/schemas/v1.1/versions.rng delete mode 100644 nova/api/openstack/v2/server_metadata.py delete mode 100644 nova/api/openstack/v2/servers.py delete mode 100644 nova/api/openstack/v2/urlmap.py delete mode 100644 nova/api/openstack/v2/versions.py delete mode 100644 nova/api/openstack/v2/views/__init__.py delete mode 100644 nova/api/openstack/v2/views/addresses.py delete mode 100644 nova/api/openstack/v2/views/flavors.py delete mode 100644 nova/api/openstack/v2/views/images.py delete mode 100644 nova/api/openstack/v2/views/limits.py delete mode 100644 nova/api/openstack/v2/views/servers.py delete mode 100644 nova/api/openstack/v2/views/versions.py create mode 100644 nova/api/openstack/volume/__init__.py create mode 100644 nova/api/openstack/volume/contrib/__init__.py create mode 100644 nova/api/openstack/volume/extensions.py create mode 100644 nova/api/openstack/volume/snapshots.py create mode 100644 nova/api/openstack/volume/types.py create mode 100644 nova/api/openstack/volume/versions.py create mode 100644 nova/api/openstack/volume/views/__init__.py create mode 100644 nova/api/openstack/volume/views/versions.py create mode 100644 nova/api/openstack/volume/volumes.py (limited to 'nova/api') diff --git a/nova/api/mapper.py b/nova/api/mapper.py new file mode 100644 index 000000000..cd26e06ee --- /dev/null +++ b/nova/api/mapper.py @@ -0,0 +1,76 @@ +# 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. + +""" +WSGI middleware for OpenStack API controllers. +""" + +import routes +import webob.dec +import webob.exc + +from nova.api.openstack import wsgi +from nova import flags +from nova import log as logging +from nova import wsgi as base_wsgi + + +LOG = logging.getLogger('nova.api.openstack.compute') +FLAGS = flags.FLAGS +flags.DEFINE_bool('allow_admin_api', + False, + 'When True, this API service will accept admin operations.') +flags.DEFINE_bool('allow_instance_snapshots', + True, + 'When True, this API service will permit instance snapshot operations.') + + +class FaultWrapper(base_wsgi.Middleware): + """Calls down the middleware stack, making exceptions into faults.""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + try: + return req.get_response(self.application) + except Exception as ex: + LOG.exception(_("Caught error: %s"), unicode(ex)) + exc = webob.exc.HTTPInternalServerError() + return wsgi.Fault(exc) + + +class APIMapper(routes.Mapper): + def routematch(self, url=None, environ=None): + if url is "": + result = self._match("", environ) + return result[0], result[1] + return routes.Mapper.routematch(self, url, environ) + + +class ProjectMapper(APIMapper): + def resource(self, member_name, collection_name, **kwargs): + if not ('parent_resource' in kwargs): + kwargs['path_prefix'] = '{project_id}/' + else: + parent_resource = kwargs['parent_resource'] + p_collection = parent_resource['collection_name'] + p_member = parent_resource['member_name'] + kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection, + p_member) + routes.Mapper.resource(self, member_name, + collection_name, + **kwargs) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index e69de29bb..dfc174a58 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -0,0 +1,69 @@ +# 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. + +""" +WSGI middleware for OpenStack API controllers. +""" + +import routes +import webob.dec +import webob.exc + +from nova.api.openstack import wsgi +from nova import flags +from nova import log as logging +from nova import wsgi as base_wsgi + + +LOG = logging.getLogger('nova.api.openstack') + + +class FaultWrapper(base_wsgi.Middleware): + """Calls down the middleware stack, making exceptions into faults.""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + try: + return req.get_response(self.application) + except Exception as ex: + LOG.exception(_("Caught error: %s"), unicode(ex)) + exc = webob.exc.HTTPInternalServerError() + return wsgi.Fault(exc) + + +class APIMapper(routes.Mapper): + def routematch(self, url=None, environ=None): + if url is "": + result = self._match("", environ) + return result[0], result[1] + return routes.Mapper.routematch(self, url, environ) + + +class ProjectMapper(APIMapper): + def resource(self, member_name, collection_name, **kwargs): + if not ('parent_resource' in kwargs): + kwargs['path_prefix'] = '{project_id}/' + else: + parent_resource = kwargs['parent_resource'] + p_collection = parent_resource['collection_name'] + p_member = parent_resource['member_name'] + kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection, + p_member) + routes.Mapper.resource(self, member_name, + collection_name, + **kwargs) diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py new file mode 100644 index 000000000..9a0432d51 --- /dev/null +++ b/nova/api/openstack/auth.py @@ -0,0 +1,257 @@ +# 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 hashlib +import os +import time + +import webob.dec +import webob.exc + +from nova.api.openstack import common +from nova.api.openstack import wsgi +from nova import auth +from nova import context +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils +from nova import wsgi as base_wsgi + +LOG = logging.getLogger('nova.api.openstack.compute.auth') +FLAGS = flags.FLAGS +flags.DECLARE('use_forwarded_for', 'nova.api.auth') + + +class NoAuthMiddleware(base_wsgi.Middleware): + """Return a fake token if one isn't specified.""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + if 'X-Auth-Token' not in req.headers: + user_id = req.headers.get('X-Auth-User', 'admin') + project_id = req.headers.get('X-Auth-Project-Id', 'admin') + os_url = os.path.join(req.url, project_id) + res = webob.Response() + # NOTE(vish): This is expecting and returning Auth(1.1), whereas + # keystone uses 2.0 auth. We should probably allow + # 2.0 auth here as well. + res.headers['X-Auth-Token'] = '%s:%s' % (user_id, project_id) + res.headers['X-Server-Management-Url'] = os_url + res.headers['X-Storage-Url'] = '' + res.headers['X-CDN-Management-Url'] = '' + res.content_type = 'text/plain' + res.status = '204' + return res + + token = req.headers['X-Auth-Token'] + user_id, _sep, project_id = token.partition(':') + project_id = project_id or user_id + remote_address = getattr(req, 'remote_address', '127.0.0.1') + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) + ctx = context.RequestContext(user_id, + project_id, + is_admin=True, + remote_address=remote_address) + + req.environ['nova.context'] = ctx + return self.application + + +class AuthMiddleware(base_wsgi.Middleware): + """Authorize the openstack API request or return an HTTP Forbidden.""" + + def __init__(self, application, db_driver=None): + if not db_driver: + db_driver = FLAGS.db_driver + self.db = utils.import_object(db_driver) + self.auth = auth.manager.AuthManager() + super(AuthMiddleware, self).__init__(application) + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + if not self.has_authentication(req): + return self.authenticate(req) + user_id = self.get_user_by_authentication(req) + if not user_id: + token = req.headers["X-Auth-Token"] + msg = _("%(user_id)s could not be found with token '%(token)s'") + LOG.warn(msg % locals()) + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + + # Get all valid projects for the user + projects = self.auth.get_projects(user_id) + if not projects: + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + + project_id = "" + path_parts = req.path.split('/') + # TODO(wwolf): this v1.1 check will be temporary as + # keystone should be taking this over at some point + if len(path_parts) > 1 and path_parts[1] in ('v1.1', 'v2'): + project_id = path_parts[2] + # Check that the project for project_id exists, and that user + # is authorized to use it + try: + self.auth.get_project(project_id) + except exception.ProjectNotFound: + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + if project_id not in [p.id for p in projects]: + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + else: + # As a fallback, set project_id from the headers, which is the v1.0 + # behavior. As a last resort, be forgiving to the user and set + # project_id based on a valid project of theirs. + try: + project_id = req.headers["X-Auth-Project-Id"] + except KeyError: + project_id = projects[0].id + + is_admin = self.auth.is_admin(user_id) + remote_address = getattr(req, 'remote_address', '127.0.0.1') + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) + ctx = context.RequestContext(user_id, + project_id, + is_admin=is_admin, + remote_address=remote_address) + req.environ['nova.context'] = ctx + + if not is_admin and not self.auth.is_project_member(user_id, + project_id): + msg = _("%(user_id)s must be an admin or a " + "member of %(project_id)s") + LOG.warn(msg % locals()) + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + + return self.application + + def has_authentication(self, req): + return 'X-Auth-Token' in req.headers + + def get_user_by_authentication(self, req): + return self.authorize_token(req.headers["X-Auth-Token"]) + + def authenticate(self, req): + # Unless the request is explicitly made against // don't + # honor it + path_info = req.path_info + if len(path_info) > 1: + msg = _("Authentication requests must be made against a version " + "root (e.g. /v2).") + LOG.warn(msg) + return wsgi.Fault(webob.exc.HTTPUnauthorized(explanation=msg)) + + def _get_auth_header(key): + """Ensures that the KeyError returned is meaningful.""" + try: + return req.headers[key] + except KeyError as ex: + raise KeyError(key) + try: + username = _get_auth_header('X-Auth-User') + key = _get_auth_header('X-Auth-Key') + except KeyError as ex: + msg = _("Could not find %s in request.") % ex + LOG.warn(msg) + return wsgi.Fault(webob.exc.HTTPUnauthorized(explanation=msg)) + + token, user = self._authorize_user(username, key, req) + if user and token: + res = webob.Response() + res.headers['X-Auth-Token'] = token['token_hash'] + res.headers['X-Server-Management-Url'] = \ + token['server_management_url'] + res.headers['X-Storage-Url'] = token['storage_url'] + res.headers['X-CDN-Management-Url'] = token['cdn_management_url'] + res.content_type = 'text/plain' + res.status = '204' + LOG.debug(_("Successfully authenticated '%s'") % username) + return res + else: + return wsgi.Fault(webob.exc.HTTPUnauthorized()) + + def authorize_token(self, token_hash): + """ retrieves user information from the datastore given a token + + If the token has expired, returns None + If the token is not found, returns None + Otherwise returns dict(id=(the authorized user's id)) + + This method will also remove the token if the timestamp is older than + 2 days ago. + """ + ctxt = context.get_admin_context() + try: + token = self.db.auth_token_get(ctxt, token_hash) + except exception.NotFound: + return None + if token: + delta = utils.utcnow() - token['created_at'] + if delta.days >= 2: + self.db.auth_token_destroy(ctxt, token['token_hash']) + else: + return token['user_id'] + return None + + def _authorize_user(self, username, key, req): + """Generates a new token and assigns it to a user. + + username - string + key - string API key + req - wsgi.Request object + """ + ctxt = context.get_admin_context() + + project_id = req.headers.get('X-Auth-Project-Id') + if project_id is None: + # If the project_id is not provided in the headers, be forgiving to + # the user and set project_id based on a valid project of theirs. + user = self.auth.get_user_from_access_key(key) + projects = self.auth.get_projects(user.id) + if not projects: + raise webob.exc.HTTPUnauthorized() + project_id = projects[0].id + + try: + user = self.auth.get_user_from_access_key(key) + except exception.NotFound: + LOG.warn(_("User not found with provided API key.")) + user = None + + if user and user.name == username: + 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'] = '' + os_url = req.url + token_dict['server_management_url'] = os_url.strip('/') + version = common.get_version_from_href(os_url) + if version in ('1.1', '2'): + token_dict['server_management_url'] += '/' + project_id + token_dict['storage_url'] = '' + token_dict['user_id'] = user.id + token = self.db.auth_token_create(ctxt, token_dict) + return token, user + elif user and user.name != username: + msg = _("Provided API key is valid, but not for user " + "'%(username)s'") % locals() + LOG.warn(msg) + + return None, None diff --git a/nova/api/openstack/compute/__init__.py b/nova/api/openstack/compute/__init__.py new file mode 100644 index 000000000..2f6e92a42 --- /dev/null +++ b/nova/api/openstack/compute/__init__.py @@ -0,0 +1,146 @@ +# 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. + +""" +WSGI middleware for OpenStack API controllers. +""" + +import routes +import webob.dec +import webob.exc + +import nova.api.openstack +from nova.api.openstack.compute import consoles +from nova.api.openstack.compute import extensions +from nova.api.openstack.compute import flavors +from nova.api.openstack.compute import images +from nova.api.openstack.compute import image_metadata +from nova.api.openstack.compute import ips +from nova.api.openstack.compute import limits +from nova.api.openstack.compute import servers +from nova.api.openstack.compute import server_metadata +from nova.api.openstack.compute import versions +from nova.api.openstack import wsgi +from nova import flags +from nova import log as logging +from nova import wsgi as base_wsgi + + +LOG = logging.getLogger('nova.api.openstack.compute') +FLAGS = flags.FLAGS +flags.DEFINE_bool('allow_admin_api', + False, + 'When True, this API service will accept admin operations.') +flags.DEFINE_bool('allow_instance_snapshots', + True, + 'When True, this API service will permit instance snapshot operations.') + + +class APIRouter(base_wsgi.Router): + """ + Routes requests on the OpenStack API to the appropriate controller + and method. + """ + + @classmethod + def factory(cls, global_config, **local_config): + """Simple paste factory, :class:`nova.wsgi.Router` doesn't have one""" + return cls() + + def __init__(self, ext_mgr=None): + if ext_mgr is None: + ext_mgr = extensions.ExtensionManager() + + mapper = nova.api.openstack.ProjectMapper() + self._setup_routes(mapper) + self._setup_ext_routes(mapper, ext_mgr) + super(APIRouter, self).__init__(mapper) + + def _setup_ext_routes(self, mapper, ext_mgr): + for resource in ext_mgr.get_resources(): + LOG.debug(_('Extended resource: %s'), + resource.collection) + + kargs = dict( + controller=wsgi.Resource( + resource.controller, resource.deserializer, + resource.serializer), + collection=resource.collection_actions, + member=resource.member_actions) + + if resource.parent: + kargs['parent_resource'] = resource.parent + + mapper.resource(resource.collection, resource.collection, **kargs) + + def _setup_routes(self, mapper): + mapper.connect("versions", "/", + controller=versions.create_resource(), + action='show') + + mapper.redirect("", "/") + + mapper.resource("console", "consoles", + controller=consoles.create_resource(), + parent_resource=dict(member_name='server', + collection_name='servers')) + + mapper.resource("server", "servers", + controller=servers.create_resource(), + collection={'detail': 'GET'}, + member={'action': 'POST'}) + + mapper.resource("ip", "ips", controller=ips.create_resource(), + parent_resource=dict(member_name='server', + collection_name='servers')) + + mapper.resource("image", "images", + controller=images.create_resource(), + collection={'detail': 'GET'}) + + mapper.resource("limit", "limits", + controller=limits.create_resource()) + + mapper.resource("flavor", "flavors", + controller=flavors.create_resource(), + collection={'detail': 'GET'}) + + image_metadata_controller = image_metadata.create_resource() + + mapper.resource("image_meta", "metadata", + controller=image_metadata_controller, + parent_resource=dict(member_name='image', + collection_name='images')) + + mapper.connect("metadata", "/{project_id}/images/{image_id}/metadata", + controller=image_metadata_controller, + action='update_all', + conditions={"method": ['PUT']}) + + server_metadata_controller = server_metadata.create_resource() + + mapper.resource("server_meta", "metadata", + controller=server_metadata_controller, + parent_resource=dict(member_name='server', + collection_name='servers')) + + mapper.connect("metadata", + "/{project_id}/servers/{server_id}/metadata", + controller=server_metadata_controller, + action='update_all', + conditions={"method": ['PUT']}) diff --git a/nova/api/openstack/compute/consoles.py b/nova/api/openstack/compute/consoles.py new file mode 100644 index 000000000..e9eee4c75 --- /dev/null +++ b/nova/api/openstack/compute/consoles.py @@ -0,0 +1,131 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob +from webob import exc + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import console +from nova import exception + + +def _translate_keys(cons): + """Coerces a console instance into proper dictionary format """ + pool = cons['pool'] + info = {'id': cons['id'], + 'console_type': pool['console_type']} + return dict(console=info) + + +def _translate_detail_keys(cons): + """Coerces a console instance into proper dictionary format with + correctly mapped attributes """ + pool = cons['pool'] + info = {'id': cons['id'], + 'console_type': pool['console_type'], + 'password': cons['password'], + 'instance_name': cons['instance_name'], + 'port': cons['port'], + 'host': pool['public_hostname']} + return dict(console=info) + + +class ConsoleTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('console', selector='console') + + id_elem = xmlutil.SubTemplateElement(root, 'id', selector='id') + id_elem.text = xmlutil.Selector() + + port_elem = xmlutil.SubTemplateElement(root, 'port', selector='port') + port_elem.text = xmlutil.Selector() + + host_elem = xmlutil.SubTemplateElement(root, 'host', selector='host') + host_elem.text = xmlutil.Selector() + + passwd_elem = xmlutil.SubTemplateElement(root, 'password', + selector='password') + passwd_elem.text = xmlutil.Selector() + + constype_elem = xmlutil.SubTemplateElement(root, 'console_type', + selector='console_type') + constype_elem.text = xmlutil.Selector() + + return xmlutil.MasterTemplate(root, 1) + + +class ConsolesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('consoles') + console = xmlutil.SubTemplateElement(root, 'console', + selector='consoles') + console.append(ConsoleTemplate()) + + return xmlutil.MasterTemplate(root, 1) + + +class Controller(object): + """The Consoles controller for the Openstack API""" + + def __init__(self): + self.console_api = console.API() + + @wsgi.serializers(xml=ConsolesTemplate) + def index(self, req, server_id): + """Returns a list of consoles for this instance""" + consoles = self.console_api.get_consoles( + req.environ['nova.context'], + server_id) + return dict(consoles=[_translate_keys(console) + for console in consoles]) + + def create(self, req, server_id): + """Creates a new console""" + self.console_api.create_console( + req.environ['nova.context'], + server_id) + + @wsgi.serializers(xml=ConsoleTemplate) + def show(self, req, server_id, id): + """Shows in-depth information on a specific console""" + try: + console = self.console_api.get_console( + req.environ['nova.context'], + server_id, + int(id)) + except exception.NotFound: + raise exc.HTTPNotFound() + return _translate_detail_keys(console) + + def update(self, req, server_id, id): + """You can't update a console""" + raise exc.HTTPNotImplemented() + + def delete(self, req, server_id, id): + """Deletes a console""" + try: + self.console_api.delete_console(req.environ['nova.context'], + server_id, + int(id)) + except exception.NotFound: + raise exc.HTTPNotFound() + return webob.Response(status_int=202) + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/contrib/__init__.py b/nova/api/openstack/compute/contrib/__init__.py new file mode 100644 index 000000000..2713a82f4 --- /dev/null +++ b/nova/api/openstack/compute/contrib/__init__.py @@ -0,0 +1,32 @@ +# 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. + +"""Contrib contains extensions that are shipped with nova. + +It can't be called 'extensions' because that causes namespacing problems. + +""" + +from nova import log as logging +from nova.api.openstack import extensions + + +LOG = logging.getLogger('nova.api.openstack.compute.contrib') + + +def standard_extensions(ext_mgr): + extensions.load_standard_extensions(ext_mgr, LOG, __path__, __package__) diff --git a/nova/api/openstack/compute/contrib/accounts.py b/nova/api/openstack/compute/contrib/accounts.py new file mode 100644 index 000000000..9253c037d --- /dev/null +++ b/nova/api/openstack/compute/contrib/accounts.py @@ -0,0 +1,107 @@ +# 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 webob.exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.auth import manager +from nova import exception +from nova import flags +from nova import log as logging + + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.api.openstack.compute.contrib.accounts') + + +class AccountTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('account', selector='account') + root.set('id', 'id') + root.set('name', 'name') + root.set('description', 'description') + root.set('manager', 'manager') + + return xmlutil.MasterTemplate(root, 1) + + +def _translate_keys(account): + return dict(id=account.id, + name=account.name, + description=account.description, + manager=account.project_manager_id) + + +class Controller(object): + + 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.AdminRequired() + + def index(self, req): + raise webob.exc.HTTPNotImplemented() + + @wsgi.serializers(xml=AccountTemplate) + 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 create(self, req, body): + """We use update with create-or-update semantics + because the id comes from an external source""" + raise webob.exc.HTTPNotImplemented() + + @wsgi.serializers(xml=AccountTemplate) + def update(self, req, id, body): + """This is really create or update.""" + self._check_admin(req.environ['nova.context']) + description = body['account'].get('description') + manager = body['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)) + + +class Accounts(extensions.ExtensionDescriptor): + """Admin-only access to accounts""" + + name = "Accounts" + alias = "os-accounts" + namespace = "http://docs.openstack.org/compute/ext/accounts/api/v1.1" + updated = "2011-12-23T00:00:00+00:00" + admin_only = True + + def get_resources(self): + #TODO(bcwaldon): This should be prefixed with 'os-' + res = extensions.ResourceExtension('accounts', + Controller()) + + return [res] diff --git a/nova/api/openstack/compute/contrib/admin_actions.py b/nova/api/openstack/compute/contrib/admin_actions.py new file mode 100644 index 000000000..dedef3061 --- /dev/null +++ b/nova/api/openstack/compute/contrib/admin_actions.py @@ -0,0 +1,291 @@ +# Copyright 2011 Openstack, LLC. +# +# 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 os.path +import traceback + +import webob +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova.scheduler import api as scheduler_api + + +FLAGS = flags.FLAGS +LOG = logging.getLogger("nova.api.openstack.compute.contrib.admin_actions") + + +class Admin_actions(extensions.ExtensionDescriptor): + """Enable admin-only server actions + + Actions include: pause, unpause, suspend, resume, migrate, + resetNetwork, injectNetworkInfo, lock, unlock, createBackup + """ + + name = "AdminActions" + alias = "os-admin-actions" + namespace = "http://docs.openstack.org/compute/ext/admin-actions/api/v1.1" + updated = "2011-09-20T00:00:00+00:00" + admin_only = True + + def __init__(self, ext_mgr): + super(Admin_actions, self).__init__(ext_mgr) + self.compute_api = compute.API() + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _pause(self, input_dict, req, id): + """Permit Admins to pause the server""" + ctxt = req.environ['nova.context'] + try: + server = self.compute_api.get(ctxt, id) + self.compute_api.pause(ctxt, server) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'pause') + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::pause %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _unpause(self, input_dict, req, id): + """Permit Admins to unpause the server""" + ctxt = req.environ['nova.context'] + try: + server = self.compute_api.get(ctxt, id) + self.compute_api.unpause(ctxt, server) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'unpause') + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::unpause %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _suspend(self, input_dict, req, id): + """Permit admins to suspend the server""" + context = req.environ['nova.context'] + try: + server = self.compute_api.get(context, id) + self.compute_api.suspend(context, server) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'suspend') + except Exception: + readable = traceback.format_exc() + LOG.exception(_("compute.api::suspend %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _resume(self, input_dict, req, id): + """Permit admins to resume the server from suspend""" + context = req.environ['nova.context'] + try: + server = self.compute_api.get(context, id) + self.compute_api.resume(context, server) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'resume') + except Exception: + readable = traceback.format_exc() + LOG.exception(_("compute.api::resume %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _migrate(self, input_dict, req, id): + """Permit admins to migrate a server to a new host""" + context = req.environ['nova.context'] + try: + instance = self.compute_api.get(context, id) + self.compute_api.resize(req.environ['nova.context'], instance) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'migrate') + except Exception, e: + LOG.exception(_("Error in migrate %s"), e) + raise exc.HTTPBadRequest() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _reset_network(self, input_dict, req, id): + """Permit admins to reset networking on an server""" + context = req.environ['nova.context'] + try: + instance = self.compute_api.get(context, id) + self.compute_api.reset_network(context, instance) + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::reset_network %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _inject_network_info(self, input_dict, req, id): + """Permit admins to inject network info into a server""" + context = req.environ['nova.context'] + try: + instance = self.compute_api.get(context, id) + self.compute_api.inject_network_info(context, instance) + except exception.InstanceNotFound: + raise exc.HTTPNotFound(_("Server not found")) + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::inject_network_info %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _lock(self, input_dict, req, id): + """Permit admins to lock a server""" + context = req.environ['nova.context'] + try: + instance = self.compute_api.get(context, id) + self.compute_api.lock(context, instance) + except exception.InstanceNotFound: + raise exc.HTTPNotFound(_("Server not found")) + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::lock %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + @exception.novaclient_converter + @scheduler_api.redirect_handler + def _unlock(self, input_dict, req, id): + """Permit admins to lock a server""" + context = req.environ['nova.context'] + try: + instance = self.compute_api.get(context, id) + self.compute_api.unlock(context, instance) + except exception.InstanceNotFound: + raise exc.HTTPNotFound(_("Server not found")) + except Exception: + readable = traceback.format_exc() + LOG.exception(_("Compute.api::unlock %s"), readable) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + def _create_backup(self, input_dict, req, instance_id): + """Backup a server instance. + + Images now have an `image_type` associated with them, which can be + 'snapshot' or the backup type, like 'daily' or 'weekly'. + + If the image_type is backup-like, then the rotation factor can be + included and that will cause the oldest backups that exceed the + rotation factor to be deleted. + + """ + context = req.environ["nova.context"] + + try: + entity = input_dict["createBackup"] + except (KeyError, TypeError): + raise exc.HTTPBadRequest(_("Malformed request body")) + + try: + image_name = entity["name"] + backup_type = entity["backup_type"] + rotation = entity["rotation"] + + except KeyError as missing_key: + msg = _("createBackup entity requires %s attribute") % missing_key + raise exc.HTTPBadRequest(explanation=msg) + + except TypeError: + msg = _("Malformed createBackup entity") + raise exc.HTTPBadRequest(explanation=msg) + + try: + rotation = int(rotation) + except ValueError: + msg = _("createBackup attribute 'rotation' must be an integer") + raise exc.HTTPBadRequest(explanation=msg) + + # preserve link to server in image properties + server_ref = os.path.join(req.application_url, 'servers', instance_id) + props = {'instance_ref': server_ref} + + metadata = entity.get('metadata', {}) + common.check_img_metadata_quota_limit(context, metadata) + try: + props.update(metadata) + except ValueError: + msg = _("Invalid metadata") + raise exc.HTTPBadRequest(explanation=msg) + + try: + instance = self.compute_api.get(context, instance_id) + except exception.NotFound: + raise exc.HTTPNotFound(_("Instance not found")) + + try: + image = self.compute_api.backup(context, instance, image_name, + backup_type, rotation, extra_properties=props) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'createBackup') + + # build location of newly-created image entity + image_id = str(image['id']) + image_ref = os.path.join(req.application_url, 'images', image_id) + + resp = webob.Response(status_int=202) + resp.headers['Location'] = image_ref + return resp + + def get_actions(self): + actions = [ + #TODO(bcwaldon): These actions should be prefixed with 'os-' + extensions.ActionExtension("servers", "pause", self._pause), + extensions.ActionExtension("servers", "unpause", self._unpause), + extensions.ActionExtension("servers", "suspend", self._suspend), + extensions.ActionExtension("servers", "resume", self._resume), + extensions.ActionExtension("servers", "migrate", self._migrate), + + extensions.ActionExtension("servers", + "createBackup", + self._create_backup), + + extensions.ActionExtension("servers", + "resetNetwork", + self._reset_network), + + extensions.ActionExtension("servers", + "injectNetworkInfo", + self._inject_network_info), + + extensions.ActionExtension("servers", "lock", self._lock), + extensions.ActionExtension("servers", "unlock", self._unlock), + ] + + return actions diff --git a/nova/api/openstack/compute/contrib/cloudpipe.py b/nova/api/openstack/compute/contrib/cloudpipe.py new file mode 100644 index 000000000..17bfa810b --- /dev/null +++ b/nova/api/openstack/compute/contrib/cloudpipe.py @@ -0,0 +1,172 @@ +# Copyright 2011 Openstack, LLC. +# +# 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. + +"""Connect your vlan to the world.""" + +import os + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova.auth import manager +from nova.cloudpipe import pipelib +from nova import compute +from nova.compute import vm_states +from nova import db +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils + + +FLAGS = flags.FLAGS +LOG = logging.getLogger("nova.api.openstack.compute.contrib.cloudpipe") + + +class CloudpipeTemplate(xmlutil.TemplateBuilder): + def construct(self): + return xmlutil.MasterTemplate(xmlutil.make_flat_dict('cloudpipe'), 1) + + +class CloudpipesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('cloudpipes') + elem = xmlutil.make_flat_dict('cloudpipe', selector='cloudpipes', + subselector='cloudpipe') + root.append(elem) + return xmlutil.MasterTemplate(root, 1) + + +class CloudpipeController(object): + """Handle creating and listing cloudpipe instances.""" + + def __init__(self): + self.compute_api = compute.API() + self.auth_manager = manager.AuthManager() + self.cloudpipe = pipelib.CloudPipe() + self.setup() + + def setup(self): + """Ensure the keychains and folders exist.""" + # TODO(todd): this was copyed from api.ec2.cloud + # FIXME(ja): this should be moved to a nova-manage command, + # if not setup throw exceptions instead of running + # Create keys folder, if it doesn't exist + if not os.path.exists(FLAGS.keys_path): + os.makedirs(FLAGS.keys_path) + # Gen root CA, if we don't have one + root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file) + if not os.path.exists(root_ca_path): + genrootca_sh_path = os.path.join(os.path.dirname(__file__), + os.path.pardir, + os.path.pardir, + 'CA', + 'genrootca.sh') + + start = os.getcwd() + if not os.path.exists(FLAGS.ca_path): + os.makedirs(FLAGS.ca_path) + os.chdir(FLAGS.ca_path) + # TODO(vish): Do this with M2Crypto instead + utils.runthis(_("Generating root CA: %s"), "sh", genrootca_sh_path) + os.chdir(start) + + def _get_cloudpipe_for_project(self, context, project_id): + """Get the cloudpipe instance for a project ID.""" + # NOTE(todd): this should probably change to compute_api.get_all + # or db.instance_get_project_vpn + for instance in db.instance_get_all_by_project(context, project_id): + if (instance['image_id'] == str(FLAGS.vpn_image_id) + and instance['vm_state'] != vm_states.DELETED): + return instance + + def _vpn_dict(self, project, vpn_instance): + rv = {'project_id': project.id, + 'public_ip': project.vpn_ip, + 'public_port': project.vpn_port} + if vpn_instance: + rv['instance_id'] = vpn_instance['uuid'] + rv['created_at'] = utils.isotime(vpn_instance['created_at']) + address = vpn_instance.get('fixed_ip', None) + if address: + rv['internal_ip'] = address['address'] + 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'] = 'invalid' + else: + rv['state'] = 'pending' + return rv + + @wsgi.serializers(xml=CloudpipeTemplate) + def create(self, req, body): + """Create a new cloudpipe instance, if none exists. + + Parameters: {cloudpipe: {project_id: XYZ}} + """ + + ctxt = req.environ['nova.context'] + params = body.get('cloudpipe', {}) + project_id = params.get('project_id', ctxt.project_id) + instance = self._get_cloudpipe_for_project(ctxt, project_id) + if not instance: + proj = self.auth_manager.get_project(project_id) + user_id = proj.project_manager_id + try: + self.cloudpipe.launch_vpn_instance(project_id, user_id) + except db.NoMoreNetworks: + msg = _("Unable to claim IP for VPN instances, ensure it " + "isn't running, and try again in a few minutes") + raise exception.ApiError(msg) + instance = self._get_cloudpipe_for_project(ctxt, proj) + return {'instance_id': instance['uuid']} + + @wsgi.serializers(xml=CloudpipesTemplate) + def index(self, req): + """Show admins the list of running cloudpipe instances.""" + context = req.environ['nova.context'] + vpns = [] + # TODO(todd): could use compute_api.get_all with admin context? + for project in self.auth_manager.get_projects(): + instance = self._get_cloudpipe_for_project(context, project.id) + vpns.append(self._vpn_dict(project, instance)) + return {'cloudpipes': vpns} + + +class Cloudpipe(extensions.ExtensionDescriptor): + """Adds actions to create cloudpipe instances. + + When running with the Vlan network mode, you need a mechanism to route + from the public Internet to your vlans. This mechanism is known as a + cloudpipe. + + At the time of creating this class, only OpenVPN is supported. Support for + a SSH Bastion host is forthcoming. + """ + + name = "Cloudpipe" + alias = "os-cloudpipe" + namespace = "http://docs.openstack.org/compute/ext/cloudpipe/api/v1.1" + updated = "2011-12-16T00:00:00+00:00" + admin_only = True + + def get_resources(self): + resources = [] + res = extensions.ResourceExtension('os-cloudpipe', + CloudpipeController()) + resources.append(res) + return resources diff --git a/nova/api/openstack/compute/contrib/console_output.py b/nova/api/openstack/compute/contrib/console_output.py new file mode 100644 index 000000000..a27d8663b --- /dev/null +++ b/nova/api/openstack/compute/contrib/console_output.py @@ -0,0 +1,73 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Grid Dynamics +# Copyright 2011 Eldar Nugaev, Kirill Shileev, Ilya Alekseyev +# +# 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 + +from nova import compute +from nova import exception +from nova import log as logging +from nova.api.openstack import extensions + + +LOG = logging.getLogger('nova.api.openstack.compute.contrib.console_output') + + +class Console_output(extensions.ExtensionDescriptor): + """Console log output support, with tailing ability.""" + + name = "Console_output" + alias = "os-console-output" + namespace = "http://docs.openstack.org/compute/ext/" \ + "os-console-output/api/v2" + updated = "2011-12-08T00:00:00+00:00" + + def __init__(self, ext_mgr): + self.compute_api = compute.API() + super(Console_output, self).__init__(ext_mgr) + + def get_console_output(self, input_dict, req, server_id): + """Get text console output.""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.routing_get(context, server_id) + except exception.NotFound: + raise webob.exc.HTTPNotFound(_('Instance not found')) + + try: + length = input_dict['os-getConsoleOutput'].get('length') + except (TypeError, KeyError): + raise webob.exc.HTTPBadRequest(_('Malformed request body')) + + try: + output = self.compute_api.get_console_output(context, + instance, + length) + except exception.ApiError, e: + raise webob.exc.HTTPBadRequest(explanation=e.message) + except exception.NotAuthorized, e: + raise webob.exc.HTTPUnauthorized() + + return {'output': output} + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [extensions.ActionExtension("servers", "os-getConsoleOutput", + self.get_console_output)] + + return actions diff --git a/nova/api/openstack/compute/contrib/createserverext.py b/nova/api/openstack/compute/contrib/createserverext.py new file mode 100644 index 000000000..25b53a0d6 --- /dev/null +++ b/nova/api/openstack/compute/contrib/createserverext.py @@ -0,0 +1,60 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# +# 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.compute import servers +from nova.api.openstack.compute import views +from nova.api.openstack import wsgi + + +class ViewBuilder(views.servers.ViewBuilder): + """Adds security group output when viewing server details.""" + + def show(self, request, instance): + """Detailed view of a single instance.""" + server = super(ViewBuilder, self).show(request, instance) + server["server"]["security_groups"] = self._get_groups(instance) + return server + + def _get_groups(self, instance): + """Get a list of security groups for this instance.""" + groups = instance.get('security_groups') + if groups is not None: + return [{"name": group["name"]} for group in groups] + + +class Controller(servers.Controller): + _view_builder_class = ViewBuilder + + +class Createserverext(extensions.ExtensionDescriptor): + """Extended support to the Create Server v1.1 API""" + + name = "Createserverext" + alias = "os-create-server-ext" + namespace = "http://docs.openstack.org/compute/ext/" \ + "createserverext/api/v1.1" + updated = "2011-07-19T00:00:00+00:00" + + def get_resources(self): + resources = [] + controller = Controller() + + res = extensions.ResourceExtension('os-create-server-ext', + controller=controller) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/deferred_delete.py b/nova/api/openstack/compute/contrib/deferred_delete.py new file mode 100644 index 000000000..312c22c80 --- /dev/null +++ b/nova/api/openstack/compute/contrib/deferred_delete.py @@ -0,0 +1,77 @@ +# 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. + +"""The deferred instance delete extension.""" + +import webob + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack.compute import servers +from nova import compute +from nova import exception +from nova import log as logging + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.deferred-delete") + + +class Deferred_delete(extensions.ExtensionDescriptor): + """Instance deferred delete""" + + name = "DeferredDelete" + alias = "os-deferred-delete" + namespace = "http://docs.openstack.org/compute/ext/" \ + "deferred-delete/api/v1.1" + updated = "2011-09-01T00:00:00+00:00" + + def __init__(self, ext_mgr): + super(Deferred_delete, self).__init__(ext_mgr) + self.compute_api = compute.API() + + def _restore(self, input_dict, req, instance_id): + """Restore a previously deleted instance.""" + + context = req.environ["nova.context"] + instance = self.compute_api.get(context, instance_id) + try: + self.compute_api.restore(context, instance) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'restore') + return webob.Response(status_int=202) + + def _force_delete(self, input_dict, req, instance_id): + """Force delete of instance before deferred cleanup.""" + + context = req.environ["nova.context"] + instance = self.compute_api.get(context, instance_id) + try: + self.compute_api.force_delete(context, instance) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'forceDelete') + return webob.Response(status_int=202) + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + extensions.ActionExtension("servers", "restore", + self._restore), + extensions.ActionExtension("servers", "forceDelete", + self._force_delete), + ] + + return actions diff --git a/nova/api/openstack/compute/contrib/disk_config.py b/nova/api/openstack/compute/contrib/disk_config.py new file mode 100644 index 000000000..392291652 --- /dev/null +++ b/nova/api/openstack/compute/contrib/disk_config.py @@ -0,0 +1,200 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# +# 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 + +"""Disk Config extension.""" + +from xml.dom import minidom + +from webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import xmlutil +from nova import compute +from nova import db +from nova import log as logging +from nova import utils + +LOG = logging.getLogger('nova.api.openstack.contrib.disk_config') + +ALIAS = 'RAX-DCF' +XMLNS_DCF = "http://docs.rackspacecloud.com/servers/api/ext/diskConfig/v1.0" + + +class ServerDiskConfigTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server') + root.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) + return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) + + +class ServersDiskConfigTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('servers') + elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') + elem.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) + return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) + + +class ImageDiskConfigTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('image') + root.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) + return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) + + +class ImagesDiskConfigTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('images') + elem = xmlutil.SubTemplateElement(root, 'image', selector='images') + elem.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) + return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) + + +def disk_config_to_api(value): + return 'AUTO' if value else 'MANUAL' + + +def disk_config_from_api(value): + if value == 'AUTO': + return True + elif value == 'MANUAL': + return False + else: + msg = _("RAX-DCF:diskConfig must be either 'MANUAL' or 'AUTO'.") + raise exc.HTTPBadRequest(explanation=msg) + + +class Disk_config(extensions.ExtensionDescriptor): + """Disk Management Extension""" + + name = "DiskConfig" + alias = ALIAS + namespace = XMLNS_DCF + updated = "2011-09-27:00:00+00:00" + + API_DISK_CONFIG = "%s:diskConfig" % ALIAS + INTERNAL_DISK_CONFIG = "auto_disk_config" + + def __init__(self, ext_mgr): + super(Disk_config, self).__init__(ext_mgr) + self.compute_api = compute.API() + + def _extract_resource_from_body(self, res, body, + singular, singular_template, plural, plural_template): + """Returns a list of the given resources from the request body. + + The templates passed in are used for XML serialization. + """ + template = res.environ.get('nova.template') + if plural in body: + resources = body[plural] + if template: + template.attach(plural_template) + elif singular in body: + resources = [body[singular]] + if template: + template.attach(singular_template) + else: + resources = [] + + return resources + + def _GET_servers(self, req, res, body): + context = req.environ['nova.context'] + + servers = self._extract_resource_from_body(res, body, + singular='server', singular_template=ServerDiskConfigTemplate(), + plural='servers', plural_template=ServersDiskConfigTemplate()) + + # Filter out any servers that already have the key set (most likely + # from a remote zone) + servers = filter(lambda s: self.API_DISK_CONFIG not in s, servers) + + # Get DB information for servers + uuids = [server['id'] for server in servers] + db_servers = db.instance_get_all_by_filters(context, {'uuid': uuids}) + db_servers = dict([(s['uuid'], s) for s in db_servers]) + + for server in servers: + db_server = db_servers.get(server['id']) + if db_server: + value = db_server[self.INTERNAL_DISK_CONFIG] + server[self.API_DISK_CONFIG] = disk_config_to_api(value) + + return res + + def _GET_images(self, req, res, body): + images = self._extract_resource_from_body(res, body, + singular='image', singular_template=ImageDiskConfigTemplate(), + plural='images', plural_template=ImagesDiskConfigTemplate()) + + for image in images: + metadata = image['metadata'] + + if self.INTERNAL_DISK_CONFIG in metadata: + raw_value = metadata[self.INTERNAL_DISK_CONFIG] + value = utils.bool_from_str(raw_value) + image[self.API_DISK_CONFIG] = disk_config_to_api(value) + + return res + + def _POST_servers(self, req, res, body): + return self._GET_servers(req, res, body) + + def _pre_POST_servers(self, req): + # NOTE(sirp): deserialization currently occurs *after* pre-processing + # extensions are called. Until extensions are refactored so that + # deserialization occurs earlier, we have to perform the + # deserialization ourselves. + content_type = req.content_type + + if 'xml' in content_type: + node = minidom.parseString(req.body) + server = node.getElementsByTagName('server')[0] + api_value = server.getAttribute(self.API_DISK_CONFIG) + if api_value: + value = disk_config_from_api(api_value) + server.setAttribute(self.INTERNAL_DISK_CONFIG, str(value)) + req.body = str(node.toxml()) + else: + body = utils.loads(req.body) + server = body['server'] + api_value = server.get(self.API_DISK_CONFIG) + if api_value: + value = disk_config_from_api(api_value) + server[self.INTERNAL_DISK_CONFIG] = value + req.body = utils.dumps(body) + + def _pre_PUT_servers(self, req): + return self._pre_POST_servers(req) + + def get_request_extensions(self): + ReqExt = extensions.RequestExtension + return [ + ReqExt(method='GET', + url_route='/:(project_id)/servers/:(id)', + handler=self._GET_servers), + ReqExt(method='POST', + url_route='/:(project_id)/servers', + handler=self._POST_servers, + pre_handler=self._pre_POST_servers), + ReqExt(method='PUT', + url_route='/:(project_id)/servers/:(id)', + pre_handler=self._pre_PUT_servers), + ReqExt(method='GET', + url_route='/:(project_id)/images/:(id)', + handler=self._GET_images) + ] diff --git a/nova/api/openstack/compute/contrib/extended_status.py b/nova/api/openstack/compute/contrib/extended_status.py new file mode 100644 index 000000000..7f9301b93 --- /dev/null +++ b/nova/api/openstack/compute/contrib/extended_status.py @@ -0,0 +1,116 @@ +# Copyright 2011 Openstack, LLC. +# +# 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 Extended Status Admin API extension.""" + +from webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging + + +FLAGS = flags.FLAGS +LOG = logging.getLogger("nova.api.openstack.compute.contrib.extendedstatus") + + +class Extended_status(extensions.ExtensionDescriptor): + """Extended Status support""" + + name = "ExtendedStatus" + alias = "OS-EXT-STS" + namespace = "http://docs.openstack.org/compute/ext/" \ + "extended_status/api/v1.1" + updated = "2011-11-03T00:00:00+00:00" + admin_only = True + + def get_request_extensions(self): + request_extensions = [] + + def _get_and_extend_one(context, server_id, body): + compute_api = compute.API() + try: + inst_ref = compute_api.routing_get(context, server_id) + except exception.NotFound: + LOG.warn("Instance %s not found (one)" % server_id) + explanation = _("Server not found.") + raise exc.HTTPNotFound(explanation=explanation) + + for state in ['task_state', 'vm_state', 'power_state']: + key = "%s:%s" % (Extended_status.alias, state) + body['server'][key] = inst_ref[state] + + def _get_and_extend_all(context, body): + # TODO(mdietz): This is a brilliant argument for this to *not* + # be an extension. The problem is we either have to 1) duplicate + # the logic from the servers controller or 2) do what we did + # and iterate over the list of potentially sorted, limited + # and whatever else elements and find each individual. + compute_api = compute.API() + + for server in list(body['servers']): + try: + inst_ref = compute_api.routing_get(context, server['id']) + except exception.NotFound: + # NOTE(dtroyer): A NotFound exception at this point + # happens because a delete was in progress and the + # server that was present in the original call to + # compute.api.get_all() is no longer present. + # Delete it from the response and move on. + LOG.warn("Instance %s not found (all)" % server['id']) + body['servers'].remove(server) + continue + + #TODO(bcwaldon): these attributes should be prefixed with + # something specific to this extension + for state in ['task_state', 'vm_state', 'power_state']: + key = "%s:%s" % (Extended_status.alias, state) + server[key] = inst_ref[state] + + def _extended_status_handler(req, res, body): + context = req.environ['nova.context'] + server_id = req.environ['wsgiorg.routing_args'][1].get('id') + + if 'nova.template' in req.environ: + tmpl = req.environ['nova.template'] + tmpl.attach(ExtendedStatusTemplate()) + + if server_id: + _get_and_extend_one(context, server_id, body) + else: + _get_and_extend_all(context, body) + return res + + req_ext = extensions.RequestExtension('GET', + '/:(project_id)/servers/:(id)', + _extended_status_handler) + request_extensions.append(req_ext) + + return request_extensions + + +class ExtendedStatusTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server') + root.set('{%s}task_state' % Extended_status.namespace, + '%s:task_state' % Extended_status.alias) + root.set('{%s}power_state' % Extended_status.namespace, + '%s:power_state' % Extended_status.alias) + root.set('{%s}vm_state' % Extended_status.namespace, + '%s:vm_state' % Extended_status.alias) + return xmlutil.SlaveTemplate(root, 1, nsmap={ + Extended_status.alias: Extended_status.namespace}) diff --git a/nova/api/openstack/compute/contrib/flavorextradata.py b/nova/api/openstack/compute/contrib/flavorextradata.py new file mode 100644 index 000000000..bf6fa8040 --- /dev/null +++ b/nova/api/openstack/compute/contrib/flavorextradata.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Canonical Ltd. +# 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 Flavor extra data extension +Openstack API version 1.1 lists "name", "ram", "disk", "vcpus" as flavor +attributes. This extension adds to that list: + rxtx_cap + rxtx_quota + swap +""" + +from nova.api.openstack import extensions + + +class Flavorextradata(extensions.ExtensionDescriptor): + """Provide additional data for flavors""" + + name = "FlavorExtraData" + alias = "os-flavor-extra-data" + namespace = "http://docs.openstack.org/compute/ext/" \ + "flavor_extra_data/api/v1.1" + updated = "2011-09-14T00:00:00+00:00" diff --git a/nova/api/openstack/compute/contrib/flavorextraspecs.py b/nova/api/openstack/compute/contrib/flavorextraspecs.py new file mode 100644 index 000000000..eafea5d1f --- /dev/null +++ b/nova/api/openstack/compute/contrib/flavorextraspecs.py @@ -0,0 +1,127 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 University of Southern California +# 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 instance type extra specs extension""" + +from webob import exc + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import db +from nova import exception + + +class ExtraSpecsTemplate(xmlutil.TemplateBuilder): + def construct(self): + return xmlutil.MasterTemplate(xmlutil.make_flat_dict('extra_specs'), 1) + + +class FlavorExtraSpecsController(object): + """ The flavor extra specs API controller for the Openstack API """ + + def _get_extra_specs(self, context, flavor_id): + extra_specs = db.instance_type_extra_specs_get(context, flavor_id) + specs_dict = {} + for key, value in extra_specs.iteritems(): + specs_dict[key] = value + return dict(extra_specs=specs_dict) + + def _check_body(self, body): + if body is None or body == "": + expl = _('No Request Body') + raise exc.HTTPBadRequest(explanation=expl) + + @wsgi.serializers(xml=ExtraSpecsTemplate) + def index(self, req, flavor_id): + """ Returns the list of extra specs for a givenflavor """ + context = req.environ['nova.context'] + return self._get_extra_specs(context, flavor_id) + + @wsgi.serializers(xml=ExtraSpecsTemplate) + def create(self, req, flavor_id, body): + self._check_body(body) + context = req.environ['nova.context'] + specs = body.get('extra_specs') + try: + db.instance_type_extra_specs_update_or_create(context, + flavor_id, + specs) + except exception.QuotaError as error: + self._handle_quota_error(error) + return body + + @wsgi.serializers(xml=ExtraSpecsTemplate) + def update(self, req, flavor_id, id, body): + self._check_body(body) + context = req.environ['nova.context'] + 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) + try: + db.instance_type_extra_specs_update_or_create(context, + flavor_id, + body) + except exception.QuotaError as error: + self._handle_quota_error(error) + + return body + + @wsgi.serializers(xml=ExtraSpecsTemplate) + def show(self, req, flavor_id, id): + """ Return a single extra spec item """ + context = req.environ['nova.context'] + specs = self._get_extra_specs(context, flavor_id) + if id in specs['extra_specs']: + return {id: specs['extra_specs'][id]} + else: + raise exc.HTTPNotFound() + + def delete(self, req, flavor_id, id): + """ Deletes an existing extra spec """ + context = req.environ['nova.context'] + db.instance_type_extra_specs_delete(context, flavor_id, id) + + def _handle_quota_error(self, error): + """Reraise quota errors as api-specific http exceptions.""" + if error.code == "MetadataLimitExceeded": + raise exc.HTTPBadRequest(explanation=error.message) + raise error + + +class Flavorextraspecs(extensions.ExtensionDescriptor): + """Instance type (flavor) extra specs""" + + name = "FlavorExtraSpecs" + alias = "os-flavor-extra-specs" + namespace = "http://docs.openstack.org/compute/ext/" \ + "flavor_extra_specs/api/v1.1" + updated = "2011-06-23T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'os-extra_specs', + FlavorExtraSpecsController(), + parent=dict(member_name='flavor', collection_name='flavors')) + + resources.append(res) + return resources diff --git a/nova/api/openstack/compute/contrib/floating_ip_dns.py b/nova/api/openstack/compute/contrib/floating_ip_dns.py new file mode 100644 index 000000000..032d5bd7e --- /dev/null +++ b/nova/api/openstack/compute/contrib/floating_ip_dns.py @@ -0,0 +1,227 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Andrew Bogott for the Wikimedia Foundation +# +# 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 urllib + +import webob + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import exception +from nova import log as logging +from nova import network + + +LOG = logging.getLogger('nova.api.openstack.compute.contrib.floating_ip_dns') + + +def make_dns_entry(elem): + elem.set('id') + elem.set('ip') + elem.set('type') + elem.set('zone') + elem.set('name') + + +def make_zone_entry(elem): + elem.set('zone') + + +class FloatingIPDNSTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('dns_entry', + selector='dns_entry') + make_dns_entry(root) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPDNSsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('dns_entries') + elem = xmlutil.SubTemplateElement(root, 'dns_entry', + selector='dns_entries') + make_dns_entry(elem) + return xmlutil.MasterTemplate(root, 1) + + +class ZonesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('zones') + elem = xmlutil.SubTemplateElement(root, 'zone', + selector='zones') + make_zone_entry(elem) + return xmlutil.MasterTemplate(root, 1) + + +def _translate_dns_entry_view(dns_entry): + result = {} + result['ip'] = dns_entry.get('ip') + result['id'] = dns_entry.get('id') + result['type'] = dns_entry.get('type') + result['zone'] = dns_entry.get('zone') + result['name'] = dns_entry.get('name') + return {'dns_entry': result} + + +def _translate_dns_entries_view(dns_entries): + return {'dns_entries': [_translate_dns_entry_view(entry)['dns_entry'] + for entry in dns_entries]} + + +def _translate_zone_entries_view(zonelist): + return {'zones': [{'zone': zone} for zone in zonelist]} + + +def _unquote_zone(zone): + """Unquoting function for receiving a zone name in a URL. + + Zone names tend to have .'s in them. Urllib doesn't quote dots, + but Routes tends to choke on them, so we need an extra level of + by-hand quoting here. + """ + return urllib.unquote(zone).replace('%2E', '.') + + +def _create_dns_entry(ip, name, zone): + return {'ip': ip, 'name': name, 'zone': zone} + + +class FloatingIPDNSController(object): + """DNS Entry controller for OpenStack API""" + + def __init__(self): + self.network_api = network.API() + super(FloatingIPDNSController, self).__init__() + + @wsgi.serializers(xml=FloatingIPDNSsTemplate) + def show(self, req, id): + """Return a list of dns entries. If ip is specified, query for + names. if name is specified, query for ips. + Quoted domain (aka 'zone') specified as id.""" + context = req.environ['nova.context'] + params = req.GET + floating_ip = params['ip'] if 'ip' in params else "" + name = params['name'] if 'name' in params else "" + zone = _unquote_zone(id) + + if floating_ip: + entries = self.network_api.get_dns_entries_by_address(context, + floating_ip, + zone) + entrylist = [_create_dns_entry(floating_ip, entry, zone) + for entry in entries] + elif name: + entries = self.network_api.get_dns_entries_by_name(context, + name, zone) + entrylist = [_create_dns_entry(entry, name, zone) + for entry in entries] + else: + entrylist = [] + + return _translate_dns_entries_view(entrylist) + + @wsgi.serializers(xml=ZonesTemplate) + def index(self, req): + """Return a list of available DNS zones.""" + + context = req.environ['nova.context'] + zones = self.network_api.get_dns_zones(context) + + return _translate_zone_entries_view(zones) + + @wsgi.serializers(xml=FloatingIPDNSTemplate) + def create(self, req, body): + """Add dns entry for name and address""" + context = req.environ['nova.context'] + + try: + entry = body['dns_entry'] + address = entry['ip'] + name = entry['name'] + dns_type = entry['dns_type'] + zone = entry['zone'] + except (TypeError, KeyError): + raise webob.exc.HTTPUnprocessableEntity() + + try: + self.network_api.add_dns_entry(context, address, name, + dns_type, zone) + except exception.FloatingIpDNSExists: + return webob.Response(status_int=409) + + return _translate_dns_entry_view({'ip': address, + 'name': name, + 'type': dns_type, + 'zone': zone}) + + def update(self, req, id, body): + """Modify a dns entry.""" + context = req.environ['nova.context'] + zone = _unquote_zone(id) + + try: + entry = body['dns_entry'] + name = entry['name'] + new_ip = entry['ip'] + except (TypeError, KeyError): + raise webob.exc.HTTPUnprocessableEntity() + + try: + self.network_api.modify_dns_entry(context, name, + new_ip, zone) + except exception.NotFound: + return webob.Response(status_int=404) + + return _translate_dns_entry_view({'ip': new_ip, + 'name': name, + 'zone': zone}) + + def delete(self, req, id): + """Delete the entry identified by req and id. """ + context = req.environ['nova.context'] + params = req.GET + name = params['name'] if 'name' in params else "" + zone = _unquote_zone(id) + + try: + self.network_api.delete_dns_entry(context, name, zone) + except exception.NotFound: + return webob.Response(status_int=404) + + return webob.Response(status_int=200) + + +class Floating_ip_dns(extensions.ExtensionDescriptor): + """Floating IP DNS support""" + + name = "Floating_ip_dns" + alias = "os-floating-ip-dns" + namespace = "http://docs.openstack.org/ext/floating_ip_dns/api/v1.1" + updated = "2011-12-23:00:00+00:00" + + def __init__(self, ext_mgr): + self.network_api = network.API() + super(Floating_ip_dns, self).__init__(ext_mgr) + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension('os-floating-ip-dns', + FloatingIPDNSController()) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/floating_ip_pools.py b/nova/api/openstack/compute/contrib/floating_ip_pools.py new file mode 100644 index 000000000..01b9a3645 --- /dev/null +++ b/nova/api/openstack/compute/contrib/floating_ip_pools.py @@ -0,0 +1,104 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# +# 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 wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import log as logging +from nova import network + + +LOG = logging.getLogger('nova.api.openstack.compute.contrib.floating_ip_pools') + + +def _translate_floating_ip_view(pool): + return { + 'name': pool['name'], + } + + +def _translate_floating_ip_pools_view(pools): + return { + 'floating_ip_pools': [_translate_floating_ip_view(pool) + for pool in pools] + } + + +class FloatingIPPoolsController(object): + """The Floating IP Pool API controller for the OpenStack API.""" + + def __init__(self): + self.network_api = network.API() + super(FloatingIPPoolsController, self).__init__() + + def index(self, req): + """Return a list of pools.""" + context = req.environ['nova.context'] + pools = self.network_api.get_floating_ip_pools(context) + return _translate_floating_ip_pools_view(pools) + + +def make_float_ip(elem): + elem.set('name') + + +class FloatingIPPoolTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('floating_ip_pool', + selector='floating_ip_pool') + make_float_ip(root) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPPoolsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('floating_ip_pools') + elem = xmlutil.SubTemplateElement(root, 'floating_ip_pool', + selector='floating_ip_pools') + make_float_ip(elem) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPPoolsSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return FloatingIPPoolsTemplate() + + +class Floating_ip_pools(extensions.ExtensionDescriptor): + """Floating IPs support""" + + name = "Floating_ip_pools" + alias = "os-floating-ip-pools" + namespace = \ + "http://docs.openstack.org/compute/ext/floating_ip_pools/api/v1.1" + updated = "2012-01-04T00:00:00+00:00" + + def get_resources(self): + resources = [] + + body_serializers = { + 'application/xml': FloatingIPPoolsSerializer(), + } + + serializer = wsgi.ResponseSerializer(body_serializers) + + res = extensions.ResourceExtension('os-floating-ip-pools', + FloatingIPPoolsController(), + serializer=serializer, + member_actions={}) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/floating_ips.py b/nova/api/openstack/compute/contrib/floating_ips.py new file mode 100644 index 000000000..2400f6c83 --- /dev/null +++ b/nova/api/openstack/compute/contrib/floating_ips.py @@ -0,0 +1,237 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 Grid Dynamics +# Copyright 2011 Eldar Nugaev, Kirill Shileev, Ilya Alekseyev +# +# 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 + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import compute +from nova import exception +from nova import log as logging +from nova import network +from nova import rpc + + +LOG = logging.getLogger('nova.api.openstack.compute.contrib.floating_ips') + + +def make_float_ip(elem): + elem.set('id') + elem.set('ip') + elem.set('pool') + elem.set('fixed_ip') + elem.set('instance_id') + + +class FloatingIPTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('floating_ip', + selector='floating_ip') + make_float_ip(root) + return xmlutil.MasterTemplate(root, 1) + + +class FloatingIPsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('floating_ips') + elem = xmlutil.SubTemplateElement(root, 'floating_ip', + selector='floating_ips') + make_float_ip(elem) + return xmlutil.MasterTemplate(root, 1) + + +def _translate_floating_ip_view(floating_ip): + result = { + 'id': floating_ip['id'], + 'ip': floating_ip['address'], + 'pool': floating_ip['pool'], + } + try: + result['fixed_ip'] = floating_ip['fixed_ip']['address'] + except (TypeError, KeyError): + result['fixed_ip'] = None + try: + result['instance_id'] = floating_ip['fixed_ip']['instance']['uuid'] + except (TypeError, KeyError): + result['instance_id'] = None + return {'floating_ip': result} + + +def _translate_floating_ips_view(floating_ips): + return {'floating_ips': [_translate_floating_ip_view(ip)['floating_ip'] + for ip in floating_ips]} + + +class FloatingIPController(object): + """The Floating IPs API controller for the OpenStack API.""" + + def __init__(self): + self.network_api = network.API() + super(FloatingIPController, self).__init__() + + @wsgi.serializers(xml=FloatingIPTemplate) + def show(self, req, id): + """Return data about the given floating ip.""" + context = req.environ['nova.context'] + + try: + floating_ip = self.network_api.get_floating_ip(context, id) + except exception.NotFound: + raise webob.exc.HTTPNotFound() + + return _translate_floating_ip_view(floating_ip) + + @wsgi.serializers(xml=FloatingIPsTemplate) + def index(self, req): + """Return a list of floating ips allocated to a project.""" + context = req.environ['nova.context'] + + floating_ips = self.network_api.get_floating_ips_by_project(context) + + return _translate_floating_ips_view(floating_ips) + + @wsgi.serializers(xml=FloatingIPTemplate) + def create(self, req, body=None): + context = req.environ['nova.context'] + + pool = None + if body and 'pool' in body: + pool = body['pool'] + try: + address = self.network_api.allocate_floating_ip(context, pool) + ip = self.network_api.get_floating_ip_by_address(context, address) + except rpc.RemoteError as ex: + # NOTE(tr3buchet) - why does this block exist? + if ex.exc_type == 'NoMoreFloatingIps': + if pool: + msg = _("No more floating ips in pool %s.") % pool + else: + msg = _("No more floating ips available.") + raise webob.exc.HTTPBadRequest(explanation=msg) + else: + raise + + return _translate_floating_ip_view(ip) + + def delete(self, req, id): + context = req.environ['nova.context'] + floating_ip = self.network_api.get_floating_ip(context, id) + + if floating_ip.get('fixed_ip'): + self.network_api.disassociate_floating_ip(context, + floating_ip['address']) + + self.network_api.release_floating_ip(context, + address=floating_ip['address']) + return webob.Response(status_int=202) + + def _get_ip_by_id(self, context, value): + """Checks that value is id and then returns its address.""" + return self.network_api.get_floating_ip(context, value)['address'] + + +class FloatingIPSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return FloatingIPsTemplate() + + def default(self): + return FloatingIPTemplate() + + +class Floating_ips(extensions.ExtensionDescriptor): + """Floating IPs support""" + + name = "Floating_ips" + alias = "os-floating-ips" + namespace = "http://docs.openstack.org/compute/ext/floating_ips/api/v1.1" + updated = "2011-06-16T00:00:00+00:00" + + def __init__(self, ext_mgr): + self.compute_api = compute.API() + self.network_api = network.API() + super(Floating_ips, self).__init__(ext_mgr) + + def _add_floating_ip(self, input_dict, req, instance_id): + """Associate floating_ip to an instance.""" + context = req.environ['nova.context'] + + try: + address = input_dict['addFloatingIp']['address'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Address not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + instance = self.compute_api.get(context, instance_id) + self.compute_api.associate_floating_ip(context, instance, + address) + except exception.ApiError, e: + raise webob.exc.HTTPBadRequest(explanation=e.message) + except exception.NotAuthorized, e: + raise webob.exc.HTTPUnauthorized() + + return webob.Response(status_int=202) + + def _remove_floating_ip(self, input_dict, req, instance_id): + """Dissociate floating_ip from an instance.""" + context = req.environ['nova.context'] + + try: + address = input_dict['removeFloatingIp']['address'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Address not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + floating_ip = self.network_api.get_floating_ip_by_address(context, + address) + if floating_ip.get('fixed_ip'): + try: + self.network_api.disassociate_floating_ip(context, address) + except exception.NotAuthorized, e: + raise webob.exc.HTTPUnauthorized() + + return webob.Response(status_int=202) + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension('os-floating-ips', + FloatingIPController(), + member_actions={}) + resources.append(res) + + return resources + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + extensions.ActionExtension("servers", "addFloatingIp", + self._add_floating_ip), + extensions.ActionExtension("servers", "removeFloatingIp", + self._remove_floating_ip), + ] + + return actions diff --git a/nova/api/openstack/compute/contrib/hosts.py b/nova/api/openstack/compute/contrib/hosts.py new file mode 100644 index 000000000..66dd64def --- /dev/null +++ b/nova/api/openstack/compute/contrib/hosts.py @@ -0,0 +1,187 @@ +# Copyright (c) 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. + +"""The hosts admin extension.""" + +import webob.exc +from xml.dom import minidom +from xml.parsers import expat + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova.scheduler import api as scheduler_api + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.hosts") +FLAGS = flags.FLAGS + + +class HostIndexTemplate(xmlutil.TemplateBuilder): + def construct(self): + def shimmer(obj, do_raise=False): + # A bare list is passed in; we need to wrap it in a dict + return dict(hosts=obj) + + root = xmlutil.TemplateElement('hosts', selector=shimmer) + elem = xmlutil.SubTemplateElement(root, 'host', selector='hosts') + elem.set('host_name') + elem.set('service') + + return xmlutil.MasterTemplate(root, 1) + + +class HostUpdateTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('host') + root.set('host') + root.set('status') + + return xmlutil.MasterTemplate(root, 1) + + +class HostActionTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('host') + root.set('host') + root.set('power_action') + + return xmlutil.MasterTemplate(root, 1) + + +class HostDeserializer(wsgi.XMLDeserializer): + def default(self, string): + try: + node = minidom.parseString(string) + except expat.ExpatError: + msg = _("cannot understand XML") + raise exception.MalformedRequestBody(reason=msg) + + updates = {} + for child in node.childNodes[0].childNodes: + updates[child.tagName] = self.extract_text(child) + + return dict(body=updates) + + +def _list_hosts(req, service=None): + """Returns a summary list of hosts, optionally filtering + by service type. + """ + context = req.environ['nova.context'] + hosts = scheduler_api.get_host_list(context) + if service: + hosts = [host for host in hosts + if host["service"] == service] + return hosts + + +def check_host(fn): + """Makes sure that the host exists.""" + def wrapped(self, req, id, service=None, *args, **kwargs): + listed_hosts = _list_hosts(req, service) + hosts = [h["host_name"] for h in listed_hosts] + if id in hosts: + return fn(self, req, id, *args, **kwargs) + else: + raise exception.HostNotFound(host=id) + return wrapped + + +class HostController(object): + """The Hosts API controller for the OpenStack API.""" + def __init__(self): + self.compute_api = compute.API() + super(HostController, self).__init__() + + @wsgi.serializers(xml=HostIndexTemplate) + def index(self, req): + return {'hosts': _list_hosts(req)} + + @wsgi.serializers(xml=HostUpdateTemplate) + @wsgi.deserializers(xml=HostDeserializer) + @check_host + def update(self, req, id, body): + for raw_key, raw_val in body.iteritems(): + key = raw_key.lower().strip() + val = raw_val.lower().strip() + # NOTE: (dabo) Right now only 'status' can be set, but other + # settings may follow. + if key == "status": + if val[:6] in ("enable", "disabl"): + return self._set_enabled_status(req, id, + enabled=(val.startswith("enable"))) + else: + explanation = _("Invalid status: '%s'") % raw_val + raise webob.exc.HTTPBadRequest(explanation=explanation) + else: + explanation = _("Invalid update setting: '%s'") % raw_key + raise webob.exc.HTTPBadRequest(explanation=explanation) + + def _set_enabled_status(self, req, host, enabled): + """Sets the specified host's ability to accept new instances.""" + context = req.environ['nova.context'] + state = "enabled" if enabled else "disabled" + LOG.audit(_("Setting host %(host)s to %(state)s.") % locals()) + result = self.compute_api.set_host_enabled(context, host=host, + enabled=enabled) + if result not in ("enabled", "disabled"): + # An error message was returned + raise webob.exc.HTTPBadRequest(explanation=result) + return {"host": host, "status": result} + + def _host_power_action(self, req, host, action): + """Reboots, shuts down or powers up the host.""" + context = req.environ['nova.context'] + try: + result = self.compute_api.host_power_action(context, host=host, + action=action) + except NotImplementedError as e: + raise webob.exc.HTTPBadRequest(explanation=e.msg) + return {"host": host, "power_action": result} + + @wsgi.serializers(xml=HostActionTemplate) + def startup(self, req, id): + return self._host_power_action(req, host=id, action="startup") + + @wsgi.serializers(xml=HostActionTemplate) + def shutdown(self, req, id): + return self._host_power_action(req, host=id, action="shutdown") + + @wsgi.serializers(xml=HostActionTemplate) + def reboot(self, req, id): + return self._host_power_action(req, host=id, action="reboot") + + +class Hosts(extensions.ExtensionDescriptor): + """Admin-only host administration""" + + name = "Hosts" + alias = "os-hosts" + namespace = "http://docs.openstack.org/compute/ext/hosts/api/v1.1" + updated = "2011-06-29T00:00:00+00:00" + admin_only = True + + def get_resources(self): + resources = [extensions.ResourceExtension('os-hosts', + HostController(), + collection_actions={'update': 'PUT'}, + member_actions={"startup": "GET", "shutdown": "GET", + "reboot": "GET"})] + return resources diff --git a/nova/api/openstack/compute/contrib/keypairs.py b/nova/api/openstack/compute/contrib/keypairs.py new file mode 100644 index 000000000..5ac205df5 --- /dev/null +++ b/nova/api/openstack/compute/contrib/keypairs.py @@ -0,0 +1,163 @@ +# 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. + +""" Keypair management extension""" + +import os +import shutil +import tempfile + +import webob +from webob import exc + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import crypto +from nova import db +from nova import exception + + +class KeypairTemplate(xmlutil.TemplateBuilder): + def construct(self): + return xmlutil.MasterTemplate(xmlutil.make_flat_dict('keypair'), 1) + + +class KeypairsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('keypairs') + elem = xmlutil.make_flat_dict('keypair', selector='keypairs', + subselector='keypair') + root.append(elem) + + return xmlutil.MasterTemplate(root, 1) + + +class KeypairController(object): + """ Keypair API controller for the Openstack API """ + + # TODO(ja): both this file and nova.api.ec2.cloud.py have similar logic. + # move the common keypair logic to nova.compute.API? + + def _gen_key(self): + """ + Generate a key + """ + private_key, public_key, fingerprint = crypto.generate_key_pair() + return {'private_key': private_key, + 'public_key': public_key, + 'fingerprint': fingerprint} + + @wsgi.serializers(xml=KeypairTemplate) + def create(self, req, body): + """ + Create or import keypair. + + Sending name will generate a key and return private_key + and fingerprint. + + You can send a public_key to add an existing ssh key + + params: keypair object with: + name (required) - string + public_key (optional) - string + """ + + context = req.environ['nova.context'] + params = body['keypair'] + name = params['name'] + + # NOTE(ja): generation is slow, so shortcut invalid name exception + try: + db.key_pair_get(context, context.user_id, name) + raise exception.KeyPairExists(key_name=name) + except exception.NotFound: + pass + + keypair = {'user_id': context.user_id, + 'name': name} + + # import if public_key is sent + if 'public_key' in params: + tmpdir = tempfile.mkdtemp() + fn = os.path.join(tmpdir, 'import.pub') + with open(fn, 'w') as pub: + pub.write(params['public_key']) + fingerprint = crypto.generate_fingerprint(fn) + shutil.rmtree(tmpdir) + keypair['public_key'] = params['public_key'] + keypair['fingerprint'] = fingerprint + else: + generated_key = self._gen_key() + keypair['private_key'] = generated_key['private_key'] + keypair['public_key'] = generated_key['public_key'] + keypair['fingerprint'] = generated_key['fingerprint'] + + db.key_pair_create(context, keypair) + return {'keypair': keypair} + + def delete(self, req, id): + """ + Delete a keypair with a given name + """ + context = req.environ['nova.context'] + db.key_pair_destroy(context, context.user_id, id) + return webob.Response(status_int=202) + + @wsgi.serializers(xml=KeypairsTemplate) + def index(self, req): + """ + List of keypairs for a user + """ + context = req.environ['nova.context'] + key_pairs = db.key_pair_get_all_by_user(context, context.user_id) + rval = [] + for key_pair in key_pairs: + rval.append({'keypair': { + 'name': key_pair['name'], + 'public_key': key_pair['public_key'], + 'fingerprint': key_pair['fingerprint'], + }}) + + return {'keypairs': rval} + + +class KeypairsSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return KeypairsTemplate() + + def default(self): + return KeypairTemplate() + + +class Keypairs(extensions.ExtensionDescriptor): + """Keypair Support""" + + name = "Keypairs" + alias = "os-keypairs" + namespace = "http://docs.openstack.org/compute/ext/keypairs/api/v1.1" + updated = "2011-08-08T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'os-keypairs', + KeypairController()) + + resources.append(res) + return resources diff --git a/nova/api/openstack/compute/contrib/multinic.py b/nova/api/openstack/compute/contrib/multinic.py new file mode 100644 index 000000000..18b95e63d --- /dev/null +++ b/nova/api/openstack/compute/contrib/multinic.py @@ -0,0 +1,106 @@ +# 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. + +"""The multinic extension.""" + +import webob +from webob import exc + +from nova.api.openstack import extensions +from nova import compute +from nova import exception +from nova import log as logging + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.multinic") + + +# Note: The class name is as it has to be for this to be loaded as an +# extension--only first character capitalized. +class Multinic(extensions.ExtensionDescriptor): + """Multiple network support""" + + name = "Multinic" + alias = "NMN" + namespace = "http://docs.openstack.org/compute/ext/multinic/api/v1.1" + updated = "2011-06-09T00:00:00+00:00" + + def __init__(self, ext_mgr): + """Initialize the extension. + + Gets a compute.API object so we can call the back-end + add_fixed_ip() and remove_fixed_ip() methods. + """ + + super(Multinic, self).__init__(ext_mgr) + self.compute_api = compute.API() + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + + actions = [] + + # Add the add_fixed_ip action + act = extensions.ActionExtension("servers", "addFixedIp", + self._add_fixed_ip) + actions.append(act) + + # Add the remove_fixed_ip action + act = extensions.ActionExtension("servers", "removeFixedIp", + self._remove_fixed_ip) + actions.append(act) + + return actions + + def _get_instance(self, context, instance_id): + try: + return self.compute_api.get(context, instance_id) + except exception.InstanceNotFound: + msg = _("Server not found") + raise exc.HTTPNotFound(msg) + + def _add_fixed_ip(self, input_dict, req, id): + """Adds an IP on a given network to an instance.""" + + # Validate the input entity + if 'networkId' not in input_dict['addFixedIp']: + msg = _("Missing 'networkId' argument for addFixedIp") + raise exc.HTTPUnprocessableEntity(explanation=msg) + + context = req.environ['nova.context'] + instance = self._get_instance(context, id) + network_id = input_dict['addFixedIp']['networkId'] + self.compute_api.add_fixed_ip(context, instance, network_id) + return webob.Response(status_int=202) + + def _remove_fixed_ip(self, input_dict, req, id): + """Removes an IP from an instance.""" + + # Validate the input entity + if 'address' not in input_dict['removeFixedIp']: + msg = _("Missing 'address' argument for removeFixedIp") + raise exc.HTTPUnprocessableEntity(explanation=msg) + + context = req.environ['nova.context'] + instance = self._get_instance(context, id) + address = input_dict['removeFixedIp']['address'] + + try: + self.compute_api.remove_fixed_ip(context, instance, address) + except exceptions.FixedIpNotFoundForSpecificInstance: + LOG.exception(_("Unable to find address %r") % address) + raise exc.HTTPBadRequest() + + return webob.Response(status_int=202) diff --git a/nova/api/openstack/compute/contrib/networks.py b/nova/api/openstack/compute/contrib/networks.py new file mode 100644 index 000000000..f2381a19d --- /dev/null +++ b/nova/api/openstack/compute/contrib/networks.py @@ -0,0 +1,117 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Grid Dynamics +# 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.api.openstack import extensions +from nova import exception +from nova import flags +from nova import log as logging +import nova.network.api + + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.api.openstack.compute.contrib.networks') + + +def network_dict(network): + if network: + fields = ('bridge', 'vpn_public_port', 'dhcp_start', + 'bridge_interface', 'updated_at', 'id', 'cidr_v6', + 'deleted_at', 'gateway', 'label', 'project_id', + 'vpn_private_address', 'deleted', 'vlan', 'broadcast', + 'netmask', 'injected', 'cidr', 'vpn_public_address', + 'multi_host', 'dns1', 'host', 'gateway_v6', 'netmask_v6', + 'created_at') + return dict((field, network[field]) for field in fields) + else: + return {} + + +class NetworkController(object): + + def __init__(self, network_api=None): + self.network_api = network_api or nova.network.api.API() + + def action(self, req, id, body): + _actions = { + 'disassociate': self._disassociate, + } + + for action, data in body.iteritems(): + try: + return _actions[action](req, id, body) + except KeyError: + msg = _("Network does not have %s action") % action + raise exc.HTTPBadRequest(explanation=msg) + + raise exc.HTTPBadRequest(explanation=_("Invalid request body")) + + def _disassociate(self, request, network_id, body): + context = request.environ['nova.context'] + LOG.debug(_("Disassociating network with id %s" % network_id)) + try: + self.network_api.disassociate(context, network_id) + except exception.NetworkNotFound: + raise exc.HTTPNotFound(_("Network not found")) + return exc.HTTPAccepted() + + def index(self, req): + context = req.environ['nova.context'] + networks = self.network_api.get_all(context) + result = [network_dict(net_ref) for net_ref in networks] + return {'networks': result} + + def show(self, req, id): + context = req.environ['nova.context'] + LOG.debug(_("Showing network with id %s") % id) + try: + network = self.network_api.get(context, id) + except exception.NetworkNotFound: + raise exc.HTTPNotFound(_("Network not found")) + return {'network': network_dict(network)} + + def delete(self, req, id): + context = req.environ['nova.context'] + LOG.info(_("Deleting network with id %s") % id) + try: + self.network_api.delete(context, id) + except exception.NetworkNotFound: + raise exc.HTTPNotFound(_("Network not found")) + return exc.HTTPAccepted() + + def create(self, req, id, body=None): + raise exc.HTTPNotImplemented() + + +class Networks(extensions.ExtensionDescriptor): + """Admin-only Network Management Extension""" + + name = "Networks" + alias = "os-networks" + namespace = "http://docs.openstack.org/compute/ext/networks/api/v1.1" + updated = "2011-12-23 00:00:00" + admin_only = True + + def get_resources(self): + member_actions = {'action': 'POST'} + res = extensions.ResourceExtension('os-networks', + NetworkController(), + member_actions=member_actions) + return [res] diff --git a/nova/api/openstack/compute/contrib/quotas.py b/nova/api/openstack/compute/contrib/quotas.py new file mode 100644 index 000000000..5e4a20568 --- /dev/null +++ b/nova/api/openstack/compute/contrib/quotas.py @@ -0,0 +1,102 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob + +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.api.openstack import extensions +from nova import db +from nova import exception +from nova import quota + + +quota_resources = ['metadata_items', 'injected_file_content_bytes', + 'volumes', 'gigabytes', 'ram', 'floating_ips', 'instances', + 'injected_files', 'cores'] + + +class QuotaTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('quota_set', selector='quota_set') + root.set('id') + + for resource in quota_resources: + elem = xmlutil.SubTemplateElement(root, resource) + elem.text = resource + + return xmlutil.MasterTemplate(root, 1) + + +class QuotaSetsController(object): + + def _format_quota_set(self, project_id, quota_set): + """Convert the quota object to a result dict""" + + result = dict(id=str(project_id)) + + for resource in quota_resources: + result[resource] = quota_set[resource] + + return dict(quota_set=result) + + @wsgi.serializers(xml=QuotaTemplate) + def show(self, req, id): + context = req.environ['nova.context'] + try: + db.sqlalchemy.api.authorize_project_context(context, id) + return self._format_quota_set(id, + quota.get_project_quotas(context, id)) + except exception.NotAuthorized: + raise webob.exc.HTTPForbidden() + + @wsgi.serializers(xml=QuotaTemplate) + def update(self, req, id, body): + context = req.environ['nova.context'] + project_id = id + for key in body['quota_set'].keys(): + if key in quota_resources: + value = int(body['quota_set'][key]) + try: + db.quota_update(context, project_id, key, value) + except exception.ProjectQuotaNotFound: + db.quota_create(context, project_id, key, value) + except exception.AdminRequired: + raise webob.exc.HTTPForbidden() + return {'quota_set': quota.get_project_quotas(context, project_id)} + + def defaults(self, req, id): + return self._format_quota_set(id, quota._get_default_quotas()) + + +class Quotas(extensions.ExtensionDescriptor): + """Quotas management support""" + + name = "Quotas" + alias = "os-quota-sets" + namespace = "http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1" + updated = "2011-08-08T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension('os-quota-sets', + QuotaSetsController(), + member_actions={'defaults': 'GET'}) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/rescue.py b/nova/api/openstack/compute/contrib/rescue.py new file mode 100644 index 000000000..0ea77cc6a --- /dev/null +++ b/nova/api/openstack/compute/contrib/rescue.py @@ -0,0 +1,80 @@ +# Copyright 2011 Openstack, LLC. +# +# 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 rescue mode extension.""" + +import webob +from webob import exc + +from nova.api.openstack import extensions as exts +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils + + +FLAGS = flags.FLAGS +LOG = logging.getLogger("nova.api.openstack.compute.contrib.rescue") + + +class Rescue(exts.ExtensionDescriptor): + """Instance rescue mode""" + + name = "Rescue" + alias = "os-rescue" + namespace = "http://docs.openstack.org/compute/ext/rescue/api/v1.1" + updated = "2011-08-18T00:00:00+00:00" + + def __init__(self, ext_mgr): + super(Rescue, self).__init__(ext_mgr) + self.compute_api = compute.API() + + def _get_instance(self, context, instance_id): + try: + return self.compute_api.get(context, instance_id) + except exception.InstanceNotFound: + msg = _("Server not found") + raise exc.HTTPNotFound(msg) + + @exts.wrap_errors + def _rescue(self, input_dict, req, instance_id): + """Rescue an instance.""" + context = req.environ["nova.context"] + + if input_dict['rescue'] and 'adminPass' in input_dict['rescue']: + password = input_dict['rescue']['adminPass'] + else: + password = utils.generate_password(FLAGS.password_length) + + instance = self._get_instance(context, instance_id) + self.compute_api.rescue(context, instance, rescue_password=password) + return {'adminPass': password} + + @exts.wrap_errors + def _unrescue(self, input_dict, req, instance_id): + """Unrescue an instance.""" + context = req.environ["nova.context"] + instance = self._get_instance(context, instance_id) + self.compute_api.unrescue(context, instance) + return webob.Response(status_int=202) + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + exts.ActionExtension("servers", "rescue", self._rescue), + exts.ActionExtension("servers", "unrescue", self._unrescue), + ] + + return actions diff --git a/nova/api/openstack/compute/contrib/security_groups.py b/nova/api/openstack/compute/contrib/security_groups.py new file mode 100644 index 000000000..f0d3dfe16 --- /dev/null +++ b/nova/api/openstack/compute/contrib/security_groups.py @@ -0,0 +1,592 @@ +# 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. + +"""The security groups extension.""" + +import urllib +from xml.dom import minidom + +from webob import exc +import webob + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import db +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.security_groups") +FLAGS = flags.FLAGS + + +def make_rule(elem): + elem.set('id') + elem.set('parent_group_id') + + proto = xmlutil.SubTemplateElement(elem, 'ip_protocol') + proto.text = 'ip_protocol' + + from_port = xmlutil.SubTemplateElement(elem, 'from_port') + from_port.text = 'from_port' + + to_port = xmlutil.SubTemplateElement(elem, 'to_port') + to_port.text = 'to_port' + + group = xmlutil.SubTemplateElement(elem, 'group', selector='group') + name = xmlutil.SubTemplateElement(group, 'name') + name.text = 'name' + tenant_id = xmlutil.SubTemplateElement(group, 'tenant_id') + tenant_id.text = 'tenant_id' + + ip_range = xmlutil.SubTemplateElement(elem, 'ip_range', + selector='ip_range') + cidr = xmlutil.SubTemplateElement(ip_range, 'cidr') + cidr.text = 'cidr' + + +def make_sg(elem): + elem.set('id') + elem.set('tenant_id') + elem.set('name') + + desc = xmlutil.SubTemplateElement(elem, 'description') + desc.text = 'description' + + rules = xmlutil.SubTemplateElement(elem, 'rules') + rule = xmlutil.SubTemplateElement(rules, 'rule', selector='rules') + make_rule(rule) + + +sg_nsmap = {None: wsgi.XMLNS_V11} + + +class SecurityGroupRuleTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('security_group_rule', + selector='security_group_rule') + make_rule(root) + return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) + + +class SecurityGroupTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('security_group', + selector='security_group') + make_sg(root) + return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) + + +class SecurityGroupsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('security_groups') + elem = xmlutil.SubTemplateElement(root, 'security_group', + selector='security_groups') + make_sg(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) + + +class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer): + """ + Deserializer to handle xml-formatted security group requests. + """ + def default(self, string): + """Deserialize an xml-formatted security group create request""" + dom = minidom.parseString(string) + security_group = {} + sg_node = self.find_first_child_named(dom, + 'security_group') + if sg_node is not None: + if sg_node.hasAttribute('name'): + security_group['name'] = sg_node.getAttribute('name') + desc_node = self.find_first_child_named(sg_node, + "description") + if desc_node: + security_group['description'] = self.extract_text(desc_node) + return {'body': {'security_group': security_group}} + + +class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer): + """ + Deserializer to handle xml-formatted security group requests. + """ + + def default(self, string): + """Deserialize an xml-formatted security group create request""" + dom = minidom.parseString(string) + security_group_rule = self._extract_security_group_rule(dom) + return {'body': {'security_group_rule': security_group_rule}} + + def _extract_security_group_rule(self, node): + """Marshal the security group rule attribute of a parsed request""" + sg_rule = {} + sg_rule_node = self.find_first_child_named(node, + 'security_group_rule') + if sg_rule_node is not None: + ip_protocol_node = self.find_first_child_named(sg_rule_node, + "ip_protocol") + if ip_protocol_node is not None: + sg_rule['ip_protocol'] = self.extract_text(ip_protocol_node) + + from_port_node = self.find_first_child_named(sg_rule_node, + "from_port") + if from_port_node is not None: + sg_rule['from_port'] = self.extract_text(from_port_node) + + to_port_node = self.find_first_child_named(sg_rule_node, "to_port") + if to_port_node is not None: + sg_rule['to_port'] = self.extract_text(to_port_node) + + parent_group_id_node = self.find_first_child_named(sg_rule_node, + "parent_group_id") + if parent_group_id_node is not None: + sg_rule['parent_group_id'] = self.extract_text( + parent_group_id_node) + + group_id_node = self.find_first_child_named(sg_rule_node, + "group_id") + if group_id_node is not None: + sg_rule['group_id'] = self.extract_text(group_id_node) + + cidr_node = self.find_first_child_named(sg_rule_node, "cidr") + if cidr_node is not None: + sg_rule['cidr'] = self.extract_text(cidr_node) + + return sg_rule + + +class SecurityGroupController(object): + """The Security group API controller for the OpenStack API.""" + + def __init__(self): + self.compute_api = compute.API() + super(SecurityGroupController, self).__init__() + + def _format_security_group_rule(self, context, rule): + sg_rule = {} + sg_rule['id'] = rule.id + sg_rule['parent_group_id'] = rule.parent_group_id + sg_rule['ip_protocol'] = rule.protocol + sg_rule['from_port'] = rule.from_port + sg_rule['to_port'] = rule.to_port + sg_rule['group'] = {} + sg_rule['ip_range'] = {} + if rule.group_id: + source_group = db.security_group_get(context, rule.group_id) + sg_rule['group'] = {'name': source_group.name, + 'tenant_id': source_group.project_id} + else: + sg_rule['ip_range'] = {'cidr': rule.cidr} + return sg_rule + + def _format_security_group(self, context, group): + security_group = {} + security_group['id'] = group.id + security_group['description'] = group.description + security_group['name'] = group.name + security_group['tenant_id'] = group.project_id + security_group['rules'] = [] + for rule in group.rules: + security_group['rules'] += [self._format_security_group_rule( + context, rule)] + return security_group + + def _get_security_group(self, context, id): + try: + id = int(id) + security_group = db.security_group_get(context, id) + except ValueError: + msg = _("Security group id should be integer") + raise exc.HTTPBadRequest(explanation=msg) + except exception.NotFound as exp: + raise exc.HTTPNotFound(explanation=unicode(exp)) + return security_group + + @wsgi.serializers(xml=SecurityGroupTemplate) + def show(self, req, id): + """Return data about the given security group.""" + context = req.environ['nova.context'] + security_group = self._get_security_group(context, id) + return {'security_group': self._format_security_group(context, + security_group)} + + def delete(self, req, id): + """Delete a security group.""" + context = req.environ['nova.context'] + security_group = self._get_security_group(context, id) + LOG.audit(_("Delete security group %s"), id, context=context) + db.security_group_destroy(context, security_group.id) + + return webob.Response(status_int=202) + + @wsgi.serializers(xml=SecurityGroupsTemplate) + def index(self, req): + """Returns a list of security groups""" + context = req.environ['nova.context'] + + self.compute_api.ensure_default_security_group(context) + groups = db.security_group_get_by_project(context, + context.project_id) + limited_list = common.limited(groups, req) + result = [self._format_security_group(context, group) + for group in limited_list] + + return {'security_groups': + list(sorted(result, + key=lambda k: (k['tenant_id'], k['name'])))} + + @wsgi.serializers(xml=SecurityGroupTemplate) + @wsgi.deserializers(xml=SecurityGroupXMLDeserializer) + def create(self, req, body): + """Creates a new security group.""" + context = req.environ['nova.context'] + if not body: + raise exc.HTTPUnprocessableEntity() + + security_group = body.get('security_group', None) + + if security_group is None: + raise exc.HTTPUnprocessableEntity() + + group_name = security_group.get('name', None) + group_description = security_group.get('description', None) + + self._validate_security_group_property(group_name, "name") + self._validate_security_group_property(group_description, + "description") + group_name = group_name.strip() + group_description = group_description.strip() + + LOG.audit(_("Create Security Group %s"), group_name, context=context) + self.compute_api.ensure_default_security_group(context) + if db.security_group_exists(context, context.project_id, group_name): + msg = _('Security group %s already exists') % group_name + raise exc.HTTPBadRequest(explanation=msg) + + group = {'user_id': context.user_id, + 'project_id': context.project_id, + 'name': group_name, + 'description': group_description} + group_ref = db.security_group_create(context, group) + + return {'security_group': self._format_security_group(context, + group_ref)} + + def _validate_security_group_property(self, value, typ): + """ typ will be either 'name' or 'description', + depending on the caller + """ + try: + val = value.strip() + except AttributeError: + msg = _("Security group %s is not a string or unicode") % typ + raise exc.HTTPBadRequest(explanation=msg) + if not val: + msg = _("Security group %s cannot be empty.") % typ + raise exc.HTTPBadRequest(explanation=msg) + if len(val) > 255: + msg = _("Security group %s should not be greater " + "than 255 characters.") % typ + raise exc.HTTPBadRequest(explanation=msg) + + +class SecurityGroupRulesController(SecurityGroupController): + + @wsgi.serializers(xml=SecurityGroupRuleTemplate) + @wsgi.deserializers(xml=SecurityGroupRulesXMLDeserializer) + def create(self, req, body): + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + if not 'security_group_rule' in body: + raise exc.HTTPUnprocessableEntity() + + self.compute_api.ensure_default_security_group(context) + + sg_rule = body['security_group_rule'] + parent_group_id = sg_rule.get('parent_group_id', None) + try: + parent_group_id = int(parent_group_id) + security_group = db.security_group_get(context, parent_group_id) + except ValueError: + msg = _("Parent group id is not integer") + raise exc.HTTPBadRequest(explanation=msg) + except exception.NotFound as exp: + msg = _("Security group (%s) not found") % parent_group_id + raise exc.HTTPNotFound(explanation=msg) + + msg = _("Authorize security group ingress %s") + LOG.audit(msg, security_group['name'], context=context) + + try: + values = self._rule_args_to_dict(context, + to_port=sg_rule.get('to_port'), + from_port=sg_rule.get('from_port'), + parent_group_id=sg_rule.get('parent_group_id'), + ip_protocol=sg_rule.get('ip_protocol'), + cidr=sg_rule.get('cidr'), + group_id=sg_rule.get('group_id')) + except Exception as exp: + raise exc.HTTPBadRequest(explanation=unicode(exp)) + + if values is None: + msg = _("Not enough parameters to build a " + "valid rule.") + raise exc.HTTPBadRequest(explanation=msg) + + values['parent_group_id'] = security_group.id + + if self._security_group_rule_exists(security_group, values): + msg = _('This rule already exists in group %s') % parent_group_id + raise exc.HTTPBadRequest(explanation=msg) + + security_group_rule = db.security_group_rule_create(context, values) + + self.compute_api.trigger_security_group_rules_refresh(context, + security_group_id=security_group['id']) + + return {"security_group_rule": self._format_security_group_rule( + context, + security_group_rule)} + + def _security_group_rule_exists(self, security_group, values): + """Indicates whether the specified rule values are already + defined in the given security group. + """ + for rule in security_group.rules: + if 'group_id' in values: + if rule['group_id'] == values['group_id']: + return True + else: + is_duplicate = True + for key in ('cidr', 'from_port', 'to_port', 'protocol'): + if rule[key] != values[key]: + is_duplicate = False + break + if is_duplicate: + return True + return False + + def _rule_args_to_dict(self, context, to_port=None, from_port=None, + parent_group_id=None, ip_protocol=None, + cidr=None, group_id=None): + values = {} + + if group_id is not None: + try: + parent_group_id = int(parent_group_id) + group_id = int(group_id) + except ValueError: + msg = _("Parent or group id is not integer") + raise exception.InvalidInput(reason=msg) + + if parent_group_id == group_id: + msg = _("Parent group id and group id cannot be same") + raise exception.InvalidInput(reason=msg) + + values['group_id'] = group_id + #check if groupId exists + db.security_group_get(context, group_id) + elif cidr: + # If this fails, it throws an exception. This is what we want. + try: + cidr = urllib.unquote(cidr).decode() + except Exception: + raise exception.InvalidCidr(cidr=cidr) + + if not utils.is_valid_cidr(cidr): + # Raise exception for non-valid address + raise exception.InvalidCidr(cidr=cidr) + + values['cidr'] = cidr + else: + values['cidr'] = '0.0.0.0/0' + + if ip_protocol and from_port and to_port: + + ip_protocol = str(ip_protocol) + try: + from_port = int(from_port) + to_port = int(to_port) + except ValueError: + if ip_protocol.upper() == 'ICMP': + raise exception.InvalidInput(reason="Type and" + " Code must be integers for ICMP protocol type") + else: + raise exception.InvalidInput(reason="To and From ports " + "must be integers") + + if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: + raise exception.InvalidIpProtocol(protocol=ip_protocol) + + # Verify that from_port must always be less than + # or equal to to_port + if from_port > to_port: + raise exception.InvalidPortRange(from_port=from_port, + to_port=to_port, msg="Former value cannot" + " be greater than the later") + + # Verify valid TCP, UDP port ranges + if (ip_protocol.upper() in ['TCP', 'UDP'] and + (from_port < 1 or to_port > 65535)): + raise exception.InvalidPortRange(from_port=from_port, + to_port=to_port, msg="Valid TCP ports should" + " be between 1-65535") + + # Verify ICMP type and code + if (ip_protocol.upper() == "ICMP" and + (from_port < -1 or to_port > 255)): + raise exception.InvalidPortRange(from_port=from_port, + to_port=to_port, msg="For ICMP, the" + " type:code must be valid") + + values['protocol'] = ip_protocol + values['from_port'] = from_port + values['to_port'] = to_port + else: + # If cidr based filtering, protocol and ports are mandatory + if 'cidr' in values: + return None + + return values + + def delete(self, req, id): + context = req.environ['nova.context'] + + self.compute_api.ensure_default_security_group(context) + try: + id = int(id) + rule = db.security_group_rule_get(context, id) + except ValueError: + msg = _("Rule id is not integer") + raise exc.HTTPBadRequest(explanation=msg) + except exception.NotFound as exp: + msg = _("Rule (%s) not found") % id + raise exc.HTTPNotFound(explanation=msg) + + group_id = rule.parent_group_id + self.compute_api.ensure_default_security_group(context) + security_group = db.security_group_get(context, group_id) + + msg = _("Revoke security group ingress %s") + LOG.audit(msg, security_group['name'], context=context) + + db.security_group_rule_destroy(context, rule['id']) + self.compute_api.trigger_security_group_rules_refresh(context, + security_group_id=security_group['id']) + + return webob.Response(status_int=202) + + +class Security_groups(extensions.ExtensionDescriptor): + """Security group support""" + + name = "SecurityGroups" + alias = "security_groups" + namespace = "http://docs.openstack.org/compute/ext/securitygroups/api/v1.1" + updated = "2011-07-21T00:00:00+00:00" + + def __init__(self, ext_mgr): + self.compute_api = compute.API() + super(Security_groups, self).__init__(ext_mgr) + + def _addSecurityGroup(self, input_dict, req, instance_id): + context = req.environ['nova.context'] + + try: + body = input_dict['addSecurityGroup'] + group_name = body['name'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Security group not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + if not group_name or group_name.strip() == '': + msg = _("Security group name cannot be empty") + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + instance = self.compute_api.get(context, instance_id) + self.compute_api.add_security_group(context, instance, group_name) + except exception.SecurityGroupNotFound as exp: + raise exc.HTTPNotFound(explanation=unicode(exp)) + except exception.InstanceNotFound as exp: + raise exc.HTTPNotFound(explanation=unicode(exp)) + except exception.Invalid as exp: + raise exc.HTTPBadRequest(explanation=unicode(exp)) + + return webob.Response(status_int=202) + + def _removeSecurityGroup(self, input_dict, req, instance_id): + context = req.environ['nova.context'] + + try: + body = input_dict['removeSecurityGroup'] + group_name = body['name'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Security group not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + if not group_name or group_name.strip() == '': + msg = _("Security group name cannot be empty") + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + instance = self.compute_api.get(context, instance_id) + self.compute_api.remove_security_group(context, instance, + group_name) + except exception.SecurityGroupNotFound as exp: + raise exc.HTTPNotFound(explanation=unicode(exp)) + except exception.InstanceNotFound as exp: + raise exc.HTTPNotFound(explanation=unicode(exp)) + except exception.Invalid as exp: + raise exc.HTTPBadRequest(explanation=unicode(exp)) + + return webob.Response(status_int=202) + + def get_actions(self): + """Return the actions the extensions adds""" + actions = [ + extensions.ActionExtension("servers", "addSecurityGroup", + self._addSecurityGroup), + extensions.ActionExtension("servers", "removeSecurityGroup", + self._removeSecurityGroup) + ] + return actions + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension('os-security-groups', + controller=SecurityGroupController()) + + resources.append(res) + + res = extensions.ResourceExtension('os-security-group-rules', + controller=SecurityGroupRulesController()) + resources.append(res) + return resources diff --git a/nova/api/openstack/compute/contrib/server_action_list.py b/nova/api/openstack/compute/contrib/server_action_list.py new file mode 100644 index 000000000..436758572 --- /dev/null +++ b/nova/api/openstack/compute/contrib/server_action_list.py @@ -0,0 +1,77 @@ +# 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 webob.exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception + + +sa_nsmap = {None: wsgi.XMLNS_V11} + + +class ServerActionsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('actions') + elem = xmlutil.SubTemplateElement(root, 'action', selector='actions') + elem.set('created_at') + elem.set('action') + elem.set('error') + return xmlutil.MasterTemplate(root, 1, nsmap=sa_nsmap) + + +class ServerActionListController(object): + @wsgi.serializers(xml=ServerActionsTemplate) + def index(self, req, server_id): + context = req.environ["nova.context"] + compute_api = compute.API() + + try: + instance = compute_api.get(context, server_id) + except exception.NotFound: + raise webob.exc.HTTPNotFound(_("Instance not found")) + + items = compute_api.get_actions(context, instance) + + def _format_item(item): + return { + 'created_at': str(item['created_at']), + 'action': item['action'], + 'error': item['error'], + } + + return {'actions': [_format_item(item) for item in items]} + + +class Server_action_list(extensions.ExtensionDescriptor): + """Allow Admins to view pending server actions""" + + name = "ServerActionList" + alias = "os-server-action-list" + namespace = "http://docs.openstack.org/compute/ext/" \ + "server-actions-list/api/v1.1" + updated = "2011-12-21T00:00:00+00:00" + admin_only = True + + def get_resources(self): + parent_def = {'member_name': 'server', 'collection_name': 'servers'} + #NOTE(bcwaldon): This should be prefixed with 'os-' + ext = extensions.ResourceExtension('actions', + ServerActionListController(), + parent=parent_def) + return [ext] diff --git a/nova/api/openstack/compute/contrib/server_diagnostics.py b/nova/api/openstack/compute/contrib/server_diagnostics.py new file mode 100644 index 000000000..11d1affaf --- /dev/null +++ b/nova/api/openstack/compute/contrib/server_diagnostics.py @@ -0,0 +1,69 @@ +# 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 webob.exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception +from nova.scheduler import api as scheduler_api + + +sd_nsmap = {None: wsgi.XMLNS_V11} + + +class ServerDiagnosticsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('diagnostics') + elem = xmlutil.SubTemplateElement(root, xmlutil.Selector(0), + selector=xmlutil.get_items) + elem.text = 1 + return xmlutil.MasterTemplate(root, 1, nsmap=sd_nsmap) + + +class ServerDiagnosticsController(object): + @wsgi.serializers(xml=ServerDiagnosticsTemplate) + @exception.novaclient_converter + @scheduler_api.redirect_handler + def index(self, req, server_id): + context = req.environ["nova.context"] + compute_api = compute.API() + try: + instance = compute_api.get(context, id) + except exception.NotFound(): + raise webob.exc.HTTPNotFound(_("Instance not found")) + + return compute_api.get_diagnostics(context, instance) + + +class Server_diagnostics(extensions.ExtensionDescriptor): + """Allow Admins to view server diagnostics through server action""" + + name = "ServerDiagnostics" + alias = "os-server-diagnostics" + namespace = "http://docs.openstack.org/compute/ext/" \ + "server-diagnostics/api/v1.1" + updated = "2011-12-21T00:00:00+00:00" + admin_only = True + + def get_resources(self): + parent_def = {'member_name': 'server', 'collection_name': 'servers'} + #NOTE(bcwaldon): This should be prefixed with 'os-' + ext = extensions.ResourceExtension('diagnostics', + ServerDiagnosticsController(), + parent=parent_def) + return [ext] diff --git a/nova/api/openstack/compute/contrib/simple_tenant_usage.py b/nova/api/openstack/compute/contrib/simple_tenant_usage.py new file mode 100644 index 000000000..f34581f6c --- /dev/null +++ b/nova/api/openstack/compute/contrib/simple_tenant_usage.py @@ -0,0 +1,265 @@ +# 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 datetime import datetime +import urlparse + +import webob + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.compute import api +from nova import exception +from nova import flags + + +FLAGS = flags.FLAGS + + +def make_usage(elem): + for subelem_tag in ('tenant_id', 'total_local_gb_usage', + 'total_vcpus_usage', 'total_memory_mb_usage', + 'total_hours', 'start', 'stop'): + subelem = xmlutil.SubTemplateElement(elem, subelem_tag) + subelem.text = subelem_tag + + server_usages = xmlutil.SubTemplateElement(elem, 'server_usages') + server_usage = xmlutil.SubTemplateElement(server_usages, 'server_usage', + selector='server_usages') + for subelem_tag in ('name', 'hours', 'memory_mb', 'local_gb', 'vcpus', + 'tenant_id', 'flavor', 'started_at', 'ended_at', + 'state', 'uptime'): + subelem = xmlutil.SubTemplateElement(server_usage, subelem_tag) + subelem.text = subelem_tag + + +class SimpleTenantUsageTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('tenant_usage', selector='tenant_usage') + make_usage(root) + return xmlutil.MasterTemplate(root, 1) + + +class SimpleTenantUsagesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('tenant_usages') + elem = xmlutil.SubTemplateElement(root, 'tenant_usage', + selector='tenant_usages') + make_usage(elem) + return xmlutil.MasterTemplate(root, 1) + + +class SimpleTenantUsageController(object): + def _hours_for(self, instance, period_start, period_stop): + launched_at = instance['launched_at'] + terminated_at = instance['terminated_at'] + if terminated_at is not None: + if not isinstance(terminated_at, datetime): + terminated_at = datetime.strptime(terminated_at, + "%Y-%m-%d %H:%M:%S.%f") + + if launched_at is not None: + if not isinstance(launched_at, datetime): + launched_at = datetime.strptime(launched_at, + "%Y-%m-%d %H:%M:%S.%f") + + if terminated_at and terminated_at < period_start: + return 0 + # nothing if it started after the usage report ended + if launched_at and launched_at > period_stop: + return 0 + if launched_at: + # if instance launched after period_started, don't charge for first + start = max(launched_at, period_start) + if terminated_at: + # if instance stopped before period_stop, don't charge after + stop = min(period_stop, terminated_at) + else: + # instance is still running, so charge them up to current time + stop = period_stop + dt = stop - start + seconds = dt.days * 3600 * 24 + dt.seconds\ + + dt.microseconds / 100000.0 + + return seconds / 3600.0 + else: + # instance hasn't launched, so no charge + return 0 + + def _tenant_usages_for_period(self, context, period_start, + period_stop, tenant_id=None, detailed=True): + + compute_api = api.API() + instances = compute_api.get_active_by_window(context, + period_start, + period_stop, + tenant_id) + from nova import log as logging + logging.info(instances) + rval = {} + flavors = {} + + for instance in instances: + info = {} + info['hours'] = self._hours_for(instance, + period_start, + period_stop) + flavor_type = instance['instance_type_id'] + + if not flavors.get(flavor_type): + try: + it_ref = compute_api.get_instance_type(context, + flavor_type) + flavors[flavor_type] = it_ref + except exception.InstanceTypeNotFound: + # can't bill if there is no instance type + continue + + flavor = flavors[flavor_type] + + info['name'] = instance['display_name'] + + info['memory_mb'] = flavor['memory_mb'] + info['local_gb'] = flavor['local_gb'] + info['vcpus'] = flavor['vcpus'] + + info['tenant_id'] = instance['project_id'] + + info['flavor'] = flavor['name'] + + info['started_at'] = instance['launched_at'] + + info['ended_at'] = instance['terminated_at'] + + if info['ended_at']: + info['state'] = 'terminated' + else: + info['state'] = instance['vm_state'] + + now = datetime.utcnow() + + if info['state'] == 'terminated': + delta = info['ended_at'] - info['started_at'] + else: + delta = now - info['started_at'] + + info['uptime'] = delta.days * 24 * 60 + delta.seconds + + if not info['tenant_id'] in rval: + summary = {} + summary['tenant_id'] = info['tenant_id'] + if detailed: + summary['server_usages'] = [] + summary['total_local_gb_usage'] = 0 + summary['total_vcpus_usage'] = 0 + summary['total_memory_mb_usage'] = 0 + summary['total_hours'] = 0 + summary['start'] = period_start + summary['stop'] = period_stop + rval[info['tenant_id']] = summary + + summary = rval[info['tenant_id']] + summary['total_local_gb_usage'] += info['local_gb'] * info['hours'] + summary['total_vcpus_usage'] += info['vcpus'] * info['hours'] + summary['total_memory_mb_usage'] += info['memory_mb']\ + * info['hours'] + + summary['total_hours'] += info['hours'] + if detailed: + summary['server_usages'].append(info) + + return rval.values() + + def _parse_datetime(self, dtstr): + if isinstance(dtstr, datetime): + return dtstr + try: + return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S") + except Exception: + try: + return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S.%f") + except Exception: + return datetime.strptime(dtstr, "%Y-%m-%d %H:%M:%S.%f") + + def _get_datetime_range(self, req): + qs = req.environ.get('QUERY_STRING', '') + env = urlparse.parse_qs(qs) + period_start = self._parse_datetime(env.get('start', + [datetime.utcnow().isoformat()])[0]) + period_stop = self._parse_datetime(env.get('end', + [datetime.utcnow().isoformat()])[0]) + + detailed = bool(env.get('detailed', False)) + return (period_start, period_stop, detailed) + + @wsgi.serializers(xml=SimpleTenantUsagesTemplate) + def index(self, req): + """Retrive tenant_usage for all tenants""" + context = req.environ['nova.context'] + + if not context.is_admin: + return webob.Response(status_int=403) + + (period_start, period_stop, detailed) = self._get_datetime_range(req) + usages = self._tenant_usages_for_period(context, + period_start, + period_stop, + detailed=detailed) + return {'tenant_usages': usages} + + @wsgi.serializers(xml=SimpleTenantUsageTemplate) + def show(self, req, id): + """Retrive tenant_usage for a specified tenant""" + tenant_id = id + context = req.environ['nova.context'] + + if not context.is_admin: + if tenant_id != context.project_id: + return webob.Response(status_int=403) + + (period_start, period_stop, ignore) = self._get_datetime_range(req) + usage = self._tenant_usages_for_period(context, + period_start, + period_stop, + tenant_id=tenant_id, + detailed=True) + if len(usage): + usage = usage[0] + else: + usage = {} + return {'tenant_usage': usage} + + +class Simple_tenant_usage(extensions.ExtensionDescriptor): + """Simple tenant usage extension""" + + name = "SimpleTenantUsage" + alias = "os-simple-tenant-usage" + namespace = "http://docs.openstack.org/compute/ext/" \ + "os-simple-tenant-usage/api/v1.1" + updated = "2011-08-19T00:00:00+00:00" + admin_only = True + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension('os-simple-tenant-usage', + SimpleTenantUsageController()) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/users.py b/nova/api/openstack/compute/contrib/users.py new file mode 100644 index 000000000..55dba02e4 --- /dev/null +++ b/nova/api/openstack/compute/contrib/users.py @@ -0,0 +1,145 @@ +# 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.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.auth import manager +from nova import exception +from nova import flags +from nova import log as logging + + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.api.openstack.users') + + +def make_user(elem): + elem.set('id') + elem.set('name') + elem.set('access') + elem.set('secret') + elem.set('admin') + + +class UserTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('user', selector='user') + make_user(root) + return xmlutil.MasterTemplate(root, 1) + + +class UsersTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('users') + elem = xmlutil.SubTemplateElement(root, 'user', selector='users') + make_user(elem) + return xmlutil.MasterTemplate(root, 1) + + +def _translate_keys(user): + return dict(id=user.id, + name=user.name, + access=user.access, + secret=user.secret, + admin=user.admin) + + +class Controller(object): + + 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.AdminRequired() + + @wsgi.serializers(xml=UsersTemplate) + 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) + + @wsgi.serializers(xml=UsersTemplate) + def detail(self, req): + """Return all users in detail""" + return self.index(req) + + @wsgi.serializers(xml=UserTemplate) + def show(self, req, id): + """Return data about the given 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 exc.HTTPNotFound() + + return dict(user=_translate_keys(user)) + + def delete(self, req, id): + self._check_admin(req.environ['nova.context']) + self.manager.delete_user(id) + return {} + + @wsgi.serializers(xml=UserTemplate) + def create(self, req, body): + self._check_admin(req.environ['nova.context']) + is_admin = body['user'].get('admin') in ('T', 'True', True) + name = body['user'].get('name') + access = body['user'].get('access') + secret = body['user'].get('secret') + user = self.manager.create_user(name, access, secret, is_admin) + return dict(user=_translate_keys(user)) + + @wsgi.serializers(xml=UserTemplate) + def update(self, req, id, body): + self._check_admin(req.environ['nova.context']) + is_admin = body['user'].get('admin') + if is_admin is not None: + is_admin = is_admin in ('T', 'True', True) + access = body['user'].get('access') + secret = body['user'].get('secret') + self.manager.modify_user(id, access, secret, is_admin) + return dict(user=_translate_keys(self.manager.get_user(id))) + + +class Users(extensions.ExtensionDescriptor): + """Allow admins to acces user information""" + + name = "Users" + alias = "os-users" + namespace = "http://docs.openstack.org/compute/ext/users/api/v1.1" + updated = "2011-08-08T00:00:00+00:00" + admin_only = True + + def get_resources(self): + coll_actions = {'detail': 'GET'} + res = extensions.ResourceExtension('users', + Controller(), + collection_actions=coll_actions) + + return [res] diff --git a/nova/api/openstack/compute/contrib/virtual_interfaces.py b/nova/api/openstack/compute/contrib/virtual_interfaces.py new file mode 100644 index 000000000..ea37c4d97 --- /dev/null +++ b/nova/api/openstack/compute/contrib/virtual_interfaces.py @@ -0,0 +1,93 @@ +# Copyright (C) 2011 Midokura KK +# 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 virtual interfaces extension.""" + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import log as logging +from nova import network + + +LOG = logging.getLogger("nova.api.openstack.compute." + "contrib.virtual_interfaces") + + +vif_nsmap = {None: wsgi.XMLNS_V11} + + +class VirtualInterfaceTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('virtual_interfaces') + elem = xmlutil.SubTemplateElement(root, 'virtual_interface', + selector='virtual_interfaces') + elem.set('id') + elem.set('mac_address') + return xmlutil.MasterTemplate(root, 1, nsmap=vif_nsmap) + + +def _translate_vif_summary_view(_context, vif): + """Maps keys for VIF summary view.""" + d = {} + d['id'] = vif['uuid'] + d['mac_address'] = vif['address'] + return d + + +class ServerVirtualInterfaceController(object): + """The instance VIF API controller for the Openstack API. + """ + + def __init__(self): + self.network_api = network.API() + super(ServerVirtualInterfaceController, self).__init__() + + def _items(self, req, server_id, entity_maker): + """Returns a list of VIFs, transformed through entity_maker.""" + context = req.environ['nova.context'] + + vifs = self.network_api.get_vifs_by_instance(context, server_id) + limited_list = common.limited(vifs, req) + res = [entity_maker(context, vif) for vif in limited_list] + return {'virtual_interfaces': res} + + @wsgi.serializers(xml=VirtualInterfaceTemplate) + def index(self, req, server_id): + """Returns the list of VIFs for a given instance.""" + return self._items(req, server_id, + entity_maker=_translate_vif_summary_view) + + +class Virtual_interfaces(extensions.ExtensionDescriptor): + """Virtual interface support""" + + name = "VirtualInterfaces" + alias = "virtual_interfaces" + namespace = "http://docs.openstack.org/compute/ext/" \ + "virtual_interfaces/api/v1.1" + updated = "2011-08-17T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'os-virtual-interfaces', + controller=ServerVirtualInterfaceController(), + parent=dict(member_name='server', collection_name='servers')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/virtual_storage_arrays.py b/nova/api/openstack/compute/contrib/virtual_storage_arrays.py new file mode 100644 index 000000000..39edd155b --- /dev/null +++ b/nova/api/openstack/compute/contrib/virtual_storage_arrays.py @@ -0,0 +1,687 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Zadara Storage Inc. +# Copyright (c) 2011 OpenStack LLC. +# +# 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 virtul storage array extension""" + + +import webob +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack.compute.contrib import volumes +from nova.api.openstack import extensions +from nova.api.openstack.compute import servers +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova.compute import instance_types +from nova import network +from nova import db +from nova import quota +from nova import exception +from nova import flags +from nova import log as logging +from nova import vsa +from nova import volume + +FLAGS = flags.FLAGS + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.vsa") + + +def _vsa_view(context, vsa, details=False, instances=None): + """Map keys for vsa summary/detailed view.""" + d = {} + + d['id'] = vsa.get('id') + d['name'] = vsa.get('name') + d['displayName'] = vsa.get('display_name') + d['displayDescription'] = vsa.get('display_description') + + d['createTime'] = vsa.get('created_at') + d['status'] = vsa.get('status') + + if 'vsa_instance_type' in vsa: + d['vcType'] = vsa['vsa_instance_type'].get('name', None) + else: + d['vcType'] = vsa['instance_type_id'] + + d['vcCount'] = vsa.get('vc_count') + d['driveCount'] = vsa.get('vol_count') + + d['ipAddress'] = None + for instance in instances: + fixed_addr = None + floating_addr = None + if instance['fixed_ips']: + fixed = instance['fixed_ips'][0] + fixed_addr = fixed['address'] + if fixed['floating_ips']: + floating_addr = fixed['floating_ips'][0]['address'] + + if floating_addr: + d['ipAddress'] = floating_addr + break + else: + d['ipAddress'] = d['ipAddress'] or fixed_addr + + return d + + +def make_vsa(elem): + elem.set('id') + elem.set('name') + elem.set('displayName') + elem.set('displayDescription') + elem.set('createTime') + elem.set('status') + elem.set('vcType') + elem.set('vcCount') + elem.set('driveCount') + elem.set('ipAddress') + + +class VsaTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('vsa', selector='vsa') + make_vsa(root) + return xmlutil.MasterTemplate(root, 1) + + +class VsaSetTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('vsaSet') + elem = xmlutil.SubTemplateElement(root, 'vsa', selector='vsaSet') + make_vsa(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VsaController(object): + """The Virtual Storage Array API controller for the OpenStack API.""" + + def __init__(self): + self.vsa_api = vsa.API() + self.compute_api = compute.API() + self.network_api = network.API() + super(VsaController, self).__init__() + + def _get_instances_by_vsa_id(self, context, id): + return self.compute_api.get_all(context, + search_opts={'metadata': dict(vsa_id=str(id))}) + + def _items(self, req, details): + """Return summary or detailed list of VSAs.""" + context = req.environ['nova.context'] + vsas = self.vsa_api.get_all(context) + limited_list = common.limited(vsas, req) + + vsa_list = [] + for vsa in limited_list: + instances = self._get_instances_by_vsa_id(context, vsa.get('id')) + vsa_list.append(_vsa_view(context, vsa, details, instances)) + return {'vsaSet': vsa_list} + + @wsgi.serializers(xml=VsaSetTemplate) + def index(self, req): + """Return a short list of VSAs.""" + return self._items(req, details=False) + + @wsgi.serializers(xml=VsaSetTemplate) + def detail(self, req): + """Return a detailed list of VSAs.""" + return self._items(req, details=True) + + @wsgi.serializers(xml=VsaTemplate) + def show(self, req, id): + """Return data about the given VSA.""" + context = req.environ['nova.context'] + + try: + vsa = self.vsa_api.get(context, vsa_id=id) + except exception.NotFound: + raise exc.HTTPNotFound() + + instances = self._get_instances_by_vsa_id(context, vsa.get('id')) + return {'vsa': _vsa_view(context, vsa, True, instances)} + + @wsgi.serializers(xml=VsaTemplate) + def create(self, req, body): + """Create a new VSA.""" + context = req.environ['nova.context'] + + if not body or 'vsa' not in body: + LOG.debug(_("No body provided"), context=context) + raise exc.HTTPUnprocessableEntity() + + vsa = body['vsa'] + + display_name = vsa.get('displayName') + vc_type = vsa.get('vcType', FLAGS.default_vsa_instance_type) + try: + instance_type = instance_types.get_instance_type_by_name(vc_type) + except exception.NotFound: + raise exc.HTTPNotFound() + + LOG.audit(_("Create VSA %(display_name)s of type %(vc_type)s"), + locals(), context=context) + + args = dict(display_name=display_name, + display_description=vsa.get('displayDescription'), + instance_type=instance_type, + storage=vsa.get('storage'), + shared=vsa.get('shared'), + availability_zone=vsa.get('placement', {}).\ + get('AvailabilityZone')) + + vsa = self.vsa_api.create(context, **args) + + instances = self._get_instances_by_vsa_id(context, vsa.get('id')) + return {'vsa': _vsa_view(context, vsa, True, instances)} + + def delete(self, req, id): + """Delete a VSA.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete VSA with id: %s"), id, context=context) + + try: + self.vsa_api.delete(context, vsa_id=id) + except exception.NotFound: + raise exc.HTTPNotFound() + + def associate_address(self, req, id, body): + """ /zadr-vsa/{vsa_id}/associate_address + auto or manually associate an IP to VSA + """ + context = req.environ['nova.context'] + + if body is None: + ip = 'auto' + else: + ip = body.get('ipAddress', 'auto') + + LOG.audit(_("Associate address %(ip)s to VSA %(id)s"), + locals(), context=context) + + try: + instances = self._get_instances_by_vsa_id(context, id) + if instances is None or len(instances) == 0: + raise exc.HTTPNotFound() + + for instance in instances: + self.network_api.allocate_for_instance(context, instance, + vpn=False) + # Placeholder + return + + except exception.NotFound: + raise exc.HTTPNotFound() + + def disassociate_address(self, req, id, body): + """ /zadr-vsa/{vsa_id}/disassociate_address + auto or manually associate an IP to VSA + """ + context = req.environ['nova.context'] + + if body is None: + ip = 'auto' + else: + ip = body.get('ipAddress', 'auto') + + LOG.audit(_("Disassociate address from VSA %(id)s"), + locals(), context=context) + # Placeholder + + +def make_volume(elem): + volumes.make_volume(elem) + elem.set('name') + elem.set('vsaId') + + +class VsaVolumeDriveController(volumes.VolumeController): + """The base class for VSA volumes & drives. + + A child resource of the VSA object. Allows operations with + volumes and drives created to/from particular VSA + + """ + + def __init__(self): + self.volume_api = volume.API() + self.vsa_api = vsa.API() + super(VsaVolumeDriveController, self).__init__() + + def _translation(self, context, vol, vsa_id, details): + if details: + translation = volumes._translate_volume_detail_view + else: + translation = volumes._translate_volume_summary_view + + d = translation(context, vol) + d['vsaId'] = vsa_id + d['name'] = vol['name'] + return d + + def _check_volume_ownership(self, context, vsa_id, id): + obj = self.object + try: + volume_ref = self.volume_api.get(context, volume_id=id) + except exception.NotFound: + LOG.error(_("%(obj)s with ID %(id)s not found"), locals()) + raise + + own_vsa_id = self.volume_api.get_volume_metadata_value(volume_ref, + self.direction) + if own_vsa_id != vsa_id: + LOG.error(_("%(obj)s with ID %(id)s belongs to VSA %(own_vsa_id)s"\ + " and not to VSA %(vsa_id)s."), locals()) + raise exception.Invalid() + + def _items(self, req, vsa_id, details): + """Return summary or detailed list of volumes for particular VSA.""" + context = req.environ['nova.context'] + + vols = self.volume_api.get_all(context, + search_opts={'metadata': {self.direction: str(vsa_id)}}) + limited_list = common.limited(vols, req) + + res = [self._translation(context, vol, vsa_id, details) \ + for vol in limited_list] + + return {self.objects: res} + + def index(self, req, vsa_id): + """Return a short list of volumes created from particular VSA.""" + LOG.audit(_("Index. vsa_id=%(vsa_id)s"), locals()) + return self._items(req, vsa_id, details=False) + + def detail(self, req, vsa_id): + """Return a detailed list of volumes created from particular VSA.""" + LOG.audit(_("Detail. vsa_id=%(vsa_id)s"), locals()) + return self._items(req, vsa_id, details=True) + + def create(self, req, vsa_id, body): + """Create a new volume from VSA.""" + LOG.audit(_("Create. vsa_id=%(vsa_id)s, body=%(body)s"), locals()) + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + vol = body[self.object] + size = vol['size'] + LOG.audit(_("Create volume of %(size)s GB from VSA ID %(vsa_id)s"), + locals(), context=context) + try: + # create is supported for volumes only (drives created through VSA) + volume_type = self.vsa_api.get_vsa_volume_type(context) + except exception.NotFound: + raise exc.HTTPNotFound() + + new_volume = self.volume_api.create(context, + size, + None, + vol.get('displayName'), + vol.get('displayDescription'), + volume_type=volume_type, + metadata=dict(from_vsa_id=str(vsa_id))) + + return {self.object: self._translation(context, new_volume, + vsa_id, True)} + + def update(self, req, vsa_id, id, body): + """Update a volume.""" + context = req.environ['nova.context'] + + try: + self._check_volume_ownership(context, vsa_id, id) + except exception.NotFound: + raise exc.HTTPNotFound() + except exception.Invalid: + raise exc.HTTPBadRequest() + + vol = body[self.object] + updatable_fields = [{'displayName': 'display_name'}, + {'displayDescription': 'display_description'}, + {'status': 'status'}, + {'providerLocation': 'provider_location'}, + {'providerAuth': 'provider_auth'}] + changes = {} + for field in updatable_fields: + key = field.keys()[0] + val = field[key] + if key in vol: + changes[val] = vol[key] + + obj = self.object + LOG.audit(_("Update %(obj)s with id: %(id)s, changes: %(changes)s"), + locals(), context=context) + + try: + self.volume_api.update(context, volume_id=id, fields=changes) + except exception.NotFound: + raise exc.HTTPNotFound() + return webob.Response(status_int=202) + + def delete(self, req, vsa_id, id): + """Delete a volume.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete. vsa_id=%(vsa_id)s, id=%(id)s"), locals()) + + try: + self._check_volume_ownership(context, vsa_id, id) + except exception.NotFound: + raise exc.HTTPNotFound() + except exception.Invalid: + raise exc.HTTPBadRequest() + + return super(VsaVolumeDriveController, self).delete(req, id) + + def show(self, req, vsa_id, id): + """Return data about the given volume.""" + context = req.environ['nova.context'] + + LOG.audit(_("Show. vsa_id=%(vsa_id)s, id=%(id)s"), locals()) + + try: + self._check_volume_ownership(context, vsa_id, id) + except exception.NotFound: + raise exc.HTTPNotFound() + except exception.Invalid: + raise exc.HTTPBadRequest() + + return super(VsaVolumeDriveController, self).show(req, id) + + +class VsaVolumeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume', selector='volume') + make_volume(root) + return xmlutil.MasterTemplate(root, 1) + + +class VsaVolumesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumes') + elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes') + make_volume(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VsaVolumeController(VsaVolumeDriveController): + """The VSA volume API controller for the Openstack API. + + A child resource of the VSA object. Allows operations with volumes created + by particular VSA + + """ + + def __init__(self): + self.direction = 'from_vsa_id' + self.objects = 'volumes' + self.object = 'volume' + super(VsaVolumeController, self).__init__() + + @wsgi.serializers(xml=VsaVolumesTemplate) + def index(self, req, vsa_id): + return super(VsaVolumeController, self).index(req, vsa_id) + + @wsgi.serializers(xml=VsaVolumesTemplate) + def detail(self, req, vsa_id): + return super(VsaVolumeController, self).detail(req, vsa_id) + + @wsgi.serializers(xml=VsaVolumeTemplate) + def create(self, req, vsa_id, body): + return super(VsaVolumeController, self).create(req, vsa_id, body) + + @wsgi.serializers(xml=VsaVolumeTemplate) + def update(self, req, vsa_id, id, body): + return super(VsaVolumeController, self).update(req, vsa_id, id, body) + + @wsgi.serializers(xml=VsaVolumeTemplate) + def show(self, req, vsa_id, id): + return super(VsaVolumeController, self).show(req, vsa_id, id) + + +class VsaDriveTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('drive', selector='drive') + make_volume(root) + return xmlutil.MasterTemplate(root, 1) + + +class VsaDrivesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('drives') + elem = xmlutil.SubTemplateElement(root, 'drive', selector='drives') + make_volume(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VsaDriveController(VsaVolumeDriveController): + """The VSA Drive API controller for the Openstack API. + + A child resource of the VSA object. Allows operations with drives created + for particular VSA + + """ + + def __init__(self): + self.direction = 'to_vsa_id' + self.objects = 'drives' + self.object = 'drive' + super(VsaDriveController, self).__init__() + + def create(self, req, vsa_id, body): + """Create a new drive for VSA. Should be done through VSA APIs""" + raise exc.HTTPBadRequest() + + def update(self, req, vsa_id, id, body): + """Update a drive. Should be done through VSA APIs""" + raise exc.HTTPBadRequest() + + def delete(self, req, vsa_id, id): + """Delete a volume. Should be done through VSA APIs""" + raise exc.HTTPBadRequest() + + @wsgi.serializers(xml=VsaDrivesTemplate) + def index(self, req, vsa_id): + return super(VsaDriveController, self).index(req, vsa_id) + + @wsgi.serializers(xml=VsaDrivesTemplate) + def detail(self, req, vsa_id): + return super(VsaDriveController, self).detail(req, vsa_id) + + @wsgi.serializers(xml=VsaDriveTemplate) + def show(self, req, vsa_id, id): + return super(VsaDriveController, self).show(req, vsa_id, id) + + +def make_vpool(elem): + elem.set('id') + elem.set('vsaId') + elem.set('name') + elem.set('displayName') + elem.set('displayDescription') + elem.set('driveCount') + elem.set('protection') + elem.set('stripeSize') + elem.set('stripeWidth') + elem.set('createTime') + elem.set('status') + + drive_ids = xmlutil.SubTemplateElement(elem, 'driveIds') + drive_id = xmlutil.SubTemplateElement(drive_ids, 'driveId', + selector='driveIds') + drive_id.text = xmlutil.Selector() + + +class VsaVPoolTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('vpool', selector='vpool') + make_vpool(root) + return xmlutil.MasterTemplate(root, 1) + + +class VsaVPoolsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('vpools') + elem = xmlutil.SubTemplateElement(root, 'vpool', selector='vpools') + make_vpool(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VsaVPoolController(object): + """The vPool VSA API controller for the OpenStack API.""" + + def __init__(self): + self.vsa_api = vsa.API() + super(VsaVPoolController, self).__init__() + + @wsgi.serializers(xml=VsaVPoolsTemplate) + def index(self, req, vsa_id): + """Return a short list of vpools created from particular VSA.""" + return {'vpools': []} + + def create(self, req, vsa_id, body): + """Create a new vPool for VSA.""" + raise exc.HTTPBadRequest() + + def update(self, req, vsa_id, id, body): + """Update vPool parameters.""" + raise exc.HTTPBadRequest() + + def delete(self, req, vsa_id, id): + """Delete a vPool.""" + raise exc.HTTPBadRequest() + + def show(self, req, vsa_id, id): + """Return data about the given vPool.""" + raise exc.HTTPBadRequest() + + +class VsaVCController(servers.Controller): + """The VSA Virtual Controller API controller for the OpenStack API.""" + + def __init__(self): + self.vsa_api = vsa.API() + self.compute_api = compute.API() + self.vsa_id = None # VP-TODO: temporary ugly hack + super(VsaVCController, self).__init__() + + def _get_servers(self, req, is_detail): + """Returns a list of servers, taking into account any search + options specified. + """ + + if self.vsa_id is None: + super(VsaVCController, self)._get_servers(req, is_detail) + + context = req.environ['nova.context'] + + search_opts = {'metadata': dict(vsa_id=str(self.vsa_id))} + instance_list = self.compute_api.get_all( + context, search_opts=search_opts) + + limited_list = self._limit_items(instance_list, req) + servers = [self._build_view(req, inst, is_detail)['server'] + for inst in limited_list] + return dict(servers=servers) + + @wsgi.serializers(xml=servers.MinimalServersTemplate) + def index(self, req, vsa_id): + """Return list of instances for particular VSA.""" + + LOG.audit(_("Index instances for VSA %s"), vsa_id) + + self.vsa_id = vsa_id # VP-TODO: temporary ugly hack + result = super(VsaVCController, self).detail(req) + self.vsa_id = None + return result + + def create(self, req, vsa_id, body): + """Create a new instance for VSA.""" + raise exc.HTTPBadRequest() + + def update(self, req, vsa_id, id, body): + """Update VSA instance.""" + raise exc.HTTPBadRequest() + + def delete(self, req, vsa_id, id): + """Delete VSA instance.""" + raise exc.HTTPBadRequest() + + @wsgi.serializers(xml=servers.ServerTemplate) + def show(self, req, vsa_id, id): + """Return data about the given instance.""" + return super(VsaVCController, self).show(req, id) + + +class Virtual_storage_arrays(extensions.ExtensionDescriptor): + """Virtual Storage Arrays support""" + + name = "VSAs" + alias = "zadr-vsa" + namespace = "http://docs.openstack.org/compute/ext/vsa/api/v1.1" + updated = "2011-08-25T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'zadr-vsa', + VsaController(), + collection_actions={'detail': 'GET'}, + member_actions={'add_capacity': 'POST', + 'remove_capacity': 'POST', + 'associate_address': 'POST', + 'disassociate_address': 'POST'}) + resources.append(res) + + res = extensions.ResourceExtension('volumes', + VsaVolumeController(), + collection_actions={'detail': 'GET'}, + parent=dict( + member_name='vsa', + collection_name='zadr-vsa')) + resources.append(res) + + res = extensions.ResourceExtension('drives', + VsaDriveController(), + collection_actions={'detail': 'GET'}, + parent=dict( + member_name='vsa', + collection_name='zadr-vsa')) + resources.append(res) + + res = extensions.ResourceExtension('vpools', + VsaVPoolController(), + parent=dict( + member_name='vsa', + collection_name='zadr-vsa')) + resources.append(res) + + res = extensions.ResourceExtension('instances', + VsaVCController(), + parent=dict( + member_name='vsa', + collection_name='zadr-vsa')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/volumes.py b/nova/api/openstack/compute/contrib/volumes.py new file mode 100644 index 000000000..972c000ef --- /dev/null +++ b/nova/api/openstack/compute/contrib/volumes.py @@ -0,0 +1,550 @@ +# 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 +import webob + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack.compute import servers +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova.volume import volume_types + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.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'] + + if vol['volume_type_id'] and vol.get('volume_type'): + d['volumeType'] = vol['volume_type']['name'] + else: + d['volumeType'] = vol['volume_type_id'] + + d['snapshotId'] = vol['snapshot_id'] + LOG.audit(_("vol=%s"), vol, context=context) + + if vol.get('volume_metadata'): + meta_dict = {} + for i in vol['volume_metadata']: + meta_dict[i['key']] = i['value'] + d['metadata'] = meta_dict + else: + d['metadata'] = {} + + return d + + +def make_volume(elem): + elem.set('id') + elem.set('status') + elem.set('size') + elem.set('availabilityZone') + elem.set('createdAt') + elem.set('displayName') + elem.set('displayDescription') + elem.set('volumeType') + elem.set('snapshotId') + + attachments = xmlutil.SubTemplateElement(elem, 'attachments') + attachment = xmlutil.SubTemplateElement(attachments, 'attachment', + selector='attachments') + make_attachment(attachment) + + metadata = xmlutil.make_flat_dict('metadata') + elem.append(metadata) + + +class VolumeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume', selector='volume') + make_volume(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumes') + elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes') + make_volume(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeController(object): + """The Volumes API controller for the OpenStack API.""" + + def __init__(self): + self.volume_api = volume.API() + super(VolumeController, self).__init__() + + @wsgi.serializers(xml=VolumeTemplate) + 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: + raise 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: + raise exc.HTTPNotFound() + return webob.Response(status_int=202) + + @wsgi.serializers(xml=VolumesTemplate) + def index(self, req): + """Returns a summary list of volumes.""" + return self._items(req, entity_maker=_translate_volume_summary_view) + + @wsgi.serializers(xml=VolumesTemplate) + 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} + + @wsgi.serializers(xml=VolumeTemplate) + def create(self, req, body): + """Creates a new volume.""" + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + vol = body['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + + vol_type = vol.get('volume_type', None) + if vol_type: + try: + vol_type = volume_types.get_volume_type_by_name(context, + vol_type) + except exception.NotFound: + raise exc.HTTPNotFound() + + metadata = vol.get('metadata', None) + + new_volume = self.volume_api.create(context, size, + vol.get('snapshot_id'), + vol.get('display_name'), + vol.get('display_description'), + volume_type=vol_type, + metadata=metadata) + + # Work around problem that instance is lazy-loaded... + new_volume = self.volume_api.get(context, new_volume['id']) + + 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'): + d['serverId'] = vol['instance']['uuid'] + if vol.get('mountpoint'): + d['device'] = vol['mountpoint'] + + return d + + +def make_attachment(elem): + elem.set('id') + elem.set('serverId') + elem.set('volumeId') + elem.set('device') + + +class VolumeAttachmentTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumeAttachment', + selector='volumeAttachment') + make_attachment(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeAttachmentsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumeAttachments') + elem = xmlutil.SubTemplateElement(root, 'volumeAttachment', + selector='volumeAttachments') + make_attachment(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeAttachmentController(object): + """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) + + """ + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(VolumeAttachmentController, self).__init__() + + @wsgi.serializers(xml=VolumeAttachmentsTemplate) + 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) + + @wsgi.serializers(xml=VolumeAttachmentTemplate) + 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") + raise exc.HTTPNotFound() + + instance = vol['instance'] + if instance is None or str(instance['uuid']) != server_id: + LOG.debug("instance_id != server_id") + raise exc.HTTPNotFound() + + return {'volumeAttachment': _translate_attachment_detail_view(context, + vol)} + + @wsgi.serializers(xml=VolumeAttachmentTemplate) + def create(self, req, server_id, body): + """Attach a volume to an instance.""" + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + volume_id = body['volumeAttachment']['volumeId'] + device = body['volumeAttachment']['device'] + + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" + " at %(device)s") % locals() + LOG.audit(msg, context=context) + + try: + instance = self.compute_api.get(context, server_id) + self.compute_api.attach_volume(context, instance, + volume_id, device) + except exception.NotFound: + raise 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, body): + """Update a volume attachment. We don't currently support this.""" + raise 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: + raise exc.HTTPNotFound() + + instance = vol['instance'] + if instance is None or str(instance['uuid']) != server_id: + LOG.debug("instance_id != server_id") + raise exc.HTTPNotFound() + + self.compute_api.detach_volume(context, + volume_id=volume_id) + + return webob.Response(status_int=202) + + 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: + raise 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 BootFromVolumeController(servers.Controller): + """The boot from volume API controller for the Openstack API.""" + + def _get_block_device_mapping(self, data): + return data.get('block_device_mapping') + + +def _translate_snapshot_detail_view(context, vol): + """Maps keys for snapshots details view.""" + + d = _translate_snapshot_summary_view(context, vol) + + # NOTE(gagupta): No additional data / lookups at the moment + return d + + +def _translate_snapshot_summary_view(context, vol): + """Maps keys for snapshots summary view.""" + d = {} + + d['id'] = vol['id'] + d['volumeId'] = vol['volume_id'] + d['status'] = vol['status'] + # NOTE(gagupta): We map volume_size as the snapshot size + d['size'] = vol['volume_size'] + d['createdAt'] = vol['created_at'] + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +def make_snapshot(elem): + elem.set('id') + elem.set('status') + elem.set('size') + elem.set('createdAt') + elem.set('displayName') + elem.set('displayDescription') + elem.set('volumeId') + + +class SnapshotTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('snapshot', selector='snapshot') + make_snapshot(root) + return xmlutil.MasterTemplate(root, 1) + + +class SnapshotsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('snapshots') + elem = xmlutil.SubTemplateElement(root, 'snapshot', + selector='snapshots') + make_snapshot(elem) + return xmlutil.MasterTemplate(root, 1) + + +class SnapshotController(object): + """The Volumes API controller for the OpenStack API.""" + + def __init__(self): + self.volume_api = volume.API() + super(SnapshotController, self).__init__() + + @wsgi.serializers(xml=SnapshotTemplate) + def show(self, req, id): + """Return data about the given snapshot.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get_snapshot(context, id) + except exception.NotFound: + return exc.HTTPNotFound() + + return {'snapshot': _translate_snapshot_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a snapshot.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete snapshot with id: %s"), id, context=context) + + try: + self.volume_api.delete_snapshot(context, snapshot_id=id) + except exception.NotFound: + return exc.HTTPNotFound() + return webob.Response(status_int=202) + + @wsgi.serializers(xml=SnapshotsTemplate) + def index(self, req): + """Returns a summary list of snapshots.""" + return self._items(req, entity_maker=_translate_snapshot_summary_view) + + @wsgi.serializers(xml=SnapshotsTemplate) + def detail(self, req): + """Returns a detailed list of snapshots.""" + return self._items(req, entity_maker=_translate_snapshot_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of snapshots, transformed through entity_maker.""" + context = req.environ['nova.context'] + + snapshots = self.volume_api.get_all_snapshots(context) + limited_list = common.limited(snapshots, req) + res = [entity_maker(context, snapshot) for snapshot in limited_list] + return {'snapshots': res} + + @wsgi.serializers(xml=SnapshotTemplate) + def create(self, req, body): + """Creates a new snapshot.""" + context = req.environ['nova.context'] + + if not body: + return exc.HTTPUnprocessableEntity() + + snapshot = body['snapshot'] + volume_id = snapshot['volume_id'] + force = snapshot.get('force', False) + LOG.audit(_("Create snapshot from volume %s"), volume_id, + context=context) + + if force: + new_snapshot = self.volume_api.create_snapshot_force(context, + volume_id, + snapshot.get('display_name'), + snapshot.get('display_description')) + else: + new_snapshot = self.volume_api.create_snapshot(context, + volume_id, + snapshot.get('display_name'), + snapshot.get('display_description')) + + retval = _translate_snapshot_detail_view(context, new_snapshot) + + return {'snapshot': retval} + + +class Volumes(extensions.ExtensionDescriptor): + """Volumes support""" + + name = "Volumes" + alias = "os-volumes" + namespace = "http://docs.openstack.org/compute/ext/volumes/api/v1.1" + updated = "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('os-volumes', + VolumeController(), + collection_actions={'detail': 'GET'}) + resources.append(res) + + res = extensions.ResourceExtension('os-volume_attachments', + VolumeAttachmentController(), + parent=dict( + member_name='server', + collection_name='servers')) + resources.append(res) + + res = extensions.ResourceExtension('os-volumes_boot', + BootFromVolumeController()) + resources.append(res) + + res = extensions.ResourceExtension('os-snapshots', + SnapshotController(), + collection_actions={'detail': 'GET'}) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/volumetypes.py b/nova/api/openstack/compute/contrib/volumetypes.py new file mode 100644 index 000000000..bf249f3f8 --- /dev/null +++ b/nova/api/openstack/compute/contrib/volumetypes.py @@ -0,0 +1,237 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Zadara Storage Inc. +# Copyright (c) 2011 OpenStack LLC. +# +# 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 volume type & volume types extra specs extension""" + +from webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import db +from nova import exception +from nova.volume import volume_types + + +def make_voltype(elem): + elem.set('id') + elem.set('name') + extra_specs = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') + elem.append(extra_specs) + + +class VolumeTypeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume_type', selector='volume_type') + make_voltype(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume_types') + sel = lambda obj, do_raise=False: obj.values() + elem = xmlutil.SubTemplateElement(root, 'volume_type', selector=sel) + make_voltype(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypesController(object): + """ The volume types API controller for the Openstack API """ + + @wsgi.serializers(xml=VolumeTypesTemplate) + def index(self, req): + """ Returns the list of volume types """ + context = req.environ['nova.context'] + return volume_types.get_all_types(context) + + @wsgi.serializers(xml=VolumeTypeTemplate) + def create(self, req, body): + """Creates a new volume type.""" + context = req.environ['nova.context'] + + if not body or body == "": + raise exc.HTTPUnprocessableEntity() + + vol_type = body.get('volume_type', None) + if vol_type is None or vol_type == "": + raise exc.HTTPUnprocessableEntity() + + name = vol_type.get('name', None) + specs = vol_type.get('extra_specs', {}) + + if name is None or name == "": + raise exc.HTTPUnprocessableEntity() + + try: + volume_types.create(context, name, specs) + vol_type = volume_types.get_volume_type_by_name(context, name) + except exception.QuotaError as error: + self._handle_quota_error(error) + except exception.NotFound: + raise exc.HTTPNotFound() + + return {'volume_type': vol_type} + + @wsgi.serializers(xml=VolumeTypeTemplate) + def show(self, req, id): + """ Return a single volume type item """ + context = req.environ['nova.context'] + + try: + vol_type = volume_types.get_volume_type(context, id) + except exception.NotFound or exception.ApiError: + raise exc.HTTPNotFound() + + return {'volume_type': vol_type} + + def delete(self, req, id): + """ Deletes an existing volume type """ + context = req.environ['nova.context'] + + try: + vol_type = volume_types.get_volume_type(context, id) + volume_types.destroy(context, vol_type['name']) + except exception.NotFound: + raise exc.HTTPNotFound() + + def _handle_quota_error(self, error): + """Reraise quota errors as api-specific http exceptions.""" + if error.code == "MetadataLimitExceeded": + raise exc.HTTPBadRequest(explanation=error.message) + raise error + + +class VolumeTypeExtraSpecsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypeExtraSpecTemplate(xmlutil.TemplateBuilder): + def construct(self): + tagname = xmlutil.Selector('key') + + def extraspec_sel(obj, do_raise=False): + # Have to extract the key and value for later use... + key, value = obj.items()[0] + return dict(key=key, value=value) + + root = xmlutil.TemplateElement(tagname, selector=extraspec_sel) + root.text = 'value' + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypeExtraSpecsController(object): + """ The volume type extra specs API controller for the Openstack API """ + + def _get_extra_specs(self, context, vol_type_id): + extra_specs = db.volume_type_extra_specs_get(context, vol_type_id) + specs_dict = {} + for key, value in extra_specs.iteritems(): + specs_dict[key] = value + return dict(extra_specs=specs_dict) + + def _check_body(self, body): + if body is None or body == "": + expl = _('No Request Body') + raise exc.HTTPBadRequest(explanation=expl) + + @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) + def index(self, req, vol_type_id): + """ Returns the list of extra specs for a given volume type """ + context = req.environ['nova.context'] + return self._get_extra_specs(context, vol_type_id) + + @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) + def create(self, req, vol_type_id, body): + self._check_body(body) + context = req.environ['nova.context'] + specs = body.get('extra_specs') + try: + db.volume_type_extra_specs_update_or_create(context, + vol_type_id, + specs) + except exception.QuotaError as error: + self._handle_quota_error(error) + return body + + @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) + def update(self, req, vol_type_id, id, body): + self._check_body(body) + context = req.environ['nova.context'] + 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) + try: + db.volume_type_extra_specs_update_or_create(context, + vol_type_id, + body) + except exception.QuotaError as error: + self._handle_quota_error(error) + + return body + + @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) + def show(self, req, vol_type_id, id): + """ Return a single extra spec item """ + context = req.environ['nova.context'] + specs = self._get_extra_specs(context, vol_type_id) + if id in specs['extra_specs']: + return {id: specs['extra_specs'][id]} + else: + raise exc.HTTPNotFound() + + def delete(self, req, vol_type_id, id): + """ Deletes an existing extra spec """ + context = req.environ['nova.context'] + db.volume_type_extra_specs_delete(context, vol_type_id, id) + + def _handle_quota_error(self, error): + """Reraise quota errors as api-specific http exceptions.""" + if error.code == "MetadataLimitExceeded": + raise exc.HTTPBadRequest(explanation=error.message) + raise error + + +class Volumetypes(extensions.ExtensionDescriptor): + """Volume types support""" + + name = "VolumeTypes" + alias = "os-volume-types" + namespace = "http://docs.openstack.org/compute/ext/volume_types/api/v1.1" + updated = "2011-08-24T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'os-volume-types', + VolumeTypesController()) + resources.append(res) + + res = extensions.ResourceExtension('extra_specs', + VolumeTypeExtraSpecsController(), + parent=dict( + member_name='vol_type', + collection_name='os-volume-types')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/compute/contrib/zones.py b/nova/api/openstack/compute/contrib/zones.py new file mode 100644 index 000000000..28e6f0772 --- /dev/null +++ b/nova/api/openstack/compute/contrib/zones.py @@ -0,0 +1,239 @@ +# 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. + +"""The zones extension.""" + +import json + +from nova.api.openstack import common +from nova.api.openstack.compute import servers +from nova.api.openstack import extensions +from nova.api.openstack import xmlutil +from nova.api.openstack import wsgi +from nova.compute import api as compute +from nova import crypto +from nova import exception +from nova import flags +from nova import log as logging +import nova.scheduler.api + + +LOG = logging.getLogger("nova.api.openstack.compute.contrib.zones") +FLAGS = flags.FLAGS + + +class CapabilitySelector(object): + def __call__(self, obj, do_raise=False): + return [(k, v) for k, v in obj.items() + if k not in ('id', 'api_url', 'name', 'capabilities')] + + +def make_zone(elem): + elem.set('id') + elem.set('api_url') + elem.set('name') + elem.set('capabilities') + + cap = xmlutil.SubTemplateElement(elem, xmlutil.Selector(0), + selector=CapabilitySelector()) + cap.text = 1 + + +zone_nsmap = {None: wsgi.XMLNS_V10} + + +class ZoneTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('zone', selector='zone') + make_zone(root) + return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) + + +class ZonesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('zones') + elem = xmlutil.SubTemplateElement(root, 'zone', selector='zones') + make_zone(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) + + +class WeightsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('weights') + weight = xmlutil.SubTemplateElement(root, 'weight', selector='weights') + blob = xmlutil.SubTemplateElement(weight, 'blob') + blob.text = 'blob' + inner_weight = xmlutil.SubTemplateElement(weight, 'weight') + inner_weight.text = 'weight' + return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) + + +def _filter_keys(item, keys): + """ + Filters all model attributes except for keys + item is a dict + + """ + 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 and (k not in keys)) + + +def _scrub_zone(zone): + return _exclude_keys(zone, ('username', 'password', 'created_at', + 'deleted', 'deleted_at', 'updated_at')) + + +def check_encryption_key(func): + def wrapped(*args, **kwargs): + if not FLAGS.build_plan_encryption_key: + raise exception.Error(_("--build_plan_encryption_key not set")) + return func(*args, **kwargs) + return wrapped + + +class Controller(object): + """Controller for Zone resources.""" + + def __init__(self): + self.compute_api = compute.API() + + @wsgi.serializers(xml=ZonesTemplate) + def index(self, req): + """Return all zones in brief""" + # Ask the ZoneManager in the Scheduler for most recent data, + # or fall-back to the database ... + items = nova.scheduler.api.get_zone_list(req.environ['nova.context']) + items = common.limited(items, req) + items = [_scrub_zone(item) for item in items] + return dict(zones=items) + + @wsgi.serializers(xml=ZonesTemplate) + def detail(self, req): + """Return all zones in detail""" + return self.index(req) + + @wsgi.serializers(xml=ZoneTemplate) + def info(self, req): + """Return name and capabilities for this zone.""" + context = req.environ['nova.context'] + items = nova.scheduler.api.get_zone_capabilities(context) + + zone = dict(name=FLAGS.zone_name) + caps = FLAGS.zone_capabilities + for cap in caps: + 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) + + @wsgi.serializers(xml=ZoneTemplate) + def show(self, req, id): + """Return data about the given zone id""" + zone_id = int(id) + context = req.environ['nova.context'] + zone = nova.scheduler.api.zone_get(context, zone_id) + return dict(zone=_scrub_zone(zone)) + + def delete(self, req, id): + """Delete a child zone entry.""" + zone_id = int(id) + nova.scheduler.api.zone_delete(req.environ['nova.context'], zone_id) + return {} + + @wsgi.serializers(xml=ZoneTemplate) + @wsgi.deserializers(xml=servers.CreateDeserializer) + def create(self, req, body): + """Create a child zone entry.""" + context = req.environ['nova.context'] + zone = nova.scheduler.api.zone_create(context, body["zone"]) + return dict(zone=_scrub_zone(zone)) + + @wsgi.serializers(xml=ZoneTemplate) + def update(self, req, id, body): + """Update a child zone entry.""" + context = req.environ['nova.context'] + zone_id = int(id) + zone = nova.scheduler.api.zone_update(context, zone_id, body["zone"]) + return dict(zone=_scrub_zone(zone)) + + @wsgi.serializers(xml=WeightsTemplate) + @check_encryption_key + def select(self, req, body): + """Returns a weighted list of costs to create instances + of desired capabilities.""" + ctx = req.environ['nova.context'] + specs = json.loads(body) + build_plan = nova.scheduler.api.select(ctx, specs=specs) + cooked = self._scrub_build_plan(build_plan) + return {"weights": cooked} + + def _scrub_build_plan(self, build_plan): + """Remove all the confidential data and return a sanitized + version of the build plan. Include an encrypted full version + of the weighting entry so we can get back to it later.""" + encryptor = crypto.encryptor(FLAGS.build_plan_encryption_key) + cooked = [] + for entry in build_plan: + json_entry = json.dumps(entry) + cipher_text = encryptor(json_entry) + cooked.append(dict(weight=entry['weight'], + blob=cipher_text)) + return cooked + + +class ZonesXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return ZonesTemplate() + + def detail(self): + return ZonesTemplate() + + def select(self): + return WeightsTemplate() + + def default(self): + return ZoneTemplate() + + +class Zones(extensions.ExtensionDescriptor): + """Enables zones-related functionality such as adding child zones, + listing child zones, getting the capabilities of the local zone, + and returning build plans to parent zones' schedulers + """ + + name = "Zones" + alias = "os-zones" + namespace = "http://docs.openstack.org/compute/ext/zones/api/v1.1" + updated = "2011-09-21T00:00:00+00:00" + admin_only = True + + def get_resources(self): + #NOTE(bcwaldon): This resource should be prefixed with 'os-' + coll_actions = { + 'detail': 'GET', + 'info': 'GET', + 'select': 'POST', + } + + res = extensions.ResourceExtension('zones', + Controller(), + collection_actions=coll_actions) + return [res] diff --git a/nova/api/openstack/compute/extensions.py b/nova/api/openstack/compute/extensions.py new file mode 100644 index 000000000..39849e802 --- /dev/null +++ b/nova/api/openstack/compute/extensions.py @@ -0,0 +1,45 @@ +# 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 nova.api.openstack import extensions as base_extensions +from nova import flags +from nova import log as logging + + +LOG = logging.getLogger('nova.api.openstack.compute.extensions') +FLAGS = flags.FLAGS + + +class ExtensionManager(base_extensions.ExtensionManager): + def __new__(cls): + if cls._ext_mgr is None: + LOG.audit(_('Initializing extension manager.')) + + cls._ext_mgr = super(ExtensionManager, cls).__new__(cls) + + cls.cls_list = FLAGS.osapi_compute_extension + cls._ext_mgr.extensions = {} + cls._ext_mgr._load_extensions() + + return cls._ext_mgr + + +class ExtensionMiddleware(base_extensions.ExtensionMiddleware): + def __init__(self, application, ext_mgr=None): + if not ext_mgr: + ext_mgr = ExtensionManager() + super(ExtensionMiddleware, self).__init__(application, ext_mgr) diff --git a/nova/api/openstack/compute/flavors.py b/nova/api/openstack/compute/flavors.py new file mode 100644 index 000000000..b5212e703 --- /dev/null +++ b/nova/api/openstack/compute/flavors.py @@ -0,0 +1,112 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob + +from nova.api.openstack.compute.views import flavors as flavors_view +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.compute import instance_types +from nova import exception + + +def make_flavor(elem, detailed=False): + elem.set('name') + elem.set('id') + if detailed: + elem.set('ram') + elem.set('disk') + + for attr in ("vcpus", "swap", "rxtx_factor"): + elem.set(attr, xmlutil.EmptyStringSelector(attr)) + + xmlutil.make_links(elem, 'links') + + +flavor_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class FlavorTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavor', selector='flavor') + make_flavor(root, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) + + +class MinimalFlavorsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavors') + elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors') + make_flavor(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) + + +class FlavorsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavors') + elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors') + make_flavor(elem, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) + + +class Controller(wsgi.Controller): + """Flavor controller for the OpenStack API.""" + + _view_builder_class = flavors_view.ViewBuilder + + @wsgi.serializers(xml=MinimalFlavorsTemplate) + def index(self, req): + """Return all flavors in brief.""" + flavors = self._get_flavors(req) + return self._view_builder.index(req, flavors) + + @wsgi.serializers(xml=FlavorsTemplate) + def detail(self, req): + """Return all flavors in detail.""" + flavors = self._get_flavors(req) + return self._view_builder.detail(req, flavors) + + @wsgi.serializers(xml=FlavorTemplate) + def show(self, req, id): + """Return data about the given flavor id.""" + try: + flavor = instance_types.get_instance_type_by_flavor_id(id) + except exception.NotFound: + raise webob.exc.HTTPNotFound() + + return self._view_builder.show(req, flavor) + + def _get_flavors(self, req): + """Helper function that returns a list of flavor dicts.""" + filters = {} + if 'minRam' in req.params: + try: + filters['min_memory_mb'] = int(req.params['minRam']) + except ValueError: + pass # ignore bogus values per spec + + if 'minDisk' in req.params: + try: + filters['min_local_gb'] = int(req.params['minDisk']) + except ValueError: + pass # ignore bogus values per spec + + return instance_types.get_all_types(filters=filters) + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/image_metadata.py b/nova/api/openstack/compute/image_metadata.py new file mode 100644 index 000000000..1e29d23ce --- /dev/null +++ b/nova/api/openstack/compute/image_metadata.py @@ -0,0 +1,118 @@ +# 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.api.openstack import common +from nova.api.openstack import wsgi +from nova import exception +from nova import flags +from nova import image + + +FLAGS = flags.FLAGS + + +class Controller(object): + """The image metadata API controller for the Openstack API""" + + def __init__(self): + self.image_service = image.get_default_image_service() + + def _get_image(self, context, image_id): + try: + return self.image_service.show(context, image_id) + except exception.NotFound: + msg = _("Image not found.") + raise exc.HTTPNotFound(explanation=msg) + + @wsgi.serializers(xml=common.MetadataTemplate) + def index(self, req, image_id): + """Returns the list of metadata for a given instance""" + context = req.environ['nova.context'] + metadata = self._get_image(context, image_id)['properties'] + return dict(metadata=metadata) + + @wsgi.serializers(xml=common.MetaItemTemplate) + def show(self, req, image_id, id): + context = req.environ['nova.context'] + metadata = self._get_image(context, image_id)['properties'] + if id in metadata: + return {'meta': {id: metadata[id]}} + else: + raise exc.HTTPNotFound() + + @wsgi.serializers(xml=common.MetadataTemplate) + @wsgi.deserializers(xml=common.MetadataDeserializer) + def create(self, req, image_id, body): + context = req.environ['nova.context'] + image = self._get_image(context, image_id) + if 'metadata' in body: + for key, value in body['metadata'].iteritems(): + image['properties'][key] = value + common.check_img_metadata_quota_limit(context, image['properties']) + self.image_service.update(context, image_id, image, None) + return dict(metadata=image['properties']) + + @wsgi.serializers(xml=common.MetaItemTemplate) + @wsgi.deserializers(xml=common.MetaItemDeserializer) + def update(self, req, image_id, id, body): + context = req.environ['nova.context'] + + try: + meta = body['meta'] + except KeyError: + expl = _('Incorrect request body format') + raise exc.HTTPBadRequest(explanation=expl) + + if not id in meta: + expl = _('Request body and URI mismatch') + raise exc.HTTPBadRequest(explanation=expl) + if len(meta) > 1: + expl = _('Request body contains too many items') + raise exc.HTTPBadRequest(explanation=expl) + + image = self._get_image(context, image_id) + image['properties'][id] = meta[id] + common.check_img_metadata_quota_limit(context, image['properties']) + self.image_service.update(context, image_id, image, None) + return dict(meta=meta) + + @wsgi.serializers(xml=common.MetadataTemplate) + @wsgi.deserializers(xml=common.MetadataDeserializer) + def update_all(self, req, image_id, body): + context = req.environ['nova.context'] + image = self._get_image(context, image_id) + metadata = body.get('metadata', {}) + common.check_img_metadata_quota_limit(context, metadata) + image['properties'] = metadata + self.image_service.update(context, image_id, image, None) + return dict(metadata=metadata) + + @wsgi.response(204) + def delete(self, req, image_id, id): + context = req.environ['nova.context'] + image = self._get_image(context, image_id) + if not id in image['properties']: + msg = _("Invalid metadata key") + raise exc.HTTPNotFound(explanation=msg) + image['properties'].pop(id) + self.image_service.update(context, image_id, image, None) + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/images.py b/nova/api/openstack/compute/images.py new file mode 100644 index 000000000..194c5071e --- /dev/null +++ b/nova/api/openstack/compute/images.py @@ -0,0 +1,195 @@ +# 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 webob.exc + +from nova.api.openstack import common +from nova.api.openstack.compute.views import images as views_images +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception +from nova import flags +import nova.image +from nova import log + + +LOG = log.getLogger('nova.api.openstack.compute.images') +FLAGS = flags.FLAGS + +SUPPORTED_FILTERS = { + 'name': 'name', + 'status': 'status', + 'changes-since': 'changes-since', + 'server': 'property-instance_ref', + 'type': 'property-image_type', + 'minRam': 'min_ram', + 'minDisk': 'min_disk', +} + + +def make_image(elem, detailed=False): + elem.set('name') + elem.set('id') + + if detailed: + elem.set('updated') + elem.set('created') + elem.set('status') + elem.set('progress') + elem.set('minRam') + elem.set('minDisk') + + server = xmlutil.SubTemplateElement(elem, 'server', selector='server') + server.set('id') + xmlutil.make_links(server, 'links') + + elem.append(common.MetadataTemplate()) + + xmlutil.make_links(elem, 'links') + + +image_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class ImageTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('image', selector='image') + make_image(root, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) + + +class MinimalImagesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('images') + elem = xmlutil.SubTemplateElement(root, 'image', selector='images') + make_image(elem) + xmlutil.make_links(root, 'images_links') + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) + + +class ImagesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('images') + elem = xmlutil.SubTemplateElement(root, 'image', selector='images') + make_image(elem, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) + + +class Controller(wsgi.Controller): + """Base controller for retrieving/displaying images.""" + + _view_builder_class = views_images.ViewBuilder + + def __init__(self, image_service=None, compute_service=None, **kwargs): + """Initialize new `ImageController`. + + :param compute_service: `nova.compute.api:API` + :param image_service: `nova.image.glance:GlancemageService` + + """ + super(Controller, self).__init__(**kwargs) + self._compute_service = compute_service or compute.API() + self._image_service = image_service or \ + nova.image.get_default_image_service() + + def _get_filters(self, req): + """ + Return a dictionary of query param filters from the request + + :param req: the Request object coming from the wsgi layer + :retval a dict of key/value filters + """ + filters = {} + for param in req.params: + if param in SUPPORTED_FILTERS or param.startswith('property-'): + # map filter name or carry through if property-* + filter_name = SUPPORTED_FILTERS.get(param, param) + filters[filter_name] = req.params.get(param) + return filters + + @wsgi.serializers(xml=ImageTemplate) + def show(self, req, id): + """Return detailed information about a specific image. + + :param req: `wsgi.Request` object + :param id: Image identifier + """ + context = req.environ['nova.context'] + + try: + image = self._image_service.show(context, id) + except (exception.NotFound, exception.InvalidImageRef): + explanation = _("Image not found.") + raise webob.exc.HTTPNotFound(explanation=explanation) + + return self._view_builder.show(req, image) + + def delete(self, req, id): + """Delete an image, if allowed. + + :param req: `wsgi.Request` object + :param id: Image identifier (integer) + """ + context = req.environ['nova.context'] + try: + self._image_service.delete(context, id) + except exception.ImageNotFound: + explanation = _("Image not found.") + raise webob.exc.HTTPNotFound(explanation=explanation) + return webob.exc.HTTPNoContent() + + @wsgi.serializers(xml=MinimalImagesTemplate) + def index(self, req): + """Return an index listing of images available to the request. + + :param req: `wsgi.Request` object + + """ + context = req.environ['nova.context'] + filters = self._get_filters(req) + params = req.GET.copy() + page_params = common.get_pagination_params(req) + for key, val in page_params.iteritems(): + params[key] = val + + images = self._image_service.index(context, filters=filters, + **page_params) + return self._view_builder.index(req, images) + + @wsgi.serializers(xml=ImagesTemplate) + def detail(self, req): + """Return a detailed index listing of images available to the request. + + :param req: `wsgi.Request` object. + + """ + context = req.environ['nova.context'] + filters = self._get_filters(req) + params = req.GET.copy() + page_params = common.get_pagination_params(req) + for key, val in page_params.iteritems(): + params[key] = val + images = self._image_service.detail(context, filters=filters, + **page_params) + + return self._view_builder.detail(req, images) + + def create(self, *args, **kwargs): + raise webob.exc.HTTPMethodNotAllowed() + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/ips.py b/nova/api/openstack/compute/ips.py new file mode 100644 index 000000000..ec107914a --- /dev/null +++ b/nova/api/openstack/compute/ips.py @@ -0,0 +1,105 @@ +# 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 + +import nova +from nova.api.openstack import common +from nova.api.openstack.compute.views import addresses as view_addresses +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import log as logging +from nova import flags + + +LOG = logging.getLogger('nova.api.openstack.compute.ips') +FLAGS = flags.FLAGS + + +def make_network(elem): + elem.set('id', 0) + + ip = xmlutil.SubTemplateElement(elem, 'ip', selector=1) + ip.set('version') + ip.set('addr') + + +network_nsmap = {None: xmlutil.XMLNS_V11} + + +class NetworkTemplate(xmlutil.TemplateBuilder): + def construct(self): + sel = xmlutil.Selector(xmlutil.get_items, 0) + root = xmlutil.TemplateElement('network', selector=sel) + make_network(root) + return xmlutil.MasterTemplate(root, 1, nsmap=network_nsmap) + + +class AddressesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('addresses', selector='addresses') + elem = xmlutil.SubTemplateElement(root, 'network', + selector=xmlutil.get_items) + make_network(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=network_nsmap) + + +class Controller(wsgi.Controller): + """The servers addresses API controller for the Openstack API.""" + + _view_builder_class = view_addresses.ViewBuilder + + def __init__(self, **kwargs): + super(Controller, self).__init__(**kwargs) + self._compute_api = nova.compute.API() + + def _get_instance(self, context, server_id): + try: + instance = self._compute_api.get(context, server_id) + except nova.exception.NotFound: + msg = _("Instance does not exist") + raise exc.HTTPNotFound(explanation=msg) + return instance + + def create(self, req, server_id, body): + raise exc.HTTPNotImplemented() + + def delete(self, req, server_id, id): + raise exc.HTTPNotImplemented() + + @wsgi.serializers(xml=AddressesTemplate) + def index(self, req, server_id): + context = req.environ["nova.context"] + instance = self._get_instance(context, server_id) + networks = common.get_networks_for_instance(context, instance) + return self._view_builder.index(networks) + + @wsgi.serializers(xml=NetworkTemplate) + def show(self, req, server_id, id): + context = req.environ["nova.context"] + instance = self._get_instance(context, server_id) + networks = common.get_networks_for_instance(context, instance) + + if id not in networks: + msg = _("Instance is not a member of specified network") + raise exc.HTTPNotFound(explanation=msg) + + return self._view_builder.show(networks[id], id) + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/limits.py b/nova/api/openstack/compute/limits.py new file mode 100644 index 000000000..0a1b80057 --- /dev/null +++ b/nova/api/openstack/compute/limits.py @@ -0,0 +1,477 @@ +# 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. + +""" +Module dedicated functions/classes dealing with rate limiting requests. +""" + +from collections import defaultdict +import copy +import httplib +import json +import math +import re +import time + +from webob.dec import wsgify +import webob.exc + +from nova.api.openstack.compute.views import limits as limits_views +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import quota +from nova import utils +from nova import wsgi as base_wsgi + + +# Convenience constants for the limits dictionary passed to Limiter(). +PER_SECOND = 1 +PER_MINUTE = 60 +PER_HOUR = 60 * 60 +PER_DAY = 60 * 60 * 24 + + +limits_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class LimitsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('limits', selector='limits') + + rates = xmlutil.SubTemplateElement(root, 'rates') + rate = xmlutil.SubTemplateElement(rates, 'rate', selector='rate') + rate.set('uri', 'uri') + rate.set('regex', 'regex') + limit = xmlutil.SubTemplateElement(rate, 'limit', selector='limit') + limit.set('value', 'value') + limit.set('verb', 'verb') + limit.set('remaining', 'remaining') + limit.set('unit', 'unit') + limit.set('next-available', 'next-available') + + absolute = xmlutil.SubTemplateElement(root, 'absolute', + selector='absolute') + limit = xmlutil.SubTemplateElement(absolute, 'limit', + selector=xmlutil.get_items) + limit.set('name', 0) + limit.set('value', 1) + + return xmlutil.MasterTemplate(root, 1, nsmap=limits_nsmap) + + +class LimitsController(object): + """ + Controller for accessing limits in the OpenStack API. + """ + + @wsgi.serializers(xml=LimitsTemplate) + def index(self, req): + """ + Return all global and rate limit information. + """ + context = req.environ['nova.context'] + abs_limits = quota.get_project_quotas(context, context.project_id) + rate_limits = req.environ.get("nova.limits", []) + + builder = self._get_view_builder(req) + return builder.build(rate_limits, abs_limits) + + def _get_view_builder(self, req): + return limits_views.ViewBuilder() + + +def create_resource(): + return wsgi.Resource(LimitsController()) + + +class Limit(object): + """ + Stores information about a limit for HTTP requests. + """ + + UNITS = { + 1: "SECOND", + 60: "MINUTE", + 60 * 60: "HOUR", + 60 * 60 * 24: "DAY", + } + + UNIT_MAP = dict([(v, k) for k, v in UNITS.items()]) + + 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.unit_string = self.display_unit().lower() + 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 = 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): + """ + 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) + 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(base_wsgi.Middleware): + """ + Rate-limits requests passing through this middleware. All limit information + is stored in memory for this implementation. + """ + + def __init__(self, application, limits=None, limiter=None, **kwargs): + """ + Initialize new `RateLimitingMiddleware`, which wraps the given WSGI + application and sets up the given limits. + + @param application: WSGI application to wrap + @param limits: String describing limits + @param limiter: String identifying class for representing limits + + Other parameters are passed to the constructor for the limiter. + """ + base_wsgi.Middleware.__init__(self, application) + + # Select the limiter class + if limiter is None: + limiter = Limiter + else: + limiter = utils.import_class(limiter) + + # Parse the limits, if any are provided + if limits is not None: + limits = limiter.parse_limits(limits) + + self._limiter = limiter(limits or DEFAULT_LIMITS, **kwargs) + + @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 + 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) + + if delay: + msg = _("This request was rate-limited.") + retry = time.time() + delay + return wsgi.OverLimitFault(msg, error, 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, **kwargs): + """ + Initialize the new `Limiter`. + + @param limits: List of `Limit` objects + """ + self.limits = copy.deepcopy(limits) + self.levels = defaultdict(lambda: copy.deepcopy(limits)) + + # Pick up any per-user limit information + for key, value in kwargs.items(): + if key.startswith('user:'): + username = key[5:] + self.levels[username] = self.parse_limits(value) + + 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. + + @return: Tuple of delay (in seconds) and error message (or None, None) + """ + delays = [] + + for limit in self.levels[username]: + delay = limit(verb, url) + if delay: + delays.append((delay, limit.error_message)) + + if delays: + delays.sort() + return delays[0] + + return None, None + + # Note: This method gets called before the class is instantiated, + # so this must be either a static method or a class method. It is + # used to develop a list of limits to feed to the constructor. We + # put this in the class so that subclasses can override the + # default limit parsing. + @staticmethod + def parse_limits(limits): + """ + Convert a string into a list of Limit instances. This + implementation expects a semicolon-separated sequence of + parenthesized groups, where each group contains a + comma-separated sequence consisting of HTTP method, + user-readable URI, a URI reg-exp, an integer number of + requests which can be made, and a unit of measure. Valid + values for the latter are "SECOND", "MINUTE", "HOUR", and + "DAY". + + @return: List of Limit instances. + """ + + # Handle empty limit strings + limits = limits.strip() + if not limits: + return [] + + # Split up the limits by semicolon + result = [] + for group in limits.split(';'): + group = group.strip() + if group[:1] != '(' or group[-1:] != ')': + raise ValueError("Limit rules must be surrounded by " + "parentheses") + group = group[1:-1] + + # Extract the Limit arguments + args = [a.strip() for a in group.split(',')] + if len(args) != 5: + raise ValueError("Limit rules must contain the following " + "arguments: verb, uri, regex, value, unit") + + # Pull out the arguments + verb, uri, regex, value, unit = args + + # Upper-case the verb + verb = verb.upper() + + # Convert value--raises ValueError if it's not integer + value = int(value) + + # Convert unit + unit = unit.upper() + if unit not in Limit.UNIT_MAP: + raise ValueError("Invalid units specified") + unit = Limit.UNIT_MAP[unit] + + # Build a limit + result.append(Limit(verb, uri, regex, value, unit)) + + return result + + +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, error = self._limiter.check_for_delay(verb, path, username) + + if delay: + headers = {"X-Wait-Seconds": "%.2f" % delay} + return webob.exc.HTTPForbidden(headers=headers, explanation=error) + 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, None + + return resp.getheader("X-Wait-Seconds"), resp.read() or None + + # Note: This method gets called before the class is instantiated, + # so this must be either a static method or a class method. It is + # used to develop a list of limits to feed to the constructor. + # This implementation returns an empty list, since all limit + # decisions are made by a remote server. + @staticmethod + def parse_limits(limits): + """ + Ignore a limits string--simply doesn't apply for the limit + proxy. + + @return: Empty list. + """ + + return [] diff --git a/nova/api/openstack/compute/ratelimiting/__init__.py b/nova/api/openstack/compute/ratelimiting/__init__.py new file mode 100644 index 000000000..78dc465a7 --- /dev/null +++ b/nova/api/openstack/compute/ratelimiting/__init__.py @@ -0,0 +1,222 @@ +# 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. + +"""Rate limiting of arbitrary actions.""" + +import httplib +import time +import urllib + +import webob.dec +import webob.exc + +from nova import wsgi +from nova.api.openstack import wsgi as os_wsgi + +# 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 RateLimitingMiddleware(wsgi.Middleware): + """Rate limit incoming requests according to the OpenStack rate limits.""" + + def __init__(self, application, service_host=None): + """Create a rate limiting middleware that wraps the given application. + + By default, rate counters are stored in memory. If service_host is + specified, the middleware instead relies on the ratelimiting.WSGIApp + at the given host+port to keep rate counters. + """ + if not service_host: + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = Limiter(limits={ + 'DELETE': (100, PER_MINUTE), + 'PUT': (10, PER_MINUTE), + 'POST': (10, PER_MINUTE), + 'POST servers': (50, PER_DAY), + 'GET changes-since': (3, PER_MINUTE), + }) + else: + self.limiter = WSGIAppProxy(service_host) + super(RateLimitingMiddleware, self).__init__(application) + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + """Rate limit the request. + + If the request should be rate limited, return a 413 status with a + Retry-After header giving the time when the request would succeed. + """ + return self.rate_limited_request(req, self.application) + + def rate_limited_request(self, req, application): + """Rate limit the request. + + If the request should be rate limited, return a 413 status with a + Retry-After header giving the time when the request would succeed. + """ + action_name = self.get_action_name(req) + if not action_name: + # Not rate limited + return application + delay = self.get_delay(action_name, + req.environ['nova.context'].user_id) + if delay: + # TODO(gundlach): Get the retry-after format correct. + exc = webob.exc.HTTPRequestEntityTooLarge( + explanation=('Too many requests.'), + headers={'Retry-After': time.time() + delay}) + raise os_wsgi.Fault(exc) + return application + + def get_delay(self, action_name, username): + """Return the delay for the given action and username, or None if + the action would not be rate limited. + """ + if action_name == 'POST servers': + # "POST servers" is a POST, so it counts against "POST" too. + # Attempt the "POST" first, lest we are rate limited by "POST" but + # use up a precious "POST servers" call. + delay = self.limiter.perform("POST", username=username) + if delay: + return delay + return self.limiter.perform(action_name, username=username) + + def get_action_name(self, req): + """Return the action name for this request.""" + if req.method == 'GET' and 'changes-since' in req.GET: + return 'GET changes-since' + if req.method == 'POST' and req.path_info.startswith('/servers'): + return 'POST servers' + if req.method in ['PUT', 'POST', 'DELETE']: + return req.method + return None + + +class Limiter(object): + + """Class providing rate limiting of arbitrary actions.""" + + def __init__(self, limits): + """Create a rate limiter. + + limits: a dict mapping from action name to a tuple. The tuple contains + the number of times the action may be performed, and the time period + (in seconds) during which the number must not be exceeded for this + action. Example: dict(reboot=(10, ratelimiting.PER_MINUTE)) would + allow 10 'reboot' actions per minute. + """ + self.limits = limits + self._levels = {} + + def perform(self, action_name, username='nobody'): + """Attempt to perform an action by the given username. + + action_name: the string name of the action to perform. This must + be a key in the limits dict passed to the ctor. + + username: an optional string name of the user performing the action. + Each user has her own set of rate limiting counters. Defaults to + 'nobody' (so that if you never specify a username when calling + perform(), a single set of counters will be used.) + + Return None if the action may proceed. If the action may not proceed + because it has been rate limited, return the float number of seconds + until the action would succeed. + """ + # Think of rate limiting as a bucket leaking water at 1cc/second. The + # bucket can hold as many ccs as there are seconds in the rate + # limiting period (e.g. 3600 for per-hour ratelimits), and if you can + # perform N actions in that time, each action fills the bucket by + # 1/Nth of its volume. You may only perform an action if the bucket + # would not overflow. + now = time.time() + key = '%s:%s' % (username, action_name) + last_time_performed, water_level = self._levels.get(key, (now, 0)) + # The bucket leaks 1cc/second. + water_level -= (now - last_time_performed) + if water_level < 0: + water_level = 0 + num_allowed_per_period, period_in_secs = self.limits[action_name] + # Fill the bucket by 1/Nth its capacity, and hope it doesn't overflow. + capacity = period_in_secs + new_level = water_level + (capacity * 1.0 / num_allowed_per_period) + if new_level > capacity: + # Delay this many seconds. + return new_level - capacity + self._levels[key] = (now, new_level) + return None + +# If one instance of this WSGIApps is unable to handle your load, put a +# sharding app in front that shards by username to one of many backends. + + +class WSGIApp(object): + + """Application that tracks rate limits in memory. Send requests to it of + this form: + + POST /limiter// + + and receive a 200 OK, or a 403 Forbidden with an X-Wait-Seconds header + containing the number of seconds to wait before the action would succeed. + """ + + def __init__(self, limiter): + """Create the WSGI application using the given Limiter instance.""" + self.limiter = limiter + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + parts = req.path_info.split('/') + # format: /limiter// + if req.method != 'POST': + raise webob.exc.HTTPMethodNotAllowed() + if len(parts) != 4 or parts[1] != 'limiter': + raise webob.exc.HTTPNotFound() + username = parts[2] + action_name = urllib.unquote(parts[3]) + delay = self.limiter.perform(action_name, username) + if delay: + return webob.exc.HTTPForbidden( + headers={'X-Wait-Seconds': "%.2f" % delay}) + else: + # 200 OK + return '' + + +class WSGIAppProxy(object): + + """Limiter lookalike that proxies to a ratelimiting.WSGIApp.""" + + def __init__(self, service_host): + """Creates a proxy pointing to a ratelimiting.WSGIApp at the given + host.""" + self.service_host = service_host + + def perform(self, action, username='nobody'): + conn = httplib.HTTPConnection(self.service_host) + conn.request('POST', '/limiter/%s/%s' % (username, action)) + resp = conn.getresponse() + if resp.status == 200: + # No delay + return None + return float(resp.getheader('X-Wait-Seconds')) diff --git a/nova/api/openstack/compute/schemas/atom-link.rng b/nova/api/openstack/compute/schemas/atom-link.rng new file mode 100644 index 000000000..edba5eee6 --- /dev/null +++ b/nova/api/openstack/compute/schemas/atom-link.rng @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + [^:]* + + + + + + .+/.+ + + + + + + [A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})* + + + + + + + + + + + + xml:base + xml:lang + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/atom.rng b/nova/api/openstack/compute/schemas/atom.rng new file mode 100644 index 000000000..c2df4e410 --- /dev/null +++ b/nova/api/openstack/compute/schemas/atom.rng @@ -0,0 +1,597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text + html + + + + + + + + + xhtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An atom:feed must have an atom:author unless all of its atom:entry children have an atom:author. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An atom:entry must have at least one atom:link element with a rel attribute of 'alternate' or an atom:content. + + + An atom:entry must have an atom:author if its feed does not. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text + html + + + + + + + + + + + + + xhtml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + [^:]* + + + + + + .+/.+ + + + + + + [A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})* + + + + + + + + + + .+@.+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + xml:base + xml:lang + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/addresses.rng b/nova/api/openstack/compute/schemas/v1.1/addresses.rng new file mode 100644 index 000000000..b498e8a63 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/addresses.rng @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/extension.rng b/nova/api/openstack/compute/schemas/v1.1/extension.rng new file mode 100644 index 000000000..336659755 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/extension.rng @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/extensions.rng b/nova/api/openstack/compute/schemas/v1.1/extensions.rng new file mode 100644 index 000000000..4d8bff646 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/extensions.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/flavor.rng b/nova/api/openstack/compute/schemas/v1.1/flavor.rng new file mode 100644 index 000000000..08746ce3d --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/flavor.rng @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/flavors.rng b/nova/api/openstack/compute/schemas/v1.1/flavors.rng new file mode 100644 index 000000000..b7a3acc01 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/flavors.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/flavors_index.rng b/nova/api/openstack/compute/schemas/v1.1/flavors_index.rng new file mode 100644 index 000000000..d1a4fedb1 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/flavors_index.rng @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/image.rng b/nova/api/openstack/compute/schemas/v1.1/image.rng new file mode 100644 index 000000000..505081fba --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/image.rng @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/images.rng b/nova/api/openstack/compute/schemas/v1.1/images.rng new file mode 100644 index 000000000..064d4d9cc --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/images.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/images_index.rng b/nova/api/openstack/compute/schemas/v1.1/images_index.rng new file mode 100644 index 000000000..3db0b2672 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/images_index.rng @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/limits.rng b/nova/api/openstack/compute/schemas/v1.1/limits.rng new file mode 100644 index 000000000..1af8108ec --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/limits.rng @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/metadata.rng b/nova/api/openstack/compute/schemas/v1.1/metadata.rng new file mode 100644 index 000000000..b2f5d702a --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/metadata.rng @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/server.rng b/nova/api/openstack/compute/schemas/v1.1/server.rng new file mode 100644 index 000000000..07fa16daa --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/server.rng @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/servers.rng b/nova/api/openstack/compute/schemas/v1.1/servers.rng new file mode 100644 index 000000000..4e2bb8853 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/servers.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/servers_index.rng b/nova/api/openstack/compute/schemas/v1.1/servers_index.rng new file mode 100644 index 000000000..023e4b66a --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/servers_index.rng @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/version.rng b/nova/api/openstack/compute/schemas/v1.1/version.rng new file mode 100644 index 000000000..ae76270ba --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/version.rng @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/compute/schemas/v1.1/versions.rng b/nova/api/openstack/compute/schemas/v1.1/versions.rng new file mode 100644 index 000000000..8b2cc7f71 --- /dev/null +++ b/nova/api/openstack/compute/schemas/v1.1/versions.rng @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/nova/api/openstack/compute/server_metadata.py b/nova/api/openstack/compute/server_metadata.py new file mode 100644 index 000000000..52a90f96e --- /dev/null +++ b/nova/api/openstack/compute/server_metadata.py @@ -0,0 +1,175 @@ +# 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.api.openstack import common +from nova.api.openstack import wsgi +from nova import compute +from nova import exception + + +class Controller(object): + """ 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): + try: + server = self.compute_api.get(context, server_id) + meta = self.compute_api.get_instance_metadata(context, server) + except exception.InstanceNotFound: + msg = _('Server does not exist') + raise exc.HTTPNotFound(explanation=msg) + + meta_dict = {} + for key, value in meta.iteritems(): + meta_dict[key] = value + return meta_dict + + @wsgi.serializers(xml=common.MetadataTemplate) + def index(self, req, server_id): + """ Returns the list of metadata for a given instance """ + context = req.environ['nova.context'] + return {'metadata': self._get_metadata(context, server_id)} + + @wsgi.serializers(xml=common.MetadataTemplate) + @wsgi.deserializers(xml=common.MetadataDeserializer) + def create(self, req, server_id, body): + try: + metadata = body['metadata'] + except (KeyError, TypeError): + msg = _("Malformed request body") + raise exc.HTTPBadRequest(explanation=msg) + + context = req.environ['nova.context'] + + new_metadata = self._update_instance_metadata(context, + server_id, + metadata, + delete=False) + + return {'metadata': new_metadata} + + @wsgi.serializers(xml=common.MetaItemTemplate) + @wsgi.deserializers(xml=common.MetaItemDeserializer) + def update(self, req, server_id, id, body): + try: + meta_item = body['meta'] + except (TypeError, KeyError): + expl = _('Malformed request body') + raise exc.HTTPBadRequest(explanation=expl) + + try: + meta_value = meta_item[id] + except (AttributeError, KeyError): + expl = _('Request body and URI mismatch') + raise exc.HTTPBadRequest(explanation=expl) + + if len(meta_item) > 1: + expl = _('Request body contains too many items') + raise exc.HTTPBadRequest(explanation=expl) + + context = req.environ['nova.context'] + self._update_instance_metadata(context, + server_id, + meta_item, + delete=False) + + return {'meta': meta_item} + + @wsgi.serializers(xml=common.MetadataTemplate) + @wsgi.deserializers(xml=common.MetadataDeserializer) + def update_all(self, req, server_id, body): + try: + metadata = body['metadata'] + except (TypeError, KeyError): + expl = _('Malformed request body') + raise exc.HTTPBadRequest(explanation=expl) + + context = req.environ['nova.context'] + new_metadata = self._update_instance_metadata(context, + server_id, + metadata, + delete=True) + + return {'metadata': new_metadata} + + def _update_instance_metadata(self, context, server_id, metadata, + delete=False): + try: + server = self.compute_api.get(context, server_id) + return self.compute_api.update_instance_metadata(context, + server, + metadata, + delete) + + except exception.InstanceNotFound: + msg = _('Server does not exist') + raise exc.HTTPNotFound(explanation=msg) + + except (ValueError, AttributeError): + msg = _("Malformed request body") + raise exc.HTTPBadRequest(explanation=msg) + + except exception.QuotaError as error: + self._handle_quota_error(error) + + @wsgi.serializers(xml=common.MetaItemTemplate) + def show(self, req, server_id, id): + """ Return a single metadata item """ + context = req.environ['nova.context'] + data = self._get_metadata(context, server_id) + + try: + return {'meta': {id: data[id]}} + except KeyError: + msg = _("Metadata item was not found") + raise exc.HTTPNotFound(explanation=msg) + + @wsgi.response(204) + def delete(self, req, server_id, id): + """ Deletes an existing metadata """ + context = req.environ['nova.context'] + + metadata = self._get_metadata(context, server_id) + + try: + meta_value = metadata[id] + except KeyError: + msg = _("Metadata item was not found") + raise exc.HTTPNotFound(explanation=msg) + + try: + server = self.compute_api.get(context, server_id) + self.compute_api.delete_instance_metadata(context, server, id) + except exception.InstanceNotFound: + msg = _('Server does not exist') + raise exc.HTTPNotFound(explanation=msg) + + def _handle_quota_error(self, error): + """Reraise quota errors as api-specific http exceptions.""" + if error.code == "MetadataLimitExceeded": + raise exc.HTTPRequestEntityTooLarge(explanation=error.message, + headers={'Retry-After': 0}) + raise error + + +def create_resource(): + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py new file mode 100644 index 000000000..60b85f591 --- /dev/null +++ b/nova/api/openstack/compute/servers.py @@ -0,0 +1,1123 @@ +# Copyright 2010 OpenStack LLC. +# Copyright 2011 Piston Cloud Computing, Inc +# 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 base64 +import os +from xml.dom import minidom + +from webob import exc +import webob + +from nova.api.openstack import common +from nova.api.openstack.compute import ips +from nova.api.openstack.compute.views import servers as views_servers +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova.compute import instance_types +from nova import exception +from nova import flags +from nova import log as logging +from nova.rpc import common as rpc_common +from nova.scheduler import api as scheduler_api +from nova import utils + + +LOG = logging.getLogger('nova.api.openstack.compute.servers') +FLAGS = flags.FLAGS + + +class SecurityGroupsTemplateElement(xmlutil.TemplateElement): + def will_render(self, datum): + return 'security_groups' in datum + + +def make_fault(elem): + fault = xmlutil.SubTemplateElement(elem, 'fault', selector='fault') + fault.set('code') + fault.set('created') + msg = xmlutil.SubTemplateElement(fault, 'message') + msg.text = 'message' + det = xmlutil.SubTemplateElement(fault, 'details') + det.text = 'details' + + +def make_server(elem, detailed=False): + elem.set('name') + elem.set('id') + + if detailed: + elem.set('userId', 'user_id') + elem.set('tenantId', 'tenant_id') + elem.set('updated') + elem.set('created') + elem.set('hostId') + elem.set('accessIPv4') + elem.set('accessIPv6') + elem.set('status') + elem.set('progress') + + # Attach image node + image = xmlutil.SubTemplateElement(elem, 'image', selector='image') + image.set('id') + xmlutil.make_links(image, 'links') + + # Attach flavor node + flavor = xmlutil.SubTemplateElement(elem, 'flavor', selector='flavor') + flavor.set('id') + xmlutil.make_links(flavor, 'links') + + # Attach fault node + make_fault(elem) + + # Attach metadata node + elem.append(common.MetadataTemplate()) + + # Attach addresses node + elem.append(ips.AddressesTemplate()) + + # Attach security groups node + secgrps = SecurityGroupsTemplateElement('security_groups') + elem.append(secgrps) + secgrp = xmlutil.SubTemplateElement(secgrps, 'security_group', + selector='security_groups') + secgrp.set('name') + + xmlutil.make_links(elem, 'links') + + +server_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class ServerTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server', selector='server') + make_server(root, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) + + +class MinimalServersTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('servers') + elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') + make_server(elem) + xmlutil.make_links(root, 'servers_links') + return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) + + +class ServersTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('servers') + elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') + make_server(elem, detailed=True) + return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) + + +class ServerAdminPassTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server') + root.set('adminPass') + return xmlutil.SlaveTemplate(root, 1, nsmap=server_nsmap) + + +def FullServerTemplate(): + master = ServerTemplate() + master.attach(ServerAdminPassTemplate()) + return master + + +class CommonDeserializer(wsgi.MetadataXMLDeserializer): + """ + Common deserializer to handle xml-formatted server create + requests. + + Handles standard server attributes as well as optional metadata + and personality attributes + """ + + metadata_deserializer = common.MetadataXMLDeserializer() + + def _extract_personality(self, server_node): + """Marshal the personality attribute of a parsed request""" + node = self.find_first_child_named(server_node, "personality") + if node is not None: + personality = [] + for file_node in self.find_children_named(node, "file"): + item = {} + if file_node.hasAttribute("path"): + item["path"] = file_node.getAttribute("path") + item["contents"] = self.extract_text(file_node) + personality.append(item) + return personality + else: + return None + + def _extract_server(self, node): + """Marshal the server attribute of a parsed request""" + server = {} + server_node = self.find_first_child_named(node, 'server') + + attributes = ["name", "imageRef", "flavorRef", "adminPass", + "accessIPv4", "accessIPv6"] + for attr in attributes: + if server_node.getAttribute(attr): + server[attr] = server_node.getAttribute(attr) + + metadata_node = self.find_first_child_named(server_node, "metadata") + if metadata_node is not None: + server["metadata"] = self.extract_metadata(metadata_node) + + personality = self._extract_personality(server_node) + if personality is not None: + server["personality"] = personality + + networks = self._extract_networks(server_node) + if networks is not None: + server["networks"] = networks + + security_groups = self._extract_security_groups(server_node) + if security_groups is not None: + server["security_groups"] = security_groups + + auto_disk_config = server_node.getAttribute('auto_disk_config') + if auto_disk_config: + server['auto_disk_config'] = utils.bool_from_str(auto_disk_config) + + return server + + def _extract_networks(self, server_node): + """Marshal the networks attribute of a parsed request""" + node = self.find_first_child_named(server_node, "networks") + if node is not None: + networks = [] + for network_node in self.find_children_named(node, + "network"): + item = {} + if network_node.hasAttribute("uuid"): + item["uuid"] = network_node.getAttribute("uuid") + if network_node.hasAttribute("fixed_ip"): + item["fixed_ip"] = network_node.getAttribute("fixed_ip") + networks.append(item) + return networks + else: + return None + + def _extract_security_groups(self, server_node): + """Marshal the security_groups attribute of a parsed request""" + node = self.find_first_child_named(server_node, "security_groups") + if node is not None: + security_groups = [] + for sg_node in self.find_children_named(node, "security_group"): + item = {} + name_node = self.find_first_child_named(sg_node, "name") + if name_node: + item["name"] = self.extract_text(name_node) + security_groups.append(item) + return security_groups + else: + return None + + +class ActionDeserializer(CommonDeserializer): + """ + Deserializer to handle xml-formatted server action requests. + + Handles standard server attributes as well as optional metadata + and personality attributes + """ + + def default(self, string): + dom = minidom.parseString(string) + action_node = dom.childNodes[0] + action_name = action_node.tagName + + action_deserializer = { + 'createImage': self._action_create_image, + 'changePassword': self._action_change_password, + 'reboot': self._action_reboot, + 'rebuild': self._action_rebuild, + 'resize': self._action_resize, + 'confirmResize': self._action_confirm_resize, + 'revertResize': self._action_revert_resize, + }.get(action_name, super(ActionDeserializer, self).default) + + action_data = action_deserializer(action_node) + + return {'body': {action_name: action_data}} + + def _action_create_image(self, node): + return self._deserialize_image_action(node, ('name',)) + + def _action_change_password(self, node): + if not node.hasAttribute("adminPass"): + raise AttributeError("No adminPass was specified in request") + return {"adminPass": node.getAttribute("adminPass")} + + def _action_reboot(self, node): + if not node.hasAttribute("type"): + raise AttributeError("No reboot type was specified in request") + return {"type": node.getAttribute("type")} + + def _action_rebuild(self, node): + rebuild = {} + if node.hasAttribute("name"): + rebuild['name'] = node.getAttribute("name") + + metadata_node = self.find_first_child_named(node, "metadata") + if metadata_node is not None: + rebuild["metadata"] = self.extract_metadata(metadata_node) + + personality = self._extract_personality(node) + if personality is not None: + rebuild["personality"] = personality + + if not node.hasAttribute("imageRef"): + raise AttributeError("No imageRef was specified in request") + rebuild["imageRef"] = node.getAttribute("imageRef") + + return rebuild + + def _action_resize(self, node): + if not node.hasAttribute("flavorRef"): + raise AttributeError("No flavorRef was specified in request") + return {"flavorRef": node.getAttribute("flavorRef")} + + def _action_confirm_resize(self, node): + return None + + def _action_revert_resize(self, node): + return None + + def _deserialize_image_action(self, node, allowed_attributes): + data = {} + for attribute in allowed_attributes: + value = node.getAttribute(attribute) + if value: + data[attribute] = value + metadata_node = self.find_first_child_named(node, 'metadata') + if metadata_node is not None: + metadata = self.metadata_deserializer.extract_metadata( + metadata_node) + data['metadata'] = metadata + return data + + +class CreateDeserializer(CommonDeserializer): + """ + Deserializer to handle xml-formatted server create requests. + + Handles standard server attributes as well as optional metadata + and personality attributes + """ + + def default(self, string): + """Deserialize an xml-formatted server create request""" + dom = minidom.parseString(string) + server = self._extract_server(dom) + return {'body': {'server': server}} + + +class Controller(wsgi.Controller): + """ The Server API base controller class for the OpenStack API """ + + _view_builder_class = views_servers.ViewBuilder + + @staticmethod + def _add_location(robj): + # Just in case... + if 'server' not in robj.obj: + return robj + + link = filter(lambda l: l['rel'] == 'self', + robj.obj['server']['links']) + if link: + robj['Location'] = link[0]['href'] + + # Convenience return + return robj + + def __init__(self, **kwargs): + super(Controller, self).__init__(**kwargs) + self.compute_api = compute.API() + + @wsgi.serializers(xml=MinimalServersTemplate) + def index(self, req): + """ Returns a list of server names and ids for a given user """ + try: + servers = self._get_servers(req, is_detail=False) + except exception.Invalid as err: + raise exc.HTTPBadRequest(explanation=str(err)) + except exception.NotFound: + raise exc.HTTPNotFound() + return servers + + @wsgi.serializers(xml=ServersTemplate) + def detail(self, req): + """ Returns a list of server details for a given user """ + try: + servers = self._get_servers(req, is_detail=True) + except exception.Invalid as err: + raise exc.HTTPBadRequest(explanation=str(err)) + except exception.NotFound as err: + raise exc.HTTPNotFound() + return servers + + def _get_block_device_mapping(self, data): + """Get block_device_mapping from 'server' dictionary. + Overridden by volumes controller. + """ + return None + + def _add_instance_faults(self, ctxt, instances): + faults = self.compute_api.get_instance_faults(ctxt, instances) + if faults is not None: + for instance in instances: + faults_list = faults.get(instance['uuid'], []) + try: + instance['fault'] = faults_list[0] + except IndexError: + pass + + return instances + + def _get_servers(self, req, is_detail): + """Returns a list of servers, taking into account any search + options specified. + """ + + search_opts = {} + search_opts.update(req.str_GET) + + context = req.environ['nova.context'] + remove_invalid_options(context, search_opts, + self._get_server_search_options()) + + # Convert local_zone_only into a boolean + search_opts['local_zone_only'] = utils.bool_from_str( + search_opts.get('local_zone_only', False)) + + # If search by 'status', we need to convert it to 'vm_state' + # to pass on to child zones. + if 'status' in search_opts: + status = search_opts['status'] + state = common.vm_state_from_status(status) + if state is None: + reason = _('Invalid server status: %(status)s') % locals() + raise exception.InvalidInput(reason=reason) + search_opts['vm_state'] = state + + if 'changes-since' in search_opts: + try: + parsed = utils.parse_isotime(search_opts['changes-since']) + except ValueError: + msg = _('Invalid changes-since value') + raise exc.HTTPBadRequest(explanation=msg) + search_opts['changes-since'] = parsed + + # By default, compute's get_all() will return deleted instances. + # If an admin hasn't specified a 'deleted' search option, we need + # to filter out deleted instances by setting the filter ourselves. + # ... Unless 'changes-since' is specified, because 'changes-since' + # should return recently deleted images according to the API spec. + + if 'deleted' not in search_opts: + if 'changes-since' not in search_opts: + # No 'changes-since', so we only want non-deleted servers + search_opts['deleted'] = False + + instance_list = self.compute_api.get_all(context, + search_opts=search_opts) + + limited_list = self._limit_items(instance_list, req) + if is_detail: + self._add_instance_faults(context, limited_list) + return self._view_builder.detail(req, limited_list) + else: + return self._view_builder.index(req, limited_list) + + def _get_server(self, context, instance_uuid): + """Utility function for looking up an instance by uuid""" + try: + return self.compute_api.routing_get(context, instance_uuid) + except exception.NotFound: + raise exc.HTTPNotFound() + + def _handle_quota_error(self, error): + """ + Reraise quota errors as api-specific http exceptions + """ + + code_mappings = { + "OnsetFileLimitExceeded": + _("Personality file limit exceeded"), + "OnsetFilePathLimitExceeded": + _("Personality file path too long"), + "OnsetFileContentLimitExceeded": + _("Personality file content too long"), + + # NOTE(bcwaldon): expose the message generated below in order + # to better explain how the quota was exceeded + "InstanceLimitExceeded": error.message, + } + + expl = code_mappings.get(error.code) + if expl: + raise exc.HTTPRequestEntityTooLarge(explanation=expl, + headers={'Retry-After': 0}) + # if the original error is okay, just reraise it + raise error + + def _validate_server_name(self, value): + if not isinstance(value, basestring): + msg = _("Server name is not a string or unicode") + raise exc.HTTPBadRequest(explanation=msg) + + if value.strip() == '': + msg = _("Server name is an empty string") + raise exc.HTTPBadRequest(explanation=msg) + + def _get_injected_files(self, personality): + """ + Create a list of injected files from the personality attribute + + At this time, injected_files must be formatted as a list of + (file_path, file_content) pairs for compatibility with the + underlying compute service. + """ + injected_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: + expl = _('Bad personality format') + raise exc.HTTPBadRequest(explanation=expl) + try: + contents = base64.b64decode(contents) + except TypeError: + expl = _('Personality content for %s cannot be decoded') % path + raise exc.HTTPBadRequest(explanation=expl) + injected_files.append((path, contents)) + return injected_files + + def _get_requested_networks(self, requested_networks): + """ + Create a list of requested networks from the networks attribute + """ + networks = [] + for network in requested_networks: + try: + network_uuid = network['uuid'] + + if not utils.is_uuid_like(network_uuid): + msg = _("Bad networks format: network uuid is not in" + " proper format (%s)") % network_uuid + raise exc.HTTPBadRequest(explanation=msg) + + #fixed IP address is optional + #if the fixed IP address is not provided then + #it will use one of the available IP address from the network + address = network.get('fixed_ip', None) + if address is not None and not utils.is_valid_ipv4(address): + msg = _("Invalid fixed IP address (%s)") % address + raise exc.HTTPBadRequest(explanation=msg) + # check if the network id is already present in the list, + # we don't want duplicate networks to be passed + # at the boot time + for id, ip in networks: + if id == network_uuid: + expl = _("Duplicate networks (%s) are not allowed")\ + % network_uuid + raise exc.HTTPBadRequest(explanation=expl) + + networks.append((network_uuid, address)) + except KeyError as key: + expl = _('Bad network format: missing %s') % key + raise exc.HTTPBadRequest(explanation=expl) + except TypeError: + expl = _('Bad networks format') + raise exc.HTTPBadRequest(explanation=expl) + + return networks + + def _validate_user_data(self, user_data): + """Check if the user_data is encoded properly""" + if not user_data: + return + try: + user_data = base64.b64decode(user_data) + except TypeError: + expl = _('Userdata content cannot be decoded') + raise exc.HTTPBadRequest(explanation=expl) + + @wsgi.serializers(xml=ServerTemplate) + @exception.novaclient_converter + @scheduler_api.redirect_handler + def show(self, req, id): + """ Returns server details by server id """ + try: + context = req.environ['nova.context'] + instance = self.compute_api.routing_get(context, id) + self._add_instance_faults(context, [instance]) + return self._view_builder.show(req, instance) + except exception.NotFound: + raise exc.HTTPNotFound() + + @wsgi.response(202) + @wsgi.serializers(xml=FullServerTemplate) + @wsgi.deserializers(xml=CreateDeserializer) + def create(self, req, body): + """ Creates a new server for a given user """ + if not body: + raise exc.HTTPUnprocessableEntity() + + if not 'server' in body: + raise exc.HTTPUnprocessableEntity() + + body['server']['key_name'] = self._get_key_name(req, body) + + context = req.environ['nova.context'] + server_dict = body['server'] + password = self._get_server_admin_password(server_dict) + + if not 'name' in server_dict: + msg = _("Server name is not defined") + raise exc.HTTPBadRequest(explanation=msg) + + name = server_dict['name'] + self._validate_server_name(name) + name = name.strip() + + image_href = self._image_ref_from_req_data(body) + + # If the image href was generated by nova api, strip image_href + # down to an id and use the default glance connection params + if str(image_href).startswith(req.application_url): + image_href = image_href.split('/').pop() + + personality = server_dict.get('personality') + config_drive = server_dict.get('config_drive') + + injected_files = [] + if personality: + injected_files = self._get_injected_files(personality) + + sg_names = [] + security_groups = server_dict.get('security_groups') + if security_groups is not None: + sg_names = [sg['name'] for sg in security_groups if sg.get('name')] + if not sg_names: + sg_names.append('default') + + sg_names = list(set(sg_names)) + + requested_networks = server_dict.get('networks') + if requested_networks is not None: + requested_networks = self._get_requested_networks( + requested_networks) + + try: + flavor_id = self._flavor_id_from_req_data(body) + except ValueError as error: + msg = _("Invalid flavorRef provided.") + raise exc.HTTPBadRequest(explanation=msg) + + zone_blob = server_dict.get('blob') + + # optional openstack extensions: + key_name = server_dict.get('key_name') + user_data = server_dict.get('user_data') + self._validate_user_data(user_data) + + availability_zone = server_dict.get('availability_zone') + name = server_dict['name'] + self._validate_server_name(name) + name = name.strip() + + block_device_mapping = self._get_block_device_mapping(server_dict) + + # Only allow admins to specify their own reservation_ids + # This is really meant to allow zones to work. + reservation_id = server_dict.get('reservation_id') + if all([reservation_id is not None, + reservation_id != '', + not context.is_admin]): + reservation_id = None + + ret_resv_id = server_dict.get('return_reservation_id', False) + + min_count = server_dict.get('min_count') + max_count = server_dict.get('max_count') + # min_count and max_count are optional. If they exist, they come + # in as strings. We want to default 'min_count' to 1, and default + # 'max_count' to be 'min_count'. + min_count = int(min_count) if min_count else 1 + max_count = int(max_count) if max_count else min_count + if min_count > max_count: + min_count = max_count + + auto_disk_config = server_dict.get('auto_disk_config') + + try: + inst_type = \ + instance_types.get_instance_type_by_flavor_id(flavor_id) + + (instances, resv_id) = self.compute_api.create(context, + inst_type, + image_href, + display_name=name, + display_description=name, + key_name=key_name, + metadata=server_dict.get('metadata', {}), + access_ip_v4=server_dict.get('accessIPv4'), + access_ip_v6=server_dict.get('accessIPv6'), + injected_files=injected_files, + admin_password=password, + zone_blob=zone_blob, + reservation_id=reservation_id, + min_count=min_count, + max_count=max_count, + requested_networks=requested_networks, + security_group=sg_names, + user_data=user_data, + availability_zone=availability_zone, + config_drive=config_drive, + block_device_mapping=block_device_mapping, + auto_disk_config=auto_disk_config) + except exception.QuotaError as error: + self._handle_quota_error(error) + except exception.InstanceTypeMemoryTooSmall as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) + except exception.InstanceTypeDiskTooSmall as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) + except exception.ImageNotFound as error: + msg = _("Can not find requested image") + raise exc.HTTPBadRequest(explanation=msg) + except exception.FlavorNotFound as error: + msg = _("Invalid flavorRef provided.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.KeypairNotFound as error: + msg = _("Invalid key_name provided.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.SecurityGroupNotFound as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) + except rpc_common.RemoteError as err: + msg = "%(err_type)s: %(err_msg)s" % \ + {'err_type': err.exc_type, 'err_msg': err.value} + raise exc.HTTPBadRequest(explanation=msg) + # Let the caller deal with unhandled exceptions. + + # If the caller wanted a reservation_id, return it + if ret_resv_id: + return {'reservation_id': resv_id} + + server = self._view_builder.create(req, instances[0]) + + if '_is_precooked' in server['server'].keys(): + del server['server']['_is_precooked'] + else: + server['server']['adminPass'] = password + + robj = wsgi.ResponseObject(server) + + return self._add_location(robj) + + def _delete(self, context, id): + instance = self._get_server(context, id) + if FLAGS.reclaim_instance_interval: + self.compute_api.soft_delete(context, instance) + else: + self.compute_api.delete(context, instance) + + @wsgi.serializers(xml=ServerTemplate) + @scheduler_api.redirect_handler + def update(self, req, id, body): + """Update server then pass on to version-specific controller""" + if len(req.body) == 0: + raise exc.HTTPUnprocessableEntity() + + if not body: + raise exc.HTTPUnprocessableEntity() + + ctxt = req.environ['nova.context'] + update_dict = {} + + if 'name' in body['server']: + name = body['server']['name'] + self._validate_server_name(name) + update_dict['display_name'] = name.strip() + + if 'accessIPv4' in body['server']: + access_ipv4 = body['server']['accessIPv4'] + update_dict['access_ip_v4'] = access_ipv4.strip() + + if 'accessIPv6' in body['server']: + access_ipv6 = body['server']['accessIPv6'] + update_dict['access_ip_v6'] = access_ipv6.strip() + + if 'auto_disk_config' in body['server']: + auto_disk_config = utils.bool_from_str( + body['server']['auto_disk_config']) + update_dict['auto_disk_config'] = auto_disk_config + + instance = self.compute_api.routing_get(ctxt, id) + + try: + self.compute_api.update(ctxt, instance, **update_dict) + except exception.NotFound: + raise exc.HTTPNotFound() + + instance.update(update_dict) + + self._add_instance_faults(ctxt, [instance]) + return self._view_builder.show(req, instance) + + @wsgi.response(202) + @wsgi.serializers(xml=FullServerTemplate) + @wsgi.deserializers(xml=ActionDeserializer) + @exception.novaclient_converter + @scheduler_api.redirect_handler + def action(self, req, id, body): + """Multi-purpose method used to take actions on a server""" + _actions = { + 'changePassword': self._action_change_password, + 'reboot': self._action_reboot, + 'resize': self._action_resize, + 'confirmResize': self._action_confirm_resize, + 'revertResize': self._action_revert_resize, + 'rebuild': self._action_rebuild, + 'createImage': self._action_create_image, + } + + for key in body: + if key in _actions: + return _actions[key](body, req, id) + else: + msg = _("There is no such server action: %s") % (key,) + raise exc.HTTPBadRequest(explanation=msg) + + msg = _("Invalid request body") + raise exc.HTTPBadRequest(explanation=msg) + + def _action_confirm_resize(self, input_dict, req, id): + context = req.environ['nova.context'] + instance = self._get_server(context, id) + try: + self.compute_api.confirm_resize(context, instance) + except exception.MigrationNotFound: + msg = _("Instance has not been resized.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'confirmResize') + except Exception, e: + LOG.exception(_("Error in confirm-resize %s"), e) + raise exc.HTTPBadRequest() + return exc.HTTPNoContent() + + def _action_revert_resize(self, input_dict, req, id): + context = req.environ['nova.context'] + instance = self._get_server(context, id) + try: + self.compute_api.revert_resize(context, instance) + except exception.MigrationNotFound: + msg = _("Instance has not been resized.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'revertResize') + except Exception, e: + LOG.exception(_("Error in revert-resize %s"), e) + raise exc.HTTPBadRequest() + return webob.Response(status_int=202) + + def _action_reboot(self, input_dict, req, id): + if 'reboot' in input_dict and 'type' in input_dict['reboot']: + valid_reboot_types = ['HARD', 'SOFT'] + reboot_type = input_dict['reboot']['type'].upper() + if not valid_reboot_types.count(reboot_type): + msg = _("Argument 'type' for reboot is not HARD or SOFT") + LOG.exception(msg) + raise exc.HTTPBadRequest(explanation=msg) + else: + msg = _("Missing argument 'type' for reboot") + LOG.exception(msg) + raise exc.HTTPBadRequest(explanation=msg) + + context = req.environ['nova.context'] + instance = self._get_server(context, id) + + try: + self.compute_api.reboot(context, instance, reboot_type) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'reboot') + except Exception, e: + LOG.exception(_("Error in reboot %s"), e) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + def _resize(self, req, instance_id, flavor_id): + """Begin the resize process with given instance/flavor.""" + context = req.environ["nova.context"] + instance = self._get_server(context, instance_id) + + try: + self.compute_api.resize(context, instance, flavor_id) + except exception.FlavorNotFound: + msg = _("Unable to locate requested flavor.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.CannotResizeToSameSize: + msg = _("Resize requires a change in size.") + raise exc.HTTPBadRequest(explanation=msg) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'resize') + + return webob.Response(status_int=202) + + @wsgi.response(204) + @exception.novaclient_converter + @scheduler_api.redirect_handler + def delete(self, req, id): + """ Destroys a server """ + try: + self._delete(req.environ['nova.context'], id) + except exception.NotFound: + raise exc.HTTPNotFound() + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'delete') + + def _get_key_name(self, req, body): + if 'server' in body: + try: + return body['server'].get('key_name') + except AttributeError: + msg = _("Malformed server entity") + raise exc.HTTPBadRequest(explanation=msg) + + def _image_ref_from_req_data(self, data): + try: + return data['server']['imageRef'] + except (TypeError, KeyError): + msg = _("Missing imageRef attribute") + raise exc.HTTPBadRequest(explanation=msg) + + def _flavor_id_from_req_data(self, data): + try: + flavor_ref = data['server']['flavorRef'] + except (TypeError, KeyError): + msg = _("Missing flavorRef attribute") + raise exc.HTTPBadRequest(explanation=msg) + + return common.get_id_from_href(flavor_ref) + + 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']): + msg = _("No adminPass was specified") + raise exc.HTTPBadRequest(explanation=msg) + password = input_dict['changePassword']['adminPass'] + if not isinstance(password, basestring) or password == '': + msg = _("Invalid adminPass") + raise exc.HTTPBadRequest(explanation=msg) + server = self._get_server(context, id) + self.compute_api.set_admin_password(context, server, password) + return webob.Response(status_int=202) + + def _limit_items(self, items, req): + return common.limited_by_marker(items, req) + + def _validate_metadata(self, metadata): + """Ensure that we can work with the metadata given.""" + try: + metadata.iteritems() + except AttributeError as ex: + msg = _("Unable to parse metadata key/value pairs.") + LOG.debug(msg) + raise exc.HTTPBadRequest(explanation=msg) + + def _action_resize(self, input_dict, req, id): + """ Resizes a given instance to the flavor size requested """ + try: + flavor_ref = input_dict["resize"]["flavorRef"] + if not flavor_ref: + msg = _("Resize request has invalid 'flavorRef' attribute.") + raise exc.HTTPBadRequest(explanation=msg) + except (KeyError, TypeError): + msg = _("Resize requests require 'flavorRef' attribute.") + raise exc.HTTPBadRequest(explanation=msg) + + return self._resize(req, id, flavor_ref) + + def _action_rebuild(self, info, request, instance_id): + """Rebuild an instance with the given attributes""" + try: + body = info['rebuild'] + except (KeyError, TypeError): + raise exc.HTTPBadRequest(_("Invalid request body")) + + try: + image_href = body["imageRef"] + except (KeyError, TypeError): + msg = _("Could not parse imageRef from request.") + raise exc.HTTPBadRequest(explanation=msg) + + try: + password = body['adminPass'] + except (KeyError, TypeError): + password = utils.generate_password(FLAGS.password_length) + + context = request.environ['nova.context'] + instance = self._get_server(context, instance_id) + + attr_map = { + 'personality': 'files_to_inject', + 'name': 'display_name', + 'accessIPv4': 'access_ip_v4', + 'accessIPv6': 'access_ip_v6', + 'metadata': 'metadata', + } + + kwargs = {} + + for request_attribute, instance_attribute in attr_map.items(): + try: + kwargs[instance_attribute] = body[request_attribute] + except (KeyError, TypeError): + pass + + self._validate_metadata(kwargs.get('metadata', {})) + + if 'files_to_inject' in kwargs: + personality = kwargs['files_to_inject'] + kwargs['files_to_inject'] = self._get_injected_files(personality) + + try: + self.compute_api.rebuild(context, + instance, + image_href, + password, + **kwargs) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'rebuild') + except exception.InstanceNotFound: + msg = _("Instance could not be found") + raise exc.HTTPNotFound(explanation=msg) + + instance = self._get_server(context, instance_id) + + self._add_instance_faults(context, [instance]) + view = self._view_builder.show(request, instance) + + # Add on the adminPass attribute since the view doesn't do it + view['server']['adminPass'] = password + + robj = wsgi.ResponseObject(view) + return self._add_location(robj) + + @common.check_snapshots_enabled + def _action_create_image(self, input_dict, req, instance_id): + """Snapshot a server instance.""" + context = req.environ['nova.context'] + entity = input_dict.get("createImage", {}) + + try: + image_name = entity["name"] + + except KeyError: + msg = _("createImage entity requires name attribute") + raise exc.HTTPBadRequest(explanation=msg) + + except TypeError: + msg = _("Malformed createImage entity") + raise exc.HTTPBadRequest(explanation=msg) + + # preserve link to server in image properties + server_ref = os.path.join(req.application_url, 'servers', instance_id) + props = {'instance_ref': server_ref} + + metadata = entity.get('metadata', {}) + common.check_img_metadata_quota_limit(context, metadata) + try: + props.update(metadata) + except ValueError: + msg = _("Invalid metadata") + raise exc.HTTPBadRequest(explanation=msg) + + instance = self._get_server(context, instance_id) + + try: + image = self.compute_api.snapshot(context, + instance, + image_name, + extra_properties=props) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'createImage') + + # build location of newly-created image entity + image_id = str(image['id']) + image_ref = os.path.join(req.application_url, + context.project_id, + 'images', + image_id) + + resp = webob.Response(status_int=202) + resp.headers['Location'] = image_ref + return resp + + def _get_server_admin_password(self, server): + """ Determine the admin password for a server on creation """ + password = server.get('adminPass') + + if password is None: + return utils.generate_password(FLAGS.password_length) + if not isinstance(password, basestring) or password == '': + msg = _("Invalid adminPass") + raise exc.HTTPBadRequest(explanation=msg) + return password + + def _get_server_search_options(self): + """Return server search options allowed by non-admin""" + return ('reservation_id', 'name', 'local_zone_only', + 'status', 'image', 'flavor', 'changes-since') + + +def create_resource(): + return wsgi.Resource(Controller()) + + +def remove_invalid_options(context, search_options, allowed_search_options): + """Remove search options that are not valid for non-admin API/context""" + if FLAGS.allow_admin_api and context.is_admin: + # Allow all options + return + # Otherwise, strip out all unknown options + unknown_options = [opt for opt in search_options + if opt not in allowed_search_options] + unk_opt_str = ", ".join(unknown_options) + log_msg = _("Removing options '%(unk_opt_str)s' from query") % locals() + LOG.debug(log_msg) + for opt in unknown_options: + search_options.pop(opt, None) diff --git a/nova/api/openstack/compute/versions.py b/nova/api/openstack/compute/versions.py new file mode 100644 index 000000000..ddc31e9f0 --- /dev/null +++ b/nova/api/openstack/compute/versions.py @@ -0,0 +1,236 @@ +# 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 datetime import datetime + +from lxml import etree + +from nova.api.openstack.compute.views import versions as views_versions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil + + +VERSIONS = { + "v2.0": { + "id": "v2.0", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/cs-devguide-20110125.pdf", + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml;version=2", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=2", + } + ], + } +} + + +class MediaTypesTemplateElement(xmlutil.TemplateElement): + def will_render(self, datum): + return 'media-types' in datum + + +def make_version(elem): + elem.set('id') + elem.set('status') + elem.set('updated') + + mts = MediaTypesTemplateElement('media-types') + elem.append(mts) + + mt = xmlutil.SubTemplateElement(mts, 'media-type', selector='media-types') + mt.set('base') + mt.set('type') + + xmlutil.make_links(elem, 'links') + + +version_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class VersionTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('version', selector='version') + make_version(root) + return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) + + +class VersionsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('versions') + elem = xmlutil.SubTemplateElement(root, 'version', selector='versions') + make_version(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) + + +class ChoicesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('choices') + elem = xmlutil.SubTemplateElement(root, 'version', selector='choices') + make_version(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) + + +class AtomSerializer(wsgi.XMLDictSerializer): + + NSMAP = {None: xmlutil.XMLNS_ATOM} + + def __init__(self, metadata=None, xmlns=None): + self.metadata = metadata or {} + if not xmlns: + self.xmlns = wsgi.XMLNS_ATOM + else: + self.xmlns = xmlns + + def _get_most_recent_update(self, versions): + recent = None + for version in versions: + updated = datetime.strptime(version['updated'], + '%Y-%m-%dT%H:%M:%SZ') + if not recent: + recent = updated + elif updated > recent: + recent = updated + + return recent.strftime('%Y-%m-%dT%H:%M:%SZ') + + def _get_base_url(self, link_href): + # Make sure no trailing / + link_href = link_href.rstrip('/') + return link_href.rsplit('/', 1)[0] + '/' + + def _create_feed(self, versions, feed_title, feed_id): + feed = etree.Element('feed', nsmap=self.NSMAP) + title = etree.SubElement(feed, 'title') + title.set('type', 'text') + title.text = feed_title + + # Set this updated to the most recently updated version + recent = self._get_most_recent_update(versions) + etree.SubElement(feed, 'updated').text = recent + + etree.SubElement(feed, 'id').text = feed_id + + link = etree.SubElement(feed, 'link') + link.set('rel', 'self') + link.set('href', feed_id) + + author = etree.SubElement(feed, 'author') + etree.SubElement(author, 'name').text = 'Rackspace' + etree.SubElement(author, 'uri').text = 'http://www.rackspace.com/' + + for version in versions: + feed.append(self._create_version_entry(version)) + + return feed + + def _create_version_entry(self, version): + entry = etree.Element('entry') + etree.SubElement(entry, 'id').text = version['links'][0]['href'] + title = etree.SubElement(entry, 'title') + title.set('type', 'text') + title.text = 'Version %s' % version['id'] + etree.SubElement(entry, 'updated').text = version['updated'] + + for link in version['links']: + link_elem = etree.SubElement(entry, 'link') + link_elem.set('rel', link['rel']) + link_elem.set('href', link['href']) + if 'type' in link: + link_elem.set('type', link['type']) + + content = etree.SubElement(entry, 'content') + content.set('type', 'text') + content.text = 'Version %s %s (%s)' % (version['id'], + version['status'], + version['updated']) + return entry + + +class VersionsAtomSerializer(AtomSerializer): + def default(self, data): + versions = data['versions'] + feed_id = self._get_base_url(versions[0]['links'][0]['href']) + feed = self._create_feed(versions, 'Available API Versions', feed_id) + return self._to_xml(feed) + + +class VersionAtomSerializer(AtomSerializer): + def default(self, data): + version = data['version'] + feed_id = version['links'][0]['href'] + feed = self._create_feed([version], 'About This Version', feed_id) + return self._to_xml(feed) + + +class Versions(wsgi.Resource): + def __init__(self): + super(Versions, self).__init__(None) + + @wsgi.serializers(xml=VersionsTemplate, + atom=VersionsAtomSerializer) + def index(self, req): + """Return all versions.""" + builder = views_versions.get_view_builder(req) + return builder.build_versions(VERSIONS) + + @wsgi.serializers(xml=ChoicesTemplate) + @wsgi.response(300) + def multi(self, req): + """Return multiple choices.""" + builder = views_versions.get_view_builder(req) + return builder.build_choices(VERSIONS, req) + + def get_action_args(self, request_environment): + """Parse dictionary created by routes library.""" + args = {} + if request_environment['PATH_INFO'] == '/': + args['action'] = 'index' + else: + args['action'] = 'multi' + + return args + + +class VersionV2(object): + @wsgi.serializers(xml=VersionTemplate, + atom=VersionAtomSerializer) + def show(self, req): + builder = views_versions.get_view_builder(req) + return builder.build_version(VERSIONS['v2.0']) + + +def create_resource(): + return wsgi.Resource(VersionV2()) diff --git a/nova/api/openstack/compute/views/__init__.py b/nova/api/openstack/compute/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nova/api/openstack/compute/views/addresses.py b/nova/api/openstack/compute/views/addresses.py new file mode 100644 index 000000000..776ba9e59 --- /dev/null +++ b/nova/api/openstack/compute/views/addresses.py @@ -0,0 +1,52 @@ +# 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 itertools + +from nova.api.openstack import common +from nova import flags +from nova import log as logging + + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.api.openstack.compute.views.addresses') + + +class ViewBuilder(common.ViewBuilder): + """Models server addresses as a dictionary.""" + + _collection_name = "addresses" + + def basic(self, ip): + """Return a dictionary describing an IP address.""" + return { + "version": ip["version"], + "addr": ip["addr"], + } + + def show(self, network, label): + """Returns a dictionary describing a network.""" + all_ips = itertools.chain(network["ips"], network["floating_ips"]) + return {label: [self.basic(ip) for ip in all_ips]} + + def index(self, networks): + """Return a dictionary describing a list of networks.""" + addresses = {} + for label, network in networks.items(): + network_dict = self.show(network, label) + addresses[label] = network_dict[label] + return dict(addresses=addresses) diff --git a/nova/api/openstack/compute/views/flavors.py b/nova/api/openstack/compute/views/flavors.py new file mode 100644 index 000000000..64284e406 --- /dev/null +++ b/nova/api/openstack/compute/views/flavors.py @@ -0,0 +1,62 @@ +# 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 + + +class ViewBuilder(common.ViewBuilder): + + _collection_name = "flavors" + + def basic(self, request, flavor): + return { + "flavor": { + "id": flavor["flavorid"], + "name": flavor["name"], + "links": self._get_links(request, flavor["flavorid"]), + }, + } + + def show(self, request, flavor): + return { + "flavor": { + "id": flavor["flavorid"], + "name": flavor["name"], + "ram": flavor["memory_mb"], + "disk": flavor["local_gb"], + "vcpus": flavor.get("vcpus") or "", + "swap": flavor.get("swap") or "", + "rxtx_factor": flavor.get("rxtx_factor") or "", + "links": self._get_links(request, flavor["flavorid"]), + }, + } + + def index(self, request, flavors): + """Return the 'index' view of flavors.""" + def _get_flavors(request, flavors): + for _, flavor in flavors.iteritems(): + yield self.basic(request, flavor)["flavor"] + + return dict(flavors=list(_get_flavors(request, flavors))) + + def detail(self, request, flavors): + """Return the 'detail' view of flavors.""" + def _get_flavors(request, flavors): + for _, flavor in flavors.iteritems(): + yield self.show(request, flavor)["flavor"] + + return dict(flavors=list(_get_flavors(request, flavors))) diff --git a/nova/api/openstack/compute/views/images.py b/nova/api/openstack/compute/views/images.py new file mode 100644 index 000000000..c4cfe8031 --- /dev/null +++ b/nova/api/openstack/compute/views/images.py @@ -0,0 +1,139 @@ +# 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 os.path + +from nova.api.openstack import common +from nova import utils + + +class ViewBuilder(common.ViewBuilder): + + _collection_name = "images" + + def basic(self, request, image): + """Return a dictionary with basic image attributes.""" + return { + "image": { + "id": image.get("id"), + "name": image.get("name"), + "links": self._get_links(request, image["id"]), + }, + } + + def show(self, request, image): + """Return a dictionary with image details.""" + image_dict = { + "id": image.get("id"), + "name": image.get("name"), + "minRam": int(image.get("min_ram") or 0), + "minDisk": int(image.get("min_disk") or 0), + "metadata": image.get("properties", {}), + "created": self._format_date(image.get("created_at")), + "updated": self._format_date(image.get("updated_at")), + "status": self._get_status(image), + "progress": self._get_progress(image), + "links": self._get_links(request, image["id"]), + } + + server_ref = image.get("properties", {}).get("instance_ref") + + if server_ref is not None: + image_dict["server"] = { + "id": common.get_id_from_href(server_ref), + "links": [{ + "rel": "self", + "href": server_ref, + }, + { + "rel": "bookmark", + "href": common.remove_version_from_href(server_ref), + }], + } + + return dict(image=image_dict) + + def detail(self, request, images): + """Show a list of images with details.""" + list_func = self.show + return self._list_view(list_func, request, images) + + def index(self, request, images): + """Show a list of images with basic attributes.""" + list_func = self.basic + return self._list_view(list_func, request, images) + + def _list_view(self, list_func, request, images): + """Provide a view for a list of images.""" + image_list = [list_func(request, image)["image"] for image in images] + images_links = self._get_collection_links(request, images) + images_dict = dict(images=image_list) + + if images_links: + images_dict["images_links"] = images_links + + return images_dict + + def _get_links(self, request, identifier): + """Return a list of links for this image.""" + return [{ + "rel": "self", + "href": self._get_href_link(request, identifier), + }, + { + "rel": "bookmark", + "href": self._get_bookmark_link(request, identifier), + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": self._get_alternate_link(request, identifier), + }] + + def _get_alternate_link(self, request, identifier): + """Create an alternate link for a specific flavor id.""" + glance_url = utils.generate_glance_url() + return os.path.join(glance_url, + request.environ["nova.context"].project_id, + self._collection_name, + str(identifier)) + + @staticmethod + def _format_date(date_string): + """Return standard format for given date.""" + if date_string is not None: + return date_string.strftime('%Y-%m-%dT%H:%M:%SZ') + + @staticmethod + def _get_status(image): + """Update the status field to standardize format.""" + return { + 'active': 'ACTIVE', + 'queued': 'SAVING', + 'saving': 'SAVING', + 'deleted': 'DELETED', + 'pending_delete': 'DELETED', + 'killed': 'ERROR', + }.get(image.get("status"), 'UNKNOWN') + + @staticmethod + def _get_progress(image): + return { + "queued": 25, + "saving": 50, + "active": 100, + }.get(image.get("status"), 0) diff --git a/nova/api/openstack/compute/views/limits.py b/nova/api/openstack/compute/views/limits.py new file mode 100644 index 000000000..cff6781be --- /dev/null +++ b/nova/api/openstack/compute/views/limits.py @@ -0,0 +1,96 @@ +# 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 datetime + +from nova import utils + + +class ViewBuilder(object): + """Openstack API base limits view builder.""" + + def build(self, rate_limits, absolute_limits): + rate_limits = self._build_rate_limits(rate_limits) + absolute_limits = self._build_absolute_limits(absolute_limits) + + output = { + "limits": { + "rate": rate_limits, + "absolute": absolute_limits, + }, + } + + return output + + def _build_absolute_limits(self, absolute_limits): + """Builder for absolute limits + + absolute_limits should be given as a dict of limits. + For example: {"ram": 512, "gigabytes": 1024}. + + """ + limit_names = { + "ram": ["maxTotalRAMSize"], + "instances": ["maxTotalInstances"], + "cores": ["maxTotalCores"], + "metadata_items": ["maxServerMeta", "maxImageMeta"], + "injected_files": ["maxPersonality"], + "injected_file_content_bytes": ["maxPersonalitySize"], + } + limits = {} + for name, value in absolute_limits.iteritems(): + if name in limit_names and value is not None: + for name in limit_names[name]: + limits[name] = value + return limits + + def _build_rate_limits(self, rate_limits): + limits = [] + for rate_limit in rate_limits: + _rate_limit_key = None + _rate_limit = self._build_rate_limit(rate_limit) + + # check for existing key + for limit in limits: + if limit["uri"] == rate_limit["URI"] and \ + limit["regex"] == rate_limit["regex"]: + _rate_limit_key = limit + break + + # ensure we have a key if we didn't find one + if not _rate_limit_key: + _rate_limit_key = { + "uri": rate_limit["URI"], + "regex": rate_limit["regex"], + "limit": [], + } + limits.append(_rate_limit_key) + + _rate_limit_key["limit"].append(_rate_limit) + + return limits + + def _build_rate_limit(self, rate_limit): + next_avail = \ + datetime.datetime.utcfromtimestamp(rate_limit["resetTime"]) + return { + "verb": rate_limit["verb"], + "value": rate_limit["value"], + "remaining": int(rate_limit["remaining"]), + "unit": rate_limit["unit"], + "next-available": utils.isotime(at=next_avail), + } diff --git a/nova/api/openstack/compute/views/servers.py b/nova/api/openstack/compute/views/servers.py new file mode 100644 index 000000000..6a1622a66 --- /dev/null +++ b/nova/api/openstack/compute/views/servers.py @@ -0,0 +1,193 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# Copyright 2011 Piston Cloud Computing, Inc. +# 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.api.openstack import common +from nova.api.openstack.compute.views import addresses as views_addresses +from nova.api.openstack.compute.views import flavors as views_flavors +from nova.api.openstack.compute.views import images as views_images +from nova import log as logging +from nova import utils + + +LOG = logging.getLogger('nova.api.openstack.compute.views.servers') + + +class ViewBuilder(common.ViewBuilder): + """Model a server API response as a python dictionary.""" + + _collection_name = "servers" + + _progress_statuses = ( + "ACTIVE", + "BUILD", + "REBUILD", + "RESIZE", + "VERIFY_RESIZE", + ) + + _fault_statuses = ( + "ERROR", + ) + + def __init__(self): + """Initialize view builder.""" + super(ViewBuilder, self).__init__() + self._address_builder = views_addresses.ViewBuilder() + self._flavor_builder = views_flavors.ViewBuilder() + self._image_builder = views_images.ViewBuilder() + + def _skip_precooked(func): + def wrapped(self, request, instance): + if instance.get("_is_precooked"): + return dict(server=instance) + else: + return func(self, request, instance) + return wrapped + + def create(self, request, instance): + """View that should be returned when an instance is created.""" + return { + "server": { + "id": instance["uuid"], + "links": self._get_links(request, instance["uuid"]), + }, + } + + @_skip_precooked + def basic(self, request, instance): + """Generic, non-detailed view of an instance.""" + return { + "server": { + "id": instance["uuid"], + "name": instance["display_name"], + "links": self._get_links(request, instance["uuid"]), + }, + } + + @_skip_precooked + def show(self, request, instance): + """Detailed view of a single instance.""" + server = { + "server": { + "id": instance["uuid"], + "name": instance["display_name"], + "status": self._get_vm_state(instance), + "tenant_id": instance.get("project_id") or "", + "user_id": instance.get("user_id") or "", + "metadata": self._get_metadata(instance), + "hostId": self._get_host_id(instance) or "", + "image": self._get_image(request, instance), + "flavor": self._get_flavor(request, instance), + "created": utils.isotime(instance["created_at"]), + "updated": utils.isotime(instance["updated_at"]), + "addresses": self._get_addresses(request, instance), + "accessIPv4": instance.get("access_ip_v4") or "", + "accessIPv6": instance.get("access_ip_v6") or "", + "key_name": instance.get("key_name") or "", + "config_drive": instance.get("config_drive"), + "links": self._get_links(request, instance["uuid"]), + }, + } + _inst_fault = self._get_fault(request, instance) + if server["server"]["status"] in self._fault_statuses and _inst_fault: + server['server']['fault'] = _inst_fault + + if server["server"]["status"] in self._progress_statuses: + server["server"]["progress"] = instance.get("progress", 0) + + return server + + def index(self, request, instances): + """Show a list of servers without many details.""" + return self._list_view(self.basic, request, instances) + + def detail(self, request, instances): + """Detailed view of a list of instance.""" + return self._list_view(self.show, request, instances) + + def _list_view(self, func, request, servers): + """Provide a view for a list of servers.""" + server_list = [func(request, server)["server"] for server in servers] + servers_links = self._get_collection_links(request, servers) + servers_dict = dict(servers=server_list) + + if servers_links: + servers_dict["servers_links"] = servers_links + + return servers_dict + + @staticmethod + def _get_metadata(instance): + metadata = instance.get("metadata", []) + return dict((item['key'], str(item['value'])) for item in metadata) + + @staticmethod + def _get_vm_state(instance): + return common.status_from_state(instance.get("vm_state"), + instance.get("task_state")) + + @staticmethod + def _get_host_id(instance): + host = instance.get("host") + if host: + return hashlib.sha224(host).hexdigest() # pylint: disable=E1101 + + def _get_addresses(self, request, instance): + context = request.environ["nova.context"] + networks = common.get_networks_for_instance(context, instance) + return self._address_builder.index(networks)["addresses"] + + def _get_image(self, request, instance): + image_ref = instance["image_ref"] + image_id = str(common.get_id_from_href(image_ref)) + bookmark = self._image_builder._get_bookmark_link(request, image_id) + return { + "id": image_id, + "links": [{ + "rel": "bookmark", + "href": bookmark, + }], + } + + def _get_flavor(self, request, instance): + flavor_id = instance["instance_type"]["flavorid"] + flavor_ref = self._flavor_builder._get_href_link(request, flavor_id) + flavor_bookmark = self._flavor_builder._get_bookmark_link(request, + flavor_id) + return { + "id": str(common.get_id_from_href(flavor_ref)), + "links": [{ + "rel": "bookmark", + "href": flavor_bookmark, + }], + } + + def _get_fault(self, request, instance): + fault = instance.get("fault", None) + + if not fault: + return None + + return { + "code": fault["code"], + "created": utils.isotime(fault["created_at"]), + "message": fault["message"], + "details": fault["details"], + } diff --git a/nova/api/openstack/compute/views/versions.py b/nova/api/openstack/compute/views/versions.py new file mode 100644 index 000000000..cb2fd9f4a --- /dev/null +++ b/nova/api/openstack/compute/views/versions.py @@ -0,0 +1,94 @@ +# 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 copy +import os + + +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_choices(self, VERSIONS, req): + version_objs = [] + for version in VERSIONS: + version = VERSIONS[version] + version_objs.append({ + "id": version['id'], + "status": version['status'], + "links": [ + { + "rel": "self", + "href": self.generate_href(req.path), + }, + ], + "media-types": version['media-types'], + }) + + return dict(choices=version_objs) + + def build_versions(self, versions): + version_objs = [] + for version in sorted(versions.keys()): + version = versions[version] + version_objs.append({ + "id": version['id'], + "status": version['status'], + "updated": version['updated'], + "links": self._build_links(version), + }) + + return dict(versions=version_objs) + + def build_version(self, version): + reval = copy.deepcopy(version) + reval['links'].insert(0, { + "rel": "self", + "href": self.base_url.rstrip('/') + '/', + }) + return dict(version=reval) + + def _build_links(self, version_data): + """Generate a container of links that refer to the provided version.""" + href = self.generate_href() + + links = [ + { + "rel": "self", + "href": href, + }, + ] + + return links + + def generate_href(self, path=None): + """Create an url that refers to a specific version_number.""" + version_number = 'v2' + if path: + path = path.strip('/') + return os.path.join(self.base_url, version_number, path) + else: + return os.path.join(self.base_url, version_number) + '/' diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py new file mode 100644 index 000000000..6c49e8ace --- /dev/null +++ b/nova/api/openstack/extensions.py @@ -0,0 +1,623 @@ +# 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 +# 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 os +import routes +import webob.dec +import webob.exc + +import nova.api.openstack +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils +from nova import wsgi as base_wsgi + + +LOG = logging.getLogger('nova.api.openstack.extensions') + + +FLAGS = flags.FLAGS + + +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. + + """ + + # The name of the extension, e.g., 'Fox In Socks' + name = None + + # The alias for the extension, e.g., 'FOXNSOX' + alias = None + + # Description comes from the docstring for the class + + # The XML namespace for the extension, e.g., + # 'http://www.fox.in.socks/api/ext/pie/v1.0' + namespace = None + + # The timestamp when the extension was last updated, e.g., + # '2011-01-22T13:25:27-06:00' + updated = None + + # This attribute causes the extension to load only when + # the admin api is enabled + admin_only = False + + def __init__(self, ext_mgr): + """Register extension with the extension manager.""" + + ext_mgr.register(self) + + 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_request_extensions(self): + """List of extensions.RequestExtension extension objects. + + Request extensions are used to handle custom request data. + + """ + request_exts = [] + return request_exts + + @classmethod + def nsmap(cls): + """Synthesize a namespace map from extension.""" + + # Start with a base nsmap + nsmap = ext_nsmap.copy() + + # Add the namespace for the extension + nsmap[cls.alias] = cls.namespace + + return nsmap + + @classmethod + def xmlname(cls, name): + """Synthesize element and attribute names.""" + + return '{%s}%s' % (cls.namespace, name) + + +class ActionExtensionController(object): + def __init__(self, application): + self.application = application + self.action_handlers = {} + + def add_action(self, action_name, handler): + self.action_handlers[action_name] = handler + + def action(self, req, id, body): + for action_name, handler in self.action_handlers.iteritems(): + if action_name in body: + return handler(body, req, id) + # no action handler found (bump to downstream application) + res = self.application + return res + + +class ActionExtensionResource(wsgi.Resource): + + def __init__(self, application): + controller = ActionExtensionController(application) + wsgi.Resource.__init__(self, controller, + serializer=wsgi.ResponseSerializer(), + deserializer=wsgi.RequestDeserializer()) + + def add_action(self, action_name, handler): + self.controller.add_action(action_name, handler) + + +class RequestExtensionController(object): + + def __init__(self, application): + self.application = application + self.handlers = [] + self.pre_handlers = [] + + def add_handler(self, handler): + self.handlers.append(handler) + + def add_pre_handler(self, pre_handler): + self.pre_handlers.append(pre_handler) + + def process(self, req, *args, **kwargs): + for pre_handler in self.pre_handlers: + pre_handler(req) + + res = req.get_response(self.application) + res.environ = req.environ + + # Don't call extensions if the main application returned an + # unsuccessful status + successful = 200 <= res.status_int < 400 + if not successful: + return res + + # Deserialize the response body, if any + body = None + if res.body: + body = utils.loads(res.body) + + # currently request handlers are un-ordered + for handler in self.handlers: + res = handler(req, res, body) + + # Reserialize the response body + if body is not None: + res.body = utils.dumps(body) + + return res + + +class RequestExtensionResource(wsgi.Resource): + + def __init__(self, application): + controller = RequestExtensionController(application) + wsgi.Resource.__init__(self, controller, + serializer=wsgi.ResponseSerializer(), + deserializer=wsgi.RequestDeserializer()) + + def add_handler(self, handler): + self.controller.add_handler(handler) + + def add_pre_handler(self, pre_handler): + self.controller.add_pre_handler(pre_handler) + + +def make_ext(elem): + elem.set('name') + elem.set('namespace') + elem.set('alias') + elem.set('updated') + + desc = xmlutil.SubTemplateElement(elem, 'description') + desc.text = 'description' + + xmlutil.make_links(elem, 'links') + + +ext_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} + + +class ExtensionTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('extension', selector='extension') + make_ext(root) + return xmlutil.MasterTemplate(root, 1, nsmap=ext_nsmap) + + +class ExtensionsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('extensions') + elem = xmlutil.SubTemplateElement(root, 'extension', + selector='extensions') + make_ext(elem) + return xmlutil.MasterTemplate(root, 1, nsmap=ext_nsmap) + + +class ExtensionsResource(wsgi.Resource): + + def __init__(self, extension_manager): + self.extension_manager = extension_manager + super(ExtensionsResource, self).__init__(None) + + def _translate(self, ext): + ext_data = {} + ext_data['name'] = ext.name + ext_data['alias'] = ext.alias + ext_data['description'] = ext.__doc__ + ext_data['namespace'] = ext.namespace + ext_data['updated'] = ext.updated + ext_data['links'] = [] # TODO(dprince): implement extension links + return ext_data + + @wsgi.serializers(xml=ExtensionsTemplate) + def index(self, req): + extensions = [] + for _alias, ext in self.extension_manager.extensions.iteritems(): + extensions.append(self._translate(ext)) + return dict(extensions=extensions) + + @wsgi.serializers(xml=ExtensionTemplate) + def show(self, req, id): + try: + # NOTE(dprince): the extensions alias is used as the 'id' for show + ext = self.extension_manager.extensions[id] + except KeyError: + raise webob.exc.HTTPNotFound() + + return dict(extension=self._translate(ext)) + + def delete(self, req, id): + raise webob.exc.HTTPNotFound() + + def create(self, req): + raise webob.exc.HTTPNotFound() + + +class ExtensionMiddleware(base_wsgi.Middleware): + """Extensions middleware for WSGI.""" + @classmethod + def factory(cls, global_config, **local_config): + """Paste factory.""" + def _factory(app): + return cls(app, **local_config) + return _factory + + def _action_ext_resources(self, application, ext_mgr, mapper): + """Return a dict of ActionExtensionResource-s by collection.""" + action_resources = {} + for action in ext_mgr.get_actions(): + if not action.collection in action_resources.keys(): + resource = ActionExtensionResource(application) + mapper.connect("/:(project_id)/%s/:(id)/action.:(format)" % + action.collection, + action='action', + controller=resource, + conditions=dict(method=['POST'])) + mapper.connect("/:(project_id)/%s/:(id)/action" % + action.collection, + action='action', + controller=resource, + conditions=dict(method=['POST'])) + action_resources[action.collection] = resource + + return action_resources + + def _request_ext_resources(self, application, ext_mgr, mapper): + """Returns a dict of RequestExtensionResource-s by collection.""" + request_ext_resources = {} + for req_ext in ext_mgr.get_request_extensions(): + if not req_ext.key in request_ext_resources.keys(): + resource = RequestExtensionResource(application) + mapper.connect(req_ext.url_route + '.:(format)', + action='process', + controller=resource, + conditions=req_ext.conditions) + + mapper.connect(req_ext.url_route, + action='process', + controller=resource, + conditions=req_ext.conditions) + request_ext_resources[req_ext.key] = resource + + return request_ext_resources + + def __init__(self, application, ext_mgr=None): + + if ext_mgr is None: + ext_mgr = ExtensionManager() + self.ext_mgr = ext_mgr + + mapper = nova.api.openstack.ProjectMapper() + + # extended actions + action_resources = self._action_ext_resources(application, ext_mgr, + mapper) + for action in ext_mgr.get_actions(): + LOG.debug(_('Extended action: %s'), action.action_name) + resource = action_resources[action.collection] + resource.add_action(action.action_name, action.handler) + + # extended requests + req_controllers = self._request_ext_resources(application, ext_mgr, + mapper) + for request_ext in ext_mgr.get_request_extensions(): + LOG.debug(_('Extended request: %s'), request_ext.key) + controller = req_controllers[request_ext.key] + if request_ext.handler: + controller.add_handler(request_ext.handler) + if request_ext.pre_handler: + controller.add_pre_handler(request_ext.pre_handler) + + 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): + """Dispatch the request. + + 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): + """Load extensions from the configured extension path. + + See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an + example extension implementation. + + """ + + _ext_mgr = None + + @classmethod + def reset(cls): + cls._ext_mgr = None + + def register(self, ext): + # Do nothing if the extension doesn't check out + if not self._check_extension(ext): + return + + alias = ext.alias + LOG.audit(_('Loaded extension: %s'), alias) + + if alias in self.extensions: + raise exception.Error("Found duplicate extension: %s" % alias) + self.extensions[alias] = ext + + def get_resources(self): + """Returns a list of ResourceExtension objects.""" + + resources = [] + resources.append(ResourceExtension('extensions', + ExtensionsResource(self))) + + for ext in self.extensions.values(): + 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 ext in self.extensions.values(): + try: + actions.extend(ext.get_actions()) + except AttributeError: + # NOTE(dprince): Extension aren't required to have action + # extensions + pass + return actions + + def get_request_extensions(self): + """Returns a list of RequestExtension objects.""" + request_exts = [] + for ext in self.extensions.values(): + try: + request_exts.extend(ext.get_request_extensions()) + except AttributeError: + # NOTE(dprince): Extension aren't required to have request + # extensions + pass + return request_exts + + def _check_extension(self, extension): + """Checks for required methods in extension objects.""" + try: + LOG.debug(_('Ext name: %s'), extension.name) + LOG.debug(_('Ext alias: %s'), extension.alias) + LOG.debug(_('Ext description: %s'), + ' '.join(extension.__doc__.strip().split())) + LOG.debug(_('Ext namespace: %s'), extension.namespace) + LOG.debug(_('Ext updated: %s'), extension.updated) + LOG.debug(_('Ext admin_only: %s'), extension.admin_only) + except AttributeError as ex: + LOG.exception(_("Exception loading extension: %s"), unicode(ex)) + return False + + # Don't load admin api extensions if the admin api isn't enabled + if not FLAGS.allow_admin_api and extension.admin_only: + return False + + return True + + def load_extension(self, ext_factory): + """Execute an extension factory. + + Loads an extension. The 'ext_factory' is the name of a + callable that will be imported and called with one + argument--the extension manager. The factory callable is + expected to call the register() method at least once. + """ + + LOG.debug(_("Loading extension %s"), ext_factory) + + # Load the factory + factory = utils.import_class(ext_factory) + + # Call it + LOG.debug(_("Calling extension factory %s"), ext_factory) + factory(self) + + def _load_extensions(self): + """Load extensions specified on the command line.""" + + extensions = list(self.cls_list) + + for ext_factory in extensions: + try: + self.load_extension(ext_factory) + except Exception as exc: + LOG.warn(_('Failed to load extension %(ext_factory)s: ' + '%(exc)s') % locals()) + + +class RequestExtension(object): + """Extend requests and responses of core nova OpenStack API resources. + + Provide a way to add data to responses and handle custom request data + that is sent to core nova OpenStack API controllers. + + """ + def __init__(self, method, url_route, handler=None, pre_handler=None): + self.url_route = url_route + self.handler = handler + self.conditions = dict(method=[method]) + self.key = "%s-%s" % (method, url_route) + self.pre_handler = pre_handler + + +class ActionExtension(object): + """Add custom actions to core nova OpenStack API resources.""" + + def __init__(self, collection, action_name, handler): + self.collection = collection + self.action_name = action_name + self.handler = handler + + +class ResourceExtension(object): + """Add top level resources to the OpenStack API in nova.""" + + def __init__(self, collection, controller, parent=None, + collection_actions=None, member_actions=None, + deserializer=None, serializer=None): + if not collection_actions: + collection_actions = {} + if not member_actions: + member_actions = {} + self.collection = collection + self.controller = controller + self.parent = parent + self.collection_actions = collection_actions + self.member_actions = member_actions + self.deserializer = deserializer + self.serializer = serializer + + +class ExtensionsXMLSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return ExtensionsTemplate() + + def show(self): + return ExtensionTemplate() + + +def require_admin(f): + @functools.wraps(f) + def wraps(self, req, *args, **kwargs): + if 'nova.context' in req.environ and\ + req.environ['nova.context'].is_admin: + return f(self, req, *args, **kwargs) + else: + raise exception.AdminRequired() + return wraps + + +def wrap_errors(fn): + """Ensure errors are not passed along.""" + def wrapped(*args): + try: + return fn(*args) + except Exception, e: + raise webob.exc.HTTPInternalServerError() + return wrapped + + +def load_standard_extensions(ext_mgr, logger, path, package): + """Registers all standard API extensions.""" + + # Walk through all the modules in our directory... + our_dir = path[0] + for dirpath, dirnames, filenames in os.walk(our_dir): + # Compute the relative package name from the dirpath + relpath = os.path.relpath(dirpath, our_dir) + if relpath == '.': + relpkg = '' + else: + relpkg = '.%s' % '.'.join(relpath.split(os.sep)) + + # Now, consider each file in turn, only considering .py files + for fname in filenames: + root, ext = os.path.splitext(fname) + + # Skip __init__ and anything that's not .py + if ext != '.py' or root == '__init__': + continue + + # Try loading it + classname = ("%s%s.%s.%s%s" % + (package, relpkg, root, + root[0].upper(), root[1:])) + try: + ext_mgr.load_extension(classname) + except Exception as exc: + logger.warn(_('Failed to load extension %(classname)s: ' + '%(exc)s') % locals()) + + # Now, let's consider any subdirectories we may have... + subdirs = [] + for dname in dirnames: + # Skip it if it does not have __init__.py + if not os.path.exists(os.path.join(dirpath, dname, + '__init__.py')): + continue + + # If it has extension(), delegate... + ext_name = ("%s%s.%s.extension" % + (package, relpkg, dname)) + try: + ext = utils.import_class(ext_name) + except exception.ClassNotFound: + # extension() doesn't exist on it, so we'll explore + # the directory for ourselves + subdirs.append(dname) + else: + try: + ext(ext_mgr) + except Exception as exc: + logger.warn(_('Failed to load extension %(ext_name)s: ' + '%(exc)s') % locals()) + + # Update the list of directories we'll explore... + dirnames[:] = subdirs diff --git a/nova/api/openstack/urlmap.py b/nova/api/openstack/urlmap.py new file mode 100644 index 000000000..825a12af2 --- /dev/null +++ b/nova/api/openstack/urlmap.py @@ -0,0 +1,297 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import paste.urlmap +import re +import urllib2 + +from nova import log as logging +from nova.api.openstack import wsgi + + +_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"' +_option_header_piece_re = re.compile(r';\s*([^\s;=]+|%s)\s*' + r'(?:=\s*([^;]+|%s))?\s*' % + (_quoted_string_re, _quoted_string_re)) + +LOG = logging.getLogger('nova.api.openstack.compute.map') + + +def unquote_header_value(value): + """Unquotes a header value. + This does not use the real unquoting but what browsers are actually + using for quoting. + + :param value: the header value to unquote. + """ + if value and value[0] == value[-1] == '"': + # this is not the real unquoting, but fixing this so that the + # RFC is met will result in bugs with internet explorer and + # probably some other browsers as well. IE for example is + # uploading files with "C:\foo\bar.txt" as filename + value = value[1:-1] + return value + + +def parse_list_header(value): + """Parse lists as described by RFC 2068 Section 2. + + In particular, parse comma-separated lists where the elements of + the list may include quoted-strings. A quoted-string could + contain a comma. A non-quoted string could have quotes in the + middle. Quotes are removed automatically after parsing. + + The return value is a standard :class:`list`: + + >>> parse_list_header('token, "quoted value"') + ['token', 'quoted value'] + + :param value: a string with a list header. + :return: :class:`list` + """ + result = [] + for item in urllib2.parse_http_list(value): + if item[:1] == item[-1:] == '"': + item = unquote_header_value(item[1:-1]) + result.append(item) + return result + + +def parse_options_header(value): + """Parse a ``Content-Type`` like header into a tuple with the content + type and the options: + + >>> parse_options_header('Content-Type: text/html; mimetype=text/html') + ('Content-Type:', {'mimetype': 'text/html'}) + + :param value: the header to parse. + :return: (str, options) + """ + def _tokenize(string): + for match in _option_header_piece_re.finditer(string): + key, value = match.groups() + key = unquote_header_value(key) + if value is not None: + value = unquote_header_value(value) + yield key, value + + if not value: + return '', {} + + parts = _tokenize(';' + value) + name = parts.next()[0] + extra = dict(parts) + return name, extra + + +class Accept(object): + def __init__(self, value): + self._content_types = [parse_options_header(v) for v in + parse_list_header(value)] + + def best_match(self, supported_content_types): + # FIXME: Should we have a more sophisticated matching algorithm that + # takes into account the version as well? + best_quality = -1 + best_content_type = None + best_params = {} + best_match = '*/*' + + for content_type in supported_content_types: + for content_mask, params in self._content_types: + try: + quality = float(params.get('q', 1)) + except ValueError: + continue + + if quality < best_quality: + continue + elif best_quality == quality: + if best_match.count('*') <= content_mask.count('*'): + continue + + if self._match_mask(content_mask, content_type): + best_quality = quality + best_content_type = content_type + best_params = params + best_match = content_mask + + return best_content_type, best_params + + def content_type_params(self, best_content_type): + """Find parameters in Accept header for given content type.""" + for content_type, params in self._content_types: + if best_content_type == content_type: + return params + + return {} + + def _match_mask(self, mask, content_type): + if '*' not in mask: + return content_type == mask + if mask == '*/*': + return True + mask_major = mask[:-2] + content_type_major = content_type.split('/', 1)[0] + return content_type_major == mask_major + + +def urlmap_factory(loader, global_conf, **local_conf): + if 'not_found_app' in local_conf: + not_found_app = local_conf.pop('not_found_app') + else: + not_found_app = global_conf.get('not_found_app') + if not_found_app: + not_found_app = loader.get_app(not_found_app, global_conf=global_conf) + urlmap = URLMap(not_found_app=not_found_app) + for path, app_name in local_conf.items(): + path = paste.urlmap.parse_path_expression(path) + app = loader.get_app(app_name, global_conf=global_conf) + urlmap[path] = app + return urlmap + + +class URLMap(paste.urlmap.URLMap): + def _match(self, host, port, path_info): + """Find longest match for a given URL path.""" + for (domain, app_url), app in self.applications: + if domain and domain != host and domain != host + ':' + port: + continue + if (path_info == app_url + or path_info.startswith(app_url + '/')): + return app, app_url + + return None, None + + def _set_script_name(self, app, app_url): + def wrap(environ, start_response): + environ['SCRIPT_NAME'] += app_url + return app(environ, start_response) + + return wrap + + def _munge_path(self, app, path_info, app_url): + def wrap(environ, start_response): + environ['SCRIPT_NAME'] += app_url + environ['PATH_INFO'] = path_info[len(app_url):] + return app(environ, start_response) + + return wrap + + def _path_strategy(self, host, port, path_info): + """Check path suffix for MIME type and path prefix for API version.""" + mime_type = app = app_url = None + + parts = path_info.rsplit('.', 1) + if len(parts) > 1: + possible_type = 'application/' + parts[1] + if possible_type in wsgi.SUPPORTED_CONTENT_TYPES: + mime_type = possible_type + + parts = path_info.split('/') + if len(parts) > 1: + possible_app, possible_app_url = self._match(host, port, path_info) + # Don't use prefix if it ends up matching default + if possible_app and possible_app_url: + app_url = possible_app_url + app = self._munge_path(possible_app, path_info, app_url) + + return mime_type, app, app_url + + def _content_type_strategy(self, host, port, environ): + """Check Content-Type header for API version.""" + app = None + params = parse_options_header(environ.get('CONTENT_TYPE', ''))[1] + if 'version' in params: + app, app_url = self._match(host, port, '/v' + params['version']) + if app: + app = self._set_script_name(app, app_url) + + return app + + def _accept_strategy(self, host, port, environ, supported_content_types): + """Check Accept header for best matching MIME type and API version.""" + accept = Accept(environ.get('HTTP_ACCEPT', '')) + + app = None + + # Find the best match in the Accept header + mime_type, params = accept.best_match(supported_content_types) + if 'version' in params: + app, app_url = self._match(host, port, '/v' + params['version']) + if app: + app = self._set_script_name(app, app_url) + + return mime_type, app + + def __call__(self, environ, start_response): + host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower() + if ':' in host: + host, port = host.split(':', 1) + else: + if environ['wsgi.url_scheme'] == 'http': + port = '80' + else: + port = '443' + + path_info = environ['PATH_INFO'] + path_info = self.normalize_url(path_info, False)[1] + + # The MIME type for the response is determined in one of two ways: + # 1) URL path suffix (eg /servers/detail.json) + # 2) Accept header (eg application/json;q=0.8, application/xml;q=0.2) + + # The API version is determined in one of three ways: + # 1) URL path prefix (eg /v1.1/tenant/servers/detail) + # 2) Content-Type header (eg application/json;version=1.1) + # 3) Accept header (eg application/json;q=0.8;version=1.1) + + supported_content_types = list(wsgi.SUPPORTED_CONTENT_TYPES) + + mime_type, app, app_url = self._path_strategy(host, port, path_info) + + # Accept application/atom+xml for the index query of each API + # version mount point as well as the root index + if (app_url and app_url + '/' == path_info) or path_info == '/': + supported_content_types.append('application/atom+xml') + + if not app: + app = self._content_type_strategy(host, port, environ) + + if not mime_type or not app: + possible_mime_type, possible_app = self._accept_strategy( + host, port, environ, supported_content_types) + if possible_mime_type and not mime_type: + mime_type = possible_mime_type + if possible_app and not app: + app = possible_app + + if not mime_type: + mime_type = 'application/json' + + if not app: + # Didn't match a particular version, probably matches default + app, app_url = self._match(host, port, path_info) + if app: + app = self._munge_path(app, path_info, app_url) + + if app: + environ['nova.best_content_type'] = mime_type + return app(environ, start_response) + + environ['paste.urlmap_object'] = self + return self.not_found_application(environ, start_response) diff --git a/nova/api/openstack/v2/__init__.py b/nova/api/openstack/v2/__init__.py deleted file mode 100644 index c211cd2f9..000000000 --- a/nova/api/openstack/v2/__init__.py +++ /dev/null @@ -1,182 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -WSGI middleware for OpenStack API controllers. -""" - -import routes -import webob.dec -import webob.exc - -from nova.api.openstack.v2 import consoles -from nova.api.openstack.v2 import extensions -from nova.api.openstack.v2 import flavors -from nova.api.openstack.v2 import images -from nova.api.openstack.v2 import image_metadata -from nova.api.openstack.v2 import ips -from nova.api.openstack.v2 import limits -from nova.api.openstack.v2 import servers -from nova.api.openstack.v2 import server_metadata -from nova.api.openstack.v2 import versions -from nova.api.openstack import wsgi -from nova import flags -from nova import log as logging -from nova import wsgi as base_wsgi - - -LOG = logging.getLogger('nova.api.openstack.v2') -FLAGS = flags.FLAGS -flags.DEFINE_bool('allow_admin_api', - False, - 'When True, this API service will accept admin operations.') -flags.DEFINE_bool('allow_instance_snapshots', - True, - 'When True, this API service will permit instance snapshot operations.') - - -class FaultWrapper(base_wsgi.Middleware): - """Calls down the middleware stack, making exceptions into faults.""" - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - try: - return req.get_response(self.application) - except Exception as ex: - LOG.exception(_("Caught error: %s"), unicode(ex)) - exc = webob.exc.HTTPInternalServerError() - return wsgi.Fault(exc) - - -class APIMapper(routes.Mapper): - def routematch(self, url=None, environ=None): - if url is "": - result = self._match("", environ) - return result[0], result[1] - return routes.Mapper.routematch(self, url, environ) - - -class ProjectMapper(APIMapper): - - def resource(self, member_name, collection_name, **kwargs): - if not ('parent_resource' in kwargs): - kwargs['path_prefix'] = '{project_id}/' - else: - parent_resource = kwargs['parent_resource'] - p_collection = parent_resource['collection_name'] - p_member = parent_resource['member_name'] - kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection, - p_member) - routes.Mapper.resource(self, member_name, - collection_name, - **kwargs) - - -class APIRouter(base_wsgi.Router): - """ - Routes requests on the OpenStack API to the appropriate controller - and method. - """ - - @classmethod - def factory(cls, global_config, **local_config): - """Simple paste factory, :class:`nova.wsgi.Router` doesn't have one""" - return cls() - - def __init__(self, ext_mgr=None): - if ext_mgr is None: - ext_mgr = extensions.ExtensionManager() - - mapper = ProjectMapper() - self._setup_routes(mapper) - self._setup_ext_routes(mapper, ext_mgr) - super(APIRouter, self).__init__(mapper) - - def _setup_ext_routes(self, mapper, ext_mgr): - for resource in ext_mgr.get_resources(): - LOG.debug(_('Extended resource: %s'), - resource.collection) - - kargs = dict( - controller=wsgi.Resource( - resource.controller, resource.deserializer, - resource.serializer), - collection=resource.collection_actions, - member=resource.member_actions) - - if resource.parent: - kargs['parent_resource'] = resource.parent - - mapper.resource(resource.collection, resource.collection, **kargs) - - def _setup_routes(self, mapper): - mapper.connect("versions", "/", - controller=versions.create_resource(), - action='show') - - mapper.redirect("", "/") - - mapper.resource("console", "consoles", - controller=consoles.create_resource(), - parent_resource=dict(member_name='server', - collection_name='servers')) - - mapper.resource("server", "servers", - controller=servers.create_resource(), - collection={'detail': 'GET'}, - member={'action': 'POST'}) - - mapper.resource("ip", "ips", controller=ips.create_resource(), - parent_resource=dict(member_name='server', - collection_name='servers')) - - mapper.resource("image", "images", - controller=images.create_resource(), - collection={'detail': 'GET'}) - - mapper.resource("limit", "limits", - controller=limits.create_resource()) - - mapper.resource("flavor", "flavors", - controller=flavors.create_resource(), - collection={'detail': 'GET'}) - - image_metadata_controller = image_metadata.create_resource() - - mapper.resource("image_meta", "metadata", - controller=image_metadata_controller, - parent_resource=dict(member_name='image', - collection_name='images')) - - mapper.connect("metadata", "/{project_id}/images/{image_id}/metadata", - controller=image_metadata_controller, - action='update_all', - conditions={"method": ['PUT']}) - - server_metadata_controller = server_metadata.create_resource() - - mapper.resource("server_meta", "metadata", - controller=server_metadata_controller, - parent_resource=dict(member_name='server', - collection_name='servers')) - - mapper.connect("metadata", - "/{project_id}/servers/{server_id}/metadata", - controller=server_metadata_controller, - action='update_all', - conditions={"method": ['PUT']}) diff --git a/nova/api/openstack/v2/auth.py b/nova/api/openstack/v2/auth.py deleted file mode 100644 index ba5fb603f..000000000 --- a/nova/api/openstack/v2/auth.py +++ /dev/null @@ -1,257 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import hashlib -import os -import time - -import webob.dec -import webob.exc - -from nova.api.openstack import common -from nova.api.openstack import wsgi -from nova import auth -from nova import context -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils -from nova import wsgi as base_wsgi - -LOG = logging.getLogger('nova.api.openstack.v2.auth') -FLAGS = flags.FLAGS -flags.DECLARE('use_forwarded_for', 'nova.api.auth') - - -class NoAuthMiddleware(base_wsgi.Middleware): - """Return a fake token if one isn't specified.""" - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - if 'X-Auth-Token' not in req.headers: - user_id = req.headers.get('X-Auth-User', 'admin') - project_id = req.headers.get('X-Auth-Project-Id', 'admin') - os_url = os.path.join(req.url, project_id) - res = webob.Response() - # NOTE(vish): This is expecting and returning Auth(1.1), whereas - # keystone uses 2.0 auth. We should probably allow - # 2.0 auth here as well. - res.headers['X-Auth-Token'] = '%s:%s' % (user_id, project_id) - res.headers['X-Server-Management-Url'] = os_url - res.headers['X-Storage-Url'] = '' - res.headers['X-CDN-Management-Url'] = '' - res.content_type = 'text/plain' - res.status = '204' - return res - - token = req.headers['X-Auth-Token'] - user_id, _sep, project_id = token.partition(':') - project_id = project_id or user_id - remote_address = getattr(req, 'remote_address', '127.0.0.1') - if FLAGS.use_forwarded_for: - remote_address = req.headers.get('X-Forwarded-For', remote_address) - ctx = context.RequestContext(user_id, - project_id, - is_admin=True, - remote_address=remote_address) - - req.environ['nova.context'] = ctx - return self.application - - -class AuthMiddleware(base_wsgi.Middleware): - """Authorize the openstack API request or return an HTTP Forbidden.""" - - def __init__(self, application, db_driver=None): - if not db_driver: - db_driver = FLAGS.db_driver - self.db = utils.import_object(db_driver) - self.auth = auth.manager.AuthManager() - super(AuthMiddleware, self).__init__(application) - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - if not self.has_authentication(req): - return self.authenticate(req) - user_id = self.get_user_by_authentication(req) - if not user_id: - token = req.headers["X-Auth-Token"] - msg = _("%(user_id)s could not be found with token '%(token)s'") - LOG.warn(msg % locals()) - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - - # Get all valid projects for the user - projects = self.auth.get_projects(user_id) - if not projects: - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - - project_id = "" - path_parts = req.path.split('/') - # TODO(wwolf): this v1.1 check will be temporary as - # keystone should be taking this over at some point - if len(path_parts) > 1 and path_parts[1] in ('v1.1', 'v2'): - project_id = path_parts[2] - # Check that the project for project_id exists, and that user - # is authorized to use it - try: - self.auth.get_project(project_id) - except exception.ProjectNotFound: - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - if project_id not in [p.id for p in projects]: - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - else: - # As a fallback, set project_id from the headers, which is the v1.0 - # behavior. As a last resort, be forgiving to the user and set - # project_id based on a valid project of theirs. - try: - project_id = req.headers["X-Auth-Project-Id"] - except KeyError: - project_id = projects[0].id - - is_admin = self.auth.is_admin(user_id) - remote_address = getattr(req, 'remote_address', '127.0.0.1') - if FLAGS.use_forwarded_for: - remote_address = req.headers.get('X-Forwarded-For', remote_address) - ctx = context.RequestContext(user_id, - project_id, - is_admin=is_admin, - remote_address=remote_address) - req.environ['nova.context'] = ctx - - if not is_admin and not self.auth.is_project_member(user_id, - project_id): - msg = _("%(user_id)s must be an admin or a " - "member of %(project_id)s") - LOG.warn(msg % locals()) - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - - return self.application - - def has_authentication(self, req): - return 'X-Auth-Token' in req.headers - - def get_user_by_authentication(self, req): - return self.authorize_token(req.headers["X-Auth-Token"]) - - def authenticate(self, req): - # Unless the request is explicitly made against // don't - # honor it - path_info = req.path_info - if len(path_info) > 1: - msg = _("Authentication requests must be made against a version " - "root (e.g. /v2).") - LOG.warn(msg) - return wsgi.Fault(webob.exc.HTTPUnauthorized(explanation=msg)) - - def _get_auth_header(key): - """Ensures that the KeyError returned is meaningful.""" - try: - return req.headers[key] - except KeyError as ex: - raise KeyError(key) - try: - username = _get_auth_header('X-Auth-User') - key = _get_auth_header('X-Auth-Key') - except KeyError as ex: - msg = _("Could not find %s in request.") % ex - LOG.warn(msg) - return wsgi.Fault(webob.exc.HTTPUnauthorized(explanation=msg)) - - token, user = self._authorize_user(username, key, req) - if user and token: - res = webob.Response() - res.headers['X-Auth-Token'] = token['token_hash'] - res.headers['X-Server-Management-Url'] = \ - token['server_management_url'] - res.headers['X-Storage-Url'] = token['storage_url'] - res.headers['X-CDN-Management-Url'] = token['cdn_management_url'] - res.content_type = 'text/plain' - res.status = '204' - LOG.debug(_("Successfully authenticated '%s'") % username) - return res - else: - return wsgi.Fault(webob.exc.HTTPUnauthorized()) - - def authorize_token(self, token_hash): - """ retrieves user information from the datastore given a token - - If the token has expired, returns None - If the token is not found, returns None - Otherwise returns dict(id=(the authorized user's id)) - - This method will also remove the token if the timestamp is older than - 2 days ago. - """ - ctxt = context.get_admin_context() - try: - token = self.db.auth_token_get(ctxt, token_hash) - except exception.NotFound: - return None - if token: - delta = utils.utcnow() - token['created_at'] - if delta.days >= 2: - self.db.auth_token_destroy(ctxt, token['token_hash']) - else: - return token['user_id'] - return None - - def _authorize_user(self, username, key, req): - """Generates a new token and assigns it to a user. - - username - string - key - string API key - req - wsgi.Request object - """ - ctxt = context.get_admin_context() - - project_id = req.headers.get('X-Auth-Project-Id') - if project_id is None: - # If the project_id is not provided in the headers, be forgiving to - # the user and set project_id based on a valid project of theirs. - user = self.auth.get_user_from_access_key(key) - projects = self.auth.get_projects(user.id) - if not projects: - raise webob.exc.HTTPUnauthorized() - project_id = projects[0].id - - try: - user = self.auth.get_user_from_access_key(key) - except exception.NotFound: - LOG.warn(_("User not found with provided API key.")) - user = None - - if user and user.name == username: - 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'] = '' - os_url = req.url - token_dict['server_management_url'] = os_url.strip('/') - version = common.get_version_from_href(os_url) - if version in ('1.1', '2'): - token_dict['server_management_url'] += '/' + project_id - token_dict['storage_url'] = '' - token_dict['user_id'] = user.id - token = self.db.auth_token_create(ctxt, token_dict) - return token, user - elif user and user.name != username: - msg = _("Provided API key is valid, but not for user " - "'%(username)s'") % locals() - LOG.warn(msg) - - return None, None diff --git a/nova/api/openstack/v2/consoles.py b/nova/api/openstack/v2/consoles.py deleted file mode 100644 index e9eee4c75..000000000 --- a/nova/api/openstack/v2/consoles.py +++ /dev/null @@ -1,131 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob -from webob import exc - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import console -from nova import exception - - -def _translate_keys(cons): - """Coerces a console instance into proper dictionary format """ - pool = cons['pool'] - info = {'id': cons['id'], - 'console_type': pool['console_type']} - return dict(console=info) - - -def _translate_detail_keys(cons): - """Coerces a console instance into proper dictionary format with - correctly mapped attributes """ - pool = cons['pool'] - info = {'id': cons['id'], - 'console_type': pool['console_type'], - 'password': cons['password'], - 'instance_name': cons['instance_name'], - 'port': cons['port'], - 'host': pool['public_hostname']} - return dict(console=info) - - -class ConsoleTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('console', selector='console') - - id_elem = xmlutil.SubTemplateElement(root, 'id', selector='id') - id_elem.text = xmlutil.Selector() - - port_elem = xmlutil.SubTemplateElement(root, 'port', selector='port') - port_elem.text = xmlutil.Selector() - - host_elem = xmlutil.SubTemplateElement(root, 'host', selector='host') - host_elem.text = xmlutil.Selector() - - passwd_elem = xmlutil.SubTemplateElement(root, 'password', - selector='password') - passwd_elem.text = xmlutil.Selector() - - constype_elem = xmlutil.SubTemplateElement(root, 'console_type', - selector='console_type') - constype_elem.text = xmlutil.Selector() - - return xmlutil.MasterTemplate(root, 1) - - -class ConsolesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('consoles') - console = xmlutil.SubTemplateElement(root, 'console', - selector='consoles') - console.append(ConsoleTemplate()) - - return xmlutil.MasterTemplate(root, 1) - - -class Controller(object): - """The Consoles controller for the Openstack API""" - - def __init__(self): - self.console_api = console.API() - - @wsgi.serializers(xml=ConsolesTemplate) - def index(self, req, server_id): - """Returns a list of consoles for this instance""" - consoles = self.console_api.get_consoles( - req.environ['nova.context'], - server_id) - return dict(consoles=[_translate_keys(console) - for console in consoles]) - - def create(self, req, server_id): - """Creates a new console""" - self.console_api.create_console( - req.environ['nova.context'], - server_id) - - @wsgi.serializers(xml=ConsoleTemplate) - def show(self, req, server_id, id): - """Shows in-depth information on a specific console""" - try: - console = self.console_api.get_console( - req.environ['nova.context'], - server_id, - int(id)) - except exception.NotFound: - raise exc.HTTPNotFound() - return _translate_detail_keys(console) - - def update(self, req, server_id, id): - """You can't update a console""" - raise exc.HTTPNotImplemented() - - def delete(self, req, server_id, id): - """Deletes a console""" - try: - self.console_api.delete_console(req.environ['nova.context'], - server_id, - int(id)) - except exception.NotFound: - raise exc.HTTPNotFound() - return webob.Response(status_int=202) - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/contrib/__init__.py b/nova/api/openstack/v2/contrib/__init__.py deleted file mode 100644 index d361dac9c..000000000 --- a/nova/api/openstack/v2/contrib/__init__.py +++ /dev/null @@ -1,90 +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. - -"""Contrib contains extensions that are shipped with nova. - -It can't be called 'extensions' because that causes namespacing problems. - -""" - -import os - -from nova import exception -from nova import log as logging -from nova import utils - - -LOG = logging.getLogger('nova.api.openstack.v2.contrib') - - -def standard_extensions(ext_mgr): - """Registers all standard API extensions.""" - - # Walk through all the modules in our directory... - our_dir = __path__[0] - for dirpath, dirnames, filenames in os.walk(our_dir): - # Compute the relative package name from the dirpath - relpath = os.path.relpath(dirpath, our_dir) - if relpath == '.': - relpkg = '' - else: - relpkg = '.%s' % '.'.join(relpath.split(os.sep)) - - # Now, consider each file in turn, only considering .py files - for fname in filenames: - root, ext = os.path.splitext(fname) - - # Skip __init__ and anything that's not .py - if ext != '.py' or root == '__init__': - continue - - # Try loading it - classname = ("%s%s.%s.%s%s" % - (__package__, relpkg, root, - root[0].upper(), root[1:])) - try: - ext_mgr.load_extension(classname) - except Exception as exc: - LOG.warn(_('Failed to load extension %(classname)s: ' - '%(exc)s') % locals()) - - # Now, let's consider any subdirectories we may have... - subdirs = [] - for dname in dirnames: - # Skip it if it does not have __init__.py - if not os.path.exists(os.path.join(dirpath, dname, - '__init__.py')): - continue - - # If it has extension(), delegate... - ext_name = ("%s%s.%s.extension" % - (__package__, relpkg, dname)) - try: - ext = utils.import_class(ext_name) - except exception.ClassNotFound: - # extension() doesn't exist on it, so we'll explore - # the directory for ourselves - subdirs.append(dname) - else: - try: - ext(ext_mgr) - except Exception as exc: - LOG.warn(_('Failed to load extension %(ext_name)s: ' - '%(exc)s') % locals()) - - # Update the list of directories we'll explore... - dirnames[:] = subdirs diff --git a/nova/api/openstack/v2/contrib/accounts.py b/nova/api/openstack/v2/contrib/accounts.py deleted file mode 100644 index 9bfff31d7..000000000 --- a/nova/api/openstack/v2/contrib/accounts.py +++ /dev/null @@ -1,107 +0,0 @@ -# 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 webob.exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.auth import manager -from nova import exception -from nova import flags -from nova import log as logging - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.api.openstack.v2.contrib.accounts') - - -class AccountTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('account', selector='account') - root.set('id', 'id') - root.set('name', 'name') - root.set('description', 'description') - root.set('manager', 'manager') - - return xmlutil.MasterTemplate(root, 1) - - -def _translate_keys(account): - return dict(id=account.id, - name=account.name, - description=account.description, - manager=account.project_manager_id) - - -class Controller(object): - - 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.AdminRequired() - - def index(self, req): - raise webob.exc.HTTPNotImplemented() - - @wsgi.serializers(xml=AccountTemplate) - 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 create(self, req, body): - """We use update with create-or-update semantics - because the id comes from an external source""" - raise webob.exc.HTTPNotImplemented() - - @wsgi.serializers(xml=AccountTemplate) - def update(self, req, id, body): - """This is really create or update.""" - self._check_admin(req.environ['nova.context']) - description = body['account'].get('description') - manager = body['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)) - - -class Accounts(extensions.ExtensionDescriptor): - """Admin-only access to accounts""" - - name = "Accounts" - alias = "os-accounts" - namespace = "http://docs.openstack.org/compute/ext/accounts/api/v1.1" - updated = "2011-12-23T00:00:00+00:00" - admin_only = True - - def get_resources(self): - #TODO(bcwaldon): This should be prefixed with 'os-' - res = extensions.ResourceExtension('accounts', - Controller()) - - return [res] diff --git a/nova/api/openstack/v2/contrib/admin_actions.py b/nova/api/openstack/v2/contrib/admin_actions.py deleted file mode 100644 index 5479f88c4..000000000 --- a/nova/api/openstack/v2/contrib/admin_actions.py +++ /dev/null @@ -1,291 +0,0 @@ -# Copyright 2011 Openstack, LLC. -# -# 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 os.path -import traceback - -import webob -from webob import exc - -from nova.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova.scheduler import api as scheduler_api - - -FLAGS = flags.FLAGS -LOG = logging.getLogger("nova.api.openstack.v2.contrib.admin_actions") - - -class Admin_actions(extensions.ExtensionDescriptor): - """Enable admin-only server actions - - Actions include: pause, unpause, suspend, resume, migrate, - resetNetwork, injectNetworkInfo, lock, unlock, createBackup - """ - - name = "AdminActions" - alias = "os-admin-actions" - namespace = "http://docs.openstack.org/compute/ext/admin-actions/api/v1.1" - updated = "2011-09-20T00:00:00+00:00" - admin_only = True - - def __init__(self, ext_mgr): - super(Admin_actions, self).__init__(ext_mgr) - self.compute_api = compute.API() - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _pause(self, input_dict, req, id): - """Permit Admins to pause the server""" - ctxt = req.environ['nova.context'] - try: - server = self.compute_api.get(ctxt, id) - self.compute_api.pause(ctxt, server) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'pause') - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::pause %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _unpause(self, input_dict, req, id): - """Permit Admins to unpause the server""" - ctxt = req.environ['nova.context'] - try: - server = self.compute_api.get(ctxt, id) - self.compute_api.unpause(ctxt, server) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'unpause') - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::unpause %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _suspend(self, input_dict, req, id): - """Permit admins to suspend the server""" - context = req.environ['nova.context'] - try: - server = self.compute_api.get(context, id) - self.compute_api.suspend(context, server) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'suspend') - except Exception: - readable = traceback.format_exc() - LOG.exception(_("compute.api::suspend %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _resume(self, input_dict, req, id): - """Permit admins to resume the server from suspend""" - context = req.environ['nova.context'] - try: - server = self.compute_api.get(context, id) - self.compute_api.resume(context, server) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'resume') - except Exception: - readable = traceback.format_exc() - LOG.exception(_("compute.api::resume %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _migrate(self, input_dict, req, id): - """Permit admins to migrate a server to a new host""" - context = req.environ['nova.context'] - try: - instance = self.compute_api.get(context, id) - self.compute_api.resize(req.environ['nova.context'], instance) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'migrate') - except Exception, e: - LOG.exception(_("Error in migrate %s"), e) - raise exc.HTTPBadRequest() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _reset_network(self, input_dict, req, id): - """Permit admins to reset networking on an server""" - context = req.environ['nova.context'] - try: - instance = self.compute_api.get(context, id) - self.compute_api.reset_network(context, instance) - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::reset_network %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _inject_network_info(self, input_dict, req, id): - """Permit admins to inject network info into a server""" - context = req.environ['nova.context'] - try: - instance = self.compute_api.get(context, id) - self.compute_api.inject_network_info(context, instance) - except exception.InstanceNotFound: - raise exc.HTTPNotFound(_("Server not found")) - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::inject_network_info %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _lock(self, input_dict, req, id): - """Permit admins to lock a server""" - context = req.environ['nova.context'] - try: - instance = self.compute_api.get(context, id) - self.compute_api.lock(context, instance) - except exception.InstanceNotFound: - raise exc.HTTPNotFound(_("Server not found")) - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::lock %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - @exception.novaclient_converter - @scheduler_api.redirect_handler - def _unlock(self, input_dict, req, id): - """Permit admins to lock a server""" - context = req.environ['nova.context'] - try: - instance = self.compute_api.get(context, id) - self.compute_api.unlock(context, instance) - except exception.InstanceNotFound: - raise exc.HTTPNotFound(_("Server not found")) - except Exception: - readable = traceback.format_exc() - LOG.exception(_("Compute.api::unlock %s"), readable) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - def _create_backup(self, input_dict, req, instance_id): - """Backup a server instance. - - Images now have an `image_type` associated with them, which can be - 'snapshot' or the backup type, like 'daily' or 'weekly'. - - If the image_type is backup-like, then the rotation factor can be - included and that will cause the oldest backups that exceed the - rotation factor to be deleted. - - """ - context = req.environ["nova.context"] - - try: - entity = input_dict["createBackup"] - except (KeyError, TypeError): - raise exc.HTTPBadRequest(_("Malformed request body")) - - try: - image_name = entity["name"] - backup_type = entity["backup_type"] - rotation = entity["rotation"] - - except KeyError as missing_key: - msg = _("createBackup entity requires %s attribute") % missing_key - raise exc.HTTPBadRequest(explanation=msg) - - except TypeError: - msg = _("Malformed createBackup entity") - raise exc.HTTPBadRequest(explanation=msg) - - try: - rotation = int(rotation) - except ValueError: - msg = _("createBackup attribute 'rotation' must be an integer") - raise exc.HTTPBadRequest(explanation=msg) - - # preserve link to server in image properties - server_ref = os.path.join(req.application_url, 'servers', instance_id) - props = {'instance_ref': server_ref} - - metadata = entity.get('metadata', {}) - common.check_img_metadata_quota_limit(context, metadata) - try: - props.update(metadata) - except ValueError: - msg = _("Invalid metadata") - raise exc.HTTPBadRequest(explanation=msg) - - try: - instance = self.compute_api.get(context, instance_id) - except exception.NotFound: - raise exc.HTTPNotFound(_("Instance not found")) - - try: - image = self.compute_api.backup(context, instance, image_name, - backup_type, rotation, extra_properties=props) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'createBackup') - - # build location of newly-created image entity - image_id = str(image['id']) - image_ref = os.path.join(req.application_url, 'images', image_id) - - resp = webob.Response(status_int=202) - resp.headers['Location'] = image_ref - return resp - - def get_actions(self): - actions = [ - #TODO(bcwaldon): These actions should be prefixed with 'os-' - extensions.ActionExtension("servers", "pause", self._pause), - extensions.ActionExtension("servers", "unpause", self._unpause), - extensions.ActionExtension("servers", "suspend", self._suspend), - extensions.ActionExtension("servers", "resume", self._resume), - extensions.ActionExtension("servers", "migrate", self._migrate), - - extensions.ActionExtension("servers", - "createBackup", - self._create_backup), - - extensions.ActionExtension("servers", - "resetNetwork", - self._reset_network), - - extensions.ActionExtension("servers", - "injectNetworkInfo", - self._inject_network_info), - - extensions.ActionExtension("servers", "lock", self._lock), - extensions.ActionExtension("servers", "unlock", self._unlock), - ] - - return actions diff --git a/nova/api/openstack/v2/contrib/cloudpipe.py b/nova/api/openstack/v2/contrib/cloudpipe.py deleted file mode 100644 index 9327e3987..000000000 --- a/nova/api/openstack/v2/contrib/cloudpipe.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright 2011 Openstack, LLC. -# -# 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. - -"""Connect your vlan to the world.""" - -import os - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova.auth import manager -from nova.cloudpipe import pipelib -from nova import compute -from nova.compute import vm_states -from nova import db -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils - - -FLAGS = flags.FLAGS -LOG = logging.getLogger("nova.api.openstack.v2.contrib.cloudpipe") - - -class CloudpipeTemplate(xmlutil.TemplateBuilder): - def construct(self): - return xmlutil.MasterTemplate(xmlutil.make_flat_dict('cloudpipe'), 1) - - -class CloudpipesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('cloudpipes') - elem = xmlutil.make_flat_dict('cloudpipe', selector='cloudpipes', - subselector='cloudpipe') - root.append(elem) - return xmlutil.MasterTemplate(root, 1) - - -class CloudpipeController(object): - """Handle creating and listing cloudpipe instances.""" - - def __init__(self): - self.compute_api = compute.API() - self.auth_manager = manager.AuthManager() - self.cloudpipe = pipelib.CloudPipe() - self.setup() - - def setup(self): - """Ensure the keychains and folders exist.""" - # TODO(todd): this was copyed from api.ec2.cloud - # FIXME(ja): this should be moved to a nova-manage command, - # if not setup throw exceptions instead of running - # Create keys folder, if it doesn't exist - if not os.path.exists(FLAGS.keys_path): - os.makedirs(FLAGS.keys_path) - # Gen root CA, if we don't have one - root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file) - if not os.path.exists(root_ca_path): - genrootca_sh_path = os.path.join(os.path.dirname(__file__), - os.path.pardir, - os.path.pardir, - 'CA', - 'genrootca.sh') - - start = os.getcwd() - if not os.path.exists(FLAGS.ca_path): - os.makedirs(FLAGS.ca_path) - os.chdir(FLAGS.ca_path) - # TODO(vish): Do this with M2Crypto instead - utils.runthis(_("Generating root CA: %s"), "sh", genrootca_sh_path) - os.chdir(start) - - def _get_cloudpipe_for_project(self, context, project_id): - """Get the cloudpipe instance for a project ID.""" - # NOTE(todd): this should probably change to compute_api.get_all - # or db.instance_get_project_vpn - for instance in db.instance_get_all_by_project(context, project_id): - if (instance['image_id'] == str(FLAGS.vpn_image_id) - and instance['vm_state'] != vm_states.DELETED): - return instance - - def _vpn_dict(self, project, vpn_instance): - rv = {'project_id': project.id, - 'public_ip': project.vpn_ip, - 'public_port': project.vpn_port} - if vpn_instance: - rv['instance_id'] = vpn_instance['uuid'] - rv['created_at'] = utils.isotime(vpn_instance['created_at']) - address = vpn_instance.get('fixed_ip', None) - if address: - rv['internal_ip'] = address['address'] - 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'] = 'invalid' - else: - rv['state'] = 'pending' - return rv - - @wsgi.serializers(xml=CloudpipeTemplate) - def create(self, req, body): - """Create a new cloudpipe instance, if none exists. - - Parameters: {cloudpipe: {project_id: XYZ}} - """ - - ctxt = req.environ['nova.context'] - params = body.get('cloudpipe', {}) - project_id = params.get('project_id', ctxt.project_id) - instance = self._get_cloudpipe_for_project(ctxt, project_id) - if not instance: - proj = self.auth_manager.get_project(project_id) - user_id = proj.project_manager_id - try: - self.cloudpipe.launch_vpn_instance(project_id, user_id) - except db.NoMoreNetworks: - msg = _("Unable to claim IP for VPN instances, ensure it " - "isn't running, and try again in a few minutes") - raise exception.ApiError(msg) - instance = self._get_cloudpipe_for_project(ctxt, proj) - return {'instance_id': instance['uuid']} - - @wsgi.serializers(xml=CloudpipesTemplate) - def index(self, req): - """Show admins the list of running cloudpipe instances.""" - context = req.environ['nova.context'] - vpns = [] - # TODO(todd): could use compute_api.get_all with admin context? - for project in self.auth_manager.get_projects(): - instance = self._get_cloudpipe_for_project(context, project.id) - vpns.append(self._vpn_dict(project, instance)) - return {'cloudpipes': vpns} - - -class Cloudpipe(extensions.ExtensionDescriptor): - """Adds actions to create cloudpipe instances. - - When running with the Vlan network mode, you need a mechanism to route - from the public Internet to your vlans. This mechanism is known as a - cloudpipe. - - At the time of creating this class, only OpenVPN is supported. Support for - a SSH Bastion host is forthcoming. - """ - - name = "Cloudpipe" - alias = "os-cloudpipe" - namespace = "http://docs.openstack.org/compute/ext/cloudpipe/api/v1.1" - updated = "2011-12-16T00:00:00+00:00" - admin_only = True - - def get_resources(self): - resources = [] - res = extensions.ResourceExtension('os-cloudpipe', - CloudpipeController()) - resources.append(res) - return resources diff --git a/nova/api/openstack/v2/contrib/console_output.py b/nova/api/openstack/v2/contrib/console_output.py deleted file mode 100644 index 0c9196398..000000000 --- a/nova/api/openstack/v2/contrib/console_output.py +++ /dev/null @@ -1,73 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# Copyright 2011 Grid Dynamics -# Copyright 2011 Eldar Nugaev, Kirill Shileev, Ilya Alekseyev -# -# 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 - -from nova import compute -from nova import exception -from nova import log as logging -from nova.api.openstack.v2 import extensions - - -LOG = logging.getLogger('nova.api.openstack.v2.contrib.console_output') - - -class Console_output(extensions.ExtensionDescriptor): - """Console log output support, with tailing ability.""" - - name = "Console_output" - alias = "os-console-output" - namespace = "http://docs.openstack.org/compute/ext/" \ - "os-console-output/api/v2" - updated = "2011-12-08T00:00:00+00:00" - - def __init__(self, ext_mgr): - self.compute_api = compute.API() - super(Console_output, self).__init__(ext_mgr) - - def get_console_output(self, input_dict, req, server_id): - """Get text console output.""" - context = req.environ['nova.context'] - - try: - instance = self.compute_api.routing_get(context, server_id) - except exception.NotFound: - raise webob.exc.HTTPNotFound(_('Instance not found')) - - try: - length = input_dict['os-getConsoleOutput'].get('length') - except (TypeError, KeyError): - raise webob.exc.HTTPBadRequest(_('Malformed request body')) - - try: - output = self.compute_api.get_console_output(context, - instance, - length) - except exception.ApiError, e: - raise webob.exc.HTTPBadRequest(explanation=e.message) - except exception.NotAuthorized, e: - raise webob.exc.HTTPUnauthorized() - - return {'output': output} - - def get_actions(self): - """Return the actions the extension adds, as required by contract.""" - actions = [extensions.ActionExtension("servers", "os-getConsoleOutput", - self.get_console_output)] - - return actions diff --git a/nova/api/openstack/v2/contrib/createserverext.py b/nova/api/openstack/v2/contrib/createserverext.py deleted file mode 100644 index 70fe2796f..000000000 --- a/nova/api/openstack/v2/contrib/createserverext.py +++ /dev/null @@ -1,60 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# -# 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.v2 import extensions -from nova.api.openstack.v2 import servers -from nova.api.openstack.v2 import views -from nova.api.openstack import wsgi - - -class ViewBuilder(views.servers.ViewBuilder): - """Adds security group output when viewing server details.""" - - def show(self, request, instance): - """Detailed view of a single instance.""" - server = super(ViewBuilder, self).show(request, instance) - server["server"]["security_groups"] = self._get_groups(instance) - return server - - def _get_groups(self, instance): - """Get a list of security groups for this instance.""" - groups = instance.get('security_groups') - if groups is not None: - return [{"name": group["name"]} for group in groups] - - -class Controller(servers.Controller): - _view_builder_class = ViewBuilder - - -class Createserverext(extensions.ExtensionDescriptor): - """Extended support to the Create Server v1.1 API""" - - name = "Createserverext" - alias = "os-create-server-ext" - namespace = "http://docs.openstack.org/compute/ext/" \ - "createserverext/api/v1.1" - updated = "2011-07-19T00:00:00+00:00" - - def get_resources(self): - resources = [] - controller = Controller() - - res = extensions.ResourceExtension('os-create-server-ext', - controller=controller) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/deferred_delete.py b/nova/api/openstack/v2/contrib/deferred_delete.py deleted file mode 100644 index 0b7c60073..000000000 --- a/nova/api/openstack/v2/contrib/deferred_delete.py +++ /dev/null @@ -1,77 +0,0 @@ -# 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. - -"""The deferred instance delete extension.""" - -import webob - -from nova.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova.api.openstack.v2 import servers -from nova import compute -from nova import exception -from nova import log as logging - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.deferred-delete") - - -class Deferred_delete(extensions.ExtensionDescriptor): - """Instance deferred delete""" - - name = "DeferredDelete" - alias = "os-deferred-delete" - namespace = "http://docs.openstack.org/compute/ext/" \ - "deferred-delete/api/v1.1" - updated = "2011-09-01T00:00:00+00:00" - - def __init__(self, ext_mgr): - super(Deferred_delete, self).__init__(ext_mgr) - self.compute_api = compute.API() - - def _restore(self, input_dict, req, instance_id): - """Restore a previously deleted instance.""" - - context = req.environ["nova.context"] - instance = self.compute_api.get(context, instance_id) - try: - self.compute_api.restore(context, instance) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'restore') - return webob.Response(status_int=202) - - def _force_delete(self, input_dict, req, instance_id): - """Force delete of instance before deferred cleanup.""" - - context = req.environ["nova.context"] - instance = self.compute_api.get(context, instance_id) - try: - self.compute_api.force_delete(context, instance) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'forceDelete') - return webob.Response(status_int=202) - - def get_actions(self): - """Return the actions the extension adds, as required by contract.""" - actions = [ - extensions.ActionExtension("servers", "restore", - self._restore), - extensions.ActionExtension("servers", "forceDelete", - self._force_delete), - ] - - return actions diff --git a/nova/api/openstack/v2/contrib/disk_config.py b/nova/api/openstack/v2/contrib/disk_config.py deleted file mode 100644 index 7ce24a3fd..000000000 --- a/nova/api/openstack/v2/contrib/disk_config.py +++ /dev/null @@ -1,200 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# -# 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 - -"""Disk Config extension.""" - -from xml.dom import minidom - -from webob import exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import xmlutil -from nova import compute -from nova import db -from nova import log as logging -from nova import utils - -LOG = logging.getLogger('nova.api.openstack.contrib.disk_config') - -ALIAS = 'RAX-DCF' -XMLNS_DCF = "http://docs.rackspacecloud.com/servers/api/ext/diskConfig/v1.0" - - -class ServerDiskConfigTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('server') - root.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) - return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) - - -class ServersDiskConfigTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('servers') - elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') - elem.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) - return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) - - -class ImageDiskConfigTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('image') - root.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) - return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) - - -class ImagesDiskConfigTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('images') - elem = xmlutil.SubTemplateElement(root, 'image', selector='images') - elem.set('{%s}diskConfig' % XMLNS_DCF, '%s:diskConfig' % ALIAS) - return xmlutil.SlaveTemplate(root, 1, nsmap={ALIAS: XMLNS_DCF}) - - -def disk_config_to_api(value): - return 'AUTO' if value else 'MANUAL' - - -def disk_config_from_api(value): - if value == 'AUTO': - return True - elif value == 'MANUAL': - return False - else: - msg = _("RAX-DCF:diskConfig must be either 'MANUAL' or 'AUTO'.") - raise exc.HTTPBadRequest(explanation=msg) - - -class Disk_config(extensions.ExtensionDescriptor): - """Disk Management Extension""" - - name = "DiskConfig" - alias = ALIAS - namespace = XMLNS_DCF - updated = "2011-09-27:00:00+00:00" - - API_DISK_CONFIG = "%s:diskConfig" % ALIAS - INTERNAL_DISK_CONFIG = "auto_disk_config" - - def __init__(self, ext_mgr): - super(Disk_config, self).__init__(ext_mgr) - self.compute_api = compute.API() - - def _extract_resource_from_body(self, res, body, - singular, singular_template, plural, plural_template): - """Returns a list of the given resources from the request body. - - The templates passed in are used for XML serialization. - """ - template = res.environ.get('nova.template') - if plural in body: - resources = body[plural] - if template: - template.attach(plural_template) - elif singular in body: - resources = [body[singular]] - if template: - template.attach(singular_template) - else: - resources = [] - - return resources - - def _GET_servers(self, req, res, body): - context = req.environ['nova.context'] - - servers = self._extract_resource_from_body(res, body, - singular='server', singular_template=ServerDiskConfigTemplate(), - plural='servers', plural_template=ServersDiskConfigTemplate()) - - # Filter out any servers that already have the key set (most likely - # from a remote zone) - servers = filter(lambda s: self.API_DISK_CONFIG not in s, servers) - - # Get DB information for servers - uuids = [server['id'] for server in servers] - db_servers = db.instance_get_all_by_filters(context, {'uuid': uuids}) - db_servers = dict([(s['uuid'], s) for s in db_servers]) - - for server in servers: - db_server = db_servers.get(server['id']) - if db_server: - value = db_server[self.INTERNAL_DISK_CONFIG] - server[self.API_DISK_CONFIG] = disk_config_to_api(value) - - return res - - def _GET_images(self, req, res, body): - images = self._extract_resource_from_body(res, body, - singular='image', singular_template=ImageDiskConfigTemplate(), - plural='images', plural_template=ImagesDiskConfigTemplate()) - - for image in images: - metadata = image['metadata'] - - if self.INTERNAL_DISK_CONFIG in metadata: - raw_value = metadata[self.INTERNAL_DISK_CONFIG] - value = utils.bool_from_str(raw_value) - image[self.API_DISK_CONFIG] = disk_config_to_api(value) - - return res - - def _POST_servers(self, req, res, body): - return self._GET_servers(req, res, body) - - def _pre_POST_servers(self, req): - # NOTE(sirp): deserialization currently occurs *after* pre-processing - # extensions are called. Until extensions are refactored so that - # deserialization occurs earlier, we have to perform the - # deserialization ourselves. - content_type = req.content_type - - if 'xml' in content_type: - node = minidom.parseString(req.body) - server = node.getElementsByTagName('server')[0] - api_value = server.getAttribute(self.API_DISK_CONFIG) - if api_value: - value = disk_config_from_api(api_value) - server.setAttribute(self.INTERNAL_DISK_CONFIG, str(value)) - req.body = str(node.toxml()) - else: - body = utils.loads(req.body) - server = body['server'] - api_value = server.get(self.API_DISK_CONFIG) - if api_value: - value = disk_config_from_api(api_value) - server[self.INTERNAL_DISK_CONFIG] = value - req.body = utils.dumps(body) - - def _pre_PUT_servers(self, req): - return self._pre_POST_servers(req) - - def get_request_extensions(self): - ReqExt = extensions.RequestExtension - return [ - ReqExt(method='GET', - url_route='/:(project_id)/servers/:(id)', - handler=self._GET_servers), - ReqExt(method='POST', - url_route='/:(project_id)/servers', - handler=self._POST_servers, - pre_handler=self._pre_POST_servers), - ReqExt(method='PUT', - url_route='/:(project_id)/servers/:(id)', - pre_handler=self._pre_PUT_servers), - ReqExt(method='GET', - url_route='/:(project_id)/images/:(id)', - handler=self._GET_images) - ] diff --git a/nova/api/openstack/v2/contrib/extended_status.py b/nova/api/openstack/v2/contrib/extended_status.py deleted file mode 100644 index a3b0410f6..000000000 --- a/nova/api/openstack/v2/contrib/extended_status.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2011 Openstack, LLC. -# -# 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 Extended Status Admin API extension.""" - -from webob import exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import xmlutil -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging - - -FLAGS = flags.FLAGS -LOG = logging.getLogger("nova.api.openstack.v2.contrib.extendedstatus") - - -class Extended_status(extensions.ExtensionDescriptor): - """Extended Status support""" - - name = "ExtendedStatus" - alias = "OS-EXT-STS" - namespace = "http://docs.openstack.org/compute/ext/" \ - "extended_status/api/v1.1" - updated = "2011-11-03T00:00:00+00:00" - admin_only = True - - def get_request_extensions(self): - request_extensions = [] - - def _get_and_extend_one(context, server_id, body): - compute_api = compute.API() - try: - inst_ref = compute_api.routing_get(context, server_id) - except exception.NotFound: - LOG.warn("Instance %s not found (one)" % server_id) - explanation = _("Server not found.") - raise exc.HTTPNotFound(explanation=explanation) - - for state in ['task_state', 'vm_state', 'power_state']: - key = "%s:%s" % (Extended_status.alias, state) - body['server'][key] = inst_ref[state] - - def _get_and_extend_all(context, body): - # TODO(mdietz): This is a brilliant argument for this to *not* - # be an extension. The problem is we either have to 1) duplicate - # the logic from the servers controller or 2) do what we did - # and iterate over the list of potentially sorted, limited - # and whatever else elements and find each individual. - compute_api = compute.API() - - for server in list(body['servers']): - try: - inst_ref = compute_api.routing_get(context, server['id']) - except exception.NotFound: - # NOTE(dtroyer): A NotFound exception at this point - # happens because a delete was in progress and the - # server that was present in the original call to - # compute.api.get_all() is no longer present. - # Delete it from the response and move on. - LOG.warn("Instance %s not found (all)" % server['id']) - body['servers'].remove(server) - continue - - #TODO(bcwaldon): these attributes should be prefixed with - # something specific to this extension - for state in ['task_state', 'vm_state', 'power_state']: - key = "%s:%s" % (Extended_status.alias, state) - server[key] = inst_ref[state] - - def _extended_status_handler(req, res, body): - context = req.environ['nova.context'] - server_id = req.environ['wsgiorg.routing_args'][1].get('id') - - if 'nova.template' in req.environ: - tmpl = req.environ['nova.template'] - tmpl.attach(ExtendedStatusTemplate()) - - if server_id: - _get_and_extend_one(context, server_id, body) - else: - _get_and_extend_all(context, body) - return res - - req_ext = extensions.RequestExtension('GET', - '/:(project_id)/servers/:(id)', - _extended_status_handler) - request_extensions.append(req_ext) - - return request_extensions - - -class ExtendedStatusTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('server') - root.set('{%s}task_state' % Extended_status.namespace, - '%s:task_state' % Extended_status.alias) - root.set('{%s}power_state' % Extended_status.namespace, - '%s:power_state' % Extended_status.alias) - root.set('{%s}vm_state' % Extended_status.namespace, - '%s:vm_state' % Extended_status.alias) - return xmlutil.SlaveTemplate(root, 1, nsmap={ - Extended_status.alias: Extended_status.namespace}) diff --git a/nova/api/openstack/v2/contrib/flavorextradata.py b/nova/api/openstack/v2/contrib/flavorextradata.py deleted file mode 100644 index 462ad1aba..000000000 --- a/nova/api/openstack/v2/contrib/flavorextradata.py +++ /dev/null @@ -1,37 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Canonical Ltd. -# 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 Flavor extra data extension -Openstack API version 1.1 lists "name", "ram", "disk", "vcpus" as flavor -attributes. This extension adds to that list: - rxtx_cap - rxtx_quota - swap -""" - -from nova.api.openstack.v2 import extensions - - -class Flavorextradata(extensions.ExtensionDescriptor): - """Provide additional data for flavors""" - - name = "FlavorExtraData" - alias = "os-flavor-extra-data" - namespace = "http://docs.openstack.org/compute/ext/" \ - "flavor_extra_data/api/v1.1" - updated = "2011-09-14T00:00:00+00:00" diff --git a/nova/api/openstack/v2/contrib/flavorextraspecs.py b/nova/api/openstack/v2/contrib/flavorextraspecs.py deleted file mode 100644 index cb5b572fa..000000000 --- a/nova/api/openstack/v2/contrib/flavorextraspecs.py +++ /dev/null @@ -1,127 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 University of Southern California -# 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 instance type extra specs extension""" - -from webob import exc - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import db -from nova import exception - - -class ExtraSpecsTemplate(xmlutil.TemplateBuilder): - def construct(self): - return xmlutil.MasterTemplate(xmlutil.make_flat_dict('extra_specs'), 1) - - -class FlavorExtraSpecsController(object): - """ The flavor extra specs API controller for the Openstack API """ - - def _get_extra_specs(self, context, flavor_id): - extra_specs = db.instance_type_extra_specs_get(context, flavor_id) - specs_dict = {} - for key, value in extra_specs.iteritems(): - specs_dict[key] = value - return dict(extra_specs=specs_dict) - - def _check_body(self, body): - if body is None or body == "": - expl = _('No Request Body') - raise exc.HTTPBadRequest(explanation=expl) - - @wsgi.serializers(xml=ExtraSpecsTemplate) - def index(self, req, flavor_id): - """ Returns the list of extra specs for a givenflavor """ - context = req.environ['nova.context'] - return self._get_extra_specs(context, flavor_id) - - @wsgi.serializers(xml=ExtraSpecsTemplate) - def create(self, req, flavor_id, body): - self._check_body(body) - context = req.environ['nova.context'] - specs = body.get('extra_specs') - try: - db.instance_type_extra_specs_update_or_create(context, - flavor_id, - specs) - except exception.QuotaError as error: - self._handle_quota_error(error) - return body - - @wsgi.serializers(xml=ExtraSpecsTemplate) - def update(self, req, flavor_id, id, body): - self._check_body(body) - context = req.environ['nova.context'] - 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) - try: - db.instance_type_extra_specs_update_or_create(context, - flavor_id, - body) - except exception.QuotaError as error: - self._handle_quota_error(error) - - return body - - @wsgi.serializers(xml=ExtraSpecsTemplate) - def show(self, req, flavor_id, id): - """ Return a single extra spec item """ - context = req.environ['nova.context'] - specs = self._get_extra_specs(context, flavor_id) - if id in specs['extra_specs']: - return {id: specs['extra_specs'][id]} - else: - raise exc.HTTPNotFound() - - def delete(self, req, flavor_id, id): - """ Deletes an existing extra spec """ - context = req.environ['nova.context'] - db.instance_type_extra_specs_delete(context, flavor_id, id) - - def _handle_quota_error(self, error): - """Reraise quota errors as api-specific http exceptions.""" - if error.code == "MetadataLimitExceeded": - raise exc.HTTPBadRequest(explanation=error.message) - raise error - - -class Flavorextraspecs(extensions.ExtensionDescriptor): - """Instance type (flavor) extra specs""" - - name = "FlavorExtraSpecs" - alias = "os-flavor-extra-specs" - namespace = "http://docs.openstack.org/compute/ext/" \ - "flavor_extra_specs/api/v1.1" - updated = "2011-06-23T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension( - 'os-extra_specs', - FlavorExtraSpecsController(), - parent=dict(member_name='flavor', collection_name='flavors')) - - resources.append(res) - return resources diff --git a/nova/api/openstack/v2/contrib/floating_ip_dns.py b/nova/api/openstack/v2/contrib/floating_ip_dns.py deleted file mode 100644 index de1a0a27e..000000000 --- a/nova/api/openstack/v2/contrib/floating_ip_dns.py +++ /dev/null @@ -1,227 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Andrew Bogott for the Wikimedia Foundation -# -# 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 urllib - -import webob - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import exception -from nova import log as logging -from nova import network - - -LOG = logging.getLogger('nova.api.openstack.v2.contrib.floating_ip_dns') - - -def make_dns_entry(elem): - elem.set('id') - elem.set('ip') - elem.set('type') - elem.set('zone') - elem.set('name') - - -def make_zone_entry(elem): - elem.set('zone') - - -class FloatingIPDNSTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('dns_entry', - selector='dns_entry') - make_dns_entry(root) - return xmlutil.MasterTemplate(root, 1) - - -class FloatingIPDNSsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('dns_entries') - elem = xmlutil.SubTemplateElement(root, 'dns_entry', - selector='dns_entries') - make_dns_entry(elem) - return xmlutil.MasterTemplate(root, 1) - - -class ZonesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('zones') - elem = xmlutil.SubTemplateElement(root, 'zone', - selector='zones') - make_zone_entry(elem) - return xmlutil.MasterTemplate(root, 1) - - -def _translate_dns_entry_view(dns_entry): - result = {} - result['ip'] = dns_entry.get('ip') - result['id'] = dns_entry.get('id') - result['type'] = dns_entry.get('type') - result['zone'] = dns_entry.get('zone') - result['name'] = dns_entry.get('name') - return {'dns_entry': result} - - -def _translate_dns_entries_view(dns_entries): - return {'dns_entries': [_translate_dns_entry_view(entry)['dns_entry'] - for entry in dns_entries]} - - -def _translate_zone_entries_view(zonelist): - return {'zones': [{'zone': zone} for zone in zonelist]} - - -def _unquote_zone(zone): - """Unquoting function for receiving a zone name in a URL. - - Zone names tend to have .'s in them. Urllib doesn't quote dots, - but Routes tends to choke on them, so we need an extra level of - by-hand quoting here. - """ - return urllib.unquote(zone).replace('%2E', '.') - - -def _create_dns_entry(ip, name, zone): - return {'ip': ip, 'name': name, 'zone': zone} - - -class FloatingIPDNSController(object): - """DNS Entry controller for OpenStack API""" - - def __init__(self): - self.network_api = network.API() - super(FloatingIPDNSController, self).__init__() - - @wsgi.serializers(xml=FloatingIPDNSsTemplate) - def show(self, req, id): - """Return a list of dns entries. If ip is specified, query for - names. if name is specified, query for ips. - Quoted domain (aka 'zone') specified as id.""" - context = req.environ['nova.context'] - params = req.GET - floating_ip = params['ip'] if 'ip' in params else "" - name = params['name'] if 'name' in params else "" - zone = _unquote_zone(id) - - if floating_ip: - entries = self.network_api.get_dns_entries_by_address(context, - floating_ip, - zone) - entrylist = [_create_dns_entry(floating_ip, entry, zone) - for entry in entries] - elif name: - entries = self.network_api.get_dns_entries_by_name(context, - name, zone) - entrylist = [_create_dns_entry(entry, name, zone) - for entry in entries] - else: - entrylist = [] - - return _translate_dns_entries_view(entrylist) - - @wsgi.serializers(xml=ZonesTemplate) - def index(self, req): - """Return a list of available DNS zones.""" - - context = req.environ['nova.context'] - zones = self.network_api.get_dns_zones(context) - - return _translate_zone_entries_view(zones) - - @wsgi.serializers(xml=FloatingIPDNSTemplate) - def create(self, req, body): - """Add dns entry for name and address""" - context = req.environ['nova.context'] - - try: - entry = body['dns_entry'] - address = entry['ip'] - name = entry['name'] - dns_type = entry['dns_type'] - zone = entry['zone'] - except (TypeError, KeyError): - raise webob.exc.HTTPUnprocessableEntity() - - try: - self.network_api.add_dns_entry(context, address, name, - dns_type, zone) - except exception.FloatingIpDNSExists: - return webob.Response(status_int=409) - - return _translate_dns_entry_view({'ip': address, - 'name': name, - 'type': dns_type, - 'zone': zone}) - - def update(self, req, id, body): - """Modify a dns entry.""" - context = req.environ['nova.context'] - zone = _unquote_zone(id) - - try: - entry = body['dns_entry'] - name = entry['name'] - new_ip = entry['ip'] - except (TypeError, KeyError): - raise webob.exc.HTTPUnprocessableEntity() - - try: - self.network_api.modify_dns_entry(context, name, - new_ip, zone) - except exception.NotFound: - return webob.Response(status_int=404) - - return _translate_dns_entry_view({'ip': new_ip, - 'name': name, - 'zone': zone}) - - def delete(self, req, id): - """Delete the entry identified by req and id. """ - context = req.environ['nova.context'] - params = req.GET - name = params['name'] if 'name' in params else "" - zone = _unquote_zone(id) - - try: - self.network_api.delete_dns_entry(context, name, zone) - except exception.NotFound: - return webob.Response(status_int=404) - - return webob.Response(status_int=200) - - -class Floating_ip_dns(extensions.ExtensionDescriptor): - """Floating IP DNS support""" - - name = "Floating_ip_dns" - alias = "os-floating-ip-dns" - namespace = "http://docs.openstack.org/ext/floating_ip_dns/api/v1.1" - updated = "2011-12-23:00:00+00:00" - - def __init__(self, ext_mgr): - self.network_api = network.API() - super(Floating_ip_dns, self).__init__(ext_mgr) - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-floating-ip-dns', - FloatingIPDNSController()) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/floating_ip_pools.py b/nova/api/openstack/v2/contrib/floating_ip_pools.py deleted file mode 100644 index 9d6386f25..000000000 --- a/nova/api/openstack/v2/contrib/floating_ip_pools.py +++ /dev/null @@ -1,104 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# -# 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 wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import log as logging -from nova import network - - -LOG = logging.getLogger('nova.api.openstack.v2.contrib.floating_ip_poolss') - - -def _translate_floating_ip_view(pool): - return { - 'name': pool['name'], - } - - -def _translate_floating_ip_pools_view(pools): - return { - 'floating_ip_pools': [_translate_floating_ip_view(pool) - for pool in pools] - } - - -class FloatingIPPoolsController(object): - """The Floating IP Pool API controller for the OpenStack API.""" - - def __init__(self): - self.network_api = network.API() - super(FloatingIPPoolsController, self).__init__() - - def index(self, req): - """Return a list of pools.""" - context = req.environ['nova.context'] - pools = self.network_api.get_floating_ip_pools(context) - return _translate_floating_ip_pools_view(pools) - - -def make_float_ip(elem): - elem.set('name') - - -class FloatingIPPoolTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('floating_ip_pool', - selector='floating_ip_pool') - make_float_ip(root) - return xmlutil.MasterTemplate(root, 1) - - -class FloatingIPPoolsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('floating_ip_pools') - elem = xmlutil.SubTemplateElement(root, 'floating_ip_pool', - selector='floating_ip_pools') - make_float_ip(elem) - return xmlutil.MasterTemplate(root, 1) - - -class FloatingIPPoolsSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return FloatingIPPoolsTemplate() - - -class Floating_ip_pools(extensions.ExtensionDescriptor): - """Floating IPs support""" - - name = "Floating_ip_pools" - alias = "os-floating-ip-pools" - namespace = \ - "http://docs.openstack.org/compute/ext/floating_ip_pools/api/v1.1" - updated = "2012-01-04T00:00:00+00:00" - - def get_resources(self): - resources = [] - - body_serializers = { - 'application/xml': FloatingIPPoolsSerializer(), - } - - serializer = wsgi.ResponseSerializer(body_serializers) - - res = extensions.ResourceExtension('os-floating-ip-pools', - FloatingIPPoolsController(), - serializer=serializer, - member_actions={}) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/floating_ips.py b/nova/api/openstack/v2/contrib/floating_ips.py deleted file mode 100644 index 3a6f4ee34..000000000 --- a/nova/api/openstack/v2/contrib/floating_ips.py +++ /dev/null @@ -1,237 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2011 Grid Dynamics -# Copyright 2011 Eldar Nugaev, Kirill Shileev, Ilya Alekseyev -# -# 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 - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import compute -from nova import exception -from nova import log as logging -from nova import network -from nova import rpc - - -LOG = logging.getLogger('nova.api.openstack.v2.contrib.floating_ips') - - -def make_float_ip(elem): - elem.set('id') - elem.set('ip') - elem.set('pool') - elem.set('fixed_ip') - elem.set('instance_id') - - -class FloatingIPTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('floating_ip', - selector='floating_ip') - make_float_ip(root) - return xmlutil.MasterTemplate(root, 1) - - -class FloatingIPsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('floating_ips') - elem = xmlutil.SubTemplateElement(root, 'floating_ip', - selector='floating_ips') - make_float_ip(elem) - return xmlutil.MasterTemplate(root, 1) - - -def _translate_floating_ip_view(floating_ip): - result = { - 'id': floating_ip['id'], - 'ip': floating_ip['address'], - 'pool': floating_ip['pool'], - } - try: - result['fixed_ip'] = floating_ip['fixed_ip']['address'] - except (TypeError, KeyError): - result['fixed_ip'] = None - try: - result['instance_id'] = floating_ip['fixed_ip']['instance']['uuid'] - except (TypeError, KeyError): - result['instance_id'] = None - return {'floating_ip': result} - - -def _translate_floating_ips_view(floating_ips): - return {'floating_ips': [_translate_floating_ip_view(ip)['floating_ip'] - for ip in floating_ips]} - - -class FloatingIPController(object): - """The Floating IPs API controller for the OpenStack API.""" - - def __init__(self): - self.network_api = network.API() - super(FloatingIPController, self).__init__() - - @wsgi.serializers(xml=FloatingIPTemplate) - def show(self, req, id): - """Return data about the given floating ip.""" - context = req.environ['nova.context'] - - try: - floating_ip = self.network_api.get_floating_ip(context, id) - except exception.NotFound: - raise webob.exc.HTTPNotFound() - - return _translate_floating_ip_view(floating_ip) - - @wsgi.serializers(xml=FloatingIPsTemplate) - def index(self, req): - """Return a list of floating ips allocated to a project.""" - context = req.environ['nova.context'] - - floating_ips = self.network_api.get_floating_ips_by_project(context) - - return _translate_floating_ips_view(floating_ips) - - @wsgi.serializers(xml=FloatingIPTemplate) - def create(self, req, body=None): - context = req.environ['nova.context'] - - pool = None - if body and 'pool' in body: - pool = body['pool'] - try: - address = self.network_api.allocate_floating_ip(context, pool) - ip = self.network_api.get_floating_ip_by_address(context, address) - except rpc.RemoteError as ex: - # NOTE(tr3buchet) - why does this block exist? - if ex.exc_type == 'NoMoreFloatingIps': - if pool: - msg = _("No more floating ips in pool %s.") % pool - else: - msg = _("No more floating ips available.") - raise webob.exc.HTTPBadRequest(explanation=msg) - else: - raise - - return _translate_floating_ip_view(ip) - - def delete(self, req, id): - context = req.environ['nova.context'] - floating_ip = self.network_api.get_floating_ip(context, id) - - if floating_ip.get('fixed_ip'): - self.network_api.disassociate_floating_ip(context, - floating_ip['address']) - - self.network_api.release_floating_ip(context, - address=floating_ip['address']) - return webob.Response(status_int=202) - - def _get_ip_by_id(self, context, value): - """Checks that value is id and then returns its address.""" - return self.network_api.get_floating_ip(context, value)['address'] - - -class FloatingIPSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return FloatingIPsTemplate() - - def default(self): - return FloatingIPTemplate() - - -class Floating_ips(extensions.ExtensionDescriptor): - """Floating IPs support""" - - name = "Floating_ips" - alias = "os-floating-ips" - namespace = "http://docs.openstack.org/compute/ext/floating_ips/api/v1.1" - updated = "2011-06-16T00:00:00+00:00" - - def __init__(self, ext_mgr): - self.compute_api = compute.API() - self.network_api = network.API() - super(Floating_ips, self).__init__(ext_mgr) - - def _add_floating_ip(self, input_dict, req, instance_id): - """Associate floating_ip to an instance.""" - context = req.environ['nova.context'] - - try: - address = input_dict['addFloatingIp']['address'] - except TypeError: - msg = _("Missing parameter dict") - raise webob.exc.HTTPBadRequest(explanation=msg) - except KeyError: - msg = _("Address not specified") - raise webob.exc.HTTPBadRequest(explanation=msg) - - try: - instance = self.compute_api.get(context, instance_id) - self.compute_api.associate_floating_ip(context, instance, - address) - except exception.ApiError, e: - raise webob.exc.HTTPBadRequest(explanation=e.message) - except exception.NotAuthorized, e: - raise webob.exc.HTTPUnauthorized() - - return webob.Response(status_int=202) - - def _remove_floating_ip(self, input_dict, req, instance_id): - """Dissociate floating_ip from an instance.""" - context = req.environ['nova.context'] - - try: - address = input_dict['removeFloatingIp']['address'] - except TypeError: - msg = _("Missing parameter dict") - raise webob.exc.HTTPBadRequest(explanation=msg) - except KeyError: - msg = _("Address not specified") - raise webob.exc.HTTPBadRequest(explanation=msg) - - floating_ip = self.network_api.get_floating_ip_by_address(context, - address) - if floating_ip.get('fixed_ip'): - try: - self.network_api.disassociate_floating_ip(context, address) - except exception.NotAuthorized, e: - raise webob.exc.HTTPUnauthorized() - - return webob.Response(status_int=202) - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-floating-ips', - FloatingIPController(), - member_actions={}) - resources.append(res) - - return resources - - def get_actions(self): - """Return the actions the extension adds, as required by contract.""" - actions = [ - extensions.ActionExtension("servers", "addFloatingIp", - self._add_floating_ip), - extensions.ActionExtension("servers", "removeFloatingIp", - self._remove_floating_ip), - ] - - return actions diff --git a/nova/api/openstack/v2/contrib/hosts.py b/nova/api/openstack/v2/contrib/hosts.py deleted file mode 100644 index 2bb61696e..000000000 --- a/nova/api/openstack/v2/contrib/hosts.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright (c) 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. - -"""The hosts admin extension.""" - -import webob.exc -from xml.dom import minidom -from xml.parsers import expat - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova.scheduler import api as scheduler_api - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.hosts") -FLAGS = flags.FLAGS - - -class HostIndexTemplate(xmlutil.TemplateBuilder): - def construct(self): - def shimmer(obj, do_raise=False): - # A bare list is passed in; we need to wrap it in a dict - return dict(hosts=obj) - - root = xmlutil.TemplateElement('hosts', selector=shimmer) - elem = xmlutil.SubTemplateElement(root, 'host', selector='hosts') - elem.set('host_name') - elem.set('service') - - return xmlutil.MasterTemplate(root, 1) - - -class HostUpdateTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('host') - root.set('host') - root.set('status') - - return xmlutil.MasterTemplate(root, 1) - - -class HostActionTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('host') - root.set('host') - root.set('power_action') - - return xmlutil.MasterTemplate(root, 1) - - -class HostDeserializer(wsgi.XMLDeserializer): - def default(self, string): - try: - node = minidom.parseString(string) - except expat.ExpatError: - msg = _("cannot understand XML") - raise exception.MalformedRequestBody(reason=msg) - - updates = {} - for child in node.childNodes[0].childNodes: - updates[child.tagName] = self.extract_text(child) - - return dict(body=updates) - - -def _list_hosts(req, service=None): - """Returns a summary list of hosts, optionally filtering - by service type. - """ - context = req.environ['nova.context'] - hosts = scheduler_api.get_host_list(context) - if service: - hosts = [host for host in hosts - if host["service"] == service] - return hosts - - -def check_host(fn): - """Makes sure that the host exists.""" - def wrapped(self, req, id, service=None, *args, **kwargs): - listed_hosts = _list_hosts(req, service) - hosts = [h["host_name"] for h in listed_hosts] - if id in hosts: - return fn(self, req, id, *args, **kwargs) - else: - raise exception.HostNotFound(host=id) - return wrapped - - -class HostController(object): - """The Hosts API controller for the OpenStack API.""" - def __init__(self): - self.compute_api = compute.API() - super(HostController, self).__init__() - - @wsgi.serializers(xml=HostIndexTemplate) - def index(self, req): - return {'hosts': _list_hosts(req)} - - @wsgi.serializers(xml=HostUpdateTemplate) - @wsgi.deserializers(xml=HostDeserializer) - @check_host - def update(self, req, id, body): - for raw_key, raw_val in body.iteritems(): - key = raw_key.lower().strip() - val = raw_val.lower().strip() - # NOTE: (dabo) Right now only 'status' can be set, but other - # settings may follow. - if key == "status": - if val[:6] in ("enable", "disabl"): - return self._set_enabled_status(req, id, - enabled=(val.startswith("enable"))) - else: - explanation = _("Invalid status: '%s'") % raw_val - raise webob.exc.HTTPBadRequest(explanation=explanation) - else: - explanation = _("Invalid update setting: '%s'") % raw_key - raise webob.exc.HTTPBadRequest(explanation=explanation) - - def _set_enabled_status(self, req, host, enabled): - """Sets the specified host's ability to accept new instances.""" - context = req.environ['nova.context'] - state = "enabled" if enabled else "disabled" - LOG.audit(_("Setting host %(host)s to %(state)s.") % locals()) - result = self.compute_api.set_host_enabled(context, host=host, - enabled=enabled) - if result not in ("enabled", "disabled"): - # An error message was returned - raise webob.exc.HTTPBadRequest(explanation=result) - return {"host": host, "status": result} - - def _host_power_action(self, req, host, action): - """Reboots, shuts down or powers up the host.""" - context = req.environ['nova.context'] - try: - result = self.compute_api.host_power_action(context, host=host, - action=action) - except NotImplementedError as e: - raise webob.exc.HTTPBadRequest(explanation=e.msg) - return {"host": host, "power_action": result} - - @wsgi.serializers(xml=HostActionTemplate) - def startup(self, req, id): - return self._host_power_action(req, host=id, action="startup") - - @wsgi.serializers(xml=HostActionTemplate) - def shutdown(self, req, id): - return self._host_power_action(req, host=id, action="shutdown") - - @wsgi.serializers(xml=HostActionTemplate) - def reboot(self, req, id): - return self._host_power_action(req, host=id, action="reboot") - - -class Hosts(extensions.ExtensionDescriptor): - """Admin-only host administration""" - - name = "Hosts" - alias = "os-hosts" - namespace = "http://docs.openstack.org/compute/ext/hosts/api/v1.1" - updated = "2011-06-29T00:00:00+00:00" - admin_only = True - - def get_resources(self): - resources = [extensions.ResourceExtension('os-hosts', - HostController(), - collection_actions={'update': 'PUT'}, - member_actions={"startup": "GET", "shutdown": "GET", - "reboot": "GET"})] - return resources diff --git a/nova/api/openstack/v2/contrib/keypairs.py b/nova/api/openstack/v2/contrib/keypairs.py deleted file mode 100644 index 2dc9b063d..000000000 --- a/nova/api/openstack/v2/contrib/keypairs.py +++ /dev/null @@ -1,163 +0,0 @@ -# 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. - -""" Keypair management extension""" - -import os -import shutil -import tempfile - -import webob -from webob import exc - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import crypto -from nova import db -from nova import exception - - -class KeypairTemplate(xmlutil.TemplateBuilder): - def construct(self): - return xmlutil.MasterTemplate(xmlutil.make_flat_dict('keypair'), 1) - - -class KeypairsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('keypairs') - elem = xmlutil.make_flat_dict('keypair', selector='keypairs', - subselector='keypair') - root.append(elem) - - return xmlutil.MasterTemplate(root, 1) - - -class KeypairController(object): - """ Keypair API controller for the Openstack API """ - - # TODO(ja): both this file and nova.api.ec2.cloud.py have similar logic. - # move the common keypair logic to nova.compute.API? - - def _gen_key(self): - """ - Generate a key - """ - private_key, public_key, fingerprint = crypto.generate_key_pair() - return {'private_key': private_key, - 'public_key': public_key, - 'fingerprint': fingerprint} - - @wsgi.serializers(xml=KeypairTemplate) - def create(self, req, body): - """ - Create or import keypair. - - Sending name will generate a key and return private_key - and fingerprint. - - You can send a public_key to add an existing ssh key - - params: keypair object with: - name (required) - string - public_key (optional) - string - """ - - context = req.environ['nova.context'] - params = body['keypair'] - name = params['name'] - - # NOTE(ja): generation is slow, so shortcut invalid name exception - try: - db.key_pair_get(context, context.user_id, name) - raise exception.KeyPairExists(key_name=name) - except exception.NotFound: - pass - - keypair = {'user_id': context.user_id, - 'name': name} - - # import if public_key is sent - if 'public_key' in params: - tmpdir = tempfile.mkdtemp() - fn = os.path.join(tmpdir, 'import.pub') - with open(fn, 'w') as pub: - pub.write(params['public_key']) - fingerprint = crypto.generate_fingerprint(fn) - shutil.rmtree(tmpdir) - keypair['public_key'] = params['public_key'] - keypair['fingerprint'] = fingerprint - else: - generated_key = self._gen_key() - keypair['private_key'] = generated_key['private_key'] - keypair['public_key'] = generated_key['public_key'] - keypair['fingerprint'] = generated_key['fingerprint'] - - db.key_pair_create(context, keypair) - return {'keypair': keypair} - - def delete(self, req, id): - """ - Delete a keypair with a given name - """ - context = req.environ['nova.context'] - db.key_pair_destroy(context, context.user_id, id) - return webob.Response(status_int=202) - - @wsgi.serializers(xml=KeypairsTemplate) - def index(self, req): - """ - List of keypairs for a user - """ - context = req.environ['nova.context'] - key_pairs = db.key_pair_get_all_by_user(context, context.user_id) - rval = [] - for key_pair in key_pairs: - rval.append({'keypair': { - 'name': key_pair['name'], - 'public_key': key_pair['public_key'], - 'fingerprint': key_pair['fingerprint'], - }}) - - return {'keypairs': rval} - - -class KeypairsSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return KeypairsTemplate() - - def default(self): - return KeypairTemplate() - - -class Keypairs(extensions.ExtensionDescriptor): - """Keypair Support""" - - name = "Keypairs" - alias = "os-keypairs" - namespace = "http://docs.openstack.org/compute/ext/keypairs/api/v1.1" - updated = "2011-08-08T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension( - 'os-keypairs', - KeypairController()) - - resources.append(res) - return resources diff --git a/nova/api/openstack/v2/contrib/multinic.py b/nova/api/openstack/v2/contrib/multinic.py deleted file mode 100644 index 6719fb18a..000000000 --- a/nova/api/openstack/v2/contrib/multinic.py +++ /dev/null @@ -1,106 +0,0 @@ -# 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. - -"""The multinic extension.""" - -import webob -from webob import exc - -from nova.api.openstack.v2 import extensions -from nova import compute -from nova import exception -from nova import log as logging - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.multinic") - - -# Note: The class name is as it has to be for this to be loaded as an -# extension--only first character capitalized. -class Multinic(extensions.ExtensionDescriptor): - """Multiple network support""" - - name = "Multinic" - alias = "NMN" - namespace = "http://docs.openstack.org/compute/ext/multinic/api/v1.1" - updated = "2011-06-09T00:00:00+00:00" - - def __init__(self, ext_mgr): - """Initialize the extension. - - Gets a compute.API object so we can call the back-end - add_fixed_ip() and remove_fixed_ip() methods. - """ - - super(Multinic, self).__init__(ext_mgr) - self.compute_api = compute.API() - - def get_actions(self): - """Return the actions the extension adds, as required by contract.""" - - actions = [] - - # Add the add_fixed_ip action - act = extensions.ActionExtension("servers", "addFixedIp", - self._add_fixed_ip) - actions.append(act) - - # Add the remove_fixed_ip action - act = extensions.ActionExtension("servers", "removeFixedIp", - self._remove_fixed_ip) - actions.append(act) - - return actions - - def _get_instance(self, context, instance_id): - try: - return self.compute_api.get(context, instance_id) - except exception.InstanceNotFound: - msg = _("Server not found") - raise exc.HTTPNotFound(msg) - - def _add_fixed_ip(self, input_dict, req, id): - """Adds an IP on a given network to an instance.""" - - # Validate the input entity - if 'networkId' not in input_dict['addFixedIp']: - msg = _("Missing 'networkId' argument for addFixedIp") - raise exc.HTTPUnprocessableEntity(explanation=msg) - - context = req.environ['nova.context'] - instance = self._get_instance(context, id) - network_id = input_dict['addFixedIp']['networkId'] - self.compute_api.add_fixed_ip(context, instance, network_id) - return webob.Response(status_int=202) - - def _remove_fixed_ip(self, input_dict, req, id): - """Removes an IP from an instance.""" - - # Validate the input entity - if 'address' not in input_dict['removeFixedIp']: - msg = _("Missing 'address' argument for removeFixedIp") - raise exc.HTTPUnprocessableEntity(explanation=msg) - - context = req.environ['nova.context'] - instance = self._get_instance(context, id) - address = input_dict['removeFixedIp']['address'] - - try: - self.compute_api.remove_fixed_ip(context, instance, address) - except exceptions.FixedIpNotFoundForSpecificInstance: - LOG.exception(_("Unable to find address %r") % address) - raise exc.HTTPBadRequest() - - return webob.Response(status_int=202) diff --git a/nova/api/openstack/v2/contrib/networks.py b/nova/api/openstack/v2/contrib/networks.py deleted file mode 100644 index 4a96e534f..000000000 --- a/nova/api/openstack/v2/contrib/networks.py +++ /dev/null @@ -1,117 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Grid Dynamics -# 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.api.openstack.v2 import extensions -from nova import exception -from nova import flags -from nova import log as logging -import nova.network.api - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.api.openstack.v2.contrib.networks') - - -def network_dict(network): - if network: - fields = ('bridge', 'vpn_public_port', 'dhcp_start', - 'bridge_interface', 'updated_at', 'id', 'cidr_v6', - 'deleted_at', 'gateway', 'label', 'project_id', - 'vpn_private_address', 'deleted', 'vlan', 'broadcast', - 'netmask', 'injected', 'cidr', 'vpn_public_address', - 'multi_host', 'dns1', 'host', 'gateway_v6', 'netmask_v6', - 'created_at') - return dict((field, network[field]) for field in fields) - else: - return {} - - -class NetworkController(object): - - def __init__(self, network_api=None): - self.network_api = network_api or nova.network.api.API() - - def action(self, req, id, body): - _actions = { - 'disassociate': self._disassociate, - } - - for action, data in body.iteritems(): - try: - return _actions[action](req, id, body) - except KeyError: - msg = _("Network does not have %s action") % action - raise exc.HTTPBadRequest(explanation=msg) - - raise exc.HTTPBadRequest(explanation=_("Invalid request body")) - - def _disassociate(self, request, network_id, body): - context = request.environ['nova.context'] - LOG.debug(_("Disassociating network with id %s" % network_id)) - try: - self.network_api.disassociate(context, network_id) - except exception.NetworkNotFound: - raise exc.HTTPNotFound(_("Network not found")) - return exc.HTTPAccepted() - - def index(self, req): - context = req.environ['nova.context'] - networks = self.network_api.get_all(context) - result = [network_dict(net_ref) for net_ref in networks] - return {'networks': result} - - def show(self, req, id): - context = req.environ['nova.context'] - LOG.debug(_("Showing network with id %s") % id) - try: - network = self.network_api.get(context, id) - except exception.NetworkNotFound: - raise exc.HTTPNotFound(_("Network not found")) - return {'network': network_dict(network)} - - def delete(self, req, id): - context = req.environ['nova.context'] - LOG.info(_("Deleting network with id %s") % id) - try: - self.network_api.delete(context, id) - except exception.NetworkNotFound: - raise exc.HTTPNotFound(_("Network not found")) - return exc.HTTPAccepted() - - def create(self, req, id, body=None): - raise exc.HTTPNotImplemented() - - -class Networks(extensions.ExtensionDescriptor): - """Admin-only Network Management Extension""" - - name = "Networks" - alias = "os-networks" - namespace = "http://docs.openstack.org/compute/ext/networks/api/v1.1" - updated = "2011-12-23 00:00:00" - admin_only = True - - def get_resources(self): - member_actions = {'action': 'POST'} - res = extensions.ResourceExtension('os-networks', - NetworkController(), - member_actions=member_actions) - return [res] diff --git a/nova/api/openstack/v2/contrib/quotas.py b/nova/api/openstack/v2/contrib/quotas.py deleted file mode 100644 index 191553a02..000000000 --- a/nova/api/openstack/v2/contrib/quotas.py +++ /dev/null @@ -1,102 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob - -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.api.openstack.v2 import extensions -from nova import db -from nova import exception -from nova import quota - - -quota_resources = ['metadata_items', 'injected_file_content_bytes', - 'volumes', 'gigabytes', 'ram', 'floating_ips', 'instances', - 'injected_files', 'cores'] - - -class QuotaTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('quota_set', selector='quota_set') - root.set('id') - - for resource in quota_resources: - elem = xmlutil.SubTemplateElement(root, resource) - elem.text = resource - - return xmlutil.MasterTemplate(root, 1) - - -class QuotaSetsController(object): - - def _format_quota_set(self, project_id, quota_set): - """Convert the quota object to a result dict""" - - result = dict(id=str(project_id)) - - for resource in quota_resources: - result[resource] = quota_set[resource] - - return dict(quota_set=result) - - @wsgi.serializers(xml=QuotaTemplate) - def show(self, req, id): - context = req.environ['nova.context'] - try: - db.sqlalchemy.api.authorize_project_context(context, id) - return self._format_quota_set(id, - quota.get_project_quotas(context, id)) - except exception.NotAuthorized: - raise webob.exc.HTTPForbidden() - - @wsgi.serializers(xml=QuotaTemplate) - def update(self, req, id, body): - context = req.environ['nova.context'] - project_id = id - for key in body['quota_set'].keys(): - if key in quota_resources: - value = int(body['quota_set'][key]) - try: - db.quota_update(context, project_id, key, value) - except exception.ProjectQuotaNotFound: - db.quota_create(context, project_id, key, value) - except exception.AdminRequired: - raise webob.exc.HTTPForbidden() - return {'quota_set': quota.get_project_quotas(context, project_id)} - - def defaults(self, req, id): - return self._format_quota_set(id, quota._get_default_quotas()) - - -class Quotas(extensions.ExtensionDescriptor): - """Quotas management support""" - - name = "Quotas" - alias = "os-quota-sets" - namespace = "http://docs.openstack.org/compute/ext/quotas-sets/api/v1.1" - updated = "2011-08-08T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-quota-sets', - QuotaSetsController(), - member_actions={'defaults': 'GET'}) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/rescue.py b/nova/api/openstack/v2/contrib/rescue.py deleted file mode 100644 index 20c18028d..000000000 --- a/nova/api/openstack/v2/contrib/rescue.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2011 Openstack, LLC. -# -# 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 rescue mode extension.""" - -import webob -from webob import exc - -from nova.api.openstack.v2 import extensions as exts -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils - - -FLAGS = flags.FLAGS -LOG = logging.getLogger("nova.api.openstack.v2.contrib.rescue") - - -class Rescue(exts.ExtensionDescriptor): - """Instance rescue mode""" - - name = "Rescue" - alias = "os-rescue" - namespace = "http://docs.openstack.org/compute/ext/rescue/api/v1.1" - updated = "2011-08-18T00:00:00+00:00" - - def __init__(self, ext_mgr): - super(Rescue, self).__init__(ext_mgr) - self.compute_api = compute.API() - - def _get_instance(self, context, instance_id): - try: - return self.compute_api.get(context, instance_id) - except exception.InstanceNotFound: - msg = _("Server not found") - raise exc.HTTPNotFound(msg) - - @exts.wrap_errors - def _rescue(self, input_dict, req, instance_id): - """Rescue an instance.""" - context = req.environ["nova.context"] - - if input_dict['rescue'] and 'adminPass' in input_dict['rescue']: - password = input_dict['rescue']['adminPass'] - else: - password = utils.generate_password(FLAGS.password_length) - - instance = self._get_instance(context, instance_id) - self.compute_api.rescue(context, instance, rescue_password=password) - return {'adminPass': password} - - @exts.wrap_errors - def _unrescue(self, input_dict, req, instance_id): - """Unrescue an instance.""" - context = req.environ["nova.context"] - instance = self._get_instance(context, instance_id) - self.compute_api.unrescue(context, instance) - return webob.Response(status_int=202) - - def get_actions(self): - """Return the actions the extension adds, as required by contract.""" - actions = [ - exts.ActionExtension("servers", "rescue", self._rescue), - exts.ActionExtension("servers", "unrescue", self._unrescue), - ] - - return actions diff --git a/nova/api/openstack/v2/contrib/security_groups.py b/nova/api/openstack/v2/contrib/security_groups.py deleted file mode 100644 index f4abbcd51..000000000 --- a/nova/api/openstack/v2/contrib/security_groups.py +++ /dev/null @@ -1,592 +0,0 @@ -# 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. - -"""The security groups extension.""" - -import urllib -from xml.dom import minidom - -from webob import exc -import webob - -from nova.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova import db -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.security_groups") -FLAGS = flags.FLAGS - - -def make_rule(elem): - elem.set('id') - elem.set('parent_group_id') - - proto = xmlutil.SubTemplateElement(elem, 'ip_protocol') - proto.text = 'ip_protocol' - - from_port = xmlutil.SubTemplateElement(elem, 'from_port') - from_port.text = 'from_port' - - to_port = xmlutil.SubTemplateElement(elem, 'to_port') - to_port.text = 'to_port' - - group = xmlutil.SubTemplateElement(elem, 'group', selector='group') - name = xmlutil.SubTemplateElement(group, 'name') - name.text = 'name' - tenant_id = xmlutil.SubTemplateElement(group, 'tenant_id') - tenant_id.text = 'tenant_id' - - ip_range = xmlutil.SubTemplateElement(elem, 'ip_range', - selector='ip_range') - cidr = xmlutil.SubTemplateElement(ip_range, 'cidr') - cidr.text = 'cidr' - - -def make_sg(elem): - elem.set('id') - elem.set('tenant_id') - elem.set('name') - - desc = xmlutil.SubTemplateElement(elem, 'description') - desc.text = 'description' - - rules = xmlutil.SubTemplateElement(elem, 'rules') - rule = xmlutil.SubTemplateElement(rules, 'rule', selector='rules') - make_rule(rule) - - -sg_nsmap = {None: wsgi.XMLNS_V11} - - -class SecurityGroupRuleTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('security_group_rule', - selector='security_group_rule') - make_rule(root) - return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) - - -class SecurityGroupTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('security_group', - selector='security_group') - make_sg(root) - return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) - - -class SecurityGroupsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('security_groups') - elem = xmlutil.SubTemplateElement(root, 'security_group', - selector='security_groups') - make_sg(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=sg_nsmap) - - -class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer): - """ - Deserializer to handle xml-formatted security group requests. - """ - def default(self, string): - """Deserialize an xml-formatted security group create request""" - dom = minidom.parseString(string) - security_group = {} - sg_node = self.find_first_child_named(dom, - 'security_group') - if sg_node is not None: - if sg_node.hasAttribute('name'): - security_group['name'] = sg_node.getAttribute('name') - desc_node = self.find_first_child_named(sg_node, - "description") - if desc_node: - security_group['description'] = self.extract_text(desc_node) - return {'body': {'security_group': security_group}} - - -class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer): - """ - Deserializer to handle xml-formatted security group requests. - """ - - def default(self, string): - """Deserialize an xml-formatted security group create request""" - dom = minidom.parseString(string) - security_group_rule = self._extract_security_group_rule(dom) - return {'body': {'security_group_rule': security_group_rule}} - - def _extract_security_group_rule(self, node): - """Marshal the security group rule attribute of a parsed request""" - sg_rule = {} - sg_rule_node = self.find_first_child_named(node, - 'security_group_rule') - if sg_rule_node is not None: - ip_protocol_node = self.find_first_child_named(sg_rule_node, - "ip_protocol") - if ip_protocol_node is not None: - sg_rule['ip_protocol'] = self.extract_text(ip_protocol_node) - - from_port_node = self.find_first_child_named(sg_rule_node, - "from_port") - if from_port_node is not None: - sg_rule['from_port'] = self.extract_text(from_port_node) - - to_port_node = self.find_first_child_named(sg_rule_node, "to_port") - if to_port_node is not None: - sg_rule['to_port'] = self.extract_text(to_port_node) - - parent_group_id_node = self.find_first_child_named(sg_rule_node, - "parent_group_id") - if parent_group_id_node is not None: - sg_rule['parent_group_id'] = self.extract_text( - parent_group_id_node) - - group_id_node = self.find_first_child_named(sg_rule_node, - "group_id") - if group_id_node is not None: - sg_rule['group_id'] = self.extract_text(group_id_node) - - cidr_node = self.find_first_child_named(sg_rule_node, "cidr") - if cidr_node is not None: - sg_rule['cidr'] = self.extract_text(cidr_node) - - return sg_rule - - -class SecurityGroupController(object): - """The Security group API controller for the OpenStack API.""" - - def __init__(self): - self.compute_api = compute.API() - super(SecurityGroupController, self).__init__() - - def _format_security_group_rule(self, context, rule): - sg_rule = {} - sg_rule['id'] = rule.id - sg_rule['parent_group_id'] = rule.parent_group_id - sg_rule['ip_protocol'] = rule.protocol - sg_rule['from_port'] = rule.from_port - sg_rule['to_port'] = rule.to_port - sg_rule['group'] = {} - sg_rule['ip_range'] = {} - if rule.group_id: - source_group = db.security_group_get(context, rule.group_id) - sg_rule['group'] = {'name': source_group.name, - 'tenant_id': source_group.project_id} - else: - sg_rule['ip_range'] = {'cidr': rule.cidr} - return sg_rule - - def _format_security_group(self, context, group): - security_group = {} - security_group['id'] = group.id - security_group['description'] = group.description - security_group['name'] = group.name - security_group['tenant_id'] = group.project_id - security_group['rules'] = [] - for rule in group.rules: - security_group['rules'] += [self._format_security_group_rule( - context, rule)] - return security_group - - def _get_security_group(self, context, id): - try: - id = int(id) - security_group = db.security_group_get(context, id) - except ValueError: - msg = _("Security group id should be integer") - raise exc.HTTPBadRequest(explanation=msg) - except exception.NotFound as exp: - raise exc.HTTPNotFound(explanation=unicode(exp)) - return security_group - - @wsgi.serializers(xml=SecurityGroupTemplate) - def show(self, req, id): - """Return data about the given security group.""" - context = req.environ['nova.context'] - security_group = self._get_security_group(context, id) - return {'security_group': self._format_security_group(context, - security_group)} - - def delete(self, req, id): - """Delete a security group.""" - context = req.environ['nova.context'] - security_group = self._get_security_group(context, id) - LOG.audit(_("Delete security group %s"), id, context=context) - db.security_group_destroy(context, security_group.id) - - return webob.Response(status_int=202) - - @wsgi.serializers(xml=SecurityGroupsTemplate) - def index(self, req): - """Returns a list of security groups""" - context = req.environ['nova.context'] - - self.compute_api.ensure_default_security_group(context) - groups = db.security_group_get_by_project(context, - context.project_id) - limited_list = common.limited(groups, req) - result = [self._format_security_group(context, group) - for group in limited_list] - - return {'security_groups': - list(sorted(result, - key=lambda k: (k['tenant_id'], k['name'])))} - - @wsgi.serializers(xml=SecurityGroupTemplate) - @wsgi.deserializers(xml=SecurityGroupXMLDeserializer) - def create(self, req, body): - """Creates a new security group.""" - context = req.environ['nova.context'] - if not body: - raise exc.HTTPUnprocessableEntity() - - security_group = body.get('security_group', None) - - if security_group is None: - raise exc.HTTPUnprocessableEntity() - - group_name = security_group.get('name', None) - group_description = security_group.get('description', None) - - self._validate_security_group_property(group_name, "name") - self._validate_security_group_property(group_description, - "description") - group_name = group_name.strip() - group_description = group_description.strip() - - LOG.audit(_("Create Security Group %s"), group_name, context=context) - self.compute_api.ensure_default_security_group(context) - if db.security_group_exists(context, context.project_id, group_name): - msg = _('Security group %s already exists') % group_name - raise exc.HTTPBadRequest(explanation=msg) - - group = {'user_id': context.user_id, - 'project_id': context.project_id, - 'name': group_name, - 'description': group_description} - group_ref = db.security_group_create(context, group) - - return {'security_group': self._format_security_group(context, - group_ref)} - - def _validate_security_group_property(self, value, typ): - """ typ will be either 'name' or 'description', - depending on the caller - """ - try: - val = value.strip() - except AttributeError: - msg = _("Security group %s is not a string or unicode") % typ - raise exc.HTTPBadRequest(explanation=msg) - if not val: - msg = _("Security group %s cannot be empty.") % typ - raise exc.HTTPBadRequest(explanation=msg) - if len(val) > 255: - msg = _("Security group %s should not be greater " - "than 255 characters.") % typ - raise exc.HTTPBadRequest(explanation=msg) - - -class SecurityGroupRulesController(SecurityGroupController): - - @wsgi.serializers(xml=SecurityGroupRuleTemplate) - @wsgi.deserializers(xml=SecurityGroupRulesXMLDeserializer) - def create(self, req, body): - context = req.environ['nova.context'] - - if not body: - raise exc.HTTPUnprocessableEntity() - - if not 'security_group_rule' in body: - raise exc.HTTPUnprocessableEntity() - - self.compute_api.ensure_default_security_group(context) - - sg_rule = body['security_group_rule'] - parent_group_id = sg_rule.get('parent_group_id', None) - try: - parent_group_id = int(parent_group_id) - security_group = db.security_group_get(context, parent_group_id) - except ValueError: - msg = _("Parent group id is not integer") - raise exc.HTTPBadRequest(explanation=msg) - except exception.NotFound as exp: - msg = _("Security group (%s) not found") % parent_group_id - raise exc.HTTPNotFound(explanation=msg) - - msg = _("Authorize security group ingress %s") - LOG.audit(msg, security_group['name'], context=context) - - try: - values = self._rule_args_to_dict(context, - to_port=sg_rule.get('to_port'), - from_port=sg_rule.get('from_port'), - parent_group_id=sg_rule.get('parent_group_id'), - ip_protocol=sg_rule.get('ip_protocol'), - cidr=sg_rule.get('cidr'), - group_id=sg_rule.get('group_id')) - except Exception as exp: - raise exc.HTTPBadRequest(explanation=unicode(exp)) - - if values is None: - msg = _("Not enough parameters to build a " - "valid rule.") - raise exc.HTTPBadRequest(explanation=msg) - - values['parent_group_id'] = security_group.id - - if self._security_group_rule_exists(security_group, values): - msg = _('This rule already exists in group %s') % parent_group_id - raise exc.HTTPBadRequest(explanation=msg) - - security_group_rule = db.security_group_rule_create(context, values) - - self.compute_api.trigger_security_group_rules_refresh(context, - security_group_id=security_group['id']) - - return {"security_group_rule": self._format_security_group_rule( - context, - security_group_rule)} - - def _security_group_rule_exists(self, security_group, values): - """Indicates whether the specified rule values are already - defined in the given security group. - """ - for rule in security_group.rules: - if 'group_id' in values: - if rule['group_id'] == values['group_id']: - return True - else: - is_duplicate = True - for key in ('cidr', 'from_port', 'to_port', 'protocol'): - if rule[key] != values[key]: - is_duplicate = False - break - if is_duplicate: - return True - return False - - def _rule_args_to_dict(self, context, to_port=None, from_port=None, - parent_group_id=None, ip_protocol=None, - cidr=None, group_id=None): - values = {} - - if group_id is not None: - try: - parent_group_id = int(parent_group_id) - group_id = int(group_id) - except ValueError: - msg = _("Parent or group id is not integer") - raise exception.InvalidInput(reason=msg) - - if parent_group_id == group_id: - msg = _("Parent group id and group id cannot be same") - raise exception.InvalidInput(reason=msg) - - values['group_id'] = group_id - #check if groupId exists - db.security_group_get(context, group_id) - elif cidr: - # If this fails, it throws an exception. This is what we want. - try: - cidr = urllib.unquote(cidr).decode() - except Exception: - raise exception.InvalidCidr(cidr=cidr) - - if not utils.is_valid_cidr(cidr): - # Raise exception for non-valid address - raise exception.InvalidCidr(cidr=cidr) - - values['cidr'] = cidr - else: - values['cidr'] = '0.0.0.0/0' - - if ip_protocol and from_port and to_port: - - ip_protocol = str(ip_protocol) - try: - from_port = int(from_port) - to_port = int(to_port) - except ValueError: - if ip_protocol.upper() == 'ICMP': - raise exception.InvalidInput(reason="Type and" - " Code must be integers for ICMP protocol type") - else: - raise exception.InvalidInput(reason="To and From ports " - "must be integers") - - if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']: - raise exception.InvalidIpProtocol(protocol=ip_protocol) - - # Verify that from_port must always be less than - # or equal to to_port - if from_port > to_port: - raise exception.InvalidPortRange(from_port=from_port, - to_port=to_port, msg="Former value cannot" - " be greater than the later") - - # Verify valid TCP, UDP port ranges - if (ip_protocol.upper() in ['TCP', 'UDP'] and - (from_port < 1 or to_port > 65535)): - raise exception.InvalidPortRange(from_port=from_port, - to_port=to_port, msg="Valid TCP ports should" - " be between 1-65535") - - # Verify ICMP type and code - if (ip_protocol.upper() == "ICMP" and - (from_port < -1 or to_port > 255)): - raise exception.InvalidPortRange(from_port=from_port, - to_port=to_port, msg="For ICMP, the" - " type:code must be valid") - - values['protocol'] = ip_protocol - values['from_port'] = from_port - values['to_port'] = to_port - else: - # If cidr based filtering, protocol and ports are mandatory - if 'cidr' in values: - return None - - return values - - def delete(self, req, id): - context = req.environ['nova.context'] - - self.compute_api.ensure_default_security_group(context) - try: - id = int(id) - rule = db.security_group_rule_get(context, id) - except ValueError: - msg = _("Rule id is not integer") - raise exc.HTTPBadRequest(explanation=msg) - except exception.NotFound as exp: - msg = _("Rule (%s) not found") % id - raise exc.HTTPNotFound(explanation=msg) - - group_id = rule.parent_group_id - self.compute_api.ensure_default_security_group(context) - security_group = db.security_group_get(context, group_id) - - msg = _("Revoke security group ingress %s") - LOG.audit(msg, security_group['name'], context=context) - - db.security_group_rule_destroy(context, rule['id']) - self.compute_api.trigger_security_group_rules_refresh(context, - security_group_id=security_group['id']) - - return webob.Response(status_int=202) - - -class Security_groups(extensions.ExtensionDescriptor): - """Security group support""" - - name = "SecurityGroups" - alias = "security_groups" - namespace = "http://docs.openstack.org/compute/ext/securitygroups/api/v1.1" - updated = "2011-07-21T00:00:00+00:00" - - def __init__(self, ext_mgr): - self.compute_api = compute.API() - super(Security_groups, self).__init__(ext_mgr) - - def _addSecurityGroup(self, input_dict, req, instance_id): - context = req.environ['nova.context'] - - try: - body = input_dict['addSecurityGroup'] - group_name = body['name'] - except TypeError: - msg = _("Missing parameter dict") - raise webob.exc.HTTPBadRequest(explanation=msg) - except KeyError: - msg = _("Security group not specified") - raise webob.exc.HTTPBadRequest(explanation=msg) - - if not group_name or group_name.strip() == '': - msg = _("Security group name cannot be empty") - raise webob.exc.HTTPBadRequest(explanation=msg) - - try: - instance = self.compute_api.get(context, instance_id) - self.compute_api.add_security_group(context, instance, group_name) - except exception.SecurityGroupNotFound as exp: - raise exc.HTTPNotFound(explanation=unicode(exp)) - except exception.InstanceNotFound as exp: - raise exc.HTTPNotFound(explanation=unicode(exp)) - except exception.Invalid as exp: - raise exc.HTTPBadRequest(explanation=unicode(exp)) - - return webob.Response(status_int=202) - - def _removeSecurityGroup(self, input_dict, req, instance_id): - context = req.environ['nova.context'] - - try: - body = input_dict['removeSecurityGroup'] - group_name = body['name'] - except TypeError: - msg = _("Missing parameter dict") - raise webob.exc.HTTPBadRequest(explanation=msg) - except KeyError: - msg = _("Security group not specified") - raise webob.exc.HTTPBadRequest(explanation=msg) - - if not group_name or group_name.strip() == '': - msg = _("Security group name cannot be empty") - raise webob.exc.HTTPBadRequest(explanation=msg) - - try: - instance = self.compute_api.get(context, instance_id) - self.compute_api.remove_security_group(context, instance, - group_name) - except exception.SecurityGroupNotFound as exp: - raise exc.HTTPNotFound(explanation=unicode(exp)) - except exception.InstanceNotFound as exp: - raise exc.HTTPNotFound(explanation=unicode(exp)) - except exception.Invalid as exp: - raise exc.HTTPBadRequest(explanation=unicode(exp)) - - return webob.Response(status_int=202) - - def get_actions(self): - """Return the actions the extensions adds""" - actions = [ - extensions.ActionExtension("servers", "addSecurityGroup", - self._addSecurityGroup), - extensions.ActionExtension("servers", "removeSecurityGroup", - self._removeSecurityGroup) - ] - return actions - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-security-groups', - controller=SecurityGroupController()) - - resources.append(res) - - res = extensions.ResourceExtension('os-security-group-rules', - controller=SecurityGroupRulesController()) - resources.append(res) - return resources diff --git a/nova/api/openstack/v2/contrib/server_action_list.py b/nova/api/openstack/v2/contrib/server_action_list.py deleted file mode 100644 index 90573043e..000000000 --- a/nova/api/openstack/v2/contrib/server_action_list.py +++ /dev/null @@ -1,77 +0,0 @@ -# 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 webob.exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova import exception - - -sa_nsmap = {None: wsgi.XMLNS_V11} - - -class ServerActionsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('actions') - elem = xmlutil.SubTemplateElement(root, 'action', selector='actions') - elem.set('created_at') - elem.set('action') - elem.set('error') - return xmlutil.MasterTemplate(root, 1, nsmap=sa_nsmap) - - -class ServerActionListController(object): - @wsgi.serializers(xml=ServerActionsTemplate) - def index(self, req, server_id): - context = req.environ["nova.context"] - compute_api = compute.API() - - try: - instance = compute_api.get(context, server_id) - except exception.NotFound: - raise webob.exc.HTTPNotFound(_("Instance not found")) - - items = compute_api.get_actions(context, instance) - - def _format_item(item): - return { - 'created_at': str(item['created_at']), - 'action': item['action'], - 'error': item['error'], - } - - return {'actions': [_format_item(item) for item in items]} - - -class Server_action_list(extensions.ExtensionDescriptor): - """Allow Admins to view pending server actions""" - - name = "ServerActionList" - alias = "os-server-action-list" - namespace = "http://docs.openstack.org/compute/ext/" \ - "server-actions-list/api/v1.1" - updated = "2011-12-21T00:00:00+00:00" - admin_only = True - - def get_resources(self): - parent_def = {'member_name': 'server', 'collection_name': 'servers'} - #NOTE(bcwaldon): This should be prefixed with 'os-' - ext = extensions.ResourceExtension('actions', - ServerActionListController(), - parent=parent_def) - return [ext] diff --git a/nova/api/openstack/v2/contrib/server_diagnostics.py b/nova/api/openstack/v2/contrib/server_diagnostics.py deleted file mode 100644 index daeda5081..000000000 --- a/nova/api/openstack/v2/contrib/server_diagnostics.py +++ /dev/null @@ -1,69 +0,0 @@ -# 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 webob.exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova import exception -from nova.scheduler import api as scheduler_api - - -sd_nsmap = {None: wsgi.XMLNS_V11} - - -class ServerDiagnosticsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('diagnostics') - elem = xmlutil.SubTemplateElement(root, xmlutil.Selector(0), - selector=xmlutil.get_items) - elem.text = 1 - return xmlutil.MasterTemplate(root, 1, nsmap=sd_nsmap) - - -class ServerDiagnosticsController(object): - @wsgi.serializers(xml=ServerDiagnosticsTemplate) - @exception.novaclient_converter - @scheduler_api.redirect_handler - def index(self, req, server_id): - context = req.environ["nova.context"] - compute_api = compute.API() - try: - instance = compute_api.get(context, id) - except exception.NotFound(): - raise webob.exc.HTTPNotFound(_("Instance not found")) - - return compute_api.get_diagnostics(context, instance) - - -class Server_diagnostics(extensions.ExtensionDescriptor): - """Allow Admins to view server diagnostics through server action""" - - name = "ServerDiagnostics" - alias = "os-server-diagnostics" - namespace = "http://docs.openstack.org/compute/ext/" \ - "server-diagnostics/api/v1.1" - updated = "2011-12-21T00:00:00+00:00" - admin_only = True - - def get_resources(self): - parent_def = {'member_name': 'server', 'collection_name': 'servers'} - #NOTE(bcwaldon): This should be prefixed with 'os-' - ext = extensions.ResourceExtension('diagnostics', - ServerDiagnosticsController(), - parent=parent_def) - return [ext] diff --git a/nova/api/openstack/v2/contrib/simple_tenant_usage.py b/nova/api/openstack/v2/contrib/simple_tenant_usage.py deleted file mode 100644 index 7b07dfae1..000000000 --- a/nova/api/openstack/v2/contrib/simple_tenant_usage.py +++ /dev/null @@ -1,265 +0,0 @@ -# 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 datetime import datetime -import urlparse - -import webob - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.compute import api -from nova import exception -from nova import flags - - -FLAGS = flags.FLAGS - - -def make_usage(elem): - for subelem_tag in ('tenant_id', 'total_local_gb_usage', - 'total_vcpus_usage', 'total_memory_mb_usage', - 'total_hours', 'start', 'stop'): - subelem = xmlutil.SubTemplateElement(elem, subelem_tag) - subelem.text = subelem_tag - - server_usages = xmlutil.SubTemplateElement(elem, 'server_usages') - server_usage = xmlutil.SubTemplateElement(server_usages, 'server_usage', - selector='server_usages') - for subelem_tag in ('name', 'hours', 'memory_mb', 'local_gb', 'vcpus', - 'tenant_id', 'flavor', 'started_at', 'ended_at', - 'state', 'uptime'): - subelem = xmlutil.SubTemplateElement(server_usage, subelem_tag) - subelem.text = subelem_tag - - -class SimpleTenantUsageTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('tenant_usage', selector='tenant_usage') - make_usage(root) - return xmlutil.MasterTemplate(root, 1) - - -class SimpleTenantUsagesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('tenant_usages') - elem = xmlutil.SubTemplateElement(root, 'tenant_usage', - selector='tenant_usages') - make_usage(elem) - return xmlutil.MasterTemplate(root, 1) - - -class SimpleTenantUsageController(object): - def _hours_for(self, instance, period_start, period_stop): - launched_at = instance['launched_at'] - terminated_at = instance['terminated_at'] - if terminated_at is not None: - if not isinstance(terminated_at, datetime): - terminated_at = datetime.strptime(terminated_at, - "%Y-%m-%d %H:%M:%S.%f") - - if launched_at is not None: - if not isinstance(launched_at, datetime): - launched_at = datetime.strptime(launched_at, - "%Y-%m-%d %H:%M:%S.%f") - - if terminated_at and terminated_at < period_start: - return 0 - # nothing if it started after the usage report ended - if launched_at and launched_at > period_stop: - return 0 - if launched_at: - # if instance launched after period_started, don't charge for first - start = max(launched_at, period_start) - if terminated_at: - # if instance stopped before period_stop, don't charge after - stop = min(period_stop, terminated_at) - else: - # instance is still running, so charge them up to current time - stop = period_stop - dt = stop - start - seconds = dt.days * 3600 * 24 + dt.seconds\ - + dt.microseconds / 100000.0 - - return seconds / 3600.0 - else: - # instance hasn't launched, so no charge - return 0 - - def _tenant_usages_for_period(self, context, period_start, - period_stop, tenant_id=None, detailed=True): - - compute_api = api.API() - instances = compute_api.get_active_by_window(context, - period_start, - period_stop, - tenant_id) - from nova import log as logging - logging.info(instances) - rval = {} - flavors = {} - - for instance in instances: - info = {} - info['hours'] = self._hours_for(instance, - period_start, - period_stop) - flavor_type = instance['instance_type_id'] - - if not flavors.get(flavor_type): - try: - it_ref = compute_api.get_instance_type(context, - flavor_type) - flavors[flavor_type] = it_ref - except exception.InstanceTypeNotFound: - # can't bill if there is no instance type - continue - - flavor = flavors[flavor_type] - - info['name'] = instance['display_name'] - - info['memory_mb'] = flavor['memory_mb'] - info['local_gb'] = flavor['local_gb'] - info['vcpus'] = flavor['vcpus'] - - info['tenant_id'] = instance['project_id'] - - info['flavor'] = flavor['name'] - - info['started_at'] = instance['launched_at'] - - info['ended_at'] = instance['terminated_at'] - - if info['ended_at']: - info['state'] = 'terminated' - else: - info['state'] = instance['vm_state'] - - now = datetime.utcnow() - - if info['state'] == 'terminated': - delta = info['ended_at'] - info['started_at'] - else: - delta = now - info['started_at'] - - info['uptime'] = delta.days * 24 * 60 + delta.seconds - - if not info['tenant_id'] in rval: - summary = {} - summary['tenant_id'] = info['tenant_id'] - if detailed: - summary['server_usages'] = [] - summary['total_local_gb_usage'] = 0 - summary['total_vcpus_usage'] = 0 - summary['total_memory_mb_usage'] = 0 - summary['total_hours'] = 0 - summary['start'] = period_start - summary['stop'] = period_stop - rval[info['tenant_id']] = summary - - summary = rval[info['tenant_id']] - summary['total_local_gb_usage'] += info['local_gb'] * info['hours'] - summary['total_vcpus_usage'] += info['vcpus'] * info['hours'] - summary['total_memory_mb_usage'] += info['memory_mb']\ - * info['hours'] - - summary['total_hours'] += info['hours'] - if detailed: - summary['server_usages'].append(info) - - return rval.values() - - def _parse_datetime(self, dtstr): - if isinstance(dtstr, datetime): - return dtstr - try: - return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S") - except Exception: - try: - return datetime.strptime(dtstr, "%Y-%m-%dT%H:%M:%S.%f") - except Exception: - return datetime.strptime(dtstr, "%Y-%m-%d %H:%M:%S.%f") - - def _get_datetime_range(self, req): - qs = req.environ.get('QUERY_STRING', '') - env = urlparse.parse_qs(qs) - period_start = self._parse_datetime(env.get('start', - [datetime.utcnow().isoformat()])[0]) - period_stop = self._parse_datetime(env.get('end', - [datetime.utcnow().isoformat()])[0]) - - detailed = bool(env.get('detailed', False)) - return (period_start, period_stop, detailed) - - @wsgi.serializers(xml=SimpleTenantUsagesTemplate) - def index(self, req): - """Retrive tenant_usage for all tenants""" - context = req.environ['nova.context'] - - if not context.is_admin: - return webob.Response(status_int=403) - - (period_start, period_stop, detailed) = self._get_datetime_range(req) - usages = self._tenant_usages_for_period(context, - period_start, - period_stop, - detailed=detailed) - return {'tenant_usages': usages} - - @wsgi.serializers(xml=SimpleTenantUsageTemplate) - def show(self, req, id): - """Retrive tenant_usage for a specified tenant""" - tenant_id = id - context = req.environ['nova.context'] - - if not context.is_admin: - if tenant_id != context.project_id: - return webob.Response(status_int=403) - - (period_start, period_stop, ignore) = self._get_datetime_range(req) - usage = self._tenant_usages_for_period(context, - period_start, - period_stop, - tenant_id=tenant_id, - detailed=True) - if len(usage): - usage = usage[0] - else: - usage = {} - return {'tenant_usage': usage} - - -class Simple_tenant_usage(extensions.ExtensionDescriptor): - """Simple tenant usage extension""" - - name = "SimpleTenantUsage" - alias = "os-simple-tenant-usage" - namespace = "http://docs.openstack.org/compute/ext/" \ - "os-simple-tenant-usage/api/v1.1" - updated = "2011-08-19T00:00:00+00:00" - admin_only = True - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension('os-simple-tenant-usage', - SimpleTenantUsageController()) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/users.py b/nova/api/openstack/v2/contrib/users.py deleted file mode 100644 index e24c7c068..000000000 --- a/nova/api/openstack/v2/contrib/users.py +++ /dev/null @@ -1,145 +0,0 @@ -# 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.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.auth import manager -from nova import exception -from nova import flags -from nova import log as logging - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.api.openstack.users') - - -def make_user(elem): - elem.set('id') - elem.set('name') - elem.set('access') - elem.set('secret') - elem.set('admin') - - -class UserTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('user', selector='user') - make_user(root) - return xmlutil.MasterTemplate(root, 1) - - -class UsersTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('users') - elem = xmlutil.SubTemplateElement(root, 'user', selector='users') - make_user(elem) - return xmlutil.MasterTemplate(root, 1) - - -def _translate_keys(user): - return dict(id=user.id, - name=user.name, - access=user.access, - secret=user.secret, - admin=user.admin) - - -class Controller(object): - - 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.AdminRequired() - - @wsgi.serializers(xml=UsersTemplate) - 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) - - @wsgi.serializers(xml=UsersTemplate) - def detail(self, req): - """Return all users in detail""" - return self.index(req) - - @wsgi.serializers(xml=UserTemplate) - def show(self, req, id): - """Return data about the given 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 exc.HTTPNotFound() - - return dict(user=_translate_keys(user)) - - def delete(self, req, id): - self._check_admin(req.environ['nova.context']) - self.manager.delete_user(id) - return {} - - @wsgi.serializers(xml=UserTemplate) - def create(self, req, body): - self._check_admin(req.environ['nova.context']) - is_admin = body['user'].get('admin') in ('T', 'True', True) - name = body['user'].get('name') - access = body['user'].get('access') - secret = body['user'].get('secret') - user = self.manager.create_user(name, access, secret, is_admin) - return dict(user=_translate_keys(user)) - - @wsgi.serializers(xml=UserTemplate) - def update(self, req, id, body): - self._check_admin(req.environ['nova.context']) - is_admin = body['user'].get('admin') - if is_admin is not None: - is_admin = is_admin in ('T', 'True', True) - access = body['user'].get('access') - secret = body['user'].get('secret') - self.manager.modify_user(id, access, secret, is_admin) - return dict(user=_translate_keys(self.manager.get_user(id))) - - -class Users(extensions.ExtensionDescriptor): - """Allow admins to acces user information""" - - name = "Users" - alias = "os-users" - namespace = "http://docs.openstack.org/compute/ext/users/api/v1.1" - updated = "2011-08-08T00:00:00+00:00" - admin_only = True - - def get_resources(self): - coll_actions = {'detail': 'GET'} - res = extensions.ResourceExtension('users', - Controller(), - collection_actions=coll_actions) - - return [res] diff --git a/nova/api/openstack/v2/contrib/virtual_interfaces.py b/nova/api/openstack/v2/contrib/virtual_interfaces.py deleted file mode 100644 index 401c7133e..000000000 --- a/nova/api/openstack/v2/contrib/virtual_interfaces.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (C) 2011 Midokura KK -# 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 virtual interfaces extension.""" - -from nova.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import log as logging -from nova import network - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.virtual_interfaces") - - -vif_nsmap = {None: wsgi.XMLNS_V11} - - -class VirtualInterfaceTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('virtual_interfaces') - elem = xmlutil.SubTemplateElement(root, 'virtual_interface', - selector='virtual_interfaces') - elem.set('id') - elem.set('mac_address') - return xmlutil.MasterTemplate(root, 1, nsmap=vif_nsmap) - - -def _translate_vif_summary_view(_context, vif): - """Maps keys for VIF summary view.""" - d = {} - d['id'] = vif['uuid'] - d['mac_address'] = vif['address'] - return d - - -class ServerVirtualInterfaceController(object): - """The instance VIF API controller for the Openstack API. - """ - - def __init__(self): - self.network_api = network.API() - super(ServerVirtualInterfaceController, self).__init__() - - def _items(self, req, server_id, entity_maker): - """Returns a list of VIFs, transformed through entity_maker.""" - context = req.environ['nova.context'] - - vifs = self.network_api.get_vifs_by_instance(context, server_id) - limited_list = common.limited(vifs, req) - res = [entity_maker(context, vif) for vif in limited_list] - return {'virtual_interfaces': res} - - @wsgi.serializers(xml=VirtualInterfaceTemplate) - def index(self, req, server_id): - """Returns the list of VIFs for a given instance.""" - return self._items(req, server_id, - entity_maker=_translate_vif_summary_view) - - -class Virtual_interfaces(extensions.ExtensionDescriptor): - """Virtual interface support""" - - name = "VirtualInterfaces" - alias = "virtual_interfaces" - namespace = "http://docs.openstack.org/compute/ext/" \ - "virtual_interfaces/api/v1.1" - updated = "2011-08-17T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension( - 'os-virtual-interfaces', - controller=ServerVirtualInterfaceController(), - parent=dict(member_name='server', collection_name='servers')) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/virtual_storage_arrays.py b/nova/api/openstack/v2/contrib/virtual_storage_arrays.py deleted file mode 100644 index 0dee3f1b4..000000000 --- a/nova/api/openstack/v2/contrib/virtual_storage_arrays.py +++ /dev/null @@ -1,687 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 Zadara Storage Inc. -# Copyright (c) 2011 OpenStack LLC. -# -# 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 virtul storage array extension""" - - -import webob -from webob import exc - -from nova.api.openstack import common -from nova.api.openstack.v2.contrib import volumes -from nova.api.openstack.v2 import extensions -from nova.api.openstack.v2 import servers -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova.compute import instance_types -from nova import network -from nova import db -from nova import quota -from nova import exception -from nova import flags -from nova import log as logging -from nova import vsa -from nova import volume - -FLAGS = flags.FLAGS - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.vsa") - - -def _vsa_view(context, vsa, details=False, instances=None): - """Map keys for vsa summary/detailed view.""" - d = {} - - d['id'] = vsa.get('id') - d['name'] = vsa.get('name') - d['displayName'] = vsa.get('display_name') - d['displayDescription'] = vsa.get('display_description') - - d['createTime'] = vsa.get('created_at') - d['status'] = vsa.get('status') - - if 'vsa_instance_type' in vsa: - d['vcType'] = vsa['vsa_instance_type'].get('name', None) - else: - d['vcType'] = vsa['instance_type_id'] - - d['vcCount'] = vsa.get('vc_count') - d['driveCount'] = vsa.get('vol_count') - - d['ipAddress'] = None - for instance in instances: - fixed_addr = None - floating_addr = None - if instance['fixed_ips']: - fixed = instance['fixed_ips'][0] - fixed_addr = fixed['address'] - if fixed['floating_ips']: - floating_addr = fixed['floating_ips'][0]['address'] - - if floating_addr: - d['ipAddress'] = floating_addr - break - else: - d['ipAddress'] = d['ipAddress'] or fixed_addr - - return d - - -def make_vsa(elem): - elem.set('id') - elem.set('name') - elem.set('displayName') - elem.set('displayDescription') - elem.set('createTime') - elem.set('status') - elem.set('vcType') - elem.set('vcCount') - elem.set('driveCount') - elem.set('ipAddress') - - -class VsaTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('vsa', selector='vsa') - make_vsa(root) - return xmlutil.MasterTemplate(root, 1) - - -class VsaSetTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('vsaSet') - elem = xmlutil.SubTemplateElement(root, 'vsa', selector='vsaSet') - make_vsa(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VsaController(object): - """The Virtual Storage Array API controller for the OpenStack API.""" - - def __init__(self): - self.vsa_api = vsa.API() - self.compute_api = compute.API() - self.network_api = network.API() - super(VsaController, self).__init__() - - def _get_instances_by_vsa_id(self, context, id): - return self.compute_api.get_all(context, - search_opts={'metadata': dict(vsa_id=str(id))}) - - def _items(self, req, details): - """Return summary or detailed list of VSAs.""" - context = req.environ['nova.context'] - vsas = self.vsa_api.get_all(context) - limited_list = common.limited(vsas, req) - - vsa_list = [] - for vsa in limited_list: - instances = self._get_instances_by_vsa_id(context, vsa.get('id')) - vsa_list.append(_vsa_view(context, vsa, details, instances)) - return {'vsaSet': vsa_list} - - @wsgi.serializers(xml=VsaSetTemplate) - def index(self, req): - """Return a short list of VSAs.""" - return self._items(req, details=False) - - @wsgi.serializers(xml=VsaSetTemplate) - def detail(self, req): - """Return a detailed list of VSAs.""" - return self._items(req, details=True) - - @wsgi.serializers(xml=VsaTemplate) - def show(self, req, id): - """Return data about the given VSA.""" - context = req.environ['nova.context'] - - try: - vsa = self.vsa_api.get(context, vsa_id=id) - except exception.NotFound: - raise exc.HTTPNotFound() - - instances = self._get_instances_by_vsa_id(context, vsa.get('id')) - return {'vsa': _vsa_view(context, vsa, True, instances)} - - @wsgi.serializers(xml=VsaTemplate) - def create(self, req, body): - """Create a new VSA.""" - context = req.environ['nova.context'] - - if not body or 'vsa' not in body: - LOG.debug(_("No body provided"), context=context) - raise exc.HTTPUnprocessableEntity() - - vsa = body['vsa'] - - display_name = vsa.get('displayName') - vc_type = vsa.get('vcType', FLAGS.default_vsa_instance_type) - try: - instance_type = instance_types.get_instance_type_by_name(vc_type) - except exception.NotFound: - raise exc.HTTPNotFound() - - LOG.audit(_("Create VSA %(display_name)s of type %(vc_type)s"), - locals(), context=context) - - args = dict(display_name=display_name, - display_description=vsa.get('displayDescription'), - instance_type=instance_type, - storage=vsa.get('storage'), - shared=vsa.get('shared'), - availability_zone=vsa.get('placement', {}).\ - get('AvailabilityZone')) - - vsa = self.vsa_api.create(context, **args) - - instances = self._get_instances_by_vsa_id(context, vsa.get('id')) - return {'vsa': _vsa_view(context, vsa, True, instances)} - - def delete(self, req, id): - """Delete a VSA.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete VSA with id: %s"), id, context=context) - - try: - self.vsa_api.delete(context, vsa_id=id) - except exception.NotFound: - raise exc.HTTPNotFound() - - def associate_address(self, req, id, body): - """ /zadr-vsa/{vsa_id}/associate_address - auto or manually associate an IP to VSA - """ - context = req.environ['nova.context'] - - if body is None: - ip = 'auto' - else: - ip = body.get('ipAddress', 'auto') - - LOG.audit(_("Associate address %(ip)s to VSA %(id)s"), - locals(), context=context) - - try: - instances = self._get_instances_by_vsa_id(context, id) - if instances is None or len(instances) == 0: - raise exc.HTTPNotFound() - - for instance in instances: - self.network_api.allocate_for_instance(context, instance, - vpn=False) - # Placeholder - return - - except exception.NotFound: - raise exc.HTTPNotFound() - - def disassociate_address(self, req, id, body): - """ /zadr-vsa/{vsa_id}/disassociate_address - auto or manually associate an IP to VSA - """ - context = req.environ['nova.context'] - - if body is None: - ip = 'auto' - else: - ip = body.get('ipAddress', 'auto') - - LOG.audit(_("Disassociate address from VSA %(id)s"), - locals(), context=context) - # Placeholder - - -def make_volume(elem): - volumes.make_volume(elem) - elem.set('name') - elem.set('vsaId') - - -class VsaVolumeDriveController(volumes.VolumeController): - """The base class for VSA volumes & drives. - - A child resource of the VSA object. Allows operations with - volumes and drives created to/from particular VSA - - """ - - def __init__(self): - self.volume_api = volume.API() - self.vsa_api = vsa.API() - super(VsaVolumeDriveController, self).__init__() - - def _translation(self, context, vol, vsa_id, details): - if details: - translation = volumes._translate_volume_detail_view - else: - translation = volumes._translate_volume_summary_view - - d = translation(context, vol) - d['vsaId'] = vsa_id - d['name'] = vol['name'] - return d - - def _check_volume_ownership(self, context, vsa_id, id): - obj = self.object - try: - volume_ref = self.volume_api.get(context, volume_id=id) - except exception.NotFound: - LOG.error(_("%(obj)s with ID %(id)s not found"), locals()) - raise - - own_vsa_id = self.volume_api.get_volume_metadata_value(volume_ref, - self.direction) - if own_vsa_id != vsa_id: - LOG.error(_("%(obj)s with ID %(id)s belongs to VSA %(own_vsa_id)s"\ - " and not to VSA %(vsa_id)s."), locals()) - raise exception.Invalid() - - def _items(self, req, vsa_id, details): - """Return summary or detailed list of volumes for particular VSA.""" - context = req.environ['nova.context'] - - vols = self.volume_api.get_all(context, - search_opts={'metadata': {self.direction: str(vsa_id)}}) - limited_list = common.limited(vols, req) - - res = [self._translation(context, vol, vsa_id, details) \ - for vol in limited_list] - - return {self.objects: res} - - def index(self, req, vsa_id): - """Return a short list of volumes created from particular VSA.""" - LOG.audit(_("Index. vsa_id=%(vsa_id)s"), locals()) - return self._items(req, vsa_id, details=False) - - def detail(self, req, vsa_id): - """Return a detailed list of volumes created from particular VSA.""" - LOG.audit(_("Detail. vsa_id=%(vsa_id)s"), locals()) - return self._items(req, vsa_id, details=True) - - def create(self, req, vsa_id, body): - """Create a new volume from VSA.""" - LOG.audit(_("Create. vsa_id=%(vsa_id)s, body=%(body)s"), locals()) - context = req.environ['nova.context'] - - if not body: - raise exc.HTTPUnprocessableEntity() - - vol = body[self.object] - size = vol['size'] - LOG.audit(_("Create volume of %(size)s GB from VSA ID %(vsa_id)s"), - locals(), context=context) - try: - # create is supported for volumes only (drives created through VSA) - volume_type = self.vsa_api.get_vsa_volume_type(context) - except exception.NotFound: - raise exc.HTTPNotFound() - - new_volume = self.volume_api.create(context, - size, - None, - vol.get('displayName'), - vol.get('displayDescription'), - volume_type=volume_type, - metadata=dict(from_vsa_id=str(vsa_id))) - - return {self.object: self._translation(context, new_volume, - vsa_id, True)} - - def update(self, req, vsa_id, id, body): - """Update a volume.""" - context = req.environ['nova.context'] - - try: - self._check_volume_ownership(context, vsa_id, id) - except exception.NotFound: - raise exc.HTTPNotFound() - except exception.Invalid: - raise exc.HTTPBadRequest() - - vol = body[self.object] - updatable_fields = [{'displayName': 'display_name'}, - {'displayDescription': 'display_description'}, - {'status': 'status'}, - {'providerLocation': 'provider_location'}, - {'providerAuth': 'provider_auth'}] - changes = {} - for field in updatable_fields: - key = field.keys()[0] - val = field[key] - if key in vol: - changes[val] = vol[key] - - obj = self.object - LOG.audit(_("Update %(obj)s with id: %(id)s, changes: %(changes)s"), - locals(), context=context) - - try: - self.volume_api.update(context, volume_id=id, fields=changes) - except exception.NotFound: - raise exc.HTTPNotFound() - return webob.Response(status_int=202) - - def delete(self, req, vsa_id, id): - """Delete a volume.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete. vsa_id=%(vsa_id)s, id=%(id)s"), locals()) - - try: - self._check_volume_ownership(context, vsa_id, id) - except exception.NotFound: - raise exc.HTTPNotFound() - except exception.Invalid: - raise exc.HTTPBadRequest() - - return super(VsaVolumeDriveController, self).delete(req, id) - - def show(self, req, vsa_id, id): - """Return data about the given volume.""" - context = req.environ['nova.context'] - - LOG.audit(_("Show. vsa_id=%(vsa_id)s, id=%(id)s"), locals()) - - try: - self._check_volume_ownership(context, vsa_id, id) - except exception.NotFound: - raise exc.HTTPNotFound() - except exception.Invalid: - raise exc.HTTPBadRequest() - - return super(VsaVolumeDriveController, self).show(req, id) - - -class VsaVolumeTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume', selector='volume') - make_volume(root) - return xmlutil.MasterTemplate(root, 1) - - -class VsaVolumesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volumes') - elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes') - make_volume(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VsaVolumeController(VsaVolumeDriveController): - """The VSA volume API controller for the Openstack API. - - A child resource of the VSA object. Allows operations with volumes created - by particular VSA - - """ - - def __init__(self): - self.direction = 'from_vsa_id' - self.objects = 'volumes' - self.object = 'volume' - super(VsaVolumeController, self).__init__() - - @wsgi.serializers(xml=VsaVolumesTemplate) - def index(self, req, vsa_id): - return super(VsaVolumeController, self).index(req, vsa_id) - - @wsgi.serializers(xml=VsaVolumesTemplate) - def detail(self, req, vsa_id): - return super(VsaVolumeController, self).detail(req, vsa_id) - - @wsgi.serializers(xml=VsaVolumeTemplate) - def create(self, req, vsa_id, body): - return super(VsaVolumeController, self).create(req, vsa_id, body) - - @wsgi.serializers(xml=VsaVolumeTemplate) - def update(self, req, vsa_id, id, body): - return super(VsaVolumeController, self).update(req, vsa_id, id, body) - - @wsgi.serializers(xml=VsaVolumeTemplate) - def show(self, req, vsa_id, id): - return super(VsaVolumeController, self).show(req, vsa_id, id) - - -class VsaDriveTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('drive', selector='drive') - make_volume(root) - return xmlutil.MasterTemplate(root, 1) - - -class VsaDrivesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('drives') - elem = xmlutil.SubTemplateElement(root, 'drive', selector='drives') - make_volume(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VsaDriveController(VsaVolumeDriveController): - """The VSA Drive API controller for the Openstack API. - - A child resource of the VSA object. Allows operations with drives created - for particular VSA - - """ - - def __init__(self): - self.direction = 'to_vsa_id' - self.objects = 'drives' - self.object = 'drive' - super(VsaDriveController, self).__init__() - - def create(self, req, vsa_id, body): - """Create a new drive for VSA. Should be done through VSA APIs""" - raise exc.HTTPBadRequest() - - def update(self, req, vsa_id, id, body): - """Update a drive. Should be done through VSA APIs""" - raise exc.HTTPBadRequest() - - def delete(self, req, vsa_id, id): - """Delete a volume. Should be done through VSA APIs""" - raise exc.HTTPBadRequest() - - @wsgi.serializers(xml=VsaDrivesTemplate) - def index(self, req, vsa_id): - return super(VsaDriveController, self).index(req, vsa_id) - - @wsgi.serializers(xml=VsaDrivesTemplate) - def detail(self, req, vsa_id): - return super(VsaDriveController, self).detail(req, vsa_id) - - @wsgi.serializers(xml=VsaDriveTemplate) - def show(self, req, vsa_id, id): - return super(VsaDriveController, self).show(req, vsa_id, id) - - -def make_vpool(elem): - elem.set('id') - elem.set('vsaId') - elem.set('name') - elem.set('displayName') - elem.set('displayDescription') - elem.set('driveCount') - elem.set('protection') - elem.set('stripeSize') - elem.set('stripeWidth') - elem.set('createTime') - elem.set('status') - - drive_ids = xmlutil.SubTemplateElement(elem, 'driveIds') - drive_id = xmlutil.SubTemplateElement(drive_ids, 'driveId', - selector='driveIds') - drive_id.text = xmlutil.Selector() - - -class VsaVPoolTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('vpool', selector='vpool') - make_vpool(root) - return xmlutil.MasterTemplate(root, 1) - - -class VsaVPoolsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('vpools') - elem = xmlutil.SubTemplateElement(root, 'vpool', selector='vpools') - make_vpool(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VsaVPoolController(object): - """The vPool VSA API controller for the OpenStack API.""" - - def __init__(self): - self.vsa_api = vsa.API() - super(VsaVPoolController, self).__init__() - - @wsgi.serializers(xml=VsaVPoolsTemplate) - def index(self, req, vsa_id): - """Return a short list of vpools created from particular VSA.""" - return {'vpools': []} - - def create(self, req, vsa_id, body): - """Create a new vPool for VSA.""" - raise exc.HTTPBadRequest() - - def update(self, req, vsa_id, id, body): - """Update vPool parameters.""" - raise exc.HTTPBadRequest() - - def delete(self, req, vsa_id, id): - """Delete a vPool.""" - raise exc.HTTPBadRequest() - - def show(self, req, vsa_id, id): - """Return data about the given vPool.""" - raise exc.HTTPBadRequest() - - -class VsaVCController(servers.Controller): - """The VSA Virtual Controller API controller for the OpenStack API.""" - - def __init__(self): - self.vsa_api = vsa.API() - self.compute_api = compute.API() - self.vsa_id = None # VP-TODO: temporary ugly hack - super(VsaVCController, self).__init__() - - def _get_servers(self, req, is_detail): - """Returns a list of servers, taking into account any search - options specified. - """ - - if self.vsa_id is None: - super(VsaVCController, self)._get_servers(req, is_detail) - - context = req.environ['nova.context'] - - search_opts = {'metadata': dict(vsa_id=str(self.vsa_id))} - instance_list = self.compute_api.get_all( - context, search_opts=search_opts) - - limited_list = self._limit_items(instance_list, req) - servers = [self._build_view(req, inst, is_detail)['server'] - for inst in limited_list] - return dict(servers=servers) - - @wsgi.serializers(xml=servers.MinimalServersTemplate) - def index(self, req, vsa_id): - """Return list of instances for particular VSA.""" - - LOG.audit(_("Index instances for VSA %s"), vsa_id) - - self.vsa_id = vsa_id # VP-TODO: temporary ugly hack - result = super(VsaVCController, self).detail(req) - self.vsa_id = None - return result - - def create(self, req, vsa_id, body): - """Create a new instance for VSA.""" - raise exc.HTTPBadRequest() - - def update(self, req, vsa_id, id, body): - """Update VSA instance.""" - raise exc.HTTPBadRequest() - - def delete(self, req, vsa_id, id): - """Delete VSA instance.""" - raise exc.HTTPBadRequest() - - @wsgi.serializers(xml=servers.ServerTemplate) - def show(self, req, vsa_id, id): - """Return data about the given instance.""" - return super(VsaVCController, self).show(req, id) - - -class Virtual_storage_arrays(extensions.ExtensionDescriptor): - """Virtual Storage Arrays support""" - - name = "VSAs" - alias = "zadr-vsa" - namespace = "http://docs.openstack.org/compute/ext/vsa/api/v1.1" - updated = "2011-08-25T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension( - 'zadr-vsa', - VsaController(), - collection_actions={'detail': 'GET'}, - member_actions={'add_capacity': 'POST', - 'remove_capacity': 'POST', - 'associate_address': 'POST', - 'disassociate_address': 'POST'}) - resources.append(res) - - res = extensions.ResourceExtension('volumes', - VsaVolumeController(), - collection_actions={'detail': 'GET'}, - parent=dict( - member_name='vsa', - collection_name='zadr-vsa')) - resources.append(res) - - res = extensions.ResourceExtension('drives', - VsaDriveController(), - collection_actions={'detail': 'GET'}, - parent=dict( - member_name='vsa', - collection_name='zadr-vsa')) - resources.append(res) - - res = extensions.ResourceExtension('vpools', - VsaVPoolController(), - parent=dict( - member_name='vsa', - collection_name='zadr-vsa')) - resources.append(res) - - res = extensions.ResourceExtension('instances', - VsaVCController(), - parent=dict( - member_name='vsa', - collection_name='zadr-vsa')) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/volumes.py b/nova/api/openstack/v2/contrib/volumes.py deleted file mode 100644 index 0ca50b288..000000000 --- a/nova/api/openstack/v2/contrib/volumes.py +++ /dev/null @@ -1,550 +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 -import webob - -from nova.api.openstack import common -from nova.api.openstack.v2 import extensions -from nova.api.openstack.v2 import servers -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova import exception -from nova import flags -from nova import log as logging -from nova import volume -from nova.volume import volume_types - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.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'] - - if vol['volume_type_id'] and vol.get('volume_type'): - d['volumeType'] = vol['volume_type']['name'] - else: - d['volumeType'] = vol['volume_type_id'] - - d['snapshotId'] = vol['snapshot_id'] - LOG.audit(_("vol=%s"), vol, context=context) - - if vol.get('volume_metadata'): - meta_dict = {} - for i in vol['volume_metadata']: - meta_dict[i['key']] = i['value'] - d['metadata'] = meta_dict - else: - d['metadata'] = {} - - return d - - -def make_volume(elem): - elem.set('id') - elem.set('status') - elem.set('size') - elem.set('availabilityZone') - elem.set('createdAt') - elem.set('displayName') - elem.set('displayDescription') - elem.set('volumeType') - elem.set('snapshotId') - - attachments = xmlutil.SubTemplateElement(elem, 'attachments') - attachment = xmlutil.SubTemplateElement(attachments, 'attachment', - selector='attachments') - make_attachment(attachment) - - metadata = xmlutil.make_flat_dict('metadata') - elem.append(metadata) - - -class VolumeTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume', selector='volume') - make_volume(root) - return xmlutil.MasterTemplate(root, 1) - - -class VolumesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volumes') - elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes') - make_volume(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeController(object): - """The Volumes API controller for the OpenStack API.""" - - def __init__(self): - self.volume_api = volume.API() - super(VolumeController, self).__init__() - - @wsgi.serializers(xml=VolumeTemplate) - 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: - raise 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: - raise exc.HTTPNotFound() - return webob.Response(status_int=202) - - @wsgi.serializers(xml=VolumesTemplate) - def index(self, req): - """Returns a summary list of volumes.""" - return self._items(req, entity_maker=_translate_volume_summary_view) - - @wsgi.serializers(xml=VolumesTemplate) - 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} - - @wsgi.serializers(xml=VolumeTemplate) - def create(self, req, body): - """Creates a new volume.""" - context = req.environ['nova.context'] - - if not body: - raise exc.HTTPUnprocessableEntity() - - vol = body['volume'] - size = vol['size'] - LOG.audit(_("Create volume of %s GB"), size, context=context) - - vol_type = vol.get('volume_type', None) - if vol_type: - try: - vol_type = volume_types.get_volume_type_by_name(context, - vol_type) - except exception.NotFound: - raise exc.HTTPNotFound() - - metadata = vol.get('metadata', None) - - new_volume = self.volume_api.create(context, size, - vol.get('snapshot_id'), - vol.get('display_name'), - vol.get('display_description'), - volume_type=vol_type, - metadata=metadata) - - # Work around problem that instance is lazy-loaded... - new_volume = self.volume_api.get(context, new_volume['id']) - - 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'): - d['serverId'] = vol['instance']['uuid'] - if vol.get('mountpoint'): - d['device'] = vol['mountpoint'] - - return d - - -def make_attachment(elem): - elem.set('id') - elem.set('serverId') - elem.set('volumeId') - elem.set('device') - - -class VolumeAttachmentTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volumeAttachment', - selector='volumeAttachment') - make_attachment(root) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeAttachmentsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volumeAttachments') - elem = xmlutil.SubTemplateElement(root, 'volumeAttachment', - selector='volumeAttachments') - make_attachment(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeAttachmentController(object): - """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) - - """ - - def __init__(self): - self.compute_api = compute.API() - self.volume_api = volume.API() - super(VolumeAttachmentController, self).__init__() - - @wsgi.serializers(xml=VolumeAttachmentsTemplate) - 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) - - @wsgi.serializers(xml=VolumeAttachmentTemplate) - 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") - raise exc.HTTPNotFound() - - instance = vol['instance'] - if instance is None or str(instance['uuid']) != server_id: - LOG.debug("instance_id != server_id") - raise exc.HTTPNotFound() - - return {'volumeAttachment': _translate_attachment_detail_view(context, - vol)} - - @wsgi.serializers(xml=VolumeAttachmentTemplate) - def create(self, req, server_id, body): - """Attach a volume to an instance.""" - context = req.environ['nova.context'] - - if not body: - raise exc.HTTPUnprocessableEntity() - - volume_id = body['volumeAttachment']['volumeId'] - device = body['volumeAttachment']['device'] - - msg = _("Attach volume %(volume_id)s to instance %(server_id)s" - " at %(device)s") % locals() - LOG.audit(msg, context=context) - - try: - instance = self.compute_api.get(context, server_id) - self.compute_api.attach_volume(context, instance, - volume_id, device) - except exception.NotFound: - raise 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, body): - """Update a volume attachment. We don't currently support this.""" - raise 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: - raise exc.HTTPNotFound() - - instance = vol['instance'] - if instance is None or str(instance['uuid']) != server_id: - LOG.debug("instance_id != server_id") - raise exc.HTTPNotFound() - - self.compute_api.detach_volume(context, - volume_id=volume_id) - - return webob.Response(status_int=202) - - 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: - raise 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 BootFromVolumeController(servers.Controller): - """The boot from volume API controller for the Openstack API.""" - - def _get_block_device_mapping(self, data): - return data.get('block_device_mapping') - - -def _translate_snapshot_detail_view(context, vol): - """Maps keys for snapshots details view.""" - - d = _translate_snapshot_summary_view(context, vol) - - # NOTE(gagupta): No additional data / lookups at the moment - return d - - -def _translate_snapshot_summary_view(context, vol): - """Maps keys for snapshots summary view.""" - d = {} - - d['id'] = vol['id'] - d['volumeId'] = vol['volume_id'] - d['status'] = vol['status'] - # NOTE(gagupta): We map volume_size as the snapshot size - d['size'] = vol['volume_size'] - d['createdAt'] = vol['created_at'] - d['displayName'] = vol['display_name'] - d['displayDescription'] = vol['display_description'] - return d - - -def make_snapshot(elem): - elem.set('id') - elem.set('status') - elem.set('size') - elem.set('createdAt') - elem.set('displayName') - elem.set('displayDescription') - elem.set('volumeId') - - -class SnapshotTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('snapshot', selector='snapshot') - make_snapshot(root) - return xmlutil.MasterTemplate(root, 1) - - -class SnapshotsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('snapshots') - elem = xmlutil.SubTemplateElement(root, 'snapshot', - selector='snapshots') - make_snapshot(elem) - return xmlutil.MasterTemplate(root, 1) - - -class SnapshotController(object): - """The Volumes API controller for the OpenStack API.""" - - def __init__(self): - self.volume_api = volume.API() - super(SnapshotController, self).__init__() - - @wsgi.serializers(xml=SnapshotTemplate) - def show(self, req, id): - """Return data about the given snapshot.""" - context = req.environ['nova.context'] - - try: - vol = self.volume_api.get_snapshot(context, id) - except exception.NotFound: - return exc.HTTPNotFound() - - return {'snapshot': _translate_snapshot_detail_view(context, vol)} - - def delete(self, req, id): - """Delete a snapshot.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete snapshot with id: %s"), id, context=context) - - try: - self.volume_api.delete_snapshot(context, snapshot_id=id) - except exception.NotFound: - return exc.HTTPNotFound() - return webob.Response(status_int=202) - - @wsgi.serializers(xml=SnapshotsTemplate) - def index(self, req): - """Returns a summary list of snapshots.""" - return self._items(req, entity_maker=_translate_snapshot_summary_view) - - @wsgi.serializers(xml=SnapshotsTemplate) - def detail(self, req): - """Returns a detailed list of snapshots.""" - return self._items(req, entity_maker=_translate_snapshot_detail_view) - - def _items(self, req, entity_maker): - """Returns a list of snapshots, transformed through entity_maker.""" - context = req.environ['nova.context'] - - snapshots = self.volume_api.get_all_snapshots(context) - limited_list = common.limited(snapshots, req) - res = [entity_maker(context, snapshot) for snapshot in limited_list] - return {'snapshots': res} - - @wsgi.serializers(xml=SnapshotTemplate) - def create(self, req, body): - """Creates a new snapshot.""" - context = req.environ['nova.context'] - - if not body: - return exc.HTTPUnprocessableEntity() - - snapshot = body['snapshot'] - volume_id = snapshot['volume_id'] - force = snapshot.get('force', False) - LOG.audit(_("Create snapshot from volume %s"), volume_id, - context=context) - - if force: - new_snapshot = self.volume_api.create_snapshot_force(context, - volume_id, - snapshot.get('display_name'), - snapshot.get('display_description')) - else: - new_snapshot = self.volume_api.create_snapshot(context, - volume_id, - snapshot.get('display_name'), - snapshot.get('display_description')) - - retval = _translate_snapshot_detail_view(context, new_snapshot) - - return {'snapshot': retval} - - -class Volumes(extensions.ExtensionDescriptor): - """Volumes support""" - - name = "Volumes" - alias = "os-volumes" - namespace = "http://docs.openstack.org/compute/ext/volumes/api/v1.1" - updated = "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('os-volumes', - VolumeController(), - collection_actions={'detail': 'GET'}) - resources.append(res) - - res = extensions.ResourceExtension('os-volume_attachments', - VolumeAttachmentController(), - parent=dict( - member_name='server', - collection_name='servers')) - resources.append(res) - - res = extensions.ResourceExtension('os-volumes_boot', - BootFromVolumeController()) - resources.append(res) - - res = extensions.ResourceExtension('os-snapshots', - SnapshotController(), - collection_actions={'detail': 'GET'}) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/volumetypes.py b/nova/api/openstack/v2/contrib/volumetypes.py deleted file mode 100644 index 231c86b1b..000000000 --- a/nova/api/openstack/v2/contrib/volumetypes.py +++ /dev/null @@ -1,237 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 Zadara Storage Inc. -# Copyright (c) 2011 OpenStack LLC. -# -# 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 volume type & volume types extra specs extension""" - -from webob import exc - -from nova.api.openstack.v2 import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import db -from nova import exception -from nova.volume import volume_types - - -def make_voltype(elem): - elem.set('id') - elem.set('name') - extra_specs = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') - elem.append(extra_specs) - - -class VolumeTypeTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume_type', selector='volume_type') - make_voltype(root) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume_types') - sel = lambda obj, do_raise=False: obj.values() - elem = xmlutil.SubTemplateElement(root, 'volume_type', selector=sel) - make_voltype(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypesController(object): - """ The volume types API controller for the Openstack API """ - - @wsgi.serializers(xml=VolumeTypesTemplate) - def index(self, req): - """ Returns the list of volume types """ - context = req.environ['nova.context'] - return volume_types.get_all_types(context) - - @wsgi.serializers(xml=VolumeTypeTemplate) - def create(self, req, body): - """Creates a new volume type.""" - context = req.environ['nova.context'] - - if not body or body == "": - raise exc.HTTPUnprocessableEntity() - - vol_type = body.get('volume_type', None) - if vol_type is None or vol_type == "": - raise exc.HTTPUnprocessableEntity() - - name = vol_type.get('name', None) - specs = vol_type.get('extra_specs', {}) - - if name is None or name == "": - raise exc.HTTPUnprocessableEntity() - - try: - volume_types.create(context, name, specs) - vol_type = volume_types.get_volume_type_by_name(context, name) - except exception.QuotaError as error: - self._handle_quota_error(error) - except exception.NotFound: - raise exc.HTTPNotFound() - - return {'volume_type': vol_type} - - @wsgi.serializers(xml=VolumeTypeTemplate) - def show(self, req, id): - """ Return a single volume type item """ - context = req.environ['nova.context'] - - try: - vol_type = volume_types.get_volume_type(context, id) - except exception.NotFound or exception.ApiError: - raise exc.HTTPNotFound() - - return {'volume_type': vol_type} - - def delete(self, req, id): - """ Deletes an existing volume type """ - context = req.environ['nova.context'] - - try: - vol_type = volume_types.get_volume_type(context, id) - volume_types.destroy(context, vol_type['name']) - except exception.NotFound: - raise exc.HTTPNotFound() - - def _handle_quota_error(self, error): - """Reraise quota errors as api-specific http exceptions.""" - if error.code == "MetadataLimitExceeded": - raise exc.HTTPBadRequest(explanation=error.message) - raise error - - -class VolumeTypeExtraSpecsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypeExtraSpecTemplate(xmlutil.TemplateBuilder): - def construct(self): - tagname = xmlutil.Selector('key') - - def extraspec_sel(obj, do_raise=False): - # Have to extract the key and value for later use... - key, value = obj.items()[0] - return dict(key=key, value=value) - - root = xmlutil.TemplateElement(tagname, selector=extraspec_sel) - root.text = 'value' - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypeExtraSpecsController(object): - """ The volume type extra specs API controller for the Openstack API """ - - def _get_extra_specs(self, context, vol_type_id): - extra_specs = db.volume_type_extra_specs_get(context, vol_type_id) - specs_dict = {} - for key, value in extra_specs.iteritems(): - specs_dict[key] = value - return dict(extra_specs=specs_dict) - - def _check_body(self, body): - if body is None or body == "": - expl = _('No Request Body') - raise exc.HTTPBadRequest(explanation=expl) - - @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) - def index(self, req, vol_type_id): - """ Returns the list of extra specs for a given volume type """ - context = req.environ['nova.context'] - return self._get_extra_specs(context, vol_type_id) - - @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) - def create(self, req, vol_type_id, body): - self._check_body(body) - context = req.environ['nova.context'] - specs = body.get('extra_specs') - try: - db.volume_type_extra_specs_update_or_create(context, - vol_type_id, - specs) - except exception.QuotaError as error: - self._handle_quota_error(error) - return body - - @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) - def update(self, req, vol_type_id, id, body): - self._check_body(body) - context = req.environ['nova.context'] - 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) - try: - db.volume_type_extra_specs_update_or_create(context, - vol_type_id, - body) - except exception.QuotaError as error: - self._handle_quota_error(error) - - return body - - @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) - def show(self, req, vol_type_id, id): - """ Return a single extra spec item """ - context = req.environ['nova.context'] - specs = self._get_extra_specs(context, vol_type_id) - if id in specs['extra_specs']: - return {id: specs['extra_specs'][id]} - else: - raise exc.HTTPNotFound() - - def delete(self, req, vol_type_id, id): - """ Deletes an existing extra spec """ - context = req.environ['nova.context'] - db.volume_type_extra_specs_delete(context, vol_type_id, id) - - def _handle_quota_error(self, error): - """Reraise quota errors as api-specific http exceptions.""" - if error.code == "MetadataLimitExceeded": - raise exc.HTTPBadRequest(explanation=error.message) - raise error - - -class Volumetypes(extensions.ExtensionDescriptor): - """Volume types support""" - - name = "VolumeTypes" - alias = "os-volume-types" - namespace = "http://docs.openstack.org/compute/ext/volume_types/api/v1.1" - updated = "2011-08-24T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension( - 'os-volume-types', - VolumeTypesController()) - resources.append(res) - - res = extensions.ResourceExtension('extra_specs', - VolumeTypeExtraSpecsController(), - parent=dict( - member_name='vol_type', - collection_name='os-volume-types')) - resources.append(res) - - return resources diff --git a/nova/api/openstack/v2/contrib/zones.py b/nova/api/openstack/v2/contrib/zones.py deleted file mode 100644 index adbc6580f..000000000 --- a/nova/api/openstack/v2/contrib/zones.py +++ /dev/null @@ -1,239 +0,0 @@ -# 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. - -"""The zones extension.""" - -import json - -from nova.api.openstack import common -from nova.api.openstack.v2 import servers -from nova.api.openstack.v2 import extensions -from nova.api.openstack import xmlutil -from nova.api.openstack import wsgi -from nova.compute import api as compute -from nova import crypto -from nova import exception -from nova import flags -from nova import log as logging -import nova.scheduler.api - - -LOG = logging.getLogger("nova.api.openstack.v2.contrib.zones") -FLAGS = flags.FLAGS - - -class CapabilitySelector(object): - def __call__(self, obj, do_raise=False): - return [(k, v) for k, v in obj.items() - if k not in ('id', 'api_url', 'name', 'capabilities')] - - -def make_zone(elem): - elem.set('id') - elem.set('api_url') - elem.set('name') - elem.set('capabilities') - - cap = xmlutil.SubTemplateElement(elem, xmlutil.Selector(0), - selector=CapabilitySelector()) - cap.text = 1 - - -zone_nsmap = {None: wsgi.XMLNS_V10} - - -class ZoneTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('zone', selector='zone') - make_zone(root) - return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) - - -class ZonesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('zones') - elem = xmlutil.SubTemplateElement(root, 'zone', selector='zones') - make_zone(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) - - -class WeightsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('weights') - weight = xmlutil.SubTemplateElement(root, 'weight', selector='weights') - blob = xmlutil.SubTemplateElement(weight, 'blob') - blob.text = 'blob' - inner_weight = xmlutil.SubTemplateElement(weight, 'weight') - inner_weight.text = 'weight' - return xmlutil.MasterTemplate(root, 1, nsmap=zone_nsmap) - - -def _filter_keys(item, keys): - """ - Filters all model attributes except for keys - item is a dict - - """ - 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 and (k not in keys)) - - -def _scrub_zone(zone): - return _exclude_keys(zone, ('username', 'password', 'created_at', - 'deleted', 'deleted_at', 'updated_at')) - - -def check_encryption_key(func): - def wrapped(*args, **kwargs): - if not FLAGS.build_plan_encryption_key: - raise exception.Error(_("--build_plan_encryption_key not set")) - return func(*args, **kwargs) - return wrapped - - -class Controller(object): - """Controller for Zone resources.""" - - def __init__(self): - self.compute_api = compute.API() - - @wsgi.serializers(xml=ZonesTemplate) - def index(self, req): - """Return all zones in brief""" - # Ask the ZoneManager in the Scheduler for most recent data, - # or fall-back to the database ... - items = nova.scheduler.api.get_zone_list(req.environ['nova.context']) - items = common.limited(items, req) - items = [_scrub_zone(item) for item in items] - return dict(zones=items) - - @wsgi.serializers(xml=ZonesTemplate) - def detail(self, req): - """Return all zones in detail""" - return self.index(req) - - @wsgi.serializers(xml=ZoneTemplate) - def info(self, req): - """Return name and capabilities for this zone.""" - context = req.environ['nova.context'] - items = nova.scheduler.api.get_zone_capabilities(context) - - zone = dict(name=FLAGS.zone_name) - caps = FLAGS.zone_capabilities - for cap in caps: - 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) - - @wsgi.serializers(xml=ZoneTemplate) - def show(self, req, id): - """Return data about the given zone id""" - zone_id = int(id) - context = req.environ['nova.context'] - zone = nova.scheduler.api.zone_get(context, zone_id) - return dict(zone=_scrub_zone(zone)) - - def delete(self, req, id): - """Delete a child zone entry.""" - zone_id = int(id) - nova.scheduler.api.zone_delete(req.environ['nova.context'], zone_id) - return {} - - @wsgi.serializers(xml=ZoneTemplate) - @wsgi.deserializers(xml=servers.CreateDeserializer) - def create(self, req, body): - """Create a child zone entry.""" - context = req.environ['nova.context'] - zone = nova.scheduler.api.zone_create(context, body["zone"]) - return dict(zone=_scrub_zone(zone)) - - @wsgi.serializers(xml=ZoneTemplate) - def update(self, req, id, body): - """Update a child zone entry.""" - context = req.environ['nova.context'] - zone_id = int(id) - zone = nova.scheduler.api.zone_update(context, zone_id, body["zone"]) - return dict(zone=_scrub_zone(zone)) - - @wsgi.serializers(xml=WeightsTemplate) - @check_encryption_key - def select(self, req, body): - """Returns a weighted list of costs to create instances - of desired capabilities.""" - ctx = req.environ['nova.context'] - specs = json.loads(body) - build_plan = nova.scheduler.api.select(ctx, specs=specs) - cooked = self._scrub_build_plan(build_plan) - return {"weights": cooked} - - def _scrub_build_plan(self, build_plan): - """Remove all the confidential data and return a sanitized - version of the build plan. Include an encrypted full version - of the weighting entry so we can get back to it later.""" - encryptor = crypto.encryptor(FLAGS.build_plan_encryption_key) - cooked = [] - for entry in build_plan: - json_entry = json.dumps(entry) - cipher_text = encryptor(json_entry) - cooked.append(dict(weight=entry['weight'], - blob=cipher_text)) - return cooked - - -class ZonesXMLSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return ZonesTemplate() - - def detail(self): - return ZonesTemplate() - - def select(self): - return WeightsTemplate() - - def default(self): - return ZoneTemplate() - - -class Zones(extensions.ExtensionDescriptor): - """Enables zones-related functionality such as adding child zones, - listing child zones, getting the capabilities of the local zone, - and returning build plans to parent zones' schedulers - """ - - name = "Zones" - alias = "os-zones" - namespace = "http://docs.openstack.org/compute/ext/zones/api/v1.1" - updated = "2011-09-21T00:00:00+00:00" - admin_only = True - - def get_resources(self): - #NOTE(bcwaldon): This resource should be prefixed with 'os-' - coll_actions = { - 'detail': 'GET', - 'info': 'GET', - 'select': 'POST', - } - - res = extensions.ResourceExtension('zones', - Controller(), - collection_actions=coll_actions) - return [res] diff --git a/nova/api/openstack/v2/extensions.py b/nova/api/openstack/v2/extensions.py deleted file mode 100644 index 0aa3146a9..000000000 --- a/nova/api/openstack/v2/extensions.py +++ /dev/null @@ -1,575 +0,0 @@ -# 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 -# 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 routes -import webob.dec -import webob.exc - -import nova.api.openstack.v2 -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils -from nova import wsgi as base_wsgi - - -LOG = logging.getLogger('nova.api.openstack.v2.extensions') - - -FLAGS = flags.FLAGS - - -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. - - """ - - # The name of the extension, e.g., 'Fox In Socks' - name = None - - # The alias for the extension, e.g., 'FOXNSOX' - alias = None - - # Description comes from the docstring for the class - - # The XML namespace for the extension, e.g., - # 'http://www.fox.in.socks/api/ext/pie/v1.0' - namespace = None - - # The timestamp when the extension was last updated, e.g., - # '2011-01-22T13:25:27-06:00' - updated = None - - # This attribute causes the extension to load only when - # the admin api is enabled - admin_only = False - - def __init__(self, ext_mgr): - """Register extension with the extension manager.""" - - ext_mgr.register(self) - - 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_request_extensions(self): - """List of extensions.RequestExtension extension objects. - - Request extensions are used to handle custom request data. - - """ - request_exts = [] - return request_exts - - @classmethod - def nsmap(cls): - """Synthesize a namespace map from extension.""" - - # Start with a base nsmap - nsmap = ext_nsmap.copy() - - # Add the namespace for the extension - nsmap[cls.alias] = cls.namespace - - return nsmap - - @classmethod - def xmlname(cls, name): - """Synthesize element and attribute names.""" - - return '{%s}%s' % (cls.namespace, name) - - -class ActionExtensionController(object): - def __init__(self, application): - self.application = application - self.action_handlers = {} - - def add_action(self, action_name, handler): - self.action_handlers[action_name] = handler - - def action(self, req, id, body): - for action_name, handler in self.action_handlers.iteritems(): - if action_name in body: - return handler(body, req, id) - # no action handler found (bump to downstream application) - res = self.application - return res - - -class ActionExtensionResource(wsgi.Resource): - - def __init__(self, application): - controller = ActionExtensionController(application) - wsgi.Resource.__init__(self, controller, - serializer=wsgi.ResponseSerializer(), - deserializer=wsgi.RequestDeserializer()) - - def add_action(self, action_name, handler): - self.controller.add_action(action_name, handler) - - -class RequestExtensionController(object): - - def __init__(self, application): - self.application = application - self.handlers = [] - self.pre_handlers = [] - - def add_handler(self, handler): - self.handlers.append(handler) - - def add_pre_handler(self, pre_handler): - self.pre_handlers.append(pre_handler) - - def process(self, req, *args, **kwargs): - for pre_handler in self.pre_handlers: - pre_handler(req) - - res = req.get_response(self.application) - res.environ = req.environ - - # Don't call extensions if the main application returned an - # unsuccessful status - successful = 200 <= res.status_int < 400 - if not successful: - return res - - # Deserialize the response body, if any - body = None - if res.body: - body = utils.loads(res.body) - - # currently request handlers are un-ordered - for handler in self.handlers: - res = handler(req, res, body) - - # Reserialize the response body - if body is not None: - res.body = utils.dumps(body) - - return res - - -class RequestExtensionResource(wsgi.Resource): - - def __init__(self, application): - controller = RequestExtensionController(application) - wsgi.Resource.__init__(self, controller, - serializer=wsgi.ResponseSerializer(), - deserializer=wsgi.RequestDeserializer()) - - def add_handler(self, handler): - self.controller.add_handler(handler) - - def add_pre_handler(self, pre_handler): - self.controller.add_pre_handler(pre_handler) - - -def make_ext(elem): - elem.set('name') - elem.set('namespace') - elem.set('alias') - elem.set('updated') - - desc = xmlutil.SubTemplateElement(elem, 'description') - desc.text = 'description' - - xmlutil.make_links(elem, 'links') - - -ext_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class ExtensionTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('extension', selector='extension') - make_ext(root) - return xmlutil.MasterTemplate(root, 1, nsmap=ext_nsmap) - - -class ExtensionsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('extensions') - elem = xmlutil.SubTemplateElement(root, 'extension', - selector='extensions') - make_ext(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=ext_nsmap) - - -class ExtensionsResource(wsgi.Resource): - - def __init__(self, extension_manager): - self.extension_manager = extension_manager - super(ExtensionsResource, self).__init__(None) - - def _translate(self, ext): - ext_data = {} - ext_data['name'] = ext.name - ext_data['alias'] = ext.alias - ext_data['description'] = ext.__doc__ - ext_data['namespace'] = ext.namespace - ext_data['updated'] = ext.updated - ext_data['links'] = [] # TODO(dprince): implement extension links - return ext_data - - @wsgi.serializers(xml=ExtensionsTemplate) - def index(self, req): - extensions = [] - for _alias, ext in self.extension_manager.extensions.iteritems(): - extensions.append(self._translate(ext)) - return dict(extensions=extensions) - - @wsgi.serializers(xml=ExtensionTemplate) - def show(self, req, id): - try: - # NOTE(dprince): the extensions alias is used as the 'id' for show - ext = self.extension_manager.extensions[id] - except KeyError: - raise webob.exc.HTTPNotFound() - - return dict(extension=self._translate(ext)) - - def delete(self, req, id): - raise webob.exc.HTTPNotFound() - - def create(self, req): - raise webob.exc.HTTPNotFound() - - -class ExtensionMiddleware(base_wsgi.Middleware): - """Extensions middleware for WSGI.""" - @classmethod - def factory(cls, global_config, **local_config): - """Paste factory.""" - def _factory(app): - return cls(app, **local_config) - return _factory - - def _action_ext_resources(self, application, ext_mgr, mapper): - """Return a dict of ActionExtensionResource-s by collection.""" - action_resources = {} - for action in ext_mgr.get_actions(): - if not action.collection in action_resources.keys(): - resource = ActionExtensionResource(application) - mapper.connect("/:(project_id)/%s/:(id)/action.:(format)" % - action.collection, - action='action', - controller=resource, - conditions=dict(method=['POST'])) - mapper.connect("/:(project_id)/%s/:(id)/action" % - action.collection, - action='action', - controller=resource, - conditions=dict(method=['POST'])) - action_resources[action.collection] = resource - - return action_resources - - def _request_ext_resources(self, application, ext_mgr, mapper): - """Returns a dict of RequestExtensionResource-s by collection.""" - request_ext_resources = {} - for req_ext in ext_mgr.get_request_extensions(): - if not req_ext.key in request_ext_resources.keys(): - resource = RequestExtensionResource(application) - mapper.connect(req_ext.url_route + '.:(format)', - action='process', - controller=resource, - conditions=req_ext.conditions) - - mapper.connect(req_ext.url_route, - action='process', - controller=resource, - conditions=req_ext.conditions) - request_ext_resources[req_ext.key] = resource - - return request_ext_resources - - def __init__(self, application, ext_mgr=None): - - if ext_mgr is None: - ext_mgr = ExtensionManager() - self.ext_mgr = ext_mgr - - mapper = nova.api.openstack.v2.ProjectMapper() - - # extended actions - action_resources = self._action_ext_resources(application, ext_mgr, - mapper) - for action in ext_mgr.get_actions(): - LOG.debug(_('Extended action: %s'), action.action_name) - resource = action_resources[action.collection] - resource.add_action(action.action_name, action.handler) - - # extended requests - req_controllers = self._request_ext_resources(application, ext_mgr, - mapper) - for request_ext in ext_mgr.get_request_extensions(): - LOG.debug(_('Extended request: %s'), request_ext.key) - controller = req_controllers[request_ext.key] - if request_ext.handler: - controller.add_handler(request_ext.handler) - if request_ext.pre_handler: - controller.add_pre_handler(request_ext.pre_handler) - - 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): - """Dispatch the request. - - 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): - """Load extensions from the configured extension path. - - See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an - example extension implementation. - - """ - - _ext_mgr = None - - @classmethod - def reset(cls): - cls._ext_mgr = None - - def __new__(cls): - if cls._ext_mgr is None: - LOG.audit(_('Initializing extension manager.')) - - cls._ext_mgr = super(ExtensionManager, cls).__new__(cls) - - cls._ext_mgr.extensions = {} - cls._ext_mgr._load_extensions() - - return cls._ext_mgr - - def register(self, ext): - # Do nothing if the extension doesn't check out - if not self._check_extension(ext): - return - - alias = ext.alias - LOG.audit(_('Loaded extension: %s'), alias) - - if alias in self.extensions: - raise exception.Error("Found duplicate extension: %s" % alias) - self.extensions[alias] = ext - - def get_resources(self): - """Returns a list of ResourceExtension objects.""" - - resources = [] - resources.append(ResourceExtension('extensions', - ExtensionsResource(self))) - - for ext in self.extensions.values(): - 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 ext in self.extensions.values(): - try: - actions.extend(ext.get_actions()) - except AttributeError: - # NOTE(dprince): Extension aren't required to have action - # extensions - pass - return actions - - def get_request_extensions(self): - """Returns a list of RequestExtension objects.""" - request_exts = [] - for ext in self.extensions.values(): - try: - request_exts.extend(ext.get_request_extensions()) - except AttributeError: - # NOTE(dprince): Extension aren't required to have request - # extensions - pass - return request_exts - - def _check_extension(self, extension): - """Checks for required methods in extension objects.""" - try: - LOG.debug(_('Ext name: %s'), extension.name) - LOG.debug(_('Ext alias: %s'), extension.alias) - LOG.debug(_('Ext description: %s'), - ' '.join(extension.__doc__.strip().split())) - LOG.debug(_('Ext namespace: %s'), extension.namespace) - LOG.debug(_('Ext updated: %s'), extension.updated) - LOG.debug(_('Ext admin_only: %s'), extension.admin_only) - except AttributeError as ex: - LOG.exception(_("Exception loading extension: %s"), unicode(ex)) - return False - - # Don't load admin api extensions if the admin api isn't enabled - if not FLAGS.allow_admin_api and extension.admin_only: - return False - - return True - - def load_extension(self, ext_factory): - """Execute an extension factory. - - Loads an extension. The 'ext_factory' is the name of a - callable that will be imported and called with one - argument--the extension manager. The factory callable is - expected to call the register() method at least once. - """ - - LOG.debug(_("Loading extension %s"), ext_factory) - - # Load the factory - - factory = utils.import_class(ext_factory) - - # Call it - LOG.debug(_("Calling extension factory %s"), ext_factory) - factory(self) - - def _load_extensions(self): - """Load extensions specified on the command line.""" - - extensions = list(FLAGS.osapi_extension) - - for ext_factory in extensions: - try: - self.load_extension(ext_factory) - except Exception as exc: - LOG.warn(_('Failed to load extension %(ext_factory)s: ' - '%(exc)s') % locals()) - - -class RequestExtension(object): - """Extend requests and responses of core nova OpenStack API resources. - - Provide a way to add data to responses and handle custom request data - that is sent to core nova OpenStack API controllers. - - """ - def __init__(self, method, url_route, handler=None, pre_handler=None): - self.url_route = url_route - self.handler = handler - self.conditions = dict(method=[method]) - self.key = "%s-%s" % (method, url_route) - self.pre_handler = pre_handler - - -class ActionExtension(object): - """Add custom actions to core nova OpenStack API resources.""" - - def __init__(self, collection, action_name, handler): - self.collection = collection - self.action_name = action_name - self.handler = handler - - -class ResourceExtension(object): - """Add top level resources to the OpenStack API in nova.""" - - def __init__(self, collection, controller, parent=None, - collection_actions=None, member_actions=None, - deserializer=None, serializer=None): - if not collection_actions: - collection_actions = {} - if not member_actions: - member_actions = {} - self.collection = collection - self.controller = controller - self.parent = parent - self.collection_actions = collection_actions - self.member_actions = member_actions - self.deserializer = deserializer - self.serializer = serializer - - -class ExtensionsXMLSerializer(xmlutil.XMLTemplateSerializer): - def index(self): - return ExtensionsTemplate() - - def show(self): - return ExtensionTemplate() - - -def require_admin(f): - @functools.wraps(f) - def wraps(self, req, *args, **kwargs): - if 'nova.context' in req.environ and\ - req.environ['nova.context'].is_admin: - return f(self, req, *args, **kwargs) - else: - raise exception.AdminRequired() - return wraps - - -def wrap_errors(fn): - """Ensure errors are not passed along.""" - def wrapped(*args): - try: - return fn(*args) - except Exception, e: - raise webob.exc.HTTPInternalServerError() - return wrapped diff --git a/nova/api/openstack/v2/flavors.py b/nova/api/openstack/v2/flavors.py deleted file mode 100644 index 21c2a8120..000000000 --- a/nova/api/openstack/v2/flavors.py +++ /dev/null @@ -1,112 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import webob - -from nova.api.openstack.v2.views import flavors as flavors_view -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova.compute import instance_types -from nova import exception - - -def make_flavor(elem, detailed=False): - elem.set('name') - elem.set('id') - if detailed: - elem.set('ram') - elem.set('disk') - - for attr in ("vcpus", "swap", "rxtx_factor"): - elem.set(attr, xmlutil.EmptyStringSelector(attr)) - - xmlutil.make_links(elem, 'links') - - -flavor_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class FlavorTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('flavor', selector='flavor') - make_flavor(root, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) - - -class MinimalFlavorsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('flavors') - elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors') - make_flavor(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) - - -class FlavorsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('flavors') - elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors') - make_flavor(elem, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=flavor_nsmap) - - -class Controller(wsgi.Controller): - """Flavor controller for the OpenStack API.""" - - _view_builder_class = flavors_view.ViewBuilder - - @wsgi.serializers(xml=MinimalFlavorsTemplate) - def index(self, req): - """Return all flavors in brief.""" - flavors = self._get_flavors(req) - return self._view_builder.index(req, flavors) - - @wsgi.serializers(xml=FlavorsTemplate) - def detail(self, req): - """Return all flavors in detail.""" - flavors = self._get_flavors(req) - return self._view_builder.detail(req, flavors) - - @wsgi.serializers(xml=FlavorTemplate) - def show(self, req, id): - """Return data about the given flavor id.""" - try: - flavor = instance_types.get_instance_type_by_flavor_id(id) - except exception.NotFound: - raise webob.exc.HTTPNotFound() - - return self._view_builder.show(req, flavor) - - def _get_flavors(self, req): - """Helper function that returns a list of flavor dicts.""" - filters = {} - if 'minRam' in req.params: - try: - filters['min_memory_mb'] = int(req.params['minRam']) - except ValueError: - pass # ignore bogus values per spec - - if 'minDisk' in req.params: - try: - filters['min_local_gb'] = int(req.params['minDisk']) - except ValueError: - pass # ignore bogus values per spec - - return instance_types.get_all_types(filters=filters) - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/image_metadata.py b/nova/api/openstack/v2/image_metadata.py deleted file mode 100644 index 1e29d23ce..000000000 --- a/nova/api/openstack/v2/image_metadata.py +++ /dev/null @@ -1,118 +0,0 @@ -# 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.api.openstack import common -from nova.api.openstack import wsgi -from nova import exception -from nova import flags -from nova import image - - -FLAGS = flags.FLAGS - - -class Controller(object): - """The image metadata API controller for the Openstack API""" - - def __init__(self): - self.image_service = image.get_default_image_service() - - def _get_image(self, context, image_id): - try: - return self.image_service.show(context, image_id) - except exception.NotFound: - msg = _("Image not found.") - raise exc.HTTPNotFound(explanation=msg) - - @wsgi.serializers(xml=common.MetadataTemplate) - def index(self, req, image_id): - """Returns the list of metadata for a given instance""" - context = req.environ['nova.context'] - metadata = self._get_image(context, image_id)['properties'] - return dict(metadata=metadata) - - @wsgi.serializers(xml=common.MetaItemTemplate) - def show(self, req, image_id, id): - context = req.environ['nova.context'] - metadata = self._get_image(context, image_id)['properties'] - if id in metadata: - return {'meta': {id: metadata[id]}} - else: - raise exc.HTTPNotFound() - - @wsgi.serializers(xml=common.MetadataTemplate) - @wsgi.deserializers(xml=common.MetadataDeserializer) - def create(self, req, image_id, body): - context = req.environ['nova.context'] - image = self._get_image(context, image_id) - if 'metadata' in body: - for key, value in body['metadata'].iteritems(): - image['properties'][key] = value - common.check_img_metadata_quota_limit(context, image['properties']) - self.image_service.update(context, image_id, image, None) - return dict(metadata=image['properties']) - - @wsgi.serializers(xml=common.MetaItemTemplate) - @wsgi.deserializers(xml=common.MetaItemDeserializer) - def update(self, req, image_id, id, body): - context = req.environ['nova.context'] - - try: - meta = body['meta'] - except KeyError: - expl = _('Incorrect request body format') - raise exc.HTTPBadRequest(explanation=expl) - - if not id in meta: - expl = _('Request body and URI mismatch') - raise exc.HTTPBadRequest(explanation=expl) - if len(meta) > 1: - expl = _('Request body contains too many items') - raise exc.HTTPBadRequest(explanation=expl) - - image = self._get_image(context, image_id) - image['properties'][id] = meta[id] - common.check_img_metadata_quota_limit(context, image['properties']) - self.image_service.update(context, image_id, image, None) - return dict(meta=meta) - - @wsgi.serializers(xml=common.MetadataTemplate) - @wsgi.deserializers(xml=common.MetadataDeserializer) - def update_all(self, req, image_id, body): - context = req.environ['nova.context'] - image = self._get_image(context, image_id) - metadata = body.get('metadata', {}) - common.check_img_metadata_quota_limit(context, metadata) - image['properties'] = metadata - self.image_service.update(context, image_id, image, None) - return dict(metadata=metadata) - - @wsgi.response(204) - def delete(self, req, image_id, id): - context = req.environ['nova.context'] - image = self._get_image(context, image_id) - if not id in image['properties']: - msg = _("Invalid metadata key") - raise exc.HTTPNotFound(explanation=msg) - image['properties'].pop(id) - self.image_service.update(context, image_id, image, None) - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/images.py b/nova/api/openstack/v2/images.py deleted file mode 100644 index 96a2275e6..000000000 --- a/nova/api/openstack/v2/images.py +++ /dev/null @@ -1,195 +0,0 @@ -# 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 webob.exc - -from nova.api.openstack import common -from nova.api.openstack.v2.views import images as views_images -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova import exception -from nova import flags -import nova.image -from nova import log - - -LOG = log.getLogger('nova.api.openstack.v2.images') -FLAGS = flags.FLAGS - -SUPPORTED_FILTERS = { - 'name': 'name', - 'status': 'status', - 'changes-since': 'changes-since', - 'server': 'property-instance_ref', - 'type': 'property-image_type', - 'minRam': 'min_ram', - 'minDisk': 'min_disk', -} - - -def make_image(elem, detailed=False): - elem.set('name') - elem.set('id') - - if detailed: - elem.set('updated') - elem.set('created') - elem.set('status') - elem.set('progress') - elem.set('minRam') - elem.set('minDisk') - - server = xmlutil.SubTemplateElement(elem, 'server', selector='server') - server.set('id') - xmlutil.make_links(server, 'links') - - elem.append(common.MetadataTemplate()) - - xmlutil.make_links(elem, 'links') - - -image_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class ImageTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('image', selector='image') - make_image(root, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) - - -class MinimalImagesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('images') - elem = xmlutil.SubTemplateElement(root, 'image', selector='images') - make_image(elem) - xmlutil.make_links(root, 'images_links') - return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) - - -class ImagesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('images') - elem = xmlutil.SubTemplateElement(root, 'image', selector='images') - make_image(elem, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=image_nsmap) - - -class Controller(wsgi.Controller): - """Base controller for retrieving/displaying images.""" - - _view_builder_class = views_images.ViewBuilder - - def __init__(self, image_service=None, compute_service=None, **kwargs): - """Initialize new `ImageController`. - - :param compute_service: `nova.compute.api:API` - :param image_service: `nova.image.glance:GlancemageService` - - """ - super(Controller, self).__init__(**kwargs) - self._compute_service = compute_service or compute.API() - self._image_service = image_service or \ - nova.image.get_default_image_service() - - def _get_filters(self, req): - """ - Return a dictionary of query param filters from the request - - :param req: the Request object coming from the wsgi layer - :retval a dict of key/value filters - """ - filters = {} - for param in req.params: - if param in SUPPORTED_FILTERS or param.startswith('property-'): - # map filter name or carry through if property-* - filter_name = SUPPORTED_FILTERS.get(param, param) - filters[filter_name] = req.params.get(param) - return filters - - @wsgi.serializers(xml=ImageTemplate) - def show(self, req, id): - """Return detailed information about a specific image. - - :param req: `wsgi.Request` object - :param id: Image identifier - """ - context = req.environ['nova.context'] - - try: - image = self._image_service.show(context, id) - except (exception.NotFound, exception.InvalidImageRef): - explanation = _("Image not found.") - raise webob.exc.HTTPNotFound(explanation=explanation) - - return self._view_builder.show(req, image) - - def delete(self, req, id): - """Delete an image, if allowed. - - :param req: `wsgi.Request` object - :param id: Image identifier (integer) - """ - context = req.environ['nova.context'] - try: - self._image_service.delete(context, id) - except exception.ImageNotFound: - explanation = _("Image not found.") - raise webob.exc.HTTPNotFound(explanation=explanation) - return webob.exc.HTTPNoContent() - - @wsgi.serializers(xml=MinimalImagesTemplate) - def index(self, req): - """Return an index listing of images available to the request. - - :param req: `wsgi.Request` object - - """ - context = req.environ['nova.context'] - filters = self._get_filters(req) - params = req.GET.copy() - page_params = common.get_pagination_params(req) - for key, val in page_params.iteritems(): - params[key] = val - - images = self._image_service.index(context, filters=filters, - **page_params) - return self._view_builder.index(req, images) - - @wsgi.serializers(xml=ImagesTemplate) - def detail(self, req): - """Return a detailed index listing of images available to the request. - - :param req: `wsgi.Request` object. - - """ - context = req.environ['nova.context'] - filters = self._get_filters(req) - params = req.GET.copy() - page_params = common.get_pagination_params(req) - for key, val in page_params.iteritems(): - params[key] = val - images = self._image_service.detail(context, filters=filters, - **page_params) - - return self._view_builder.detail(req, images) - - def create(self, *args, **kwargs): - raise webob.exc.HTTPMethodNotAllowed() - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/ips.py b/nova/api/openstack/v2/ips.py deleted file mode 100644 index 3dc9cf928..000000000 --- a/nova/api/openstack/v2/ips.py +++ /dev/null @@ -1,105 +0,0 @@ -# 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 - -import nova -from nova.api.openstack import common -from nova.api.openstack.v2.views import addresses as view_addresses -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import log as logging -from nova import flags - - -LOG = logging.getLogger('nova.api.openstack.v2.ips') -FLAGS = flags.FLAGS - - -def make_network(elem): - elem.set('id', 0) - - ip = xmlutil.SubTemplateElement(elem, 'ip', selector=1) - ip.set('version') - ip.set('addr') - - -network_nsmap = {None: xmlutil.XMLNS_V11} - - -class NetworkTemplate(xmlutil.TemplateBuilder): - def construct(self): - sel = xmlutil.Selector(xmlutil.get_items, 0) - root = xmlutil.TemplateElement('network', selector=sel) - make_network(root) - return xmlutil.MasterTemplate(root, 1, nsmap=network_nsmap) - - -class AddressesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('addresses', selector='addresses') - elem = xmlutil.SubTemplateElement(root, 'network', - selector=xmlutil.get_items) - make_network(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=network_nsmap) - - -class Controller(wsgi.Controller): - """The servers addresses API controller for the Openstack API.""" - - _view_builder_class = view_addresses.ViewBuilder - - def __init__(self, **kwargs): - super(Controller, self).__init__(**kwargs) - self._compute_api = nova.compute.API() - - def _get_instance(self, context, server_id): - try: - instance = self._compute_api.get(context, server_id) - except nova.exception.NotFound: - msg = _("Instance does not exist") - raise exc.HTTPNotFound(explanation=msg) - return instance - - def create(self, req, server_id, body): - raise exc.HTTPNotImplemented() - - def delete(self, req, server_id, id): - raise exc.HTTPNotImplemented() - - @wsgi.serializers(xml=AddressesTemplate) - def index(self, req, server_id): - context = req.environ["nova.context"] - instance = self._get_instance(context, server_id) - networks = common.get_networks_for_instance(context, instance) - return self._view_builder.index(networks) - - @wsgi.serializers(xml=NetworkTemplate) - def show(self, req, server_id, id): - context = req.environ["nova.context"] - instance = self._get_instance(context, server_id) - networks = common.get_networks_for_instance(context, instance) - - if id not in networks: - msg = _("Instance is not a member of specified network") - raise exc.HTTPNotFound(explanation=msg) - - return self._view_builder.show(networks[id], id) - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/limits.py b/nova/api/openstack/v2/limits.py deleted file mode 100644 index e1f5ff836..000000000 --- a/nova/api/openstack/v2/limits.py +++ /dev/null @@ -1,477 +0,0 @@ -# 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. - -""" -Module dedicated functions/classes dealing with rate limiting requests. -""" - -from collections import defaultdict -import copy -import httplib -import json -import math -import re -import time - -from webob.dec import wsgify -import webob.exc - -from nova.api.openstack.v2.views import limits as limits_views -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import quota -from nova import utils -from nova import wsgi as base_wsgi - - -# Convenience constants for the limits dictionary passed to Limiter(). -PER_SECOND = 1 -PER_MINUTE = 60 -PER_HOUR = 60 * 60 -PER_DAY = 60 * 60 * 24 - - -limits_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class LimitsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('limits', selector='limits') - - rates = xmlutil.SubTemplateElement(root, 'rates') - rate = xmlutil.SubTemplateElement(rates, 'rate', selector='rate') - rate.set('uri', 'uri') - rate.set('regex', 'regex') - limit = xmlutil.SubTemplateElement(rate, 'limit', selector='limit') - limit.set('value', 'value') - limit.set('verb', 'verb') - limit.set('remaining', 'remaining') - limit.set('unit', 'unit') - limit.set('next-available', 'next-available') - - absolute = xmlutil.SubTemplateElement(root, 'absolute', - selector='absolute') - limit = xmlutil.SubTemplateElement(absolute, 'limit', - selector=xmlutil.get_items) - limit.set('name', 0) - limit.set('value', 1) - - return xmlutil.MasterTemplate(root, 1, nsmap=limits_nsmap) - - -class LimitsController(object): - """ - Controller for accessing limits in the OpenStack API. - """ - - @wsgi.serializers(xml=LimitsTemplate) - def index(self, req): - """ - Return all global and rate limit information. - """ - context = req.environ['nova.context'] - abs_limits = quota.get_project_quotas(context, context.project_id) - rate_limits = req.environ.get("nova.limits", []) - - builder = self._get_view_builder(req) - return builder.build(rate_limits, abs_limits) - - def _get_view_builder(self, req): - return limits_views.ViewBuilder() - - -def create_resource(): - return wsgi.Resource(LimitsController()) - - -class Limit(object): - """ - Stores information about a limit for HTTP requests. - """ - - UNITS = { - 1: "SECOND", - 60: "MINUTE", - 60 * 60: "HOUR", - 60 * 60 * 24: "DAY", - } - - UNIT_MAP = dict([(v, k) for k, v in UNITS.items()]) - - 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.unit_string = self.display_unit().lower() - 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 = 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): - """ - 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) - 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(base_wsgi.Middleware): - """ - Rate-limits requests passing through this middleware. All limit information - is stored in memory for this implementation. - """ - - def __init__(self, application, limits=None, limiter=None, **kwargs): - """ - Initialize new `RateLimitingMiddleware`, which wraps the given WSGI - application and sets up the given limits. - - @param application: WSGI application to wrap - @param limits: String describing limits - @param limiter: String identifying class for representing limits - - Other parameters are passed to the constructor for the limiter. - """ - base_wsgi.Middleware.__init__(self, application) - - # Select the limiter class - if limiter is None: - limiter = Limiter - else: - limiter = utils.import_class(limiter) - - # Parse the limits, if any are provided - if limits is not None: - limits = limiter.parse_limits(limits) - - self._limiter = limiter(limits or DEFAULT_LIMITS, **kwargs) - - @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 - 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) - - if delay: - msg = _("This request was rate-limited.") - retry = time.time() + delay - return wsgi.OverLimitFault(msg, error, 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, **kwargs): - """ - Initialize the new `Limiter`. - - @param limits: List of `Limit` objects - """ - self.limits = copy.deepcopy(limits) - self.levels = defaultdict(lambda: copy.deepcopy(limits)) - - # Pick up any per-user limit information - for key, value in kwargs.items(): - if key.startswith('user:'): - username = key[5:] - self.levels[username] = self.parse_limits(value) - - 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. - - @return: Tuple of delay (in seconds) and error message (or None, None) - """ - delays = [] - - for limit in self.levels[username]: - delay = limit(verb, url) - if delay: - delays.append((delay, limit.error_message)) - - if delays: - delays.sort() - return delays[0] - - return None, None - - # Note: This method gets called before the class is instantiated, - # so this must be either a static method or a class method. It is - # used to develop a list of limits to feed to the constructor. We - # put this in the class so that subclasses can override the - # default limit parsing. - @staticmethod - def parse_limits(limits): - """ - Convert a string into a list of Limit instances. This - implementation expects a semicolon-separated sequence of - parenthesized groups, where each group contains a - comma-separated sequence consisting of HTTP method, - user-readable URI, a URI reg-exp, an integer number of - requests which can be made, and a unit of measure. Valid - values for the latter are "SECOND", "MINUTE", "HOUR", and - "DAY". - - @return: List of Limit instances. - """ - - # Handle empty limit strings - limits = limits.strip() - if not limits: - return [] - - # Split up the limits by semicolon - result = [] - for group in limits.split(';'): - group = group.strip() - if group[:1] != '(' or group[-1:] != ')': - raise ValueError("Limit rules must be surrounded by " - "parentheses") - group = group[1:-1] - - # Extract the Limit arguments - args = [a.strip() for a in group.split(',')] - if len(args) != 5: - raise ValueError("Limit rules must contain the following " - "arguments: verb, uri, regex, value, unit") - - # Pull out the arguments - verb, uri, regex, value, unit = args - - # Upper-case the verb - verb = verb.upper() - - # Convert value--raises ValueError if it's not integer - value = int(value) - - # Convert unit - unit = unit.upper() - if unit not in Limit.UNIT_MAP: - raise ValueError("Invalid units specified") - unit = Limit.UNIT_MAP[unit] - - # Build a limit - result.append(Limit(verb, uri, regex, value, unit)) - - return result - - -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, error = self._limiter.check_for_delay(verb, path, username) - - if delay: - headers = {"X-Wait-Seconds": "%.2f" % delay} - return webob.exc.HTTPForbidden(headers=headers, explanation=error) - 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, None - - return resp.getheader("X-Wait-Seconds"), resp.read() or None - - # Note: This method gets called before the class is instantiated, - # so this must be either a static method or a class method. It is - # used to develop a list of limits to feed to the constructor. - # This implementation returns an empty list, since all limit - # decisions are made by a remote server. - @staticmethod - def parse_limits(limits): - """ - Ignore a limits string--simply doesn't apply for the limit - proxy. - - @return: Empty list. - """ - - return [] diff --git a/nova/api/openstack/v2/ratelimiting/__init__.py b/nova/api/openstack/v2/ratelimiting/__init__.py deleted file mode 100644 index 78dc465a7..000000000 --- a/nova/api/openstack/v2/ratelimiting/__init__.py +++ /dev/null @@ -1,222 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Rate limiting of arbitrary actions.""" - -import httplib -import time -import urllib - -import webob.dec -import webob.exc - -from nova import wsgi -from nova.api.openstack import wsgi as os_wsgi - -# 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 RateLimitingMiddleware(wsgi.Middleware): - """Rate limit incoming requests according to the OpenStack rate limits.""" - - def __init__(self, application, service_host=None): - """Create a rate limiting middleware that wraps the given application. - - By default, rate counters are stored in memory. If service_host is - specified, the middleware instead relies on the ratelimiting.WSGIApp - at the given host+port to keep rate counters. - """ - if not service_host: - #TODO(gundlach): These limits were based on limitations of Cloud - #Servers. We should revisit them in Nova. - self.limiter = Limiter(limits={ - 'DELETE': (100, PER_MINUTE), - 'PUT': (10, PER_MINUTE), - 'POST': (10, PER_MINUTE), - 'POST servers': (50, PER_DAY), - 'GET changes-since': (3, PER_MINUTE), - }) - else: - self.limiter = WSGIAppProxy(service_host) - super(RateLimitingMiddleware, self).__init__(application) - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - """Rate limit the request. - - If the request should be rate limited, return a 413 status with a - Retry-After header giving the time when the request would succeed. - """ - return self.rate_limited_request(req, self.application) - - def rate_limited_request(self, req, application): - """Rate limit the request. - - If the request should be rate limited, return a 413 status with a - Retry-After header giving the time when the request would succeed. - """ - action_name = self.get_action_name(req) - if not action_name: - # Not rate limited - return application - delay = self.get_delay(action_name, - req.environ['nova.context'].user_id) - if delay: - # TODO(gundlach): Get the retry-after format correct. - exc = webob.exc.HTTPRequestEntityTooLarge( - explanation=('Too many requests.'), - headers={'Retry-After': time.time() + delay}) - raise os_wsgi.Fault(exc) - return application - - def get_delay(self, action_name, username): - """Return the delay for the given action and username, or None if - the action would not be rate limited. - """ - if action_name == 'POST servers': - # "POST servers" is a POST, so it counts against "POST" too. - # Attempt the "POST" first, lest we are rate limited by "POST" but - # use up a precious "POST servers" call. - delay = self.limiter.perform("POST", username=username) - if delay: - return delay - return self.limiter.perform(action_name, username=username) - - def get_action_name(self, req): - """Return the action name for this request.""" - if req.method == 'GET' and 'changes-since' in req.GET: - return 'GET changes-since' - if req.method == 'POST' and req.path_info.startswith('/servers'): - return 'POST servers' - if req.method in ['PUT', 'POST', 'DELETE']: - return req.method - return None - - -class Limiter(object): - - """Class providing rate limiting of arbitrary actions.""" - - def __init__(self, limits): - """Create a rate limiter. - - limits: a dict mapping from action name to a tuple. The tuple contains - the number of times the action may be performed, and the time period - (in seconds) during which the number must not be exceeded for this - action. Example: dict(reboot=(10, ratelimiting.PER_MINUTE)) would - allow 10 'reboot' actions per minute. - """ - self.limits = limits - self._levels = {} - - def perform(self, action_name, username='nobody'): - """Attempt to perform an action by the given username. - - action_name: the string name of the action to perform. This must - be a key in the limits dict passed to the ctor. - - username: an optional string name of the user performing the action. - Each user has her own set of rate limiting counters. Defaults to - 'nobody' (so that if you never specify a username when calling - perform(), a single set of counters will be used.) - - Return None if the action may proceed. If the action may not proceed - because it has been rate limited, return the float number of seconds - until the action would succeed. - """ - # Think of rate limiting as a bucket leaking water at 1cc/second. The - # bucket can hold as many ccs as there are seconds in the rate - # limiting period (e.g. 3600 for per-hour ratelimits), and if you can - # perform N actions in that time, each action fills the bucket by - # 1/Nth of its volume. You may only perform an action if the bucket - # would not overflow. - now = time.time() - key = '%s:%s' % (username, action_name) - last_time_performed, water_level = self._levels.get(key, (now, 0)) - # The bucket leaks 1cc/second. - water_level -= (now - last_time_performed) - if water_level < 0: - water_level = 0 - num_allowed_per_period, period_in_secs = self.limits[action_name] - # Fill the bucket by 1/Nth its capacity, and hope it doesn't overflow. - capacity = period_in_secs - new_level = water_level + (capacity * 1.0 / num_allowed_per_period) - if new_level > capacity: - # Delay this many seconds. - return new_level - capacity - self._levels[key] = (now, new_level) - return None - -# If one instance of this WSGIApps is unable to handle your load, put a -# sharding app in front that shards by username to one of many backends. - - -class WSGIApp(object): - - """Application that tracks rate limits in memory. Send requests to it of - this form: - - POST /limiter// - - and receive a 200 OK, or a 403 Forbidden with an X-Wait-Seconds header - containing the number of seconds to wait before the action would succeed. - """ - - def __init__(self, limiter): - """Create the WSGI application using the given Limiter instance.""" - self.limiter = limiter - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - parts = req.path_info.split('/') - # format: /limiter// - if req.method != 'POST': - raise webob.exc.HTTPMethodNotAllowed() - if len(parts) != 4 or parts[1] != 'limiter': - raise webob.exc.HTTPNotFound() - username = parts[2] - action_name = urllib.unquote(parts[3]) - delay = self.limiter.perform(action_name, username) - if delay: - return webob.exc.HTTPForbidden( - headers={'X-Wait-Seconds': "%.2f" % delay}) - else: - # 200 OK - return '' - - -class WSGIAppProxy(object): - - """Limiter lookalike that proxies to a ratelimiting.WSGIApp.""" - - def __init__(self, service_host): - """Creates a proxy pointing to a ratelimiting.WSGIApp at the given - host.""" - self.service_host = service_host - - def perform(self, action, username='nobody'): - conn = httplib.HTTPConnection(self.service_host) - conn.request('POST', '/limiter/%s/%s' % (username, action)) - resp = conn.getresponse() - if resp.status == 200: - # No delay - return None - return float(resp.getheader('X-Wait-Seconds')) diff --git a/nova/api/openstack/v2/schemas/atom-link.rng b/nova/api/openstack/v2/schemas/atom-link.rng deleted file mode 100644 index edba5eee6..000000000 --- a/nova/api/openstack/v2/schemas/atom-link.rng +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - [^:]* - - - - - - .+/.+ - - - - - - [A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})* - - - - - - - - - - - - xml:base - xml:lang - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/atom.rng b/nova/api/openstack/v2/schemas/atom.rng deleted file mode 100644 index c2df4e410..000000000 --- a/nova/api/openstack/v2/schemas/atom.rng +++ /dev/null @@ -1,597 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text - html - - - - - - - - - xhtml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An atom:feed must have an atom:author unless all of its atom:entry children have an atom:author. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An atom:entry must have at least one atom:link element with a rel attribute of 'alternate' or an atom:content. - - - An atom:entry must have an atom:author if its feed does not. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text - html - - - - - - - - - - - - - xhtml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - [^:]* - - - - - - .+/.+ - - - - - - [A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})* - - - - - - - - - - .+@.+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - xml:base - xml:lang - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/addresses.rng b/nova/api/openstack/v2/schemas/v1.1/addresses.rng deleted file mode 100644 index b498e8a63..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/addresses.rng +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/extension.rng b/nova/api/openstack/v2/schemas/v1.1/extension.rng deleted file mode 100644 index 336659755..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/extension.rng +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/extensions.rng b/nova/api/openstack/v2/schemas/v1.1/extensions.rng deleted file mode 100644 index 4d8bff646..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/extensions.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/flavor.rng b/nova/api/openstack/v2/schemas/v1.1/flavor.rng deleted file mode 100644 index 08746ce3d..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/flavor.rng +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/flavors.rng b/nova/api/openstack/v2/schemas/v1.1/flavors.rng deleted file mode 100644 index b7a3acc01..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/flavors.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/flavors_index.rng b/nova/api/openstack/v2/schemas/v1.1/flavors_index.rng deleted file mode 100644 index d1a4fedb1..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/flavors_index.rng +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/image.rng b/nova/api/openstack/v2/schemas/v1.1/image.rng deleted file mode 100644 index 505081fba..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/image.rng +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/images.rng b/nova/api/openstack/v2/schemas/v1.1/images.rng deleted file mode 100644 index 064d4d9cc..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/images.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/images_index.rng b/nova/api/openstack/v2/schemas/v1.1/images_index.rng deleted file mode 100644 index 3db0b2672..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/images_index.rng +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/limits.rng b/nova/api/openstack/v2/schemas/v1.1/limits.rng deleted file mode 100644 index 1af8108ec..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/limits.rng +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/metadata.rng b/nova/api/openstack/v2/schemas/v1.1/metadata.rng deleted file mode 100644 index b2f5d702a..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/metadata.rng +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/server.rng b/nova/api/openstack/v2/schemas/v1.1/server.rng deleted file mode 100644 index 07fa16daa..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/server.rng +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/servers.rng b/nova/api/openstack/v2/schemas/v1.1/servers.rng deleted file mode 100644 index 4e2bb8853..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/servers.rng +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/servers_index.rng b/nova/api/openstack/v2/schemas/v1.1/servers_index.rng deleted file mode 100644 index 023e4b66a..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/servers_index.rng +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/version.rng b/nova/api/openstack/v2/schemas/v1.1/version.rng deleted file mode 100644 index ae76270ba..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/version.rng +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/nova/api/openstack/v2/schemas/v1.1/versions.rng b/nova/api/openstack/v2/schemas/v1.1/versions.rng deleted file mode 100644 index 8b2cc7f71..000000000 --- a/nova/api/openstack/v2/schemas/v1.1/versions.rng +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/nova/api/openstack/v2/server_metadata.py b/nova/api/openstack/v2/server_metadata.py deleted file mode 100644 index 52a90f96e..000000000 --- a/nova/api/openstack/v2/server_metadata.py +++ /dev/null @@ -1,175 +0,0 @@ -# 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.api.openstack import common -from nova.api.openstack import wsgi -from nova import compute -from nova import exception - - -class Controller(object): - """ 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): - try: - server = self.compute_api.get(context, server_id) - meta = self.compute_api.get_instance_metadata(context, server) - except exception.InstanceNotFound: - msg = _('Server does not exist') - raise exc.HTTPNotFound(explanation=msg) - - meta_dict = {} - for key, value in meta.iteritems(): - meta_dict[key] = value - return meta_dict - - @wsgi.serializers(xml=common.MetadataTemplate) - def index(self, req, server_id): - """ Returns the list of metadata for a given instance """ - context = req.environ['nova.context'] - return {'metadata': self._get_metadata(context, server_id)} - - @wsgi.serializers(xml=common.MetadataTemplate) - @wsgi.deserializers(xml=common.MetadataDeserializer) - def create(self, req, server_id, body): - try: - metadata = body['metadata'] - except (KeyError, TypeError): - msg = _("Malformed request body") - raise exc.HTTPBadRequest(explanation=msg) - - context = req.environ['nova.context'] - - new_metadata = self._update_instance_metadata(context, - server_id, - metadata, - delete=False) - - return {'metadata': new_metadata} - - @wsgi.serializers(xml=common.MetaItemTemplate) - @wsgi.deserializers(xml=common.MetaItemDeserializer) - def update(self, req, server_id, id, body): - try: - meta_item = body['meta'] - except (TypeError, KeyError): - expl = _('Malformed request body') - raise exc.HTTPBadRequest(explanation=expl) - - try: - meta_value = meta_item[id] - except (AttributeError, KeyError): - expl = _('Request body and URI mismatch') - raise exc.HTTPBadRequest(explanation=expl) - - if len(meta_item) > 1: - expl = _('Request body contains too many items') - raise exc.HTTPBadRequest(explanation=expl) - - context = req.environ['nova.context'] - self._update_instance_metadata(context, - server_id, - meta_item, - delete=False) - - return {'meta': meta_item} - - @wsgi.serializers(xml=common.MetadataTemplate) - @wsgi.deserializers(xml=common.MetadataDeserializer) - def update_all(self, req, server_id, body): - try: - metadata = body['metadata'] - except (TypeError, KeyError): - expl = _('Malformed request body') - raise exc.HTTPBadRequest(explanation=expl) - - context = req.environ['nova.context'] - new_metadata = self._update_instance_metadata(context, - server_id, - metadata, - delete=True) - - return {'metadata': new_metadata} - - def _update_instance_metadata(self, context, server_id, metadata, - delete=False): - try: - server = self.compute_api.get(context, server_id) - return self.compute_api.update_instance_metadata(context, - server, - metadata, - delete) - - except exception.InstanceNotFound: - msg = _('Server does not exist') - raise exc.HTTPNotFound(explanation=msg) - - except (ValueError, AttributeError): - msg = _("Malformed request body") - raise exc.HTTPBadRequest(explanation=msg) - - except exception.QuotaError as error: - self._handle_quota_error(error) - - @wsgi.serializers(xml=common.MetaItemTemplate) - def show(self, req, server_id, id): - """ Return a single metadata item """ - context = req.environ['nova.context'] - data = self._get_metadata(context, server_id) - - try: - return {'meta': {id: data[id]}} - except KeyError: - msg = _("Metadata item was not found") - raise exc.HTTPNotFound(explanation=msg) - - @wsgi.response(204) - def delete(self, req, server_id, id): - """ Deletes an existing metadata """ - context = req.environ['nova.context'] - - metadata = self._get_metadata(context, server_id) - - try: - meta_value = metadata[id] - except KeyError: - msg = _("Metadata item was not found") - raise exc.HTTPNotFound(explanation=msg) - - try: - server = self.compute_api.get(context, server_id) - self.compute_api.delete_instance_metadata(context, server, id) - except exception.InstanceNotFound: - msg = _('Server does not exist') - raise exc.HTTPNotFound(explanation=msg) - - def _handle_quota_error(self, error): - """Reraise quota errors as api-specific http exceptions.""" - if error.code == "MetadataLimitExceeded": - raise exc.HTTPRequestEntityTooLarge(explanation=error.message, - headers={'Retry-After': 0}) - raise error - - -def create_resource(): - return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/v2/servers.py b/nova/api/openstack/v2/servers.py deleted file mode 100644 index c2655a73e..000000000 --- a/nova/api/openstack/v2/servers.py +++ /dev/null @@ -1,1123 +0,0 @@ -# Copyright 2010 OpenStack LLC. -# Copyright 2011 Piston Cloud Computing, Inc -# 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 base64 -import os -from xml.dom import minidom - -from webob import exc -import webob - -from nova.api.openstack import common -from nova.api.openstack.v2 import ips -from nova.api.openstack.v2.views import servers as views_servers -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import compute -from nova.compute import instance_types -from nova import exception -from nova import flags -from nova import log as logging -from nova.rpc import common as rpc_common -from nova.scheduler import api as scheduler_api -from nova import utils - - -LOG = logging.getLogger('nova.api.openstack.v2.servers') -FLAGS = flags.FLAGS - - -class SecurityGroupsTemplateElement(xmlutil.TemplateElement): - def will_render(self, datum): - return 'security_groups' in datum - - -def make_fault(elem): - fault = xmlutil.SubTemplateElement(elem, 'fault', selector='fault') - fault.set('code') - fault.set('created') - msg = xmlutil.SubTemplateElement(fault, 'message') - msg.text = 'message' - det = xmlutil.SubTemplateElement(fault, 'details') - det.text = 'details' - - -def make_server(elem, detailed=False): - elem.set('name') - elem.set('id') - - if detailed: - elem.set('userId', 'user_id') - elem.set('tenantId', 'tenant_id') - elem.set('updated') - elem.set('created') - elem.set('hostId') - elem.set('accessIPv4') - elem.set('accessIPv6') - elem.set('status') - elem.set('progress') - - # Attach image node - image = xmlutil.SubTemplateElement(elem, 'image', selector='image') - image.set('id') - xmlutil.make_links(image, 'links') - - # Attach flavor node - flavor = xmlutil.SubTemplateElement(elem, 'flavor', selector='flavor') - flavor.set('id') - xmlutil.make_links(flavor, 'links') - - # Attach fault node - make_fault(elem) - - # Attach metadata node - elem.append(common.MetadataTemplate()) - - # Attach addresses node - elem.append(ips.AddressesTemplate()) - - # Attach security groups node - secgrps = SecurityGroupsTemplateElement('security_groups') - elem.append(secgrps) - secgrp = xmlutil.SubTemplateElement(secgrps, 'security_group', - selector='security_groups') - secgrp.set('name') - - xmlutil.make_links(elem, 'links') - - -server_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class ServerTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('server', selector='server') - make_server(root, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) - - -class MinimalServersTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('servers') - elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') - make_server(elem) - xmlutil.make_links(root, 'servers_links') - return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) - - -class ServersTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('servers') - elem = xmlutil.SubTemplateElement(root, 'server', selector='servers') - make_server(elem, detailed=True) - return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap) - - -class ServerAdminPassTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('server') - root.set('adminPass') - return xmlutil.SlaveTemplate(root, 1, nsmap=server_nsmap) - - -def FullServerTemplate(): - master = ServerTemplate() - master.attach(ServerAdminPassTemplate()) - return master - - -class CommonDeserializer(wsgi.MetadataXMLDeserializer): - """ - Common deserializer to handle xml-formatted server create - requests. - - Handles standard server attributes as well as optional metadata - and personality attributes - """ - - metadata_deserializer = common.MetadataXMLDeserializer() - - def _extract_personality(self, server_node): - """Marshal the personality attribute of a parsed request""" - node = self.find_first_child_named(server_node, "personality") - if node is not None: - personality = [] - for file_node in self.find_children_named(node, "file"): - item = {} - if file_node.hasAttribute("path"): - item["path"] = file_node.getAttribute("path") - item["contents"] = self.extract_text(file_node) - personality.append(item) - return personality - else: - return None - - def _extract_server(self, node): - """Marshal the server attribute of a parsed request""" - server = {} - server_node = self.find_first_child_named(node, 'server') - - attributes = ["name", "imageRef", "flavorRef", "adminPass", - "accessIPv4", "accessIPv6"] - for attr in attributes: - if server_node.getAttribute(attr): - server[attr] = server_node.getAttribute(attr) - - metadata_node = self.find_first_child_named(server_node, "metadata") - if metadata_node is not None: - server["metadata"] = self.extract_metadata(metadata_node) - - personality = self._extract_personality(server_node) - if personality is not None: - server["personality"] = personality - - networks = self._extract_networks(server_node) - if networks is not None: - server["networks"] = networks - - security_groups = self._extract_security_groups(server_node) - if security_groups is not None: - server["security_groups"] = security_groups - - auto_disk_config = server_node.getAttribute('auto_disk_config') - if auto_disk_config: - server['auto_disk_config'] = utils.bool_from_str(auto_disk_config) - - return server - - def _extract_networks(self, server_node): - """Marshal the networks attribute of a parsed request""" - node = self.find_first_child_named(server_node, "networks") - if node is not None: - networks = [] - for network_node in self.find_children_named(node, - "network"): - item = {} - if network_node.hasAttribute("uuid"): - item["uuid"] = network_node.getAttribute("uuid") - if network_node.hasAttribute("fixed_ip"): - item["fixed_ip"] = network_node.getAttribute("fixed_ip") - networks.append(item) - return networks - else: - return None - - def _extract_security_groups(self, server_node): - """Marshal the security_groups attribute of a parsed request""" - node = self.find_first_child_named(server_node, "security_groups") - if node is not None: - security_groups = [] - for sg_node in self.find_children_named(node, "security_group"): - item = {} - name_node = self.find_first_child_named(sg_node, "name") - if name_node: - item["name"] = self.extract_text(name_node) - security_groups.append(item) - return security_groups - else: - return None - - -class ActionDeserializer(CommonDeserializer): - """ - Deserializer to handle xml-formatted server action requests. - - Handles standard server attributes as well as optional metadata - and personality attributes - """ - - def default(self, string): - dom = minidom.parseString(string) - action_node = dom.childNodes[0] - action_name = action_node.tagName - - action_deserializer = { - 'createImage': self._action_create_image, - 'changePassword': self._action_change_password, - 'reboot': self._action_reboot, - 'rebuild': self._action_rebuild, - 'resize': self._action_resize, - 'confirmResize': self._action_confirm_resize, - 'revertResize': self._action_revert_resize, - }.get(action_name, super(ActionDeserializer, self).default) - - action_data = action_deserializer(action_node) - - return {'body': {action_name: action_data}} - - def _action_create_image(self, node): - return self._deserialize_image_action(node, ('name',)) - - def _action_change_password(self, node): - if not node.hasAttribute("adminPass"): - raise AttributeError("No adminPass was specified in request") - return {"adminPass": node.getAttribute("adminPass")} - - def _action_reboot(self, node): - if not node.hasAttribute("type"): - raise AttributeError("No reboot type was specified in request") - return {"type": node.getAttribute("type")} - - def _action_rebuild(self, node): - rebuild = {} - if node.hasAttribute("name"): - rebuild['name'] = node.getAttribute("name") - - metadata_node = self.find_first_child_named(node, "metadata") - if metadata_node is not None: - rebuild["metadata"] = self.extract_metadata(metadata_node) - - personality = self._extract_personality(node) - if personality is not None: - rebuild["personality"] = personality - - if not node.hasAttribute("imageRef"): - raise AttributeError("No imageRef was specified in request") - rebuild["imageRef"] = node.getAttribute("imageRef") - - return rebuild - - def _action_resize(self, node): - if not node.hasAttribute("flavorRef"): - raise AttributeError("No flavorRef was specified in request") - return {"flavorRef": node.getAttribute("flavorRef")} - - def _action_confirm_resize(self, node): - return None - - def _action_revert_resize(self, node): - return None - - def _deserialize_image_action(self, node, allowed_attributes): - data = {} - for attribute in allowed_attributes: - value = node.getAttribute(attribute) - if value: - data[attribute] = value - metadata_node = self.find_first_child_named(node, 'metadata') - if metadata_node is not None: - metadata = self.metadata_deserializer.extract_metadata( - metadata_node) - data['metadata'] = metadata - return data - - -class CreateDeserializer(CommonDeserializer): - """ - Deserializer to handle xml-formatted server create requests. - - Handles standard server attributes as well as optional metadata - and personality attributes - """ - - def default(self, string): - """Deserialize an xml-formatted server create request""" - dom = minidom.parseString(string) - server = self._extract_server(dom) - return {'body': {'server': server}} - - -class Controller(wsgi.Controller): - """ The Server API base controller class for the OpenStack API """ - - _view_builder_class = views_servers.ViewBuilder - - @staticmethod - def _add_location(robj): - # Just in case... - if 'server' not in robj.obj: - return robj - - link = filter(lambda l: l['rel'] == 'self', - robj.obj['server']['links']) - if link: - robj['Location'] = link[0]['href'] - - # Convenience return - return robj - - def __init__(self, **kwargs): - super(Controller, self).__init__(**kwargs) - self.compute_api = compute.API() - - @wsgi.serializers(xml=MinimalServersTemplate) - def index(self, req): - """ Returns a list of server names and ids for a given user """ - try: - servers = self._get_servers(req, is_detail=False) - except exception.Invalid as err: - raise exc.HTTPBadRequest(explanation=str(err)) - except exception.NotFound: - raise exc.HTTPNotFound() - return servers - - @wsgi.serializers(xml=ServersTemplate) - def detail(self, req): - """ Returns a list of server details for a given user """ - try: - servers = self._get_servers(req, is_detail=True) - except exception.Invalid as err: - raise exc.HTTPBadRequest(explanation=str(err)) - except exception.NotFound as err: - raise exc.HTTPNotFound() - return servers - - def _get_block_device_mapping(self, data): - """Get block_device_mapping from 'server' dictionary. - Overridden by volumes controller. - """ - return None - - def _add_instance_faults(self, ctxt, instances): - faults = self.compute_api.get_instance_faults(ctxt, instances) - if faults is not None: - for instance in instances: - faults_list = faults.get(instance['uuid'], []) - try: - instance['fault'] = faults_list[0] - except IndexError: - pass - - return instances - - def _get_servers(self, req, is_detail): - """Returns a list of servers, taking into account any search - options specified. - """ - - search_opts = {} - search_opts.update(req.str_GET) - - context = req.environ['nova.context'] - remove_invalid_options(context, search_opts, - self._get_server_search_options()) - - # Convert local_zone_only into a boolean - search_opts['local_zone_only'] = utils.bool_from_str( - search_opts.get('local_zone_only', False)) - - # If search by 'status', we need to convert it to 'vm_state' - # to pass on to child zones. - if 'status' in search_opts: - status = search_opts['status'] - state = common.vm_state_from_status(status) - if state is None: - reason = _('Invalid server status: %(status)s') % locals() - raise exception.InvalidInput(reason=reason) - search_opts['vm_state'] = state - - if 'changes-since' in search_opts: - try: - parsed = utils.parse_isotime(search_opts['changes-since']) - except ValueError: - msg = _('Invalid changes-since value') - raise exc.HTTPBadRequest(explanation=msg) - search_opts['changes-since'] = parsed - - # By default, compute's get_all() will return deleted instances. - # If an admin hasn't specified a 'deleted' search option, we need - # to filter out deleted instances by setting the filter ourselves. - # ... Unless 'changes-since' is specified, because 'changes-since' - # should return recently deleted images according to the API spec. - - if 'deleted' not in search_opts: - if 'changes-since' not in search_opts: - # No 'changes-since', so we only want non-deleted servers - search_opts['deleted'] = False - - instance_list = self.compute_api.get_all(context, - search_opts=search_opts) - - limited_list = self._limit_items(instance_list, req) - if is_detail: - self._add_instance_faults(context, limited_list) - return self._view_builder.detail(req, limited_list) - else: - return self._view_builder.index(req, limited_list) - - def _get_server(self, context, instance_uuid): - """Utility function for looking up an instance by uuid""" - try: - return self.compute_api.routing_get(context, instance_uuid) - except exception.NotFound: - raise exc.HTTPNotFound() - - def _handle_quota_error(self, error): - """ - Reraise quota errors as api-specific http exceptions - """ - - code_mappings = { - "OnsetFileLimitExceeded": - _("Personality file limit exceeded"), - "OnsetFilePathLimitExceeded": - _("Personality file path too long"), - "OnsetFileContentLimitExceeded": - _("Personality file content too long"), - - # NOTE(bcwaldon): expose the message generated below in order - # to better explain how the quota was exceeded - "InstanceLimitExceeded": error.message, - } - - expl = code_mappings.get(error.code) - if expl: - raise exc.HTTPRequestEntityTooLarge(explanation=expl, - headers={'Retry-After': 0}) - # if the original error is okay, just reraise it - raise error - - def _validate_server_name(self, value): - if not isinstance(value, basestring): - msg = _("Server name is not a string or unicode") - raise exc.HTTPBadRequest(explanation=msg) - - if value.strip() == '': - msg = _("Server name is an empty string") - raise exc.HTTPBadRequest(explanation=msg) - - def _get_injected_files(self, personality): - """ - Create a list of injected files from the personality attribute - - At this time, injected_files must be formatted as a list of - (file_path, file_content) pairs for compatibility with the - underlying compute service. - """ - injected_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: - expl = _('Bad personality format') - raise exc.HTTPBadRequest(explanation=expl) - try: - contents = base64.b64decode(contents) - except TypeError: - expl = _('Personality content for %s cannot be decoded') % path - raise exc.HTTPBadRequest(explanation=expl) - injected_files.append((path, contents)) - return injected_files - - def _get_requested_networks(self, requested_networks): - """ - Create a list of requested networks from the networks attribute - """ - networks = [] - for network in requested_networks: - try: - network_uuid = network['uuid'] - - if not utils.is_uuid_like(network_uuid): - msg = _("Bad networks format: network uuid is not in" - " proper format (%s)") % network_uuid - raise exc.HTTPBadRequest(explanation=msg) - - #fixed IP address is optional - #if the fixed IP address is not provided then - #it will use one of the available IP address from the network - address = network.get('fixed_ip', None) - if address is not None and not utils.is_valid_ipv4(address): - msg = _("Invalid fixed IP address (%s)") % address - raise exc.HTTPBadRequest(explanation=msg) - # check if the network id is already present in the list, - # we don't want duplicate networks to be passed - # at the boot time - for id, ip in networks: - if id == network_uuid: - expl = _("Duplicate networks (%s) are not allowed")\ - % network_uuid - raise exc.HTTPBadRequest(explanation=expl) - - networks.append((network_uuid, address)) - except KeyError as key: - expl = _('Bad network format: missing %s') % key - raise exc.HTTPBadRequest(explanation=expl) - except TypeError: - expl = _('Bad networks format') - raise exc.HTTPBadRequest(explanation=expl) - - return networks - - def _validate_user_data(self, user_data): - """Check if the user_data is encoded properly""" - if not user_data: - return - try: - user_data = base64.b64decode(user_data) - except TypeError: - expl = _('Userdata content cannot be decoded') - raise exc.HTTPBadRequest(explanation=expl) - - @wsgi.serializers(xml=ServerTemplate) - @exception.novaclient_converter - @scheduler_api.redirect_handler - def show(self, req, id): - """ Returns server details by server id """ - try: - context = req.environ['nova.context'] - instance = self.compute_api.routing_get(context, id) - self._add_instance_faults(context, [instance]) - return self._view_builder.show(req, instance) - except exception.NotFound: - raise exc.HTTPNotFound() - - @wsgi.response(202) - @wsgi.serializers(xml=FullServerTemplate) - @wsgi.deserializers(xml=CreateDeserializer) - def create(self, req, body): - """ Creates a new server for a given user """ - if not body: - raise exc.HTTPUnprocessableEntity() - - if not 'server' in body: - raise exc.HTTPUnprocessableEntity() - - body['server']['key_name'] = self._get_key_name(req, body) - - context = req.environ['nova.context'] - server_dict = body['server'] - password = self._get_server_admin_password(server_dict) - - if not 'name' in server_dict: - msg = _("Server name is not defined") - raise exc.HTTPBadRequest(explanation=msg) - - name = server_dict['name'] - self._validate_server_name(name) - name = name.strip() - - image_href = self._image_ref_from_req_data(body) - - # If the image href was generated by nova api, strip image_href - # down to an id and use the default glance connection params - if str(image_href).startswith(req.application_url): - image_href = image_href.split('/').pop() - - personality = server_dict.get('personality') - config_drive = server_dict.get('config_drive') - - injected_files = [] - if personality: - injected_files = self._get_injected_files(personality) - - sg_names = [] - security_groups = server_dict.get('security_groups') - if security_groups is not None: - sg_names = [sg['name'] for sg in security_groups if sg.get('name')] - if not sg_names: - sg_names.append('default') - - sg_names = list(set(sg_names)) - - requested_networks = server_dict.get('networks') - if requested_networks is not None: - requested_networks = self._get_requested_networks( - requested_networks) - - try: - flavor_id = self._flavor_id_from_req_data(body) - except ValueError as error: - msg = _("Invalid flavorRef provided.") - raise exc.HTTPBadRequest(explanation=msg) - - zone_blob = server_dict.get('blob') - - # optional openstack extensions: - key_name = server_dict.get('key_name') - user_data = server_dict.get('user_data') - self._validate_user_data(user_data) - - availability_zone = server_dict.get('availability_zone') - name = server_dict['name'] - self._validate_server_name(name) - name = name.strip() - - block_device_mapping = self._get_block_device_mapping(server_dict) - - # Only allow admins to specify their own reservation_ids - # This is really meant to allow zones to work. - reservation_id = server_dict.get('reservation_id') - if all([reservation_id is not None, - reservation_id != '', - not context.is_admin]): - reservation_id = None - - ret_resv_id = server_dict.get('return_reservation_id', False) - - min_count = server_dict.get('min_count') - max_count = server_dict.get('max_count') - # min_count and max_count are optional. If they exist, they come - # in as strings. We want to default 'min_count' to 1, and default - # 'max_count' to be 'min_count'. - min_count = int(min_count) if min_count else 1 - max_count = int(max_count) if max_count else min_count - if min_count > max_count: - min_count = max_count - - auto_disk_config = server_dict.get('auto_disk_config') - - try: - inst_type = \ - instance_types.get_instance_type_by_flavor_id(flavor_id) - - (instances, resv_id) = self.compute_api.create(context, - inst_type, - image_href, - display_name=name, - display_description=name, - key_name=key_name, - metadata=server_dict.get('metadata', {}), - access_ip_v4=server_dict.get('accessIPv4'), - access_ip_v6=server_dict.get('accessIPv6'), - injected_files=injected_files, - admin_password=password, - zone_blob=zone_blob, - reservation_id=reservation_id, - min_count=min_count, - max_count=max_count, - requested_networks=requested_networks, - security_group=sg_names, - user_data=user_data, - availability_zone=availability_zone, - config_drive=config_drive, - block_device_mapping=block_device_mapping, - auto_disk_config=auto_disk_config) - except exception.QuotaError as error: - self._handle_quota_error(error) - except exception.InstanceTypeMemoryTooSmall as error: - raise exc.HTTPBadRequest(explanation=unicode(error)) - except exception.InstanceTypeDiskTooSmall as error: - raise exc.HTTPBadRequest(explanation=unicode(error)) - except exception.ImageNotFound as error: - msg = _("Can not find requested image") - raise exc.HTTPBadRequest(explanation=msg) - except exception.FlavorNotFound as error: - msg = _("Invalid flavorRef provided.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.KeypairNotFound as error: - msg = _("Invalid key_name provided.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.SecurityGroupNotFound as error: - raise exc.HTTPBadRequest(explanation=unicode(error)) - except rpc_common.RemoteError as err: - msg = "%(err_type)s: %(err_msg)s" % \ - {'err_type': err.exc_type, 'err_msg': err.value} - raise exc.HTTPBadRequest(explanation=msg) - # Let the caller deal with unhandled exceptions. - - # If the caller wanted a reservation_id, return it - if ret_resv_id: - return {'reservation_id': resv_id} - - server = self._view_builder.create(req, instances[0]) - - if '_is_precooked' in server['server'].keys(): - del server['server']['_is_precooked'] - else: - server['server']['adminPass'] = password - - robj = wsgi.ResponseObject(server) - - return self._add_location(robj) - - def _delete(self, context, id): - instance = self._get_server(context, id) - if FLAGS.reclaim_instance_interval: - self.compute_api.soft_delete(context, instance) - else: - self.compute_api.delete(context, instance) - - @wsgi.serializers(xml=ServerTemplate) - @scheduler_api.redirect_handler - def update(self, req, id, body): - """Update server then pass on to version-specific controller""" - if len(req.body) == 0: - raise exc.HTTPUnprocessableEntity() - - if not body: - raise exc.HTTPUnprocessableEntity() - - ctxt = req.environ['nova.context'] - update_dict = {} - - if 'name' in body['server']: - name = body['server']['name'] - self._validate_server_name(name) - update_dict['display_name'] = name.strip() - - if 'accessIPv4' in body['server']: - access_ipv4 = body['server']['accessIPv4'] - update_dict['access_ip_v4'] = access_ipv4.strip() - - if 'accessIPv6' in body['server']: - access_ipv6 = body['server']['accessIPv6'] - update_dict['access_ip_v6'] = access_ipv6.strip() - - if 'auto_disk_config' in body['server']: - auto_disk_config = utils.bool_from_str( - body['server']['auto_disk_config']) - update_dict['auto_disk_config'] = auto_disk_config - - instance = self.compute_api.routing_get(ctxt, id) - - try: - self.compute_api.update(ctxt, instance, **update_dict) - except exception.NotFound: - raise exc.HTTPNotFound() - - instance.update(update_dict) - - self._add_instance_faults(ctxt, [instance]) - return self._view_builder.show(req, instance) - - @wsgi.response(202) - @wsgi.serializers(xml=FullServerTemplate) - @wsgi.deserializers(xml=ActionDeserializer) - @exception.novaclient_converter - @scheduler_api.redirect_handler - def action(self, req, id, body): - """Multi-purpose method used to take actions on a server""" - _actions = { - 'changePassword': self._action_change_password, - 'reboot': self._action_reboot, - 'resize': self._action_resize, - 'confirmResize': self._action_confirm_resize, - 'revertResize': self._action_revert_resize, - 'rebuild': self._action_rebuild, - 'createImage': self._action_create_image, - } - - for key in body: - if key in _actions: - return _actions[key](body, req, id) - else: - msg = _("There is no such server action: %s") % (key,) - raise exc.HTTPBadRequest(explanation=msg) - - msg = _("Invalid request body") - raise exc.HTTPBadRequest(explanation=msg) - - def _action_confirm_resize(self, input_dict, req, id): - context = req.environ['nova.context'] - instance = self._get_server(context, id) - try: - self.compute_api.confirm_resize(context, instance) - except exception.MigrationNotFound: - msg = _("Instance has not been resized.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'confirmResize') - except Exception, e: - LOG.exception(_("Error in confirm-resize %s"), e) - raise exc.HTTPBadRequest() - return exc.HTTPNoContent() - - def _action_revert_resize(self, input_dict, req, id): - context = req.environ['nova.context'] - instance = self._get_server(context, id) - try: - self.compute_api.revert_resize(context, instance) - except exception.MigrationNotFound: - msg = _("Instance has not been resized.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'revertResize') - except Exception, e: - LOG.exception(_("Error in revert-resize %s"), e) - raise exc.HTTPBadRequest() - return webob.Response(status_int=202) - - def _action_reboot(self, input_dict, req, id): - if 'reboot' in input_dict and 'type' in input_dict['reboot']: - valid_reboot_types = ['HARD', 'SOFT'] - reboot_type = input_dict['reboot']['type'].upper() - if not valid_reboot_types.count(reboot_type): - msg = _("Argument 'type' for reboot is not HARD or SOFT") - LOG.exception(msg) - raise exc.HTTPBadRequest(explanation=msg) - else: - msg = _("Missing argument 'type' for reboot") - LOG.exception(msg) - raise exc.HTTPBadRequest(explanation=msg) - - context = req.environ['nova.context'] - instance = self._get_server(context, id) - - try: - self.compute_api.reboot(context, instance, reboot_type) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'reboot') - except Exception, e: - LOG.exception(_("Error in reboot %s"), e) - raise exc.HTTPUnprocessableEntity() - return webob.Response(status_int=202) - - def _resize(self, req, instance_id, flavor_id): - """Begin the resize process with given instance/flavor.""" - context = req.environ["nova.context"] - instance = self._get_server(context, instance_id) - - try: - self.compute_api.resize(context, instance, flavor_id) - except exception.FlavorNotFound: - msg = _("Unable to locate requested flavor.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.CannotResizeToSameSize: - msg = _("Resize requires a change in size.") - raise exc.HTTPBadRequest(explanation=msg) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'resize') - - return webob.Response(status_int=202) - - @wsgi.response(204) - @exception.novaclient_converter - @scheduler_api.redirect_handler - def delete(self, req, id): - """ Destroys a server """ - try: - self._delete(req.environ['nova.context'], id) - except exception.NotFound: - raise exc.HTTPNotFound() - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'delete') - - def _get_key_name(self, req, body): - if 'server' in body: - try: - return body['server'].get('key_name') - except AttributeError: - msg = _("Malformed server entity") - raise exc.HTTPBadRequest(explanation=msg) - - def _image_ref_from_req_data(self, data): - try: - return data['server']['imageRef'] - except (TypeError, KeyError): - msg = _("Missing imageRef attribute") - raise exc.HTTPBadRequest(explanation=msg) - - def _flavor_id_from_req_data(self, data): - try: - flavor_ref = data['server']['flavorRef'] - except (TypeError, KeyError): - msg = _("Missing flavorRef attribute") - raise exc.HTTPBadRequest(explanation=msg) - - return common.get_id_from_href(flavor_ref) - - 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']): - msg = _("No adminPass was specified") - raise exc.HTTPBadRequest(explanation=msg) - password = input_dict['changePassword']['adminPass'] - if not isinstance(password, basestring) or password == '': - msg = _("Invalid adminPass") - raise exc.HTTPBadRequest(explanation=msg) - server = self._get_server(context, id) - self.compute_api.set_admin_password(context, server, password) - return webob.Response(status_int=202) - - def _limit_items(self, items, req): - return common.limited_by_marker(items, req) - - def _validate_metadata(self, metadata): - """Ensure that we can work with the metadata given.""" - try: - metadata.iteritems() - except AttributeError as ex: - msg = _("Unable to parse metadata key/value pairs.") - LOG.debug(msg) - raise exc.HTTPBadRequest(explanation=msg) - - def _action_resize(self, input_dict, req, id): - """ Resizes a given instance to the flavor size requested """ - try: - flavor_ref = input_dict["resize"]["flavorRef"] - if not flavor_ref: - msg = _("Resize request has invalid 'flavorRef' attribute.") - raise exc.HTTPBadRequest(explanation=msg) - except (KeyError, TypeError): - msg = _("Resize requests require 'flavorRef' attribute.") - raise exc.HTTPBadRequest(explanation=msg) - - return self._resize(req, id, flavor_ref) - - def _action_rebuild(self, info, request, instance_id): - """Rebuild an instance with the given attributes""" - try: - body = info['rebuild'] - except (KeyError, TypeError): - raise exc.HTTPBadRequest(_("Invalid request body")) - - try: - image_href = body["imageRef"] - except (KeyError, TypeError): - msg = _("Could not parse imageRef from request.") - raise exc.HTTPBadRequest(explanation=msg) - - try: - password = body['adminPass'] - except (KeyError, TypeError): - password = utils.generate_password(FLAGS.password_length) - - context = request.environ['nova.context'] - instance = self._get_server(context, instance_id) - - attr_map = { - 'personality': 'files_to_inject', - 'name': 'display_name', - 'accessIPv4': 'access_ip_v4', - 'accessIPv6': 'access_ip_v6', - 'metadata': 'metadata', - } - - kwargs = {} - - for request_attribute, instance_attribute in attr_map.items(): - try: - kwargs[instance_attribute] = body[request_attribute] - except (KeyError, TypeError): - pass - - self._validate_metadata(kwargs.get('metadata', {})) - - if 'files_to_inject' in kwargs: - personality = kwargs['files_to_inject'] - kwargs['files_to_inject'] = self._get_injected_files(personality) - - try: - self.compute_api.rebuild(context, - instance, - image_href, - password, - **kwargs) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'rebuild') - except exception.InstanceNotFound: - msg = _("Instance could not be found") - raise exc.HTTPNotFound(explanation=msg) - - instance = self._get_server(context, instance_id) - - self._add_instance_faults(context, [instance]) - view = self._view_builder.show(request, instance) - - # Add on the adminPass attribute since the view doesn't do it - view['server']['adminPass'] = password - - robj = wsgi.ResponseObject(view) - return self._add_location(robj) - - @common.check_snapshots_enabled - def _action_create_image(self, input_dict, req, instance_id): - """Snapshot a server instance.""" - context = req.environ['nova.context'] - entity = input_dict.get("createImage", {}) - - try: - image_name = entity["name"] - - except KeyError: - msg = _("createImage entity requires name attribute") - raise exc.HTTPBadRequest(explanation=msg) - - except TypeError: - msg = _("Malformed createImage entity") - raise exc.HTTPBadRequest(explanation=msg) - - # preserve link to server in image properties - server_ref = os.path.join(req.application_url, 'servers', instance_id) - props = {'instance_ref': server_ref} - - metadata = entity.get('metadata', {}) - common.check_img_metadata_quota_limit(context, metadata) - try: - props.update(metadata) - except ValueError: - msg = _("Invalid metadata") - raise exc.HTTPBadRequest(explanation=msg) - - instance = self._get_server(context, instance_id) - - try: - image = self.compute_api.snapshot(context, - instance, - image_name, - extra_properties=props) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'createImage') - - # build location of newly-created image entity - image_id = str(image['id']) - image_ref = os.path.join(req.application_url, - context.project_id, - 'images', - image_id) - - resp = webob.Response(status_int=202) - resp.headers['Location'] = image_ref - return resp - - def _get_server_admin_password(self, server): - """ Determine the admin password for a server on creation """ - password = server.get('adminPass') - - if password is None: - return utils.generate_password(FLAGS.password_length) - if not isinstance(password, basestring) or password == '': - msg = _("Invalid adminPass") - raise exc.HTTPBadRequest(explanation=msg) - return password - - def _get_server_search_options(self): - """Return server search options allowed by non-admin""" - return ('reservation_id', 'name', 'local_zone_only', - 'status', 'image', 'flavor', 'changes-since') - - -def create_resource(): - return wsgi.Resource(Controller()) - - -def remove_invalid_options(context, search_options, allowed_search_options): - """Remove search options that are not valid for non-admin API/context""" - if FLAGS.allow_admin_api and context.is_admin: - # Allow all options - return - # Otherwise, strip out all unknown options - unknown_options = [opt for opt in search_options - if opt not in allowed_search_options] - unk_opt_str = ", ".join(unknown_options) - log_msg = _("Removing options '%(unk_opt_str)s' from query") % locals() - LOG.debug(log_msg) - for opt in unknown_options: - search_options.pop(opt, None) diff --git a/nova/api/openstack/v2/urlmap.py b/nova/api/openstack/v2/urlmap.py deleted file mode 100644 index bae69198e..000000000 --- a/nova/api/openstack/v2/urlmap.py +++ /dev/null @@ -1,297 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import paste.urlmap -import re -import urllib2 - -from nova import log as logging -from nova.api.openstack import wsgi - - -_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"' -_option_header_piece_re = re.compile(r';\s*([^\s;=]+|%s)\s*' - r'(?:=\s*([^;]+|%s))?\s*' % - (_quoted_string_re, _quoted_string_re)) - -LOG = logging.getLogger('nova.api.openstack.v2.map') - - -def unquote_header_value(value): - """Unquotes a header value. - This does not use the real unquoting but what browsers are actually - using for quoting. - - :param value: the header value to unquote. - """ - if value and value[0] == value[-1] == '"': - # this is not the real unquoting, but fixing this so that the - # RFC is met will result in bugs with internet explorer and - # probably some other browsers as well. IE for example is - # uploading files with "C:\foo\bar.txt" as filename - value = value[1:-1] - return value - - -def parse_list_header(value): - """Parse lists as described by RFC 2068 Section 2. - - In particular, parse comma-separated lists where the elements of - the list may include quoted-strings. A quoted-string could - contain a comma. A non-quoted string could have quotes in the - middle. Quotes are removed automatically after parsing. - - The return value is a standard :class:`list`: - - >>> parse_list_header('token, "quoted value"') - ['token', 'quoted value'] - - :param value: a string with a list header. - :return: :class:`list` - """ - result = [] - for item in urllib2.parse_http_list(value): - if item[:1] == item[-1:] == '"': - item = unquote_header_value(item[1:-1]) - result.append(item) - return result - - -def parse_options_header(value): - """Parse a ``Content-Type`` like header into a tuple with the content - type and the options: - - >>> parse_options_header('Content-Type: text/html; mimetype=text/html') - ('Content-Type:', {'mimetype': 'text/html'}) - - :param value: the header to parse. - :return: (str, options) - """ - def _tokenize(string): - for match in _option_header_piece_re.finditer(string): - key, value = match.groups() - key = unquote_header_value(key) - if value is not None: - value = unquote_header_value(value) - yield key, value - - if not value: - return '', {} - - parts = _tokenize(';' + value) - name = parts.next()[0] - extra = dict(parts) - return name, extra - - -class Accept(object): - def __init__(self, value): - self._content_types = [parse_options_header(v) for v in - parse_list_header(value)] - - def best_match(self, supported_content_types): - # FIXME: Should we have a more sophisticated matching algorithm that - # takes into account the version as well? - best_quality = -1 - best_content_type = None - best_params = {} - best_match = '*/*' - - for content_type in supported_content_types: - for content_mask, params in self._content_types: - try: - quality = float(params.get('q', 1)) - except ValueError: - continue - - if quality < best_quality: - continue - elif best_quality == quality: - if best_match.count('*') <= content_mask.count('*'): - continue - - if self._match_mask(content_mask, content_type): - best_quality = quality - best_content_type = content_type - best_params = params - best_match = content_mask - - return best_content_type, best_params - - def content_type_params(self, best_content_type): - """Find parameters in Accept header for given content type.""" - for content_type, params in self._content_types: - if best_content_type == content_type: - return params - - return {} - - def _match_mask(self, mask, content_type): - if '*' not in mask: - return content_type == mask - if mask == '*/*': - return True - mask_major = mask[:-2] - content_type_major = content_type.split('/', 1)[0] - return content_type_major == mask_major - - -def urlmap_factory(loader, global_conf, **local_conf): - if 'not_found_app' in local_conf: - not_found_app = local_conf.pop('not_found_app') - else: - not_found_app = global_conf.get('not_found_app') - if not_found_app: - not_found_app = loader.get_app(not_found_app, global_conf=global_conf) - urlmap = URLMap(not_found_app=not_found_app) - for path, app_name in local_conf.items(): - path = paste.urlmap.parse_path_expression(path) - app = loader.get_app(app_name, global_conf=global_conf) - urlmap[path] = app - return urlmap - - -class URLMap(paste.urlmap.URLMap): - def _match(self, host, port, path_info): - """Find longest match for a given URL path.""" - for (domain, app_url), app in self.applications: - if domain and domain != host and domain != host + ':' + port: - continue - if (path_info == app_url - or path_info.startswith(app_url + '/')): - return app, app_url - - return None, None - - def _set_script_name(self, app, app_url): - def wrap(environ, start_response): - environ['SCRIPT_NAME'] += app_url - return app(environ, start_response) - - return wrap - - def _munge_path(self, app, path_info, app_url): - def wrap(environ, start_response): - environ['SCRIPT_NAME'] += app_url - environ['PATH_INFO'] = path_info[len(app_url):] - return app(environ, start_response) - - return wrap - - def _path_strategy(self, host, port, path_info): - """Check path suffix for MIME type and path prefix for API version.""" - mime_type = app = app_url = None - - parts = path_info.rsplit('.', 1) - if len(parts) > 1: - possible_type = 'application/' + parts[1] - if possible_type in wsgi.SUPPORTED_CONTENT_TYPES: - mime_type = possible_type - - parts = path_info.split('/') - if len(parts) > 1: - possible_app, possible_app_url = self._match(host, port, path_info) - # Don't use prefix if it ends up matching default - if possible_app and possible_app_url: - app_url = possible_app_url - app = self._munge_path(possible_app, path_info, app_url) - - return mime_type, app, app_url - - def _content_type_strategy(self, host, port, environ): - """Check Content-Type header for API version.""" - app = None - params = parse_options_header(environ.get('CONTENT_TYPE', ''))[1] - if 'version' in params: - app, app_url = self._match(host, port, '/v' + params['version']) - if app: - app = self._set_script_name(app, app_url) - - return app - - def _accept_strategy(self, host, port, environ, supported_content_types): - """Check Accept header for best matching MIME type and API version.""" - accept = Accept(environ.get('HTTP_ACCEPT', '')) - - app = None - - # Find the best match in the Accept header - mime_type, params = accept.best_match(supported_content_types) - if 'version' in params: - app, app_url = self._match(host, port, '/v' + params['version']) - if app: - app = self._set_script_name(app, app_url) - - return mime_type, app - - def __call__(self, environ, start_response): - host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower() - if ':' in host: - host, port = host.split(':', 1) - else: - if environ['wsgi.url_scheme'] == 'http': - port = '80' - else: - port = '443' - - path_info = environ['PATH_INFO'] - path_info = self.normalize_url(path_info, False)[1] - - # The MIME type for the response is determined in one of two ways: - # 1) URL path suffix (eg /servers/detail.json) - # 2) Accept header (eg application/json;q=0.8, application/xml;q=0.2) - - # The API version is determined in one of three ways: - # 1) URL path prefix (eg /v1.1/tenant/servers/detail) - # 2) Content-Type header (eg application/json;version=1.1) - # 3) Accept header (eg application/json;q=0.8;version=1.1) - - supported_content_types = list(wsgi.SUPPORTED_CONTENT_TYPES) - - mime_type, app, app_url = self._path_strategy(host, port, path_info) - - # Accept application/atom+xml for the index query of each API - # version mount point as well as the root index - if (app_url and app_url + '/' == path_info) or path_info == '/': - supported_content_types.append('application/atom+xml') - - if not app: - app = self._content_type_strategy(host, port, environ) - - if not mime_type or not app: - possible_mime_type, possible_app = self._accept_strategy( - host, port, environ, supported_content_types) - if possible_mime_type and not mime_type: - mime_type = possible_mime_type - if possible_app and not app: - app = possible_app - - if not mime_type: - mime_type = 'application/json' - - if not app: - # Didn't match a particular version, probably matches default - app, app_url = self._match(host, port, path_info) - if app: - app = self._munge_path(app, path_info, app_url) - - if app: - environ['nova.best_content_type'] = mime_type - return app(environ, start_response) - - environ['paste.urlmap_object'] = self - return self.not_found_application(environ, start_response) diff --git a/nova/api/openstack/v2/versions.py b/nova/api/openstack/v2/versions.py deleted file mode 100644 index 45b84f7c2..000000000 --- a/nova/api/openstack/v2/versions.py +++ /dev/null @@ -1,236 +0,0 @@ -# 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 datetime import datetime - -from lxml import etree - -from nova.api.openstack.v2.views import versions as views_versions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil - - -VERSIONS = { - "v2.0": { - "id": "v2.0", - "status": "CURRENT", - "updated": "2011-01-21T11:33:21Z", - "links": [ - { - "rel": "describedby", - "type": "application/pdf", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/cs-devguide-20110125.pdf", - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl", - }, - ], - "media-types": [ - { - "base": "application/xml", - "type": "application/vnd.openstack.compute+xml;version=2", - }, - { - "base": "application/json", - "type": "application/vnd.openstack.compute+json;version=2", - } - ], - } -} - - -class MediaTypesTemplateElement(xmlutil.TemplateElement): - def will_render(self, datum): - return 'media-types' in datum - - -def make_version(elem): - elem.set('id') - elem.set('status') - elem.set('updated') - - mts = MediaTypesTemplateElement('media-types') - elem.append(mts) - - mt = xmlutil.SubTemplateElement(mts, 'media-type', selector='media-types') - mt.set('base') - mt.set('type') - - xmlutil.make_links(elem, 'links') - - -version_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM} - - -class VersionTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('version', selector='version') - make_version(root) - return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) - - -class VersionsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('versions') - elem = xmlutil.SubTemplateElement(root, 'version', selector='versions') - make_version(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) - - -class ChoicesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('choices') - elem = xmlutil.SubTemplateElement(root, 'version', selector='choices') - make_version(elem) - return xmlutil.MasterTemplate(root, 1, nsmap=version_nsmap) - - -class AtomSerializer(wsgi.XMLDictSerializer): - - NSMAP = {None: xmlutil.XMLNS_ATOM} - - def __init__(self, metadata=None, xmlns=None): - self.metadata = metadata or {} - if not xmlns: - self.xmlns = wsgi.XMLNS_ATOM - else: - self.xmlns = xmlns - - def _get_most_recent_update(self, versions): - recent = None - for version in versions: - updated = datetime.strptime(version['updated'], - '%Y-%m-%dT%H:%M:%SZ') - if not recent: - recent = updated - elif updated > recent: - recent = updated - - return recent.strftime('%Y-%m-%dT%H:%M:%SZ') - - def _get_base_url(self, link_href): - # Make sure no trailing / - link_href = link_href.rstrip('/') - return link_href.rsplit('/', 1)[0] + '/' - - def _create_feed(self, versions, feed_title, feed_id): - feed = etree.Element('feed', nsmap=self.NSMAP) - title = etree.SubElement(feed, 'title') - title.set('type', 'text') - title.text = feed_title - - # Set this updated to the most recently updated version - recent = self._get_most_recent_update(versions) - etree.SubElement(feed, 'updated').text = recent - - etree.SubElement(feed, 'id').text = feed_id - - link = etree.SubElement(feed, 'link') - link.set('rel', 'self') - link.set('href', feed_id) - - author = etree.SubElement(feed, 'author') - etree.SubElement(author, 'name').text = 'Rackspace' - etree.SubElement(author, 'uri').text = 'http://www.rackspace.com/' - - for version in versions: - feed.append(self._create_version_entry(version)) - - return feed - - def _create_version_entry(self, version): - entry = etree.Element('entry') - etree.SubElement(entry, 'id').text = version['links'][0]['href'] - title = etree.SubElement(entry, 'title') - title.set('type', 'text') - title.text = 'Version %s' % version['id'] - etree.SubElement(entry, 'updated').text = version['updated'] - - for link in version['links']: - link_elem = etree.SubElement(entry, 'link') - link_elem.set('rel', link['rel']) - link_elem.set('href', link['href']) - if 'type' in link: - link_elem.set('type', link['type']) - - content = etree.SubElement(entry, 'content') - content.set('type', 'text') - content.text = 'Version %s %s (%s)' % (version['id'], - version['status'], - version['updated']) - return entry - - -class VersionsAtomSerializer(AtomSerializer): - def default(self, data): - versions = data['versions'] - feed_id = self._get_base_url(versions[0]['links'][0]['href']) - feed = self._create_feed(versions, 'Available API Versions', feed_id) - return self._to_xml(feed) - - -class VersionAtomSerializer(AtomSerializer): - def default(self, data): - version = data['version'] - feed_id = version['links'][0]['href'] - feed = self._create_feed([version], 'About This Version', feed_id) - return self._to_xml(feed) - - -class Versions(wsgi.Resource): - def __init__(self): - super(Versions, self).__init__(None) - - @wsgi.serializers(xml=VersionsTemplate, - atom=VersionsAtomSerializer) - def index(self, req): - """Return all versions.""" - builder = views_versions.get_view_builder(req) - return builder.build_versions(VERSIONS) - - @wsgi.serializers(xml=ChoicesTemplate) - @wsgi.response(300) - def multi(self, req): - """Return multiple choices.""" - builder = views_versions.get_view_builder(req) - return builder.build_choices(VERSIONS, req) - - def get_action_args(self, request_environment): - """Parse dictionary created by routes library.""" - args = {} - if request_environment['PATH_INFO'] == '/': - args['action'] = 'index' - else: - args['action'] = 'multi' - - return args - - -class VersionV2(object): - @wsgi.serializers(xml=VersionTemplate, - atom=VersionAtomSerializer) - def show(self, req): - builder = views_versions.get_view_builder(req) - return builder.build_version(VERSIONS['v2.0']) - - -def create_resource(): - return wsgi.Resource(VersionV2()) diff --git a/nova/api/openstack/v2/views/__init__.py b/nova/api/openstack/v2/views/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nova/api/openstack/v2/views/addresses.py b/nova/api/openstack/v2/views/addresses.py deleted file mode 100644 index 6f518b11a..000000000 --- a/nova/api/openstack/v2/views/addresses.py +++ /dev/null @@ -1,52 +0,0 @@ -# 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 itertools - -from nova.api.openstack import common -from nova import flags -from nova import log as logging - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.api.openstack.v2.views.addresses') - - -class ViewBuilder(common.ViewBuilder): - """Models server addresses as a dictionary.""" - - _collection_name = "addresses" - - def basic(self, ip): - """Return a dictionary describing an IP address.""" - return { - "version": ip["version"], - "addr": ip["addr"], - } - - def show(self, network, label): - """Returns a dictionary describing a network.""" - all_ips = itertools.chain(network["ips"], network["floating_ips"]) - return {label: [self.basic(ip) for ip in all_ips]} - - def index(self, networks): - """Return a dictionary describing a list of networks.""" - addresses = {} - for label, network in networks.items(): - network_dict = self.show(network, label) - addresses[label] = network_dict[label] - return dict(addresses=addresses) diff --git a/nova/api/openstack/v2/views/flavors.py b/nova/api/openstack/v2/views/flavors.py deleted file mode 100644 index 64284e406..000000000 --- a/nova/api/openstack/v2/views/flavors.py +++ /dev/null @@ -1,62 +0,0 @@ -# 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 - - -class ViewBuilder(common.ViewBuilder): - - _collection_name = "flavors" - - def basic(self, request, flavor): - return { - "flavor": { - "id": flavor["flavorid"], - "name": flavor["name"], - "links": self._get_links(request, flavor["flavorid"]), - }, - } - - def show(self, request, flavor): - return { - "flavor": { - "id": flavor["flavorid"], - "name": flavor["name"], - "ram": flavor["memory_mb"], - "disk": flavor["local_gb"], - "vcpus": flavor.get("vcpus") or "", - "swap": flavor.get("swap") or "", - "rxtx_factor": flavor.get("rxtx_factor") or "", - "links": self._get_links(request, flavor["flavorid"]), - }, - } - - def index(self, request, flavors): - """Return the 'index' view of flavors.""" - def _get_flavors(request, flavors): - for _, flavor in flavors.iteritems(): - yield self.basic(request, flavor)["flavor"] - - return dict(flavors=list(_get_flavors(request, flavors))) - - def detail(self, request, flavors): - """Return the 'detail' view of flavors.""" - def _get_flavors(request, flavors): - for _, flavor in flavors.iteritems(): - yield self.show(request, flavor)["flavor"] - - return dict(flavors=list(_get_flavors(request, flavors))) diff --git a/nova/api/openstack/v2/views/images.py b/nova/api/openstack/v2/views/images.py deleted file mode 100644 index c4cfe8031..000000000 --- a/nova/api/openstack/v2/views/images.py +++ /dev/null @@ -1,139 +0,0 @@ -# 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 os.path - -from nova.api.openstack import common -from nova import utils - - -class ViewBuilder(common.ViewBuilder): - - _collection_name = "images" - - def basic(self, request, image): - """Return a dictionary with basic image attributes.""" - return { - "image": { - "id": image.get("id"), - "name": image.get("name"), - "links": self._get_links(request, image["id"]), - }, - } - - def show(self, request, image): - """Return a dictionary with image details.""" - image_dict = { - "id": image.get("id"), - "name": image.get("name"), - "minRam": int(image.get("min_ram") or 0), - "minDisk": int(image.get("min_disk") or 0), - "metadata": image.get("properties", {}), - "created": self._format_date(image.get("created_at")), - "updated": self._format_date(image.get("updated_at")), - "status": self._get_status(image), - "progress": self._get_progress(image), - "links": self._get_links(request, image["id"]), - } - - server_ref = image.get("properties", {}).get("instance_ref") - - if server_ref is not None: - image_dict["server"] = { - "id": common.get_id_from_href(server_ref), - "links": [{ - "rel": "self", - "href": server_ref, - }, - { - "rel": "bookmark", - "href": common.remove_version_from_href(server_ref), - }], - } - - return dict(image=image_dict) - - def detail(self, request, images): - """Show a list of images with details.""" - list_func = self.show - return self._list_view(list_func, request, images) - - def index(self, request, images): - """Show a list of images with basic attributes.""" - list_func = self.basic - return self._list_view(list_func, request, images) - - def _list_view(self, list_func, request, images): - """Provide a view for a list of images.""" - image_list = [list_func(request, image)["image"] for image in images] - images_links = self._get_collection_links(request, images) - images_dict = dict(images=image_list) - - if images_links: - images_dict["images_links"] = images_links - - return images_dict - - def _get_links(self, request, identifier): - """Return a list of links for this image.""" - return [{ - "rel": "self", - "href": self._get_href_link(request, identifier), - }, - { - "rel": "bookmark", - "href": self._get_bookmark_link(request, identifier), - }, - { - "rel": "alternate", - "type": "application/vnd.openstack.image", - "href": self._get_alternate_link(request, identifier), - }] - - def _get_alternate_link(self, request, identifier): - """Create an alternate link for a specific flavor id.""" - glance_url = utils.generate_glance_url() - return os.path.join(glance_url, - request.environ["nova.context"].project_id, - self._collection_name, - str(identifier)) - - @staticmethod - def _format_date(date_string): - """Return standard format for given date.""" - if date_string is not None: - return date_string.strftime('%Y-%m-%dT%H:%M:%SZ') - - @staticmethod - def _get_status(image): - """Update the status field to standardize format.""" - return { - 'active': 'ACTIVE', - 'queued': 'SAVING', - 'saving': 'SAVING', - 'deleted': 'DELETED', - 'pending_delete': 'DELETED', - 'killed': 'ERROR', - }.get(image.get("status"), 'UNKNOWN') - - @staticmethod - def _get_progress(image): - return { - "queued": 25, - "saving": 50, - "active": 100, - }.get(image.get("status"), 0) diff --git a/nova/api/openstack/v2/views/limits.py b/nova/api/openstack/v2/views/limits.py deleted file mode 100644 index cff6781be..000000000 --- a/nova/api/openstack/v2/views/limits.py +++ /dev/null @@ -1,96 +0,0 @@ -# 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 datetime - -from nova import utils - - -class ViewBuilder(object): - """Openstack API base limits view builder.""" - - def build(self, rate_limits, absolute_limits): - rate_limits = self._build_rate_limits(rate_limits) - absolute_limits = self._build_absolute_limits(absolute_limits) - - output = { - "limits": { - "rate": rate_limits, - "absolute": absolute_limits, - }, - } - - return output - - def _build_absolute_limits(self, absolute_limits): - """Builder for absolute limits - - absolute_limits should be given as a dict of limits. - For example: {"ram": 512, "gigabytes": 1024}. - - """ - limit_names = { - "ram": ["maxTotalRAMSize"], - "instances": ["maxTotalInstances"], - "cores": ["maxTotalCores"], - "metadata_items": ["maxServerMeta", "maxImageMeta"], - "injected_files": ["maxPersonality"], - "injected_file_content_bytes": ["maxPersonalitySize"], - } - limits = {} - for name, value in absolute_limits.iteritems(): - if name in limit_names and value is not None: - for name in limit_names[name]: - limits[name] = value - return limits - - def _build_rate_limits(self, rate_limits): - limits = [] - for rate_limit in rate_limits: - _rate_limit_key = None - _rate_limit = self._build_rate_limit(rate_limit) - - # check for existing key - for limit in limits: - if limit["uri"] == rate_limit["URI"] and \ - limit["regex"] == rate_limit["regex"]: - _rate_limit_key = limit - break - - # ensure we have a key if we didn't find one - if not _rate_limit_key: - _rate_limit_key = { - "uri": rate_limit["URI"], - "regex": rate_limit["regex"], - "limit": [], - } - limits.append(_rate_limit_key) - - _rate_limit_key["limit"].append(_rate_limit) - - return limits - - def _build_rate_limit(self, rate_limit): - next_avail = \ - datetime.datetime.utcfromtimestamp(rate_limit["resetTime"]) - return { - "verb": rate_limit["verb"], - "value": rate_limit["value"], - "remaining": int(rate_limit["remaining"]), - "unit": rate_limit["unit"], - "next-available": utils.isotime(at=next_avail), - } diff --git a/nova/api/openstack/v2/views/servers.py b/nova/api/openstack/v2/views/servers.py deleted file mode 100644 index 859bd48ab..000000000 --- a/nova/api/openstack/v2/views/servers.py +++ /dev/null @@ -1,193 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# Copyright 2011 Piston Cloud Computing, Inc. -# 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.api.openstack import common -from nova.api.openstack.v2.views import addresses as views_addresses -from nova.api.openstack.v2.views import flavors as views_flavors -from nova.api.openstack.v2.views import images as views_images -from nova import log as logging -from nova import utils - - -LOG = logging.getLogger('nova.api.openstack.v2.views.servers') - - -class ViewBuilder(common.ViewBuilder): - """Model a server API response as a python dictionary.""" - - _collection_name = "servers" - - _progress_statuses = ( - "ACTIVE", - "BUILD", - "REBUILD", - "RESIZE", - "VERIFY_RESIZE", - ) - - _fault_statuses = ( - "ERROR", - ) - - def __init__(self): - """Initialize view builder.""" - super(ViewBuilder, self).__init__() - self._address_builder = views_addresses.ViewBuilder() - self._flavor_builder = views_flavors.ViewBuilder() - self._image_builder = views_images.ViewBuilder() - - def _skip_precooked(func): - def wrapped(self, request, instance): - if instance.get("_is_precooked"): - return dict(server=instance) - else: - return func(self, request, instance) - return wrapped - - def create(self, request, instance): - """View that should be returned when an instance is created.""" - return { - "server": { - "id": instance["uuid"], - "links": self._get_links(request, instance["uuid"]), - }, - } - - @_skip_precooked - def basic(self, request, instance): - """Generic, non-detailed view of an instance.""" - return { - "server": { - "id": instance["uuid"], - "name": instance["display_name"], - "links": self._get_links(request, instance["uuid"]), - }, - } - - @_skip_precooked - def show(self, request, instance): - """Detailed view of a single instance.""" - server = { - "server": { - "id": instance["uuid"], - "name": instance["display_name"], - "status": self._get_vm_state(instance), - "tenant_id": instance.get("project_id") or "", - "user_id": instance.get("user_id") or "", - "metadata": self._get_metadata(instance), - "hostId": self._get_host_id(instance) or "", - "image": self._get_image(request, instance), - "flavor": self._get_flavor(request, instance), - "created": utils.isotime(instance["created_at"]), - "updated": utils.isotime(instance["updated_at"]), - "addresses": self._get_addresses(request, instance), - "accessIPv4": instance.get("access_ip_v4") or "", - "accessIPv6": instance.get("access_ip_v6") or "", - "key_name": instance.get("key_name") or "", - "config_drive": instance.get("config_drive"), - "links": self._get_links(request, instance["uuid"]), - }, - } - _inst_fault = self._get_fault(request, instance) - if server["server"]["status"] in self._fault_statuses and _inst_fault: - server['server']['fault'] = _inst_fault - - if server["server"]["status"] in self._progress_statuses: - server["server"]["progress"] = instance.get("progress", 0) - - return server - - def index(self, request, instances): - """Show a list of servers without many details.""" - return self._list_view(self.basic, request, instances) - - def detail(self, request, instances): - """Detailed view of a list of instance.""" - return self._list_view(self.show, request, instances) - - def _list_view(self, func, request, servers): - """Provide a view for a list of servers.""" - server_list = [func(request, server)["server"] for server in servers] - servers_links = self._get_collection_links(request, servers) - servers_dict = dict(servers=server_list) - - if servers_links: - servers_dict["servers_links"] = servers_links - - return servers_dict - - @staticmethod - def _get_metadata(instance): - metadata = instance.get("metadata", []) - return dict((item['key'], str(item['value'])) for item in metadata) - - @staticmethod - def _get_vm_state(instance): - return common.status_from_state(instance.get("vm_state"), - instance.get("task_state")) - - @staticmethod - def _get_host_id(instance): - host = instance.get("host") - if host: - return hashlib.sha224(host).hexdigest() # pylint: disable=E1101 - - def _get_addresses(self, request, instance): - context = request.environ["nova.context"] - networks = common.get_networks_for_instance(context, instance) - return self._address_builder.index(networks)["addresses"] - - def _get_image(self, request, instance): - image_ref = instance["image_ref"] - image_id = str(common.get_id_from_href(image_ref)) - bookmark = self._image_builder._get_bookmark_link(request, image_id) - return { - "id": image_id, - "links": [{ - "rel": "bookmark", - "href": bookmark, - }], - } - - def _get_flavor(self, request, instance): - flavor_id = instance["instance_type"]["flavorid"] - flavor_ref = self._flavor_builder._get_href_link(request, flavor_id) - flavor_bookmark = self._flavor_builder._get_bookmark_link(request, - flavor_id) - return { - "id": str(common.get_id_from_href(flavor_ref)), - "links": [{ - "rel": "bookmark", - "href": flavor_bookmark, - }], - } - - def _get_fault(self, request, instance): - fault = instance.get("fault", None) - - if not fault: - return None - - return { - "code": fault["code"], - "created": utils.isotime(fault["created_at"]), - "message": fault["message"], - "details": fault["details"], - } diff --git a/nova/api/openstack/v2/views/versions.py b/nova/api/openstack/v2/views/versions.py deleted file mode 100644 index cb2fd9f4a..000000000 --- a/nova/api/openstack/v2/views/versions.py +++ /dev/null @@ -1,94 +0,0 @@ -# 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 copy -import os - - -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_choices(self, VERSIONS, req): - version_objs = [] - for version in VERSIONS: - version = VERSIONS[version] - version_objs.append({ - "id": version['id'], - "status": version['status'], - "links": [ - { - "rel": "self", - "href": self.generate_href(req.path), - }, - ], - "media-types": version['media-types'], - }) - - return dict(choices=version_objs) - - def build_versions(self, versions): - version_objs = [] - for version in sorted(versions.keys()): - version = versions[version] - version_objs.append({ - "id": version['id'], - "status": version['status'], - "updated": version['updated'], - "links": self._build_links(version), - }) - - return dict(versions=version_objs) - - def build_version(self, version): - reval = copy.deepcopy(version) - reval['links'].insert(0, { - "rel": "self", - "href": self.base_url.rstrip('/') + '/', - }) - return dict(version=reval) - - def _build_links(self, version_data): - """Generate a container of links that refer to the provided version.""" - href = self.generate_href() - - links = [ - { - "rel": "self", - "href": href, - }, - ] - - return links - - def generate_href(self, path=None): - """Create an url that refers to a specific version_number.""" - version_number = 'v2' - if path: - path = path.strip('/') - return os.path.join(self.base_url, version_number, path) - else: - return os.path.join(self.base_url, version_number) + '/' diff --git a/nova/api/openstack/volume/__init__.py b/nova/api/openstack/volume/__init__.py new file mode 100644 index 000000000..075b53c29 --- /dev/null +++ b/nova/api/openstack/volume/__init__.py @@ -0,0 +1,99 @@ +# 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. + +""" +WSGI middleware for OpenStack Volume API. +""" + +import routes +import webob.dec +import webob.exc + +import nova.api.openstack +from nova.api.openstack.volume import extensions +from nova.api.openstack.volume import snapshots +from nova.api.openstack.volume import types +from nova.api.openstack.volume import volumes +from nova.api.openstack.volume import versions +from nova.api.openstack import wsgi +from nova import flags +from nova import log as logging +from nova import wsgi as base_wsgi + + +LOG = logging.getLogger('nova.api.openstack.volume') +FLAGS = flags.FLAGS + + +class APIRouter(base_wsgi.Router): + """ + Routes requests on the OpenStack API to the appropriate controller + and method. + """ + + @classmethod + def factory(cls, global_config, **local_config): + """Simple paste factory, :class:`nova.wsgi.Router` doesn't have one""" + return cls() + + def __init__(self, ext_mgr=None): + if ext_mgr is None: + ext_mgr = extensions.ExtensionManager() + + mapper = nova.api.openstack.ProjectMapper() + self._setup_routes(mapper) + self._setup_ext_routes(mapper, ext_mgr) + super(APIRouter, self).__init__(mapper) + + def _setup_ext_routes(self, mapper, ext_mgr): + serializer = wsgi.ResponseSerializer( + {'application/xml': wsgi.XMLDictSerializer()}) + for resource in ext_mgr.get_resources(): + LOG.debug(_('Extended resource: %s'), + resource.collection) + if resource.serializer is None: + resource.serializer = serializer + + kargs = dict( + controller=wsgi.Resource( + resource.controller, resource.deserializer, + resource.serializer), + collection=resource.collection_actions, + member=resource.member_actions) + + if resource.parent: + kargs['parent_resource'] = resource.parent + + mapper.resource(resource.collection, resource.collection, **kargs) + + def _setup_routes(self, mapper): + mapper.connect("versions", "/", + controller=versions.create_resource(), + action='show') + + mapper.redirect("", "/") + + mapper.resource("volume", "volumes", + controller=volumes.create_resource(), + collection={'detail': 'GET'}) + + mapper.resource("type", "types", + controller=types.create_resource()) + + mapper.resource("snapshot", "snapshots", + controller=snapshots.create_resource()) diff --git a/nova/api/openstack/volume/contrib/__init__.py b/nova/api/openstack/volume/contrib/__init__.py new file mode 100644 index 000000000..58c0413ab --- /dev/null +++ b/nova/api/openstack/volume/contrib/__init__.py @@ -0,0 +1,32 @@ +# 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. + +"""Contrib contains extensions that are shipped with nova. + +It can't be called 'extensions' because that causes namespacing problems. + +""" + +from nova import log as logging +from nova.api.openstack import extensions + + +LOG = logging.getLogger('nova.api.openstack.volume.contrib') + + +def standard_extensions(ext_mgr): + extensions.load_standard_extensions(ext_mgr, LOG, __path__, __package__) diff --git a/nova/api/openstack/volume/extensions.py b/nova/api/openstack/volume/extensions.py new file mode 100644 index 000000000..d1007629e --- /dev/null +++ b/nova/api/openstack/volume/extensions.py @@ -0,0 +1,44 @@ +# 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 nova.api.openstack import extensions as base_extensions +from nova import flags +from nova import log as logging + + +LOG = logging.getLogger('nova.api.openstack.volume.extensions') +FLAGS = flags.FLAGS + + +class ExtensionManager(base_extensions.ExtensionManager): + def __new__(cls): + if cls._ext_mgr is None: + LOG.audit(_('Initializing extension manager.')) + + cls._ext_mgr = super(ExtensionManager, cls).__new__(cls) + + cls.cls_list = FLAGS.osapi_volume_extension + cls._ext_mgr.extensions = {} + cls._ext_mgr._load_extensions() + + return cls._ext_mgr + + +class ExtensionMiddleware(base_extensions.ExtensionMiddleware): + def __init__(self, application, ext_mgr=None): + ext_mgr = ExtensionManager() + super(ExtensionMiddleware, self).__init__(application, ext_mgr) diff --git a/nova/api/openstack/volume/snapshots.py b/nova/api/openstack/volume/snapshots.py new file mode 100644 index 000000000..f6ec3dc5f --- /dev/null +++ b/nova/api/openstack/volume/snapshots.py @@ -0,0 +1,183 @@ +# 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 snapshots api.""" + +from webob import exc +import webob + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack.compute import servers +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova.volume import volume_types + + +LOG = logging.getLogger("nova.api.openstack.volume.snapshots") + + +FLAGS = flags.FLAGS + + +def _translate_snapshot_detail_view(context, vol): + """Maps keys for snapshots details view.""" + + d = _translate_snapshot_summary_view(context, vol) + + # NOTE(gagupta): No additional data / lookups at the moment + return d + + +def _translate_snapshot_summary_view(context, vol): + """Maps keys for snapshots summary view.""" + d = {} + + d['id'] = vol['id'] + d['volumeId'] = vol['volume_id'] + d['status'] = vol['status'] + # NOTE(gagupta): We map volume_size as the snapshot size + d['size'] = vol['volume_size'] + d['createdAt'] = vol['created_at'] + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +class SnapshotsController(object): + """The Volumes API controller for the OpenStack API.""" + + def __init__(self): + self.volume_api = volume.API() + super(SnapshotsController, self).__init__() + + def show(self, req, id): + """Return data about the given snapshot.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get_snapshot(context, id) + except exception.NotFound: + return exc.HTTPNotFound() + + return {'snapshot': _translate_snapshot_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a snapshot.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete snapshot with id: %s"), id, context=context) + + try: + self.volume_api.delete_snapshot(context, snapshot_id=id) + except exception.NotFound: + return exc.HTTPNotFound() + return webob.Response(status_int=202) + + def index(self, req): + """Returns a summary list of snapshots.""" + return self._items(req, entity_maker=_translate_snapshot_summary_view) + + def detail(self, req): + """Returns a detailed list of snapshots.""" + return self._items(req, entity_maker=_translate_snapshot_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of snapshots, transformed through entity_maker.""" + context = req.environ['nova.context'] + + snapshots = self.volume_api.get_all_snapshots(context) + limited_list = common.limited(snapshots, req) + res = [entity_maker(context, snapshot) for snapshot in limited_list] + return {'snapshots': res} + + def create(self, req, body): + """Creates a new snapshot.""" + context = req.environ['nova.context'] + + if not body: + return exc.HTTPUnprocessableEntity() + + snapshot = body['snapshot'] + volume_id = snapshot['volume_id'] + force = snapshot.get('force', False) + LOG.audit(_("Create snapshot from volume %s"), volume_id, + context=context) + + if force: + new_snapshot = self.volume_api.create_snapshot_force(context, + volume_id, + snapshot.get('display_name'), + snapshot.get('display_description')) + else: + new_snapshot = self.volume_api.create_snapshot(context, + volume_id, + snapshot.get('display_name'), + snapshot.get('display_description')) + + retval = _translate_snapshot_detail_view(context, new_snapshot) + + return {'snapshot': retval} + + +def make_snapshot(elem): + elem.set('id') + elem.set('status') + elem.set('size') + elem.set('createdAt') + elem.set('displayName') + elem.set('displayDescription') + elem.set('volumeId') + + +class SnapshotTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('snapshot', selector='snapshot') + make_snapshot(root) + return xmlutil.MasterTemplate(root, 1) + + +class SnapshotsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('snapshots') + elem = xmlutil.SubTemplateElement(root, 'snapshot', + selector='snapshots') + make_snapshot(elem) + return xmlutil.MasterTemplate(root, 1) + + +class SnapshotSerializer(xmlutil.XMLTemplateSerializer): + def default(self): + return SnapshotTemplate() + + def index(self): + return SnapshotsTemplate() + + def detail(self): + return SnapshotsTemplate() + + +def create_resource(): + body_serializers = { + 'application/xml': SnapshotSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers) + + return wsgi.Resource(SnapshotsController(), serializer=serializer) diff --git a/nova/api/openstack/volume/types.py b/nova/api/openstack/volume/types.py new file mode 100644 index 000000000..1c6a68b58 --- /dev/null +++ b/nova/api/openstack/volume/types.py @@ -0,0 +1,89 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Zadara Storage Inc. +# Copyright (c) 2011 OpenStack LLC. +# +# 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 volume type & volume types extra specs extension""" + +from webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import db +from nova import exception +from nova.volume import volume_types + + +class VolumeTypesController(object): + """ The volume types API controller for the Openstack API """ + + def index(self, req): + """ Returns the list of volume types """ + context = req.environ['nova.context'] + return volume_types.get_all_types(context) + + def show(self, req, id): + """ Return a single volume type item """ + context = req.environ['nova.context'] + + try: + vol_type = volume_types.get_volume_type(context, id) + except exception.NotFound or exception.ApiError: + raise exc.HTTPNotFound() + + return {'volume_type': vol_type} + + +def make_voltype(elem): + elem.set('id') + elem.set('name') + extra_specs = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') + elem.append(extra_specs) + + +class VolumeTypeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume_type', selector='volume_type') + make_voltype(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume_types') + sel = lambda obj, do_raise=False: obj.values() + elem = xmlutil.SubTemplateElement(root, 'volume_type', selector=sel) + make_voltype(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypesSerializer(xmlutil.XMLTemplateSerializer): + def index(self): + return VolumeTypesTemplate() + + def default(self): + return VolumeTypeTemplate() + + +def create_resource(): + body_serializers = { + 'application/xml': VolumeTypesSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers) + + deserializer = wsgi.RequestDeserializer() + + return wsgi.Resource(VolumeTypesController(), serializer=serializer) diff --git a/nova/api/openstack/volume/versions.py b/nova/api/openstack/volume/versions.py new file mode 100644 index 000000000..9a29e4adf --- /dev/null +++ b/nova/api/openstack/volume/versions.py @@ -0,0 +1,83 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from datetime import datetime + +from lxml import etree + +from nova.api.openstack.compute import versions +from nova.api.openstack.volume.views import versions as views_versions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil + + +VERSIONS = { + "v1": { + "id": "v1", + "status": "CURRENT", + "updated": "2012-01-04T11:33:21Z", + "links": [ + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://jorgew.github.com/block-storage-api/" + "content/os-block-storage-1.0.pdf", + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + #(anthony) FIXME + "href": "http://docs.rackspacecloud.com/" + "servers/api/v1.1/application.wadl", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.volume+xml;version=1", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.volume+json;version=1", + } + ], + } +} + + +class Versions(versions.Versions): + def dispatch(self, request, *args): + """Respond to a request for all OpenStack API versions.""" + builder = views_versions.get_view_builder(request) + if request.path == '/': + # List Versions + return builder.build_versions(VERSIONS) + else: + # Versions Multiple Choice + return builder.build_choices(VERSIONS, request) + + +class VolumeVersionV1(object): + @wsgi.serializers(xml=versions.VersionTemplate, + atom=versions.VersionAtomSerializer) + def show(self, req): + builder = views_versions.get_view_builder(req) + return builder.build_version(VERSIONS['v2.0']) + + +def create_resource(): + return wsgi.Resource(VolumeVersionV1()) diff --git a/nova/api/openstack/volume/views/__init__.py b/nova/api/openstack/volume/views/__init__.py new file mode 100644 index 000000000..d65c689a8 --- /dev/null +++ b/nova/api/openstack/volume/views/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/nova/api/openstack/volume/views/versions.py b/nova/api/openstack/volume/views/versions.py new file mode 100644 index 000000000..e446a4b64 --- /dev/null +++ b/nova/api/openstack/volume/views/versions.py @@ -0,0 +1,37 @@ +# 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 copy +import os + +from nova.api.openstack.compute.views import versions as compute_views + + +def get_view_builder(req): + base_url = req.application_url + return ViewBuilder(base_url) + + +class ViewBuilder(compute_views.ViewBuilder): + def generate_href(self, path=None): + """Create an url that refers to a specific version_number.""" + version_number = 'v1' + if path: + path = path.strip('/') + return os.path.join(self.base_url, version_number, path) + else: + return os.path.join(self.base_url, version_number) + '/' diff --git a/nova/api/openstack/volume/volumes.py b/nova/api/openstack/volume/volumes.py new file mode 100644 index 000000000..5e16f4fde --- /dev/null +++ b/nova/api/openstack/volume/volumes.py @@ -0,0 +1,254 @@ +# 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 api.""" + +from webob import exc +import webob + +from nova.api.openstack import common +from nova.api.openstack.compute import servers +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova.volume import volume_types + + +LOG = logging.getLogger("nova.api.openstack.volume.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'] + + if vol['volume_type_id'] and vol.get('volume_type'): + d['volumeType'] = vol['volume_type']['name'] + else: + d['volumeType'] = vol['volume_type_id'] + + d['snapshotId'] = vol['snapshot_id'] + LOG.audit(_("vol=%s"), vol, context=context) + + if vol.get('volume_metadata'): + meta_dict = {} + for i in vol['volume_metadata']: + meta_dict[i['key']] = i['value'] + d['metadata'] = meta_dict + else: + d['metadata'] = {} + + return d + + +class VolumeController(object): + """The Volumes API controller for the OpenStack API.""" + + 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: + raise 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: + raise exc.HTTPNotFound() + return webob.Response(status_int=202) + + 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, body): + """Creates a new volume.""" + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + vol = body['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + + vol_type = vol.get('volume_type', None) + if vol_type: + try: + vol_type = volume_types.get_volume_type_by_name(context, + vol_type) + except exception.NotFound: + raise exc.HTTPNotFound() + + metadata = vol.get('metadata', None) + + new_volume = self.volume_api.create(context, size, + vol.get('snapshot_id'), + vol.get('display_name'), + vol.get('display_description'), + volume_type=vol_type, + metadata=metadata) + + # Work around problem that instance is lazy-loaded... + new_volume = self.volume_api.get(context, new_volume['id']) + + retval = _translate_volume_detail_view(context, new_volume) + + return {'volume': retval} + + +class VolumeAttachmentTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumeAttachment', + selector='volumeAttachment') + make_attachment(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeAttachmentsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumeAttachments') + elem = xmlutil.SubTemplateElement(root, 'volumeAttachment', + selector='volumeAttachments') + make_attachment(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeAttachmentSerializer(xmlutil.XMLTemplateSerializer): + def default(self): + return VolumeAttachmentTemplate() + + def index(self): + return VolumeAttachmentsTemplate() + + +def make_attachment(elem): + elem.set('id') + elem.set('serverId') + elem.set('volumeId') + elem.set('device') + + +def make_volume(elem): + elem.set('id') + elem.set('status') + elem.set('size') + elem.set('availabilityZone') + elem.set('createdAt') + elem.set('displayName') + elem.set('displayDescription') + elem.set('volumeType') + elem.set('snapshotId') + + attachments = xmlutil.SubTemplateElement(elem, 'attachments') + attachment = xmlutil.SubTemplateElement(attachments, 'attachment', + selector='attachments') + make_attachment(attachment) + + metadata = xmlutil.make_flat_dict('metadata') + elem.append(metadata) + + +class VolumeTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volume', selector='volume') + make_volume(root) + return xmlutil.MasterTemplate(root, 1) + + +class VolumesTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('volumes') + elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes') + make_volume(elem) + return xmlutil.MasterTemplate(root, 1) + + +class VolumeSerializer(xmlutil.XMLTemplateSerializer): + def default(self): + return VolumeTemplate() + + def index(self): + return VolumesTemplate() + + def detail(self): + return VolumesTemplate() + + +def create_resource(): + body_serializers = { + 'application/xml': VolumeSerializer(), + } + serializer = wsgi.ResponseSerializer(body_serializers) + + deserializer = wsgi.RequestDeserializer() + + return wsgi.Resource(VolumeController(), serializer=serializer) diff --git a/nova/api/openstack/xmlutil.py b/nova/api/openstack/xmlutil.py index 90861ad4f..4b64d4a8f 100644 --- a/nova/api/openstack/xmlutil.py +++ b/nova/api/openstack/xmlutil.py @@ -31,9 +31,9 @@ XMLNS_ATOM = 'http://www.w3.org/2005/Atom' def validate_schema(xml, schema_name): if isinstance(xml, str): xml = etree.fromstring(xml) - base_path = 'nova/api/openstack/v2/schemas/v1.1/' + base_path = 'nova/api/openstack/compute/schemas/v1.1/' if schema_name in ('atom', 'atom-link'): - base_path = 'nova/api/openstack/v2/schemas/' + base_path = 'nova/api/openstack/compute/schemas/' schema_path = os.path.join(utils.novadir(), '%s%s.rng' % (base_path, schema_name)) schema_doc = etree.parse(schema_path) -- cgit