summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/ec2/admin.py20
-rw-r--r--nova/api/ec2/cloud.py136
-rw-r--r--nova/api/ec2/metadatarequesthandler.py6
-rw-r--r--nova/api/openstack/accounts.py6
-rw-r--r--nova/api/openstack/backup_schedules.py13
-rw-r--r--nova/api/openstack/common.py47
-rw-r--r--nova/api/openstack/consoles.py12
-rw-r--r--nova/api/openstack/contrib/flavorextraspecs.py126
-rw-r--r--nova/api/openstack/contrib/floating_ips.py173
-rw-r--r--nova/api/openstack/contrib/hosts.py114
-rw-r--r--nova/api/openstack/contrib/multinic.py125
-rw-r--r--nova/api/openstack/create_instance_helper.py15
-rw-r--r--nova/api/openstack/flavors.py6
-rw-r--r--nova/api/openstack/image_metadata.py23
-rw-r--r--nova/api/openstack/images.py156
-rw-r--r--nova/api/openstack/ips.py5
-rw-r--r--nova/api/openstack/limits.py6
-rw-r--r--nova/api/openstack/server_metadata.py6
-rw-r--r--nova/api/openstack/servers.py44
-rw-r--r--nova/api/openstack/shared_ip_groups.py12
-rw-r--r--nova/api/openstack/users.py6
-rw-r--r--nova/api/openstack/versions.py5
-rw-r--r--nova/api/openstack/views/addresses.py10
-rw-r--r--nova/api/openstack/views/flavors.py16
-rw-r--r--nova/api/openstack/views/images.py19
-rw-r--r--nova/api/openstack/views/servers.py14
-rw-r--r--nova/api/openstack/wsgi.py199
-rw-r--r--nova/api/openstack/zones.py9
28 files changed, 1077 insertions, 252 deletions
diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py
index 343bc61c4..df7876b9d 100644
--- a/nova/api/ec2/admin.py
+++ b/nova/api/ec2/admin.py
@@ -369,3 +369,23 @@ class AdminController(object):
raise exception.ApiError(_('Duplicate rule'))
self.compute_api.trigger_provider_fw_rules_refresh(context)
return {'status': 'OK', 'message': 'Added %s rules' % rules_added}
+
+ def describe_external_address_blocks(self, context):
+ blocks = db.provider_fw_rule_get_all(context)
+ # NOTE(todd): use a set since we have icmp/udp/tcp rules with same cidr
+ blocks = set([b.cidr for b in blocks])
+ blocks = [{'cidr': b} for b in blocks]
+ return {'externalIpBlockInfo':
+ list(sorted(blocks, key=lambda k: k['cidr']))}
+
+ def remove_external_address_block(self, context, cidr):
+ LOG.audit(_('Removing ip block from %s'), cidr, context=context)
+ cidr = urllib.unquote(cidr).decode()
+ # raise if invalid
+ netaddr.IPNetwork(cidr)
+ rules = db.provider_fw_rule_get_all_by_cidr(context, cidr)
+ for rule in rules:
+ db.provider_fw_rule_destroy(context, rule['id'])
+ if rules:
+ self.compute_api.trigger_provider_fw_rules_refresh(context)
+ return {'status': 'OK', 'message': 'Deleted %s rules' % len(rules)}
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 97875f1f5..acfd1361c 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -23,7 +23,7 @@ datastore.
"""
import base64
-import IPy
+import netaddr
import os
import urllib
import tempfile
@@ -86,8 +86,7 @@ class CloudController(object):
self.volume_api = volume.API()
self.compute_api = compute.API(
network_api=self.network_api,
- volume_api=self.volume_api,
- hostname_factory=ec2utils.id_to_ec2_id)
+ volume_api=self.volume_api)
self.setup()
def __str__(self):
@@ -121,8 +120,8 @@ class CloudController(object):
result = {}
for instance in self.compute_api.get_all(context,
project_id=project_id):
- if instance['fixed_ip']:
- line = '%s slots=%d' % (instance['fixed_ip']['address'],
+ if instance['fixed_ips']:
+ line = '%s slots=%d' % (instance['fixed_ips'][0]['address'],
instance['vcpus'])
key = str(instance['key_name'])
if key in result:
@@ -152,7 +151,7 @@ class CloudController(object):
# This ensures that all attributes of the instance
# are populated.
- instance_ref = db.instance_get(ctxt, instance_ref['id'])
+ instance_ref = db.instance_get(ctxt, instance_ref[0]['id'])
mpi = self._get_mpi_data(ctxt, instance_ref['project_id'])
if instance_ref['key_name']:
@@ -167,6 +166,9 @@ class CloudController(object):
instance_ref['id'])
ec2_id = ec2utils.id_to_ec2_id(instance_ref['id'])
image_ec2_id = self.image_ec2_id(instance_ref['image_ref'])
+ security_groups = db.security_group_get_by_instance(ctxt,
+ instance_ref['id'])
+ security_groups = [x['name'] for x in security_groups]
data = {
'user-data': base64.b64decode(instance_ref['user_data']),
'meta-data': {
@@ -190,7 +192,7 @@ class CloudController(object):
'public-ipv4': floating_ip or '',
'public-keys': keys,
'reservation-id': instance_ref['reservation_id'],
- 'security-groups': '',
+ 'security-groups': security_groups,
'mpi': mpi}}
for image_type in ['kernel', 'ramdisk']:
@@ -391,15 +393,21 @@ class CloudController(object):
pass
return True
- def describe_security_groups(self, context, group_name=None, **kwargs):
+ def describe_security_groups(self, context, group_name=None, group_id=None,
+ **kwargs):
self.compute_api.ensure_default_security_group(context)
- if group_name:
+ if group_name or group_id:
groups = []
- for name in group_name:
- group = db.security_group_get_by_name(context,
- context.project_id,
- name)
- groups.append(group)
+ if group_name:
+ for name in group_name:
+ group = db.security_group_get_by_name(context,
+ context.project_id,
+ name)
+ groups.append(group)
+ if group_id:
+ for gid in group_id:
+ group = db.security_group_get(context, gid)
+ groups.append(group)
elif context.is_admin:
groups = db.security_group_get_all(context)
else:
@@ -452,7 +460,7 @@ class CloudController(object):
elif cidr_ip:
# If this fails, it throws an exception. This is what we want.
cidr_ip = urllib.unquote(cidr_ip).decode()
- IPy.IP(cidr_ip)
+ netaddr.IPNetwork(cidr_ip)
values['cidr'] = cidr_ip
else:
values['cidr'] = '0.0.0.0/0'
@@ -497,13 +505,26 @@ class CloudController(object):
return True
return False
- def revoke_security_group_ingress(self, context, group_name, **kwargs):
- LOG.audit(_("Revoke security group ingress %s"), group_name,
- context=context)
+ def revoke_security_group_ingress(self, context, group_name=None,
+ group_id=None, **kwargs):
+ if not group_name and not group_id:
+ err = "Not enough parameters, need group_name or group_id"
+ raise exception.ApiError(_(err))
self.compute_api.ensure_default_security_group(context)
- security_group = db.security_group_get_by_name(context,
- context.project_id,
- group_name)
+ notfound = exception.SecurityGroupNotFound
+ if group_name:
+ security_group = db.security_group_get_by_name(context,
+ context.project_id,
+ group_name)
+ if not security_group:
+ raise notfound(security_group_id=group_name)
+ if group_id:
+ security_group = db.security_group_get(context, group_id)
+ if not security_group:
+ raise notfound(security_group_id=group_id)
+
+ msg = "Revoke security group ingress %s"
+ LOG.audit(_(msg), security_group['name'], context=context)
criteria = self._revoke_rule_args_to_dict(context, **kwargs)
if criteria is None:
@@ -518,7 +539,7 @@ class CloudController(object):
if match:
db.security_group_rule_destroy(context, rule['id'])
self.compute_api.trigger_security_group_rules_refresh(context,
- security_group['id'])
+ security_group_id=security_group['id'])
return True
raise exception.ApiError(_("No rule for the specified parameters."))
@@ -526,14 +547,26 @@ class CloudController(object):
# Unfortunately, it seems Boto is using an old API
# for these operations, so support for newer API versions
# is sketchy.
- def authorize_security_group_ingress(self, context, group_name, **kwargs):
- LOG.audit(_("Authorize security group ingress %s"), group_name,
- context=context)
+ def authorize_security_group_ingress(self, context, group_name=None,
+ group_id=None, **kwargs):
+ if not group_name and not group_id:
+ err = "Not enough parameters, need group_name or group_id"
+ raise exception.ApiError(_(err))
self.compute_api.ensure_default_security_group(context)
- security_group = db.security_group_get_by_name(context,
- context.project_id,
- group_name)
-
+ notfound = exception.SecurityGroupNotFound
+ if group_name:
+ security_group = db.security_group_get_by_name(context,
+ context.project_id,
+ group_name)
+ if not security_group:
+ raise notfound(security_group_id=group_name)
+ if group_id:
+ security_group = db.security_group_get(context, group_id)
+ if not security_group:
+ raise notfound(security_group_id=group_id)
+
+ msg = "Authorize security group ingress %s"
+ LOG.audit(_(msg), security_group['name'], context=context)
values = self._revoke_rule_args_to_dict(context, **kwargs)
if values is None:
raise exception.ApiError(_("Not enough parameters to build a "
@@ -547,7 +580,7 @@ class CloudController(object):
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=security_group['id'])
return True
@@ -583,11 +616,23 @@ class CloudController(object):
return {'securityGroupSet': [self._format_security_group(context,
group_ref)]}
- def delete_security_group(self, context, group_name, **kwargs):
+ def delete_security_group(self, context, group_name=None, group_id=None,
+ **kwargs):
+ if not group_name and not group_id:
+ err = "Not enough parameters, need group_name or group_id"
+ raise exception.ApiError(_(err))
+ notfound = exception.SecurityGroupNotFound
+ if group_name:
+ security_group = db.security_group_get_by_name(context,
+ context.project_id,
+ group_name)
+ if not security_group:
+ raise notfound(security_group_id=group_name)
+ elif group_id:
+ security_group = db.security_group_get(context, group_id)
+ if not security_group:
+ raise notfound(security_group_id=group_id)
LOG.audit(_("Delete security group %s"), group_name, context=context)
- security_group = db.security_group_get_by_name(context,
- context.project_id,
- group_name)
db.security_group_destroy(context, security_group.id)
return True
@@ -793,15 +838,15 @@ class CloudController(object):
'name': instance['state_description']}
fixed_addr = None
floating_addr = None
- if instance['fixed_ip']:
- fixed_addr = instance['fixed_ip']['address']
- if instance['fixed_ip']['floating_ips']:
- fixed = instance['fixed_ip']
+ 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 instance['fixed_ip']['network'] and 'use_v6' in kwargs:
+ if fixed['network'] and 'use_v6' in kwargs:
i['dnsNameV6'] = ipv6.to_global(
- instance['fixed_ip']['network']['cidr_v6'],
- instance['mac_address'],
+ fixed['network']['cidr_v6'],
+ fixed['virtual_interface']['address'],
instance['project_id'])
i['privateDnsName'] = fixed_addr
@@ -877,7 +922,8 @@ class CloudController(object):
public_ip = self.network_api.allocate_floating_ip(context)
return {'publicIp': public_ip}
except rpc.RemoteError as ex:
- if ex.exc_type == 'NoMoreAddresses':
+ # NOTE(tr3buchet) - why does this block exist?
+ if ex.exc_type == 'NoMoreFloatingIps':
raise exception.NoMoreFloatingIps()
else:
raise
@@ -1045,12 +1091,16 @@ class CloudController(object):
def _get_image(self, context, ec2_id):
try:
internal_id = ec2utils.ec2_id_to_id(ec2_id)
- return self.image_service.show(context, internal_id)
+ image = self.image_service.show(context, internal_id)
except (exception.InvalidEc2Id, exception.ImageNotFound):
try:
return self.image_service.show_by_name(context, ec2_id)
except exception.NotFound:
raise exception.ImageNotFound(image_id=ec2_id)
+ image_type = ec2_id.split('-')[0]
+ if self._image_type(image.get('container_format')) != image_type:
+ raise exception.ImageNotFound(image_id=ec2_id)
+ return image
def _format_image(self, image):
"""Convert from format defined by BaseImageService to S3 format."""
diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py
index b70266a20..1dc275c90 100644
--- a/nova/api/ec2/metadatarequesthandler.py
+++ b/nova/api/ec2/metadatarequesthandler.py
@@ -35,6 +35,9 @@ FLAGS = flags.FLAGS
class MetadataRequestHandler(wsgi.Application):
"""Serve metadata from the EC2 API."""
+ def __init__(self):
+ self.cc = cloud.CloudController()
+
def print_data(self, data):
if isinstance(data, dict):
output = ''
@@ -68,12 +71,11 @@ class MetadataRequestHandler(wsgi.Application):
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
- cc = cloud.CloudController()
remote_address = req.remote_addr
if FLAGS.use_forwarded_for:
remote_address = req.headers.get('X-Forwarded-For', remote_address)
try:
- meta_data = cc.get_metadata(remote_address)
+ meta_data = self.cc.get_metadata(remote_address)
except Exception:
LOG.exception(_('Failed to get metadata for ip: %s'),
remote_address)
diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py
index 0dcd37217..e3201b14f 100644
--- a/nova/api/openstack/accounts.py
+++ b/nova/api/openstack/accounts.py
@@ -87,8 +87,8 @@ def create_resource():
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
}
-
- return wsgi.Resource(Controller(), serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+ return wsgi.Resource(Controller(), serializer=serializer)
diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py
index 71a14d4ce..3e95aedf3 100644
--- a/nova/api/openstack/backup_schedules.py
+++ b/nova/api/openstack/backup_schedules.py
@@ -34,20 +34,20 @@ class Controller(object):
def __init__(self):
pass
- def index(self, req, server_id):
+ def index(self, req, server_id, **kwargs):
""" Returns the list of backup schedules for a given instance """
return faults.Fault(exc.HTTPNotImplemented())
- def show(self, req, server_id, id):
+ def show(self, req, server_id, id, **kwargs):
""" Returns a single backup schedule for a given instance """
return faults.Fault(exc.HTTPNotImplemented())
- def create(self, req, server_id, body):
+ def create(self, req, server_id, **kwargs):
""" No actual update method required, since the existing API allows
both create and update through a POST """
return faults.Fault(exc.HTTPNotImplemented())
- def delete(self, req, server_id, id):
+ def delete(self, req, server_id, id, **kwargs):
""" Deletes an existing backup schedule """
return faults.Fault(exc.HTTPNotImplemented())
@@ -59,9 +59,10 @@ def create_resource():
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10,
metadata=metadata),
}
- return wsgi.Resource(Controller(), serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+ return wsgi.Resource(Controller(), serializer=serializer)
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 4da7ec0ef..9aa384f33 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -45,23 +45,20 @@ def get_pagination_params(request):
exc.HTTPBadRequest() exceptions to be raised.
"""
- try:
- marker = int(request.GET.get('marker', 0))
- except ValueError:
- raise webob.exc.HTTPBadRequest(_('marker param must be an integer'))
-
- try:
- limit = int(request.GET.get('limit', 0))
- except ValueError:
- raise webob.exc.HTTPBadRequest(_('limit param must be an integer'))
-
- if limit < 0:
- raise webob.exc.HTTPBadRequest(_('limit param must be positive'))
-
- if marker < 0:
- raise webob.exc.HTTPBadRequest(_('marker param must be positive'))
-
- return(marker, limit)
+ params = {}
+ for param in ['marker', 'limit']:
+ if not param in request.GET:
+ continue
+ try:
+ params[param] = int(request.GET[param])
+ except ValueError:
+ msg = _('%s param must be an integer') % param
+ raise webob.exc.HTTPBadRequest(msg)
+ if params[param] < 0:
+ msg = _('%s param must be positive') % param
+ raise webob.exc.HTTPBadRequest(msg)
+
+ return params
def limited(items, request, max_limit=FLAGS.osapi_max_limit):
@@ -100,10 +97,10 @@ def limited(items, request, max_limit=FLAGS.osapi_max_limit):
def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit):
"""Return a slice of items according to the requested marker and limit."""
- (marker, limit) = get_pagination_params(request)
+ params = get_pagination_params(request)
- if limit == 0:
- limit = max_limit
+ limit = params.get('limit', max_limit)
+ marker = params.get('marker')
limit = min(max_limit, limit)
start_index = 0
@@ -137,3 +134,13 @@ def get_id_from_href(href):
except:
LOG.debug(_("Error extracting id from href: %s") % href)
raise webob.exc.HTTPBadRequest(_('could not parse id from href'))
+
+
+def remove_version_from_href(base_url):
+ """Removes the api version from the href.
+
+ Given: 'http://www.nova.com/v1.1/123'
+ Returns: 'http://www.nova.com/123'
+
+ """
+ return base_url.rsplit('/', 1).pop(0)
diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py
index bccf04d8f..7a43fba96 100644
--- a/nova/api/openstack/consoles.py
+++ b/nova/api/openstack/consoles.py
@@ -90,14 +90,4 @@ class Controller(object):
def create_resource():
- metadata = {
- 'attributes': {
- 'console': [],
- },
- }
-
- serializers = {
- 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
- }
-
- return wsgi.Resource(Controller(), serializers=serializers)
+ return wsgi.Resource(Controller())
diff --git a/nova/api/openstack/contrib/flavorextraspecs.py b/nova/api/openstack/contrib/flavorextraspecs.py
new file mode 100644
index 000000000..2d897a1da
--- /dev/null
+++ b/nova/api/openstack/contrib/flavorextraspecs.py
@@ -0,0 +1,126 @@
+# 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 import db
+from nova import quota
+from nova.api.openstack import extensions
+from nova.api.openstack import faults
+from nova.api.openstack import wsgi
+
+
+class FlavorExtraSpecsController(object):
+ """ The flavor extra specs API controller for the Openstack API """
+
+ def _get_extra_specs(self, context, flavor_id):
+ extra_specs = db.api.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 == None or body == "":
+ expl = _('No Request Body')
+ raise exc.HTTPBadRequest(explanation=expl)
+
+ 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)
+
+ def create(self, req, flavor_id, body):
+ self._check_body(body)
+ context = req.environ['nova.context']
+ specs = body.get('extra_specs')
+ try:
+ db.api.instance_type_extra_specs_update_or_create(context,
+ flavor_id,
+ specs)
+ except quota.QuotaError as error:
+ self._handle_quota_error(error)
+ return body
+
+ 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.api.instance_type_extra_specs_update_or_create(context,
+ flavor_id,
+ body)
+ except quota.QuotaError as error:
+ self._handle_quota_error(error)
+
+ return body
+
+ 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:
+ return faults.Fault(exc.HTTPNotFound())
+
+ def delete(self, req, flavor_id, id):
+ """ Deletes an existing extra spec """
+ context = req.environ['nova.context']
+ db.api.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):
+
+ def get_name(self):
+ return "FlavorExtraSpecs"
+
+ def get_alias(self):
+ return "os-flavor-extra-specs"
+
+ def get_description(self):
+ return "Instance type (flavor) extra specs"
+
+ def get_namespace(self):
+ return \
+ "http://docs.openstack.org/ext/flavor_extra_specs/api/v1.1"
+
+ def get_updated(self):
+ return "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/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py
new file mode 100644
index 000000000..b4a211857
--- /dev/null
+++ b/nova/api/openstack/contrib/floating_ips.py
@@ -0,0 +1,173 @@
+# 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
+from webob import exc
+
+from nova import exception
+from nova import network
+from nova import rpc
+from nova.api.openstack import faults
+from nova.api.openstack import extensions
+
+
+def _translate_floating_ip_view(floating_ip):
+ result = {'id': floating_ip['id'],
+ 'ip': floating_ip['address']}
+ if 'fixed_ip' in floating_ip:
+ result['fixed_ip'] = floating_ip['fixed_ip']['address']
+ else:
+ result['fixed_ip'] = None
+ if 'instance' in floating_ip:
+ result['instance_id'] = floating_ip['instance']['id']
+ else:
+ result['instance_id'] = None
+ return {'floating_ip': result}
+
+
+def _translate_floating_ips_view(floating_ips):
+ return {'floating_ips': [_translate_floating_ip_view(floating_ip)
+ for floating_ip in floating_ips]}
+
+
+class FloatingIPController(object):
+ """The Floating IPs API controller for the OpenStack API."""
+
+ _serialization_metadata = {
+ 'application/xml': {
+ "attributes": {
+ "floating_ip": [
+ "id",
+ "ip",
+ "instance_id",
+ "fixed_ip",
+ ]}}}
+
+ def __init__(self):
+ self.network_api = network.API()
+ super(FloatingIPController, self).__init__()
+
+ 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:
+ return faults.Fault(exc.HTTPNotFound())
+
+ return _translate_floating_ip_view(floating_ip)
+
+ def index(self, req):
+ context = req.environ['nova.context']
+
+ floating_ips = self.network_api.list_floating_ips(context)
+
+ return _translate_floating_ips_view(floating_ips)
+
+ def create(self, req):
+ context = req.environ['nova.context']
+
+ try:
+ address = self.network_api.allocate_floating_ip(context)
+ ip = self.network_api.get_floating_ip_by_ip(context, address)
+ except rpc.RemoteError as ex:
+ # NOTE(tr3buchet) - why does this block exist?
+ if ex.exc_type == 'NoMoreFloatingIps':
+ raise exception.NoMoreFloatingIps()
+ else:
+ raise
+
+ return {'allocated': {
+ "id": ip['id'],
+ "floating_ip": ip['address']}}
+
+ def delete(self, req, id):
+ context = req.environ['nova.context']
+
+ ip = self.network_api.get_floating_ip(context, id)
+ self.network_api.release_floating_ip(context, address=ip)
+
+ return {'released': {
+ "id": ip['id'],
+ "floating_ip": ip['address']}}
+
+ def associate(self, req, id, body):
+ """ /floating_ips/{id}/associate fixed ip in body """
+ context = req.environ['nova.context']
+ floating_ip = self._get_ip_by_id(context, id)
+
+ fixed_ip = body['associate_address']['fixed_ip']
+
+ try:
+ self.network_api.associate_floating_ip(context,
+ floating_ip, fixed_ip)
+ except rpc.RemoteError:
+ raise
+
+ return {'associated':
+ {
+ "floating_ip_id": id,
+ "floating_ip": floating_ip,
+ "fixed_ip": fixed_ip}}
+
+ def disassociate(self, req, id):
+ """ POST /floating_ips/{id}/disassociate """
+ context = req.environ['nova.context']
+ floating_ip = self.network_api.get_floating_ip(context, id)
+ address = floating_ip['address']
+ fixed_ip = floating_ip['fixed_ip']['address']
+
+ try:
+ self.network_api.disassociate_floating_ip(context, address)
+ except rpc.RemoteError:
+ raise
+
+ return {'disassociated': {'floating_ip': address,
+ 'fixed_ip': fixed_ip}}
+
+ 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 Floating_ips(extensions.ExtensionDescriptor):
+ def get_name(self):
+ return "Floating_ips"
+
+ def get_alias(self):
+ return "os-floating-ips"
+
+ def get_description(self):
+ return "Floating IPs support"
+
+ def get_namespace(self):
+ return "http://docs.openstack.org/ext/floating_ips/api/v1.1"
+
+ def get_updated(self):
+ return "2011-06-16T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = []
+
+ res = extensions.ResourceExtension('os-floating-ips',
+ FloatingIPController(),
+ member_actions={
+ 'associate': 'POST',
+ 'disassociate': 'POST'})
+ resources.append(res)
+
+ return resources
diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py
new file mode 100644
index 000000000..55e57e1a4
--- /dev/null
+++ b/nova/api/openstack/contrib/hosts.py
@@ -0,0 +1,114 @@
+# 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 nova import compute
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import faults
+from nova.scheduler import api as scheduler_api
+
+
+LOG = logging.getLogger("nova.api.hosts")
+FLAGS = flags.FLAGS
+
+
+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__()
+
+ def index(self, req):
+ return {'hosts': _list_hosts(req)}
+
+ @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
+ # actions 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)
+ return {"host": host, "status": result}
+
+
+class Hosts(extensions.ExtensionDescriptor):
+ def get_name(self):
+ return "Hosts"
+
+ def get_alias(self):
+ return "os-hosts"
+
+ def get_description(self):
+ return "Host administration"
+
+ def get_namespace(self):
+ return "http://docs.openstack.org/ext/hosts/api/v1.1"
+
+ def get_updated(self):
+ return "2011-06-29T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = [extensions.ResourceExtension('os-hosts', HostController(),
+ collection_actions={'update': 'PUT'}, member_actions={})]
+ return resources
diff --git a/nova/api/openstack/contrib/multinic.py b/nova/api/openstack/contrib/multinic.py
new file mode 100644
index 000000000..841061721
--- /dev/null
+++ b/nova/api/openstack/contrib/multinic.py
@@ -0,0 +1,125 @@
+# 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."""
+
+from webob import exc
+
+from nova import compute
+from nova import log as logging
+from nova.api.openstack import extensions
+from nova.api.openstack import faults
+
+
+LOG = logging.getLogger("nova.api.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):
+ """The multinic extension.
+
+ Exposes addFixedIp and removeFixedIp actions on servers.
+
+ """
+
+ def __init__(self, *args, **kwargs):
+ """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__(*args, **kwargs)
+ self.compute_api = compute.API()
+
+ def get_name(self):
+ """Return the extension name, as required by contract."""
+
+ return "Multinic"
+
+ def get_alias(self):
+ """Return the extension alias, as required by contract."""
+
+ return "NMN"
+
+ def get_description(self):
+ """Return the extension description, as required by contract."""
+
+ return "Multiple network support"
+
+ def get_namespace(self):
+ """Return the namespace, as required by contract."""
+
+ return "http://docs.openstack.org/ext/multinic/api/v1.1"
+
+ def get_updated(self):
+ """Return the last updated timestamp, as required by contract."""
+
+ return "2011-06-09T00:00:00+00:00"
+
+ 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 _add_fixed_ip(self, input_dict, req, id):
+ """Adds an IP on a given network to an instance."""
+
+ try:
+ # Validate the input entity
+ if 'networkId' not in input_dict['addFixedIp']:
+ LOG.exception(_("Missing 'networkId' argument for addFixedIp"))
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+
+ # Add the fixed IP
+ network_id = input_dict['addFixedIp']['networkId']
+ self.compute_api.add_fixed_ip(req.environ['nova.context'], id,
+ network_id)
+ except Exception, e:
+ LOG.exception(_("Error in addFixedIp %s"), e)
+ return faults.Fault(exc.HTTPBadRequest())
+ return exc.HTTPAccepted()
+
+ def _remove_fixed_ip(self, input_dict, req, id):
+ """Removes an IP from an instance."""
+
+ try:
+ # Validate the input entity
+ if 'address' not in input_dict['removeFixedIp']:
+ LOG.exception(_("Missing 'address' argument for "
+ "removeFixedIp"))
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+
+ # Remove the fixed IP
+ address = input_dict['removeFixedIp']['address']
+ self.compute_api.remove_fixed_ip(req.environ['nova.context'], id,
+ address)
+ except Exception, e:
+ LOG.exception(_("Error in removeFixedIp %s"), e)
+ return faults.Fault(exc.HTTPBadRequest())
+ return exc.HTTPAccepted()
diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py
index 436e524c1..2654e3c40 100644
--- a/nova/api/openstack/create_instance_helper.py
+++ b/nova/api/openstack/create_instance_helper.py
@@ -114,6 +114,15 @@ class CreateInstanceHelper(object):
name = name.strip()
reservation_id = body['server'].get('reservation_id')
+ min_count = body['server'].get('min_count')
+ max_count = body['server'].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
try:
inst_type = \
@@ -137,7 +146,9 @@ class CreateInstanceHelper(object):
injected_files=injected_files,
admin_password=password,
zone_blob=zone_blob,
- reservation_id=reservation_id))
+ reservation_id=reservation_id,
+ min_count=min_count,
+ max_count=max_count))
except quota.QuotaError as error:
self._handle_quota_error(error)
except exception.ImageNotFound as error:
@@ -278,7 +289,7 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
"""Deserialize an xml-formatted server create request"""
dom = minidom.parseString(string)
server = self._extract_server(dom)
- return {'server': server}
+ return {'body': {'server': server}}
def _extract_server(self, node):
"""Marshal the server attribute of a parsed request"""
diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py
index a21ff6cb2..6fab13147 100644
--- a/nova/api/openstack/flavors.py
+++ b/nova/api/openstack/flavors.py
@@ -85,8 +85,10 @@ def create_resource(version='1.0'):
'1.1': wsgi.XMLNS_V11,
}[version]
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns),
}
- return wsgi.Resource(controller, serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ return wsgi.Resource(controller, serializer=serializer)
diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py
index c0e92f2fc..4f33844fa 100644
--- a/nova/api/openstack/image_metadata.py
+++ b/nova/api/openstack/image_metadata.py
@@ -112,18 +112,18 @@ class Controller(object):
class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer):
- def __init__(self):
- xmlns = wsgi.XMLNS_V11
+ def __init__(self, xmlns=wsgi.XMLNS_V11):
super(ImageMetadataXMLSerializer, self).__init__(xmlns=xmlns)
def _meta_item_to_xml(self, doc, key, value):
node = doc.createElement('meta')
- node.setAttribute('key', key)
- text = doc.createTextNode(value)
+ doc.appendChild(node)
+ node.setAttribute('key', '%s' % key)
+ text = doc.createTextNode('%s' % value)
node.appendChild(text)
return node
- def _meta_list_to_xml(self, xml_doc, meta_items):
+ def meta_list_to_xml(self, xml_doc, meta_items):
container_node = xml_doc.createElement('metadata')
for (key, value) in meta_items:
item_node = self._meta_item_to_xml(xml_doc, key, value)
@@ -133,9 +133,10 @@ class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer):
def _meta_list_to_xml_string(self, metadata_dict):
xml_doc = minidom.Document()
items = metadata_dict['metadata'].items()
- container_node = self._meta_list_to_xml(xml_doc, items)
+ container_node = self.meta_list_to_xml(xml_doc, items)
+ xml_doc.appendChild(container_node)
self._add_xmlns(container_node)
- return container_node.toprettyxml(indent=' ')
+ return xml_doc.toprettyxml(indent=' ', encoding='UTF-8')
def index(self, metadata_dict):
return self._meta_list_to_xml_string(metadata_dict)
@@ -147,8 +148,9 @@ class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer):
xml_doc = minidom.Document()
item_key, item_value = meta_item_dict.items()[0]
item_node = self._meta_item_to_xml(xml_doc, item_key, item_value)
+ xml_doc.appendChild(item_node)
self._add_xmlns(item_node)
- return item_node.toprettyxml(indent=' ')
+ return xml_doc.toprettyxml(indent=' ', encoding='UTF-8')
def show(self, meta_item_dict):
return self._meta_item_to_xml_string(meta_item_dict['meta'])
@@ -158,8 +160,9 @@ class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer):
def create_resource():
- serializers = {
+ body_serializers = {
'application/xml': ImageMetadataXMLSerializer(),
}
+ serializer = wsgi.ResponseSerializer(body_serializers)
- return wsgi.Resource(Controller(), serializers=serializers)
+ return wsgi.Resource(Controller(), serializer=serializer)
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index d43340e10..8ff92b8fe 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -13,18 +13,20 @@
# License for the specific language governing permissions and limitations
# under the License.
+import urlparse
import os.path
import webob.exc
+from xml.dom import minidom
from nova import compute
from nova import exception
from nova import flags
import nova.image
from nova import log
-from nova import utils
from nova.api.openstack import common
from nova.api.openstack import faults
+from nova.api.openstack import image_metadata
from nova.api.openstack.views import images as images_view
from nova.api.openstack import wsgi
@@ -90,31 +92,67 @@ class Controller(object):
return webob.exc.HTTPNoContent()
def create(self, req, body):
- """Snapshot a server instance and save the image.
+ """Snapshot or backup a server instance and save the image.
+
+ 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.
:param req: `wsgi.Request` object
"""
+ def get_param(param):
+ try:
+ return body["image"][param]
+ except KeyError:
+ raise webob.exc.HTTPBadRequest(explanation="Missing required "
+ "param: %s" % param)
+
context = req.environ['nova.context']
content_type = req.get_content_type()
if not body:
raise webob.exc.HTTPBadRequest()
+ image_type = body["image"].get("image_type", "snapshot")
+
try:
server_id = self._server_id_from_req(req, body)
- image_name = body["image"]["name"]
except KeyError:
raise webob.exc.HTTPBadRequest()
+ image_name = get_param("name")
props = self._get_extra_properties(req, body)
- image = self._compute_service.snapshot(context, server_id,
- image_name, props)
+ if image_type == "snapshot":
+ image = self._compute_service.snapshot(
+ context, server_id, image_name,
+ extra_properties=props)
+ elif image_type == "backup":
+ # NOTE(sirp): Unlike snapshot, backup is not a customer facing
+ # API call; rather, it's used by the internal backup scheduler
+ if not FLAGS.allow_admin_api:
+ raise webob.exc.HTTPBadRequest(
+ explanation="Admin API Required")
+
+ backup_type = get_param("backup_type")
+ rotation = int(get_param("rotation"))
+
+ image = self._compute_service.backup(
+ context, server_id, image_name,
+ backup_type, rotation, extra_properties=props)
+ else:
+ LOG.error(_("Invalid image_type '%s' passed") % image_type)
+ raise webob.exc.HTTPBadRequest(explanation="Invalue image_type: "
+ "%s" % image_type)
+
return dict(image=self.get_builder(req).build(image, detail=True))
def get_builder(self, request):
"""Indicates that you must use a Controller subclass."""
- raise NotImplementedError
+ raise NotImplementedError()
def _server_id_from_req(self, req, data):
raise NotImplementedError()
@@ -181,9 +219,9 @@ class ControllerV11(Controller):
"""
context = req.environ['nova.context']
filters = self._get_filters(req)
- (marker, limit) = common.get_pagination_params(req)
- images = self._image_service.index(
- context, filters=filters, marker=marker, limit=limit)
+ page_params = common.get_pagination_params(req)
+ images = self._image_service.index(context, filters=filters,
+ **page_params)
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=False) for image in images])
@@ -195,9 +233,9 @@ class ControllerV11(Controller):
"""
context = req.environ['nova.context']
filters = self._get_filters(req)
- (marker, limit) = common.get_pagination_params(req)
- images = self._image_service.detail(
- context, filters=filters, marker=marker, limit=limit)
+ page_params = common.get_pagination_params(req)
+ images = self._image_service.detail(context, filters=filters,
+ **page_params)
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=True) for image in images])
@@ -208,13 +246,23 @@ class ControllerV11(Controller):
msg = _("Expected serverRef attribute on server entity.")
raise webob.exc.HTTPBadRequest(explanation=msg)
- head, tail = os.path.split(server_ref)
-
- if head and head != os.path.join(req.application_url, 'servers'):
+ if not server_ref.startswith('http'):
+ return server_ref
+
+ passed = urlparse.urlparse(server_ref)
+ expected = urlparse.urlparse(req.application_url)
+ version = expected.path.split('/')[1]
+ expected_prefix = "/%s/servers/" % version
+ _empty, _sep, server_id = passed.path.partition(expected_prefix)
+ scheme_ok = passed.scheme == expected.scheme
+ host_ok = passed.hostname == expected.hostname
+ port_ok = (passed.port == expected.port or
+ passed.port == FLAGS.osapi_port)
+ if not (scheme_ok and port_ok and host_ok and server_id):
msg = _("serverRef must match request url")
raise webob.exc.HTTPBadRequest(explanation=msg)
- return tail
+ return server_id
def _get_extra_properties(self, req, data):
server_ref = data['image']['serverRef']
@@ -224,17 +272,69 @@ class ControllerV11(Controller):
return {'instance_ref': server_ref}
+class ImageXMLSerializer(wsgi.XMLDictSerializer):
+
+ metadata = {
+ "attributes": {
+ "image": ["id", "name", "updated", "created", "status",
+ "serverId", "progress", "serverRef"],
+ "link": ["rel", "type", "href"],
+ },
+ }
+
+ xmlns = wsgi.XMLNS_V11
+
+ def __init__(self):
+ self.metadata_serializer = image_metadata.ImageMetadataXMLSerializer()
+
+ def _image_to_xml(self, xml_doc, image):
+ try:
+ metadata = image.pop('metadata').items()
+ except Exception:
+ LOG.debug(_("Image object missing metadata attribute"))
+ metadata = {}
+
+ node = self._to_xml_node(xml_doc, self.metadata, 'image', image)
+ metadata_node = self.metadata_serializer.meta_list_to_xml(xml_doc,
+ metadata)
+ node.appendChild(metadata_node)
+ return node
+
+ def _image_list_to_xml(self, xml_doc, images):
+ container_node = xml_doc.createElement('images')
+ for image in images:
+ item_node = self._image_to_xml(xml_doc, image)
+ container_node.appendChild(item_node)
+ return container_node
+
+ def _image_to_xml_string(self, image):
+ xml_doc = minidom.Document()
+ item_node = self._image_to_xml(xml_doc, image)
+ self._add_xmlns(item_node)
+ return item_node.toprettyxml(indent=' ')
+
+ def _image_list_to_xml_string(self, images):
+ xml_doc = minidom.Document()
+ container_node = self._image_list_to_xml(xml_doc, images)
+ self._add_xmlns(container_node)
+ return container_node.toprettyxml(indent=' ')
+
+ def detail(self, images_dict):
+ return self._image_list_to_xml_string(images_dict['images'])
+
+ def show(self, image_dict):
+ return self._image_to_xml_string(image_dict['image'])
+
+ def create(self, image_dict):
+ return self._image_to_xml_string(image_dict['image'])
+
+
def create_resource(version='1.0'):
controller = {
'1.0': ControllerV10,
'1.1': ControllerV11,
}[version]()
- xmlns = {
- '1.0': wsgi.XMLNS_V10,
- '1.1': wsgi.XMLNS_V11,
- }[version]
-
metadata = {
"attributes": {
"image": ["id", "name", "updated", "created", "status",
@@ -243,9 +343,15 @@ def create_resource(version='1.0'):
},
}
- serializers = {
- 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns,
- metadata=metadata),
+ xml_serializer = {
+ '1.0': wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V10),
+ '1.1': ImageXMLSerializer(),
+ }[version]
+
+ body_serializers = {
+ 'application/xml': xml_serializer,
}
- return wsgi.Resource(controller, serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ return wsgi.Resource(controller, serializer=serializer)
diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py
index 71646b6d3..23e5432d6 100644
--- a/nova/api/openstack/ips.py
+++ b/nova/api/openstack/ips.py
@@ -70,9 +70,10 @@ def create_resource():
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
xmlns=wsgi.XMLNS_V10),
}
+ serializer = wsgi.ResponseSerializer(body_serializers)
- return wsgi.Resource(Controller(), serializers=serializers)
+ return wsgi.Resource(Controller(), serializer=serializer)
diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py
index fede96e33..d08287f6b 100644
--- a/nova/api/openstack/limits.py
+++ b/nova/api/openstack/limits.py
@@ -97,12 +97,14 @@ def create_resource(version='1.0'):
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns,
metadata=metadata),
}
- return wsgi.Resource(controller, serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ return wsgi.Resource(controller, serializer=serializer)
class Limit(object):
diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py
index 8a314de22..3b9169f81 100644
--- a/nova/api/openstack/server_metadata.py
+++ b/nova/api/openstack/server_metadata.py
@@ -123,8 +123,10 @@ class Controller(object):
def create_resource():
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11),
}
- return wsgi.Resource(Controller(), serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ return wsgi.Resource(Controller(), serializer=serializer)
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index b82a6de19..206a69265 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -76,10 +76,17 @@ class Controller(object):
builder - the response model builder
"""
- reservation_id = req.str_GET.get('reservation_id')
+ query_str = req.str_GET
+ reservation_id = query_str.get('reservation_id')
+ project_id = query_str.get('project_id')
+ fixed_ip = query_str.get('fixed_ip')
+ recurse_zones = utils.bool_from_str(query_str.get('recurse_zones'))
instance_list = self.compute_api.get_all(
- req.environ['nova.context'],
- reservation_id=reservation_id)
+ req.environ['nova.context'],
+ reservation_id=reservation_id,
+ project_id=project_id,
+ fixed_ip=fixed_ip,
+ recurse_zones=recurse_zones)
limited_list = self._limit_items(instance_list, req)
builder = self._get_view_builder(req)
servers = [builder.build(inst, is_detail)['server']
@@ -111,14 +118,15 @@ class Controller(object):
extra_values = None
result = None
try:
- extra_values, result = self.helper.create_instance(
- req, body, self.compute_api.create)
+ extra_values, instances = self.helper.create_instance(
+ req, body, self.compute_api.create)
except faults.Fault, f:
return f
- instances = result
-
- (inst, ) = instances
+ # We can only return 1 instance via the API, if we happen to
+ # build more than one... instances is a list, so we'll just
+ # use the first one..
+ inst = instances[0]
for key in ['instance_type', 'image_ref']:
inst[key] = extra_values[key]
@@ -168,7 +176,7 @@ class Controller(object):
'confirmResize': self._action_confirm_resize,
'revertResize': self._action_revert_resize,
'rebuild': self._action_rebuild,
- }
+ 'migrate': self._action_migrate}
for key in actions.keys():
if key in body:
@@ -212,6 +220,14 @@ class Controller(object):
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+ def _action_migrate(self, input_dict, req, id):
+ try:
+ self.compute_api.resize(req.environ['nova.context'], id)
+ except Exception, e:
+ LOG.exception(_("Error in migrate %s"), e)
+ return faults.Fault(exc.HTTPBadRequest())
+ return exc.HTTPAccepted()
+
@scheduler_api.redirect_handler
def lock(self, req, id):
"""
@@ -608,14 +624,16 @@ def create_resource(version='1.0'):
'1.1': wsgi.XMLNS_V11,
}[version]
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
xmlns=xmlns),
}
- deserializers = {
+ body_deserializers = {
'application/xml': helper.ServerXMLDeserializer(),
}
- return wsgi.Resource(controller, serializers=serializers,
- deserializers=deserializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+ deserializer = wsgi.RequestDeserializer(body_deserializers)
+
+ return wsgi.Resource(controller, deserializer, serializer)
diff --git a/nova/api/openstack/shared_ip_groups.py b/nova/api/openstack/shared_ip_groups.py
index 4f11f8dfb..cf2ddbabb 100644
--- a/nova/api/openstack/shared_ip_groups.py
+++ b/nova/api/openstack/shared_ip_groups.py
@@ -24,27 +24,27 @@ from nova.api.openstack import wsgi
class Controller(object):
""" The Shared IP Groups Controller for the Openstack API """
- def index(self, req):
+ def index(self, req, **kwargs):
""" Returns a list of Shared IP Groups for the user """
raise faults.Fault(exc.HTTPNotImplemented())
- def show(self, req, id):
+ def show(self, req, id, **kwargs):
""" Shows in-depth information on a specific Shared IP Group """
raise faults.Fault(exc.HTTPNotImplemented())
- def update(self, req, id, body):
+ def update(self, req, id, **kwargs):
""" You can't update a Shared IP Group """
raise faults.Fault(exc.HTTPNotImplemented())
- def delete(self, req, id):
+ def delete(self, req, id, **kwargs):
""" Deletes a Shared IP Group """
raise faults.Fault(exc.HTTPNotImplemented())
- def detail(self, req):
+ def detail(self, req, **kwargs):
""" Returns a complete list of Shared IP Groups """
raise faults.Fault(exc.HTTPNotImplemented())
- def create(self, req, body):
+ def create(self, req, **kwargs):
""" Creates a new Shared IP group """
raise faults.Fault(exc.HTTPNotImplemented())
diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py
index 50975fc1f..6ae1eaf2a 100644
--- a/nova/api/openstack/users.py
+++ b/nova/api/openstack/users.py
@@ -105,8 +105,10 @@ def create_resource():
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
}
- return wsgi.Resource(Controller(), serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ return wsgi.Resource(Controller(), serializer=serializer)
diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py
index 4c682302f..a634c3267 100644
--- a/nova/api/openstack/versions.py
+++ b/nova/api/openstack/versions.py
@@ -31,11 +31,12 @@ class Versions(wsgi.Resource):
}
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
}
+ serializer = wsgi.ResponseSerializer(body_serializers)
- wsgi.Resource.__init__(self, None, serializers=serializers)
+ wsgi.Resource.__init__(self, None, serializer=serializer)
def dispatch(self, request, *args):
"""Respond to a request for all OpenStack API versions."""
diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py
index 2810cce39..b59eb4751 100644
--- a/nova/api/openstack/views/addresses.py
+++ b/nova/api/openstack/views/addresses.py
@@ -33,16 +33,18 @@ class ViewBuilderV10(ViewBuilder):
return dict(public=public_ips, private=private_ips)
def build_public_parts(self, inst):
- return utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
+ return utils.get_from_path(inst, 'fixed_ips/floating_ips/address')
def build_private_parts(self, inst):
- return utils.get_from_path(inst, 'fixed_ip/address')
+ return utils.get_from_path(inst, 'fixed_ips/address')
class ViewBuilderV11(ViewBuilder):
def build(self, inst):
- private_ips = utils.get_from_path(inst, 'fixed_ip/address')
+ # TODO(tr3buchet) - this shouldn't be hard coded to 4...
+ private_ips = utils.get_from_path(inst, 'fixed_ips/address')
private_ips = [dict(version=4, addr=a) for a in private_ips]
- public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
+ public_ips = utils.get_from_path(inst,
+ 'fixed_ips/floating_ips/address')
public_ips = [dict(version=4, addr=a) for a in public_ips]
return dict(public=public_ips, private=private_ips)
diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py
index 462890ab2..0403ece1b 100644
--- a/nova/api/openstack/views/flavors.py
+++ b/nova/api/openstack/views/flavors.py
@@ -71,6 +71,7 @@ class ViewBuilderV11(ViewBuilder):
def _build_links(self, flavor_obj):
"""Generate a container of links that refer to the provided flavor."""
href = self.generate_href(flavor_obj["id"])
+ bookmark = self.generate_bookmark(flavor_obj["id"])
links = [
{
@@ -79,13 +80,7 @@ class ViewBuilderV11(ViewBuilder):
},
{
"rel": "bookmark",
- "type": "application/json",
- "href": href,
- },
- {
- "rel": "bookmark",
- "type": "application/xml",
- "href": href,
+ "href": bookmark,
},
]
@@ -94,3 +89,10 @@ class ViewBuilderV11(ViewBuilder):
def generate_href(self, flavor_id):
"""Create an url that refers to a specific flavor id."""
return "%s/flavors/%s" % (self.base_url, flavor_id)
+
+ def generate_bookmark(self, flavor_id):
+ """Create an url that refers to a specific flavor id."""
+ return "%s/flavors/%s" % (
+ common.remove_version_from_href(self.base_url),
+ flavor_id,
+ )
diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py
index d6a054102..005341c62 100644
--- a/nova/api/openstack/views/images.py
+++ b/nova/api/openstack/views/images.py
@@ -17,6 +17,8 @@
import os.path
+from nova.api.openstack import common
+
class ViewBuilder(object):
"""Base class for generating responses to OpenStack API image requests."""
@@ -104,6 +106,10 @@ class ViewBuilderV11(ViewBuilder):
"""Return a standardized image structure for display by the API."""
image = ViewBuilder.build(self, image_obj, detail)
href = self.generate_href(image_obj["id"])
+ bookmark = self.generate_bookmark(image_obj["id"])
+
+ if detail:
+ image["metadata"] = image_obj.get("properties", {})
image["links"] = [{
"rel": "self",
@@ -111,13 +117,12 @@ class ViewBuilderV11(ViewBuilder):
},
{
"rel": "bookmark",
- "type": "application/json",
- "href": href,
- },
- {
- "rel": "bookmark",
- "type": "application/xml",
- "href": href,
+ "href": bookmark,
}]
return image
+
+ def generate_bookmark(self, image_id):
+ """Create an url that refers to a specific flavor id."""
+ return os.path.join(common.remove_version_from_href(self._url),
+ "images", str(image_id))
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
index cbfa5aae7..67fb6a84e 100644
--- a/nova/api/openstack/views/servers.py
+++ b/nova/api/openstack/views/servers.py
@@ -156,6 +156,7 @@ class ViewBuilderV11(ViewBuilder):
def _build_links(self, response, inst):
href = self.generate_href(inst["id"])
+ bookmark = self.generate_bookmark(inst["id"])
links = [
{
@@ -164,13 +165,7 @@ class ViewBuilderV11(ViewBuilder):
},
{
"rel": "bookmark",
- "type": "application/json",
- "href": href,
- },
- {
- "rel": "bookmark",
- "type": "application/xml",
- "href": href,
+ "href": bookmark,
},
]
@@ -179,3 +174,8 @@ class ViewBuilderV11(ViewBuilder):
def generate_href(self, server_id):
"""Create an url that refers to a specific server id."""
return os.path.join(self.base_url, "servers", str(server_id))
+
+ def generate_bookmark(self, server_id):
+ """Create an url that refers to a specific flavor id."""
+ return os.path.join(common.remove_version_from_href(self.base_url),
+ "servers", str(server_id))
diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py
index 5d24b4cca..8eff9e441 100644
--- a/nova/api/openstack/wsgi.py
+++ b/nova/api/openstack/wsgi.py
@@ -46,38 +46,51 @@ class Request(webob.Request):
"""
if not "Content-Type" in self.headers:
- raise exception.InvalidContentType(content_type=None)
+ return None
allowed_types = ("application/xml", "application/json")
content_type = self.content_type
if content_type not in allowed_types:
raise exception.InvalidContentType(content_type=content_type)
- else:
- return content_type
+ return content_type
-class TextDeserializer(object):
- """Custom request body deserialization based on controller action name."""
- def deserialize(self, datastring, action='default'):
- """Find local deserialization method and parse request body."""
+class ActionDispatcher(object):
+ """Maps method name to local methods through action name."""
+
+ def dispatch(self, *args, **kwargs):
+ """Find and call local method."""
+ action = kwargs.pop('action', 'default')
action_method = getattr(self, str(action), self.default)
- return action_method(datastring)
+ return action_method(*args, **kwargs)
- def default(self, datastring):
- """Default deserialization code should live here"""
+ def default(self, data):
raise NotImplementedError()
-class JSONDeserializer(TextDeserializer):
+class TextDeserializer(ActionDispatcher):
+ """Default request body deserialization"""
+
+ def deserialize(self, datastring, action='default'):
+ return self.dispatch(datastring, action=action)
def default(self, datastring):
+ return {}
+
+
+class JSONDeserializer(TextDeserializer):
+
+ def _from_json(self, datastring):
try:
return utils.loads(datastring)
except ValueError:
- raise exception.MalformedRequestBody(
- reason=_("malformed JSON in request body"))
+ msg = _("cannot understand JSON")
+ raise exception.MalformedRequestBody(reason=msg)
+
+ def default(self, datastring):
+ return {'body': self._from_json(datastring)}
class XMLDeserializer(TextDeserializer):
@@ -90,15 +103,15 @@ class XMLDeserializer(TextDeserializer):
super(XMLDeserializer, self).__init__()
self.metadata = metadata or {}
- def default(self, datastring):
+ def _from_xml(self, datastring):
plurals = set(self.metadata.get('plurals', {}))
try:
node = minidom.parseString(datastring).childNodes[0]
return {node.nodeName: self._from_xml_node(node, plurals)}
except expat.ExpatError:
- raise exception.MalformedRequestBody(
- reason=_("malformed XML in request body"))
+ msg = _("cannot understand XML")
+ raise exception.MalformedRequestBody(reason=msg)
def _from_xml_node(self, node, listnames):
"""Convert a minidom node to a simple Python type.
@@ -121,21 +134,32 @@ class XMLDeserializer(TextDeserializer):
listnames)
return result
+ def default(self, datastring):
+ return {'body': self._from_xml(datastring)}
+
+
+class RequestHeadersDeserializer(ActionDispatcher):
+ """Default request headers deserializer"""
+
+ def deserialize(self, request, action):
+ return self.dispatch(request, action=action)
+
+ def default(self, request):
+ return {}
+
class RequestDeserializer(object):
"""Break up a Request object into more useful pieces."""
- def __init__(self, deserializers=None):
- """
- :param deserializers: dictionary of content-type-specific deserializers
-
- """
- self.deserializers = {
+ def __init__(self, body_deserializers=None, headers_deserializer=None):
+ self.body_deserializers = {
'application/xml': XMLDeserializer(),
'application/json': JSONDeserializer(),
}
+ self.body_deserializers.update(body_deserializers or {})
- self.deserializers.update(deserializers or {})
+ self.headers_deserializer = headers_deserializer or \
+ RequestHeadersDeserializer()
def deserialize(self, request):
"""Extract necessary pieces of the request.
@@ -149,26 +173,42 @@ class RequestDeserializer(object):
action_args = self.get_action_args(request.environ)
action = action_args.pop('action', None)
- if request.method.lower() in ('post', 'put'):
- if len(request.body) == 0:
- action_args['body'] = None
- else:
- content_type = request.get_content_type()
- deserializer = self.get_deserializer(content_type)
-
- try:
- body = deserializer.deserialize(request.body, action)
- action_args['body'] = body
- except exception.InvalidContentType:
- action_args['body'] = None
+ action_args.update(self.deserialize_headers(request, action))
+ action_args.update(self.deserialize_body(request, action))
accept = self.get_expected_content_type(request)
return (action, action_args, accept)
- def get_deserializer(self, content_type):
+ def deserialize_headers(self, request, action):
+ return self.headers_deserializer.deserialize(request, action)
+
+ def deserialize_body(self, request, action):
try:
- return self.deserializers[content_type]
+ content_type = request.get_content_type()
+ except exception.InvalidContentType:
+ LOG.debug(_("Unrecognized Content-Type provided in request"))
+ return {}
+
+ if content_type is None:
+ LOG.debug(_("No Content-Type provided in request"))
+ return {}
+
+ if not len(request.body) > 0:
+ LOG.debug(_("Empty body provided in request"))
+ return {}
+
+ try:
+ deserializer = self.get_body_deserializer(content_type)
+ except exception.InvalidContentType:
+ LOG.debug(_("Unable to deserialize body as provided Content-Type"))
+ raise
+
+ return deserializer.deserialize(request.body, action)
+
+ def get_body_deserializer(self, content_type):
+ try:
+ return self.body_deserializers[content_type]
except (KeyError, TypeError):
raise exception.InvalidContentType(content_type=content_type)
@@ -195,20 +235,18 @@ class RequestDeserializer(object):
return args
-class DictSerializer(object):
- """Custom response body serialization based on controller action name."""
+class DictSerializer(ActionDispatcher):
+ """Default request body serialization"""
def serialize(self, data, action='default'):
- """Find local serialization method and encode response body."""
- action_method = getattr(self, str(action), self.default)
- return action_method(data)
+ return self.dispatch(data, action=action)
def default(self, data):
- """Default serialization code should live here"""
- raise NotImplementedError()
+ return ""
class JSONDictSerializer(DictSerializer):
+ """Default JSON request body serialization"""
def default(self, data):
return utils.dumps(data)
@@ -295,19 +333,28 @@ class XMLDictSerializer(DictSerializer):
return result
+class ResponseHeadersSerializer(ActionDispatcher):
+ """Default response headers serialization"""
+
+ def serialize(self, response, data, action):
+ self.dispatch(response, data, action=action)
+
+ def default(self, response, data):
+ response.status_int = 200
+
+
class ResponseSerializer(object):
"""Encode the necessary pieces into a response object"""
- def __init__(self, serializers=None):
- """
- :param serializers: dictionary of content-type-specific serializers
-
- """
- self.serializers = {
+ def __init__(self, body_serializers=None, headers_serializer=None):
+ self.body_serializers = {
'application/xml': XMLDictSerializer(),
'application/json': JSONDictSerializer(),
}
- self.serializers.update(serializers or {})
+ self.body_serializers.update(body_serializers or {})
+
+ self.headers_serializer = headers_serializer or \
+ ResponseHeadersSerializer()
def serialize(self, response_data, content_type, action='default'):
"""Serialize a dict into a string and wrap in a wsgi.Request object.
@@ -317,16 +364,21 @@ class ResponseSerializer(object):
"""
response = webob.Response()
- response.headers['Content-Type'] = content_type
+ self.serialize_headers(response, response_data, action)
+ self.serialize_body(response, response_data, content_type, action)
+ return response
- serializer = self.get_serializer(content_type)
- response.body = serializer.serialize(response_data, action)
+ def serialize_headers(self, response, data, action):
+ self.headers_serializer.serialize(response, data, action)
- return response
+ def serialize_body(self, response, data, content_type, action):
+ response.headers['Content-Type'] = content_type
+ serializer = self.get_body_serializer(content_type)
+ response.body = serializer.serialize(data, action)
- def get_serializer(self, content_type):
+ def get_body_serializer(self, content_type):
try:
- return self.serializers[content_type]
+ return self.body_serializers[content_type]
except (KeyError, TypeError):
raise exception.InvalidContentType(content_type=content_type)
@@ -343,27 +395,28 @@ class Resource(wsgi.Application):
serialized by requested content type.
"""
- def __init__(self, controller, serializers=None, deserializers=None):
+ def __init__(self, controller, deserializer=None, serializer=None):
"""
:param controller: object that implement methods created by routes lib
- :param serializers: dict of content-type specific text serializers
- :param deserializers: dict of content-type specific text deserializers
+ :param deserializer: object that can serialize the output of a
+ controller into a webob response
+ :param serializer: object that can deserialize a webob request
+ into necessary pieces
"""
self.controller = controller
- self.serializer = ResponseSerializer(serializers)
- self.deserializer = RequestDeserializer(deserializers)
+ self.deserializer = deserializer or RequestDeserializer()
+ self.serializer = serializer or ResponseSerializer()
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, request):
"""WSGI method that controls (de)serialization and method dispatch."""
- LOG.debug("%(method)s %(url)s" % {"method": request.method,
+ LOG.info("%(method)s %(url)s" % {"method": request.method,
"url": request.url})
try:
- action, action_args, accept = self.deserializer.deserialize(
- request)
+ action, args, accept = self.deserializer.deserialize(request)
except exception.InvalidContentType:
msg = _("Unsupported Content-Type")
return webob.exc.HTTPBadRequest(explanation=msg)
@@ -371,11 +424,13 @@ class Resource(wsgi.Application):
msg = _("Malformed request body")
return faults.Fault(webob.exc.HTTPBadRequest(explanation=msg))
- action_result = self.dispatch(request, action, action_args)
+ action_result = self.dispatch(request, action, args)
#TODO(bcwaldon): find a more elegant way to pass through non-dict types
- if type(action_result) is dict:
- response = self.serializer.serialize(action_result, accept, action)
+ if type(action_result) is dict or action_result is None:
+ response = self.serializer.serialize(action_result,
+ accept,
+ action=action)
else:
response = action_result
@@ -386,7 +441,7 @@ class Resource(wsgi.Application):
msg_dict = dict(url=request.url, e=e)
msg = _("%(url)s returned a fault: %(e)s" % msg_dict)
- LOG.debug(msg)
+ LOG.info(msg)
return response
@@ -394,4 +449,8 @@ class Resource(wsgi.Application):
"""Find action-spefic method on controller and call it."""
controller_method = getattr(self.controller, action)
- return controller_method(req=request, **action_args)
+ try:
+ return controller_method(req=request, **action_args)
+ except TypeError, exc:
+ LOG.debug(str(exc))
+ return webob.exc.HTTPBadRequest()
diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py
index 8864f825b..2e02ec380 100644
--- a/nova/api/openstack/zones.py
+++ b/nova/api/openstack/zones.py
@@ -196,14 +196,15 @@ def create_resource(version):
},
}
- serializers = {
+ body_serializers = {
'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10,
metadata=metadata),
}
+ serializer = wsgi.ResponseSerializer(body_serializers)
- deserializers = {
+ body_deserializers = {
'application/xml': helper.ServerXMLDeserializer(),
}
+ deserializer = wsgi.RequestDeserializer(body_deserializers)
- return wsgi.Resource(controller, serializers=serializers,
- deserializers=deserializers)
+ return wsgi.Resource(controller, deserializer, serializer)