diff options
Diffstat (limited to 'nova/api')
30 files changed, 1423 insertions, 343 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 acfd1361c..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 @@ -179,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', @@ -307,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'] @@ -441,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): @@ -526,7 +660,7 @@ class CloudController(object): msg = "Revoke security group ingress %s" LOG.audit(_(msg), security_group['name'], context=context) - criteria = self._revoke_rule_args_to_dict(context, **kwargs) + criteria = self._rule_args_to_dict(context, kwargs)[0] if criteria is None: raise exception.ApiError(_("Not enough parameters to build a " "valid rule.")) @@ -567,21 +701,34 @@ class CloudController(object): msg = "Authorize security group ingress %s" LOG.audit(_(msg), security_group['name'], context=context) - values = self._revoke_rule_args_to_dict(context, **kwargs) - if values is None: - raise exception.ApiError(_("Not enough parameters to build a " - "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) + 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=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): @@ -686,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'] @@ -708,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 @@ -772,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) @@ -784,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 == []: @@ -808,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 @@ -869,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} @@ -956,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']) @@ -1040,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): @@ -1131,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): @@ -1155,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, @@ -1209,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/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 e3201b14f..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.""" diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py index 3e95aedf3..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 @@ -36,20 +35,20 @@ class Controller(object): 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, **kwargs): """ Returns a single backup schedule for a given instance """ - return faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() 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, **kwargs): """ Deletes an existing backup schedule """ - return faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() def create_resource(): diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 79969d393..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] @@ -137,15 +142,22 @@ def get_id_from_href(href): def remove_version_from_href(href): - """Removes the api version from the 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: - #matches /v#.# - new_href = re.sub(r'[/][v][0-9]*.[0-9]*', '', href) + #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') @@ -155,3 +167,28 @@ def remove_version_from_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 7a43fba96..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,8 +85,8 @@ 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(): diff --git a/nova/api/openstack/contrib/multinic.py b/nova/api/openstack/contrib/multinic.py index 841061721..da8dcee5d 100644 --- a/nova/api/openstack/contrib/multinic.py +++ b/nova/api/openstack/contrib/multinic.py @@ -16,6 +16,7 @@ """The multinic extension.""" from webob import exc +import webob from nova import compute from nova import log as logging @@ -103,7 +104,7 @@ class Multinic(extensions.ExtensionDescriptor): except Exception, e: LOG.exception(_("Error in addFixedIp %s"), e) return faults.Fault(exc.HTTPBadRequest()) - return exc.HTTPAccepted() + return webob.Response(status_int=202) def _remove_fixed_ip(self, input_dict, req, id): """Removes an IP from an instance.""" @@ -122,4 +123,4 @@ class Multinic(extensions.ExtensionDescriptor): except Exception, e: LOG.exception(_("Error in removeFixedIp %s"), e) return faults.Fault(exc.HTTPBadRequest()) - return exc.HTTPAccepted() + return webob.Response(status_int=202) diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py index e5e2c5b50..867fe301e 100644 --- a/nova/api/openstack/contrib/volumes.py +++ b/nova/api/openstack/contrib/volumes.py @@ -16,15 +16,18 @@ """The volumes extension.""" from webob import exc +import webob from nova import compute from nova import exception from nova import flags from nova import log as logging +from nova import quota from nova import volume from nova.api.openstack import common from nova.api.openstack import extensions from nova.api.openstack import faults +from nova.api.openstack import servers LOG = logging.getLogger("nova.api.volumes") @@ -104,7 +107,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 +282,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.""" @@ -296,6 +299,53 @@ class VolumeAttachmentController(object): return {'volumeAttachments': res} +class BootFromVolumeController(servers.ControllerV11): + """The boot from volume API controller for the Openstack API.""" + + def _create_instance(self, context, instance_type, image_href, **kwargs): + try: + return self.compute_api.create(context, instance_type, + image_href, **kwargs) + except quota.QuotaError as error: + self.helper._handle_quota_error(error) + except exception.ImageNotFound as error: + msg = _("Can not find requested image") + raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + + def create(self, req, body): + """ Creates a new server for a given user """ + extra_values = None + try: + + def get_kwargs(context, instance_type, image_href, **kwargs): + kwargs['context'] = context + kwargs['instance_type'] = instance_type + kwargs['image_href'] = image_href + return kwargs + + extra_values, kwargs = self.helper.create_instance(req, body, + get_kwargs) + + block_device_mapping = body['server'].get('block_device_mapping') + kwargs['block_device_mapping'] = block_device_mapping + + instances = self._create_instance(**kwargs) + except faults.Fault, f: + return f + + # 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['server']['adminPass'] = extra_values['password'] + return server + + class Volumes(extensions.ExtensionDescriptor): def get_name(self): return "Volumes" @@ -329,4 +379,8 @@ class Volumes(extensions.ExtensionDescriptor): collection_name='servers')) resources.append(res) + res = extensions.ResourceExtension('os-volumes_boot', + BootFromVolumeController()) + resources.append(res) + return resources diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 2654e3c40..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,28 +96,32 @@ 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') - min_count = body['server'].get('min_count') - max_count = body['server'].get('max_count') + 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'. @@ -142,7 +148,7 @@ 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, @@ -153,8 +159,10 @@ class CreateInstanceHelper(object): 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): @@ -277,7 +285,7 @@ class CreateInstanceHelper(object): return password -class ServerXMLDeserializer(wsgi.XMLDeserializer): +class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer): """ Deserializer to handle xml-formatted server create requests. @@ -294,11 +302,12 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): 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) @@ -306,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 6fab13147..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,65 @@ 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] body_serializers = { - 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns), + 'application/xml': xml_serializer, } serializer = wsgi.ResponseSerializer(body_serializers) diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py index 4f33844fa..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,20 +96,54 @@ 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): super(ImageMetadataXMLSerializer, self).__init__(xmlns=xmlns) @@ -144,6 +177,9 @@ 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] @@ -158,11 +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(): + headers_serializer = HeadersSerializer() + + body_deserializers = { + 'application/xml': ImageMetadataXMLDeserializer(), + } + body_serializers = { 'application/xml': ImageMetadataXMLSerializer(), } - serializer = wsgi.ResponseSerializer(body_serializers) + serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) + deserializer = wsgi.RequestDeserializer(body_deserializers) - return wsgi.Resource(Controller(), serializer=serializer) + return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index d0317583e..30e4fd389 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -25,7 +25,6 @@ from nova import flags import nova.image from nova import log 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 @@ -35,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): @@ -62,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): @@ -78,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)) diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py index 23e5432d6..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,10 +153,11 @@ def create_resource(): }, } - body_serializers = { - 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, - xmlns=wsgi.XMLNS_V10), - } - serializer = wsgi.ResponseSerializer(body_serializers) + 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(), serializer=serializer) + return wsgi.Resource(controller, serializer=serializer) diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index d08287f6b..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,9 +150,13 @@ def create_resource(version='1.0'): }, } + xml_serializer = { + '1.0': wsgi.XMLDictSerializer(xmlns=xmlns, metadata=metadata), + '1.1': LimitsXMLSerializer(), + }[version] + body_serializers = { - 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns, - metadata=metadata), + 'application/xml': xml_serializer, } serializer = wsgi.ResponseSerializer(body_serializers) @@ -119,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`. @@ -224,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): @@ -271,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`. @@ -280,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. @@ -305,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): """ @@ -388,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 3b9169f81..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 diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 12af44a8d..d7cabb067 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): @@ -88,8 +89,7 @@ class Controller(object): 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) @@ -99,20 +99,16 @@ 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()) + raise exc.HTTPNotFound() def create(self, req, body): """ Creates a new server for a given user """ extra_values = None result = None - try: - extra_values, instances = self.helper.create_instance( - req, body, self.compute_api.create) - except faults.Fault, f: - return f + extra_values, instances = self.helper.create_instance( + req, body, self.compute_api.create) # 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 @@ -121,8 +117,7 @@ class Controller(object): 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 @@ -133,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 = {} @@ -148,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() @@ -172,7 +167,7 @@ class Controller(object): 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() @@ -182,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): @@ -190,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() @@ -201,23 +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) - return faults.Fault(exc.HTTPBadRequest()) - return exc.HTTPAccepted() + raise exc.HTTPBadRequest() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def lock(self, req, id): @@ -232,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): @@ -248,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): @@ -263,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): @@ -278,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): @@ -293,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): @@ -305,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): @@ -317,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): @@ -329,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): @@ -341,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): @@ -353,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): @@ -365,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): @@ -375,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): @@ -385,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): @@ -417,8 +412,8 @@ class ControllerV10(Controller): try: self.compute_api.delete(req.environ['nova.context'], id) except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() + raise exc.HTTPNotFound() + return webob.Response(status_int=202) def _image_ref_from_req_data(self, data): return data['server']['imageId'] @@ -426,10 +421,10 @@ class ControllerV10(Controller): 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) @@ -441,40 +436,33 @@ 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'] - instance_id = int(instance_id) try: image_id = info["rebuild"]["imageId"] 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 + msg = _("Instance %s 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 """ @@ -489,7 +477,7 @@ class ControllerV11(Controller): try: self.compute_api.delete(req.environ['nova.context'], id) except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) + raise exc.HTTPNotFound() def _image_ref_from_req_data(self, data): return data['server']['imageRef'] @@ -498,16 +486,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 @@ -519,7 +509,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) @@ -531,7 +521,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.""" @@ -542,14 +532,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 """ @@ -561,22 +551,21 @@ 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'] - instance_id = int(instance_id) try: image_href = info["rebuild"]["imageRef"] 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") @@ -590,13 +579,11 @@ class ControllerV11(Controller): self.compute_api.rebuild(context, instance_id, image_href, name, metadata, personalities) except exception.BuildInProgress: - msg = _("Instance %d is currently being rebuilt.") % instance_id + msg = _("Instance %s 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 diff --git a/nova/api/openstack/shared_ip_groups.py b/nova/api/openstack/shared_ip_groups.py index cf2ddbabb..54d0a8334 100644 --- a/nova/api/openstack/shared_ip_groups.py +++ b/nova/api/openstack/shared_ip_groups.py @@ -17,7 +17,6 @@ from webob import exc -from nova.api.openstack import faults from nova.api.openstack import wsgi @@ -26,27 +25,27 @@ class Controller(object): 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, **kwargs): """ Shows in-depth information on a specific Shared IP Group """ - raise faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() 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, **kwargs): """ Deletes a Shared IP Group """ - raise faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() 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, **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 6ae1eaf2a..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)) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index a634c3267..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 = { @@ -32,11 +37,19 @@ class Versions(wsgi.Resource): } body_serializers = { - 'application/xml': wsgi.XMLDictSerializer(metadata=metadata), + 'application/atom+xml': VersionsAtomSerializer(metadata=metadata), + 'application/xml': VersionsXMLSerializer(metadata=metadata), } serializer = wsgi.ResponseSerializer(body_serializers) - wsgi.Resource.__init__(self, None, serializer=serializer) + 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.""" @@ -44,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 b59eb4751..ddbf7a144 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -15,18 +15,22 @@ # 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) @@ -40,11 +44,46 @@ class ViewBuilderV10(ViewBuilder): class ViewBuilderV11(ViewBuilder): - def build(self, inst): - # TODO(tr3buchet) - this shouldn't be hard coded to 4... - private_ips = utils.get_from_path(inst, 'fixed_ips/address') - private_ips = [dict(version=4, addr=a) for a in private_ips] - public_ips = utils.get_from_path(inst, - 'fixed_ips/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/images.py b/nova/api/openstack/views/images.py index 5c0510377..873ce212a 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -121,16 +121,20 @@ class ViewBuilderV11(ViewBuilder): href = self.generate_href(image_obj["id"]) bookmark = self.generate_bookmark(image_obj["id"]) - image["links"] = [{ - "rel": "self", - "href": href, - }] + image["links"] = [ + { + "rel": "self", + "href": href, + }, + { + "rel": "bookmark", + "href": bookmark, + }, + + ] if detail: image["metadata"] = image_obj.get("properties", {}) - image["links"].append({"rel": "bookmark", - "href": bookmark, - }) return image 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 67fb6a84e..be25e1e40 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -50,7 +50,7 @@ class ViewBuilder(object): else: server = self._build_simple(inst) - self._build_extra(server, inst) + self._build_extra(server['server'], inst) return server @@ -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,14 @@ 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() @@ -117,6 +120,9 @@ class ViewBuilder(object): class ViewBuilderV10(ViewBuilder): """Model an Openstack API V1.0 server response.""" + def _build_extra(self, response, inst): + response['uuid'] = inst['uuid'] + def _build_image(self, response, inst): if 'image_ref' in dict(inst): image_ref = inst['image_ref'] @@ -128,6 +134,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.""" @@ -138,21 +147,54 @@ class ViewBuilderV11(ViewBuilder): self.image_builder = image_builder self.base_url = base_url + def _build_detail(self, inst): + response = super(ViewBuilderV11, self)._build_detail(inst) + response['server']['created'] = inst['created_at'] + response['server']['updated'] = inst['updated_at'] + if 'status' in response['server']: + if response['server']['status'] == "ACTIVE": + response['server']['progress'] = 100 + elif response['server']['status'] == "BUILD": + response['server']['progress'] = 0 + return response + def _build_image(self, response, inst): if 'image_ref' in dict(inst): image_href = inst['image_ref'] - if str(image_href).isdigit(): - image_href = int(image_href) - response['imageRef'] = image_href + image_id = str(common.get_id_from_href(image_href)) + _bookmark = self.image_builder.generate_bookmark(image_id) + response['image'] = { + "id": image_id, + "links": [ + { + "rel": "bookmark", + "href": _bookmark, + }, + ] + } def _build_flavor(self, response, inst): if "instance_type" in dict(inst): flavor_id = inst["instance_type"]['flavorid'] flavor_ref = self.flavor_builder.generate_href(flavor_id) - response["flavorRef"] = flavor_ref + flavor_bookmark = self.flavor_builder.generate_bookmark(flavor_id) + response["flavor"] = { + "id": str(common.get_id_from_href(flavor_ref)), + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ] + } + + 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) + response['uuid'] = inst['uuid'] def _build_links(self, response, inst): href = self.generate_href(inst["id"]) @@ -169,7 +211,7 @@ class ViewBuilderV11(ViewBuilder): }, ] - response["server"]["links"] = links + response["links"] = links def generate_href(self, server_id): """Create an url that refers to a specific 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 c3f841aa5..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' @@ -134,10 +136,44 @@ 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""" @@ -151,7 +187,12 @@ class RequestHeadersDeserializer(ActionDispatcher): class RequestDeserializer(object): """Break up a Request object into more useful pieces.""" - def __init__(self, body_deserializers=None, headers_deserializer=None): + def __init__(self, body_deserializers=None, headers_deserializer=None, + supported_content_types=None): + + self.supported_content_types = supported_content_types or \ + ('application/json', 'application/xml') + self.body_deserializers = { 'application/xml': XMLDeserializer(), 'application/json': JSONDeserializer(), @@ -213,7 +254,7 @@ class RequestDeserializer(object): 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.""" @@ -390,8 +431,9 @@ class ResponseSerializer(object): def serialize_body(self, response, data, content_type, action): response.headers['Content-Type'] = content_type - serializer = self.get_body_serializer(content_type) - response.body = serializer.serialize(data, action) + if data is not None: + serializer = self.get_body_serializer(content_type) + response.body = serializer.serialize(data, action) def get_body_serializer(self, content_type): try: @@ -412,6 +454,7 @@ class Resource(wsgi.Application): serialized by requested content type. """ + def __init__(self, controller, deserializer=None, serializer=None): """ :param controller: object that implement methods created by routes lib @@ -436,14 +479,17 @@ class Resource(wsgi.Application): 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, args) + 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) - #TODO(bcwaldon): find a more elegant way to pass through non-dict types if type(action_result) is dict or action_result is None: response = self.serializer.serialize(action_result, accept, diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index 2e02ec380..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} |
