summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
authorWilliam Wolf <throughnothing@gmail.com>2011-07-26 10:33:05 +0000
committerTarmac <>2011-07-26 10:33:05 +0000
commitcb631be09c36d29ddb6e89a647c5161bc43c4aa7 (patch)
tree5be2f5dfaeaee8dbc268f3c2f8cb26d56bd4b78d /nova/api
parent85522bba82a4139a89915bba99865a50fd9b8f58 (diff)
parent5df221e970d8b060423034fa627735c5c24fce5d (diff)
downloadnova-cb631be09c36d29ddb6e89a647c5161bc43c4aa7.tar.gz
nova-cb631be09c36d29ddb6e89a647c5161bc43c4aa7.tar.xz
nova-cb631be09c36d29ddb6e89a647c5161bc43c4aa7.zip
Merge diablo-3 development from trunk (rev1322)
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/ec2/__init__.py7
-rw-r--r--nova/api/ec2/cloud.py549
-rw-r--r--nova/api/ec2/ec2utils.py40
-rw-r--r--nova/api/ec2/metadatarequesthandler.py6
-rw-r--r--nova/api/openstack/__init__.py22
-rw-r--r--nova/api/openstack/accounts.py12
-rw-r--r--nova/api/openstack/backup_schedules.py22
-rw-r--r--nova/api/openstack/common.py74
-rw-r--r--nova/api/openstack/consoles.py22
-rw-r--r--nova/api/openstack/contrib/floating_ips.py7
-rw-r--r--nova/api/openstack/contrib/hosts.py114
-rw-r--r--nova/api/openstack/contrib/multinic.py126
-rw-r--r--nova/api/openstack/contrib/volumes.py5
-rw-r--r--nova/api/openstack/create_instance_helper.py96
-rw-r--r--nova/api/openstack/extensions.py51
-rw-r--r--nova/api/openstack/faults.py18
-rw-r--r--nova/api/openstack/flavors.py61
-rw-r--r--nova/api/openstack/image_metadata.py77
-rw-r--r--nova/api/openstack/images.py201
-rw-r--r--nova/api/openstack/ips.py125
-rw-r--r--nova/api/openstack/limits.py175
-rw-r--r--nova/api/openstack/server_metadata.py7
-rw-r--r--nova/api/openstack/servers.py232
-rw-r--r--nova/api/openstack/shared_ip_groups.py25
-rw-r--r--nova/api/openstack/users.py9
-rw-r--r--nova/api/openstack/versions.py150
-rw-r--r--nova/api/openstack/views/addresses.py59
-rw-r--r--nova/api/openstack/views/flavors.py16
-rw-r--r--nova/api/openstack/views/images.py51
-rw-r--r--nova/api/openstack/views/limits.py6
-rw-r--r--nova/api/openstack/views/servers.py29
-rw-r--r--nova/api/openstack/views/versions.py3
-rw-r--r--nova/api/openstack/wsgi.py278
-rw-r--r--nova/api/openstack/zones.py17
34 files changed, 2149 insertions, 543 deletions
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index 890d57fe7..cf1734281 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -262,6 +262,8 @@ class Authorizer(wsgi.Middleware):
'TerminateInstances': ['projectmanager', 'sysadmin'],
'RebootInstances': ['projectmanager', 'sysadmin'],
'UpdateInstance': ['projectmanager', 'sysadmin'],
+ 'StartInstances': ['projectmanager', 'sysadmin'],
+ 'StopInstances': ['projectmanager', 'sysadmin'],
'DeleteVolume': ['projectmanager', 'sysadmin'],
'DescribeImages': ['all'],
'DeregisterImage': ['projectmanager', 'sysadmin'],
@@ -269,6 +271,7 @@ class Authorizer(wsgi.Middleware):
'DescribeImageAttribute': ['all'],
'ModifyImageAttribute': ['projectmanager', 'sysadmin'],
'UpdateImage': ['projectmanager', 'sysadmin'],
+ 'CreateImage': ['projectmanager', 'sysadmin'],
},
'AdminController': {
# All actions have the same permission: ['none'] (the default)
@@ -325,13 +328,13 @@ class Executor(wsgi.Application):
except exception.VolumeNotFound as ex:
LOG.info(_('VolumeNotFound raised: %s'), unicode(ex),
context=context)
- ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x')
+ ec2_id = ec2utils.id_to_ec2_vol_id(ex.volume_id)
message = _('Volume %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
except exception.SnapshotNotFound as ex:
LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex),
context=context)
- ec2_id = ec2utils.id_to_ec2_id(ex.snapshot_id, 'snap-%08x')
+ ec2_id = ec2utils.id_to_ec2_snap_id(ex.snapshot_id)
message = _('Snapshot %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
except exception.NotFound as ex:
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 9aaf37a2d..10720a804 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -27,6 +27,7 @@ import netaddr
import os
import urllib
import tempfile
+import time
import shutil
from nova import compute
@@ -75,6 +76,95 @@ def _gen_key(context, user_id, key_name):
return {'private_key': private_key, 'fingerprint': fingerprint}
+# TODO(yamahata): hypervisor dependent default device name
+_DEFAULT_ROOT_DEVICE_NAME = '/dev/sda1'
+
+
+def _parse_block_device_mapping(bdm):
+ """Parse BlockDeviceMappingItemType into flat hash
+ BlockDevicedMapping.<N>.DeviceName
+ BlockDevicedMapping.<N>.Ebs.SnapshotId
+ BlockDevicedMapping.<N>.Ebs.VolumeSize
+ BlockDevicedMapping.<N>.Ebs.DeleteOnTermination
+ BlockDevicedMapping.<N>.Ebs.NoDevice
+ BlockDevicedMapping.<N>.VirtualName
+ => remove .Ebs and allow volume id in SnapshotId
+ """
+ ebs = bdm.pop('ebs', None)
+ if ebs:
+ ec2_id = ebs.pop('snapshot_id', None)
+ if ec2_id:
+ id = ec2utils.ec2_id_to_id(ec2_id)
+ if ec2_id.startswith('snap-'):
+ bdm['snapshot_id'] = id
+ elif ec2_id.startswith('vol-'):
+ bdm['volume_id'] = id
+ ebs.setdefault('delete_on_termination', True)
+ bdm.update(ebs)
+ return bdm
+
+
+def _properties_get_mappings(properties):
+ return ec2utils.mappings_prepend_dev(properties.get('mappings', []))
+
+
+def _format_block_device_mapping(bdm):
+ """Contruct BlockDeviceMappingItemType
+ {'device_name': '...', 'snapshot_id': , ...}
+ => BlockDeviceMappingItemType
+ """
+ keys = (('deviceName', 'device_name'),
+ ('virtualName', 'virtual_name'))
+ item = {}
+ for name, k in keys:
+ if k in bdm:
+ item[name] = bdm[k]
+ if bdm.get('no_device'):
+ item['noDevice'] = True
+ if ('snapshot_id' in bdm) or ('volume_id' in bdm):
+ ebs_keys = (('snapshotId', 'snapshot_id'),
+ ('snapshotId', 'volume_id'), # snapshotId is abused
+ ('volumeSize', 'volume_size'),
+ ('deleteOnTermination', 'delete_on_termination'))
+ ebs = {}
+ for name, k in ebs_keys:
+ if k in bdm:
+ if k == 'snapshot_id':
+ ebs[name] = ec2utils.id_to_ec2_snap_id(bdm[k])
+ elif k == 'volume_id':
+ ebs[name] = ec2utils.id_to_ec2_vol_id(bdm[k])
+ else:
+ ebs[name] = bdm[k]
+ assert 'snapshotId' in ebs
+ item['ebs'] = ebs
+ return item
+
+
+def _format_mappings(properties, result):
+ """Format multiple BlockDeviceMappingItemType"""
+ mappings = [{'virtualName': m['virtual'], 'deviceName': m['device']}
+ for m in _properties_get_mappings(properties)
+ if (m['virtual'] == 'swap' or
+ m['virtual'].startswith('ephemeral'))]
+
+ block_device_mapping = [_format_block_device_mapping(bdm) for bdm in
+ properties.get('block_device_mapping', [])]
+
+ # NOTE(yamahata): overwrite mappings with block_device_mapping
+ for bdm in block_device_mapping:
+ for i in range(len(mappings)):
+ if bdm['deviceName'] == mappings[i]['deviceName']:
+ del mappings[i]
+ break
+ mappings.append(bdm)
+
+ # NOTE(yamahata): trim ebs.no_device == true. Is this necessary?
+ mappings = [bdm for bdm in mappings if not (bdm.get('noDevice', False))]
+
+ if mappings:
+ result['blockDeviceMapping'] = mappings
+
+
class CloudController(object):
""" CloudController provides the critical dispatch between
inbound API calls through the endpoint and messages
@@ -86,8 +176,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 +210,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 +241,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 +256,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': {
@@ -177,7 +269,7 @@ class CloudController(object):
# TODO(vish): replace with real data
'ami': 'sda1',
'ephemeral0': 'sda2',
- 'root': '/dev/sda1',
+ 'root': _DEFAULT_ROOT_DEVICE_NAME,
'swap': 'sda3'},
'hostname': hostname,
'instance-action': 'none',
@@ -190,7 +282,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']:
@@ -305,9 +397,8 @@ class CloudController(object):
def _format_snapshot(self, context, snapshot):
s = {}
- s['snapshotId'] = ec2utils.id_to_ec2_id(snapshot['id'], 'snap-%08x')
- s['volumeId'] = ec2utils.id_to_ec2_id(snapshot['volume_id'],
- 'vol-%08x')
+ s['snapshotId'] = ec2utils.id_to_ec2_snap_id(snapshot['id'])
+ s['volumeId'] = ec2utils.id_to_ec2_vol_id(snapshot['volume_id'])
s['status'] = snapshot['status']
s['startTime'] = snapshot['created_at']
s['progress'] = snapshot['progress']
@@ -391,15 +482,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:
@@ -433,7 +530,52 @@ class CloudController(object):
g['ipPermissions'] += [r]
return g
- def _revoke_rule_args_to_dict(self, context, to_port=None, from_port=None,
+ def _rule_args_to_dict(self, context, kwargs):
+ rules = []
+ if not 'groups' in kwargs and not 'ip_ranges' in kwargs:
+ rule = self._rule_dict_last_step(context, **kwargs)
+ if rule:
+ rules.append(rule)
+ return rules
+ if 'ip_ranges' in kwargs:
+ rules = self._cidr_args_split(kwargs)
+ finalset = []
+ for rule in rules:
+ if 'groups' in rule:
+ groups_values = self._groups_args_split(rule)
+ for groups_value in groups_values:
+ finalset.append(groups_value)
+ else:
+ if rule:
+ finalset.append(rule)
+ return finalset
+
+ def _cidr_args_split(self, kwargs):
+ cidr_args_split = []
+ cidrs = kwargs['ip_ranges']
+ for key, cidr in cidrs.iteritems():
+ mykwargs = kwargs.copy()
+ del mykwargs['ip_ranges']
+ mykwargs['cidr_ip'] = cidr['cidr_ip']
+ cidr_args_split.append(mykwargs)
+ return cidr_args_split
+
+ def _groups_args_split(self, kwargs):
+ groups_args_split = []
+ groups = kwargs['groups']
+ for key, group in groups.iteritems():
+ mykwargs = kwargs.copy()
+ del mykwargs['groups']
+ if 'group_name' in group:
+ mykwargs['source_security_group_name'] = group['group_name']
+ if 'user_id' in group:
+ mykwargs['source_security_group_owner_id'] = group['user_id']
+ if 'group_id' in group:
+ mykwargs['source_security_group_id'] = group['group_id']
+ groups_args_split.append(mykwargs)
+ return groups_args_split
+
+ def _rule_dict_last_step(self, context, to_port=None, from_port=None,
ip_protocol=None, cidr_ip=None, user_id=None,
source_security_group_name=None,
source_security_group_owner_id=None):
@@ -497,15 +639,28 @@ 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)
-
- criteria = self._revoke_rule_args_to_dict(context, **kwargs)
+ 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._rule_args_to_dict(context, kwargs)[0]
if criteria is None:
raise exception.ApiError(_("Not enough parameters to build a "
"valid rule."))
@@ -518,7 +673,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,29 +681,54 @@ 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)
-
- values = self._revoke_rule_args_to_dict(context, **kwargs)
- if values is None:
- raise exception.ApiError(_("Not enough parameters to build a "
- "valid rule."))
- values['parent_group_id'] = security_group.id
-
- if self._security_group_rule_exists(security_group, values):
- raise exception.ApiError(_('This rule already exists in group %s')
- % group_name)
-
- security_group_rule = db.security_group_rule_create(context, values)
+ 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)
+ prevalues = []
+ try:
+ prevalues = kwargs['ip_permissions']
+ except KeyError:
+ prevalues.append(kwargs)
+ postvalues = []
+ for values in prevalues:
+ rulesvalues = self._rule_args_to_dict(context, values)
+ if not rulesvalues:
+ err = "%s Not enough parameters to build a valid rule"
+ raise exception.ApiError(_(err % rulesvalues))
+ for values_for_rule in rulesvalues:
+ values_for_rule['parent_group_id'] = security_group.id
+ if self._security_group_rule_exists(security_group,
+ values_for_rule):
+ err = '%s - This rule already exists in group'
+ raise exception.ApiError(_(err) % values_for_rule)
+ postvalues.append(values_for_rule)
+
+ for values_for_rule in postvalues:
+ security_group_rule = db.security_group_rule_create(context,
+ values_for_rule)
self.compute_api.trigger_security_group_rules_refresh(context,
- security_group['id'])
+ security_group_id=security_group['id'])
+ group = db.security_group_get_by_name(context, context.project_id,
+ security_group['name'])
return True
def _get_source_project_id(self, context, source_security_group_owner_id):
@@ -583,11 +763,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
@@ -641,7 +833,7 @@ class CloudController(object):
instance_data = '%s[%s]' % (instance_ec2_id,
volume['instance']['host'])
v = {}
- v['volumeId'] = ec2utils.id_to_ec2_id(volume['id'], 'vol-%08x')
+ v['volumeId'] = ec2utils.id_to_ec2_vol_id(volume['id'])
v['status'] = volume['status']
v['size'] = volume['size']
v['availabilityZone'] = volume['availability_zone']
@@ -663,8 +855,7 @@ class CloudController(object):
else:
v['attachmentSet'] = [{}]
if volume.get('snapshot_id') != None:
- v['snapshotId'] = ec2utils.id_to_ec2_id(volume['snapshot_id'],
- 'snap-%08x')
+ v['snapshotId'] = ec2utils.id_to_ec2_snap_id(volume['snapshot_id'])
else:
v['snapshotId'] = None
@@ -727,7 +918,7 @@ class CloudController(object):
'instanceId': ec2utils.id_to_ec2_id(instance_id),
'requestId': context.request_id,
'status': volume['attach_status'],
- 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')}
+ 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)}
def detach_volume(self, context, volume_id, **kwargs):
volume_id = ec2utils.ec2_id_to_id(volume_id)
@@ -739,7 +930,7 @@ class CloudController(object):
'instanceId': ec2utils.id_to_ec2_id(instance['id']),
'requestId': context.request_id,
'status': volume['attach_status'],
- 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')}
+ 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)}
def _convert_to_set(self, lst, label):
if lst is None or lst == []:
@@ -763,6 +954,37 @@ class CloudController(object):
assert len(i) == 1
return i[0]
+ def _format_instance_bdm(self, context, instance_id, root_device_name,
+ result):
+ """Format InstanceBlockDeviceMappingResponseItemType"""
+ root_device_type = 'instance-store'
+ mapping = []
+ for bdm in db.block_device_mapping_get_all_by_instance(context,
+ instance_id):
+ volume_id = bdm['volume_id']
+ if (volume_id is None or bdm['no_device']):
+ continue
+
+ if (bdm['device_name'] == root_device_name and
+ (bdm['snapshot_id'] or bdm['volume_id'])):
+ assert not bdm['virtual_name']
+ root_device_type = 'ebs'
+
+ vol = self.volume_api.get(context, volume_id=volume_id)
+ LOG.debug(_("vol = %s\n"), vol)
+ # TODO(yamahata): volume attach time
+ ebs = {'volumeId': volume_id,
+ 'deleteOnTermination': bdm['delete_on_termination'],
+ 'attachTime': vol['attach_time'] or '-',
+ 'status': vol['status'], }
+ res = {'deviceName': bdm['device_name'],
+ 'ebs': ebs, }
+ mapping.append(res)
+
+ if mapping:
+ result['blockDeviceMapping'] = mapping
+ result['rootDeviceType'] = root_device_type
+
def _format_instances(self, context, instance_id=None, **kwargs):
# TODO(termie): this method is poorly named as its name does not imply
# that it will be making a variety of database calls
@@ -793,15 +1015,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
@@ -824,6 +1046,10 @@ class CloudController(object):
i['amiLaunchIndex'] = instance['launch_index']
i['displayName'] = instance['display_name']
i['displayDescription'] = instance['display_description']
+ i['rootDeviceName'] = (instance.get('root_device_name') or
+ _DEFAULT_ROOT_DEVICE_NAME)
+ self._format_instance_bdm(context, instance_id,
+ i['rootDeviceName'], i)
host = instance['host']
zone = self._get_availability_zone_by_host(context, host)
i['placement'] = {'availabilityZone': zone}
@@ -877,7 +1103,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
@@ -910,23 +1137,7 @@ class CloudController(object):
ramdisk = self._get_image(context, kwargs['ramdisk_id'])
kwargs['ramdisk_id'] = ramdisk['id']
for bdm in kwargs.get('block_device_mapping', []):
- # NOTE(yamahata)
- # BlockDevicedMapping.<N>.DeviceName
- # BlockDevicedMapping.<N>.Ebs.SnapshotId
- # BlockDevicedMapping.<N>.Ebs.VolumeSize
- # BlockDevicedMapping.<N>.Ebs.DeleteOnTermination
- # BlockDevicedMapping.<N>.VirtualName
- # => remove .Ebs and allow volume id in SnapshotId
- ebs = bdm.pop('ebs', None)
- if ebs:
- ec2_id = ebs.pop('snapshot_id')
- id = ec2utils.ec2_id_to_id(ec2_id)
- if ec2_id.startswith('snap-'):
- bdm['snapshot_id'] = id
- elif ec2_id.startswith('vol-'):
- bdm['volume_id'] = id
- ebs.setdefault('delete_on_termination', True)
- bdm.update(ebs)
+ _parse_block_device_mapping(bdm)
image = self._get_image(context, kwargs['image_id'])
@@ -994,7 +1205,7 @@ class CloudController(object):
def rescue_instance(self, context, instance_id, **kwargs):
"""This is an extension to the normal ec2_api"""
- self._do_instance(self.compute_api.rescue, contect, instnace_id)
+ self._do_instance(self.compute_api.rescue, context, instance_id)
return True
def unrescue_instance(self, context, instance_id, **kwargs):
@@ -1045,12 +1256,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."""
@@ -1081,6 +1296,20 @@ class CloudController(object):
i['imageType'] = display_mapping.get(image_type)
i['isPublic'] = image.get('is_public') == True
i['architecture'] = image['properties'].get('architecture')
+
+ properties = image['properties']
+ root_device_name = ec2utils.properties_root_device_name(properties)
+ root_device_type = 'instance-store'
+ for bdm in properties.get('block_device_mapping', []):
+ if (bdm.get('device_name') == root_device_name and
+ ('snapshot_id' in bdm or 'volume_id' in bdm) and
+ not bdm.get('no_device')):
+ root_device_type = 'ebs'
+ i['rootDeviceName'] = (root_device_name or _DEFAULT_ROOT_DEVICE_NAME)
+ i['rootDeviceType'] = root_device_type
+
+ _format_mappings(properties, i)
+
return i
def describe_images(self, context, image_id=None, **kwargs):
@@ -1105,30 +1334,64 @@ class CloudController(object):
self.image_service.delete(context, internal_id)
return {'imageId': image_id}
+ def _register_image(self, context, metadata):
+ image = self.image_service.create(context, metadata)
+ image_type = self._image_type(image.get('container_format'))
+ image_id = self.image_ec2_id(image['id'], image_type)
+ return image_id
+
def register_image(self, context, image_location=None, **kwargs):
if image_location is None and 'name' in kwargs:
image_location = kwargs['name']
metadata = {'properties': {'image_location': image_location}}
- image = self.image_service.create(context, metadata)
- image_type = self._image_type(image.get('container_format'))
- image_id = self.image_ec2_id(image['id'],
- image_type)
+
+ if 'root_device_name' in kwargs:
+ metadata['properties']['root_device_name'] = \
+ kwargs.get('root_device_name')
+
+ mappings = [_parse_block_device_mapping(bdm) for bdm in
+ kwargs.get('block_device_mapping', [])]
+ if mappings:
+ metadata['properties']['block_device_mapping'] = mappings
+
+ image_id = self._register_image(context, metadata)
msg = _("Registered image %(image_location)s with"
" id %(image_id)s") % locals()
LOG.audit(msg, context=context)
return {'imageId': image_id}
def describe_image_attribute(self, context, image_id, attribute, **kwargs):
- if attribute != 'launchPermission':
+ def _block_device_mapping_attribute(image, result):
+ _format_mappings(image['properties'], result)
+
+ def _launch_permission_attribute(image, result):
+ result['launchPermission'] = []
+ if image['is_public']:
+ result['launchPermission'].append({'group': 'all'})
+
+ def _root_device_name_attribute(image, result):
+ result['rootDeviceName'] = \
+ ec2utils.properties_root_device_name(image['properties'])
+ if result['rootDeviceName'] is None:
+ result['rootDeviceName'] = _DEFAULT_ROOT_DEVICE_NAME
+
+ supported_attributes = {
+ 'blockDeviceMapping': _block_device_mapping_attribute,
+ 'launchPermission': _launch_permission_attribute,
+ 'rootDeviceName': _root_device_name_attribute,
+ }
+
+ fn = supported_attributes.get(attribute)
+ if fn is None:
raise exception.ApiError(_('attribute not supported: %s')
% attribute)
try:
image = self._get_image(context, image_id)
except exception.NotFound:
raise exception.ImageNotFound(image_id=image_id)
- result = {'imageId': image_id, 'launchPermission': []}
- if image['is_public']:
- result['launchPermission'].append({'group': 'all'})
+
+ result = {'imageId': image_id}
+ fn(image, result)
return result
def modify_image_attribute(self, context, image_id, attribute,
@@ -1159,3 +1422,109 @@ class CloudController(object):
internal_id = ec2utils.ec2_id_to_id(image_id)
result = self.image_service.update(context, internal_id, dict(kwargs))
return result
+
+ # TODO(yamahata): race condition
+ # At the moment there is no way to prevent others from
+ # manipulating instances/volumes/snapshots.
+ # As other code doesn't take it into consideration, here we don't
+ # care of it for now. Ostrich algorithm
+ def create_image(self, context, instance_id, **kwargs):
+ # NOTE(yamahata): name/description are ignored by register_image(),
+ # do so here
+ no_reboot = kwargs.get('no_reboot', False)
+
+ ec2_instance_id = instance_id
+ instance_id = ec2utils.ec2_id_to_id(ec2_instance_id)
+ instance = self.compute_api.get(context, instance_id)
+
+ # stop the instance if necessary
+ restart_instance = False
+ if not no_reboot:
+ state_description = instance['state_description']
+
+ # if the instance is in subtle state, refuse to proceed.
+ if state_description not in ('running', 'stopping', 'stopped'):
+ raise exception.InstanceNotRunning(instance_id=ec2_instance_id)
+
+ if state_description == 'running':
+ restart_instance = True
+ self.compute_api.stop(context, instance_id=instance_id)
+
+ # wait instance for really stopped
+ start_time = time.time()
+ while state_description != 'stopped':
+ time.sleep(1)
+ instance = self.compute_api.get(context, instance_id)
+ state_description = instance['state_description']
+ # NOTE(yamahata): timeout and error. 1 hour for now for safety.
+ # Is it too short/long?
+ # Or is there any better way?
+ timeout = 1 * 60 * 60 * 60
+ if time.time() > start_time + timeout:
+ raise exception.ApiError(
+ _('Couldn\'t stop instance with in %d sec') % timeout)
+
+ src_image = self._get_image(context, instance['image_ref'])
+ properties = src_image['properties']
+ if instance['root_device_name']:
+ properties['root_device_name'] = instance['root_device_name']
+
+ mapping = []
+ bdms = db.block_device_mapping_get_all_by_instance(context,
+ instance_id)
+ for bdm in bdms:
+ if bdm.no_device:
+ continue
+ m = {}
+ for attr in ('device_name', 'snapshot_id', 'volume_id',
+ 'volume_size', 'delete_on_termination', 'no_device',
+ 'virtual_name'):
+ val = getattr(bdm, attr)
+ if val is not None:
+ m[attr] = val
+
+ volume_id = m.get('volume_id')
+ if m.get('snapshot_id') and volume_id:
+ # create snapshot based on volume_id
+ vol = self.volume_api.get(context, volume_id=volume_id)
+ # NOTE(yamahata): Should we wait for snapshot creation?
+ # Linux LVM snapshot creation completes in
+ # short time, it doesn't matter for now.
+ snapshot = self.volume_api.create_snapshot_force(
+ context, volume_id=volume_id, name=vol['display_name'],
+ description=vol['display_description'])
+ m['snapshot_id'] = snapshot['id']
+ del m['volume_id']
+
+ if m:
+ mapping.append(m)
+
+ for m in _properties_get_mappings(properties):
+ virtual_name = m['virtual']
+ if virtual_name in ('ami', 'root'):
+ continue
+
+ assert (virtual_name == 'swap' or
+ virtual_name.startswith('ephemeral'))
+ device_name = m['device']
+ if device_name in [b['device_name'] for b in mapping
+ if not b.get('no_device', False)]:
+ continue
+
+ # NOTE(yamahata): swap and ephemeral devices are specified in
+ # AMI, but disabled for this instance by user.
+ # So disable those device by no_device.
+ mapping.append({'device_name': device_name, 'no_device': True})
+
+ if mapping:
+ properties['block_device_mapping'] = mapping
+
+ for attr in ('status', 'location', 'id'):
+ src_image.pop(attr, None)
+
+ image_id = self._register_image(context, src_image)
+
+ if restart_instance:
+ self.compute_api.start(context, instance_id=instance_id)
+
+ return {'imageId': image_id}
diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py
index 222e1de1e..bae1e0ee5 100644
--- a/nova/api/ec2/ec2utils.py
+++ b/nova/api/ec2/ec2utils.py
@@ -34,6 +34,17 @@ def id_to_ec2_id(instance_id, template='i-%08x'):
return template % instance_id
+def id_to_ec2_snap_id(instance_id):
+ """Convert an snapshot ID (int) to an ec2 snapshot ID
+ (snap-[base 16 number])"""
+ return id_to_ec2_id(instance_id, 'snap-%08x')
+
+
+def id_to_ec2_vol_id(instance_id):
+ """Convert an volume ID (int) to an ec2 volume ID (vol-[base 16 number])"""
+ return id_to_ec2_id(instance_id, 'vol-%08x')
+
+
_c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
@@ -124,3 +135,32 @@ def dict_from_dotted_str(items):
args[key] = value
return args
+
+
+def properties_root_device_name(properties):
+ """get root device name from image meta data.
+ If it isn't specified, return None.
+ """
+ root_device_name = None
+
+ # NOTE(yamahata): see image_service.s3.s3create()
+ for bdm in properties.get('mappings', []):
+ if bdm['virtual'] == 'root':
+ root_device_name = bdm['device']
+
+ # NOTE(yamahata): register_image's command line can override
+ # <machine>.manifest.xml
+ if 'root_device_name' in properties:
+ root_device_name = properties['root_device_name']
+
+ return root_device_name
+
+
+def mappings_prepend_dev(mappings):
+ """Prepend '/dev/' to 'device' entry of swap/ephemeral virtual type"""
+ for m in mappings:
+ virtual = m['virtual']
+ if ((virtual == 'swap' or virtual.startswith('ephemeral')) and
+ (not m['device'].startswith('/'))):
+ m['device'] = '/dev/' + m['device']
+ return mappings
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/__init__.py b/nova/api/openstack/__init__.py
index f24017df0..868b98a31 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -125,6 +125,10 @@ class APIRouter(base_wsgi.Router):
collection={'detail': 'GET'},
member=self.server_members)
+ mapper.resource("ip", "ips", controller=ips.create_resource(version),
+ parent_resource=dict(member_name='server',
+ collection_name='servers'))
+
mapper.resource("image", "images",
controller=images.create_resource(version),
collection={'detail': 'GET'})
@@ -144,9 +148,6 @@ class APIRouterV10(APIRouter):
def _setup_routes(self, mapper):
super(APIRouterV10, self)._setup_routes(mapper, '1.0')
- mapper.resource("image", "images",
- controller=images.create_resource('1.0'),
- collection={'detail': 'GET'})
mapper.resource("shared_ip_group", "shared_ip_groups",
collection={'detail': 'GET'},
@@ -157,22 +158,23 @@ class APIRouterV10(APIRouter):
parent_resource=dict(member_name='server',
collection_name='servers'))
- mapper.resource("ip", "ips", controller=ips.create_resource(),
- collection=dict(public='GET', private='GET'),
- parent_resource=dict(member_name='server',
- collection_name='servers'))
-
class APIRouterV11(APIRouter):
"""Define routes specific to OpenStack API V1.1."""
def _setup_routes(self, mapper):
super(APIRouterV11, self)._setup_routes(mapper, '1.1')
- mapper.resource("image_meta", "meta",
- controller=image_metadata.create_resource(),
+ 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", "/images/{image_id}/metadata",
+ controller=image_metadata_controller,
+ action='update_all',
+ conditions={"method": ['PUT']})
+
mapper.resource("server_meta", "meta",
controller=server_metadata.create_resource(),
parent_resource=dict(member_name='server',
diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py
index 0dcd37217..a13a758ab 100644
--- a/nova/api/openstack/accounts.py
+++ b/nova/api/openstack/accounts.py
@@ -47,10 +47,10 @@ class Controller(object):
raise exception.AdminRequired()
def index(self, req):
- raise faults.Fault(webob.exc.HTTPNotImplemented())
+ raise webob.exc.HTTPNotImplemented()
def detail(self, req):
- raise faults.Fault(webob.exc.HTTPNotImplemented())
+ raise webob.exc.HTTPNotImplemented()
def show(self, req, id):
"""Return data about the given account id"""
@@ -65,7 +65,7 @@ class Controller(object):
def create(self, req, body):
"""We use update with create-or-update semantics
because the id comes from an external source"""
- raise faults.Fault(webob.exc.HTTPNotImplemented())
+ raise webob.exc.HTTPNotImplemented()
def update(self, req, id, body):
"""This is really create or update."""
@@ -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..7ff0d999e 100644
--- a/nova/api/openstack/backup_schedules.py
+++ b/nova/api/openstack/backup_schedules.py
@@ -19,7 +19,6 @@ import time
from webob import exc
-from nova.api.openstack import faults
from nova.api.openstack import wsgi
@@ -34,22 +33,22 @@ 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())
+ raise 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())
+ raise 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())
+ raise 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())
+ raise exc.HTTPNotImplemented()
def create_resource():
@@ -59,9 +58,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 aa8911b62..bd14a1389 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -53,10 +53,10 @@ def get_pagination_params(request):
params[param] = int(request.GET[param])
except ValueError:
msg = _('%s param must be an integer') % param
- raise webob.exc.HTTPBadRequest(msg)
+ raise webob.exc.HTTPBadRequest(explanation=msg)
if params[param] < 0:
msg = _('%s param must be positive') % param
- raise webob.exc.HTTPBadRequest(msg)
+ raise webob.exc.HTTPBadRequest(explanation=msg)
return params
@@ -77,18 +77,22 @@ def limited(items, request, max_limit=FLAGS.osapi_max_limit):
try:
offset = int(request.GET.get('offset', 0))
except ValueError:
- raise webob.exc.HTTPBadRequest(_('offset param must be an integer'))
+ msg = _('offset param must be an integer')
+ raise webob.exc.HTTPBadRequest(explanation=msg)
try:
limit = int(request.GET.get('limit', max_limit))
except ValueError:
- raise webob.exc.HTTPBadRequest(_('limit param must be an integer'))
+ msg = _('limit param must be an integer')
+ raise webob.exc.HTTPBadRequest(explanation=msg)
if limit < 0:
- raise webob.exc.HTTPBadRequest(_('limit param must be positive'))
+ msg = _('limit param must be positive')
+ raise webob.exc.HTTPBadRequest(explanation=msg)
if offset < 0:
- raise webob.exc.HTTPBadRequest(_('offset param must be positive'))
+ msg = _('offset param must be positive')
+ raise webob.exc.HTTPBadRequest(explanation=msg)
limit = min(max_limit, limit or max_limit)
range_end = offset + limit
@@ -111,7 +115,8 @@ def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit):
start_index = i + 1
break
if start_index < 0:
- raise webob.exc.HTTPBadRequest(_('marker [%s] not found' % marker))
+ msg = _('marker [%s] not found') % marker
+ raise webob.exc.HTTPBadRequest(explanation=msg)
range_end = start_index + limit
return items[start_index:range_end]
@@ -133,4 +138,57 @@ def get_id_from_href(href):
return int(urlparse(href).path.split('/')[-1])
except:
LOG.debug(_("Error extracting id from href: %s") % href)
- raise webob.exc.HTTPBadRequest(_('could not parse id from href'))
+ raise ValueError(_('could not parse id from href'))
+
+
+def remove_version_from_href(href):
+ """Removes the first api version from the href.
+
+ Given: 'http://www.nova.com/v1.1/123'
+ Returns: 'http://www.nova.com/123'
+
+ Given: 'http://www.nova.com/v1.1'
+ Returns: 'http://www.nova.com'
+
+ """
+ try:
+ #removes the first instance that matches /v#.#/
+ new_href = re.sub(r'[/][v][0-9]+\.[0-9]+[/]', '/', href, count=1)
+
+ #if no version was found, try finding /v#.# at the end of the string
+ if new_href == href:
+ new_href = re.sub(r'[/][v][0-9]+\.[0-9]+$', '', href, count=1)
+ except:
+ LOG.debug(_("Error removing version from href: %s") % href)
+ msg = _('could not parse version from href')
+ raise ValueError(msg)
+
+ if new_href == href:
+ msg = _('href does not contain version')
+ raise ValueError(msg)
+ return new_href
+
+
+def get_version_from_href(href):
+ """Returns the api version in the href.
+
+ Returns the api version in the href.
+ If no version is found, 1.0 is returned
+
+ Given: 'http://www.nova.com/123'
+ Returns: '1.0'
+
+ Given: 'http://www.nova.com/v1.1'
+ Returns: '1.1'
+
+ """
+ try:
+ #finds the first instance that matches /v#.#/
+ version = re.findall(r'[/][v][0-9]+\.[0-9]+[/]', href)
+ #if no version was found, try finding /v#.# at the end of the string
+ if not version:
+ version = re.findall(r'[/][v][0-9]+\.[0-9]+$', href)
+ version = re.findall(r'[0-9]+\.[0-9]', version[0])[0]
+ except IndexError:
+ version = '1.0'
+ return version
diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py
index bccf04d8f..d2655acfa 100644
--- a/nova/api/openstack/consoles.py
+++ b/nova/api/openstack/consoles.py
@@ -16,10 +16,10 @@
# under the License.
from webob import exc
+import webob
from nova import console
from nova import exception
-from nova.api.openstack import faults
from nova.api.openstack import wsgi
@@ -71,12 +71,12 @@ class Controller(object):
int(server_id),
int(id))
except exception.NotFound:
- return faults.Fault(exc.HTTPNotFound())
+ raise exc.HTTPNotFound()
return _translate_detail_keys(console)
def update(self, req, server_id, id):
"""You can't update a console"""
- raise faults.Fault(exc.HTTPNotImplemented())
+ raise exc.HTTPNotImplemented()
def delete(self, req, server_id, id):
"""Deletes a console"""
@@ -85,19 +85,9 @@ class Controller(object):
int(server_id),
int(id))
except exception.NotFound:
- return faults.Fault(exc.HTTPNotFound())
- return exc.HTTPAccepted()
+ raise exc.HTTPNotFound()
+ return webob.Response(status_int=202)
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/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py
index 914ec5bfb..b4a211857 100644
--- a/nova/api/openstack/contrib/floating_ips.py
+++ b/nova/api/openstack/contrib/floating_ips.py
@@ -78,14 +78,15 @@ class FloatingIPController(object):
return _translate_floating_ips_view(floating_ips)
- def create(self, req, body):
+ 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:
- if ex.exc_type == 'NoMoreAddresses':
+ # NOTE(tr3buchet) - why does this block exist?
+ if ex.exc_type == 'NoMoreFloatingIps':
raise exception.NoMoreFloatingIps()
else:
raise
@@ -123,7 +124,7 @@ class FloatingIPController(object):
"floating_ip": floating_ip,
"fixed_ip": fixed_ip}}
- def disassociate(self, req, id, body):
+ 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)
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..da8dcee5d
--- /dev/null
+++ b/nova/api/openstack/contrib/multinic.py
@@ -0,0 +1,126 @@
+# 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
+import webob
+
+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 webob.Response(status_int=202)
+
+ 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 webob.Response(status_int=202)
diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py
index e5e2c5b50..827e36097 100644
--- a/nova/api/openstack/contrib/volumes.py
+++ b/nova/api/openstack/contrib/volumes.py
@@ -16,6 +16,7 @@
"""The volumes extension."""
from webob import exc
+import webob
from nova import compute
from nova import exception
@@ -104,7 +105,7 @@ class VolumeController(object):
self.volume_api.delete(context, volume_id=id)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
- return exc.HTTPAccepted()
+ return webob.Response(status_int=202)
def index(self, req):
"""Returns a summary list of volumes."""
@@ -279,7 +280,7 @@ class VolumeAttachmentController(object):
self.compute_api.detach_volume(context,
volume_id=volume_id)
- return exc.HTTPAccepted()
+ return webob.Response(status_int=202)
def _items(self, req, server_id, entity_maker):
"""Returns a list of attachments, transformed through entity_maker."""
diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py
index 436e524c1..f8317565e 100644
--- a/nova/api/openstack/create_instance_helper.py
+++ b/nova/api/openstack/create_instance_helper.py
@@ -28,7 +28,6 @@ from nova import quota
from nova import utils
from nova.compute import instance_types
-from nova.api.openstack import faults
from nova.api.openstack import wsgi
from nova.auth import manager as auth_manager
@@ -70,11 +69,14 @@ class CreateInstanceHelper(object):
return type from this method is left to the caller.
"""
if not body:
- raise faults.Fault(exc.HTTPUnprocessableEntity())
+ raise exc.HTTPUnprocessableEntity()
- context = req.environ['nova.context']
+ if not 'server' in body:
+ raise exc.HTTPUnprocessableEntity()
- password = self.controller._get_server_admin_password(body['server'])
+ server_dict = body['server']
+ context = req.environ['nova.context']
+ password = self.controller._get_server_admin_password(server_dict)
key_name = None
key_data = None
@@ -94,26 +96,39 @@ class CreateInstanceHelper(object):
except Exception, e:
msg = _("Cannot find requested image %(image_href)s: %(e)s" %
locals())
- raise faults.Fault(exc.HTTPBadRequest(explanation=msg))
+ raise exc.HTTPBadRequest(explanation=msg)
- personality = body['server'].get('personality')
+ personality = server_dict.get('personality')
injected_files = []
if personality:
injected_files = self._get_injected_files(personality)
- flavor_id = self.controller._flavor_id_from_req_data(body)
+ try:
+ flavor_id = self.controller._flavor_id_from_req_data(body)
+ except ValueError as error:
+ msg = _("Invalid flavorRef provided.")
+ raise exc.HTTPBadRequest(explanation=msg)
- if not 'name' in body['server']:
+ if not 'name' in server_dict:
msg = _("Server name is not defined")
raise exc.HTTPBadRequest(explanation=msg)
- zone_blob = body['server'].get('blob')
- name = body['server']['name']
+ zone_blob = server_dict.get('blob')
+ name = server_dict['name']
self._validate_server_name(name)
name = name.strip()
- reservation_id = body['server'].get('reservation_id')
+ reservation_id = server_dict.get('reservation_id')
+ 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
try:
inst_type = \
@@ -133,17 +148,21 @@ class CreateInstanceHelper(object):
display_description=name,
key_name=key_name,
key_data=key_data,
- metadata=body['server'].get('metadata', {}),
+ metadata=server_dict.get('metadata', {}),
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:
msg = _("Can not find requested image")
- raise faults.Fault(exc.HTTPBadRequest(explanation=msg))
-
+ raise exc.HTTPBadRequest(explanation=msg)
+ except exception.FlavorNotFound as error:
+ msg = _("Invalid flavorRef provided.")
+ raise exc.HTTPBadRequest(explanation=msg)
# Let the caller deal with unhandled exceptions.
def _handle_quota_error(self, error):
@@ -266,7 +285,7 @@ class CreateInstanceHelper(object):
return password
-class ServerXMLDeserializer(wsgi.XMLDeserializer):
+class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer):
"""
Deserializer to handle xml-formatted server create requests.
@@ -278,16 +297,17 @@ 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"""
server = {}
- server_node = self._find_first_child_named(node, 'server')
+ server_node = self.find_first_child_named(node, 'server')
for attr in ["name", "imageId", "flavorId", "imageRef", "flavorRef"]:
if server_node.getAttribute(attr):
server[attr] = server_node.getAttribute(attr)
- metadata = self._extract_metadata(server_node)
+ metadata_node = self.find_first_child_named(server_node, "metadata")
+ metadata = self.extract_metadata(metadata_node)
if metadata is not None:
server["metadata"] = metadata
personality = self._extract_personality(server_node)
@@ -295,49 +315,17 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer):
server["personality"] = personality
return server
- def _extract_metadata(self, server_node):
- """Marshal the metadata attribute of a parsed request"""
- metadata_node = self._find_first_child_named(server_node, "metadata")
- if metadata_node is None:
- return None
- metadata = {}
- for meta_node in self._find_children_named(metadata_node, "meta"):
- key = meta_node.getAttribute("key")
- metadata[key] = self._extract_text(meta_node)
- return metadata
-
def _extract_personality(self, server_node):
"""Marshal the personality attribute of a parsed request"""
personality_node = \
- self._find_first_child_named(server_node, "personality")
+ self.find_first_child_named(server_node, "personality")
if personality_node is None:
return None
personality = []
- for file_node in self._find_children_named(personality_node, "file"):
+ for file_node in self.find_children_named(personality_node, "file"):
item = {}
if file_node.hasAttribute("path"):
item["path"] = file_node.getAttribute("path")
- item["contents"] = self._extract_text(file_node)
+ item["contents"] = self.extract_text(file_node)
personality.append(item)
return personality
-
- def _find_first_child_named(self, parent, name):
- """Search a nodes children for the first child with a given name"""
- for node in parent.childNodes:
- if node.nodeName == name:
- return node
- return None
-
- def _find_children_named(self, parent, name):
- """Return all of a nodes children who have the given name"""
- for node in parent.childNodes:
- if node.nodeName == name:
- yield node
-
- def _extract_text(self, node):
- """Get the text field contained by the given node"""
- if len(node.childNodes) == 1:
- child = node.childNodes[0]
- if child.nodeType == child.TEXT_NODE:
- return child.nodeValue
- return ""
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
index da06ecd15..cc889703e 100644
--- a/nova/api/openstack/extensions.py
+++ b/nova/api/openstack/extensions.py
@@ -23,6 +23,7 @@ import sys
import routes
import webob.dec
import webob.exc
+from xml.etree import ElementTree
from nova import exception
from nova import flags
@@ -194,7 +195,7 @@ class ExtensionsResource(wsgi.Resource):
def show(self, req, id):
# NOTE(dprince): the extensions alias is used as the 'id' for show
ext = self.extension_manager.extensions[id]
- return self._translate(ext)
+ return dict(extension=self._translate(ext))
def delete(self, req, id):
raise faults.Fault(webob.exc.HTTPNotFound())
@@ -258,15 +259,18 @@ class ExtensionMiddleware(base_wsgi.Middleware):
mapper = routes.Mapper()
+ serializer = wsgi.ResponseSerializer(
+ {'application/xml': ExtensionsXMLSerializer()})
# extended resources
for resource in ext_mgr.get_resources():
LOG.debug(_('Extended resource: %s'),
resource.collection)
mapper.resource(resource.collection, resource.collection,
- controller=wsgi.Resource(resource.controller),
- collection=resource.collection_actions,
- member=resource.member_actions,
- parent_resource=resource.parent)
+ controller=wsgi.Resource(
+ resource.controller, serializer=serializer),
+ collection=resource.collection_actions,
+ member=resource.member_actions,
+ parent_resource=resource.parent)
# extended actions
action_resources = self._action_ext_resources(application, ext_mgr,
@@ -462,3 +466,40 @@ class ResourceExtension(object):
self.parent = parent
self.collection_actions = collection_actions
self.member_actions = member_actions
+
+
+class ExtensionsXMLSerializer(wsgi.XMLDictSerializer):
+
+ def show(self, ext_dict):
+ ext = self._create_ext_elem(ext_dict['extension'])
+ return self._to_xml(ext)
+
+ def index(self, exts_dict):
+ exts = ElementTree.Element('extensions')
+ for ext_dict in exts_dict['extensions']:
+ exts.append(self._create_ext_elem(ext_dict))
+ return self._to_xml(exts)
+
+ def _create_ext_elem(self, ext_dict):
+ """Create an extension xml element from a dict."""
+ ext_elem = ElementTree.Element('extension')
+ ext_elem.set('name', ext_dict['name'])
+ ext_elem.set('namespace', ext_dict['namespace'])
+ ext_elem.set('alias', ext_dict['alias'])
+ ext_elem.set('updated', ext_dict['updated'])
+ desc = ElementTree.Element('description')
+ desc.text = ext_dict['description']
+ ext_elem.append(desc)
+ for link in ext_dict.get('links', []):
+ elem = ElementTree.Element('atom:link')
+ elem.set('rel', link['rel'])
+ elem.set('href', link['href'])
+ elem.set('type', link['type'])
+ ext_elem.append(elem)
+ return ext_elem
+
+ def _to_xml(self, root):
+ """Convert the xml tree object to an xml string."""
+ root.set('xmlns', wsgi.XMLNS_V11)
+ root.set('xmlns:atom', wsgi.XMLNS_ATOM)
+ return ElementTree.tostring(root, encoding='UTF-8')
diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py
index b9a23c126..1ab45d4f1 100644
--- a/nova/api/openstack/faults.py
+++ b/nova/api/openstack/faults.py
@@ -19,6 +19,7 @@
import webob.dec
import webob.exc
+from nova.api.openstack import common
from nova.api.openstack import wsgi
@@ -40,6 +41,7 @@ class Fault(webob.exc.HTTPException):
def __init__(self, exception):
"""Create a Fault for the given webob.exc.exception."""
self.wrapped_exc = exception
+ self.status_int = exception.status_int
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
@@ -60,9 +62,13 @@ class Fault(webob.exc.HTTPException):
content_type = req.best_match_content_type()
+ xml_serializer = {
+ '1.0': wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V10),
+ '1.1': wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V11),
+ }[common.get_version_from_href(req.url)]
+
serializer = {
- 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
- xmlns=wsgi.XMLNS_V10),
+ 'application/xml': xml_serializer,
'application/json': wsgi.JSONDictSerializer(),
}[content_type]
@@ -99,9 +105,13 @@ class OverLimitFault(webob.exc.HTTPException):
content_type = request.best_match_content_type()
metadata = {"attributes": {"overLimitFault": "code"}}
+ xml_serializer = {
+ '1.0': wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V10),
+ '1.1': wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V11),
+ }[common.get_version_from_href(request.url)]
+
serializer = {
- 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
- xmlns=wsgi.XMLNS_V10),
+ 'application/xml': xml_serializer,
'application/json': wsgi.JSONDictSerializer(),
}[content_type]
diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py
index a21ff6cb2..b4bda68d4 100644
--- a/nova/api/openstack/flavors.py
+++ b/nova/api/openstack/flavors.py
@@ -16,6 +16,7 @@
# under the License.
import webob
+import xml.dom.minidom as minidom
from nova import db
from nova import exception
@@ -74,19 +75,67 @@ class ControllerV11(Controller):
return views.flavors.ViewBuilderV11(base_url)
+class FlavorXMLSerializer(wsgi.XMLDictSerializer):
+
+ def __init__(self):
+ super(FlavorXMLSerializer, self).__init__(xmlns=wsgi.XMLNS_V11)
+
+ def _flavor_to_xml(self, xml_doc, flavor, detailed):
+ flavor_node = xml_doc.createElement('flavor')
+ flavor_node.setAttribute('id', str(flavor['id']))
+ flavor_node.setAttribute('name', flavor['name'])
+
+ if detailed:
+ flavor_node.setAttribute('ram', str(flavor['ram']))
+ flavor_node.setAttribute('disk', str(flavor['disk']))
+
+ link_nodes = self._create_link_nodes(xml_doc, flavor['links'])
+ for link_node in link_nodes:
+ flavor_node.appendChild(link_node)
+ return flavor_node
+
+ def _flavors_list_to_xml(self, xml_doc, flavors, detailed):
+ container_node = xml_doc.createElement('flavors')
+
+ for flavor in flavors:
+ item_node = self._flavor_to_xml(xml_doc, flavor, detailed)
+ container_node.appendChild(item_node)
+ return container_node
+
+ def show(self, flavor_container):
+ xml_doc = minidom.Document()
+ flavor = flavor_container['flavor']
+ node = self._flavor_to_xml(xml_doc, flavor, True)
+ return self.to_xml_string(node, True)
+
+ def detail(self, flavors_container):
+ xml_doc = minidom.Document()
+ flavors = flavors_container['flavors']
+ node = self._flavors_list_to_xml(xml_doc, flavors, True)
+ return self.to_xml_string(node, True)
+
+ def index(self, flavors_container):
+ xml_doc = minidom.Document()
+ flavors = flavors_container['flavors']
+ node = self._flavors_list_to_xml(xml_doc, flavors, False)
+ return self.to_xml_string(node, True)
+
+
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,
+ xml_serializer = {
+ '1.0': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10),
+ '1.1': FlavorXMLSerializer(),
}[version]
- serializers = {
- 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns),
+ 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/image_metadata.py b/nova/api/openstack/image_metadata.py
index c0e92f2fc..ee181c924 100644
--- a/nova/api/openstack/image_metadata.py
+++ b/nova/api/openstack/image_metadata.py
@@ -22,7 +22,6 @@ from nova import flags
from nova import image
from nova import quota
from nova import utils
-from nova.api.openstack import faults
from nova.api.openstack import wsgi
@@ -62,7 +61,7 @@ class Controller(object):
if id in metadata:
return {'meta': {id: metadata[id]}}
else:
- return faults.Fault(exc.HTTPNotFound())
+ raise exc.HTTPNotFound()
def create(self, req, image_id, body):
context = req.environ['nova.context']
@@ -97,33 +96,67 @@ class Controller(object):
self._check_quota_limit(context, metadata)
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
+ return dict(meta=meta)
- return req.body
+ def update_all(self, req, image_id, body):
+ context = req.environ['nova.context']
+ img = self.image_service.show(context, image_id)
+ metadata = body.get('metadata', {})
+ self._check_quota_limit(context, metadata)
+ img['properties'] = metadata
+ self.image_service.update(context, image_id, img, None)
+ return dict(metadata=metadata)
def delete(self, req, image_id, id):
context = req.environ['nova.context']
img = self.image_service.show(context, image_id)
metadata = self._get_metadata(context, image_id)
if not id in metadata:
- return faults.Fault(exc.HTTPNotFound())
+ raise exc.HTTPNotFound()
metadata.pop(id)
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
+class ImageMetadataXMLDeserializer(wsgi.MetadataXMLDeserializer):
+
+ def _extract_metadata_container(self, datastring):
+ dom = minidom.parseString(datastring)
+ metadata_node = self.find_first_child_named(dom, "metadata")
+ metadata = self.extract_metadata(metadata_node)
+ return {'body': {'metadata': metadata}}
+
+ def create(self, datastring):
+ return self._extract_metadata_container(datastring)
+
+ def update_all(self, datastring):
+ return self._extract_metadata_container(datastring)
+
+ def update(self, datastring):
+ dom = minidom.parseString(datastring)
+ metadata_item = self.extract_metadata(dom)
+ return {'body': {'meta': metadata_item}}
+
+
+class HeadersSerializer(wsgi.ResponseHeadersSerializer):
+
+ def delete(self, response, data):
+ response.status_int = 204
+
+
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 +166,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)
@@ -143,12 +177,16 @@ class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer):
def create(self, metadata_dict):
return self._meta_list_to_xml_string(metadata_dict)
+ def update_all(self, metadata_dict):
+ return self._meta_list_to_xml_string(metadata_dict)
+
def _meta_item_to_xml_string(self, meta_item_dict):
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'])
@@ -156,10 +194,21 @@ class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer):
def update(self, meta_item_dict):
return self._meta_item_to_xml_string(meta_item_dict['meta'])
+ def default(self, *args, **kwargs):
+ return ''
+
def create_resource():
- serializers = {
+ headers_serializer = HeadersSerializer()
+
+ body_deserializers = {
+ 'application/xml': ImageMetadataXMLDeserializer(),
+ }
+
+ body_serializers = {
'application/xml': ImageMetadataXMLSerializer(),
}
+ serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer)
+ deserializer = wsgi.RequestDeserializer(body_deserializers)
- return wsgi.Resource(Controller(), serializers=serializers)
+ return wsgi.Resource(Controller(), deserializer, serializer)
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 64d003a0f..30e4fd389 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 import servers
from nova.api.openstack.views import images as images_view
from nova.api.openstack import wsgi
@@ -32,7 +34,13 @@ from nova.api.openstack import wsgi
LOG = log.getLogger('nova.api.openstack.images')
FLAGS = flags.FLAGS
-SUPPORTED_FILTERS = ['name', 'status']
+SUPPORTED_FILTERS = {
+ 'name': 'name',
+ 'status': 'status',
+ 'changes-since': 'changes-since',
+ 'server': 'property-instance_ref',
+ 'type': 'property-image_type',
+}
class Controller(object):
@@ -59,8 +67,9 @@ class Controller(object):
filters = {}
for param in req.str_params:
if param in SUPPORTED_FILTERS or param.startswith('property-'):
- filters[param] = req.str_params.get(param)
-
+ # map filter name or carry through if property-*
+ filter_name = SUPPORTED_FILTERS.get(param, param)
+ filters[filter_name] = req.str_params.get(param)
return filters
def show(self, req, id):
@@ -75,7 +84,7 @@ class Controller(object):
image = self._image_service.show(context, id)
except (exception.NotFound, exception.InvalidImageRef):
explanation = _("Image not found.")
- raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation))
+ raise webob.exc.HTTPNotFound(explanation=explanation)
return dict(image=self.get_builder(req).build(image, detail=True))
@@ -90,31 +99,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()
@@ -208,13 +253,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 +279,109 @@ class ControllerV11(Controller):
return {'instance_ref': server_ref}
+class ImageXMLSerializer(wsgi.XMLDictSerializer):
+
+ xmlns = wsgi.XMLNS_V11
+
+ def __init__(self):
+ self.metadata_serializer = image_metadata.ImageMetadataXMLSerializer()
+
+ def _image_to_xml(self, xml_doc, image):
+ image_node = xml_doc.createElement('image')
+ image_node.setAttribute('id', str(image['id']))
+ image_node.setAttribute('name', image['name'])
+ link_nodes = self._create_link_nodes(xml_doc,
+ image['links'])
+ for link_node in link_nodes:
+ image_node.appendChild(link_node)
+ return image_node
+
+ def _image_to_xml_detailed(self, xml_doc, image):
+ image_node = xml_doc.createElement('image')
+ self._add_image_attributes(image_node, image)
+
+ if 'server' in image:
+ server_node = self._create_server_node(xml_doc, image['server'])
+ image_node.appendChild(server_node)
+
+ metadata = image.get('metadata', {}).items()
+ if len(metadata) > 0:
+ metadata_node = self._create_metadata_node(xml_doc, metadata)
+ image_node.appendChild(metadata_node)
+
+ link_nodes = self._create_link_nodes(xml_doc,
+ image['links'])
+ for link_node in link_nodes:
+ image_node.appendChild(link_node)
+
+ return image_node
+
+ def _add_image_attributes(self, node, image):
+ node.setAttribute('id', str(image['id']))
+ node.setAttribute('name', image['name'])
+ node.setAttribute('created', image['created'])
+ node.setAttribute('updated', image['updated'])
+ node.setAttribute('status', image['status'])
+ if 'progress' in image:
+ node.setAttribute('progress', str(image['progress']))
+
+ def _create_metadata_node(self, xml_doc, metadata):
+ return self.metadata_serializer.meta_list_to_xml(xml_doc, metadata)
+
+ def _create_server_node(self, xml_doc, server):
+ server_node = xml_doc.createElement('server')
+ server_node.setAttribute('id', str(server['id']))
+ link_nodes = self._create_link_nodes(xml_doc,
+ server['links'])
+ for link_node in link_nodes:
+ server_node.appendChild(link_node)
+ return server_node
+
+ def _image_list_to_xml(self, xml_doc, images, detailed):
+ container_node = xml_doc.createElement('images')
+ if detailed:
+ image_to_xml = self._image_to_xml_detailed
+ else:
+ image_to_xml = self._image_to_xml
+
+ for image in images:
+ item_node = image_to_xml(xml_doc, image)
+ container_node.appendChild(item_node)
+ return container_node
+
+ def index(self, images_dict):
+ xml_doc = minidom.Document()
+ node = self._image_list_to_xml(xml_doc,
+ images_dict['images'],
+ detailed=False)
+ return self.to_xml_string(node, True)
+
+ def detail(self, images_dict):
+ xml_doc = minidom.Document()
+ node = self._image_list_to_xml(xml_doc,
+ images_dict['images'],
+ detailed=True)
+ return self.to_xml_string(node, True)
+
+ def show(self, image_dict):
+ xml_doc = minidom.Document()
+ node = self._image_to_xml_detailed(xml_doc,
+ image_dict['image'])
+ return self.to_xml_string(node, True)
+
+ def create(self, image_dict):
+ xml_doc = minidom.Document()
+ node = self._image_to_xml_detailed(xml_doc,
+ image_dict['image'])
+ return self.to_xml_string(node, True)
+
+
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 +390,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..a74fae487 100644
--- a/nova/api/openstack/ips.py
+++ b/nova/api/openstack/ips.py
@@ -16,13 +16,14 @@
# under the License.
import time
+from xml.dom import minidom
from webob import exc
import nova
-from nova.api.openstack import faults
import nova.api.openstack.views.addresses
from nova.api.openstack import wsgi
+from nova import db
class Controller(object):
@@ -30,39 +31,121 @@ class Controller(object):
def __init__(self):
self.compute_api = nova.compute.API()
- self.builder = nova.api.openstack.views.addresses.ViewBuilderV10()
def _get_instance(self, req, server_id):
try:
instance = self.compute_api.get(
req.environ['nova.context'], server_id)
except nova.exception.NotFound:
- return faults.Fault(exc.HTTPNotFound())
+ raise exc.HTTPNotFound()
return instance
+ def create(self, req, server_id, body):
+ raise exc.HTTPNotImplemented()
+
+ def delete(self, req, server_id, id):
+ raise exc.HTTPNotImplemented()
+
+
+class ControllerV10(Controller):
+
def index(self, req, server_id):
instance = self._get_instance(req, server_id)
- return {'addresses': self.builder.build(instance)}
+ builder = nova.api.openstack.views.addresses.ViewBuilderV10()
+ return {'addresses': builder.build(instance)}
- def public(self, req, server_id):
+ def show(self, req, server_id, id):
instance = self._get_instance(req, server_id)
- return {'public': self.builder.build_public_parts(instance)}
+ builder = self._get_view_builder(req)
+ if id == 'private':
+ view = builder.build_private_parts(instance)
+ elif id == 'public':
+ view = builder.build_public_parts(instance)
+ else:
+ msg = _("Only private and public networks available")
+ raise exc.HTTPNotFound(explanation=msg)
- def private(self, req, server_id):
- instance = self._get_instance(req, server_id)
- return {'private': self.builder.build_private_parts(instance)}
+ return {id: view}
+
+ def _get_view_builder(self, req):
+ return nova.api.openstack.views.addresses.ViewBuilderV10()
+
+
+class ControllerV11(Controller):
+
+ def index(self, req, server_id):
+ context = req.environ['nova.context']
+ interfaces = self._get_virtual_interfaces(context, server_id)
+ networks = self._get_view_builder(req).build(interfaces)
+ return {'addresses': networks}
def show(self, req, server_id, id):
- return faults.Fault(exc.HTTPNotImplemented())
+ context = req.environ['nova.context']
+ interfaces = self._get_virtual_interfaces(context, server_id)
+ network = self._get_view_builder(req).build_network(interfaces, id)
- def create(self, req, server_id, body):
- return faults.Fault(exc.HTTPNotImplemented())
+ if network is None:
+ msg = _("Instance is not a member of specified network")
+ raise exc.HTTPNotFound(explanation=msg)
- def delete(self, req, server_id, id):
- return faults.Fault(exc.HTTPNotImplemented())
+ return network
+ def _get_virtual_interfaces(self, context, server_id):
+ try:
+ return db.api.virtual_interface_get_by_instance(context, server_id)
+ except nova.exception.InstanceNotFound:
+ msg = _("Instance does not exist")
+ raise exc.HTTPNotFound(explanation=msg)
+
+ def _get_view_builder(self, req):
+ return nova.api.openstack.views.addresses.ViewBuilderV11()
+
+
+class IPXMLSerializer(wsgi.XMLDictSerializer):
+ def __init__(self, xmlns=wsgi.XMLNS_V11):
+ super(IPXMLSerializer, self).__init__(xmlns=xmlns)
+
+ def _ip_to_xml(self, xml_doc, ip_dict):
+ ip_node = xml_doc.createElement('ip')
+ ip_node.setAttribute('addr', ip_dict['addr'])
+ ip_node.setAttribute('version', str(ip_dict['version']))
+ return ip_node
+
+ def _network_to_xml(self, xml_doc, network_id, ip_dicts):
+ network_node = xml_doc.createElement('network')
+ network_node.setAttribute('id', network_id)
+
+ for ip_dict in ip_dicts:
+ ip_node = self._ip_to_xml(xml_doc, ip_dict)
+ network_node.appendChild(ip_node)
+
+ return network_node
+
+ def networks_to_xml(self, xml_doc, networks_container):
+ addresses_node = xml_doc.createElement('addresses')
+ for (network_id, ip_dicts) in networks_container.items():
+ network_node = self._network_to_xml(xml_doc, network_id, ip_dicts)
+ addresses_node.appendChild(network_node)
+ return addresses_node
+
+ def show(self, network_container):
+ (network_id, ip_dicts) = network_container.items()[0]
+ xml_doc = minidom.Document()
+ node = self._network_to_xml(xml_doc, network_id, ip_dicts)
+ return self.to_xml_string(node, False)
+
+ def index(self, addresses_container):
+ xml_doc = minidom.Document()
+ node = self.networks_to_xml(xml_doc, addresses_container['addresses'])
+ return self.to_xml_string(node, False)
+
+
+def create_resource(version):
+ controller = {
+ '1.0': ControllerV10,
+ '1.1': ControllerV11,
+ }[version]()
-def create_resource():
metadata = {
'list_collections': {
'public': {'item_name': 'ip', 'item_key': 'addr'},
@@ -70,9 +153,11 @@ def create_resource():
},
}
- serializers = {
- 'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
- xmlns=wsgi.XMLNS_V10),
- }
+ xml_serializer = {
+ '1.0': wsgi.XMLDictSerializer(metadata=metadata, xmlns=wsgi.XMLNS_V11),
+ '1.1': IPXMLSerializer(),
+ }[version]
+
+ serializer = wsgi.ResponseSerializer({'application/xml': xml_serializer})
- 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..86afa3b62 100644
--- a/nova/api/openstack/limits.py
+++ b/nova/api/openstack/limits.py
@@ -25,14 +25,15 @@ import re
import time
import urllib
import webob.exc
+from xml.dom import minidom
from collections import defaultdict
from webob.dec import wsgify
from nova import quota
+from nova import utils
from nova import wsgi as base_wsgi
-from nova import wsgi
from nova.api.openstack import common
from nova.api.openstack import faults
from nova.api.openstack.views import limits as limits_views
@@ -76,6 +77,58 @@ class LimitsControllerV11(LimitsController):
return limits_views.ViewBuilderV11()
+class LimitsXMLSerializer(wsgi.XMLDictSerializer):
+
+ xmlns = wsgi.XMLNS_V11
+
+ def __init__(self):
+ pass
+
+ def _create_rates_node(self, xml_doc, rates):
+ rates_node = xml_doc.createElement('rates')
+ for rate in rates:
+ rate_node = xml_doc.createElement('rate')
+ rate_node.setAttribute('uri', rate['uri'])
+ rate_node.setAttribute('regex', rate['regex'])
+
+ for limit in rate['limit']:
+ limit_node = xml_doc.createElement('limit')
+ limit_node.setAttribute('value', str(limit['value']))
+ limit_node.setAttribute('verb', limit['verb'])
+ limit_node.setAttribute('remaining', str(limit['remaining']))
+ limit_node.setAttribute('unit', limit['unit'])
+ limit_node.setAttribute('next-available',
+ str(limit['next-available']))
+ rate_node.appendChild(limit_node)
+
+ rates_node.appendChild(rate_node)
+ return rates_node
+
+ def _create_absolute_node(self, xml_doc, absolutes):
+ absolute_node = xml_doc.createElement('absolute')
+ for key, value in absolutes.iteritems():
+ limit_node = xml_doc.createElement('limit')
+ limit_node.setAttribute('name', key)
+ limit_node.setAttribute('value', str(value))
+ absolute_node.appendChild(limit_node)
+ return absolute_node
+
+ def _limits_to_xml(self, xml_doc, limits):
+ limits_node = xml_doc.createElement('limits')
+ rates_node = self._create_rates_node(xml_doc, limits['rate'])
+ limits_node.appendChild(rates_node)
+
+ absolute_node = self._create_absolute_node(xml_doc, limits['absolute'])
+ limits_node.appendChild(absolute_node)
+
+ return limits_node
+
+ def index(self, limits_dict):
+ xml_doc = minidom.Document()
+ node = self._limits_to_xml(xml_doc, limits_dict['limits'])
+ return self.to_xml_string(node, False)
+
+
def create_resource(version='1.0'):
controller = {
'1.0': LimitsControllerV10,
@@ -97,12 +150,18 @@ def create_resource(version='1.0'):
},
}
- serializers = {
- 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns,
- metadata=metadata),
+ xml_serializer = {
+ '1.0': wsgi.XMLDictSerializer(xmlns=xmlns, metadata=metadata),
+ '1.1': LimitsXMLSerializer(),
+ }[version]
+
+ body_serializers = {
+ 'application/xml': xml_serializer,
}
- return wsgi.Resource(controller, serializers=serializers)
+ serializer = wsgi.ResponseSerializer(body_serializers)
+
+ return wsgi.Resource(controller, serializer=serializer)
class Limit(object):
@@ -117,6 +176,8 @@ class Limit(object):
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`.
@@ -222,16 +283,30 @@ class RateLimitingMiddleware(base_wsgi.Middleware):
is stored in memory for this implementation.
"""
- def __init__(self, application, limits=None):
+ 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: List of dictionaries describing limits
+ @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)
- self._limiter = Limiter(limits or DEFAULT_LIMITS)
+
+ # 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):
@@ -269,7 +344,7 @@ class Limiter(object):
Rate-limit checking class which handles limits in memory.
"""
- def __init__(self, limits):
+ def __init__(self, limits, **kwargs):
"""
Initialize the new `Limiter`.
@@ -278,6 +353,12 @@ class Limiter(object):
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.
@@ -303,6 +384,66 @@ class Limiter(object):
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):
"""
@@ -386,3 +527,19 @@ class WsgiLimiterProxy(object):
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/server_metadata.py b/nova/api/openstack/server_metadata.py
index 8a314de22..d4f42bbf5 100644
--- a/nova/api/openstack/server_metadata.py
+++ b/nova/api/openstack/server_metadata.py
@@ -18,7 +18,6 @@
from webob import exc
from nova import compute
-from nova.api.openstack import faults
from nova.api.openstack import wsgi
from nova import exception
from nova import quota
@@ -123,8 +122,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..7bef1d9b2 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -17,15 +17,16 @@ import base64
import traceback
from webob import exc
+import webob
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
from nova.api.openstack import common
from nova.api.openstack import create_instance_helper as helper
-from nova.api.openstack import faults
import nova.api.openstack.views.addresses
import nova.api.openstack.views.flavors
import nova.api.openstack.views.images
@@ -62,7 +63,7 @@ class Controller(object):
return exc.HTTPBadRequest(explanation=str(err))
return servers
- def _get_view_builder(self, req):
+ def _build_view(self, req, instance, is_detail=False):
raise NotImplementedError()
def _limit_items(self, items, req):
@@ -76,13 +77,19 @@ 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']
+ servers = [self._build_view(req, inst, is_detail)['server']
for inst in limited_list]
return dict(servers=servers)
@@ -92,38 +99,25 @@ class Controller(object):
try:
instance = self.compute_api.routing_get(
req.environ['nova.context'], id)
- builder = self._get_view_builder(req)
- return builder.build(instance, is_detail=True)
+ return self._build_view(req, instance, is_detail=True)
except exception.NotFound:
- return faults.Fault(exc.HTTPNotFound())
-
- @scheduler_api.redirect_handler
- def delete(self, req, id):
- """ Destroys a server """
- try:
- self.compute_api.delete(req.environ['nova.context'], id)
- except exception.NotFound:
- return faults.Fault(exc.HTTPNotFound())
- return exc.HTTPAccepted()
+ raise exc.HTTPNotFound()
def create(self, req, body):
""" Creates a new server for a given user """
extra_values = None
result = None
- try:
- extra_values, result = self.helper.create_instance(
- req, body, self.compute_api.create)
- except faults.Fault, f:
- return f
-
- instances = result
+ extra_values, instances = self.helper.create_instance(
+ req, body, self.compute_api.create)
- (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]
- builder = self._get_view_builder(req)
- server = builder.build(inst, is_detail=True)
+ server = self._build_view(req, inst, is_detail=True)
server['server']['adminPass'] = extra_values['password']
return server
@@ -134,7 +128,7 @@ class Controller(object):
raise exc.HTTPUnprocessableEntity()
if not body:
- return faults.Fault(exc.HTTPUnprocessableEntity())
+ raise exc.HTTPUnprocessableEntity()
ctxt = req.environ['nova.context']
update_dict = {}
@@ -149,7 +143,7 @@ class Controller(object):
try:
self.compute_api.update(ctxt, id, **update_dict)
except exception.NotFound:
- return faults.Fault(exc.HTTPNotFound())
+ raise exc.HTTPNotFound()
return exc.HTTPNoContent()
@@ -168,12 +162,12 @@ 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:
return actions[key](body, req, id)
- return faults.Fault(exc.HTTPNotImplemented())
+ raise exc.HTTPNotImplemented()
def _action_change_password(self, input_dict, req, id):
return exc.HTTPNotImplemented()
@@ -183,7 +177,7 @@ class Controller(object):
self.compute_api.confirm_resize(req.environ['nova.context'], id)
except Exception, e:
LOG.exception(_("Error in confirm-resize %s"), e)
- return faults.Fault(exc.HTTPBadRequest())
+ raise exc.HTTPBadRequest()
return exc.HTTPNoContent()
def _action_revert_resize(self, input_dict, req, id):
@@ -191,8 +185,8 @@ class Controller(object):
self.compute_api.revert_resize(req.environ['nova.context'], id)
except Exception, e:
LOG.exception(_("Error in revert-resize %s"), e)
- return faults.Fault(exc.HTTPBadRequest())
- return exc.HTTPAccepted()
+ raise exc.HTTPBadRequest()
+ return webob.Response(status_int=202)
def _action_resize(self, input_dict, req, id):
return exc.HTTPNotImplemented()
@@ -202,15 +196,23 @@ class Controller(object):
reboot_type = input_dict['reboot']['type']
else:
LOG.exception(_("Missing argument 'type' for reboot"))
- return faults.Fault(exc.HTTPUnprocessableEntity())
+ raise exc.HTTPUnprocessableEntity()
try:
# TODO(gundlach): pass reboot_type, support soft reboot in
# virt driver
self.compute_api.reboot(req.environ['nova.context'], id)
except Exception, e:
LOG.exception(_("Error in reboot %s"), e)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
+
+ 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)
+ raise exc.HTTPBadRequest()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def lock(self, req, id):
@@ -225,8 +227,8 @@ class Controller(object):
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::lock %s"), readable)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def unlock(self, req, id):
@@ -241,8 +243,8 @@ class Controller(object):
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::unlock %s"), readable)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def get_lock(self, req, id):
@@ -256,8 +258,8 @@ class Controller(object):
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::get_lock %s"), readable)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def reset_network(self, req, id, body):
@@ -271,8 +273,8 @@ class Controller(object):
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::reset_network %s"), readable)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def inject_network_info(self, req, id, body):
@@ -286,8 +288,8 @@ class Controller(object):
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::inject_network_info %s"), readable)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def pause(self, req, id, body):
@@ -298,8 +300,8 @@ class Controller(object):
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::pause %s"), readable)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def unpause(self, req, id, body):
@@ -310,8 +312,8 @@ class Controller(object):
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::unpause %s"), readable)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def suspend(self, req, id, body):
@@ -322,8 +324,8 @@ class Controller(object):
except:
readable = traceback.format_exc()
LOG.exception(_("compute.api::suspend %s"), readable)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def resume(self, req, id, body):
@@ -334,8 +336,8 @@ class Controller(object):
except:
readable = traceback.format_exc()
LOG.exception(_("compute.api::resume %s"), readable)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def rescue(self, req, id):
@@ -346,8 +348,8 @@ class Controller(object):
except:
readable = traceback.format_exc()
LOG.exception(_("compute.api::rescue %s"), readable)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def unrescue(self, req, id):
@@ -358,8 +360,8 @@ class Controller(object):
except:
readable = traceback.format_exc()
LOG.exception(_("compute.api::unrescue %s"), readable)
- return faults.Fault(exc.HTTPUnprocessableEntity())
- return exc.HTTPAccepted()
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def get_ajax_console(self, req, id):
@@ -368,8 +370,8 @@ class Controller(object):
self.compute_api.get_ajax_console(req.environ['nova.context'],
int(id))
except exception.NotFound:
- return faults.Fault(exc.HTTPNotFound())
- return exc.HTTPAccepted()
+ raise exc.HTTPNotFound()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def get_vnc_console(self, req, id):
@@ -378,8 +380,8 @@ class Controller(object):
self.compute_api.get_vnc_console(req.environ['nova.context'],
int(id))
except exception.NotFound:
- return faults.Fault(exc.HTTPNotFound())
- return exc.HTTPAccepted()
+ raise exc.HTTPNotFound()
+ return webob.Response(status_int=202)
@scheduler_api.redirect_handler
def diagnostics(self, req, id):
@@ -404,16 +406,25 @@ class Controller(object):
class ControllerV10(Controller):
+ @scheduler_api.redirect_handler
+ def delete(self, req, id):
+ """ Destroys a server """
+ try:
+ self.compute_api.delete(req.environ['nova.context'], id)
+ except exception.NotFound:
+ raise exc.HTTPNotFound()
+ return webob.Response(status_int=202)
+
def _image_ref_from_req_data(self, data):
return data['server']['imageId']
def _flavor_id_from_req_data(self, data):
return data['server']['flavorId']
- def _get_view_builder(self, req):
- addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV10()
- return nova.api.openstack.views.servers.ViewBuilderV10(
- addresses_builder)
+ def _build_view(self, req, instance, is_detail=False):
+ addresses = nova.api.openstack.views.addresses.ViewBuilderV10()
+ builder = nova.api.openstack.views.servers.ViewBuilderV10(addresses)
+ return builder.build(instance, is_detail=is_detail)
def _limit_items(self, items, req):
return common.limited(items, req)
@@ -425,18 +436,14 @@ class ControllerV10(Controller):
def _action_resize(self, input_dict, req, id):
""" Resizes a given instance to the flavor size requested """
- try:
- if 'resize' in input_dict and 'flavorId' in input_dict['resize']:
- flavor_id = input_dict['resize']['flavorId']
- self.compute_api.resize(req.environ['nova.context'], id,
- flavor_id)
- else:
- LOG.exception(_("Missing 'flavorId' argument for resize"))
- return faults.Fault(exc.HTTPUnprocessableEntity())
- except Exception, e:
- LOG.exception(_("Error in resize %s"), e)
- return faults.Fault(exc.HTTPBadRequest())
- return exc.HTTPAccepted()
+ if 'resize' in input_dict and 'flavorId' in input_dict['resize']:
+ flavor_id = input_dict['resize']['flavorId']
+ self.compute_api.resize(req.environ['nova.context'], id,
+ flavor_id)
+ else:
+ LOG.exception(_("Missing 'flavorId' argument for resize"))
+ raise exc.HTTPUnprocessableEntity()
+ return webob.Response(status_int=202)
def _action_rebuild(self, info, request, instance_id):
context = request.environ['nova.context']
@@ -447,18 +454,16 @@ class ControllerV10(Controller):
except (KeyError, TypeError):
msg = _("Could not parse imageId from request.")
LOG.debug(msg)
- return faults.Fault(exc.HTTPBadRequest(explanation=msg))
+ raise exc.HTTPBadRequest(explanation=msg)
try:
self.compute_api.rebuild(context, instance_id, image_id)
except exception.BuildInProgress:
msg = _("Instance %d is currently being rebuilt.") % instance_id
LOG.debug(msg)
- return faults.Fault(exc.HTTPConflict(explanation=msg))
+ raise exc.HTTPConflict(explanation=msg)
- response = exc.HTTPAccepted()
- response.empty_body = True
- return response
+ return webob.Response(status_int=202)
def _get_server_admin_password(self, server):
""" Determine the admin password for a server on creation """
@@ -466,6 +471,15 @@ class ControllerV10(Controller):
class ControllerV11(Controller):
+
+ @scheduler_api.redirect_handler
+ def delete(self, req, id):
+ """ Destroys a server """
+ try:
+ self.compute_api.delete(req.environ['nova.context'], id)
+ except exception.NotFound:
+ raise exc.HTTPNotFound()
+
def _image_ref_from_req_data(self, data):
return data['server']['imageRef']
@@ -473,16 +487,18 @@ class ControllerV11(Controller):
href = data['server']['flavorRef']
return common.get_id_from_href(href)
- def _get_view_builder(self, req):
+ def _build_view(self, req, instance, is_detail=False):
base_url = req.application_url
flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11(
base_url)
image_builder = nova.api.openstack.views.images.ViewBuilderV11(
base_url)
addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11()
- return nova.api.openstack.views.servers.ViewBuilderV11(
+ builder = nova.api.openstack.views.servers.ViewBuilderV11(
addresses_builder, flavor_builder, image_builder, base_url)
+ return builder.build(instance, is_detail=is_detail)
+
def _action_change_password(self, input_dict, req, id):
context = req.environ['nova.context']
if (not 'changePassword' in input_dict
@@ -494,7 +510,7 @@ class ControllerV11(Controller):
msg = _("Invalid adminPass")
return exc.HTTPBadRequest(explanation=msg)
self.compute_api.set_admin_password(context, id, password)
- return exc.HTTPAccepted()
+ return webob.Response(status_int=202)
def _limit_items(self, items, req):
return common.limited_by_marker(items, req)
@@ -506,7 +522,7 @@ class ControllerV11(Controller):
except AttributeError as ex:
msg = _("Unable to parse metadata key/value pairs.")
LOG.debug(msg)
- raise faults.Fault(exc.HTTPBadRequest(explanation=msg))
+ raise exc.HTTPBadRequest(explanation=msg)
def _decode_personalities(self, personalities):
"""Decode the Base64-encoded personalities."""
@@ -517,14 +533,14 @@ class ControllerV11(Controller):
except (KeyError, TypeError):
msg = _("Unable to parse personality path/contents.")
LOG.info(msg)
- raise faults.Fault(exc.HTTPBadRequest(explanation=msg))
+ raise exc.HTTPBadRequest(explanation=msg)
try:
personality["contents"] = base64.b64decode(contents)
except TypeError:
msg = _("Personality content could not be Base64 decoded.")
LOG.info(msg)
- raise faults.Fault(exc.HTTPBadRequest(explanation=msg))
+ raise exc.HTTPBadRequest(explanation=msg)
def _action_resize(self, input_dict, req, id):
""" Resizes a given instance to the flavor size requested """
@@ -536,11 +552,11 @@ class ControllerV11(Controller):
flavor_id)
else:
LOG.exception(_("Missing 'flavorRef' argument for resize"))
- return faults.Fault(exc.HTTPUnprocessableEntity())
+ raise exc.HTTPUnprocessableEntity()
except Exception, e:
LOG.exception(_("Error in resize %s"), e)
- return faults.Fault(exc.HTTPBadRequest())
- return exc.HTTPAccepted()
+ raise exc.HTTPBadRequest()
+ return webob.Response(status_int=202)
def _action_rebuild(self, info, request, instance_id):
context = request.environ['nova.context']
@@ -551,7 +567,7 @@ class ControllerV11(Controller):
except (KeyError, TypeError):
msg = _("Could not parse imageRef from request.")
LOG.debug(msg)
- return faults.Fault(exc.HTTPBadRequest(explanation=msg))
+ raise exc.HTTPBadRequest(explanation=msg)
personalities = info["rebuild"].get("personality", [])
metadata = info["rebuild"].get("metadata")
@@ -567,11 +583,9 @@ class ControllerV11(Controller):
except exception.BuildInProgress:
msg = _("Instance %d is currently being rebuilt.") % instance_id
LOG.debug(msg)
- return faults.Fault(exc.HTTPConflict(explanation=msg))
+ raise exc.HTTPConflict(explanation=msg)
- response = exc.HTTPAccepted()
- response.empty_body = True
- return response
+ return webob.Response(status_int=202)
def get_default_xmlns(self, req):
return common.XML_NS_V11
@@ -581,6 +595,12 @@ class ControllerV11(Controller):
return self.helper._get_server_admin_password_new_style(server)
+class HeadersSerializer(wsgi.ResponseHeadersSerializer):
+
+ def delete(self, response, data):
+ response.status_int = 204
+
+
def create_resource(version='1.0'):
controller = {
'1.0': ControllerV10,
@@ -608,14 +628,18 @@ def create_resource(version='1.0'):
'1.1': wsgi.XMLNS_V11,
}[version]
- serializers = {
+ headers_serializer = HeadersSerializer()
+
+ 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, headers_serializer)
+ 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..54d0a8334 100644
--- a/nova/api/openstack/shared_ip_groups.py
+++ b/nova/api/openstack/shared_ip_groups.py
@@ -17,36 +17,35 @@
from webob import exc
-from nova.api.openstack import faults
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())
+ raise 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())
+ raise 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())
+ raise exc.HTTPNotImplemented()
- def delete(self, req, id):
+ def delete(self, req, id, **kwargs):
""" Deletes a Shared IP Group """
- raise faults.Fault(exc.HTTPNotImplemented())
+ raise exc.HTTPNotImplemented()
- def detail(self, req):
+ def detail(self, req, **kwargs):
""" Returns a complete list of Shared IP Groups """
- raise faults.Fault(exc.HTTPNotImplemented())
+ raise exc.HTTPNotImplemented()
- def create(self, req, body):
+ def create(self, req, **kwargs):
""" Creates a new Shared IP group """
- raise faults.Fault(exc.HTTPNotImplemented())
+ raise exc.HTTPNotImplemented()
def create_resource():
diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py
index 50975fc1f..8dd72d559 100644
--- a/nova/api/openstack/users.py
+++ b/nova/api/openstack/users.py
@@ -19,7 +19,6 @@ 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 faults
from nova.api.openstack import wsgi
from nova.auth import manager
@@ -69,7 +68,7 @@ class Controller(object):
user = None
if user is None:
- raise faults.Fault(exc.HTTPNotFound())
+ raise exc.HTTPNotFound()
return dict(user=_translate_keys(user))
@@ -105,8 +104,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..df7a94b7e 100644
--- a/nova/api/openstack/versions.py
+++ b/nova/api/openstack/versions.py
@@ -15,13 +15,18 @@
# License for the specific language governing permissions and limitations
# under the License.
+from datetime import datetime
import webob
import webob.dec
+from xml.dom import minidom
import nova.api.openstack.views.versions
from nova.api.openstack import wsgi
+ATOM_XMLNS = "http://www.w3.org/2005/Atom"
+
+
class Versions(wsgi.Resource):
def __init__(self):
metadata = {
@@ -31,11 +36,20 @@ class Versions(wsgi.Resource):
}
}
- serializers = {
- 'application/xml': wsgi.XMLDictSerializer(metadata=metadata),
+ body_serializers = {
+ 'application/atom+xml': VersionsAtomSerializer(metadata=metadata),
+ 'application/xml': VersionsXMLSerializer(metadata=metadata),
}
+ serializer = wsgi.ResponseSerializer(body_serializers)
- wsgi.Resource.__init__(self, None, serializers=serializers)
+ supported_content_types = ('application/json',
+ 'application/xml',
+ 'application/atom+xml')
+ deserializer = wsgi.RequestDeserializer(
+ supported_content_types=supported_content_types)
+
+ wsgi.Resource.__init__(self, None, serializer=serializer,
+ deserializer=deserializer)
def dispatch(self, request, *args):
"""Respond to a request for all OpenStack API versions."""
@@ -43,13 +57,143 @@ class Versions(wsgi.Resource):
{
"id": "v1.1",
"status": "CURRENT",
+ #TODO(wwolf) get correct value for these
+ "updated": "2011-07-18T11:30:00Z",
},
{
"id": "v1.0",
"status": "DEPRECATED",
+ #TODO(wwolf) get correct value for these
+ "updated": "2010-10-09T11:30:00Z",
},
]
builder = nova.api.openstack.views.versions.get_view_builder(request)
versions = [builder.build(version) for version in version_objs]
return dict(versions=versions)
+
+
+class VersionsXMLSerializer(wsgi.XMLDictSerializer):
+ def _versions_to_xml(self, versions):
+ root = self._xml_doc.createElement('versions')
+
+ for version in versions:
+ root.appendChild(self._create_version_node(version))
+
+ return root
+
+ def _create_version_node(self, version):
+ version_node = self._xml_doc.createElement('version')
+ version_node.setAttribute('id', version['id'])
+ version_node.setAttribute('status', version['status'])
+ version_node.setAttribute('updated', version['updated'])
+
+ for link in version['links']:
+ link_node = self._xml_doc.createElement('atom:link')
+ link_node.setAttribute('rel', link['rel'])
+ link_node.setAttribute('href', link['href'])
+ version_node.appendChild(link_node)
+
+ return version_node
+
+ def default(self, data):
+ self._xml_doc = minidom.Document()
+ node = self._versions_to_xml(data['versions'])
+
+ return self.to_xml_string(node)
+
+
+class VersionsAtomSerializer(wsgi.XMLDictSerializer):
+ def __init__(self, metadata=None, xmlns=None):
+ if not xmlns:
+ self.xmlns = ATOM_XMLNS
+ else:
+ self.xmlns = xmlns
+
+ def _create_text_elem(self, name, text, type=None):
+ elem = self._xml_doc.createElement(name)
+ if type:
+ elem.setAttribute('type', type)
+ elem_text = self._xml_doc.createTextNode(text)
+ elem.appendChild(elem_text)
+ return elem
+
+ 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_meta(self, root, versions):
+ title = self._create_text_elem('title', 'Available API Versions',
+ type='text')
+ # Set this updated to the most recently updated version
+ recent = self._get_most_recent_update(versions)
+ updated = self._create_text_elem('updated', recent)
+
+ base_url = self._get_base_url(versions[0]['links'][0]['href'])
+ id = self._create_text_elem('id', base_url)
+ link = self._xml_doc.createElement('link')
+ link.setAttribute('rel', 'self')
+ link.setAttribute('href', base_url)
+
+ author = self._xml_doc.createElement('author')
+ author_name = self._create_text_elem('name', 'Rackspace')
+ author_uri = self._create_text_elem('uri', 'http://www.rackspace.com/')
+ author.appendChild(author_name)
+ author.appendChild(author_uri)
+
+ root.appendChild(title)
+ root.appendChild(updated)
+ root.appendChild(id)
+ root.appendChild(author)
+ root.appendChild(link)
+
+ def _create_version_entries(self, root, versions):
+ for version in versions:
+ entry = self._xml_doc.createElement('entry')
+
+ id = self._create_text_elem('id', version['links'][0]['href'])
+ title = self._create_text_elem('title',
+ 'Version %s' % version['id'],
+ type='text')
+ updated = self._create_text_elem('updated', version['updated'])
+
+ entry.appendChild(id)
+ entry.appendChild(title)
+ entry.appendChild(updated)
+
+ for link in version['links']:
+ link_node = self._xml_doc.createElement('link')
+ link_node.setAttribute('rel', link['rel'])
+ link_node.setAttribute('href', link['href'])
+ entry.appendChild(link_node)
+
+ content = self._create_text_elem('content',
+ 'Version %s %s (%s)' %
+ (version['id'],
+ version['status'],
+ version['updated']),
+ type='text')
+
+ entry.appendChild(content)
+ root.appendChild(entry)
+
+ def default(self, data):
+ self._xml_doc = minidom.Document()
+ node = self._xml_doc.createElementNS(self.xmlns, 'feed')
+ self._create_meta(node, data['versions'])
+ self._create_version_entries(node, data['versions'])
+
+ return self.to_xml_string(node)
diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py
index 2810cce39..ddbf7a144 100644
--- a/nova/api/openstack/views/addresses.py
+++ b/nova/api/openstack/views/addresses.py
@@ -15,34 +15,75 @@
# License for the specific language governing permissions and limitations
# under the License.
+from nova import flags
from nova import utils
from nova.api.openstack import common
+FLAGS = flags.FLAGS
+
class ViewBuilder(object):
- ''' Models a server addresses response as a python dictionary.'''
+ """Models a server addresses response as a python dictionary."""
def build(self, inst):
raise NotImplementedError()
class ViewBuilderV10(ViewBuilder):
+
def build(self, inst):
private_ips = self.build_private_parts(inst)
public_ips = self.build_public_parts(inst)
return dict(public=public_ips, private=private_ips)
def build_public_parts(self, inst):
- return utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
+ 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')
- private_ips = [dict(version=4, addr=a) for a in private_ips]
- public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
- public_ips = [dict(version=4, addr=a) for a in public_ips]
- return dict(public=public_ips, private=private_ips)
+
+ def build(self, interfaces):
+ networks = {}
+ for interface in interfaces:
+ network_label = interface['network']['label']
+
+ if network_label not in networks:
+ networks[network_label] = []
+
+ ip_addresses = list(self._extract_ipv4_addresses(interface))
+
+ if FLAGS.use_ipv6:
+ ipv6_address = self._extract_ipv6_address(interface)
+ if ipv6_address is not None:
+ ip_addresses.append(ipv6_address)
+
+ networks[network_label].extend(ip_addresses)
+
+ return networks
+
+ def build_network(self, interfaces, network_label):
+ for interface in interfaces:
+ if interface['network']['label'] == network_label:
+ ips = list(self._extract_ipv4_addresses(interface))
+ ipv6 = self._extract_ipv6_address(interface)
+ if ipv6 is not None:
+ ips.append(ipv6)
+ return {network_label: ips}
+ return None
+
+ def _extract_ipv4_addresses(self, interface):
+ for fixed_ip in interface['fixed_ips']:
+ yield self._build_ip_entity(fixed_ip['address'], 4)
+ for floating_ip in fixed_ip.get('floating_ips', []):
+ yield self._build_ip_entity(floating_ip['address'], 4)
+
+ def _extract_ipv6_address(self, interface):
+ fixed_ipv6 = interface.get('fixed_ipv6')
+ if fixed_ipv6 is not None:
+ return self._build_ip_entity(fixed_ipv6, 6)
+
+ def _build_ip_entity(self, address, version):
+ return {'addr': address, 'version': version}
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..873ce212a 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."""
@@ -96,7 +98,20 @@ class ViewBuilderV11(ViewBuilder):
def _build_server(self, image, image_obj):
try:
- image['serverRef'] = image_obj['properties']['instance_ref']
+ serverRef = image_obj['properties']['instance_ref']
+ image['server'] = {
+ "id": common.get_id_from_href(serverRef),
+ "links": [
+ {
+ "rel": "self",
+ "href": serverRef,
+ },
+ {
+ "rel": "bookmark",
+ "href": common.remove_version_from_href(serverRef),
+ },
+ ]
+ }
except KeyError:
return
@@ -104,20 +119,26 @@ 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"])
+
+ image["links"] = [
+ {
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "href": bookmark,
+ },
- image["links"] = [{
- "rel": "self",
- "href": href,
- },
- {
- "rel": "bookmark",
- "type": "application/json",
- "href": href,
- },
- {
- "rel": "bookmark",
- "type": "application/xml",
- "href": href,
- }]
+ ]
+
+ if detail:
+ image["metadata"] = image_obj.get("properties", {})
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/limits.py b/nova/api/openstack/views/limits.py
index 934b4921a..f603d7cb4 100644
--- a/nova/api/openstack/views/limits.py
+++ b/nova/api/openstack/views/limits.py
@@ -15,9 +15,11 @@
# License for the specific language governing permissions and limitations
# under the License.
+import datetime
import time
from nova.api.openstack import common
+from nova import utils
class ViewBuilder(object):
@@ -113,10 +115,12 @@ class ViewBuilderV11(ViewBuilder):
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": rate_limit["resetTime"],
+ "next-available": utils.isotime(at=next_avail),
}
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
index cbfa5aae7..7131db088 100644
--- a/nova/api/openstack/views/servers.py
+++ b/nova/api/openstack/views/servers.py
@@ -77,13 +77,12 @@ class ViewBuilder(object):
inst_dict = {
'id': inst['id'],
'name': inst['display_name'],
- 'addresses': self.addresses_builder.build(inst),
'status': power_mapping[inst.get('state')]}
ctxt = nova.context.get_admin_context()
compute_api = nova.compute.API()
- if compute_api.has_finished_migration(ctxt, inst['id']):
+ if compute_api.has_finished_migration(ctxt, inst['uuid']):
inst_dict['status'] = 'RESIZE-CONFIRM'
# Return the metadata as a dictionary
@@ -98,10 +97,15 @@ class ViewBuilder(object):
self._build_image(inst_dict, inst)
self._build_flavor(inst_dict, inst)
+ self._build_addresses(inst_dict, inst)
inst_dict['uuid'] = inst['uuid']
return dict(server=inst_dict)
+ def _build_addresses(self, response, inst):
+ """Return the addresses sub-resource of a server."""
+ raise NotImplementedError()
+
def _build_image(self, response, inst):
"""Return the image sub-resource of a server."""
raise NotImplementedError()
@@ -128,6 +132,9 @@ class ViewBuilderV10(ViewBuilder):
if 'instance_type' in dict(inst):
response['flavorId'] = inst['instance_type']['flavorid']
+ def _build_addresses(self, response, inst):
+ response['addresses'] = self.addresses_builder.build(inst)
+
class ViewBuilderV11(ViewBuilder):
"""Model an Openstack API V1.0 server response."""
@@ -151,11 +158,16 @@ class ViewBuilderV11(ViewBuilder):
flavor_ref = self.flavor_builder.generate_href(flavor_id)
response["flavorRef"] = flavor_ref
+ def _build_addresses(self, response, inst):
+ interfaces = inst.get('virtual_interfaces', [])
+ response['addresses'] = self.addresses_builder.build(interfaces)
+
def _build_extra(self, response, inst):
self._build_links(response, inst)
def _build_links(self, response, inst):
href = self.generate_href(inst["id"])
+ bookmark = self.generate_bookmark(inst["id"])
links = [
{
@@ -164,13 +176,7 @@ class ViewBuilderV11(ViewBuilder):
},
{
"rel": "bookmark",
- "type": "application/json",
- "href": href,
- },
- {
- "rel": "bookmark",
- "type": "application/xml",
- "href": href,
+ "href": bookmark,
},
]
@@ -179,3 +185,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/views/versions.py b/nova/api/openstack/views/versions.py
index d0145c94a..9fa8f49dc 100644
--- a/nova/api/openstack/views/versions.py
+++ b/nova/api/openstack/views/versions.py
@@ -36,6 +36,7 @@ class ViewBuilder(object):
version = {
"id": version_data["id"],
"status": version_data["status"],
+ "updated": version_data["updated"],
"links": self._build_links(version_data),
}
@@ -56,4 +57,4 @@ class ViewBuilder(object):
def generate_href(self, version_number):
"""Create an url that refers to a specific version_number."""
- return os.path.join(self.base_url, version_number)
+ return os.path.join(self.base_url, version_number) + '/'
diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py
index 5b6e3cb1d..a28443d12 100644
--- a/nova/api/openstack/wsgi.py
+++ b/nova/api/openstack/wsgi.py
@@ -13,6 +13,7 @@ from nova import wsgi
XMLNS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0'
XMLNS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
+XMLNS_ATOM = 'http://www.w3.org/2005/Atom'
LOG = logging.getLogger('nova.api.openstack.wsgi')
@@ -20,21 +21,22 @@ LOG = logging.getLogger('nova.api.openstack.wsgi')
class Request(webob.Request):
"""Add some Openstack API-specific logic to the base webob.Request."""
- def best_match_content_type(self):
+ def best_match_content_type(self, supported_content_types=None):
"""Determine the requested response content-type.
Based on the query extension then the Accept header.
"""
- supported = ('application/json', 'application/xml')
+ supported_content_types = supported_content_types or \
+ ('application/json', 'application/xml')
parts = self.path.rsplit('.', 1)
if len(parts) > 1:
ctype = 'application/{0}'.format(parts[1])
- if ctype in supported:
+ if ctype in supported_content_types:
return ctype
- bm = self.accept.best_match(supported)
+ bm = self.accept.best_match(supported_content_types)
# default to application/json if we don't find a preference
return bm or 'application/json'
@@ -46,38 +48,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 +105,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 +136,71 @@ class XMLDeserializer(TextDeserializer):
listnames)
return result
+ def find_first_child_named(self, parent, name):
+ """Search a nodes children for the first child with a given name"""
+ for node in parent.childNodes:
+ if node.nodeName == name:
+ return node
+ return None
+
+ def find_children_named(self, parent, name):
+ """Return all of a nodes children who have the given name"""
+ for node in parent.childNodes:
+ if node.nodeName == name:
+ yield node
+
+ def extract_text(self, node):
+ """Get the text field contained by the given node"""
+ if len(node.childNodes) == 1:
+ child = node.childNodes[0]
+ if child.nodeType == child.TEXT_NODE:
+ return child.nodeValue
+ return ""
+
+ def default(self, datastring):
+ return {'body': self._from_xml(datastring)}
+
+
+class MetadataXMLDeserializer(XMLDeserializer):
+
+ def extract_metadata(self, metadata_node):
+ """Marshal the metadata attribute of a parsed request"""
+ if metadata_node is None:
+ return None
+ metadata = {}
+ for meta_node in self.find_children_named(metadata_node, "meta"):
+ key = meta_node.getAttribute("key")
+ metadata[key] = self.extract_text(meta_node)
+ return metadata
+
+
+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
+ def __init__(self, body_deserializers=None, headers_deserializer=None,
+ supported_content_types=None):
- """
- self.deserializers = {
+ self.supported_content_types = supported_content_types or \
+ ('application/json', 'application/xml')
+
+ 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,31 +214,47 @@ 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:
+ 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.deserializers[content_type]
+ return self.body_deserializers[content_type]
except (KeyError, TypeError):
raise exception.InvalidContentType(content_type=content_type)
def get_expected_content_type(self, request):
- return request.best_match_content_type()
+ return request.best_match_content_type(self.supported_content_types)
def get_action_args(self, request_environment):
"""Parse dictionary created by routes library."""
@@ -195,20 +276,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)
@@ -232,13 +311,21 @@ class XMLDictSerializer(DictSerializer):
doc = minidom.Document()
node = self._to_xml_node(doc, self.metadata, root_key, data[root_key])
- self._add_xmlns(node)
+ return self.to_xml_string(node)
- return node.toprettyxml(indent=' ', encoding='utf-8')
+ def to_xml_string(self, node, has_atom=False):
+ self._add_xmlns(node, has_atom)
+ return node.toprettyxml(indent=' ', encoding='UTF-8')
- def _add_xmlns(self, node):
+ #NOTE (ameade): the has_atom should be removed after all of the
+ # xml serializers and view builders have been updated to the current
+ # spec that required all responses include the xmlns:atom, the has_atom
+ # flag is to prevent current tests from breaking
+ def _add_xmlns(self, node, has_atom=False):
if self.xmlns is not None:
node.setAttribute('xmlns', self.xmlns)
+ if has_atom:
+ node.setAttribute('xmlns:atom', "http://www.w3.org/2005/Atom")
def _to_xml_node(self, doc, metadata, nodename, data):
"""Recursive method to convert data members to XML nodes."""
@@ -294,20 +381,38 @@ class XMLDictSerializer(DictSerializer):
result.appendChild(node)
return result
+ def _create_link_nodes(self, xml_doc, links):
+ link_nodes = []
+ for link in links:
+ link_node = xml_doc.createElement('atom:link')
+ link_node.setAttribute('rel', link['rel'])
+ link_node.setAttribute('href', link['href'])
+ link_nodes.append(link_node)
+ return link_nodes
+
+
+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 +422,22 @@ 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
+ if data is not None:
+ 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,16 +454,19 @@ 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):
@@ -362,20 +476,24 @@ class Resource(wsgi.Application):
"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)
+ return faults.Fault(webob.exc.HTTPBadRequest(explanation=msg))
except exception.MalformedRequestBody:
msg = _("Malformed request body")
return faults.Fault(webob.exc.HTTPBadRequest(explanation=msg))
- action_result = self.dispatch(request, action, 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)
+ try:
+ action_result = self.dispatch(request, action, args)
+ except webob.exc.HTTPException as ex:
+ LOG.info(_("HTTP exception thrown: %s"), unicode(ex))
+ action_result = faults.Fault(ex)
+
+ if type(action_result) is dict or action_result is None:
+ response = self.serializer.serialize(action_result,
+ accept,
+ action=action)
else:
response = action_result
@@ -394,4 +512,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..f7fd87bcd 100644
--- a/nova/api/openstack/zones.py
+++ b/nova/api/openstack/zones.py
@@ -27,7 +27,6 @@ from nova.scheduler import api
from nova.api.openstack import create_instance_helper as helper
from nova.api.openstack import common
-from nova.api.openstack import faults
from nova.api.openstack import wsgi
@@ -127,11 +126,8 @@ class Controller(object):
Returns a reservation ID (a UUID).
"""
result = None
- try:
- extra_values, result = self.helper.create_instance(req, body,
- self.compute_api.create_all_at_once)
- except faults.Fault, f:
- return f
+ extra_values, result = self.helper.create_instance(req, body,
+ self.compute_api.create_all_at_once)
reservation_id = result
return {'reservation_id': reservation_id}
@@ -196,14 +192,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)