diff options
| author | William Wolf <throughnothing@gmail.com> | 2011-07-26 10:33:05 +0000 |
|---|---|---|
| committer | Tarmac <> | 2011-07-26 10:33:05 +0000 |
| commit | cb631be09c36d29ddb6e89a647c5161bc43c4aa7 (patch) | |
| tree | 5be2f5dfaeaee8dbc268f3c2f8cb26d56bd4b78d /nova | |
| parent | 85522bba82a4139a89915bba99865a50fd9b8f58 (diff) | |
| parent | 5df221e970d8b060423034fa627735c5c24fce5d (diff) | |
Merge diablo-3 development from trunk (rev1322)
Diffstat (limited to 'nova')
147 files changed, 12667 insertions, 4144 deletions
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 890d57fe7..cf1734281 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -262,6 +262,8 @@ class Authorizer(wsgi.Middleware): 'TerminateInstances': ['projectmanager', 'sysadmin'], 'RebootInstances': ['projectmanager', 'sysadmin'], 'UpdateInstance': ['projectmanager', 'sysadmin'], + 'StartInstances': ['projectmanager', 'sysadmin'], + 'StopInstances': ['projectmanager', 'sysadmin'], 'DeleteVolume': ['projectmanager', 'sysadmin'], 'DescribeImages': ['all'], 'DeregisterImage': ['projectmanager', 'sysadmin'], @@ -269,6 +271,7 @@ class Authorizer(wsgi.Middleware): 'DescribeImageAttribute': ['all'], 'ModifyImageAttribute': ['projectmanager', 'sysadmin'], 'UpdateImage': ['projectmanager', 'sysadmin'], + 'CreateImage': ['projectmanager', 'sysadmin'], }, 'AdminController': { # All actions have the same permission: ['none'] (the default) @@ -325,13 +328,13 @@ class Executor(wsgi.Application): except exception.VolumeNotFound as ex: LOG.info(_('VolumeNotFound raised: %s'), unicode(ex), context=context) - ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x') + ec2_id = ec2utils.id_to_ec2_vol_id(ex.volume_id) message = _('Volume %s not found') % ec2_id return self._error(req, context, type(ex).__name__, message) except exception.SnapshotNotFound as ex: LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex), context=context) - ec2_id = ec2utils.id_to_ec2_id(ex.snapshot_id, 'snap-%08x') + ec2_id = ec2utils.id_to_ec2_snap_id(ex.snapshot_id) message = _('Snapshot %s not found') % ec2_id return self._error(req, context, type(ex).__name__, message) except exception.NotFound as ex: diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 9aaf37a2d..10720a804 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -27,6 +27,7 @@ import netaddr import os import urllib import tempfile +import time import shutil from nova import compute @@ -75,6 +76,95 @@ def _gen_key(context, user_id, key_name): return {'private_key': private_key, 'fingerprint': fingerprint} +# TODO(yamahata): hypervisor dependent default device name +_DEFAULT_ROOT_DEVICE_NAME = '/dev/sda1' + + +def _parse_block_device_mapping(bdm): + """Parse BlockDeviceMappingItemType into flat hash + BlockDevicedMapping.<N>.DeviceName + BlockDevicedMapping.<N>.Ebs.SnapshotId + BlockDevicedMapping.<N>.Ebs.VolumeSize + BlockDevicedMapping.<N>.Ebs.DeleteOnTermination + BlockDevicedMapping.<N>.Ebs.NoDevice + BlockDevicedMapping.<N>.VirtualName + => remove .Ebs and allow volume id in SnapshotId + """ + ebs = bdm.pop('ebs', None) + if ebs: + ec2_id = ebs.pop('snapshot_id', None) + if ec2_id: + id = ec2utils.ec2_id_to_id(ec2_id) + if ec2_id.startswith('snap-'): + bdm['snapshot_id'] = id + elif ec2_id.startswith('vol-'): + bdm['volume_id'] = id + ebs.setdefault('delete_on_termination', True) + bdm.update(ebs) + return bdm + + +def _properties_get_mappings(properties): + return ec2utils.mappings_prepend_dev(properties.get('mappings', [])) + + +def _format_block_device_mapping(bdm): + """Contruct BlockDeviceMappingItemType + {'device_name': '...', 'snapshot_id': , ...} + => BlockDeviceMappingItemType + """ + keys = (('deviceName', 'device_name'), + ('virtualName', 'virtual_name')) + item = {} + for name, k in keys: + if k in bdm: + item[name] = bdm[k] + if bdm.get('no_device'): + item['noDevice'] = True + if ('snapshot_id' in bdm) or ('volume_id' in bdm): + ebs_keys = (('snapshotId', 'snapshot_id'), + ('snapshotId', 'volume_id'), # snapshotId is abused + ('volumeSize', 'volume_size'), + ('deleteOnTermination', 'delete_on_termination')) + ebs = {} + for name, k in ebs_keys: + if k in bdm: + if k == 'snapshot_id': + ebs[name] = ec2utils.id_to_ec2_snap_id(bdm[k]) + elif k == 'volume_id': + ebs[name] = ec2utils.id_to_ec2_vol_id(bdm[k]) + else: + ebs[name] = bdm[k] + assert 'snapshotId' in ebs + item['ebs'] = ebs + return item + + +def _format_mappings(properties, result): + """Format multiple BlockDeviceMappingItemType""" + mappings = [{'virtualName': m['virtual'], 'deviceName': m['device']} + for m in _properties_get_mappings(properties) + if (m['virtual'] == 'swap' or + m['virtual'].startswith('ephemeral'))] + + block_device_mapping = [_format_block_device_mapping(bdm) for bdm in + properties.get('block_device_mapping', [])] + + # NOTE(yamahata): overwrite mappings with block_device_mapping + for bdm in block_device_mapping: + for i in range(len(mappings)): + if bdm['deviceName'] == mappings[i]['deviceName']: + del mappings[i] + break + mappings.append(bdm) + + # NOTE(yamahata): trim ebs.no_device == true. Is this necessary? + mappings = [bdm for bdm in mappings if not (bdm.get('noDevice', False))] + + if mappings: + result['blockDeviceMapping'] = mappings + + class CloudController(object): """ CloudController provides the critical dispatch between inbound API calls through the endpoint and messages @@ -86,8 +176,7 @@ class CloudController(object): self.volume_api = volume.API() self.compute_api = compute.API( network_api=self.network_api, - volume_api=self.volume_api, - hostname_factory=ec2utils.id_to_ec2_id) + volume_api=self.volume_api) self.setup() def __str__(self): @@ -121,8 +210,8 @@ class CloudController(object): result = {} for instance in self.compute_api.get_all(context, project_id=project_id): - if instance['fixed_ip']: - line = '%s slots=%d' % (instance['fixed_ip']['address'], + if instance['fixed_ips']: + line = '%s slots=%d' % (instance['fixed_ips'][0]['address'], instance['vcpus']) key = str(instance['key_name']) if key in result: @@ -152,7 +241,7 @@ class CloudController(object): # This ensures that all attributes of the instance # are populated. - instance_ref = db.instance_get(ctxt, instance_ref['id']) + instance_ref = db.instance_get(ctxt, instance_ref[0]['id']) mpi = self._get_mpi_data(ctxt, instance_ref['project_id']) if instance_ref['key_name']: @@ -167,6 +256,9 @@ class CloudController(object): instance_ref['id']) ec2_id = ec2utils.id_to_ec2_id(instance_ref['id']) image_ec2_id = self.image_ec2_id(instance_ref['image_ref']) + security_groups = db.security_group_get_by_instance(ctxt, + instance_ref['id']) + security_groups = [x['name'] for x in security_groups] data = { 'user-data': base64.b64decode(instance_ref['user_data']), 'meta-data': { @@ -177,7 +269,7 @@ class CloudController(object): # TODO(vish): replace with real data 'ami': 'sda1', 'ephemeral0': 'sda2', - 'root': '/dev/sda1', + 'root': _DEFAULT_ROOT_DEVICE_NAME, 'swap': 'sda3'}, 'hostname': hostname, 'instance-action': 'none', @@ -190,7 +282,7 @@ class CloudController(object): 'public-ipv4': floating_ip or '', 'public-keys': keys, 'reservation-id': instance_ref['reservation_id'], - 'security-groups': '', + 'security-groups': security_groups, 'mpi': mpi}} for image_type in ['kernel', 'ramdisk']: @@ -305,9 +397,8 @@ class CloudController(object): def _format_snapshot(self, context, snapshot): s = {} - s['snapshotId'] = ec2utils.id_to_ec2_id(snapshot['id'], 'snap-%08x') - s['volumeId'] = ec2utils.id_to_ec2_id(snapshot['volume_id'], - 'vol-%08x') + s['snapshotId'] = ec2utils.id_to_ec2_snap_id(snapshot['id']) + s['volumeId'] = ec2utils.id_to_ec2_vol_id(snapshot['volume_id']) s['status'] = snapshot['status'] s['startTime'] = snapshot['created_at'] s['progress'] = snapshot['progress'] @@ -391,15 +482,21 @@ class CloudController(object): pass return True - def describe_security_groups(self, context, group_name=None, **kwargs): + def describe_security_groups(self, context, group_name=None, group_id=None, + **kwargs): self.compute_api.ensure_default_security_group(context) - if group_name: + if group_name or group_id: groups = [] - for name in group_name: - group = db.security_group_get_by_name(context, - context.project_id, - name) - groups.append(group) + if group_name: + for name in group_name: + group = db.security_group_get_by_name(context, + context.project_id, + name) + groups.append(group) + if group_id: + for gid in group_id: + group = db.security_group_get(context, gid) + groups.append(group) elif context.is_admin: groups = db.security_group_get_all(context) else: @@ -433,7 +530,52 @@ class CloudController(object): g['ipPermissions'] += [r] return g - def _revoke_rule_args_to_dict(self, context, to_port=None, from_port=None, + def _rule_args_to_dict(self, context, kwargs): + rules = [] + if not 'groups' in kwargs and not 'ip_ranges' in kwargs: + rule = self._rule_dict_last_step(context, **kwargs) + if rule: + rules.append(rule) + return rules + if 'ip_ranges' in kwargs: + rules = self._cidr_args_split(kwargs) + finalset = [] + for rule in rules: + if 'groups' in rule: + groups_values = self._groups_args_split(rule) + for groups_value in groups_values: + finalset.append(groups_value) + else: + if rule: + finalset.append(rule) + return finalset + + def _cidr_args_split(self, kwargs): + cidr_args_split = [] + cidrs = kwargs['ip_ranges'] + for key, cidr in cidrs.iteritems(): + mykwargs = kwargs.copy() + del mykwargs['ip_ranges'] + mykwargs['cidr_ip'] = cidr['cidr_ip'] + cidr_args_split.append(mykwargs) + return cidr_args_split + + def _groups_args_split(self, kwargs): + groups_args_split = [] + groups = kwargs['groups'] + for key, group in groups.iteritems(): + mykwargs = kwargs.copy() + del mykwargs['groups'] + if 'group_name' in group: + mykwargs['source_security_group_name'] = group['group_name'] + if 'user_id' in group: + mykwargs['source_security_group_owner_id'] = group['user_id'] + if 'group_id' in group: + mykwargs['source_security_group_id'] = group['group_id'] + groups_args_split.append(mykwargs) + return groups_args_split + + def _rule_dict_last_step(self, context, to_port=None, from_port=None, ip_protocol=None, cidr_ip=None, user_id=None, source_security_group_name=None, source_security_group_owner_id=None): @@ -497,15 +639,28 @@ class CloudController(object): return True return False - def revoke_security_group_ingress(self, context, group_name, **kwargs): - LOG.audit(_("Revoke security group ingress %s"), group_name, - context=context) + def revoke_security_group_ingress(self, context, group_name=None, + group_id=None, **kwargs): + if not group_name and not group_id: + err = "Not enough parameters, need group_name or group_id" + raise exception.ApiError(_(err)) self.compute_api.ensure_default_security_group(context) - security_group = db.security_group_get_by_name(context, - context.project_id, - group_name) - - criteria = self._revoke_rule_args_to_dict(context, **kwargs) + notfound = exception.SecurityGroupNotFound + if group_name: + security_group = db.security_group_get_by_name(context, + context.project_id, + group_name) + if not security_group: + raise notfound(security_group_id=group_name) + if group_id: + security_group = db.security_group_get(context, group_id) + if not security_group: + raise notfound(security_group_id=group_id) + + msg = "Revoke security group ingress %s" + LOG.audit(_(msg), security_group['name'], context=context) + + criteria = self._rule_args_to_dict(context, kwargs)[0] if criteria is None: raise exception.ApiError(_("Not enough parameters to build a " "valid rule.")) @@ -518,7 +673,7 @@ class CloudController(object): if match: db.security_group_rule_destroy(context, rule['id']) self.compute_api.trigger_security_group_rules_refresh(context, - security_group['id']) + security_group_id=security_group['id']) return True raise exception.ApiError(_("No rule for the specified parameters.")) @@ -526,29 +681,54 @@ class CloudController(object): # Unfortunately, it seems Boto is using an old API # for these operations, so support for newer API versions # is sketchy. - def authorize_security_group_ingress(self, context, group_name, **kwargs): - LOG.audit(_("Authorize security group ingress %s"), group_name, - context=context) + def authorize_security_group_ingress(self, context, group_name=None, + group_id=None, **kwargs): + if not group_name and not group_id: + err = "Not enough parameters, need group_name or group_id" + raise exception.ApiError(_(err)) self.compute_api.ensure_default_security_group(context) - security_group = db.security_group_get_by_name(context, - context.project_id, - group_name) - - values = self._revoke_rule_args_to_dict(context, **kwargs) - if values is None: - raise exception.ApiError(_("Not enough parameters to build a " - "valid rule.")) - values['parent_group_id'] = security_group.id - - if self._security_group_rule_exists(security_group, values): - raise exception.ApiError(_('This rule already exists in group %s') - % group_name) - - security_group_rule = db.security_group_rule_create(context, values) + notfound = exception.SecurityGroupNotFound + if group_name: + security_group = db.security_group_get_by_name(context, + context.project_id, + group_name) + if not security_group: + raise notfound(security_group_id=group_name) + if group_id: + security_group = db.security_group_get(context, group_id) + if not security_group: + raise notfound(security_group_id=group_id) + + msg = "Authorize security group ingress %s" + LOG.audit(_(msg), security_group['name'], context=context) + prevalues = [] + try: + prevalues = kwargs['ip_permissions'] + except KeyError: + prevalues.append(kwargs) + postvalues = [] + for values in prevalues: + rulesvalues = self._rule_args_to_dict(context, values) + if not rulesvalues: + err = "%s Not enough parameters to build a valid rule" + raise exception.ApiError(_(err % rulesvalues)) + for values_for_rule in rulesvalues: + values_for_rule['parent_group_id'] = security_group.id + if self._security_group_rule_exists(security_group, + values_for_rule): + err = '%s - This rule already exists in group' + raise exception.ApiError(_(err) % values_for_rule) + postvalues.append(values_for_rule) + + for values_for_rule in postvalues: + security_group_rule = db.security_group_rule_create(context, + values_for_rule) self.compute_api.trigger_security_group_rules_refresh(context, - security_group['id']) + security_group_id=security_group['id']) + group = db.security_group_get_by_name(context, context.project_id, + security_group['name']) return True def _get_source_project_id(self, context, source_security_group_owner_id): @@ -583,11 +763,23 @@ class CloudController(object): return {'securityGroupSet': [self._format_security_group(context, group_ref)]} - def delete_security_group(self, context, group_name, **kwargs): + def delete_security_group(self, context, group_name=None, group_id=None, + **kwargs): + if not group_name and not group_id: + err = "Not enough parameters, need group_name or group_id" + raise exception.ApiError(_(err)) + notfound = exception.SecurityGroupNotFound + if group_name: + security_group = db.security_group_get_by_name(context, + context.project_id, + group_name) + if not security_group: + raise notfound(security_group_id=group_name) + elif group_id: + security_group = db.security_group_get(context, group_id) + if not security_group: + raise notfound(security_group_id=group_id) LOG.audit(_("Delete security group %s"), group_name, context=context) - security_group = db.security_group_get_by_name(context, - context.project_id, - group_name) db.security_group_destroy(context, security_group.id) return True @@ -641,7 +833,7 @@ class CloudController(object): instance_data = '%s[%s]' % (instance_ec2_id, volume['instance']['host']) v = {} - v['volumeId'] = ec2utils.id_to_ec2_id(volume['id'], 'vol-%08x') + v['volumeId'] = ec2utils.id_to_ec2_vol_id(volume['id']) v['status'] = volume['status'] v['size'] = volume['size'] v['availabilityZone'] = volume['availability_zone'] @@ -663,8 +855,7 @@ class CloudController(object): else: v['attachmentSet'] = [{}] if volume.get('snapshot_id') != None: - v['snapshotId'] = ec2utils.id_to_ec2_id(volume['snapshot_id'], - 'snap-%08x') + v['snapshotId'] = ec2utils.id_to_ec2_snap_id(volume['snapshot_id']) else: v['snapshotId'] = None @@ -727,7 +918,7 @@ class CloudController(object): 'instanceId': ec2utils.id_to_ec2_id(instance_id), 'requestId': context.request_id, 'status': volume['attach_status'], - 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')} + 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)} def detach_volume(self, context, volume_id, **kwargs): volume_id = ec2utils.ec2_id_to_id(volume_id) @@ -739,7 +930,7 @@ class CloudController(object): 'instanceId': ec2utils.id_to_ec2_id(instance['id']), 'requestId': context.request_id, 'status': volume['attach_status'], - 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')} + 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)} def _convert_to_set(self, lst, label): if lst is None or lst == []: @@ -763,6 +954,37 @@ class CloudController(object): assert len(i) == 1 return i[0] + def _format_instance_bdm(self, context, instance_id, root_device_name, + result): + """Format InstanceBlockDeviceMappingResponseItemType""" + root_device_type = 'instance-store' + mapping = [] + for bdm in db.block_device_mapping_get_all_by_instance(context, + instance_id): + volume_id = bdm['volume_id'] + if (volume_id is None or bdm['no_device']): + continue + + if (bdm['device_name'] == root_device_name and + (bdm['snapshot_id'] or bdm['volume_id'])): + assert not bdm['virtual_name'] + root_device_type = 'ebs' + + vol = self.volume_api.get(context, volume_id=volume_id) + LOG.debug(_("vol = %s\n"), vol) + # TODO(yamahata): volume attach time + ebs = {'volumeId': volume_id, + 'deleteOnTermination': bdm['delete_on_termination'], + 'attachTime': vol['attach_time'] or '-', + 'status': vol['status'], } + res = {'deviceName': bdm['device_name'], + 'ebs': ebs, } + mapping.append(res) + + if mapping: + result['blockDeviceMapping'] = mapping + result['rootDeviceType'] = root_device_type + def _format_instances(self, context, instance_id=None, **kwargs): # TODO(termie): this method is poorly named as its name does not imply # that it will be making a variety of database calls @@ -793,15 +1015,15 @@ class CloudController(object): 'name': instance['state_description']} fixed_addr = None floating_addr = None - if instance['fixed_ip']: - fixed_addr = instance['fixed_ip']['address'] - if instance['fixed_ip']['floating_ips']: - fixed = instance['fixed_ip'] + if instance['fixed_ips']: + fixed = instance['fixed_ips'][0] + fixed_addr = fixed['address'] + if fixed['floating_ips']: floating_addr = fixed['floating_ips'][0]['address'] - if instance['fixed_ip']['network'] and 'use_v6' in kwargs: + if fixed['network'] and 'use_v6' in kwargs: i['dnsNameV6'] = ipv6.to_global( - instance['fixed_ip']['network']['cidr_v6'], - instance['mac_address'], + fixed['network']['cidr_v6'], + fixed['virtual_interface']['address'], instance['project_id']) i['privateDnsName'] = fixed_addr @@ -824,6 +1046,10 @@ class CloudController(object): i['amiLaunchIndex'] = instance['launch_index'] i['displayName'] = instance['display_name'] i['displayDescription'] = instance['display_description'] + i['rootDeviceName'] = (instance.get('root_device_name') or + _DEFAULT_ROOT_DEVICE_NAME) + self._format_instance_bdm(context, instance_id, + i['rootDeviceName'], i) host = instance['host'] zone = self._get_availability_zone_by_host(context, host) i['placement'] = {'availabilityZone': zone} @@ -877,7 +1103,8 @@ class CloudController(object): public_ip = self.network_api.allocate_floating_ip(context) return {'publicIp': public_ip} except rpc.RemoteError as ex: - if ex.exc_type == 'NoMoreAddresses': + # NOTE(tr3buchet) - why does this block exist? + if ex.exc_type == 'NoMoreFloatingIps': raise exception.NoMoreFloatingIps() else: raise @@ -910,23 +1137,7 @@ class CloudController(object): ramdisk = self._get_image(context, kwargs['ramdisk_id']) kwargs['ramdisk_id'] = ramdisk['id'] for bdm in kwargs.get('block_device_mapping', []): - # NOTE(yamahata) - # BlockDevicedMapping.<N>.DeviceName - # BlockDevicedMapping.<N>.Ebs.SnapshotId - # BlockDevicedMapping.<N>.Ebs.VolumeSize - # BlockDevicedMapping.<N>.Ebs.DeleteOnTermination - # BlockDevicedMapping.<N>.VirtualName - # => remove .Ebs and allow volume id in SnapshotId - ebs = bdm.pop('ebs', None) - if ebs: - ec2_id = ebs.pop('snapshot_id') - id = ec2utils.ec2_id_to_id(ec2_id) - if ec2_id.startswith('snap-'): - bdm['snapshot_id'] = id - elif ec2_id.startswith('vol-'): - bdm['volume_id'] = id - ebs.setdefault('delete_on_termination', True) - bdm.update(ebs) + _parse_block_device_mapping(bdm) image = self._get_image(context, kwargs['image_id']) @@ -994,7 +1205,7 @@ class CloudController(object): def rescue_instance(self, context, instance_id, **kwargs): """This is an extension to the normal ec2_api""" - self._do_instance(self.compute_api.rescue, contect, instnace_id) + self._do_instance(self.compute_api.rescue, context, instance_id) return True def unrescue_instance(self, context, instance_id, **kwargs): @@ -1045,12 +1256,16 @@ class CloudController(object): def _get_image(self, context, ec2_id): try: internal_id = ec2utils.ec2_id_to_id(ec2_id) - return self.image_service.show(context, internal_id) + image = self.image_service.show(context, internal_id) except (exception.InvalidEc2Id, exception.ImageNotFound): try: return self.image_service.show_by_name(context, ec2_id) except exception.NotFound: raise exception.ImageNotFound(image_id=ec2_id) + image_type = ec2_id.split('-')[0] + if self._image_type(image.get('container_format')) != image_type: + raise exception.ImageNotFound(image_id=ec2_id) + return image def _format_image(self, image): """Convert from format defined by BaseImageService to S3 format.""" @@ -1081,6 +1296,20 @@ class CloudController(object): i['imageType'] = display_mapping.get(image_type) i['isPublic'] = image.get('is_public') == True i['architecture'] = image['properties'].get('architecture') + + properties = image['properties'] + root_device_name = ec2utils.properties_root_device_name(properties) + root_device_type = 'instance-store' + for bdm in properties.get('block_device_mapping', []): + if (bdm.get('device_name') == root_device_name and + ('snapshot_id' in bdm or 'volume_id' in bdm) and + not bdm.get('no_device')): + root_device_type = 'ebs' + i['rootDeviceName'] = (root_device_name or _DEFAULT_ROOT_DEVICE_NAME) + i['rootDeviceType'] = root_device_type + + _format_mappings(properties, i) + return i def describe_images(self, context, image_id=None, **kwargs): @@ -1105,30 +1334,64 @@ class CloudController(object): self.image_service.delete(context, internal_id) return {'imageId': image_id} + def _register_image(self, context, metadata): + image = self.image_service.create(context, metadata) + image_type = self._image_type(image.get('container_format')) + image_id = self.image_ec2_id(image['id'], image_type) + return image_id + def register_image(self, context, image_location=None, **kwargs): if image_location is None and 'name' in kwargs: image_location = kwargs['name'] metadata = {'properties': {'image_location': image_location}} - image = self.image_service.create(context, metadata) - image_type = self._image_type(image.get('container_format')) - image_id = self.image_ec2_id(image['id'], - image_type) + + if 'root_device_name' in kwargs: + metadata['properties']['root_device_name'] = \ + kwargs.get('root_device_name') + + mappings = [_parse_block_device_mapping(bdm) for bdm in + kwargs.get('block_device_mapping', [])] + if mappings: + metadata['properties']['block_device_mapping'] = mappings + + image_id = self._register_image(context, metadata) msg = _("Registered image %(image_location)s with" " id %(image_id)s") % locals() LOG.audit(msg, context=context) return {'imageId': image_id} def describe_image_attribute(self, context, image_id, attribute, **kwargs): - if attribute != 'launchPermission': + def _block_device_mapping_attribute(image, result): + _format_mappings(image['properties'], result) + + def _launch_permission_attribute(image, result): + result['launchPermission'] = [] + if image['is_public']: + result['launchPermission'].append({'group': 'all'}) + + def _root_device_name_attribute(image, result): + result['rootDeviceName'] = \ + ec2utils.properties_root_device_name(image['properties']) + if result['rootDeviceName'] is None: + result['rootDeviceName'] = _DEFAULT_ROOT_DEVICE_NAME + + supported_attributes = { + 'blockDeviceMapping': _block_device_mapping_attribute, + 'launchPermission': _launch_permission_attribute, + 'rootDeviceName': _root_device_name_attribute, + } + + fn = supported_attributes.get(attribute) + if fn is None: raise exception.ApiError(_('attribute not supported: %s') % attribute) try: image = self._get_image(context, image_id) except exception.NotFound: raise exception.ImageNotFound(image_id=image_id) - result = {'imageId': image_id, 'launchPermission': []} - if image['is_public']: - result['launchPermission'].append({'group': 'all'}) + + result = {'imageId': image_id} + fn(image, result) return result def modify_image_attribute(self, context, image_id, attribute, @@ -1159,3 +1422,109 @@ class CloudController(object): internal_id = ec2utils.ec2_id_to_id(image_id) result = self.image_service.update(context, internal_id, dict(kwargs)) return result + + # TODO(yamahata): race condition + # At the moment there is no way to prevent others from + # manipulating instances/volumes/snapshots. + # As other code doesn't take it into consideration, here we don't + # care of it for now. Ostrich algorithm + def create_image(self, context, instance_id, **kwargs): + # NOTE(yamahata): name/description are ignored by register_image(), + # do so here + no_reboot = kwargs.get('no_reboot', False) + + ec2_instance_id = instance_id + instance_id = ec2utils.ec2_id_to_id(ec2_instance_id) + instance = self.compute_api.get(context, instance_id) + + # stop the instance if necessary + restart_instance = False + if not no_reboot: + state_description = instance['state_description'] + + # if the instance is in subtle state, refuse to proceed. + if state_description not in ('running', 'stopping', 'stopped'): + raise exception.InstanceNotRunning(instance_id=ec2_instance_id) + + if state_description == 'running': + restart_instance = True + self.compute_api.stop(context, instance_id=instance_id) + + # wait instance for really stopped + start_time = time.time() + while state_description != 'stopped': + time.sleep(1) + instance = self.compute_api.get(context, instance_id) + state_description = instance['state_description'] + # NOTE(yamahata): timeout and error. 1 hour for now for safety. + # Is it too short/long? + # Or is there any better way? + timeout = 1 * 60 * 60 * 60 + if time.time() > start_time + timeout: + raise exception.ApiError( + _('Couldn\'t stop instance with in %d sec') % timeout) + + src_image = self._get_image(context, instance['image_ref']) + properties = src_image['properties'] + if instance['root_device_name']: + properties['root_device_name'] = instance['root_device_name'] + + mapping = [] + bdms = db.block_device_mapping_get_all_by_instance(context, + instance_id) + for bdm in bdms: + if bdm.no_device: + continue + m = {} + for attr in ('device_name', 'snapshot_id', 'volume_id', + 'volume_size', 'delete_on_termination', 'no_device', + 'virtual_name'): + val = getattr(bdm, attr) + if val is not None: + m[attr] = val + + volume_id = m.get('volume_id') + if m.get('snapshot_id') and volume_id: + # create snapshot based on volume_id + vol = self.volume_api.get(context, volume_id=volume_id) + # NOTE(yamahata): Should we wait for snapshot creation? + # Linux LVM snapshot creation completes in + # short time, it doesn't matter for now. + snapshot = self.volume_api.create_snapshot_force( + context, volume_id=volume_id, name=vol['display_name'], + description=vol['display_description']) + m['snapshot_id'] = snapshot['id'] + del m['volume_id'] + + if m: + mapping.append(m) + + for m in _properties_get_mappings(properties): + virtual_name = m['virtual'] + if virtual_name in ('ami', 'root'): + continue + + assert (virtual_name == 'swap' or + virtual_name.startswith('ephemeral')) + device_name = m['device'] + if device_name in [b['device_name'] for b in mapping + if not b.get('no_device', False)]: + continue + + # NOTE(yamahata): swap and ephemeral devices are specified in + # AMI, but disabled for this instance by user. + # So disable those device by no_device. + mapping.append({'device_name': device_name, 'no_device': True}) + + if mapping: + properties['block_device_mapping'] = mapping + + for attr in ('status', 'location', 'id'): + src_image.pop(attr, None) + + image_id = self._register_image(context, src_image) + + if restart_instance: + self.compute_api.start(context, instance_id=instance_id) + + return {'imageId': image_id} diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index 222e1de1e..bae1e0ee5 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -34,6 +34,17 @@ def id_to_ec2_id(instance_id, template='i-%08x'): return template % instance_id +def id_to_ec2_snap_id(instance_id): + """Convert an snapshot ID (int) to an ec2 snapshot ID + (snap-[base 16 number])""" + return id_to_ec2_id(instance_id, 'snap-%08x') + + +def id_to_ec2_vol_id(instance_id): + """Convert an volume ID (int) to an ec2 volume ID (vol-[base 16 number])""" + return id_to_ec2_id(instance_id, 'vol-%08x') + + _c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))') @@ -124,3 +135,32 @@ def dict_from_dotted_str(items): args[key] = value return args + + +def properties_root_device_name(properties): + """get root device name from image meta data. + If it isn't specified, return None. + """ + root_device_name = None + + # NOTE(yamahata): see image_service.s3.s3create() + for bdm in properties.get('mappings', []): + if bdm['virtual'] == 'root': + root_device_name = bdm['device'] + + # NOTE(yamahata): register_image's command line can override + # <machine>.manifest.xml + if 'root_device_name' in properties: + root_device_name = properties['root_device_name'] + + return root_device_name + + +def mappings_prepend_dev(mappings): + """Prepend '/dev/' to 'device' entry of swap/ephemeral virtual type""" + for m in mappings: + virtual = m['virtual'] + if ((virtual == 'swap' or virtual.startswith('ephemeral')) and + (not m['device'].startswith('/'))): + m['device'] = '/dev/' + m['device'] + return mappings diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py index b70266a20..1dc275c90 100644 --- a/nova/api/ec2/metadatarequesthandler.py +++ b/nova/api/ec2/metadatarequesthandler.py @@ -35,6 +35,9 @@ FLAGS = flags.FLAGS class MetadataRequestHandler(wsgi.Application): """Serve metadata from the EC2 API.""" + def __init__(self): + self.cc = cloud.CloudController() + def print_data(self, data): if isinstance(data, dict): output = '' @@ -68,12 +71,11 @@ class MetadataRequestHandler(wsgi.Application): @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): - cc = cloud.CloudController() remote_address = req.remote_addr if FLAGS.use_forwarded_for: remote_address = req.headers.get('X-Forwarded-For', remote_address) try: - meta_data = cc.get_metadata(remote_address) + meta_data = self.cc.get_metadata(remote_address) except Exception: LOG.exception(_('Failed to get metadata for ip: %s'), remote_address) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index f24017df0..868b98a31 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -125,6 +125,10 @@ class APIRouter(base_wsgi.Router): collection={'detail': 'GET'}, member=self.server_members) + mapper.resource("ip", "ips", controller=ips.create_resource(version), + parent_resource=dict(member_name='server', + collection_name='servers')) + mapper.resource("image", "images", controller=images.create_resource(version), collection={'detail': 'GET'}) @@ -144,9 +148,6 @@ class APIRouterV10(APIRouter): def _setup_routes(self, mapper): super(APIRouterV10, self)._setup_routes(mapper, '1.0') - mapper.resource("image", "images", - controller=images.create_resource('1.0'), - collection={'detail': 'GET'}) mapper.resource("shared_ip_group", "shared_ip_groups", collection={'detail': 'GET'}, @@ -157,22 +158,23 @@ class APIRouterV10(APIRouter): parent_resource=dict(member_name='server', collection_name='servers')) - mapper.resource("ip", "ips", controller=ips.create_resource(), - collection=dict(public='GET', private='GET'), - parent_resource=dict(member_name='server', - collection_name='servers')) - class APIRouterV11(APIRouter): """Define routes specific to OpenStack API V1.1.""" def _setup_routes(self, mapper): super(APIRouterV11, self)._setup_routes(mapper, '1.1') - mapper.resource("image_meta", "meta", - controller=image_metadata.create_resource(), + image_metadata_controller = image_metadata.create_resource() + mapper.resource("image_meta", "metadata", + controller=image_metadata_controller, parent_resource=dict(member_name='image', collection_name='images')) + mapper.connect("metadata", "/images/{image_id}/metadata", + controller=image_metadata_controller, + action='update_all', + conditions={"method": ['PUT']}) + mapper.resource("server_meta", "meta", controller=server_metadata.create_resource(), parent_resource=dict(member_name='server', diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py index 0dcd37217..a13a758ab 100644 --- a/nova/api/openstack/accounts.py +++ b/nova/api/openstack/accounts.py @@ -47,10 +47,10 @@ class Controller(object): raise exception.AdminRequired() def index(self, req): - raise faults.Fault(webob.exc.HTTPNotImplemented()) + raise webob.exc.HTTPNotImplemented() def detail(self, req): - raise faults.Fault(webob.exc.HTTPNotImplemented()) + raise webob.exc.HTTPNotImplemented() def show(self, req, id): """Return data about the given account id""" @@ -65,7 +65,7 @@ class Controller(object): def create(self, req, body): """We use update with create-or-update semantics because the id comes from an external source""" - raise faults.Fault(webob.exc.HTTPNotImplemented()) + raise webob.exc.HTTPNotImplemented() def update(self, req, id, body): """This is really create or update.""" @@ -87,8 +87,8 @@ def create_resource(): }, } - serializers = { + body_serializers = { 'application/xml': wsgi.XMLDictSerializer(metadata=metadata), } - - return wsgi.Resource(Controller(), serializers=serializers) + serializer = wsgi.ResponseSerializer(body_serializers) + return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py index 71a14d4ce..7ff0d999e 100644 --- a/nova/api/openstack/backup_schedules.py +++ b/nova/api/openstack/backup_schedules.py @@ -19,7 +19,6 @@ import time from webob import exc -from nova.api.openstack import faults from nova.api.openstack import wsgi @@ -34,22 +33,22 @@ class Controller(object): def __init__(self): pass - def index(self, req, server_id): + def index(self, req, server_id, **kwargs): """ Returns the list of backup schedules for a given instance """ - return faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() - def show(self, req, server_id, id): + def show(self, req, server_id, id, **kwargs): """ Returns a single backup schedule for a given instance """ - return faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() - def create(self, req, server_id, body): + def create(self, req, server_id, **kwargs): """ No actual update method required, since the existing API allows both create and update through a POST """ - return faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() - def delete(self, req, server_id, id): + def delete(self, req, server_id, id, **kwargs): """ Deletes an existing backup schedule """ - return faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() def create_resource(): @@ -59,9 +58,10 @@ def create_resource(): }, } - serializers = { + body_serializers = { 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10, metadata=metadata), } - return wsgi.Resource(Controller(), serializers=serializers) + serializer = wsgi.ResponseSerializer(body_serializers) + return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index aa8911b62..bd14a1389 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -53,10 +53,10 @@ def get_pagination_params(request): params[param] = int(request.GET[param]) except ValueError: msg = _('%s param must be an integer') % param - raise webob.exc.HTTPBadRequest(msg) + raise webob.exc.HTTPBadRequest(explanation=msg) if params[param] < 0: msg = _('%s param must be positive') % param - raise webob.exc.HTTPBadRequest(msg) + raise webob.exc.HTTPBadRequest(explanation=msg) return params @@ -77,18 +77,22 @@ def limited(items, request, max_limit=FLAGS.osapi_max_limit): try: offset = int(request.GET.get('offset', 0)) except ValueError: - raise webob.exc.HTTPBadRequest(_('offset param must be an integer')) + msg = _('offset param must be an integer') + raise webob.exc.HTTPBadRequest(explanation=msg) try: limit = int(request.GET.get('limit', max_limit)) except ValueError: - raise webob.exc.HTTPBadRequest(_('limit param must be an integer')) + msg = _('limit param must be an integer') + raise webob.exc.HTTPBadRequest(explanation=msg) if limit < 0: - raise webob.exc.HTTPBadRequest(_('limit param must be positive')) + msg = _('limit param must be positive') + raise webob.exc.HTTPBadRequest(explanation=msg) if offset < 0: - raise webob.exc.HTTPBadRequest(_('offset param must be positive')) + msg = _('offset param must be positive') + raise webob.exc.HTTPBadRequest(explanation=msg) limit = min(max_limit, limit or max_limit) range_end = offset + limit @@ -111,7 +115,8 @@ def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit): start_index = i + 1 break if start_index < 0: - raise webob.exc.HTTPBadRequest(_('marker [%s] not found' % marker)) + msg = _('marker [%s] not found') % marker + raise webob.exc.HTTPBadRequest(explanation=msg) range_end = start_index + limit return items[start_index:range_end] @@ -133,4 +138,57 @@ def get_id_from_href(href): return int(urlparse(href).path.split('/')[-1]) except: LOG.debug(_("Error extracting id from href: %s") % href) - raise webob.exc.HTTPBadRequest(_('could not parse id from href')) + raise ValueError(_('could not parse id from href')) + + +def remove_version_from_href(href): + """Removes the first api version from the href. + + Given: 'http://www.nova.com/v1.1/123' + Returns: 'http://www.nova.com/123' + + Given: 'http://www.nova.com/v1.1' + Returns: 'http://www.nova.com' + + """ + try: + #removes the first instance that matches /v#.#/ + new_href = re.sub(r'[/][v][0-9]+\.[0-9]+[/]', '/', href, count=1) + + #if no version was found, try finding /v#.# at the end of the string + if new_href == href: + new_href = re.sub(r'[/][v][0-9]+\.[0-9]+$', '', href, count=1) + except: + LOG.debug(_("Error removing version from href: %s") % href) + msg = _('could not parse version from href') + raise ValueError(msg) + + if new_href == href: + msg = _('href does not contain version') + raise ValueError(msg) + return new_href + + +def get_version_from_href(href): + """Returns the api version in the href. + + Returns the api version in the href. + If no version is found, 1.0 is returned + + Given: 'http://www.nova.com/123' + Returns: '1.0' + + Given: 'http://www.nova.com/v1.1' + Returns: '1.1' + + """ + try: + #finds the first instance that matches /v#.#/ + version = re.findall(r'[/][v][0-9]+\.[0-9]+[/]', href) + #if no version was found, try finding /v#.# at the end of the string + if not version: + version = re.findall(r'[/][v][0-9]+\.[0-9]+$', href) + version = re.findall(r'[0-9]+\.[0-9]', version[0])[0] + except IndexError: + version = '1.0' + return version diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py index bccf04d8f..d2655acfa 100644 --- a/nova/api/openstack/consoles.py +++ b/nova/api/openstack/consoles.py @@ -16,10 +16,10 @@ # under the License. from webob import exc +import webob from nova import console from nova import exception -from nova.api.openstack import faults from nova.api.openstack import wsgi @@ -71,12 +71,12 @@ class Controller(object): int(server_id), int(id)) except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) + raise exc.HTTPNotFound() return _translate_detail_keys(console) def update(self, req, server_id, id): """You can't update a console""" - raise faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() def delete(self, req, server_id, id): """Deletes a console""" @@ -85,19 +85,9 @@ class Controller(object): int(server_id), int(id)) except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() + raise exc.HTTPNotFound() + return webob.Response(status_int=202) def create_resource(): - metadata = { - 'attributes': { - 'console': [], - }, - } - - serializers = { - 'application/xml': wsgi.XMLDictSerializer(metadata=metadata), - } - - return wsgi.Resource(Controller(), serializers=serializers) + return wsgi.Resource(Controller()) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 914ec5bfb..b4a211857 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -78,14 +78,15 @@ class FloatingIPController(object): return _translate_floating_ips_view(floating_ips) - def create(self, req, body): + def create(self, req): context = req.environ['nova.context'] try: address = self.network_api.allocate_floating_ip(context) ip = self.network_api.get_floating_ip_by_ip(context, address) except rpc.RemoteError as ex: - if ex.exc_type == 'NoMoreAddresses': + # NOTE(tr3buchet) - why does this block exist? + if ex.exc_type == 'NoMoreFloatingIps': raise exception.NoMoreFloatingIps() else: raise @@ -123,7 +124,7 @@ class FloatingIPController(object): "floating_ip": floating_ip, "fixed_ip": fixed_ip}} - def disassociate(self, req, id, body): + def disassociate(self, req, id): """ POST /floating_ips/{id}/disassociate """ context = req.environ['nova.context'] floating_ip = self.network_api.get_floating_ip(context, id) diff --git a/nova/api/openstack/contrib/hosts.py b/nova/api/openstack/contrib/hosts.py new file mode 100644 index 000000000..55e57e1a4 --- /dev/null +++ b/nova/api/openstack/contrib/hosts.py @@ -0,0 +1,114 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The hosts admin extension.""" + +import webob.exc + +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import faults +from nova.scheduler import api as scheduler_api + + +LOG = logging.getLogger("nova.api.hosts") +FLAGS = flags.FLAGS + + +def _list_hosts(req, service=None): + """Returns a summary list of hosts, optionally filtering + by service type. + """ + context = req.environ['nova.context'] + hosts = scheduler_api.get_host_list(context) + if service: + hosts = [host for host in hosts + if host["service"] == service] + return hosts + + +def check_host(fn): + """Makes sure that the host exists.""" + def wrapped(self, req, id, service=None, *args, **kwargs): + listed_hosts = _list_hosts(req, service) + hosts = [h["host_name"] for h in listed_hosts] + if id in hosts: + return fn(self, req, id, *args, **kwargs) + else: + raise exception.HostNotFound(host=id) + return wrapped + + +class HostController(object): + """The Hosts API controller for the OpenStack API.""" + def __init__(self): + self.compute_api = compute.API() + super(HostController, self).__init__() + + def index(self, req): + return {'hosts': _list_hosts(req)} + + @check_host + def update(self, req, id, body): + for raw_key, raw_val in body.iteritems(): + key = raw_key.lower().strip() + val = raw_val.lower().strip() + # NOTE: (dabo) Right now only 'status' can be set, but other + # actions may follow. + if key == "status": + if val[:6] in ("enable", "disabl"): + return self._set_enabled_status(req, id, + enabled=(val.startswith("enable"))) + else: + explanation = _("Invalid status: '%s'") % raw_val + raise webob.exc.HTTPBadRequest(explanation=explanation) + else: + explanation = _("Invalid update setting: '%s'") % raw_key + raise webob.exc.HTTPBadRequest(explanation=explanation) + + def _set_enabled_status(self, req, host, enabled): + """Sets the specified host's ability to accept new instances.""" + context = req.environ['nova.context'] + state = "enabled" if enabled else "disabled" + LOG.audit(_("Setting host %(host)s to %(state)s.") % locals()) + result = self.compute_api.set_host_enabled(context, host=host, + enabled=enabled) + return {"host": host, "status": result} + + +class Hosts(extensions.ExtensionDescriptor): + def get_name(self): + return "Hosts" + + def get_alias(self): + return "os-hosts" + + def get_description(self): + return "Host administration" + + def get_namespace(self): + return "http://docs.openstack.org/ext/hosts/api/v1.1" + + def get_updated(self): + return "2011-06-29T00:00:00+00:00" + + def get_resources(self): + resources = [extensions.ResourceExtension('os-hosts', HostController(), + collection_actions={'update': 'PUT'}, member_actions={})] + return resources diff --git a/nova/api/openstack/contrib/multinic.py b/nova/api/openstack/contrib/multinic.py new file mode 100644 index 000000000..da8dcee5d --- /dev/null +++ b/nova/api/openstack/contrib/multinic.py @@ -0,0 +1,126 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The multinic extension.""" + +from webob import exc +import webob + +from nova import compute +from nova import log as logging +from nova.api.openstack import extensions +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.multinic") + + +# Note: The class name is as it has to be for this to be loaded as an +# extension--only first character capitalized. +class Multinic(extensions.ExtensionDescriptor): + """The multinic extension. + + Exposes addFixedIp and removeFixedIp actions on servers. + + """ + + def __init__(self, *args, **kwargs): + """Initialize the extension. + + Gets a compute.API object so we can call the back-end + add_fixed_ip() and remove_fixed_ip() methods. + """ + + super(Multinic, self).__init__(*args, **kwargs) + self.compute_api = compute.API() + + def get_name(self): + """Return the extension name, as required by contract.""" + + return "Multinic" + + def get_alias(self): + """Return the extension alias, as required by contract.""" + + return "NMN" + + def get_description(self): + """Return the extension description, as required by contract.""" + + return "Multiple network support" + + def get_namespace(self): + """Return the namespace, as required by contract.""" + + return "http://docs.openstack.org/ext/multinic/api/v1.1" + + def get_updated(self): + """Return the last updated timestamp, as required by contract.""" + + return "2011-06-09T00:00:00+00:00" + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + + actions = [] + + # Add the add_fixed_ip action + act = extensions.ActionExtension("servers", "addFixedIp", + self._add_fixed_ip) + actions.append(act) + + # Add the remove_fixed_ip action + act = extensions.ActionExtension("servers", "removeFixedIp", + self._remove_fixed_ip) + actions.append(act) + + return actions + + def _add_fixed_ip(self, input_dict, req, id): + """Adds an IP on a given network to an instance.""" + + try: + # Validate the input entity + if 'networkId' not in input_dict['addFixedIp']: + LOG.exception(_("Missing 'networkId' argument for addFixedIp")) + return faults.Fault(exc.HTTPUnprocessableEntity()) + + # Add the fixed IP + network_id = input_dict['addFixedIp']['networkId'] + self.compute_api.add_fixed_ip(req.environ['nova.context'], id, + network_id) + except Exception, e: + LOG.exception(_("Error in addFixedIp %s"), e) + return faults.Fault(exc.HTTPBadRequest()) + return webob.Response(status_int=202) + + def _remove_fixed_ip(self, input_dict, req, id): + """Removes an IP from an instance.""" + + try: + # Validate the input entity + if 'address' not in input_dict['removeFixedIp']: + LOG.exception(_("Missing 'address' argument for " + "removeFixedIp")) + return faults.Fault(exc.HTTPUnprocessableEntity()) + + # Remove the fixed IP + address = input_dict['removeFixedIp']['address'] + self.compute_api.remove_fixed_ip(req.environ['nova.context'], id, + address) + except Exception, e: + LOG.exception(_("Error in removeFixedIp %s"), e) + return faults.Fault(exc.HTTPBadRequest()) + return webob.Response(status_int=202) diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py index e5e2c5b50..827e36097 100644 --- a/nova/api/openstack/contrib/volumes.py +++ b/nova/api/openstack/contrib/volumes.py @@ -16,6 +16,7 @@ """The volumes extension.""" from webob import exc +import webob from nova import compute from nova import exception @@ -104,7 +105,7 @@ class VolumeController(object): self.volume_api.delete(context, volume_id=id) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() + return webob.Response(status_int=202) def index(self, req): """Returns a summary list of volumes.""" @@ -279,7 +280,7 @@ class VolumeAttachmentController(object): self.compute_api.detach_volume(context, volume_id=volume_id) - return exc.HTTPAccepted() + return webob.Response(status_int=202) def _items(self, req, server_id, entity_maker): """Returns a list of attachments, transformed through entity_maker.""" diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 436e524c1..f8317565e 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -28,7 +28,6 @@ from nova import quota from nova import utils from nova.compute import instance_types -from nova.api.openstack import faults from nova.api.openstack import wsgi from nova.auth import manager as auth_manager @@ -70,11 +69,14 @@ class CreateInstanceHelper(object): return type from this method is left to the caller. """ if not body: - raise faults.Fault(exc.HTTPUnprocessableEntity()) + raise exc.HTTPUnprocessableEntity() - context = req.environ['nova.context'] + if not 'server' in body: + raise exc.HTTPUnprocessableEntity() - password = self.controller._get_server_admin_password(body['server']) + server_dict = body['server'] + context = req.environ['nova.context'] + password = self.controller._get_server_admin_password(server_dict) key_name = None key_data = None @@ -94,26 +96,39 @@ class CreateInstanceHelper(object): except Exception, e: msg = _("Cannot find requested image %(image_href)s: %(e)s" % locals()) - raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + raise exc.HTTPBadRequest(explanation=msg) - personality = body['server'].get('personality') + personality = server_dict.get('personality') injected_files = [] if personality: injected_files = self._get_injected_files(personality) - flavor_id = self.controller._flavor_id_from_req_data(body) + try: + flavor_id = self.controller._flavor_id_from_req_data(body) + except ValueError as error: + msg = _("Invalid flavorRef provided.") + raise exc.HTTPBadRequest(explanation=msg) - if not 'name' in body['server']: + if not 'name' in server_dict: msg = _("Server name is not defined") raise exc.HTTPBadRequest(explanation=msg) - zone_blob = body['server'].get('blob') - name = body['server']['name'] + zone_blob = server_dict.get('blob') + name = server_dict['name'] self._validate_server_name(name) name = name.strip() - reservation_id = body['server'].get('reservation_id') + reservation_id = server_dict.get('reservation_id') + min_count = server_dict.get('min_count') + max_count = server_dict.get('max_count') + # min_count and max_count are optional. If they exist, they come + # in as strings. We want to default 'min_count' to 1, and default + # 'max_count' to be 'min_count'. + min_count = int(min_count) if min_count else 1 + max_count = int(max_count) if max_count else min_count + if min_count > max_count: + min_count = max_count try: inst_type = \ @@ -133,17 +148,21 @@ class CreateInstanceHelper(object): display_description=name, key_name=key_name, key_data=key_data, - metadata=body['server'].get('metadata', {}), + metadata=server_dict.get('metadata', {}), injected_files=injected_files, admin_password=password, zone_blob=zone_blob, - reservation_id=reservation_id)) + reservation_id=reservation_id, + min_count=min_count, + max_count=max_count)) except quota.QuotaError as error: self._handle_quota_error(error) except exception.ImageNotFound as error: msg = _("Can not find requested image") - raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) - + raise exc.HTTPBadRequest(explanation=msg) + except exception.FlavorNotFound as error: + msg = _("Invalid flavorRef provided.") + raise exc.HTTPBadRequest(explanation=msg) # Let the caller deal with unhandled exceptions. def _handle_quota_error(self, error): @@ -266,7 +285,7 @@ class CreateInstanceHelper(object): return password -class ServerXMLDeserializer(wsgi.XMLDeserializer): +class ServerXMLDeserializer(wsgi.MetadataXMLDeserializer): """ Deserializer to handle xml-formatted server create requests. @@ -278,16 +297,17 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): """Deserialize an xml-formatted server create request""" dom = minidom.parseString(string) server = self._extract_server(dom) - return {'server': server} + return {'body': {'server': server}} def _extract_server(self, node): """Marshal the server attribute of a parsed request""" server = {} - server_node = self._find_first_child_named(node, 'server') + server_node = self.find_first_child_named(node, 'server') for attr in ["name", "imageId", "flavorId", "imageRef", "flavorRef"]: if server_node.getAttribute(attr): server[attr] = server_node.getAttribute(attr) - metadata = self._extract_metadata(server_node) + metadata_node = self.find_first_child_named(server_node, "metadata") + metadata = self.extract_metadata(metadata_node) if metadata is not None: server["metadata"] = metadata personality = self._extract_personality(server_node) @@ -295,49 +315,17 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): server["personality"] = personality return server - def _extract_metadata(self, server_node): - """Marshal the metadata attribute of a parsed request""" - metadata_node = self._find_first_child_named(server_node, "metadata") - if metadata_node is None: - return None - metadata = {} - for meta_node in self._find_children_named(metadata_node, "meta"): - key = meta_node.getAttribute("key") - metadata[key] = self._extract_text(meta_node) - return metadata - def _extract_personality(self, server_node): """Marshal the personality attribute of a parsed request""" personality_node = \ - self._find_first_child_named(server_node, "personality") + self.find_first_child_named(server_node, "personality") if personality_node is None: return None personality = [] - for file_node in self._find_children_named(personality_node, "file"): + for file_node in self.find_children_named(personality_node, "file"): item = {} if file_node.hasAttribute("path"): item["path"] = file_node.getAttribute("path") - item["contents"] = self._extract_text(file_node) + item["contents"] = self.extract_text(file_node) personality.append(item) return personality - - def _find_first_child_named(self, parent, name): - """Search a nodes children for the first child with a given name""" - for node in parent.childNodes: - if node.nodeName == name: - return node - return None - - def _find_children_named(self, parent, name): - """Return all of a nodes children who have the given name""" - for node in parent.childNodes: - if node.nodeName == name: - yield node - - def _extract_text(self, node): - """Get the text field contained by the given node""" - if len(node.childNodes) == 1: - child = node.childNodes[0] - if child.nodeType == child.TEXT_NODE: - return child.nodeValue - return "" diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index da06ecd15..cc889703e 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -23,6 +23,7 @@ import sys import routes import webob.dec import webob.exc +from xml.etree import ElementTree from nova import exception from nova import flags @@ -194,7 +195,7 @@ class ExtensionsResource(wsgi.Resource): def show(self, req, id): # NOTE(dprince): the extensions alias is used as the 'id' for show ext = self.extension_manager.extensions[id] - return self._translate(ext) + return dict(extension=self._translate(ext)) def delete(self, req, id): raise faults.Fault(webob.exc.HTTPNotFound()) @@ -258,15 +259,18 @@ class ExtensionMiddleware(base_wsgi.Middleware): mapper = routes.Mapper() + serializer = wsgi.ResponseSerializer( + {'application/xml': ExtensionsXMLSerializer()}) # extended resources for resource in ext_mgr.get_resources(): LOG.debug(_('Extended resource: %s'), resource.collection) mapper.resource(resource.collection, resource.collection, - controller=wsgi.Resource(resource.controller), - collection=resource.collection_actions, - member=resource.member_actions, - parent_resource=resource.parent) + controller=wsgi.Resource( + resource.controller, serializer=serializer), + collection=resource.collection_actions, + member=resource.member_actions, + parent_resource=resource.parent) # extended actions action_resources = self._action_ext_resources(application, ext_mgr, @@ -462,3 +466,40 @@ class ResourceExtension(object): self.parent = parent self.collection_actions = collection_actions self.member_actions = member_actions + + +class ExtensionsXMLSerializer(wsgi.XMLDictSerializer): + + def show(self, ext_dict): + ext = self._create_ext_elem(ext_dict['extension']) + return self._to_xml(ext) + + def index(self, exts_dict): + exts = ElementTree.Element('extensions') + for ext_dict in exts_dict['extensions']: + exts.append(self._create_ext_elem(ext_dict)) + return self._to_xml(exts) + + def _create_ext_elem(self, ext_dict): + """Create an extension xml element from a dict.""" + ext_elem = ElementTree.Element('extension') + ext_elem.set('name', ext_dict['name']) + ext_elem.set('namespace', ext_dict['namespace']) + ext_elem.set('alias', ext_dict['alias']) + ext_elem.set('updated', ext_dict['updated']) + desc = ElementTree.Element('description') + desc.text = ext_dict['description'] + ext_elem.append(desc) + for link in ext_dict.get('links', []): + elem = ElementTree.Element('atom:link') + elem.set('rel', link['rel']) + elem.set('href', link['href']) + elem.set('type', link['type']) + ext_elem.append(elem) + return ext_elem + + def _to_xml(self, root): + """Convert the xml tree object to an xml string.""" + root.set('xmlns', wsgi.XMLNS_V11) + root.set('xmlns:atom', wsgi.XMLNS_ATOM) + return ElementTree.tostring(root, encoding='UTF-8') diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index b9a23c126..1ab45d4f1 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -19,6 +19,7 @@ import webob.dec import webob.exc +from nova.api.openstack import common from nova.api.openstack import wsgi @@ -40,6 +41,7 @@ class Fault(webob.exc.HTTPException): def __init__(self, exception): """Create a Fault for the given webob.exc.exception.""" self.wrapped_exc = exception + self.status_int = exception.status_int @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): @@ -60,9 +62,13 @@ class Fault(webob.exc.HTTPException): content_type = req.best_match_content_type() + xml_serializer = { + '1.0': wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V10), + '1.1': wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V11), + }[common.get_version_from_href(req.url)] + serializer = { - 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, - xmlns=wsgi.XMLNS_V10), + 'application/xml': xml_serializer, 'application/json': wsgi.JSONDictSerializer(), }[content_type] @@ -99,9 +105,13 @@ class OverLimitFault(webob.exc.HTTPException): content_type = request.best_match_content_type() metadata = {"attributes": {"overLimitFault": "code"}} + xml_serializer = { + '1.0': wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V10), + '1.1': wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V11), + }[common.get_version_from_href(request.url)] + serializer = { - 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, - xmlns=wsgi.XMLNS_V10), + 'application/xml': xml_serializer, 'application/json': wsgi.JSONDictSerializer(), }[content_type] diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index a21ff6cb2..b4bda68d4 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -16,6 +16,7 @@ # under the License. import webob +import xml.dom.minidom as minidom from nova import db from nova import exception @@ -74,19 +75,67 @@ class ControllerV11(Controller): return views.flavors.ViewBuilderV11(base_url) +class FlavorXMLSerializer(wsgi.XMLDictSerializer): + + def __init__(self): + super(FlavorXMLSerializer, self).__init__(xmlns=wsgi.XMLNS_V11) + + def _flavor_to_xml(self, xml_doc, flavor, detailed): + flavor_node = xml_doc.createElement('flavor') + flavor_node.setAttribute('id', str(flavor['id'])) + flavor_node.setAttribute('name', flavor['name']) + + if detailed: + flavor_node.setAttribute('ram', str(flavor['ram'])) + flavor_node.setAttribute('disk', str(flavor['disk'])) + + link_nodes = self._create_link_nodes(xml_doc, flavor['links']) + for link_node in link_nodes: + flavor_node.appendChild(link_node) + return flavor_node + + def _flavors_list_to_xml(self, xml_doc, flavors, detailed): + container_node = xml_doc.createElement('flavors') + + for flavor in flavors: + item_node = self._flavor_to_xml(xml_doc, flavor, detailed) + container_node.appendChild(item_node) + return container_node + + def show(self, flavor_container): + xml_doc = minidom.Document() + flavor = flavor_container['flavor'] + node = self._flavor_to_xml(xml_doc, flavor, True) + return self.to_xml_string(node, True) + + def detail(self, flavors_container): + xml_doc = minidom.Document() + flavors = flavors_container['flavors'] + node = self._flavors_list_to_xml(xml_doc, flavors, True) + return self.to_xml_string(node, True) + + def index(self, flavors_container): + xml_doc = minidom.Document() + flavors = flavors_container['flavors'] + node = self._flavors_list_to_xml(xml_doc, flavors, False) + return self.to_xml_string(node, True) + + def create_resource(version='1.0'): controller = { '1.0': ControllerV10, '1.1': ControllerV11, }[version]() - xmlns = { - '1.0': wsgi.XMLNS_V10, - '1.1': wsgi.XMLNS_V11, + xml_serializer = { + '1.0': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10), + '1.1': FlavorXMLSerializer(), }[version] - serializers = { - 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns), + body_serializers = { + 'application/xml': xml_serializer, } - return wsgi.Resource(controller, serializers=serializers) + serializer = wsgi.ResponseSerializer(body_serializers) + + return wsgi.Resource(controller, serializer=serializer) diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py index c0e92f2fc..ee181c924 100644 --- a/nova/api/openstack/image_metadata.py +++ b/nova/api/openstack/image_metadata.py @@ -22,7 +22,6 @@ from nova import flags from nova import image from nova import quota from nova import utils -from nova.api.openstack import faults from nova.api.openstack import wsgi @@ -62,7 +61,7 @@ class Controller(object): if id in metadata: return {'meta': {id: metadata[id]}} else: - return faults.Fault(exc.HTTPNotFound()) + raise exc.HTTPNotFound() def create(self, req, image_id, body): context = req.environ['nova.context'] @@ -97,33 +96,67 @@ class Controller(object): self._check_quota_limit(context, metadata) img['properties'] = metadata self.image_service.update(context, image_id, img, None) + return dict(meta=meta) - return req.body + def update_all(self, req, image_id, body): + context = req.environ['nova.context'] + img = self.image_service.show(context, image_id) + metadata = body.get('metadata', {}) + self._check_quota_limit(context, metadata) + img['properties'] = metadata + self.image_service.update(context, image_id, img, None) + return dict(metadata=metadata) def delete(self, req, image_id, id): context = req.environ['nova.context'] img = self.image_service.show(context, image_id) metadata = self._get_metadata(context, image_id) if not id in metadata: - return faults.Fault(exc.HTTPNotFound()) + raise exc.HTTPNotFound() metadata.pop(id) img['properties'] = metadata self.image_service.update(context, image_id, img, None) +class ImageMetadataXMLDeserializer(wsgi.MetadataXMLDeserializer): + + def _extract_metadata_container(self, datastring): + dom = minidom.parseString(datastring) + metadata_node = self.find_first_child_named(dom, "metadata") + metadata = self.extract_metadata(metadata_node) + return {'body': {'metadata': metadata}} + + def create(self, datastring): + return self._extract_metadata_container(datastring) + + def update_all(self, datastring): + return self._extract_metadata_container(datastring) + + def update(self, datastring): + dom = minidom.parseString(datastring) + metadata_item = self.extract_metadata(dom) + return {'body': {'meta': metadata_item}} + + +class HeadersSerializer(wsgi.ResponseHeadersSerializer): + + def delete(self, response, data): + response.status_int = 204 + + class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer): - def __init__(self): - xmlns = wsgi.XMLNS_V11 + def __init__(self, xmlns=wsgi.XMLNS_V11): super(ImageMetadataXMLSerializer, self).__init__(xmlns=xmlns) def _meta_item_to_xml(self, doc, key, value): node = doc.createElement('meta') - node.setAttribute('key', key) - text = doc.createTextNode(value) + doc.appendChild(node) + node.setAttribute('key', '%s' % key) + text = doc.createTextNode('%s' % value) node.appendChild(text) return node - def _meta_list_to_xml(self, xml_doc, meta_items): + def meta_list_to_xml(self, xml_doc, meta_items): container_node = xml_doc.createElement('metadata') for (key, value) in meta_items: item_node = self._meta_item_to_xml(xml_doc, key, value) @@ -133,9 +166,10 @@ class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer): def _meta_list_to_xml_string(self, metadata_dict): xml_doc = minidom.Document() items = metadata_dict['metadata'].items() - container_node = self._meta_list_to_xml(xml_doc, items) + container_node = self.meta_list_to_xml(xml_doc, items) + xml_doc.appendChild(container_node) self._add_xmlns(container_node) - return container_node.toprettyxml(indent=' ') + return xml_doc.toprettyxml(indent=' ', encoding='UTF-8') def index(self, metadata_dict): return self._meta_list_to_xml_string(metadata_dict) @@ -143,12 +177,16 @@ class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer): def create(self, metadata_dict): return self._meta_list_to_xml_string(metadata_dict) + def update_all(self, metadata_dict): + return self._meta_list_to_xml_string(metadata_dict) + def _meta_item_to_xml_string(self, meta_item_dict): xml_doc = minidom.Document() item_key, item_value = meta_item_dict.items()[0] item_node = self._meta_item_to_xml(xml_doc, item_key, item_value) + xml_doc.appendChild(item_node) self._add_xmlns(item_node) - return item_node.toprettyxml(indent=' ') + return xml_doc.toprettyxml(indent=' ', encoding='UTF-8') def show(self, meta_item_dict): return self._meta_item_to_xml_string(meta_item_dict['meta']) @@ -156,10 +194,21 @@ class ImageMetadataXMLSerializer(wsgi.XMLDictSerializer): def update(self, meta_item_dict): return self._meta_item_to_xml_string(meta_item_dict['meta']) + def default(self, *args, **kwargs): + return '' + def create_resource(): - serializers = { + headers_serializer = HeadersSerializer() + + body_deserializers = { + 'application/xml': ImageMetadataXMLDeserializer(), + } + + body_serializers = { 'application/xml': ImageMetadataXMLSerializer(), } + serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) + deserializer = wsgi.RequestDeserializer(body_deserializers) - return wsgi.Resource(Controller(), serializers=serializers) + return wsgi.Resource(Controller(), deserializer, serializer) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 64d003a0f..30e4fd389 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -13,18 +13,20 @@ # License for the specific language governing permissions and limitations # under the License. +import urlparse import os.path import webob.exc +from xml.dom import minidom from nova import compute from nova import exception from nova import flags import nova.image from nova import log -from nova import utils from nova.api.openstack import common -from nova.api.openstack import faults +from nova.api.openstack import image_metadata +from nova.api.openstack import servers from nova.api.openstack.views import images as images_view from nova.api.openstack import wsgi @@ -32,7 +34,13 @@ from nova.api.openstack import wsgi LOG = log.getLogger('nova.api.openstack.images') FLAGS = flags.FLAGS -SUPPORTED_FILTERS = ['name', 'status'] +SUPPORTED_FILTERS = { + 'name': 'name', + 'status': 'status', + 'changes-since': 'changes-since', + 'server': 'property-instance_ref', + 'type': 'property-image_type', +} class Controller(object): @@ -59,8 +67,9 @@ class Controller(object): filters = {} for param in req.str_params: if param in SUPPORTED_FILTERS or param.startswith('property-'): - filters[param] = req.str_params.get(param) - + # map filter name or carry through if property-* + filter_name = SUPPORTED_FILTERS.get(param, param) + filters[filter_name] = req.str_params.get(param) return filters def show(self, req, id): @@ -75,7 +84,7 @@ class Controller(object): image = self._image_service.show(context, id) except (exception.NotFound, exception.InvalidImageRef): explanation = _("Image not found.") - raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation)) + raise webob.exc.HTTPNotFound(explanation=explanation) return dict(image=self.get_builder(req).build(image, detail=True)) @@ -90,31 +99,67 @@ class Controller(object): return webob.exc.HTTPNoContent() def create(self, req, body): - """Snapshot a server instance and save the image. + """Snapshot or backup a server instance and save the image. + + Images now have an `image_type` associated with them, which can be + 'snapshot' or the backup type, like 'daily' or 'weekly'. + + If the image_type is backup-like, then the rotation factor can be + included and that will cause the oldest backups that exceed the + rotation factor to be deleted. :param req: `wsgi.Request` object """ + def get_param(param): + try: + return body["image"][param] + except KeyError: + raise webob.exc.HTTPBadRequest(explanation="Missing required " + "param: %s" % param) + context = req.environ['nova.context'] content_type = req.get_content_type() if not body: raise webob.exc.HTTPBadRequest() + image_type = body["image"].get("image_type", "snapshot") + try: server_id = self._server_id_from_req(req, body) - image_name = body["image"]["name"] except KeyError: raise webob.exc.HTTPBadRequest() + image_name = get_param("name") props = self._get_extra_properties(req, body) - image = self._compute_service.snapshot(context, server_id, - image_name, props) + if image_type == "snapshot": + image = self._compute_service.snapshot( + context, server_id, image_name, + extra_properties=props) + elif image_type == "backup": + # NOTE(sirp): Unlike snapshot, backup is not a customer facing + # API call; rather, it's used by the internal backup scheduler + if not FLAGS.allow_admin_api: + raise webob.exc.HTTPBadRequest( + explanation="Admin API Required") + + backup_type = get_param("backup_type") + rotation = int(get_param("rotation")) + + image = self._compute_service.backup( + context, server_id, image_name, + backup_type, rotation, extra_properties=props) + else: + LOG.error(_("Invalid image_type '%s' passed") % image_type) + raise webob.exc.HTTPBadRequest(explanation="Invalue image_type: " + "%s" % image_type) + return dict(image=self.get_builder(req).build(image, detail=True)) def get_builder(self, request): """Indicates that you must use a Controller subclass.""" - raise NotImplementedError + raise NotImplementedError() def _server_id_from_req(self, req, data): raise NotImplementedError() @@ -208,13 +253,23 @@ class ControllerV11(Controller): msg = _("Expected serverRef attribute on server entity.") raise webob.exc.HTTPBadRequest(explanation=msg) - head, tail = os.path.split(server_ref) - - if head and head != os.path.join(req.application_url, 'servers'): + if not server_ref.startswith('http'): + return server_ref + + passed = urlparse.urlparse(server_ref) + expected = urlparse.urlparse(req.application_url) + version = expected.path.split('/')[1] + expected_prefix = "/%s/servers/" % version + _empty, _sep, server_id = passed.path.partition(expected_prefix) + scheme_ok = passed.scheme == expected.scheme + host_ok = passed.hostname == expected.hostname + port_ok = (passed.port == expected.port or + passed.port == FLAGS.osapi_port) + if not (scheme_ok and port_ok and host_ok and server_id): msg = _("serverRef must match request url") raise webob.exc.HTTPBadRequest(explanation=msg) - return tail + return server_id def _get_extra_properties(self, req, data): server_ref = data['image']['serverRef'] @@ -224,17 +279,109 @@ class ControllerV11(Controller): return {'instance_ref': server_ref} +class ImageXMLSerializer(wsgi.XMLDictSerializer): + + xmlns = wsgi.XMLNS_V11 + + def __init__(self): + self.metadata_serializer = image_metadata.ImageMetadataXMLSerializer() + + def _image_to_xml(self, xml_doc, image): + image_node = xml_doc.createElement('image') + image_node.setAttribute('id', str(image['id'])) + image_node.setAttribute('name', image['name']) + link_nodes = self._create_link_nodes(xml_doc, + image['links']) + for link_node in link_nodes: + image_node.appendChild(link_node) + return image_node + + def _image_to_xml_detailed(self, xml_doc, image): + image_node = xml_doc.createElement('image') + self._add_image_attributes(image_node, image) + + if 'server' in image: + server_node = self._create_server_node(xml_doc, image['server']) + image_node.appendChild(server_node) + + metadata = image.get('metadata', {}).items() + if len(metadata) > 0: + metadata_node = self._create_metadata_node(xml_doc, metadata) + image_node.appendChild(metadata_node) + + link_nodes = self._create_link_nodes(xml_doc, + image['links']) + for link_node in link_nodes: + image_node.appendChild(link_node) + + return image_node + + def _add_image_attributes(self, node, image): + node.setAttribute('id', str(image['id'])) + node.setAttribute('name', image['name']) + node.setAttribute('created', image['created']) + node.setAttribute('updated', image['updated']) + node.setAttribute('status', image['status']) + if 'progress' in image: + node.setAttribute('progress', str(image['progress'])) + + def _create_metadata_node(self, xml_doc, metadata): + return self.metadata_serializer.meta_list_to_xml(xml_doc, metadata) + + def _create_server_node(self, xml_doc, server): + server_node = xml_doc.createElement('server') + server_node.setAttribute('id', str(server['id'])) + link_nodes = self._create_link_nodes(xml_doc, + server['links']) + for link_node in link_nodes: + server_node.appendChild(link_node) + return server_node + + def _image_list_to_xml(self, xml_doc, images, detailed): + container_node = xml_doc.createElement('images') + if detailed: + image_to_xml = self._image_to_xml_detailed + else: + image_to_xml = self._image_to_xml + + for image in images: + item_node = image_to_xml(xml_doc, image) + container_node.appendChild(item_node) + return container_node + + def index(self, images_dict): + xml_doc = minidom.Document() + node = self._image_list_to_xml(xml_doc, + images_dict['images'], + detailed=False) + return self.to_xml_string(node, True) + + def detail(self, images_dict): + xml_doc = minidom.Document() + node = self._image_list_to_xml(xml_doc, + images_dict['images'], + detailed=True) + return self.to_xml_string(node, True) + + def show(self, image_dict): + xml_doc = minidom.Document() + node = self._image_to_xml_detailed(xml_doc, + image_dict['image']) + return self.to_xml_string(node, True) + + def create(self, image_dict): + xml_doc = minidom.Document() + node = self._image_to_xml_detailed(xml_doc, + image_dict['image']) + return self.to_xml_string(node, True) + + def create_resource(version='1.0'): controller = { '1.0': ControllerV10, '1.1': ControllerV11, }[version]() - xmlns = { - '1.0': wsgi.XMLNS_V10, - '1.1': wsgi.XMLNS_V11, - }[version] - metadata = { "attributes": { "image": ["id", "name", "updated", "created", "status", @@ -243,9 +390,15 @@ def create_resource(version='1.0'): }, } - serializers = { - 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns, - metadata=metadata), + xml_serializer = { + '1.0': wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V10), + '1.1': ImageXMLSerializer(), + }[version] + + body_serializers = { + 'application/xml': xml_serializer, } - return wsgi.Resource(controller, serializers=serializers) + serializer = wsgi.ResponseSerializer(body_serializers) + + return wsgi.Resource(controller, serializer=serializer) diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py index 71646b6d3..a74fae487 100644 --- a/nova/api/openstack/ips.py +++ b/nova/api/openstack/ips.py @@ -16,13 +16,14 @@ # under the License. import time +from xml.dom import minidom from webob import exc import nova -from nova.api.openstack import faults import nova.api.openstack.views.addresses from nova.api.openstack import wsgi +from nova import db class Controller(object): @@ -30,39 +31,121 @@ class Controller(object): def __init__(self): self.compute_api = nova.compute.API() - self.builder = nova.api.openstack.views.addresses.ViewBuilderV10() def _get_instance(self, req, server_id): try: instance = self.compute_api.get( req.environ['nova.context'], server_id) except nova.exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) + raise exc.HTTPNotFound() return instance + def create(self, req, server_id, body): + raise exc.HTTPNotImplemented() + + def delete(self, req, server_id, id): + raise exc.HTTPNotImplemented() + + +class ControllerV10(Controller): + def index(self, req, server_id): instance = self._get_instance(req, server_id) - return {'addresses': self.builder.build(instance)} + builder = nova.api.openstack.views.addresses.ViewBuilderV10() + return {'addresses': builder.build(instance)} - def public(self, req, server_id): + def show(self, req, server_id, id): instance = self._get_instance(req, server_id) - return {'public': self.builder.build_public_parts(instance)} + builder = self._get_view_builder(req) + if id == 'private': + view = builder.build_private_parts(instance) + elif id == 'public': + view = builder.build_public_parts(instance) + else: + msg = _("Only private and public networks available") + raise exc.HTTPNotFound(explanation=msg) - def private(self, req, server_id): - instance = self._get_instance(req, server_id) - return {'private': self.builder.build_private_parts(instance)} + return {id: view} + + def _get_view_builder(self, req): + return nova.api.openstack.views.addresses.ViewBuilderV10() + + +class ControllerV11(Controller): + + def index(self, req, server_id): + context = req.environ['nova.context'] + interfaces = self._get_virtual_interfaces(context, server_id) + networks = self._get_view_builder(req).build(interfaces) + return {'addresses': networks} def show(self, req, server_id, id): - return faults.Fault(exc.HTTPNotImplemented()) + context = req.environ['nova.context'] + interfaces = self._get_virtual_interfaces(context, server_id) + network = self._get_view_builder(req).build_network(interfaces, id) - def create(self, req, server_id, body): - return faults.Fault(exc.HTTPNotImplemented()) + if network is None: + msg = _("Instance is not a member of specified network") + raise exc.HTTPNotFound(explanation=msg) - def delete(self, req, server_id, id): - return faults.Fault(exc.HTTPNotImplemented()) + return network + def _get_virtual_interfaces(self, context, server_id): + try: + return db.api.virtual_interface_get_by_instance(context, server_id) + except nova.exception.InstanceNotFound: + msg = _("Instance does not exist") + raise exc.HTTPNotFound(explanation=msg) + + def _get_view_builder(self, req): + return nova.api.openstack.views.addresses.ViewBuilderV11() + + +class IPXMLSerializer(wsgi.XMLDictSerializer): + def __init__(self, xmlns=wsgi.XMLNS_V11): + super(IPXMLSerializer, self).__init__(xmlns=xmlns) + + def _ip_to_xml(self, xml_doc, ip_dict): + ip_node = xml_doc.createElement('ip') + ip_node.setAttribute('addr', ip_dict['addr']) + ip_node.setAttribute('version', str(ip_dict['version'])) + return ip_node + + def _network_to_xml(self, xml_doc, network_id, ip_dicts): + network_node = xml_doc.createElement('network') + network_node.setAttribute('id', network_id) + + for ip_dict in ip_dicts: + ip_node = self._ip_to_xml(xml_doc, ip_dict) + network_node.appendChild(ip_node) + + return network_node + + def networks_to_xml(self, xml_doc, networks_container): + addresses_node = xml_doc.createElement('addresses') + for (network_id, ip_dicts) in networks_container.items(): + network_node = self._network_to_xml(xml_doc, network_id, ip_dicts) + addresses_node.appendChild(network_node) + return addresses_node + + def show(self, network_container): + (network_id, ip_dicts) = network_container.items()[0] + xml_doc = minidom.Document() + node = self._network_to_xml(xml_doc, network_id, ip_dicts) + return self.to_xml_string(node, False) + + def index(self, addresses_container): + xml_doc = minidom.Document() + node = self.networks_to_xml(xml_doc, addresses_container['addresses']) + return self.to_xml_string(node, False) + + +def create_resource(version): + controller = { + '1.0': ControllerV10, + '1.1': ControllerV11, + }[version]() -def create_resource(): metadata = { 'list_collections': { 'public': {'item_name': 'ip', 'item_key': 'addr'}, @@ -70,9 +153,11 @@ def create_resource(): }, } - serializers = { - 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, - xmlns=wsgi.XMLNS_V10), - } + xml_serializer = { + '1.0': wsgi.XMLDictSerializer(metadata=metadata, xmlns=wsgi.XMLNS_V11), + '1.1': IPXMLSerializer(), + }[version] + + serializer = wsgi.ResponseSerializer({'application/xml': xml_serializer}) - return wsgi.Resource(Controller(), serializers=serializers) + return wsgi.Resource(controller, serializer=serializer) diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index fede96e33..86afa3b62 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -25,14 +25,15 @@ import re import time import urllib import webob.exc +from xml.dom import minidom from collections import defaultdict from webob.dec import wsgify from nova import quota +from nova import utils from nova import wsgi as base_wsgi -from nova import wsgi from nova.api.openstack import common from nova.api.openstack import faults from nova.api.openstack.views import limits as limits_views @@ -76,6 +77,58 @@ class LimitsControllerV11(LimitsController): return limits_views.ViewBuilderV11() +class LimitsXMLSerializer(wsgi.XMLDictSerializer): + + xmlns = wsgi.XMLNS_V11 + + def __init__(self): + pass + + def _create_rates_node(self, xml_doc, rates): + rates_node = xml_doc.createElement('rates') + for rate in rates: + rate_node = xml_doc.createElement('rate') + rate_node.setAttribute('uri', rate['uri']) + rate_node.setAttribute('regex', rate['regex']) + + for limit in rate['limit']: + limit_node = xml_doc.createElement('limit') + limit_node.setAttribute('value', str(limit['value'])) + limit_node.setAttribute('verb', limit['verb']) + limit_node.setAttribute('remaining', str(limit['remaining'])) + limit_node.setAttribute('unit', limit['unit']) + limit_node.setAttribute('next-available', + str(limit['next-available'])) + rate_node.appendChild(limit_node) + + rates_node.appendChild(rate_node) + return rates_node + + def _create_absolute_node(self, xml_doc, absolutes): + absolute_node = xml_doc.createElement('absolute') + for key, value in absolutes.iteritems(): + limit_node = xml_doc.createElement('limit') + limit_node.setAttribute('name', key) + limit_node.setAttribute('value', str(value)) + absolute_node.appendChild(limit_node) + return absolute_node + + def _limits_to_xml(self, xml_doc, limits): + limits_node = xml_doc.createElement('limits') + rates_node = self._create_rates_node(xml_doc, limits['rate']) + limits_node.appendChild(rates_node) + + absolute_node = self._create_absolute_node(xml_doc, limits['absolute']) + limits_node.appendChild(absolute_node) + + return limits_node + + def index(self, limits_dict): + xml_doc = minidom.Document() + node = self._limits_to_xml(xml_doc, limits_dict['limits']) + return self.to_xml_string(node, False) + + def create_resource(version='1.0'): controller = { '1.0': LimitsControllerV10, @@ -97,12 +150,18 @@ def create_resource(version='1.0'): }, } - serializers = { - 'application/xml': wsgi.XMLDictSerializer(xmlns=xmlns, - metadata=metadata), + xml_serializer = { + '1.0': wsgi.XMLDictSerializer(xmlns=xmlns, metadata=metadata), + '1.1': LimitsXMLSerializer(), + }[version] + + body_serializers = { + 'application/xml': xml_serializer, } - return wsgi.Resource(controller, serializers=serializers) + serializer = wsgi.ResponseSerializer(body_serializers) + + return wsgi.Resource(controller, serializer=serializer) class Limit(object): @@ -117,6 +176,8 @@ class Limit(object): 60 * 60 * 24: "DAY", } + UNIT_MAP = dict([(v, k) for k, v in UNITS.items()]) + def __init__(self, verb, uri, regex, value, unit): """ Initialize a new `Limit`. @@ -222,16 +283,30 @@ class RateLimitingMiddleware(base_wsgi.Middleware): is stored in memory for this implementation. """ - def __init__(self, application, limits=None): + def __init__(self, application, limits=None, limiter=None, **kwargs): """ Initialize new `RateLimitingMiddleware`, which wraps the given WSGI application and sets up the given limits. @param application: WSGI application to wrap - @param limits: List of dictionaries describing limits + @param limits: String describing limits + @param limiter: String identifying class for representing limits + + Other parameters are passed to the constructor for the limiter. """ base_wsgi.Middleware.__init__(self, application) - self._limiter = Limiter(limits or DEFAULT_LIMITS) + + # Select the limiter class + if limiter is None: + limiter = Limiter + else: + limiter = utils.import_class(limiter) + + # Parse the limits, if any are provided + if limits is not None: + limits = limiter.parse_limits(limits) + + self._limiter = limiter(limits or DEFAULT_LIMITS, **kwargs) @wsgify(RequestClass=wsgi.Request) def __call__(self, req): @@ -269,7 +344,7 @@ class Limiter(object): Rate-limit checking class which handles limits in memory. """ - def __init__(self, limits): + def __init__(self, limits, **kwargs): """ Initialize the new `Limiter`. @@ -278,6 +353,12 @@ class Limiter(object): self.limits = copy.deepcopy(limits) self.levels = defaultdict(lambda: copy.deepcopy(limits)) + # Pick up any per-user limit information + for key, value in kwargs.items(): + if key.startswith('user:'): + username = key[5:] + self.levels[username] = self.parse_limits(value) + def get_limits(self, username=None): """ Return the limits for a given user. @@ -303,6 +384,66 @@ class Limiter(object): return None, None + # Note: This method gets called before the class is instantiated, + # so this must be either a static method or a class method. It is + # used to develop a list of limits to feed to the constructor. We + # put this in the class so that subclasses can override the + # default limit parsing. + @staticmethod + def parse_limits(limits): + """ + Convert a string into a list of Limit instances. This + implementation expects a semicolon-separated sequence of + parenthesized groups, where each group contains a + comma-separated sequence consisting of HTTP method, + user-readable URI, a URI reg-exp, an integer number of + requests which can be made, and a unit of measure. Valid + values for the latter are "SECOND", "MINUTE", "HOUR", and + "DAY". + + @return: List of Limit instances. + """ + + # Handle empty limit strings + limits = limits.strip() + if not limits: + return [] + + # Split up the limits by semicolon + result = [] + for group in limits.split(';'): + group = group.strip() + if group[:1] != '(' or group[-1:] != ')': + raise ValueError("Limit rules must be surrounded by " + "parentheses") + group = group[1:-1] + + # Extract the Limit arguments + args = [a.strip() for a in group.split(',')] + if len(args) != 5: + raise ValueError("Limit rules must contain the following " + "arguments: verb, uri, regex, value, unit") + + # Pull out the arguments + verb, uri, regex, value, unit = args + + # Upper-case the verb + verb = verb.upper() + + # Convert value--raises ValueError if it's not integer + value = int(value) + + # Convert unit + unit = unit.upper() + if unit not in Limit.UNIT_MAP: + raise ValueError("Invalid units specified") + unit = Limit.UNIT_MAP[unit] + + # Build a limit + result.append(Limit(verb, uri, regex, value, unit)) + + return result + class WsgiLimiter(object): """ @@ -386,3 +527,19 @@ class WsgiLimiterProxy(object): return None, None return resp.getheader("X-Wait-Seconds"), resp.read() or None + + # Note: This method gets called before the class is instantiated, + # so this must be either a static method or a class method. It is + # used to develop a list of limits to feed to the constructor. + # This implementation returns an empty list, since all limit + # decisions are made by a remote server. + @staticmethod + def parse_limits(limits): + """ + Ignore a limits string--simply doesn't apply for the limit + proxy. + + @return: Empty list. + """ + + return [] diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index 8a314de22..d4f42bbf5 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -18,7 +18,6 @@ from webob import exc from nova import compute -from nova.api.openstack import faults from nova.api.openstack import wsgi from nova import exception from nova import quota @@ -123,8 +122,10 @@ class Controller(object): def create_resource(): - serializers = { + body_serializers = { 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V11), } - return wsgi.Resource(Controller(), serializers=serializers) + serializer = wsgi.ResponseSerializer(body_serializers) + + return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index b82a6de19..7bef1d9b2 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -17,15 +17,16 @@ import base64 import traceback from webob import exc +import webob from nova import compute +from nova import db from nova import exception from nova import flags from nova import log as logging from nova import utils from nova.api.openstack import common from nova.api.openstack import create_instance_helper as helper -from nova.api.openstack import faults import nova.api.openstack.views.addresses import nova.api.openstack.views.flavors import nova.api.openstack.views.images @@ -62,7 +63,7 @@ class Controller(object): return exc.HTTPBadRequest(explanation=str(err)) return servers - def _get_view_builder(self, req): + def _build_view(self, req, instance, is_detail=False): raise NotImplementedError() def _limit_items(self, items, req): @@ -76,13 +77,19 @@ class Controller(object): builder - the response model builder """ - reservation_id = req.str_GET.get('reservation_id') + query_str = req.str_GET + reservation_id = query_str.get('reservation_id') + project_id = query_str.get('project_id') + fixed_ip = query_str.get('fixed_ip') + recurse_zones = utils.bool_from_str(query_str.get('recurse_zones')) instance_list = self.compute_api.get_all( - req.environ['nova.context'], - reservation_id=reservation_id) + req.environ['nova.context'], + reservation_id=reservation_id, + project_id=project_id, + fixed_ip=fixed_ip, + recurse_zones=recurse_zones) limited_list = self._limit_items(instance_list, req) - builder = self._get_view_builder(req) - servers = [builder.build(inst, is_detail)['server'] + servers = [self._build_view(req, inst, is_detail)['server'] for inst in limited_list] return dict(servers=servers) @@ -92,38 +99,25 @@ class Controller(object): try: instance = self.compute_api.routing_get( req.environ['nova.context'], id) - builder = self._get_view_builder(req) - return builder.build(instance, is_detail=True) + return self._build_view(req, instance, is_detail=True) except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - - @scheduler_api.redirect_handler - def delete(self, req, id): - """ Destroys a server """ - try: - self.compute_api.delete(req.environ['nova.context'], id) - except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() + raise exc.HTTPNotFound() def create(self, req, body): """ Creates a new server for a given user """ extra_values = None result = None - try: - extra_values, result = self.helper.create_instance( - req, body, self.compute_api.create) - except faults.Fault, f: - return f - - instances = result + extra_values, instances = self.helper.create_instance( + req, body, self.compute_api.create) - (inst, ) = instances + # We can only return 1 instance via the API, if we happen to + # build more than one... instances is a list, so we'll just + # use the first one.. + inst = instances[0] for key in ['instance_type', 'image_ref']: inst[key] = extra_values[key] - builder = self._get_view_builder(req) - server = builder.build(inst, is_detail=True) + server = self._build_view(req, inst, is_detail=True) server['server']['adminPass'] = extra_values['password'] return server @@ -134,7 +128,7 @@ class Controller(object): raise exc.HTTPUnprocessableEntity() if not body: - return faults.Fault(exc.HTTPUnprocessableEntity()) + raise exc.HTTPUnprocessableEntity() ctxt = req.environ['nova.context'] update_dict = {} @@ -149,7 +143,7 @@ class Controller(object): try: self.compute_api.update(ctxt, id, **update_dict) except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) + raise exc.HTTPNotFound() return exc.HTTPNoContent() @@ -168,12 +162,12 @@ class Controller(object): 'confirmResize': self._action_confirm_resize, 'revertResize': self._action_revert_resize, 'rebuild': self._action_rebuild, - } + 'migrate': self._action_migrate} for key in actions.keys(): if key in body: return actions[key](body, req, id) - return faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() def _action_change_password(self, input_dict, req, id): return exc.HTTPNotImplemented() @@ -183,7 +177,7 @@ class Controller(object): self.compute_api.confirm_resize(req.environ['nova.context'], id) except Exception, e: LOG.exception(_("Error in confirm-resize %s"), e) - return faults.Fault(exc.HTTPBadRequest()) + raise exc.HTTPBadRequest() return exc.HTTPNoContent() def _action_revert_resize(self, input_dict, req, id): @@ -191,8 +185,8 @@ class Controller(object): self.compute_api.revert_resize(req.environ['nova.context'], id) except Exception, e: LOG.exception(_("Error in revert-resize %s"), e) - return faults.Fault(exc.HTTPBadRequest()) - return exc.HTTPAccepted() + raise exc.HTTPBadRequest() + return webob.Response(status_int=202) def _action_resize(self, input_dict, req, id): return exc.HTTPNotImplemented() @@ -202,15 +196,23 @@ class Controller(object): reboot_type = input_dict['reboot']['type'] else: LOG.exception(_("Missing argument 'type' for reboot")) - return faults.Fault(exc.HTTPUnprocessableEntity()) + raise exc.HTTPUnprocessableEntity() try: # TODO(gundlach): pass reboot_type, support soft reboot in # virt driver self.compute_api.reboot(req.environ['nova.context'], id) except Exception, e: LOG.exception(_("Error in reboot %s"), e) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) + + def _action_migrate(self, input_dict, req, id): + try: + self.compute_api.resize(req.environ['nova.context'], id) + except Exception, e: + LOG.exception(_("Error in migrate %s"), e) + raise exc.HTTPBadRequest() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def lock(self, req, id): @@ -225,8 +227,8 @@ class Controller(object): except: readable = traceback.format_exc() LOG.exception(_("Compute.api::lock %s"), readable) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def unlock(self, req, id): @@ -241,8 +243,8 @@ class Controller(object): except: readable = traceback.format_exc() LOG.exception(_("Compute.api::unlock %s"), readable) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def get_lock(self, req, id): @@ -256,8 +258,8 @@ class Controller(object): except: readable = traceback.format_exc() LOG.exception(_("Compute.api::get_lock %s"), readable) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def reset_network(self, req, id, body): @@ -271,8 +273,8 @@ class Controller(object): except: readable = traceback.format_exc() LOG.exception(_("Compute.api::reset_network %s"), readable) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def inject_network_info(self, req, id, body): @@ -286,8 +288,8 @@ class Controller(object): except: readable = traceback.format_exc() LOG.exception(_("Compute.api::inject_network_info %s"), readable) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def pause(self, req, id, body): @@ -298,8 +300,8 @@ class Controller(object): except: readable = traceback.format_exc() LOG.exception(_("Compute.api::pause %s"), readable) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def unpause(self, req, id, body): @@ -310,8 +312,8 @@ class Controller(object): except: readable = traceback.format_exc() LOG.exception(_("Compute.api::unpause %s"), readable) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def suspend(self, req, id, body): @@ -322,8 +324,8 @@ class Controller(object): except: readable = traceback.format_exc() LOG.exception(_("compute.api::suspend %s"), readable) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def resume(self, req, id, body): @@ -334,8 +336,8 @@ class Controller(object): except: readable = traceback.format_exc() LOG.exception(_("compute.api::resume %s"), readable) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def rescue(self, req, id): @@ -346,8 +348,8 @@ class Controller(object): except: readable = traceback.format_exc() LOG.exception(_("compute.api::rescue %s"), readable) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def unrescue(self, req, id): @@ -358,8 +360,8 @@ class Controller(object): except: readable = traceback.format_exc() LOG.exception(_("compute.api::unrescue %s"), readable) - return faults.Fault(exc.HTTPUnprocessableEntity()) - return exc.HTTPAccepted() + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def get_ajax_console(self, req, id): @@ -368,8 +370,8 @@ class Controller(object): self.compute_api.get_ajax_console(req.environ['nova.context'], int(id)) except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() + raise exc.HTTPNotFound() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def get_vnc_console(self, req, id): @@ -378,8 +380,8 @@ class Controller(object): self.compute_api.get_vnc_console(req.environ['nova.context'], int(id)) except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) - return exc.HTTPAccepted() + raise exc.HTTPNotFound() + return webob.Response(status_int=202) @scheduler_api.redirect_handler def diagnostics(self, req, id): @@ -404,16 +406,25 @@ class Controller(object): class ControllerV10(Controller): + @scheduler_api.redirect_handler + def delete(self, req, id): + """ Destroys a server """ + try: + self.compute_api.delete(req.environ['nova.context'], id) + except exception.NotFound: + raise exc.HTTPNotFound() + return webob.Response(status_int=202) + def _image_ref_from_req_data(self, data): return data['server']['imageId'] def _flavor_id_from_req_data(self, data): return data['server']['flavorId'] - def _get_view_builder(self, req): - addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV10() - return nova.api.openstack.views.servers.ViewBuilderV10( - addresses_builder) + def _build_view(self, req, instance, is_detail=False): + addresses = nova.api.openstack.views.addresses.ViewBuilderV10() + builder = nova.api.openstack.views.servers.ViewBuilderV10(addresses) + return builder.build(instance, is_detail=is_detail) def _limit_items(self, items, req): return common.limited(items, req) @@ -425,18 +436,14 @@ class ControllerV10(Controller): def _action_resize(self, input_dict, req, id): """ Resizes a given instance to the flavor size requested """ - try: - if 'resize' in input_dict and 'flavorId' in input_dict['resize']: - flavor_id = input_dict['resize']['flavorId'] - self.compute_api.resize(req.environ['nova.context'], id, - flavor_id) - else: - LOG.exception(_("Missing 'flavorId' argument for resize")) - return faults.Fault(exc.HTTPUnprocessableEntity()) - except Exception, e: - LOG.exception(_("Error in resize %s"), e) - return faults.Fault(exc.HTTPBadRequest()) - return exc.HTTPAccepted() + if 'resize' in input_dict and 'flavorId' in input_dict['resize']: + flavor_id = input_dict['resize']['flavorId'] + self.compute_api.resize(req.environ['nova.context'], id, + flavor_id) + else: + LOG.exception(_("Missing 'flavorId' argument for resize")) + raise exc.HTTPUnprocessableEntity() + return webob.Response(status_int=202) def _action_rebuild(self, info, request, instance_id): context = request.environ['nova.context'] @@ -447,18 +454,16 @@ class ControllerV10(Controller): except (KeyError, TypeError): msg = _("Could not parse imageId from request.") LOG.debug(msg) - return faults.Fault(exc.HTTPBadRequest(explanation=msg)) + raise exc.HTTPBadRequest(explanation=msg) try: self.compute_api.rebuild(context, instance_id, image_id) except exception.BuildInProgress: msg = _("Instance %d is currently being rebuilt.") % instance_id LOG.debug(msg) - return faults.Fault(exc.HTTPConflict(explanation=msg)) + raise exc.HTTPConflict(explanation=msg) - response = exc.HTTPAccepted() - response.empty_body = True - return response + return webob.Response(status_int=202) def _get_server_admin_password(self, server): """ Determine the admin password for a server on creation """ @@ -466,6 +471,15 @@ class ControllerV10(Controller): class ControllerV11(Controller): + + @scheduler_api.redirect_handler + def delete(self, req, id): + """ Destroys a server """ + try: + self.compute_api.delete(req.environ['nova.context'], id) + except exception.NotFound: + raise exc.HTTPNotFound() + def _image_ref_from_req_data(self, data): return data['server']['imageRef'] @@ -473,16 +487,18 @@ class ControllerV11(Controller): href = data['server']['flavorRef'] return common.get_id_from_href(href) - def _get_view_builder(self, req): + def _build_view(self, req, instance, is_detail=False): base_url = req.application_url flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11( base_url) image_builder = nova.api.openstack.views.images.ViewBuilderV11( base_url) addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11() - return nova.api.openstack.views.servers.ViewBuilderV11( + builder = nova.api.openstack.views.servers.ViewBuilderV11( addresses_builder, flavor_builder, image_builder, base_url) + return builder.build(instance, is_detail=is_detail) + def _action_change_password(self, input_dict, req, id): context = req.environ['nova.context'] if (not 'changePassword' in input_dict @@ -494,7 +510,7 @@ class ControllerV11(Controller): msg = _("Invalid adminPass") return exc.HTTPBadRequest(explanation=msg) self.compute_api.set_admin_password(context, id, password) - return exc.HTTPAccepted() + return webob.Response(status_int=202) def _limit_items(self, items, req): return common.limited_by_marker(items, req) @@ -506,7 +522,7 @@ class ControllerV11(Controller): except AttributeError as ex: msg = _("Unable to parse metadata key/value pairs.") LOG.debug(msg) - raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + raise exc.HTTPBadRequest(explanation=msg) def _decode_personalities(self, personalities): """Decode the Base64-encoded personalities.""" @@ -517,14 +533,14 @@ class ControllerV11(Controller): except (KeyError, TypeError): msg = _("Unable to parse personality path/contents.") LOG.info(msg) - raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + raise exc.HTTPBadRequest(explanation=msg) try: personality["contents"] = base64.b64decode(contents) except TypeError: msg = _("Personality content could not be Base64 decoded.") LOG.info(msg) - raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + raise exc.HTTPBadRequest(explanation=msg) def _action_resize(self, input_dict, req, id): """ Resizes a given instance to the flavor size requested """ @@ -536,11 +552,11 @@ class ControllerV11(Controller): flavor_id) else: LOG.exception(_("Missing 'flavorRef' argument for resize")) - return faults.Fault(exc.HTTPUnprocessableEntity()) + raise exc.HTTPUnprocessableEntity() except Exception, e: LOG.exception(_("Error in resize %s"), e) - return faults.Fault(exc.HTTPBadRequest()) - return exc.HTTPAccepted() + raise exc.HTTPBadRequest() + return webob.Response(status_int=202) def _action_rebuild(self, info, request, instance_id): context = request.environ['nova.context'] @@ -551,7 +567,7 @@ class ControllerV11(Controller): except (KeyError, TypeError): msg = _("Could not parse imageRef from request.") LOG.debug(msg) - return faults.Fault(exc.HTTPBadRequest(explanation=msg)) + raise exc.HTTPBadRequest(explanation=msg) personalities = info["rebuild"].get("personality", []) metadata = info["rebuild"].get("metadata") @@ -567,11 +583,9 @@ class ControllerV11(Controller): except exception.BuildInProgress: msg = _("Instance %d is currently being rebuilt.") % instance_id LOG.debug(msg) - return faults.Fault(exc.HTTPConflict(explanation=msg)) + raise exc.HTTPConflict(explanation=msg) - response = exc.HTTPAccepted() - response.empty_body = True - return response + return webob.Response(status_int=202) def get_default_xmlns(self, req): return common.XML_NS_V11 @@ -581,6 +595,12 @@ class ControllerV11(Controller): return self.helper._get_server_admin_password_new_style(server) +class HeadersSerializer(wsgi.ResponseHeadersSerializer): + + def delete(self, response, data): + response.status_int = 204 + + def create_resource(version='1.0'): controller = { '1.0': ControllerV10, @@ -608,14 +628,18 @@ def create_resource(version='1.0'): '1.1': wsgi.XMLNS_V11, }[version] - serializers = { + headers_serializer = HeadersSerializer() + + body_serializers = { 'application/xml': wsgi.XMLDictSerializer(metadata=metadata, xmlns=xmlns), } - deserializers = { + body_deserializers = { 'application/xml': helper.ServerXMLDeserializer(), } - return wsgi.Resource(controller, serializers=serializers, - deserializers=deserializers) + serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer) + deserializer = wsgi.RequestDeserializer(body_deserializers) + + return wsgi.Resource(controller, deserializer, serializer) diff --git a/nova/api/openstack/shared_ip_groups.py b/nova/api/openstack/shared_ip_groups.py index 4f11f8dfb..54d0a8334 100644 --- a/nova/api/openstack/shared_ip_groups.py +++ b/nova/api/openstack/shared_ip_groups.py @@ -17,36 +17,35 @@ from webob import exc -from nova.api.openstack import faults from nova.api.openstack import wsgi class Controller(object): """ The Shared IP Groups Controller for the Openstack API """ - def index(self, req): + def index(self, req, **kwargs): """ Returns a list of Shared IP Groups for the user """ - raise faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() - def show(self, req, id): + def show(self, req, id, **kwargs): """ Shows in-depth information on a specific Shared IP Group """ - raise faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() - def update(self, req, id, body): + def update(self, req, id, **kwargs): """ You can't update a Shared IP Group """ - raise faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() - def delete(self, req, id): + def delete(self, req, id, **kwargs): """ Deletes a Shared IP Group """ - raise faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() - def detail(self, req): + def detail(self, req, **kwargs): """ Returns a complete list of Shared IP Groups """ - raise faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() - def create(self, req, body): + def create(self, req, **kwargs): """ Creates a new Shared IP group """ - raise faults.Fault(exc.HTTPNotImplemented()) + raise exc.HTTPNotImplemented() def create_resource(): diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py index 50975fc1f..8dd72d559 100644 --- a/nova/api/openstack/users.py +++ b/nova/api/openstack/users.py @@ -19,7 +19,6 @@ from nova import exception from nova import flags from nova import log as logging from nova.api.openstack import common -from nova.api.openstack import faults from nova.api.openstack import wsgi from nova.auth import manager @@ -69,7 +68,7 @@ class Controller(object): user = None if user is None: - raise faults.Fault(exc.HTTPNotFound()) + raise exc.HTTPNotFound() return dict(user=_translate_keys(user)) @@ -105,8 +104,10 @@ def create_resource(): }, } - serializers = { + body_serializers = { 'application/xml': wsgi.XMLDictSerializer(metadata=metadata), } - return wsgi.Resource(Controller(), serializers=serializers) + serializer = wsgi.ResponseSerializer(body_serializers) + + return wsgi.Resource(Controller(), serializer=serializer) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 4c682302f..df7a94b7e 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -15,13 +15,18 @@ # License for the specific language governing permissions and limitations # under the License. +from datetime import datetime import webob import webob.dec +from xml.dom import minidom import nova.api.openstack.views.versions from nova.api.openstack import wsgi +ATOM_XMLNS = "http://www.w3.org/2005/Atom" + + class Versions(wsgi.Resource): def __init__(self): metadata = { @@ -31,11 +36,20 @@ class Versions(wsgi.Resource): } } - serializers = { - 'application/xml': wsgi.XMLDictSerializer(metadata=metadata), + body_serializers = { + 'application/atom+xml': VersionsAtomSerializer(metadata=metadata), + 'application/xml': VersionsXMLSerializer(metadata=metadata), } + serializer = wsgi.ResponseSerializer(body_serializers) - wsgi.Resource.__init__(self, None, serializers=serializers) + supported_content_types = ('application/json', + 'application/xml', + 'application/atom+xml') + deserializer = wsgi.RequestDeserializer( + supported_content_types=supported_content_types) + + wsgi.Resource.__init__(self, None, serializer=serializer, + deserializer=deserializer) def dispatch(self, request, *args): """Respond to a request for all OpenStack API versions.""" @@ -43,13 +57,143 @@ class Versions(wsgi.Resource): { "id": "v1.1", "status": "CURRENT", + #TODO(wwolf) get correct value for these + "updated": "2011-07-18T11:30:00Z", }, { "id": "v1.0", "status": "DEPRECATED", + #TODO(wwolf) get correct value for these + "updated": "2010-10-09T11:30:00Z", }, ] builder = nova.api.openstack.views.versions.get_view_builder(request) versions = [builder.build(version) for version in version_objs] return dict(versions=versions) + + +class VersionsXMLSerializer(wsgi.XMLDictSerializer): + def _versions_to_xml(self, versions): + root = self._xml_doc.createElement('versions') + + for version in versions: + root.appendChild(self._create_version_node(version)) + + return root + + def _create_version_node(self, version): + version_node = self._xml_doc.createElement('version') + version_node.setAttribute('id', version['id']) + version_node.setAttribute('status', version['status']) + version_node.setAttribute('updated', version['updated']) + + for link in version['links']: + link_node = self._xml_doc.createElement('atom:link') + link_node.setAttribute('rel', link['rel']) + link_node.setAttribute('href', link['href']) + version_node.appendChild(link_node) + + return version_node + + def default(self, data): + self._xml_doc = minidom.Document() + node = self._versions_to_xml(data['versions']) + + return self.to_xml_string(node) + + +class VersionsAtomSerializer(wsgi.XMLDictSerializer): + def __init__(self, metadata=None, xmlns=None): + if not xmlns: + self.xmlns = ATOM_XMLNS + else: + self.xmlns = xmlns + + def _create_text_elem(self, name, text, type=None): + elem = self._xml_doc.createElement(name) + if type: + elem.setAttribute('type', type) + elem_text = self._xml_doc.createTextNode(text) + elem.appendChild(elem_text) + return elem + + def _get_most_recent_update(self, versions): + recent = None + for version in versions: + updated = datetime.strptime(version['updated'], + '%Y-%m-%dT%H:%M:%SZ') + if not recent: + recent = updated + elif updated > recent: + recent = updated + + return recent.strftime('%Y-%m-%dT%H:%M:%SZ') + + def _get_base_url(self, link_href): + # Make sure no trailing / + link_href = link_href.rstrip('/') + return link_href.rsplit('/', 1)[0] + '/' + + def _create_meta(self, root, versions): + title = self._create_text_elem('title', 'Available API Versions', + type='text') + # Set this updated to the most recently updated version + recent = self._get_most_recent_update(versions) + updated = self._create_text_elem('updated', recent) + + base_url = self._get_base_url(versions[0]['links'][0]['href']) + id = self._create_text_elem('id', base_url) + link = self._xml_doc.createElement('link') + link.setAttribute('rel', 'self') + link.setAttribute('href', base_url) + + author = self._xml_doc.createElement('author') + author_name = self._create_text_elem('name', 'Rackspace') + author_uri = self._create_text_elem('uri', 'http://www.rackspace.com/') + author.appendChild(author_name) + author.appendChild(author_uri) + + root.appendChild(title) + root.appendChild(updated) + root.appendChild(id) + root.appendChild(author) + root.appendChild(link) + + def _create_version_entries(self, root, versions): + for version in versions: + entry = self._xml_doc.createElement('entry') + + id = self._create_text_elem('id', version['links'][0]['href']) + title = self._create_text_elem('title', + 'Version %s' % version['id'], + type='text') + updated = self._create_text_elem('updated', version['updated']) + + entry.appendChild(id) + entry.appendChild(title) + entry.appendChild(updated) + + for link in version['links']: + link_node = self._xml_doc.createElement('link') + link_node.setAttribute('rel', link['rel']) + link_node.setAttribute('href', link['href']) + entry.appendChild(link_node) + + content = self._create_text_elem('content', + 'Version %s %s (%s)' % + (version['id'], + version['status'], + version['updated']), + type='text') + + entry.appendChild(content) + root.appendChild(entry) + + def default(self, data): + self._xml_doc = minidom.Document() + node = self._xml_doc.createElementNS(self.xmlns, 'feed') + self._create_meta(node, data['versions']) + self._create_version_entries(node, data['versions']) + + return self.to_xml_string(node) diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py index 2810cce39..ddbf7a144 100644 --- a/nova/api/openstack/views/addresses.py +++ b/nova/api/openstack/views/addresses.py @@ -15,34 +15,75 @@ # License for the specific language governing permissions and limitations # under the License. +from nova import flags from nova import utils from nova.api.openstack import common +FLAGS = flags.FLAGS + class ViewBuilder(object): - ''' Models a server addresses response as a python dictionary.''' + """Models a server addresses response as a python dictionary.""" def build(self, inst): raise NotImplementedError() class ViewBuilderV10(ViewBuilder): + def build(self, inst): private_ips = self.build_private_parts(inst) public_ips = self.build_public_parts(inst) return dict(public=public_ips, private=private_ips) def build_public_parts(self, inst): - return utils.get_from_path(inst, 'fixed_ip/floating_ips/address') + return utils.get_from_path(inst, 'fixed_ips/floating_ips/address') def build_private_parts(self, inst): - return utils.get_from_path(inst, 'fixed_ip/address') + return utils.get_from_path(inst, 'fixed_ips/address') class ViewBuilderV11(ViewBuilder): - def build(self, inst): - private_ips = utils.get_from_path(inst, 'fixed_ip/address') - private_ips = [dict(version=4, addr=a) for a in private_ips] - public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') - public_ips = [dict(version=4, addr=a) for a in public_ips] - return dict(public=public_ips, private=private_ips) + + def build(self, interfaces): + networks = {} + for interface in interfaces: + network_label = interface['network']['label'] + + if network_label not in networks: + networks[network_label] = [] + + ip_addresses = list(self._extract_ipv4_addresses(interface)) + + if FLAGS.use_ipv6: + ipv6_address = self._extract_ipv6_address(interface) + if ipv6_address is not None: + ip_addresses.append(ipv6_address) + + networks[network_label].extend(ip_addresses) + + return networks + + def build_network(self, interfaces, network_label): + for interface in interfaces: + if interface['network']['label'] == network_label: + ips = list(self._extract_ipv4_addresses(interface)) + ipv6 = self._extract_ipv6_address(interface) + if ipv6 is not None: + ips.append(ipv6) + return {network_label: ips} + return None + + def _extract_ipv4_addresses(self, interface): + for fixed_ip in interface['fixed_ips']: + yield self._build_ip_entity(fixed_ip['address'], 4) + for floating_ip in fixed_ip.get('floating_ips', []): + yield self._build_ip_entity(floating_ip['address'], 4) + + def _extract_ipv6_address(self, interface): + fixed_ipv6 = interface.get('fixed_ipv6') + if fixed_ipv6 is not None: + return self._build_ip_entity(fixed_ipv6, 6) + + def _build_ip_entity(self, address, version): + return {'addr': address, 'version': version} diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py index 462890ab2..0403ece1b 100644 --- a/nova/api/openstack/views/flavors.py +++ b/nova/api/openstack/views/flavors.py @@ -71,6 +71,7 @@ class ViewBuilderV11(ViewBuilder): def _build_links(self, flavor_obj): """Generate a container of links that refer to the provided flavor.""" href = self.generate_href(flavor_obj["id"]) + bookmark = self.generate_bookmark(flavor_obj["id"]) links = [ { @@ -79,13 +80,7 @@ class ViewBuilderV11(ViewBuilder): }, { "rel": "bookmark", - "type": "application/json", - "href": href, - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": href, + "href": bookmark, }, ] @@ -94,3 +89,10 @@ class ViewBuilderV11(ViewBuilder): def generate_href(self, flavor_id): """Create an url that refers to a specific flavor id.""" return "%s/flavors/%s" % (self.base_url, flavor_id) + + def generate_bookmark(self, flavor_id): + """Create an url that refers to a specific flavor id.""" + return "%s/flavors/%s" % ( + common.remove_version_from_href(self.base_url), + flavor_id, + ) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index d6a054102..873ce212a 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -17,6 +17,8 @@ import os.path +from nova.api.openstack import common + class ViewBuilder(object): """Base class for generating responses to OpenStack API image requests.""" @@ -96,7 +98,20 @@ class ViewBuilderV11(ViewBuilder): def _build_server(self, image, image_obj): try: - image['serverRef'] = image_obj['properties']['instance_ref'] + serverRef = image_obj['properties']['instance_ref'] + image['server'] = { + "id": common.get_id_from_href(serverRef), + "links": [ + { + "rel": "self", + "href": serverRef, + }, + { + "rel": "bookmark", + "href": common.remove_version_from_href(serverRef), + }, + ] + } except KeyError: return @@ -104,20 +119,26 @@ class ViewBuilderV11(ViewBuilder): """Return a standardized image structure for display by the API.""" image = ViewBuilder.build(self, image_obj, detail) href = self.generate_href(image_obj["id"]) + bookmark = self.generate_bookmark(image_obj["id"]) + + image["links"] = [ + { + "rel": "self", + "href": href, + }, + { + "rel": "bookmark", + "href": bookmark, + }, - image["links"] = [{ - "rel": "self", - "href": href, - }, - { - "rel": "bookmark", - "type": "application/json", - "href": href, - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": href, - }] + ] + + if detail: + image["metadata"] = image_obj.get("properties", {}) return image + + def generate_bookmark(self, image_id): + """Create an url that refers to a specific flavor id.""" + return os.path.join(common.remove_version_from_href(self._url), + "images", str(image_id)) diff --git a/nova/api/openstack/views/limits.py b/nova/api/openstack/views/limits.py index 934b4921a..f603d7cb4 100644 --- a/nova/api/openstack/views/limits.py +++ b/nova/api/openstack/views/limits.py @@ -15,9 +15,11 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime import time from nova.api.openstack import common +from nova import utils class ViewBuilder(object): @@ -113,10 +115,12 @@ class ViewBuilderV11(ViewBuilder): return limits def _build_rate_limit(self, rate_limit): + next_avail = \ + datetime.datetime.utcfromtimestamp(rate_limit["resetTime"]) return { "verb": rate_limit["verb"], "value": rate_limit["value"], "remaining": int(rate_limit["remaining"]), "unit": rate_limit["unit"], - "next-available": rate_limit["resetTime"], + "next-available": utils.isotime(at=next_avail), } diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index cbfa5aae7..7131db088 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -77,13 +77,12 @@ class ViewBuilder(object): inst_dict = { 'id': inst['id'], 'name': inst['display_name'], - 'addresses': self.addresses_builder.build(inst), 'status': power_mapping[inst.get('state')]} ctxt = nova.context.get_admin_context() compute_api = nova.compute.API() - if compute_api.has_finished_migration(ctxt, inst['id']): + if compute_api.has_finished_migration(ctxt, inst['uuid']): inst_dict['status'] = 'RESIZE-CONFIRM' # Return the metadata as a dictionary @@ -98,10 +97,15 @@ class ViewBuilder(object): self._build_image(inst_dict, inst) self._build_flavor(inst_dict, inst) + self._build_addresses(inst_dict, inst) inst_dict['uuid'] = inst['uuid'] return dict(server=inst_dict) + def _build_addresses(self, response, inst): + """Return the addresses sub-resource of a server.""" + raise NotImplementedError() + def _build_image(self, response, inst): """Return the image sub-resource of a server.""" raise NotImplementedError() @@ -128,6 +132,9 @@ class ViewBuilderV10(ViewBuilder): if 'instance_type' in dict(inst): response['flavorId'] = inst['instance_type']['flavorid'] + def _build_addresses(self, response, inst): + response['addresses'] = self.addresses_builder.build(inst) + class ViewBuilderV11(ViewBuilder): """Model an Openstack API V1.0 server response.""" @@ -151,11 +158,16 @@ class ViewBuilderV11(ViewBuilder): flavor_ref = self.flavor_builder.generate_href(flavor_id) response["flavorRef"] = flavor_ref + def _build_addresses(self, response, inst): + interfaces = inst.get('virtual_interfaces', []) + response['addresses'] = self.addresses_builder.build(interfaces) + def _build_extra(self, response, inst): self._build_links(response, inst) def _build_links(self, response, inst): href = self.generate_href(inst["id"]) + bookmark = self.generate_bookmark(inst["id"]) links = [ { @@ -164,13 +176,7 @@ class ViewBuilderV11(ViewBuilder): }, { "rel": "bookmark", - "type": "application/json", - "href": href, - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": href, + "href": bookmark, }, ] @@ -179,3 +185,8 @@ class ViewBuilderV11(ViewBuilder): def generate_href(self, server_id): """Create an url that refers to a specific server id.""" return os.path.join(self.base_url, "servers", str(server_id)) + + def generate_bookmark(self, server_id): + """Create an url that refers to a specific flavor id.""" + return os.path.join(common.remove_version_from_href(self.base_url), + "servers", str(server_id)) diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py index d0145c94a..9fa8f49dc 100644 --- a/nova/api/openstack/views/versions.py +++ b/nova/api/openstack/views/versions.py @@ -36,6 +36,7 @@ class ViewBuilder(object): version = { "id": version_data["id"], "status": version_data["status"], + "updated": version_data["updated"], "links": self._build_links(version_data), } @@ -56,4 +57,4 @@ class ViewBuilder(object): def generate_href(self, version_number): """Create an url that refers to a specific version_number.""" - return os.path.join(self.base_url, version_number) + return os.path.join(self.base_url, version_number) + '/' diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 5b6e3cb1d..a28443d12 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -13,6 +13,7 @@ from nova import wsgi XMLNS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0' XMLNS_V11 = 'http://docs.openstack.org/compute/api/v1.1' +XMLNS_ATOM = 'http://www.w3.org/2005/Atom' LOG = logging.getLogger('nova.api.openstack.wsgi') @@ -20,21 +21,22 @@ LOG = logging.getLogger('nova.api.openstack.wsgi') class Request(webob.Request): """Add some Openstack API-specific logic to the base webob.Request.""" - def best_match_content_type(self): + def best_match_content_type(self, supported_content_types=None): """Determine the requested response content-type. Based on the query extension then the Accept header. """ - supported = ('application/json', 'application/xml') + supported_content_types = supported_content_types or \ + ('application/json', 'application/xml') parts = self.path.rsplit('.', 1) if len(parts) > 1: ctype = 'application/{0}'.format(parts[1]) - if ctype in supported: + if ctype in supported_content_types: return ctype - bm = self.accept.best_match(supported) + bm = self.accept.best_match(supported_content_types) # default to application/json if we don't find a preference return bm or 'application/json' @@ -46,38 +48,51 @@ class Request(webob.Request): """ if not "Content-Type" in self.headers: - raise exception.InvalidContentType(content_type=None) + return None allowed_types = ("application/xml", "application/json") content_type = self.content_type if content_type not in allowed_types: raise exception.InvalidContentType(content_type=content_type) - else: - return content_type + return content_type -class TextDeserializer(object): - """Custom request body deserialization based on controller action name.""" - def deserialize(self, datastring, action='default'): - """Find local deserialization method and parse request body.""" +class ActionDispatcher(object): + """Maps method name to local methods through action name.""" + + def dispatch(self, *args, **kwargs): + """Find and call local method.""" + action = kwargs.pop('action', 'default') action_method = getattr(self, str(action), self.default) - return action_method(datastring) + return action_method(*args, **kwargs) - def default(self, datastring): - """Default deserialization code should live here""" + def default(self, data): raise NotImplementedError() -class JSONDeserializer(TextDeserializer): +class TextDeserializer(ActionDispatcher): + """Default request body deserialization""" + + def deserialize(self, datastring, action='default'): + return self.dispatch(datastring, action=action) def default(self, datastring): + return {} + + +class JSONDeserializer(TextDeserializer): + + def _from_json(self, datastring): try: return utils.loads(datastring) except ValueError: - raise exception.MalformedRequestBody( - reason=_("malformed JSON in request body")) + msg = _("cannot understand JSON") + raise exception.MalformedRequestBody(reason=msg) + + def default(self, datastring): + return {'body': self._from_json(datastring)} class XMLDeserializer(TextDeserializer): @@ -90,15 +105,15 @@ class XMLDeserializer(TextDeserializer): super(XMLDeserializer, self).__init__() self.metadata = metadata or {} - def default(self, datastring): + def _from_xml(self, datastring): plurals = set(self.metadata.get('plurals', {})) try: node = minidom.parseString(datastring).childNodes[0] return {node.nodeName: self._from_xml_node(node, plurals)} except expat.ExpatError: - raise exception.MalformedRequestBody( - reason=_("malformed XML in request body")) + msg = _("cannot understand XML") + raise exception.MalformedRequestBody(reason=msg) def _from_xml_node(self, node, listnames): """Convert a minidom node to a simple Python type. @@ -121,21 +136,71 @@ class XMLDeserializer(TextDeserializer): listnames) return result + def find_first_child_named(self, parent, name): + """Search a nodes children for the first child with a given name""" + for node in parent.childNodes: + if node.nodeName == name: + return node + return None + + def find_children_named(self, parent, name): + """Return all of a nodes children who have the given name""" + for node in parent.childNodes: + if node.nodeName == name: + yield node + + def extract_text(self, node): + """Get the text field contained by the given node""" + if len(node.childNodes) == 1: + child = node.childNodes[0] + if child.nodeType == child.TEXT_NODE: + return child.nodeValue + return "" + + def default(self, datastring): + return {'body': self._from_xml(datastring)} + + +class MetadataXMLDeserializer(XMLDeserializer): + + def extract_metadata(self, metadata_node): + """Marshal the metadata attribute of a parsed request""" + if metadata_node is None: + return None + metadata = {} + for meta_node in self.find_children_named(metadata_node, "meta"): + key = meta_node.getAttribute("key") + metadata[key] = self.extract_text(meta_node) + return metadata + + +class RequestHeadersDeserializer(ActionDispatcher): + """Default request headers deserializer""" + + def deserialize(self, request, action): + return self.dispatch(request, action=action) + + def default(self, request): + return {} + class RequestDeserializer(object): """Break up a Request object into more useful pieces.""" - def __init__(self, deserializers=None): - """ - :param deserializers: dictionary of content-type-specific deserializers + def __init__(self, body_deserializers=None, headers_deserializer=None, + supported_content_types=None): - """ - self.deserializers = { + self.supported_content_types = supported_content_types or \ + ('application/json', 'application/xml') + + self.body_deserializers = { 'application/xml': XMLDeserializer(), 'application/json': JSONDeserializer(), } + self.body_deserializers.update(body_deserializers or {}) - self.deserializers.update(deserializers or {}) + self.headers_deserializer = headers_deserializer or \ + RequestHeadersDeserializer() def deserialize(self, request): """Extract necessary pieces of the request. @@ -149,31 +214,47 @@ class RequestDeserializer(object): action_args = self.get_action_args(request.environ) action = action_args.pop('action', None) - if request.method.lower() in ('post', 'put'): - if len(request.body) == 0: - action_args['body'] = None - else: - content_type = request.get_content_type() - deserializer = self.get_deserializer(content_type) - - try: - body = deserializer.deserialize(request.body, action) - action_args['body'] = body - except exception.InvalidContentType: - action_args['body'] = None + action_args.update(self.deserialize_headers(request, action)) + action_args.update(self.deserialize_body(request, action)) accept = self.get_expected_content_type(request) return (action, action_args, accept) - def get_deserializer(self, content_type): + def deserialize_headers(self, request, action): + return self.headers_deserializer.deserialize(request, action) + + def deserialize_body(self, request, action): + try: + content_type = request.get_content_type() + except exception.InvalidContentType: + LOG.debug(_("Unrecognized Content-Type provided in request")) + return {} + + if content_type is None: + LOG.debug(_("No Content-Type provided in request")) + return {} + + if not len(request.body) > 0: + LOG.debug(_("Empty body provided in request")) + return {} + + try: + deserializer = self.get_body_deserializer(content_type) + except exception.InvalidContentType: + LOG.debug(_("Unable to deserialize body as provided Content-Type")) + raise + + return deserializer.deserialize(request.body, action) + + def get_body_deserializer(self, content_type): try: - return self.deserializers[content_type] + return self.body_deserializers[content_type] except (KeyError, TypeError): raise exception.InvalidContentType(content_type=content_type) def get_expected_content_type(self, request): - return request.best_match_content_type() + return request.best_match_content_type(self.supported_content_types) def get_action_args(self, request_environment): """Parse dictionary created by routes library.""" @@ -195,20 +276,18 @@ class RequestDeserializer(object): return args -class DictSerializer(object): - """Custom response body serialization based on controller action name.""" +class DictSerializer(ActionDispatcher): + """Default request body serialization""" def serialize(self, data, action='default'): - """Find local serialization method and encode response body.""" - action_method = getattr(self, str(action), self.default) - return action_method(data) + return self.dispatch(data, action=action) def default(self, data): - """Default serialization code should live here""" - raise NotImplementedError() + return "" class JSONDictSerializer(DictSerializer): + """Default JSON request body serialization""" def default(self, data): return utils.dumps(data) @@ -232,13 +311,21 @@ class XMLDictSerializer(DictSerializer): doc = minidom.Document() node = self._to_xml_node(doc, self.metadata, root_key, data[root_key]) - self._add_xmlns(node) + return self.to_xml_string(node) - return node.toprettyxml(indent=' ', encoding='utf-8') + def to_xml_string(self, node, has_atom=False): + self._add_xmlns(node, has_atom) + return node.toprettyxml(indent=' ', encoding='UTF-8') - def _add_xmlns(self, node): + #NOTE (ameade): the has_atom should be removed after all of the + # xml serializers and view builders have been updated to the current + # spec that required all responses include the xmlns:atom, the has_atom + # flag is to prevent current tests from breaking + def _add_xmlns(self, node, has_atom=False): if self.xmlns is not None: node.setAttribute('xmlns', self.xmlns) + if has_atom: + node.setAttribute('xmlns:atom', "http://www.w3.org/2005/Atom") def _to_xml_node(self, doc, metadata, nodename, data): """Recursive method to convert data members to XML nodes.""" @@ -294,20 +381,38 @@ class XMLDictSerializer(DictSerializer): result.appendChild(node) return result + def _create_link_nodes(self, xml_doc, links): + link_nodes = [] + for link in links: + link_node = xml_doc.createElement('atom:link') + link_node.setAttribute('rel', link['rel']) + link_node.setAttribute('href', link['href']) + link_nodes.append(link_node) + return link_nodes + + +class ResponseHeadersSerializer(ActionDispatcher): + """Default response headers serialization""" + + def serialize(self, response, data, action): + self.dispatch(response, data, action=action) + + def default(self, response, data): + response.status_int = 200 + class ResponseSerializer(object): """Encode the necessary pieces into a response object""" - def __init__(self, serializers=None): - """ - :param serializers: dictionary of content-type-specific serializers - - """ - self.serializers = { + def __init__(self, body_serializers=None, headers_serializer=None): + self.body_serializers = { 'application/xml': XMLDictSerializer(), 'application/json': JSONDictSerializer(), } - self.serializers.update(serializers or {}) + self.body_serializers.update(body_serializers or {}) + + self.headers_serializer = headers_serializer or \ + ResponseHeadersSerializer() def serialize(self, response_data, content_type, action='default'): """Serialize a dict into a string and wrap in a wsgi.Request object. @@ -317,16 +422,22 @@ class ResponseSerializer(object): """ response = webob.Response() - response.headers['Content-Type'] = content_type + self.serialize_headers(response, response_data, action) + self.serialize_body(response, response_data, content_type, action) + return response - serializer = self.get_serializer(content_type) - response.body = serializer.serialize(response_data, action) + def serialize_headers(self, response, data, action): + self.headers_serializer.serialize(response, data, action) - return response + def serialize_body(self, response, data, content_type, action): + response.headers['Content-Type'] = content_type + if data is not None: + serializer = self.get_body_serializer(content_type) + response.body = serializer.serialize(data, action) - def get_serializer(self, content_type): + def get_body_serializer(self, content_type): try: - return self.serializers[content_type] + return self.body_serializers[content_type] except (KeyError, TypeError): raise exception.InvalidContentType(content_type=content_type) @@ -343,16 +454,19 @@ class Resource(wsgi.Application): serialized by requested content type. """ - def __init__(self, controller, serializers=None, deserializers=None): + + def __init__(self, controller, deserializer=None, serializer=None): """ :param controller: object that implement methods created by routes lib - :param serializers: dict of content-type specific text serializers - :param deserializers: dict of content-type specific text deserializers + :param deserializer: object that can serialize the output of a + controller into a webob response + :param serializer: object that can deserialize a webob request + into necessary pieces """ self.controller = controller - self.serializer = ResponseSerializer(serializers) - self.deserializer = RequestDeserializer(deserializers) + self.deserializer = deserializer or RequestDeserializer() + self.serializer = serializer or ResponseSerializer() @webob.dec.wsgify(RequestClass=Request) def __call__(self, request): @@ -362,20 +476,24 @@ class Resource(wsgi.Application): "url": request.url}) try: - action, action_args, accept = self.deserializer.deserialize( - request) + action, args, accept = self.deserializer.deserialize(request) except exception.InvalidContentType: msg = _("Unsupported Content-Type") - return webob.exc.HTTPBadRequest(explanation=msg) + return faults.Fault(webob.exc.HTTPBadRequest(explanation=msg)) except exception.MalformedRequestBody: msg = _("Malformed request body") return faults.Fault(webob.exc.HTTPBadRequest(explanation=msg)) - action_result = self.dispatch(request, action, action_args) - - #TODO(bcwaldon): find a more elegant way to pass through non-dict types - if type(action_result) is dict: - response = self.serializer.serialize(action_result, accept, action) + try: + action_result = self.dispatch(request, action, args) + except webob.exc.HTTPException as ex: + LOG.info(_("HTTP exception thrown: %s"), unicode(ex)) + action_result = faults.Fault(ex) + + if type(action_result) is dict or action_result is None: + response = self.serializer.serialize(action_result, + accept, + action=action) else: response = action_result @@ -394,4 +512,8 @@ class Resource(wsgi.Application): """Find action-spefic method on controller and call it.""" controller_method = getattr(self.controller, action) - return controller_method(req=request, **action_args) + try: + return controller_method(req=request, **action_args) + except TypeError, exc: + LOG.debug(str(exc)) + return webob.exc.HTTPBadRequest() diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py index 8864f825b..f7fd87bcd 100644 --- a/nova/api/openstack/zones.py +++ b/nova/api/openstack/zones.py @@ -27,7 +27,6 @@ from nova.scheduler import api from nova.api.openstack import create_instance_helper as helper from nova.api.openstack import common -from nova.api.openstack import faults from nova.api.openstack import wsgi @@ -127,11 +126,8 @@ class Controller(object): Returns a reservation ID (a UUID). """ result = None - try: - extra_values, result = self.helper.create_instance(req, body, - self.compute_api.create_all_at_once) - except faults.Fault, f: - return f + extra_values, result = self.helper.create_instance(req, body, + self.compute_api.create_all_at_once) reservation_id = result return {'reservation_id': reservation_id} @@ -196,14 +192,15 @@ def create_resource(version): }, } - serializers = { + body_serializers = { 'application/xml': wsgi.XMLDictSerializer(xmlns=wsgi.XMLNS_V10, metadata=metadata), } + serializer = wsgi.ResponseSerializer(body_serializers) - deserializers = { + body_deserializers = { 'application/xml': helper.ServerXMLDeserializer(), } + deserializer = wsgi.RequestDeserializer(body_deserializers) - return wsgi.Resource(controller, serializers=serializers, - deserializers=deserializers) + return wsgi.Resource(controller, deserializer, serializer) diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 98c7dd263..b6131fb7f 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -630,13 +630,17 @@ class AuthManager(object): not been allocated for user. """ - network_ref = db.project_get_network(context.get_admin_context(), - Project.safe_id(project), False) - - if not network_ref: + networks = db.project_get_networks(context.get_admin_context(), + Project.safe_id(project), False) + if not networks: return (None, None) - return (network_ref['vpn_public_address'], - network_ref['vpn_public_port']) + + # TODO(tr3buchet): not sure what you guys plan on doing with this + # but it's possible for a project to have multiple sets of vpn data + # for now I'm just returning the first one + network = networks[0] + return (network['vpn_public_address'], + network['vpn_public_port']) def delete_project(self, project): """Deletes a project""" diff --git a/nova/compute/api.py b/nova/compute/api.py index af18741b6..c49c0d95c 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -32,6 +32,7 @@ from nova import quota from nova import rpc from nova import utils from nova import volume +from nova.api.ec2 import ec2utils from nova.compute import instance_types from nova.compute import power_state from nova.compute.utils import terminate_volumes @@ -48,9 +49,27 @@ flags.DEFINE_integer('find_host_timeout', 30, 'Timeout after NN seconds when looking for a host.') -def generate_default_hostname(instance_id): +def generate_default_hostname(instance): """Default function to generate a hostname given an instance reference.""" - return str(instance_id) + display_name = instance['display_name'] + if display_name is None: + return 'server_%d' % (instance['id'],) + table = '' + deletions = '' + for i in xrange(256): + c = chr(i) + if ('a' <= c <= 'z') or ('0' <= c <= '9') or (c == '-'): + table += c + elif c == ' ': + table += '_' + elif ('A' <= c <= 'Z'): + table += c.lower() + else: + table += '\0' + deletions += c + if isinstance(display_name, unicode): + display_name = display_name.encode('latin-1', 'ignore') + return display_name.translate(table, deletions) def _is_able_to_shutdown(instance, instance_id): @@ -83,23 +102,6 @@ class API(base.Base): self.hostname_factory = hostname_factory super(API, self).__init__(**kwargs) - def get_network_topic(self, context, instance_id): - """Get the network topic for an instance.""" - try: - instance = self.get(context, instance_id) - except exception.NotFound: - LOG.warning(_("Instance %d was not found in get_network_topic"), - instance_id) - raise - - host = instance['host'] - if not host: - raise exception.Error(_("Instance %d has no host") % instance_id) - topic = self.db.queue_get_for(context, FLAGS.compute_topic, host) - return rpc.call(context, - topic, - {"method": "get_network_topic", "args": {'fake': 1}}) - def _check_injected_file_quota(self, context, injected_files): """Enforce quota limits on injected files. @@ -125,7 +127,7 @@ class API(base.Base): quota_metadata = quota.allowed_metadata_items(context, num_metadata) if quota_metadata < num_metadata: pid = context.project_id - msg = _("Quota exceeeded for %(pid)s, tried to set " + msg = _("Quota exceeded for %(pid)s, tried to set " "%(num_metadata)s metadata properties") % locals() LOG.warn(msg) raise quota.QuotaError(msg, "MetadataLimitExceeded") @@ -136,14 +138,14 @@ class API(base.Base): for k, v in metadata.iteritems(): if len(k) > 255 or len(v) > 255: pid = context.project_id - msg = _("Quota exceeeded for %(pid)s, metadata property " + msg = _("Quota exceeded for %(pid)s, metadata property " "key or value too long") % locals() LOG.warn(msg) raise quota.QuotaError(msg, "MetadataLimitExceeded") def _check_create_parameters(self, context, instance_type, image_href, kernel_id=None, ramdisk_id=None, - min_count=1, max_count=1, + min_count=None, max_count=None, display_name='', display_description='', key_name=None, key_data=None, security_group='default', availability_zone=None, user_data=None, metadata={}, @@ -154,12 +156,16 @@ class API(base.Base): if not instance_type: instance_type = instance_types.get_default_instance_type() + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count num_instances = quota.allowed_instances(context, max_count, instance_type) if num_instances < min_count: pid = context.project_id - LOG.warn(_("Quota exceeeded for %(pid)s," + LOG.warn(_("Quota exceeded for %(pid)s," " tried to run %(min_count)s instances") % locals()) if num_instances <= 0: message = _("Instance quota exceeded. You cannot run any " @@ -203,18 +209,7 @@ class API(base.Base): if ramdisk_id: image_service.show(context, ramdisk_id) - if security_group is None: - security_group = ['default'] - if not type(security_group) is list: - security_group = [security_group] - - security_groups = [] self.ensure_default_security_group(context) - for security_group_name in security_group: - group = db.security_group_get_by_name(context, - context.project_id, - security_group_name) - security_groups.append(group['id']) if key_data is None and key_name: key_pair = db.key_pair_get(context, context.user_id, key_name) @@ -223,6 +218,9 @@ class API(base.Base): if reservation_id is None: reservation_id = utils.generate_uid('r') + root_device_name = ec2utils.properties_root_device_name( + image['properties']) + base_options = { 'reservation_id': reservation_id, 'image_ref': image_href, @@ -247,53 +245,109 @@ class API(base.Base): 'availability_zone': availability_zone, 'os_type': os_type, 'architecture': architecture, - 'vm_mode': vm_mode} + 'vm_mode': vm_mode, + 'root_device_name': root_device_name} - return (num_instances, base_options, security_groups) + return (num_instances, base_options, image) + + def _update_image_block_device_mapping(self, elevated_context, instance_id, + mappings): + """tell vm driver to create ephemeral/swap device at boot time by + updating BlockDeviceMapping + """ + for bdm in ec2utils.mappings_prepend_dev(mappings): + LOG.debug(_("bdm %s"), bdm) + + virtual_name = bdm['virtual'] + if virtual_name == 'ami' or virtual_name == 'root': + continue + + assert (virtual_name == 'swap' or + virtual_name.startswith('ephemeral')) + values = { + 'instance_id': instance_id, + 'device_name': bdm['device'], + 'virtual_name': virtual_name, } + self.db.block_device_mapping_update_or_create(elevated_context, + values) + + def _update_block_device_mapping(self, elevated_context, instance_id, + block_device_mapping): + """tell vm driver to attach volume at boot time by updating + BlockDeviceMapping + """ + for bdm in block_device_mapping: + LOG.debug(_('bdm %s'), bdm) + assert 'device_name' in bdm - def create_db_entry_for_new_instance(self, context, base_options, - security_groups, block_device_mapping, num=1): + values = {'instance_id': instance_id} + for key in ('device_name', 'delete_on_termination', 'virtual_name', + 'snapshot_id', 'volume_id', 'volume_size', + 'no_device'): + values[key] = bdm.get(key) + + # NOTE(yamahata): NoDevice eliminates devices defined in image + # files by command line option. + # (--block-device-mapping) + if bdm.get('virtual_name') == 'NoDevice': + values['no_device'] = True + for k in ('delete_on_termination', 'volume_id', + 'snapshot_id', 'volume_id', 'volume_size', + 'virtual_name'): + values[k] = None + + self.db.block_device_mapping_update_or_create(elevated_context, + values) + + def create_db_entry_for_new_instance(self, context, image, base_options, + security_group, block_device_mapping, num=1): """Create an entry in the DB for this new instance, - including any related table updates (such as security - groups, MAC address, etc). This will called by create() - in the majority of situations, but all-at-once style - Schedulers may initiate the call.""" - instance = dict(mac_address=utils.generate_mac(), - launch_index=num, - **base_options) + including any related table updates (such as security group, + etc). + + This will called by create() in the majority of situations, + but create_all_at_once() style Schedulers may initiate the call. + If you are changing this method, be sure to update both + call paths. + """ + instance = dict(launch_index=num, **base_options) instance = self.db.instance_create(context, instance) instance_id = instance['id'] elevated = context.elevated() - if not security_groups: - security_groups = [] + if security_group is None: + security_group = ['default'] + if not isinstance(security_group, list): + security_group = [security_group] + + security_groups = [] + for security_group_name in security_group: + group = db.security_group_get_by_name(context, + context.project_id, + security_group_name) + security_groups.append(group['id']) + for security_group_id in security_groups: self.db.instance_add_security_group(elevated, instance_id, security_group_id) - # NOTE(yamahata) - # tell vm driver to attach volume at boot time by updating - # BlockDeviceMapping - for bdm in block_device_mapping: - LOG.debug(_('bdm %s'), bdm) - assert 'device_name' in bdm - values = { - 'instance_id': instance_id, - 'device_name': bdm['device_name'], - 'delete_on_termination': bdm.get('delete_on_termination'), - 'virtual_name': bdm.get('virtual_name'), - 'snapshot_id': bdm.get('snapshot_id'), - 'volume_id': bdm.get('volume_id'), - 'volume_size': bdm.get('volume_size'), - 'no_device': bdm.get('no_device')} - self.db.block_device_mapping_create(elevated, values) + # BlockDeviceMapping table + self._update_image_block_device_mapping(elevated, instance_id, + image['properties'].get('mappings', [])) + self._update_block_device_mapping(elevated, instance_id, + image['properties'].get('block_device_mapping', [])) + # override via command line option + self._update_block_device_mapping(elevated, instance_id, + block_device_mapping) # Set sane defaults if not specified - updates = dict(hostname=self.hostname_factory(instance_id)) + updates = {} if (not hasattr(instance, 'display_name') or instance.display_name is None): updates['display_name'] = "Server %s" % instance_id + instance['display_name'] = updates['display_name'] + updates['hostname'] = self.hostname_factory(instance) instance = self.update(context, instance_id, **updates) @@ -338,17 +392,16 @@ class API(base.Base): def create_all_at_once(self, context, instance_type, image_href, kernel_id=None, ramdisk_id=None, - min_count=1, max_count=1, + min_count=None, max_count=None, display_name='', display_description='', key_name=None, key_data=None, security_group='default', availability_zone=None, user_data=None, metadata={}, injected_files=None, admin_password=None, zone_blob=None, - reservation_id=None): + reservation_id=None, block_device_mapping=None): """Provision the instances by passing the whole request to the Scheduler for execution. Returns a Reservation ID related to the creation of all of these instances.""" - num_instances, base_options, security_groups = \ - self._check_create_parameters( + num_instances, base_options, image = self._check_create_parameters( context, instance_type, image_href, kernel_id, ramdisk_id, min_count, max_count, @@ -368,7 +421,7 @@ class API(base.Base): def create(self, context, instance_type, image_href, kernel_id=None, ramdisk_id=None, - min_count=1, max_count=1, + min_count=None, max_count=None, display_name='', display_description='', key_name=None, key_data=None, security_group='default', availability_zone=None, user_data=None, metadata={}, @@ -380,11 +433,13 @@ class API(base.Base): Scheduler drivers, but may remove the effectiveness of the more complicated drivers. + NOTE: If you change this method, be sure to change + create_all_at_once() at the same time! + Returns a list of instance dicts. """ - num_instances, base_options, security_groups = \ - self._check_create_parameters( + num_instances, base_options, image = self._check_create_parameters( context, instance_type, image_href, kernel_id, ramdisk_id, min_count, max_count, @@ -398,8 +453,8 @@ class API(base.Base): instances = [] LOG.debug(_("Going to run %s instances..."), num_instances) for num in range(num_instances): - instance = self.create_db_entry_for_new_instance(context, - base_options, security_groups, + instance = self.create_db_entry_for_new_instance(context, image, + base_options, security_group, block_device_mapping, num=num) instances.append(instance) instance_id = instance['id'] @@ -412,10 +467,10 @@ class API(base.Base): return [dict(x.iteritems()) for x in instances] - def has_finished_migration(self, context, instance_id): + def has_finished_migration(self, context, instance_uuid): """Returns true if an instance has a finished migration.""" try: - db.migration_get_by_instance_and_status(context, instance_id, + db.migration_get_by_instance_and_status(context, instance_uuid, 'finished') return True except exception.NotFound: @@ -613,50 +668,60 @@ class API(base.Base): """ return self.get(context, instance_id) - def get_all_across_zones(self, context, reservation_id): - """Get all instances with this reservation_id, across - all available Zones (if any). - """ - context = context.elevated() - instances = self.db.instance_get_all_by_reservation( - context, reservation_id) - - children = scheduler_api.call_zone_method(context, "list", - novaclient_collection_name="servers", - reservation_id=reservation_id) - - for zone, servers in children: - for server in servers: - # Results are ready to send to user. No need to scrub. - server._info['_is_precooked'] = True - instances.append(server._info) - return instances - def get_all(self, context, project_id=None, reservation_id=None, - fixed_ip=None): + fixed_ip=None, recurse_zones=False): """Get all instances filtered by one of the given parameters. If there is no filter and the context is an admin, it will retreive all instances in the system. """ - if reservation_id is not None: - return self.get_all_across_zones(context, reservation_id) - - if fixed_ip is not None: - return self.db.fixed_ip_get_instance(context, fixed_ip) - if project_id or not context.is_admin: + if reservation_id is not None: + recurse_zones = True + instances = self.db.instance_get_all_by_reservation( + context, reservation_id) + elif fixed_ip is not None: + try: + instances = self.db.fixed_ip_get_instance(context, fixed_ip) + except exception.FloatingIpNotFound, e: + if not recurse_zones: + raise + instances = None + elif project_id or not context.is_admin: if not context.project: - return self.db.instance_get_all_by_user( + instances = self.db.instance_get_all_by_user( context, context.user_id) + else: + if project_id is None: + project_id = context.project_id + instances = self.db.instance_get_all_by_project( + context, project_id) + else: + instances = self.db.instance_get_all(context) - if project_id is None: - project_id = context.project_id + if instances is None: + instances = [] + elif not isinstance(instances, list): + instances = [instances] - return self.db.instance_get_all_by_project( - context, project_id) + if not recurse_zones: + return instances - return self.db.instance_get_all(context) + admin_context = context.elevated() + children = scheduler_api.call_zone_method(admin_context, + "list", + novaclient_collection_name="servers", + reservation_id=reservation_id, + project_id=project_id, + fixed_ip=fixed_ip, + recurse_zones=True) + + for zone, servers in children: + for server in servers: + # Results are ready to send to user. No need to scrub. + server._info['_is_precooked'] = True + instances.append(server._info) + return instances def _cast_compute_message(self, method, context, instance_id, host=None, params=None): @@ -690,7 +755,7 @@ class API(base.Base): params = {} if not host: instance = self.get(context, instance_id) - host = instance["host"] + host = instance['host'] queue = self.db.queue_get_for(context, FLAGS.compute_topic, host) params['instance_id'] = instance_id kwargs = {'method': method, 'args': params} @@ -711,19 +776,60 @@ class API(base.Base): raise exception.Error(_("Unable to find host for Instance %s") % instance_id) + def backup(self, context, instance_id, name, backup_type, rotation, + extra_properties=None): + """Backup the given instance + + :param instance_id: nova.db.sqlalchemy.models.Instance.Id + :param name: name of the backup or snapshot + name = backup_type # daily backups are called 'daily' + :param rotation: int representing how many backups to keep around; + None if rotation shouldn't be used (as in the case of snapshots) + :param extra_properties: dict of extra image properties to include + """ + recv_meta = self._create_image(context, instance_id, name, 'backup', + backup_type=backup_type, rotation=rotation, + extra_properties=extra_properties) + return recv_meta + def snapshot(self, context, instance_id, name, extra_properties=None): """Snapshot the given instance. + :param instance_id: nova.db.sqlalchemy.models.Instance.Id + :param name: name of the backup or snapshot + :param extra_properties: dict of extra image properties to include + :returns: A dict containing image metadata """ - properties = {'instance_id': str(instance_id), + return self._create_image(context, instance_id, name, 'snapshot', + extra_properties=extra_properties) + + def _create_image(self, context, instance_id, name, image_type, + backup_type=None, rotation=None, extra_properties=None): + """Create snapshot or backup for an instance on this host. + + :param context: security context + :param instance_id: nova.db.sqlalchemy.models.Instance.Id + :param name: string for name of the snapshot + :param image_type: snapshot | backup + :param backup_type: daily | weekly + :param rotation: int representing how many backups to keep around; + None if rotation shouldn't be used (as in the case of snapshots) + :param extra_properties: dict of extra image properties to include + + """ + instance = db.api.instance_get(context, instance_id) + properties = {'instance_uuid': instance['uuid'], 'user_id': str(context.user_id), - 'image_state': 'creating'} + 'image_state': 'creating', + 'image_type': image_type, + 'backup_type': backup_type} properties.update(extra_properties or {}) sent_meta = {'name': name, 'is_public': False, 'status': 'creating', 'properties': properties} recv_meta = self.image_service.create(context, sent_meta) - params = {'image_id': recv_meta['id']} + params = {'image_id': recv_meta['id'], 'image_type': image_type, + 'backup_type': backup_type, 'rotation': rotation} self._cast_compute_message('snapshot_instance', context, instance_id, params=params) return recv_meta @@ -762,46 +868,68 @@ class API(base.Base): instance_id, params=rebuild_params) + @scheduler_api.reroute_compute("revert_resize") def revert_resize(self, context, instance_id): """Reverts a resize, deleting the 'new' instance in the process.""" context = context.elevated() + instance_ref = self._get_instance(context, instance_id, + 'revert_resize') migration_ref = self.db.migration_get_by_instance_and_status(context, - instance_id, 'finished') + instance_ref['uuid'], 'finished') if not migration_ref: raise exception.MigrationNotFoundByStatus(instance_id=instance_id, status='finished') params = {'migration_id': migration_ref['id']} - self._cast_compute_message('revert_resize', context, instance_id, - migration_ref['dest_compute'], params=params) + self._cast_compute_message('revert_resize', context, + instance_ref['uuid'], + migration_ref['source_compute'], + params=params) + self.db.migration_update(context, migration_ref['id'], {'status': 'reverted'}) + @scheduler_api.reroute_compute("confirm_resize") def confirm_resize(self, context, instance_id): """Confirms a migration/resize and deletes the 'old' instance.""" context = context.elevated() + instance_ref = self._get_instance(context, instance_id, + 'confirm_resize') migration_ref = self.db.migration_get_by_instance_and_status(context, - instance_id, 'finished') + instance_ref['uuid'], 'finished') if not migration_ref: raise exception.MigrationNotFoundByStatus(instance_id=instance_id, status='finished') - instance_ref = self.db.instance_get(context, instance_id) params = {'migration_id': migration_ref['id']} - self._cast_compute_message('confirm_resize', context, instance_id, - migration_ref['source_compute'], params=params) + self._cast_compute_message('confirm_resize', context, + instance_ref['uuid'], + migration_ref['dest_compute'], + params=params) self.db.migration_update(context, migration_ref['id'], {'status': 'confirmed'}) self.db.instance_update(context, instance_id, {'host': migration_ref['dest_compute'], }) - def resize(self, context, instance_id, flavor_id): - """Resize a running instance.""" - instance = self.db.instance_get(context, instance_id) - current_instance_type = instance['instance_type'] + @scheduler_api.reroute_compute("resize") + def resize(self, context, instance_id, flavor_id=None): + """Resize (ie, migrate) a running instance. + + If flavor_id is None, the process is considered a migration, keeping + the original flavor_id. If flavor_id is not None, the instance should + be migrated to a new host and resized to the new flavor_id. + """ + instance_ref = self._get_instance(context, instance_id, 'resize') + current_instance_type = instance_ref['instance_type'] + + # If flavor_id is not provided, only migrate the instance. + if not flavor_id: + LOG.debug(_("flavor_id is None. Assuming migration.")) + new_instance_type = current_instance_type + else: + new_instance_type = self.db.instance_type_get_by_flavor_id( + context, flavor_id) - new_instance_type = self.db.instance_type_get_by_flavor_id( - context, flavor_id) current_instance_type_name = current_instance_type['name'] new_instance_type_name = new_instance_type['name'] LOG.debug(_("Old instance type %(current_instance_type_name)s, " @@ -815,15 +943,40 @@ class API(base.Base): if current_memory_mb > new_memory_mb: raise exception.ApiError(_("Invalid flavor: cannot downsize" "instances")) - if current_memory_mb == new_memory_mb: + + if (current_memory_mb == new_memory_mb) and flavor_id: raise exception.ApiError(_("Invalid flavor: cannot use" "the same flavor. ")) + instance_ref = self._get_instance(context, instance_id, 'resize') self._cast_scheduler_message(context, {"method": "prep_resize", "args": {"topic": FLAGS.compute_topic, - "instance_id": instance_id, - "flavor_id": flavor_id}}) + "instance_id": instance_ref['uuid'], + "flavor_id": new_instance_type['id']}}) + + @scheduler_api.reroute_compute("add_fixed_ip") + def add_fixed_ip(self, context, instance_id, network_id): + """Add fixed_ip from specified network to given instance.""" + self._cast_compute_message('add_fixed_ip_to_instance', context, + instance_id, + params=dict(network_id=network_id)) + + @scheduler_api.reroute_compute("remove_fixed_ip") + def remove_fixed_ip(self, context, instance_id, address): + """Remove fixed_ip from specified network to given instance.""" + self._cast_compute_message('remove_fixed_ip_from_instance', context, + instance_id, params=dict(address=address)) + + #TODO(tr3buchet): how to run this in the correct zone? + def add_network_to_project(self, context, project_id): + """Force adds a network to the project.""" + # this will raise if zone doesn't know about project so the decorator + # can catch it and pass it down + self.db.project_get(context, project_id) + + # didn't raise so this is the correct zone + self.network_api.add_network_to_project(context, project_id) @scheduler_api.reroute_compute("pause") def pause(self, context, instance_id): @@ -835,6 +988,11 @@ class API(base.Base): """Unpause the given instance.""" self._cast_compute_message('unpause_instance', context, instance_id) + def set_host_enabled(self, context, host, enabled): + """Sets the specified host's ability to accept new instances.""" + return self._call_compute_message("set_host_enabled", context, + instance_id=None, host=host, params={"enabled": enabled}) + @scheduler_api.reroute_compute("diagnostics") def get_diagnostics(self, context, instance_id): """Retrieve diagnostics for the given instance.""" @@ -967,11 +1125,34 @@ class API(base.Base): return instance def associate_floating_ip(self, context, instance_id, address): - """Associate a floating ip with an instance.""" + """Makes calls to network_api to associate_floating_ip. + + :param address: is a string floating ip address + """ instance = self.get(context, instance_id) + + # TODO(tr3buchet): currently network_info doesn't contain floating IPs + # in its info, if this changes, the next few lines will need to + # accomodate the info containing floating as well as fixed ip addresses + fixed_ip_addrs = [] + for info in self.network_api.get_instance_nw_info(context, + instance): + ips = info[1]['ips'] + fixed_ip_addrs.extend([ip_dict['ip'] for ip_dict in ips]) + + # TODO(tr3buchet): this will associate the floating IP with the first + # fixed_ip (lowest id) an instance has. This should be changed to + # support specifying a particular fixed_ip if multiple exist. + if not fixed_ip_addrs: + msg = _("instance |%s| has no fixed_ips. " + "unable to associate floating ip") % instance_id + raise exception.ApiError(msg) + if len(fixed_ip_addrs) > 1: + LOG.warning(_("multiple fixed_ips exist, using the first: %s"), + fixed_ip_addrs[0]) self.network_api.associate_floating_ip(context, floating_ip=address, - fixed_ip=instance['fixed_ip']) + fixed_ip=fixed_ip_addrs[0]) def get_instance_metadata(self, context, instance_id): """Get all metadata associated with an instance.""" diff --git a/nova/compute/instance_types.py b/nova/compute/instance_types.py index 1d246e445..c13a629a9 100644 --- a/nova/compute/instance_types.py +++ b/nova/compute/instance_types.py @@ -112,7 +112,7 @@ def get_instance_type(id): return get_default_instance_type() try: ctxt = context.get_admin_context() - return db.instance_type_get_by_id(ctxt, id) + return db.instance_type_get(ctxt, id) except exception.DBError: raise exception.ApiError(_("Unknown instance type: %s") % id) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 5aed2c677..31627fe3b 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -46,6 +46,7 @@ from eventlet import greenthread from nova import exception from nova import flags +import nova.image from nova import log as logging from nova import manager from nova import network @@ -53,6 +54,7 @@ from nova import rpc from nova import utils from nova import volume from nova.compute import power_state +from nova.notifier import api as notifier from nova.compute.utils import terminate_volumes from nova.virt import driver @@ -75,18 +77,24 @@ flags.DEFINE_integer('live_migration_retry_count', 30, flags.DEFINE_integer("rescue_timeout", 0, "Automatically unrescue an instance after N seconds." " Set to 0 to disable.") -flags.DEFINE_bool('auto_assign_floating_ip', False, - 'Autoassigning floating ip to VM') flags.DEFINE_integer('host_state_interval', 120, 'Interval in seconds for querying the host status') LOG = logging.getLogger('nova.compute.manager') +def publisher_id(host=None): + return notifier.publisher_id("compute", host) + + def checks_instance_lock(function): """Decorator to prevent action against locked instances for non-admins.""" @functools.wraps(function) def decorated_function(self, context, instance_id, *args, **kwargs): + #TODO(anyone): this being called instance_id is forcing a slightly + # confusing convention of pushing instance_uuids + # through an "instance_id" key in the queue args dict when + # casting through the compute API LOG.info(_("check_instance_lock: decorating: |%s|"), function, context=context) LOG.info(_("check_instance_lock: arguments: |%(self)s| |%(context)s|" @@ -129,9 +137,9 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.error(_("Unable to load the virtualization driver: %s") % (e)) sys.exit(1) + self.network_api = network.API() self.network_manager = utils.import_object(FLAGS.network_manager) self.volume_manager = utils.import_object(FLAGS.volume_manager) - self.network_api = network.API() self._last_host_check = 0 super(ComputeManager, self).__init__(service_name="compute", *args, **kwargs) @@ -178,24 +186,10 @@ class ComputeManager(manager.SchedulerDependentManager): FLAGS.console_topic, FLAGS.console_host) - def get_network_topic(self, context, **kwargs): - """Retrieves the network host for a project on this host.""" - # TODO(vish): This method should be memoized. This will make - # the call to get_network_host cheaper, so that - # it can pas messages instead of checking the db - # locally. - if FLAGS.stub_network: - host = FLAGS.network_host - else: - host = self.network_manager.get_network_host(context) - return self.db.queue_get_for(context, - FLAGS.network_topic, - host) - def get_console_pool_info(self, context, console_type): return self.driver.get_console_pool_info(console_type) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def refresh_security_group_rules(self, context, security_group_id, **kwargs): """Tell the virtualization driver to refresh security group rules. @@ -205,7 +199,7 @@ class ComputeManager(manager.SchedulerDependentManager): """ return self.driver.refresh_security_group_rules(security_group_id) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def refresh_security_group_members(self, context, security_group_id, **kwargs): """Tell the virtualization driver to refresh security group members. @@ -215,11 +209,20 @@ class ComputeManager(manager.SchedulerDependentManager): """ return self.driver.refresh_security_group_members(security_group_id) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def refresh_provider_fw_rules(self, context, **_kwargs): """This call passes straight through to the virtualization driver.""" return self.driver.refresh_provider_fw_rules() + def _get_instance_nw_info(self, context, instance): + """Get a list of dictionaries of network data of an instance. + Returns an empty list if stub_network flag is set.""" + network_info = [] + if not FLAGS.stub_network: + network_info = self.network_api.get_instance_nw_info(context, + instance) + return network_info + def _setup_block_device_mapping(self, context, instance_id): """setup volumes for block device mapping""" self.db.instance_set_state(context, @@ -232,6 +235,17 @@ class ComputeManager(manager.SchedulerDependentManager): for bdm in self.db.block_device_mapping_get_all_by_instance( context, instance_id): LOG.debug(_("setting up bdm %s"), bdm) + + if bdm['no_device']: + continue + if bdm['virtual_name']: + # TODO(yamahata): + # block devices for swap and ephemeralN will be + # created by virt driver locally in compute node. + assert (bdm['virtual_name'] == 'swap' or + bdm['virtual_name'].startswith('ephemeral')) + continue + if ((bdm['snapshot_id'] is not None) and (bdm['volume_id'] is None)): # TODO(yamahata): default name and description @@ -264,85 +278,67 @@ class ComputeManager(manager.SchedulerDependentManager): block_device_mapping.append({'device_path': dev_path, 'mount_device': bdm['device_name']}) - elif bdm['virtual_name'] is not None: - # TODO(yamahata): ephemeral/swap device support - LOG.debug(_('block_device_mapping: ' - 'ephemeral device is not supported yet')) - else: - # TODO(yamahata): NoDevice support - assert bdm['no_device'] - LOG.debug(_('block_device_mapping: ' - 'no device is not supported yet')) return block_device_mapping def _run_instance(self, context, instance_id, **kwargs): """Launch a new instance with specified options.""" context = context.elevated() - instance_ref = self.db.instance_get(context, instance_id) - instance_ref.injected_files = kwargs.get('injected_files', []) - instance_ref.admin_pass = kwargs.get('admin_password', None) - if instance_ref['name'] in self.driver.list_instances(): + instance = self.db.instance_get(context, instance_id) + if instance['name'] in self.driver.list_instances(): raise exception.Error(_("Instance has already been created")) LOG.audit(_("instance %s: starting..."), instance_id, context=context) - self.db.instance_update(context, - instance_id, - {'host': self.host, 'launched_on': self.host}) - + updates = {} + updates['host'] = self.host + updates['launched_on'] = self.host + # NOTE(vish): used by virt but not in database + updates['injected_files'] = kwargs.get('injected_files', []) + updates['admin_pass'] = kwargs.get('admin_password', None) + instance = self.db.instance_update(context, + instance_id, + updates) self.db.instance_set_state(context, instance_id, power_state.NOSTATE, 'networking') - is_vpn = instance_ref['image_ref'] == str(FLAGS.vpn_image_id) + is_vpn = instance['image_ref'] == str(FLAGS.vpn_image_id) try: # NOTE(vish): This could be a cast because we don't do anything # with the address currently, but I'm leaving it as # a call to ensure that network setup completes. We # will eventually also need to save the address here. if not FLAGS.stub_network: - address = rpc.call(context, - self.get_network_topic(context), - {"method": "allocate_fixed_ip", - "args": {"instance_id": instance_id, - "vpn": is_vpn}}) - - self.network_manager.setup_compute_network(context, - instance_id) + network_info = self.network_api.allocate_for_instance(context, + instance, vpn=is_vpn) + LOG.debug(_("instance network_info: |%s|"), network_info) + else: + # TODO(tr3buchet) not really sure how this should be handled. + # virt requires network_info to be passed in but stub_network + # is enabled. Setting to [] for now will cause virt to skip + # all vif creation and network injection, maybe this is correct + network_info = [] - block_device_mapping = self._setup_block_device_mapping( - context, - instance_id) + bd_mapping = self._setup_block_device_mapping(context, instance_id) # TODO(vish) check to make sure the availability zone matches self._update_state(context, instance_id, power_state.BUILDING) try: - self.driver.spawn(instance_ref, - block_device_mapping=block_device_mapping) + self.driver.spawn(instance, network_info, bd_mapping) except Exception as ex: # pylint: disable=W0702 msg = _("Instance '%(instance_id)s' failed to spawn. Is " "virtualization enabled in the BIOS? Details: " "%(ex)s") % locals() LOG.exception(msg) - if not FLAGS.stub_network and FLAGS.auto_assign_floating_ip: - public_ip = self.network_api.allocate_floating_ip(context) - - self.db.floating_ip_set_auto_assigned(context, public_ip) - fixed_ip = self.db.fixed_ip_get_by_address(context, address) - floating_ip = self.db.floating_ip_get_by_address(context, - public_ip) - - self.network_api.associate_floating_ip( - context, - floating_ip, - fixed_ip, - affect_auto_assigned=True) - self._update_launched_at(context, instance_id) self._update_state(context, instance_id) + usage_info = utils.usage_from_instance(instance) + notifier.notify('compute.%s' % self.host, + 'compute.instance.create', + notifier.INFO, usage_info) except exception.InstanceNotFound: # FIXME(wwolf): We are just ignoring InstanceNotFound # exceptions here in case the instance was immediately @@ -350,11 +346,11 @@ class ComputeManager(manager.SchedulerDependentManager): # be fixed once we have no-db-messaging pass - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def run_instance(self, context, instance_id, **kwargs): self._run_instance(context, instance_id, **kwargs) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def start_instance(self, context, instance_id): """Starting an instance on this host.""" @@ -365,74 +361,51 @@ class ComputeManager(manager.SchedulerDependentManager): def _shutdown_instance(self, context, instance_id, action_str): """Shutdown an instance on this host.""" context = context.elevated() - instance_ref = self.db.instance_get(context, instance_id) + instance = self.db.instance_get(context, instance_id) LOG.audit(_("%(action_str)s instance %(instance_id)s") % {'action_str': action_str, 'instance_id': instance_id}, context=context) - fixed_ip = instance_ref.get('fixed_ip') - if not FLAGS.stub_network and fixed_ip: - floating_ips = fixed_ip.get('floating_ips') or [] - for floating_ip in floating_ips: - address = floating_ip['address'] - LOG.debug("Disassociating address %s", address, - context=context) - # NOTE(vish): Right now we don't really care if the ip is - # disassociated. We may need to worry about - # checking this later. - self.network_api.disassociate_floating_ip(context, - address, - True) - if (FLAGS.auto_assign_floating_ip - and floating_ip.get('auto_assigned')): - LOG.debug(_("Deallocating floating ip %s"), - floating_ip['address'], - context=context) - self.network_api.release_floating_ip(context, - address, - True) - - address = fixed_ip['address'] - if address: - LOG.debug(_("Deallocating address %s"), address, - context=context) - # NOTE(vish): Currently, nothing needs to be done on the - # network node until release. If this changes, - # we will need to cast here. - self.network_manager.deallocate_fixed_ip(context.elevated(), - address) - - volumes = instance_ref.get('volumes') or [] + network_info = self._get_instance_nw_info(context, instance) + if not FLAGS.stub_network: + self.network_api.deallocate_for_instance(context, instance) + + volumes = instance.get('volumes') or [] for volume in volumes: self._detach_volume(context, instance_id, volume['id'], False) - if (instance_ref['state'] == power_state.SHUTOFF and - instance_ref['state_description'] != 'stopped'): + if (instance['state'] == power_state.SHUTOFF and + instance['state_description'] != 'stopped'): self.db.instance_destroy(context, instance_id) raise exception.Error(_('trying to destroy already destroyed' ' instance: %s') % instance_id) - self.driver.destroy(instance_ref) + self.driver.destroy(instance, network_info) if action_str == 'Terminating': terminate_volumes(self.db, context, instance_id) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def terminate_instance(self, context, instance_id): """Terminate an instance on this host.""" self._shutdown_instance(context, instance_id, 'Terminating') + instance = self.db.instance_get(context.elevated(), instance_id) # TODO(ja): should we keep it in a terminated state for a bit? self.db.instance_destroy(context, instance_id) + usage_info = utils.usage_from_instance(instance) + notifier.notify('compute.%s' % self.host, + 'compute.instance.delete', + notifier.INFO, usage_info) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def stop_instance(self, context, instance_id): """Stopping an instance on this host.""" self._shutdown_instance(context, instance_id, 'Stopping') # instance state will be updated to stopped by _poll_instance_states() - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def rebuild_instance(self, context, instance_id, **kwargs): """Destroy and re-make this instance. @@ -451,17 +424,25 @@ class ComputeManager(manager.SchedulerDependentManager): self._update_state(context, instance_id, power_state.BUILDING) - self.driver.destroy(instance_ref) + network_info = self._get_instance_nw_info(context, instance_ref) + + self.driver.destroy(instance_ref, network_info) image_ref = kwargs.get('image_ref') instance_ref.image_ref = image_ref instance_ref.injected_files = kwargs.get('injected_files', []) - self.driver.spawn(instance_ref) + self.driver.spawn(instance_ref, network_info) self._update_image_ref(context, instance_id, image_ref) self._update_launched_at(context, instance_id) self._update_state(context, instance_id) - - @exception.wrap_exception + usage_info = utils.usage_from_instance(instance_ref, + image_ref=image_ref) + notifier.notify('compute.%s' % self.host, + 'compute.instance.rebuild', + notifier.INFO, + usage_info) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def reboot_instance(self, context, instance_id): """Reboot an instance on this host.""" @@ -482,13 +463,24 @@ class ComputeManager(manager.SchedulerDependentManager): instance_id, power_state.NOSTATE, 'rebooting') - self.network_manager.setup_compute_network(context, instance_id) - self.driver.reboot(instance_ref) + network_info = self._get_instance_nw_info(context, instance_ref) + self.driver.reboot(instance_ref, network_info) self._update_state(context, instance_id) - @exception.wrap_exception - def snapshot_instance(self, context, instance_id, image_id): - """Snapshot an instance on this host.""" + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + def snapshot_instance(self, context, instance_id, image_id, + image_type='snapshot', backup_type=None, + rotation=None): + """Snapshot an instance on this host. + + :param context: security context + :param instance_id: nova.db.sqlalchemy.models.Instance.Id + :param image_id: glance.db.sqlalchemy.models.Image.Id + :param image_type: snapshot | backup + :param backup_type: daily | weekly + :param rotation: int representing how many backups to keep around; + None if rotation shouldn't be used (as in the case of snapshots) + """ context = context.elevated() instance_ref = self.db.instance_get(context, instance_id) @@ -508,7 +500,66 @@ class ComputeManager(manager.SchedulerDependentManager): self.driver.snapshot(instance_ref, image_id) - @exception.wrap_exception + if image_type == 'snapshot': + if rotation: + raise exception.ImageRotationNotAllowed() + elif image_type == 'backup': + if rotation: + instance_uuid = instance_ref['uuid'] + self.rotate_backups(context, instance_uuid, backup_type, + rotation) + else: + raise exception.RotationRequiredForBackup() + else: + raise Exception(_('Image type not recognized %s') % image_type) + + def rotate_backups(self, context, instance_uuid, backup_type, rotation): + """Delete excess backups associated to an instance. + + Instances are allowed a fixed number of backups (the rotation number); + this method deletes the oldest backups that exceed the rotation + threshold. + + :param context: security context + :param instance_uuid: string representing uuid of instance + :param backup_type: daily | weekly + :param rotation: int representing how many backups to keep around; + None if rotation shouldn't be used (as in the case of snapshots) + """ + # NOTE(jk0): Eventually extract this out to the ImageService? + def fetch_images(): + images = [] + marker = None + while True: + batch = image_service.detail(context, filters=filters, + marker=marker, sort_key='created_at', sort_dir='desc') + if not batch: + break + images += batch + marker = batch[-1]['id'] + return images + + image_service = nova.image.get_default_image_service() + filters = {'property-image_type': 'backup', + 'property-backup_type': backup_type, + 'property-instance_uuid': instance_uuid} + + images = fetch_images() + num_images = len(images) + LOG.debug(_("Found %(num_images)d images (rotation: %(rotation)d)" + % locals())) + if num_images > rotation: + # NOTE(sirp): this deletes all backups that exceed the rotation + # limit + excess = len(images) - rotation + LOG.debug(_("Rotating out %d backups" % excess)) + for i in xrange(excess): + image = images.pop() + image_id = image['id'] + LOG.debug(_("Deleting image %d" % image_id)) + image_service.delete(context, image_id) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def set_admin_password(self, context, instance_id, new_pass=None): """Set the root/admin password for an instance on this host. @@ -556,7 +607,7 @@ class ComputeManager(manager.SchedulerDependentManager): time.sleep(1) continue - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def inject_file(self, context, instance_id, path, file_contents): """Write a file to the specified path in an instance on this host.""" @@ -574,7 +625,7 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.audit(msg) self.driver.inject_file(instance_ref, path, file_contents) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def agent_update(self, context, instance_id, url, md5hash): """Update agent running on an instance on this host.""" @@ -592,7 +643,7 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.audit(msg) self.driver.agent_update(instance_ref, url, md5hash) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def rescue_instance(self, context, instance_id): """Rescue an instance on this host.""" @@ -603,13 +654,13 @@ class ComputeManager(manager.SchedulerDependentManager): instance_id, power_state.NOSTATE, 'rescuing') - self.network_manager.setup_compute_network(context, instance_id) _update_state = lambda result: self._update_state_callback( self, context, instance_id, result) - self.driver.rescue(instance_ref, _update_state) + network_info = self._get_instance_nw_info(context, instance_ref) + self.driver.rescue(instance_ref, _update_state, network_info) self._update_state(context, instance_id) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def unrescue_instance(self, context, instance_id): """Rescue an instance on this host.""" @@ -622,7 +673,8 @@ class ComputeManager(manager.SchedulerDependentManager): 'unrescuing') _update_state = lambda result: self._update_state_callback( self, context, instance_id, result) - self.driver.unrescue(instance_ref, _update_state) + network_info = self._get_instance_nw_info(context, instance_ref) + self.driver.unrescue(instance_ref, _update_state, network_info) self._update_state(context, instance_id) @staticmethod @@ -630,15 +682,23 @@ class ComputeManager(manager.SchedulerDependentManager): """Update instance state when async task completes.""" self._update_state(context, instance_id) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def confirm_resize(self, context, instance_id, migration_id): """Destroys the source instance.""" - context = context.elevated() - instance_ref = self.db.instance_get(context, instance_id) - self.driver.destroy(instance_ref) - - @exception.wrap_exception + migration_ref = self.db.migration_get(context, migration_id) + instance_ref = self.db.instance_get_by_uuid(context, + migration_ref.instance_uuid) + + network_info = self._get_instance_nw_info(context, instance_ref) + self.driver.destroy(instance_ref, network_info) + usage_info = utils.usage_from_instance(instance_ref) + notifier.notify('compute.%s' % self.host, + 'compute.instance.resize.confirm', + notifier.INFO, + usage_info) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def revert_resize(self, context, instance_id, migration_id): """Destroys the new instance on the destination machine. @@ -647,20 +707,20 @@ class ComputeManager(manager.SchedulerDependentManager): source machine. """ - instance_ref = self.db.instance_get(context, instance_id) migration_ref = self.db.migration_get(context, migration_id) + instance_ref = self.db.instance_get_by_uuid(context, + migration_ref.instance_uuid) - self.driver.destroy(instance_ref) + network_info = self._get_instance_nw_info(context, instance_ref) + self.driver.destroy(instance_ref, network_info) topic = self.db.queue_get_for(context, FLAGS.compute_topic, instance_ref['host']) rpc.cast(context, topic, {'method': 'finish_revert_resize', - 'args': { - 'migration_id': migration_ref['id'], - 'instance_id': instance_id, }, + 'args': {'migration_id': migration_ref['id']}, }) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def finish_revert_resize(self, context, instance_id, migration_id): """Finishes the second half of reverting a resize. @@ -669,23 +729,31 @@ class ComputeManager(manager.SchedulerDependentManager): in the database. """ - instance_ref = self.db.instance_get(context, instance_id) migration_ref = self.db.migration_get(context, migration_id) + instance_ref = self.db.instance_get_by_uuid(context, + migration_ref.instance_uuid) + instance_type = self.db.instance_type_get_by_flavor_id(context, migration_ref['old_flavor_id']) # Just roll back the record. There's no need to resize down since # the 'old' VM already has the preferred attributes - self.db.instance_update(context, instance_id, + self.db.instance_update(context, instance_ref['uuid'], dict(memory_mb=instance_type['memory_mb'], vcpus=instance_type['vcpus'], - local_gb=instance_type['local_gb'])) + local_gb=instance_type['local_gb'], + instance_type_id=instance_type['id'])) self.driver.revert_resize(instance_ref) self.db.migration_update(context, migration_id, {'status': 'reverted'}) + usage_info = utils.usage_from_instance(instance_ref) + notifier.notify('compute.%s' % self.host, + 'compute.instance.resize.revert', + notifier.INFO, + usage_info) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def prep_resize(self, context, instance_id, flavor_id): """Initiates the process of moving a running instance to another host. @@ -694,39 +762,55 @@ class ComputeManager(manager.SchedulerDependentManager): """ context = context.elevated() - instance_ref = self.db.instance_get(context, instance_id) + + # Because of checks_instance_lock, this must currently be called + # instance_id. However, the compute API is always passing the UUID + # of the instance down + instance_ref = self.db.instance_get_by_uuid(context, instance_id) + if instance_ref['host'] == FLAGS.host: raise exception.Error(_( 'Migration error: destination same as source!')) - instance_type = self.db.instance_type_get_by_flavor_id(context, + old_instance_type = self.db.instance_type_get(context, + instance_ref['instance_type_id']) + new_instance_type = self.db.instance_type_get_by_flavor_id(context, flavor_id) + migration_ref = self.db.migration_create(context, - {'instance_id': instance_id, + {'instance_uuid': instance_ref['uuid'], 'source_compute': instance_ref['host'], 'dest_compute': FLAGS.host, 'dest_host': self.driver.get_host_ip_addr(), - 'old_flavor_id': instance_type['flavorid'], + 'old_flavor_id': old_instance_type['flavorid'], 'new_flavor_id': flavor_id, 'status': 'pre-migrating'}) - LOG.audit(_('instance %s: migrating to '), instance_id, + LOG.audit(_('instance %s: migrating'), instance_ref['uuid'], context=context) topic = self.db.queue_get_for(context, FLAGS.compute_topic, instance_ref['host']) rpc.cast(context, topic, {'method': 'resize_instance', - 'args': { - 'migration_id': migration_ref['id'], - 'instance_id': instance_id, }, - }) - - @exception.wrap_exception + 'args': {'instance_id': instance_ref['uuid'], + 'migration_id': migration_ref['id']}}) + + usage_info = utils.usage_from_instance(instance_ref, + new_instance_type=new_instance_type['name'], + new_instance_type_id=new_instance_type['id']) + notifier.notify('compute.%s' % self.host, + 'compute.instance.resize.prep', + notifier.INFO, + usage_info) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def resize_instance(self, context, instance_id, migration_id): """Starts the migration of a running instance to another host.""" migration_ref = self.db.migration_get(context, migration_id) - instance_ref = self.db.instance_get(context, instance_id) + instance_ref = self.db.instance_get_by_uuid(context, + migration_ref.instance_uuid) + self.db.migration_update(context, migration_id, {'status': 'migrating'}) @@ -742,12 +826,13 @@ class ComputeManager(manager.SchedulerDependentManager): topic = self.db.queue_get_for(context, FLAGS.compute_topic, migration_ref['dest_compute']) + params = {'migration_id': migration_id, + 'disk_info': disk_info, + 'instance_id': instance_ref['uuid']} rpc.cast(context, topic, {'method': 'finish_resize', - 'args': {'migration_id': migration_id, - 'instance_id': instance_id, - 'disk_info': disk_info}}) + 'args': params}) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def finish_resize(self, context, instance_id, migration_id, disk_info): """Completes the migration process. @@ -757,27 +842,49 @@ class ComputeManager(manager.SchedulerDependentManager): """ migration_ref = self.db.migration_get(context, migration_id) - instance_ref = self.db.instance_get(context, - migration_ref['instance_id']) - # TODO(mdietz): apply the rest of the instance_type attributes going - # after they're supported + instance_ref = self.db.instance_get_by_uuid(context, + migration_ref.instance_uuid) instance_type = self.db.instance_type_get_by_flavor_id(context, migration_ref['new_flavor_id']) - self.db.instance_update(context, instance_id, + self.db.instance_update(context, instance_ref.uuid, dict(instance_type_id=instance_type['id'], memory_mb=instance_type['memory_mb'], vcpus=instance_type['vcpus'], local_gb=instance_type['local_gb'])) - # reload the updated instance ref - # FIXME(mdietz): is there reload functionality? - instance_ref = self.db.instance_get(context, instance_id) - self.driver.finish_resize(instance_ref, disk_info) + instance_ref = self.db.instance_get_by_uuid(context, + instance_ref.uuid) + network_info = self._get_instance_nw_info(context, instance_ref) + self.driver.finish_resize(instance_ref, disk_info, network_info) self.db.migration_update(context, migration_id, {'status': 'finished', }) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + @checks_instance_lock + def add_fixed_ip_to_instance(self, context, instance_id, network_id): + """Calls network_api to add new fixed_ip to instance + then injects the new network info and resets instance networking. + + """ + self.network_api.add_fixed_ip_to_instance(context, instance_id, + network_id) + self.inject_network_info(context, instance_id) + self.reset_network(context, instance_id) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + @checks_instance_lock + def remove_fixed_ip_from_instance(self, context, instance_id, address): + """Calls network_api to remove existing fixed_ip from instance + by injecting the altered network info and resetting + instance networking. + """ + self.network_api.remove_fixed_ip_from_instance(context, instance_id, + address) + self.inject_network_info(context, instance_id) + self.reset_network(context, instance_id) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def pause_instance(self, context, instance_id): """Pause an instance on this host.""" @@ -794,7 +901,7 @@ class ComputeManager(manager.SchedulerDependentManager): instance_id, result)) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def unpause_instance(self, context, instance_id): """Unpause a paused instance on this host.""" @@ -811,7 +918,13 @@ class ComputeManager(manager.SchedulerDependentManager): instance_id, result)) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + def set_host_enabled(self, context, instance_id=None, host=None, + enabled=None): + """Sets the specified host's ability to accept new instances.""" + return self.driver.set_host_enabled(host, enabled) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def get_diagnostics(self, context, instance_id): """Retrieve diagnostics for an instance on this host.""" instance_ref = self.db.instance_get(context, instance_id) @@ -820,7 +933,7 @@ class ComputeManager(manager.SchedulerDependentManager): context=context) return self.driver.get_diagnostics(instance_ref) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def suspend_instance(self, context, instance_id): """Suspend the given instance.""" @@ -836,7 +949,7 @@ class ComputeManager(manager.SchedulerDependentManager): instance_id, result)) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def resume_instance(self, context, instance_id): """Resume the given suspended instance.""" @@ -852,7 +965,7 @@ class ComputeManager(manager.SchedulerDependentManager): instance_id, result)) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def lock_instance(self, context, instance_id): """Lock the given instance.""" context = context.elevated() @@ -860,7 +973,7 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.debug(_('instance %s: locking'), instance_id, context=context) self.db.instance_update(context, instance_id, {'locked': True}) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def unlock_instance(self, context, instance_id): """Unlock the given instance.""" context = context.elevated() @@ -868,34 +981,39 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.debug(_('instance %s: unlocking'), instance_id, context=context) self.db.instance_update(context, instance_id, {'locked': False}) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def get_lock(self, context, instance_id): """Return the boolean state of the given instance's lock.""" context = context.elevated() LOG.debug(_('instance %s: getting locked state'), instance_id, context=context) - instance_ref = self.db.instance_get(context, instance_id) + if utils.is_uuid_like(instance_id): + uuid = instance_id + instance_ref = self.db.instance_get_by_uuid(context, uuid) + else: + instance_ref = self.db.instance_get(context, instance_id) return instance_ref['locked'] @checks_instance_lock def reset_network(self, context, instance_id): """Reset networking on the given instance.""" - context = context.elevated() - instance_ref = self.db.instance_get(context, instance_id) + instance = self.db.instance_get(context, instance_id) LOG.debug(_('instance %s: reset network'), instance_id, context=context) - self.driver.reset_network(instance_ref) + self.driver.reset_network(instance) @checks_instance_lock def inject_network_info(self, context, instance_id): """Inject network info for the given instance.""" - context = context.elevated() - instance_ref = self.db.instance_get(context, instance_id) LOG.debug(_('instance %s: inject network info'), instance_id, context=context) - self.driver.inject_network_info(instance_ref) + instance = self.db.instance_get(context, instance_id) + network_info = self._get_instance_nw_info(context, instance) + LOG.debug(_("network_info to inject: |%s|"), network_info) + + self.driver.inject_network_info(instance, network_info) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def get_console_output(self, context, instance_id): """Send the console output for the given instance.""" context = context.elevated() @@ -905,7 +1023,7 @@ class ComputeManager(manager.SchedulerDependentManager): output = self.driver.get_console_output(instance_ref) return output.decode('utf-8', 'replace').encode('ascii', 'replace') - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def get_ajax_console(self, context, instance_id): """Return connection information for an ajax console.""" context = context.elevated() @@ -913,7 +1031,7 @@ class ComputeManager(manager.SchedulerDependentManager): instance_ref = self.db.instance_get(context, instance_id) return self.driver.get_ajax_console(instance_ref) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def get_vnc_console(self, context, instance_id): """Return connection information for a vnc console.""" context = context.elevated() @@ -976,7 +1094,7 @@ class ComputeManager(manager.SchedulerDependentManager): return True - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock def _detach_volume(self, context, instance_id, volume_id, destroy_bdm): """Detach a volume from an instance.""" @@ -1011,7 +1129,7 @@ class ComputeManager(manager.SchedulerDependentManager): """ self.volume_manager.remove_compute_volume(context, volume_id) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def compare_cpu(self, context, cpu_info): """Checks that the host cpu is compatible with a cpu given by xml. @@ -1022,7 +1140,7 @@ class ComputeManager(manager.SchedulerDependentManager): """ return self.driver.compare_cpu(cpu_info) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def create_shared_storage_test_file(self, context): """Makes tmpfile under FLAGS.instance_path. @@ -1042,7 +1160,7 @@ class ComputeManager(manager.SchedulerDependentManager): os.close(fd) return os.path.basename(tmp_file) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def check_shared_storage_test_file(self, context, filename): """Confirms existence of the tmpfile under FLAGS.instances_path. @@ -1054,7 +1172,7 @@ class ComputeManager(manager.SchedulerDependentManager): if not os.path.exists(tmp_file): raise exception.FileNotFound(file_path=tmp_file) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def cleanup_shared_storage_test_file(self, context, filename): """Removes existence of the tmpfile under FLAGS.instances_path. @@ -1065,7 +1183,7 @@ class ComputeManager(manager.SchedulerDependentManager): tmp_file = os.path.join(FLAGS.instances_path, filename) os.remove(tmp_file) - @exception.wrap_exception + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def update_available_resource(self, context): """See comments update_resource_info. @@ -1087,16 +1205,16 @@ class ComputeManager(manager.SchedulerDependentManager): # Getting instance info instance_ref = self.db.instance_get(context, instance_id) - ec2_id = instance_ref['hostname'] + hostname = instance_ref['hostname'] # Getting fixed ips - fixed_ip = self.db.instance_get_fixed_address(context, instance_id) - if not fixed_ip: - raise exception.NoFixedIpsFoundForInstance(instance_id=instance_id) + fixed_ips = self.db.instance_get_fixed_addresses(context, instance_id) + if not fixed_ips: + raise exception.FixedIpNotFoundForInstance(instance_id=instance_id) # If any volume is mounted, prepare here. if not instance_ref['volumes']: - LOG.info(_("%s has no volume."), ec2_id) + LOG.info(_("%s has no volume."), hostname) else: for v in instance_ref['volumes']: self.volume_manager.setup_compute_volume(context, v['id']) @@ -1108,18 +1226,18 @@ class ComputeManager(manager.SchedulerDependentManager): # # Retry operation is necessary because continuously request comes, # concorrent request occurs to iptables, then it complains. + network_info = self._get_instance_nw_info(context, instance_ref) max_retry = FLAGS.live_migration_retry_count for cnt in range(max_retry): try: - self.network_manager.setup_compute_network(context, - instance_id) + self.driver.plug_vifs(instance_ref, network_info) break except exception.ProcessExecutionError: if cnt == max_retry - 1: raise else: - LOG.warn(_("setup_compute_network() failed %(cnt)d." - "Retry up to %(max_retry)d for %(ec2_id)s.") + LOG.warn(_("plug_vifs() failed %(cnt)d." + "Retry up to %(max_retry)d for %(hostname)s.") % locals()) time.sleep(1) @@ -1196,8 +1314,9 @@ class ComputeManager(manager.SchedulerDependentManager): # Releasing vlan. # (not necessary in current implementation?) + network_info = self._get_instance_nw_info(ctxt, instance_ref) # Releasing security group ingress rule. - self.driver.unfilter_instance(instance_ref) + self.driver.unfilter_instance(instance_ref, network_info) # Database updating. i_name = instance_ref.name @@ -1216,9 +1335,10 @@ class ComputeManager(manager.SchedulerDependentManager): {'host': dest}) except exception.NotFound: LOG.info(_('No floating_ip is found for %s.'), i_name) - except: - LOG.error(_("Live migration: Unexpected error:" - "%s cannot inherit floating ip..") % i_name) + except Exception, e: + LOG.error(_("Live migration: Unexpected error: " + "%(i_name)s cannot inherit floating " + "ip.\n%(e)s") % (locals())) # Restore instance/volume state self.recover_live_migration(ctxt, instance_ref, dest) diff --git a/nova/console/manager.py b/nova/console/manager.py index e0db21666..2c823b763 100644 --- a/nova/console/manager.py +++ b/nova/console/manager.py @@ -56,7 +56,7 @@ class ConsoleProxyManager(manager.Manager): def init_host(self): self.driver.init_host() - @exception.wrap_exception + @exception.wrap_exception() def add_console(self, context, instance_id, password=None, port=None, **kwargs): instance = self.db.instance_get(context, instance_id) @@ -83,7 +83,7 @@ class ConsoleProxyManager(manager.Manager): self.driver.setup_console(context, console) return console['id'] - @exception.wrap_exception + @exception.wrap_exception() def remove_console(self, context, console_id, **_kwargs): try: console = self.db.console_get(context, console_id) diff --git a/nova/console/vmrc_manager.py b/nova/console/vmrc_manager.py index acecc1075..0b5ce4a49 100644 --- a/nova/console/vmrc_manager.py +++ b/nova/console/vmrc_manager.py @@ -77,7 +77,7 @@ class ConsoleVMRCManager(manager.Manager): self.driver.setup_console(context, console) return console - @exception.wrap_exception + @exception.wrap_exception() def add_console(self, context, instance_id, password=None, port=None, **kwargs): """Adds a console for the instance. @@ -107,7 +107,7 @@ class ConsoleVMRCManager(manager.Manager): instance) return console['id'] - @exception.wrap_exception + @exception.wrap_exception() def remove_console(self, context, console_id, **_kwargs): """Removes a console entry.""" try: diff --git a/nova/db/api.py b/nova/db/api.py index eea9a349e..47308bdba 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -55,11 +55,6 @@ IMPL = utils.LazyPluggable(FLAGS['db_backend'], sqlalchemy='nova.db.sqlalchemy.api') -class NoMoreAddresses(exception.Error): - """No more available addresses.""" - pass - - class NoMoreBlades(exception.Error): """No more available blades.""" pass @@ -223,17 +218,17 @@ def certificate_update(context, certificate_id, values): ################### -def floating_ip_get(context, floating_ip_id): - return IMPL.floating_ip_get(context, floating_ip_id) +def floating_ip_get(context, id): + return IMPL.floating_ip_get(context, id) -def floating_ip_allocate_address(context, host, project_id): +def floating_ip_allocate_address(context, project_id): """Allocate free floating ip and return the address. Raises if one is not available. """ - return IMPL.floating_ip_allocate_address(context, host, project_id) + return IMPL.floating_ip_allocate_address(context, project_id) def floating_ip_create(context, values): @@ -292,11 +287,6 @@ def floating_ip_get_by_address(context, address): return IMPL.floating_ip_get_by_address(context, address) -def floating_ip_get_by_ip(context, ip): - """Get a floating ip by floating address.""" - return IMPL.floating_ip_get_by_ip(context, ip) - - def floating_ip_update(context, address, values): """Update a floating ip by address or raise if it doesn't exist.""" return IMPL.floating_ip_update(context, address, values) @@ -324,11 +314,12 @@ def migration_get(context, migration_id): return IMPL.migration_get(context, migration_id) -def migration_get_by_instance_and_status(context, instance_id, status): - """Finds a migration by the instance id its migrating.""" - return IMPL.migration_get_by_instance_and_status(context, instance_id, +def migration_get_by_instance_and_status(context, instance_uuid, status): + """Finds a migration by the instance uuid its migrating.""" + return IMPL.migration_get_by_instance_and_status(context, instance_uuid, status) + #################### @@ -341,13 +332,14 @@ def fixed_ip_associate(context, address, instance_id): return IMPL.fixed_ip_associate(context, address, instance_id) -def fixed_ip_associate_pool(context, network_id, instance_id): - """Find free ip in network and associate it to instance. +def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None): + """Find free ip in network and associate it to instance or host. Raises if one is not available. """ - return IMPL.fixed_ip_associate_pool(context, network_id, instance_id) + return IMPL.fixed_ip_associate_pool(context, network_id, + instance_id, host) def fixed_ip_create(context, values): @@ -370,9 +362,9 @@ def fixed_ip_get_all(context): return IMPL.fixed_ip_get_all(context) -def fixed_ip_get_all_by_host(context, host): - """Get all defined fixed ips used by a host.""" - return IMPL.fixed_ip_get_all_by_host(context, host) +def fixed_ip_get_all_by_instance_host(context, host): + """Get all allocated fixed ips filtered by instance host.""" + return IMPL.fixed_ip_get_all_instance_by_host(context, host) def fixed_ip_get_by_address(context, address): @@ -380,9 +372,19 @@ def fixed_ip_get_by_address(context, address): return IMPL.fixed_ip_get_by_address(context, address) -def fixed_ip_get_all_by_instance(context, instance_id): +def fixed_ip_get_by_instance(context, instance_id): """Get fixed ips by instance or raise if none exist.""" - return IMPL.fixed_ip_get_all_by_instance(context, instance_id) + return IMPL.fixed_ip_get_by_instance(context, instance_id) + + +def fixed_ip_get_by_network_host(context, network_id, host): + """Get fixed ip for a host in a network.""" + return IMPL.fixed_ip_get_by_network_host(context, network_id, host) + + +def fixed_ip_get_by_virtual_interface(context, vif_id): + """Get fixed ips by virtual interface or raise if none exist.""" + return IMPL.fixed_ip_get_by_virtual_interface(context, vif_id) def fixed_ip_get_instance(context, address): @@ -407,6 +409,62 @@ def fixed_ip_update(context, address, values): #################### +def virtual_interface_create(context, values): + """Create a virtual interface record in the database.""" + return IMPL.virtual_interface_create(context, values) + + +def virtual_interface_update(context, vif_id, values): + """Update a virtual interface record in the database.""" + return IMPL.virtual_interface_update(context, vif_id, values) + + +def virtual_interface_get(context, vif_id): + """Gets a virtual interface from the table,""" + return IMPL.virtual_interface_get(context, vif_id) + + +def virtual_interface_get_by_address(context, address): + """Gets a virtual interface from the table filtering on address.""" + return IMPL.virtual_interface_get_by_address(context, address) + + +def virtual_interface_get_by_fixed_ip(context, fixed_ip_id): + """Gets the virtual interface fixed_ip is associated with.""" + return IMPL.virtual_interface_get_by_fixed_ip(context, fixed_ip_id) + + +def virtual_interface_get_by_instance(context, instance_id): + """Gets all virtual_interfaces for instance.""" + return IMPL.virtual_interface_get_by_instance(context, instance_id) + + +def virtual_interface_get_by_instance_and_network(context, instance_id, + network_id): + """Gets all virtual interfaces for instance.""" + return IMPL.virtual_interface_get_by_instance_and_network(context, + instance_id, + network_id) + + +def virtual_interface_get_by_network(context, network_id): + """Gets all virtual interfaces on network.""" + return IMPL.virtual_interface_get_by_network(context, network_id) + + +def virtual_interface_delete(context, vif_id): + """Delete virtual interface record from the database.""" + return IMPL.virtual_interface_delete(context, vif_id) + + +def virtual_interface_delete_by_instance(context, instance_id): + """Delete virtual interface records associated with instance.""" + return IMPL.virtual_interface_delete_by_instance(context, instance_id) + + +#################### + + def instance_create(context, values): """Create an instance from the values dictionary.""" return IMPL.instance_create(context, values) @@ -442,6 +500,11 @@ def instance_get_all(context): return IMPL.instance_get_all(context) +def instance_get_active_by_window(context, begin, end=None): + """Get instances active during a certain time window.""" + return IMPL.instance_get_active_by_window(context, begin, end) + + def instance_get_all_by_user(context, user_id): """Get all instances.""" return IMPL.instance_get_all_by_user(context, user_id) @@ -462,13 +525,13 @@ def instance_get_all_by_reservation(context, reservation_id): return IMPL.instance_get_all_by_reservation(context, reservation_id) -def instance_get_fixed_address(context, instance_id): +def instance_get_fixed_addresses(context, instance_id): """Get the fixed ip address of an instance.""" - return IMPL.instance_get_fixed_address(context, instance_id) + return IMPL.instance_get_fixed_addresses(context, instance_id) -def instance_get_fixed_address_v6(context, instance_id): - return IMPL.instance_get_fixed_address_v6(context, instance_id) +def instance_get_fixed_addresses_v6(context, instance_id): + return IMPL.instance_get_fixed_addresses_v6(context, instance_id) def instance_get_floating_address(context, instance_id): @@ -563,9 +626,9 @@ def key_pair_get_all_by_user(context, user_id): #################### -def network_associate(context, project_id): +def network_associate(context, project_id, force=False): """Associate a free network to a project.""" - return IMPL.network_associate(context, project_id) + return IMPL.network_associate(context, project_id, force) def network_count(context): @@ -658,6 +721,11 @@ def network_get_all_by_instance(context, instance_id): return IMPL.network_get_all_by_instance(context, instance_id) +def network_get_all_by_host(context, host): + """All networks for which the given host is the network host.""" + return IMPL.network_get_all_by_host(context, host) + + def network_get_index(context, network_id): """Get non-conflicting index for network.""" return IMPL.network_get_index(context, network_id) @@ -690,23 +758,6 @@ def network_update(context, network_id, values): ################### -def project_get_network(context, project_id, associate=True): - """Return the network associated with the project. - - If associate is true, it will attempt to associate a new - network if one is not found, otherwise it returns None. - - """ - return IMPL.project_get_network(context, project_id, associate) - - -def project_get_network_v6(context, project_id): - return IMPL.project_get_network_v6(context, project_id) - - -################### - - def queue_get_for(context, topic, physical_node_id): """Return a channel to send a message to a node with a topic.""" return IMPL.queue_get_for(context, topic, physical_node_id) @@ -944,10 +995,16 @@ def block_device_mapping_create(context, values): def block_device_mapping_update(context, bdm_id, values): - """Create an entry of block device mapping""" + """Update an entry of block device mapping""" return IMPL.block_device_mapping_update(context, bdm_id, values) +def block_device_mapping_update_or_create(context, values): + """Update an entry of block device mapping. + If not existed, create a new entry""" + return IMPL.block_device_mapping_update_or_create(context, values) + + def block_device_mapping_get_all_by_instance(context, instance_id): """Get all block device mapping belonging to a instance""" return IMPL.block_device_mapping_get_all_by_instance(context, instance_id) @@ -1130,6 +1187,9 @@ def user_update(context, user_id, values): return IMPL.user_update(context, user_id, values) +################### + + def project_get(context, id): """Get project by id.""" return IMPL.project_get(context, id) @@ -1170,15 +1230,21 @@ def project_delete(context, project_id): return IMPL.project_delete(context, project_id) -################### +def project_get_networks(context, project_id, associate=True): + """Return the network associated with the project. + If associate is true, it will attempt to associate a new + network if one is not found, otherwise it returns None. + + """ + return IMPL.project_get_networks(context, project_id, associate) -def host_get_networks(context, host): - """All networks for which the given host is the network host.""" - return IMPL.host_get_networks(context, host) +def project_get_networks_v6(context, project_id): + return IMPL.project_get_networks_v6(context, project_id) -################## + +################### def console_pool_create(context, values): @@ -1245,9 +1311,9 @@ def instance_type_get_all(context, inactive=False): return IMPL.instance_type_get_all(context, inactive) -def instance_type_get_by_id(context, id): +def instance_type_get(context, id): """Get instance type by id.""" - return IMPL.instance_type_get_by_id(context, id) + return IMPL.instance_type_get(context, id) def instance_type_get_by_name(context, name): @@ -1284,7 +1350,7 @@ def zone_create(context, values): def zone_update(context, zone_id, values): """Update a child Zone entry.""" - return IMPL.zone_update(context, values) + return IMPL.zone_update(context, zone_id, values) def zone_delete(context, zone_id): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 06bb1501f..ba03cabbc 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -18,7 +18,6 @@ """ Implementation of SQLAlchemy backend. """ -import traceback import warnings from nova import db @@ -26,17 +25,18 @@ from nova import exception from nova import flags from nova import ipv6 from nova import utils +from nova import log as logging from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload_all -from sqlalchemy.sql import exists from sqlalchemy.sql import func from sqlalchemy.sql.expression import literal_column FLAGS = flags.FLAGS +LOG = logging.getLogger("nova.db.sqlalchemy") def is_admin_context(context): @@ -116,8 +116,23 @@ def require_context(f): return wrapper +def require_instance_exists(f): + """Decorator to require the specified instance to exist. + + Requres the wrapped function to use context and instance_id as + their first two arguments. + """ + + def wrapper(context, instance_id, *args, **kwargs): + db.api.instance_get(context, instance_id) + return f(context, instance_id, *args, **kwargs) + wrapper.__name__ = f.__name__ + return wrapper + + ################### + @require_admin_context def service_destroy(context, service_id): session = get_session() @@ -428,6 +443,8 @@ def certificate_update(context, certificate_id, values): ################### + + @require_context def floating_ip_get(context, id): session = get_session() @@ -448,18 +465,17 @@ def floating_ip_get(context, id): filter_by(deleted=False).\ first() if not result: - raise exception.FloatingIpNotFoundForFixedAddress() + raise exception.FloatingIpNotFound(id=id) return result @require_context -def floating_ip_allocate_address(context, host, project_id): +def floating_ip_allocate_address(context, project_id): authorize_project_context(context, project_id) session = get_session() with session.begin(): floating_ip_ref = session.query(models.FloatingIp).\ - filter_by(host=host).\ filter_by(fixed_ip_id=None).\ filter_by(project_id=None).\ filter_by(deleted=False).\ @@ -468,7 +484,7 @@ def floating_ip_allocate_address(context, host, project_id): # NOTE(vish): if with_lockmode isn't supported, as in sqlite, # then this has concurrency issues if not floating_ip_ref: - raise db.NoMoreAddresses() + raise exception.NoMoreFloatingIps() floating_ip_ref['project_id'] = project_id session.add(floating_ip_ref) return floating_ip_ref['address'] @@ -486,6 +502,7 @@ def floating_ip_create(context, values): def floating_ip_count_by_project(context, project_id): authorize_project_context(context, project_id) session = get_session() + # TODO(tr3buchet): why leave auto_assigned floating IPs out? return session.query(models.FloatingIp).\ filter_by(project_id=project_id).\ filter_by(auto_assigned=False).\ @@ -517,6 +534,7 @@ def floating_ip_deallocate(context, address): address, session=session) floating_ip_ref['project_id'] = None + floating_ip_ref['host'] = None floating_ip_ref['auto_assigned'] = False floating_ip_ref.save(session=session) @@ -565,32 +583,42 @@ def floating_ip_set_auto_assigned(context, address): @require_admin_context def floating_ip_get_all(context): session = get_session() - return session.query(models.FloatingIp).\ - options(joinedload_all('fixed_ip.instance')).\ - filter_by(deleted=False).\ - all() + floating_ip_refs = session.query(models.FloatingIp).\ + options(joinedload_all('fixed_ip.instance')).\ + filter_by(deleted=False).\ + all() + if not floating_ip_refs: + raise exception.NoFloatingIpsDefined() + return floating_ip_refs @require_admin_context def floating_ip_get_all_by_host(context, host): session = get_session() - return session.query(models.FloatingIp).\ - options(joinedload_all('fixed_ip.instance')).\ - filter_by(host=host).\ - filter_by(deleted=False).\ - all() + floating_ip_refs = session.query(models.FloatingIp).\ + options(joinedload_all('fixed_ip.instance')).\ + filter_by(host=host).\ + filter_by(deleted=False).\ + all() + if not floating_ip_refs: + raise exception.FloatingIpNotFoundForHost(host=host) + return floating_ip_refs @require_context def floating_ip_get_all_by_project(context, project_id): authorize_project_context(context, project_id) session = get_session() - return session.query(models.FloatingIp).\ - options(joinedload_all('fixed_ip.instance')).\ - filter_by(project_id=project_id).\ - filter_by(auto_assigned=False).\ - filter_by(deleted=False).\ - all() + # TODO(tr3buchet): why do we not want auto_assigned floating IPs here? + floating_ip_refs = session.query(models.FloatingIp).\ + options(joinedload_all('fixed_ip.instance')).\ + filter_by(project_id=project_id).\ + filter_by(auto_assigned=False).\ + filter_by(deleted=False).\ + all() + if not floating_ip_refs: + raise exception.FloatingIpNotFoundForProject(project_id=project_id) + return floating_ip_refs @require_context @@ -600,29 +628,12 @@ def floating_ip_get_by_address(context, address, session=None): session = get_session() result = session.query(models.FloatingIp).\ - options(joinedload_all('fixed_ip.network')).\ + options(joinedload_all('fixed_ip.network')).\ filter_by(address=address).\ filter_by(deleted=can_read_deleted(context)).\ first() if not result: - raise exception.FloatingIpNotFoundForFixedAddress(fixed_ip=address) - - return result - - -@require_context -def floating_ip_get_by_ip(context, ip, session=None): - if not session: - session = get_session() - - result = session.query(models.FloatingIp).\ - filter_by(address=ip).\ - filter_by(deleted=can_read_deleted(context)).\ - first() - - if not result: - raise exception.FloatingIpNotFound(floating_ip=ip) - + raise exception.FloatingIpNotFoundForAddress(address=address) return result @@ -653,13 +664,13 @@ def fixed_ip_associate(context, address, instance_id): # NOTE(vish): if with_lockmode isn't supported, as in sqlite, # then this has concurrency issues if not fixed_ip_ref: - raise db.NoMoreAddresses() + raise exception.NoMoreFixedIps() fixed_ip_ref.instance = instance session.add(fixed_ip_ref) @require_admin_context -def fixed_ip_associate_pool(context, network_id, instance_id): +def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None): session = get_session() with session.begin(): network_or_none = or_(models.FixedIp.network_id == network_id, @@ -669,19 +680,23 @@ def fixed_ip_associate_pool(context, network_id, instance_id): filter_by(reserved=False).\ filter_by(deleted=False).\ filter_by(instance=None).\ + filter_by(host=None).\ with_lockmode('update').\ first() # NOTE(vish): if with_lockmode isn't supported, as in sqlite, # then this has concurrency issues if not fixed_ip_ref: - raise db.NoMoreAddresses() + raise exception.NoMoreFixedIps() if not fixed_ip_ref.network: fixed_ip_ref.network = network_get(context, network_id, session=session) - fixed_ip_ref.instance = instance_get(context, - instance_id, - session=session) + if instance_id: + fixed_ip_ref.instance = instance_get(context, + instance_id, + session=session) + if host: + fixed_ip_ref.host = host session.add(fixed_ip_ref) return fixed_ip_ref['address'] @@ -715,9 +730,9 @@ def fixed_ip_disassociate_all_by_timeout(_context, host, time): filter(models.FixedIp.network_id.in_(inner_q)).\ filter(models.FixedIp.updated_at < time).\ filter(models.FixedIp.instance_id != None).\ - filter_by(allocated=0).\ + filter_by(allocated=False).\ update({'instance_id': None, - 'leased': 0, + 'leased': False, 'updated_at': utils.utcnow()}, synchronize_session='fetch') return result @@ -727,25 +742,28 @@ def fixed_ip_disassociate_all_by_timeout(_context, host, time): def fixed_ip_get_all(context, session=None): if not session: session = get_session() - result = session.query(models.FixedIp).all() + result = session.query(models.FixedIp).\ + options(joinedload('floating_ips')).\ + all() if not result: - raise exception.NoFloatingIpsDefined() + raise exception.NoFixedIpsDefined() return result @require_admin_context -def fixed_ip_get_all_by_host(context, host=None): +def fixed_ip_get_all_by_instance_host(context, host=None): session = get_session() result = session.query(models.FixedIp).\ - join(models.FixedIp.instance).\ - filter_by(state=1).\ - filter_by(host=host).\ - all() + options(joinedload('floating_ips')).\ + join(models.FixedIp.instance).\ + filter_by(state=1).\ + filter_by(host=host).\ + all() if not result: - raise exception.NoFloatingIpsDefinedForHost(host=host) + raise exception.FixedIpNotFoundForHost(host=host) return result @@ -757,11 +775,12 @@ def fixed_ip_get_by_address(context, address, session=None): result = session.query(models.FixedIp).\ filter_by(address=address).\ filter_by(deleted=can_read_deleted(context)).\ + options(joinedload('floating_ips')).\ options(joinedload('network')).\ options(joinedload('instance')).\ first() if not result: - raise exception.FloatingIpNotFoundForFixedAddress(fixed_ip=address) + raise exception.FixedIpNotFoundForAddress(address=address) if is_user_context(context): authorize_project_context(context, result.instance.project_id) @@ -770,30 +789,64 @@ def fixed_ip_get_by_address(context, address, session=None): @require_context -def fixed_ip_get_instance(context, address): - fixed_ip_ref = fixed_ip_get_by_address(context, address) - return fixed_ip_ref.instance +def fixed_ip_get_by_instance(context, instance_id): + session = get_session() + rv = session.query(models.FixedIp).\ + options(joinedload('floating_ips')).\ + filter_by(instance_id=instance_id).\ + filter_by(deleted=False).\ + all() + if not rv: + raise exception.FixedIpNotFoundForInstance(instance_id=instance_id) + return rv @require_context -def fixed_ip_get_all_by_instance(context, instance_id): +def fixed_ip_get_by_network_host(context, network_id, host): session = get_session() rv = session.query(models.FixedIp).\ - filter_by(instance_id=instance_id).\ - filter_by(deleted=False) + filter_by(network_id=network_id).\ + filter_by(host=host).\ + filter_by(deleted=False).\ + first() + if not rv: + raise exception.FixedIpNotFoundForNetworkHost(network_id=network_id, + host=host) + return rv + + +@require_context +def fixed_ip_get_by_virtual_interface(context, vif_id): + session = get_session() + rv = session.query(models.FixedIp).\ + options(joinedload('floating_ips')).\ + filter_by(virtual_interface_id=vif_id).\ + filter_by(deleted=False).\ + all() if not rv: - raise exception.NoFixedIpsFoundForInstance(instance_id=instance_id) + raise exception.FixedIpNotFoundForVirtualInterface(vif_id=vif_id) return rv @require_context +def fixed_ip_get_instance(context, address): + fixed_ip_ref = fixed_ip_get_by_address(context, address) + return fixed_ip_ref.instance + + +@require_context def fixed_ip_get_instance_v6(context, address): session = get_session() + + # convert IPv6 address to mac mac = ipv6.to_mac(address) + # get virtual interface + vif_ref = virtual_interface_get_by_address(context, mac) + + # look up instance based on instance_id from vif row result = session.query(models.Instance).\ - filter_by(mac_address=mac).\ - first() + filter_by(id=vif_ref['instance_id']) return result @@ -815,6 +868,164 @@ def fixed_ip_update(context, address, values): ################### + + +@require_context +def virtual_interface_create(context, values): + """Create a new virtual interface record in teh database. + + :param values: = dict containing column values + """ + try: + vif_ref = models.VirtualInterface() + vif_ref.update(values) + vif_ref.save() + except IntegrityError: + raise exception.VirtualInterfaceCreateException() + + return vif_ref + + +@require_context +def virtual_interface_update(context, vif_id, values): + """Update a virtual interface record in the database. + + :param vif_id: = id of virtual interface to update + :param values: = values to update + """ + session = get_session() + with session.begin(): + vif_ref = virtual_interface_get(context, vif_id, session=session) + vif_ref.update(values) + vif_ref.save(session=session) + return vif_ref + + +@require_context +def virtual_interface_get(context, vif_id, session=None): + """Gets a virtual interface from the table. + + :param vif_id: = id of the virtual interface + """ + if not session: + session = get_session() + + vif_ref = session.query(models.VirtualInterface).\ + filter_by(id=vif_id).\ + options(joinedload('network')).\ + options(joinedload('instance')).\ + options(joinedload('fixed_ips')).\ + first() + return vif_ref + + +@require_context +def virtual_interface_get_by_address(context, address): + """Gets a virtual interface from the table. + + :param address: = the address of the interface you're looking to get + """ + session = get_session() + vif_ref = session.query(models.VirtualInterface).\ + filter_by(address=address).\ + options(joinedload('network')).\ + options(joinedload('instance')).\ + options(joinedload('fixed_ips')).\ + first() + return vif_ref + + +@require_context +def virtual_interface_get_by_fixed_ip(context, fixed_ip_id): + """Gets the virtual interface fixed_ip is associated with. + + :param fixed_ip_id: = id of the fixed_ip + """ + session = get_session() + vif_ref = session.query(models.VirtualInterface).\ + filter_by(fixed_ip_id=fixed_ip_id).\ + options(joinedload('network')).\ + options(joinedload('instance')).\ + options(joinedload('fixed_ips')).\ + first() + return vif_ref + + +@require_context +@require_instance_exists +def virtual_interface_get_by_instance(context, instance_id): + """Gets all virtual interfaces for instance. + + :param instance_id: = id of the instance to retreive vifs for + """ + session = get_session() + vif_refs = session.query(models.VirtualInterface).\ + filter_by(instance_id=instance_id).\ + options(joinedload('network')).\ + options(joinedload('instance')).\ + options(joinedload('fixed_ips')).\ + all() + return vif_refs + + +@require_context +def virtual_interface_get_by_instance_and_network(context, instance_id, + network_id): + """Gets virtual interface for instance that's associated with network.""" + session = get_session() + vif_ref = session.query(models.VirtualInterface).\ + filter_by(instance_id=instance_id).\ + filter_by(network_id=network_id).\ + options(joinedload('network')).\ + options(joinedload('instance')).\ + options(joinedload('fixed_ips')).\ + first() + return vif_ref + + +@require_admin_context +def virtual_interface_get_by_network(context, network_id): + """Gets all virtual_interface on network. + + :param network_id: = network to retreive vifs for + """ + session = get_session() + vif_refs = session.query(models.VirtualInterface).\ + filter_by(network_id=network_id).\ + options(joinedload('network')).\ + options(joinedload('instance')).\ + options(joinedload('fixed_ips')).\ + all() + return vif_refs + + +@require_context +def virtual_interface_delete(context, vif_id): + """Delete virtual interface record from teh database. + + :param vif_id: = id of vif to delete + """ + session = get_session() + vif_ref = virtual_interface_get(context, vif_id, session) + with session.begin(): + session.delete(vif_ref) + + +@require_context +def virtual_interface_delete_by_instance(context, instance_id): + """Delete virtual interface records that are associated + with the instance given by instance_id. + + :param instance_id: = id of instance + """ + vif_refs = virtual_interface_get_by_instance(context, instance_id) + for vif_ref in vif_refs: + virtual_interface_delete(context, vif_ref['id']) + + +################### + + def _metadata_refs(metadata_dict): metadata_refs = [] if metadata_dict: @@ -927,10 +1138,11 @@ def _build_instance_get(context, session=None): session = get_session() partial = session.query(models.Instance).\ - options(joinedload_all('fixed_ip.floating_ips')).\ + options(joinedload_all('fixed_ips.floating_ips')).\ + options(joinedload_all('fixed_ips.network')).\ + options(joinedload('virtual_interfaces')).\ options(joinedload_all('security_groups.rules')).\ options(joinedload('volumes')).\ - options(joinedload_all('fixed_ip.network')).\ options(joinedload('metadata')).\ options(joinedload('instance_type')) @@ -946,9 +1158,10 @@ def _build_instance_get(context, session=None): def instance_get_all(context): session = get_session() return session.query(models.Instance).\ - options(joinedload_all('fixed_ip.floating_ips')).\ + options(joinedload_all('fixed_ips.floating_ips')).\ + options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ - options(joinedload_all('fixed_ip.network')).\ + options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ options(joinedload('instance_type')).\ filter_by(deleted=can_read_deleted(context)).\ @@ -956,12 +1169,31 @@ def instance_get_all(context): @require_admin_context -def instance_get_all_by_user(context, user_id): +def instance_get_active_by_window(context, begin, end=None): + """Return instances that were continuously active over the given window""" session = get_session() - return session.query(models.Instance).\ + query = session.query(models.Instance).\ options(joinedload_all('fixed_ip.floating_ips')).\ options(joinedload('security_groups')).\ options(joinedload_all('fixed_ip.network')).\ + options(joinedload('instance_type')).\ + filter(models.Instance.launched_at < begin) + if end: + query = query.filter(or_(models.Instance.terminated_at == None, + models.Instance.terminated_at > end)) + else: + query = query.filter(models.Instance.terminated_at == None) + return query.all() + + +@require_admin_context +def instance_get_all_by_user(context, user_id): + session = get_session() + return session.query(models.Instance).\ + options(joinedload_all('fixed_ips.floating_ips')).\ + options(joinedload('virtual_interfaces')).\ + options(joinedload('security_groups')).\ + options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ options(joinedload('instance_type')).\ filter_by(deleted=can_read_deleted(context)).\ @@ -973,9 +1205,10 @@ def instance_get_all_by_user(context, user_id): def instance_get_all_by_host(context, host): session = get_session() return session.query(models.Instance).\ - options(joinedload_all('fixed_ip.floating_ips')).\ + options(joinedload_all('fixed_ips.floating_ips')).\ + options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ - options(joinedload_all('fixed_ip.network')).\ + options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ options(joinedload('instance_type')).\ filter_by(host=host).\ @@ -989,9 +1222,10 @@ def instance_get_all_by_project(context, project_id): session = get_session() return session.query(models.Instance).\ - options(joinedload_all('fixed_ip.floating_ips')).\ + options(joinedload_all('fixed_ips.floating_ips')).\ + options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ - options(joinedload_all('fixed_ip.network')).\ + options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ options(joinedload('instance_type')).\ filter_by(project_id=project_id).\ @@ -1005,9 +1239,10 @@ def instance_get_all_by_reservation(context, reservation_id): if is_admin_context(context): return session.query(models.Instance).\ - options(joinedload_all('fixed_ip.floating_ips')).\ + options(joinedload_all('fixed_ips.floating_ips')).\ + options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ - options(joinedload_all('fixed_ip.network')).\ + options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ options(joinedload('instance_type')).\ filter_by(reservation_id=reservation_id).\ @@ -1015,9 +1250,10 @@ def instance_get_all_by_reservation(context, reservation_id): all() elif is_user_context(context): return session.query(models.Instance).\ - options(joinedload_all('fixed_ip.floating_ips')).\ + options(joinedload_all('fixed_ips.floating_ips')).\ + options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ - options(joinedload_all('fixed_ip.network')).\ + options(joinedload_all('fixed_ips.network')).\ options(joinedload('metadata')).\ options(joinedload('instance_type')).\ filter_by(project_id=context.project_id).\ @@ -1030,7 +1266,8 @@ def instance_get_all_by_reservation(context, reservation_id): def instance_get_project_vpn(context, project_id): session = get_session() return session.query(models.Instance).\ - options(joinedload_all('fixed_ip.floating_ips')).\ + options(joinedload_all('fixed_ips.floating_ips')).\ + options(joinedload('virtual_interfaces')).\ options(joinedload('security_groups')).\ options(joinedload_all('fixed_ip.network')).\ options(joinedload('metadata')).\ @@ -1042,38 +1279,53 @@ def instance_get_project_vpn(context, project_id): @require_context -def instance_get_fixed_address(context, instance_id): +def instance_get_fixed_addresses(context, instance_id): session = get_session() with session.begin(): instance_ref = instance_get(context, instance_id, session=session) - if not instance_ref.fixed_ip: - return None - return instance_ref.fixed_ip['address'] + try: + fixed_ips = fixed_ip_get_by_instance(context, instance_id) + except exception.NotFound: + return [] + return [fixed_ip.address for fixed_ip in fixed_ips] @require_context -def instance_get_fixed_address_v6(context, instance_id): +def instance_get_fixed_addresses_v6(context, instance_id): session = get_session() with session.begin(): + # get instance instance_ref = instance_get(context, instance_id, session=session) - network_ref = network_get_by_instance(context, instance_id) - prefix = network_ref.cidr_v6 - mac = instance_ref.mac_address + # assume instance has 1 mac for each network associated with it + # get networks associated with instance + network_refs = network_get_all_by_instance(context, instance_id) + # compile a list of cidr_v6 prefixes sorted by network id + prefixes = [ref.cidr_v6 for ref in + sorted(network_refs, key=lambda ref: ref.id)] + # get vifs associated with instance + vif_refs = virtual_interface_get_by_instance(context, instance_ref.id) + # compile list of the mac_addresses for vifs sorted by network id + macs = [vif_ref['address'] for vif_ref in + sorted(vif_refs, key=lambda vif_ref: vif_ref['network_id'])] + # get project id from instance project_id = instance_ref.project_id - return ipv6.to_global(prefix, mac, project_id) + # combine prefixes, macs, and project_id into (prefix,mac,p_id) tuples + prefix_mac_tuples = zip(prefixes, macs, [project_id for m in macs]) + # return list containing ipv6 address for each tuple + return [ipv6.to_global_ipv6(*t) for t in prefix_mac_tuples] @require_context def instance_get_floating_address(context, instance_id): - session = get_session() - with session.begin(): - instance_ref = instance_get(context, instance_id, session=session) - if not instance_ref.fixed_ip: - return None - if not instance_ref.fixed_ip.floating_ips: - return None - # NOTE(vish): this just returns the first floating ip - return instance_ref.fixed_ip.floating_ips[0]['address'] + fixed_ip_refs = fixed_ip_get_by_instance(context, instance_id) + if not fixed_ip_refs: + return None + # NOTE(tr3buchet): this only gets the first fixed_ip + # won't find floating ips associated with other fixed_ips + if not fixed_ip_refs[0].floating_ips: + return None + # NOTE(vish): this just returns the first floating ip + return fixed_ip_refs[0].floating_ips[0]['address'] @require_admin_context @@ -1097,7 +1349,11 @@ def instance_update(context, instance_id, values): instance_metadata_update_or_create(context, instance_id, values.pop('metadata')) with session.begin(): - instance_ref = instance_get(context, instance_id, session=session) + if utils.is_uuid_like(instance_id): + instance_ref = instance_get_by_uuid(context, instance_id, + session=session) + else: + instance_ref = instance_get(context, instance_id, session=session) instance_ref.update(values) instance_ref.save(session=session) return instance_ref @@ -1238,20 +1494,49 @@ def key_pair_get_all_by_user(context, user_id): @require_admin_context -def network_associate(context, project_id): +def network_associate(context, project_id, force=False): + """Associate a project with a network. + + called by project_get_networks under certain conditions + and network manager add_network_to_project() + + only associate if the project doesn't already have a network + or if force is True + + force solves race condition where a fresh project has multiple instance + builds simultaneosly picked up by multiple network hosts which attempt + to associate the project with multiple networks + force should only be used as a direct consequence of user request + all automated requests should not use force + """ session = get_session() with session.begin(): - network_ref = session.query(models.Network).\ - filter_by(deleted=False).\ - filter_by(project_id=None).\ - with_lockmode('update').\ - first() - # NOTE(vish): if with_lockmode isn't supported, as in sqlite, - # then this has concurrency issues - if not network_ref: - raise db.NoMoreNetworks() - network_ref['project_id'] = project_id - session.add(network_ref) + + def network_query(project_filter): + return session.query(models.Network).\ + filter_by(deleted=False).\ + filter_by(project_id=project_filter).\ + with_lockmode('update').\ + first() + + if not force: + # find out if project has a network + network_ref = network_query(project_id) + + if force or not network_ref: + # in force mode or project doesn't have a network so assocaite + # with a new network + + # get new network + network_ref = network_query(None) + if not network_ref: + raise db.NoMoreNetworks() + + # associate with network + # NOTE(vish): if with_lockmode isn't supported, as in sqlite, + # then this has concurrency issues + network_ref['project_id'] = project_id + session.add(network_ref) return network_ref @@ -1354,7 +1639,8 @@ def network_get(context, network_id, session=None): @require_admin_context def network_get_all(context): session = get_session() - result = session.query(models.Network) + result = session.query(models.Network).\ + filter_by(deleted=False).all() if not result: raise exception.NoNetworksFound() return result @@ -1372,6 +1658,7 @@ def network_get_associated_fixed_ips(context, network_id): options(joinedload_all('instance')).\ filter_by(network_id=network_id).\ filter(models.FixedIp.instance_id != None).\ + filter(models.FixedIp.virtual_interface_id != None).\ filter_by(deleted=False).\ all() @@ -1402,6 +1689,8 @@ def network_get_by_cidr(context, cidr): @require_admin_context def network_get_by_instance(_context, instance_id): + # note this uses fixed IP to get to instance + # only works for networks the instance has an IP from session = get_session() rv = session.query(models.Network).\ filter_by(deleted=False).\ @@ -1421,13 +1710,31 @@ def network_get_all_by_instance(_context, instance_id): filter_by(deleted=False).\ join(models.Network.fixed_ips).\ filter_by(instance_id=instance_id).\ - filter_by(deleted=False) + filter_by(deleted=False).\ + all() if not rv: raise exception.NetworkNotFoundForInstance(instance_id=instance_id) return rv @require_admin_context +def network_get_all_by_host(context, host): + session = get_session() + with session.begin(): + # NOTE(vish): return networks that have host set + # or that have a fixed ip with host set + host_filter = or_(models.Network.host == host, + models.FixedIp.host == host) + + return session.query(models.Network).\ + filter_by(deleted=False).\ + join(models.Network.fixed_ips).\ + filter(host_filter).\ + filter_by(deleted=False).\ + all() + + +@require_admin_context def network_set_host(context, network_id, host_id): session = get_session() with session.begin(): @@ -1455,37 +1762,7 @@ def network_update(context, network_id, values): network_ref = network_get(context, network_id, session=session) network_ref.update(values) network_ref.save(session=session) - - -################### - - -@require_context -def project_get_network(context, project_id, associate=True): - session = get_session() - result = session.query(models.Network).\ - filter_by(project_id=project_id).\ - filter_by(deleted=False).\ - first() - if not result: - if not associate: - return None - try: - return network_associate(context, project_id) - except IntegrityError: - # NOTE(vish): We hit this if there is a race and two - # processes are attempting to allocate the - # network at the same time - result = session.query(models.Network).\ - filter_by(project_id=project_id).\ - filter_by(deleted=False).\ - first() - return result - - -@require_context -def project_get_network_v6(context, project_id): - return project_get_network(context, project_id) + return network_ref ################### @@ -1972,6 +2249,23 @@ def block_device_mapping_update(context, bdm_id, values): @require_context +def block_device_mapping_update_or_create(context, values): + session = get_session() + with session.begin(): + result = session.query(models.BlockDeviceMapping).\ + filter_by(instance_id=values['instance_id']).\ + filter_by(device_name=values['device_name']).\ + filter_by(deleted=False).\ + first() + if not result: + bdm_ref = models.BlockDeviceMapping() + bdm_ref.update(values) + bdm_ref.save(session=session) + else: + result.update(values) + + +@require_context def block_device_mapping_get_all_by_instance(context, instance_id): session = get_session() result = session.query(models.BlockDeviceMapping).\ @@ -2323,6 +2617,73 @@ def user_get_all(context): all() +def user_get_roles(context, user_id): + session = get_session() + with session.begin(): + user_ref = user_get(context, user_id, session=session) + return [role.role for role in user_ref['roles']] + + +def user_get_roles_for_project(context, user_id, project_id): + session = get_session() + with session.begin(): + res = session.query(models.UserProjectRoleAssociation).\ + filter_by(user_id=user_id).\ + filter_by(project_id=project_id).\ + all() + return [association.role for association in res] + + +def user_remove_project_role(context, user_id, project_id, role): + session = get_session() + with session.begin(): + session.query(models.UserProjectRoleAssociation).\ + filter_by(user_id=user_id).\ + filter_by(project_id=project_id).\ + filter_by(role=role).\ + delete() + + +def user_remove_role(context, user_id, role): + session = get_session() + with session.begin(): + res = session.query(models.UserRoleAssociation).\ + filter_by(user_id=user_id).\ + filter_by(role=role).\ + all() + for role in res: + session.delete(role) + + +def user_add_role(context, user_id, role): + session = get_session() + with session.begin(): + user_ref = user_get(context, user_id, session=session) + models.UserRoleAssociation(user=user_ref, role=role).\ + save(session=session) + + +def user_add_project_role(context, user_id, project_id, role): + session = get_session() + with session.begin(): + user_ref = user_get(context, user_id, session=session) + project_ref = project_get(context, project_id, session=session) + models.UserProjectRoleAssociation(user_id=user_ref['id'], + project_id=project_ref['id'], + role=role).save(session=session) + + +def user_update(context, user_id, values): + session = get_session() + with session.begin(): + user_ref = user_get(context, user_id, session=session) + user_ref.update(values) + user_ref.save(session=session) + + +################### + + def project_create(_context, values): project_ref = models.Project() project_ref.update(values) @@ -2386,14 +2747,6 @@ def project_remove_member(context, project_id, user_id): project.save(session=session) -def user_update(context, user_id, values): - session = get_session() - with session.begin(): - user_ref = user_get(context, user_id, session=session) - user_ref.update(values) - user_ref.save(session=session) - - def project_update(context, project_id, values): session = get_session() with session.begin(): @@ -2415,73 +2768,26 @@ def project_delete(context, id): session.delete(project_ref) -def user_get_roles(context, user_id): - session = get_session() - with session.begin(): - user_ref = user_get(context, user_id, session=session) - return [role.role for role in user_ref['roles']] - - -def user_get_roles_for_project(context, user_id, project_id): - session = get_session() - with session.begin(): - res = session.query(models.UserProjectRoleAssociation).\ - filter_by(user_id=user_id).\ - filter_by(project_id=project_id).\ - all() - return [association.role for association in res] - - -def user_remove_project_role(context, user_id, project_id, role): - session = get_session() - with session.begin(): - session.query(models.UserProjectRoleAssociation).\ - filter_by(user_id=user_id).\ - filter_by(project_id=project_id).\ - filter_by(role=role).\ - delete() - - -def user_remove_role(context, user_id, role): - session = get_session() - with session.begin(): - res = session.query(models.UserRoleAssociation).\ - filter_by(user_id=user_id).\ - filter_by(role=role).\ - all() - for role in res: - session.delete(role) - - -def user_add_role(context, user_id, role): - session = get_session() - with session.begin(): - user_ref = user_get(context, user_id, session=session) - models.UserRoleAssociation(user=user_ref, role=role).\ - save(session=session) - - -def user_add_project_role(context, user_id, project_id, role): +@require_context +def project_get_networks(context, project_id, associate=True): + # NOTE(tr3buchet): as before this function will associate + # a project with a network if it doesn't have one and + # associate is true session = get_session() - with session.begin(): - user_ref = user_get(context, user_id, session=session) - project_ref = project_get(context, project_id, session=session) - models.UserProjectRoleAssociation(user_id=user_ref['id'], - project_id=project_ref['id'], - role=role).save(session=session) - + result = session.query(models.Network).\ + filter_by(project_id=project_id).\ + filter_by(deleted=False).all() -################### + if not result: + if not associate: + return [] + return [network_associate(context, project_id)] + return result -@require_admin_context -def host_get_networks(context, host): - session = get_session() - with session.begin(): - return session.query(models.Network).\ - filter_by(deleted=False).\ - filter_by(host=host).\ - all() +@require_context +def project_get_networks_v6(context, project_id): + return project_get_networks(context, project_id) ################### @@ -2517,13 +2823,13 @@ def migration_get(context, id, session=None): @require_admin_context -def migration_get_by_instance_and_status(context, instance_id, status): +def migration_get_by_instance_and_status(context, instance_uuid, status): session = get_session() result = session.query(models.Migration).\ - filter_by(instance_id=instance_id).\ + filter_by(instance_uuid=instance_uuid).\ filter_by(status=status).first() if not result: - raise exception.MigrationNotFoundByStatus(instance_id=instance_id, + raise exception.MigrationNotFoundByStatus(instance_id=instance_uuid, status=status) return result @@ -2696,17 +3002,15 @@ def instance_type_get_all(context, inactive=False): filter_by(deleted=False).\ order_by("name").\ all() + inst_dict = {} if inst_types: - inst_dict = {} for i in inst_types: inst_dict[i['name']] = _dict_with_extra_specs(i) - return inst_dict - else: - raise exception.NoInstanceTypesFound() + return inst_dict @require_context -def instance_type_get_by_id(context, id): +def instance_type_get(context, id): """Returns a dict describing specific instance_type""" session = get_session() inst_type = session.query(models.InstanceTypes).\ @@ -2794,7 +3098,7 @@ def zone_update(context, zone_id, values): if not zone: raise exception.ZoneNotFound(zone_id=zone_id) zone.update(values) - zone.save() + zone.save(session=session) return zone @@ -2825,14 +3129,6 @@ def zone_get_all(context): #################### -def require_instance_exists(func): - def new_func(context, instance_id, *args, **kwargs): - db.api.instance_get(context, instance_id) - return func(context, instance_id, *args, **kwargs) - new_func.__name__ = func.__name__ - return new_func - - @require_context @require_instance_exists def instance_metadata_get(context, instance_id): diff --git a/nova/db/sqlalchemy/migrate_repo/versions/029_add_zone_weight_offsets.py b/nova/db/sqlalchemy/migrate_repo/versions/029_add_zone_weight_offsets.py new file mode 100644 index 000000000..1b7871e5f --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/029_add_zone_weight_offsets.py @@ -0,0 +1,38 @@ +# Copyright 2011 OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, Float, Integer, MetaData, Table + +meta = MetaData() + +zones = Table('zones', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + +weight_offset = Column('weight_offset', Float(), default=0.0) +weight_scale = Column('weight_scale', Float(), default=1.0) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + + zones.create_column(weight_offset) + zones.create_column(weight_scale) + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + + zones.drop_column(weight_offset) + zones.drop_column(weight_scale) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/030_multi_nic.py b/nova/db/sqlalchemy/migrate_repo/versions/030_multi_nic.py new file mode 100644 index 000000000..4a117bb11 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/030_multi_nic.py @@ -0,0 +1,125 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from sqlalchemy import * +from migrate import * + +from nova import log as logging +from nova import utils + +meta = MetaData() + +# virtual interface table to add to DB +virtual_interfaces = Table('virtual_interfaces', meta, + Column('created_at', DateTime(timezone=False), + default=utils.utcnow()), + Column('updated_at', DateTime(timezone=False), + onupdate=utils.utcnow()), + Column('deleted_at', DateTime(timezone=False)), + Column('deleted', Boolean(create_constraint=True, name=None)), + Column('id', Integer(), primary_key=True, nullable=False), + Column('address', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + unique=True), + Column('network_id', + Integer(), + ForeignKey('networks.id')), + Column('instance_id', + Integer(), + ForeignKey('instances.id'), + nullable=False), + mysql_engine='InnoDB') + + +# bridge_interface column to add to networks table +interface = Column('bridge_interface', + String(length=255, convert_unicode=False, + assert_unicode=None, unicode_error=None, + _warn_on_bytestring=False)) + + +# virtual interface id column to add to fixed_ips table +# foreignkey added in next migration +virtual_interface_id = Column('virtual_interface_id', + Integer()) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + + # grab tables and (column for dropping later) + instances = Table('instances', meta, autoload=True) + networks = Table('networks', meta, autoload=True) + fixed_ips = Table('fixed_ips', meta, autoload=True) + c = instances.columns['mac_address'] + + # add interface column to networks table + # values will have to be set manually before running nova + try: + networks.create_column(interface) + except Exception: + logging.error(_("interface column not added to networks table")) + raise + + # create virtual_interfaces table + try: + virtual_interfaces.create() + except Exception: + logging.error(_("Table |%s| not created!"), repr(virtual_interfaces)) + raise + + # add virtual_interface_id column to fixed_ips table + try: + fixed_ips.create_column(virtual_interface_id) + except Exception: + logging.error(_("VIF column not added to fixed_ips table")) + raise + + # populate the virtual_interfaces table + # extract data from existing instance and fixed_ip tables + s = select([instances.c.id, instances.c.mac_address, + fixed_ips.c.network_id], + fixed_ips.c.instance_id == instances.c.id) + keys = ('instance_id', 'address', 'network_id') + join_list = [dict(zip(keys, row)) for row in s.execute()] + logging.debug(_("join list for moving mac_addresses |%s|"), join_list) + + # insert data into the table + if join_list: + i = virtual_interfaces.insert() + i.execute(join_list) + + # populate the fixed_ips virtual_interface_id column + s = select([fixed_ips.c.id, fixed_ips.c.instance_id], + fixed_ips.c.instance_id != None) + + for row in s.execute(): + m = select([virtual_interfaces.c.id]).\ + where(virtual_interfaces.c.instance_id == row['instance_id']).\ + as_scalar() + u = fixed_ips.update().values(virtual_interface_id=m).\ + where(fixed_ips.c.id == row['id']) + u.execute() + + # drop the mac_address column from instances + c.drop() + + +def downgrade(migrate_engine): + logging.error(_("Can't downgrade without losing data")) + raise Exception diff --git a/nova/db/sqlalchemy/migrate_repo/versions/031_fk_fixed_ips_virtual_interface_id.py b/nova/db/sqlalchemy/migrate_repo/versions/031_fk_fixed_ips_virtual_interface_id.py new file mode 100644 index 000000000..56e927717 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/031_fk_fixed_ips_virtual_interface_id.py @@ -0,0 +1,56 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from sqlalchemy import * +from migrate import * + +from nova import log as logging +from nova import utils + +meta = MetaData() + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + dialect = migrate_engine.url.get_dialect().name + + # grab tables + fixed_ips = Table('fixed_ips', meta, autoload=True) + virtual_interfaces = Table('virtual_interfaces', meta, autoload=True) + + # add foreignkey if not sqlite + try: + if not dialect.startswith('sqlite'): + ForeignKeyConstraint(columns=[fixed_ips.c.virtual_interface_id], + refcolumns=[virtual_interfaces.c.id]).create() + except Exception: + logging.error(_("foreign key constraint couldn't be added")) + raise + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + dialect = migrate_engine.url.get_dialect().name + + # drop foreignkey if not sqlite + try: + if not dialect.startswith('sqlite'): + ForeignKeyConstraint(columns=[fixed_ips.c.virtual_interface_id], + refcolumns=[virtual_interfaces.c.id]).drop() + except Exception: + logging.error(_("foreign key constraint couldn't be dropped")) + raise diff --git a/nova/db/sqlalchemy/migrate_repo/versions/031_sqlite_downgrade.sql b/nova/db/sqlalchemy/migrate_repo/versions/031_sqlite_downgrade.sql new file mode 100644 index 000000000..c1d26b180 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/031_sqlite_downgrade.sql @@ -0,0 +1,48 @@ +BEGIN TRANSACTION; + + CREATE TEMPORARY TABLE fixed_ips_backup ( + id INTEGER NOT NULL, + address VARCHAR(255), + virtual_interface_id INTEGER, + network_id INTEGER, + instance_id INTEGER, + allocated BOOLEAN default FALSE, + leased BOOLEAN default FALSE, + reserved BOOLEAN default FALSE, + created_at DATETIME NOT NULL, + updated_at DATETIME, + deleted_at DATETIME, + deleted BOOLEAN NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(virtual_interface_id) REFERENCES virtual_interfaces (id) + ); + + INSERT INTO fixed_ips_backup + SELECT id, address, virtual_interface_id, network_id, instance_id, allocated, leased, reserved, created_at, updated_at, deleted_at, deleted + FROM fixed_ips; + + DROP TABLE fixed_ips; + + CREATE TABLE fixed_ips ( + id INTEGER NOT NULL, + address VARCHAR(255), + virtual_interface_id INTEGER, + network_id INTEGER, + instance_id INTEGER, + allocated BOOLEAN default FALSE, + leased BOOLEAN default FALSE, + reserved BOOLEAN default FALSE, + created_at DATETIME NOT NULL, + updated_at DATETIME, + deleted_at DATETIME, + deleted BOOLEAN NOT NULL, + PRIMARY KEY (id) + ); + + INSERT INTO fixed_ips + SELECT id, address, virtual_interface_id, network_id, instance_id, allocated, leased, reserved, created_at, updated_at, deleted_at, deleted + FROM fixed_ips; + + DROP TABLE fixed_ips_backup; + +COMMIT; diff --git a/nova/db/sqlalchemy/migrate_repo/versions/031_sqlite_upgrade.sql b/nova/db/sqlalchemy/migrate_repo/versions/031_sqlite_upgrade.sql new file mode 100644 index 000000000..2a9362545 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/031_sqlite_upgrade.sql @@ -0,0 +1,48 @@ +BEGIN TRANSACTION; + + CREATE TEMPORARY TABLE fixed_ips_backup ( + id INTEGER NOT NULL, + address VARCHAR(255), + virtual_interface_id INTEGER, + network_id INTEGER, + instance_id INTEGER, + allocated BOOLEAN default FALSE, + leased BOOLEAN default FALSE, + reserved BOOLEAN default FALSE, + created_at DATETIME NOT NULL, + updated_at DATETIME, + deleted_at DATETIME, + deleted BOOLEAN NOT NULL, + PRIMARY KEY (id) + ); + + INSERT INTO fixed_ips_backup + SELECT id, address, virtual_interface_id, network_id, instance_id, allocated, leased, reserved, created_at, updated_at, deleted_at, deleted + FROM fixed_ips; + + DROP TABLE fixed_ips; + + CREATE TABLE fixed_ips ( + id INTEGER NOT NULL, + address VARCHAR(255), + virtual_interface_id INTEGER, + network_id INTEGER, + instance_id INTEGER, + allocated BOOLEAN default FALSE, + leased BOOLEAN default FALSE, + reserved BOOLEAN default FALSE, + created_at DATETIME NOT NULL, + updated_at DATETIME, + deleted_at DATETIME, + deleted BOOLEAN NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(virtual_interface_id) REFERENCES virtual_interfaces (id) + ); + + INSERT INTO fixed_ips + SELECT id, address, virtual_interface_id, network_id, instance_id, allocated, leased, reserved, created_at, updated_at, deleted_at, deleted + FROM fixed_ips; + + DROP TABLE fixed_ips_backup; + +COMMIT; diff --git a/nova/db/sqlalchemy/migrate_repo/versions/032_add_root_device_name.py b/nova/db/sqlalchemy/migrate_repo/versions/032_add_root_device_name.py new file mode 100644 index 000000000..6b98b9890 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/032_add_root_device_name.py @@ -0,0 +1,47 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Isaku Yamahata +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, Integer, MetaData, Table, String + +meta = MetaData() + + +# Just for the ForeignKey and column creation to succeed, these are not the +# actual definitions of instances or services. +instances = Table('instances', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + +# +# New Column +# +root_device_name = Column( + 'root_device_name', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + instances.create_column(root_device_name) + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta.bind = migrate_engine + instances.drop_column('root_device_name') diff --git a/nova/db/sqlalchemy/migrate_repo/versions/033_ha_network.py b/nova/db/sqlalchemy/migrate_repo/versions/033_ha_network.py new file mode 100644 index 000000000..3a5f7eba8 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/033_ha_network.py @@ -0,0 +1,44 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, Table, MetaData, Boolean, String + +meta = MetaData() + +fixed_ips_host = Column('host', String(255)) + +networks_multi_host = Column('multi_host', Boolean, default=False) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + + fixed_ips = Table('fixed_ips', meta, autoload=True) + fixed_ips.create_column(fixed_ips_host) + + networks = Table('networks', meta, autoload=True) + networks.create_column(networks_multi_host) + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + + fixed_ips = Table('fixed_ips', meta, autoload=True) + fixed_ips.drop_column(fixed_ips_host) + + networks = Table('networks', meta, autoload=True) + networks.drop_column(networks_multi_host) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/034_change_instance_id_in_migrations.py b/nova/db/sqlalchemy/migrate_repo/versions/034_change_instance_id_in_migrations.py new file mode 100644 index 000000000..b002ba064 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/034_change_instance_id_in_migrations.py @@ -0,0 +1,43 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License.from sqlalchemy import * + +from sqlalchemy import Column, Integer, String, MetaData, Table + +meta = MetaData() + + +# +# Tables to alter +# +# + +instance_id = Column('instance_id', Integer()) +instance_uuid = Column('instance_uuid', String(255)) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + migrations = Table('migrations', meta, autoload=True) + migrations.create_column(instance_uuid) + migrations.c.instance_id.drop() + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + migrations = Table('migrations', meta, autoload=True) + migrations.c.instance_uuid.drop() + migrations.create_column(instance_id) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/035_secondary_dns.py b/nova/db/sqlalchemy/migrate_repo/versions/035_secondary_dns.py new file mode 100644 index 000000000..c938eb716 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/035_secondary_dns.py @@ -0,0 +1,38 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, Table, MetaData, Boolean, String + +meta = MetaData() + +dns2 = Column('dns2', String(255)) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + + networks = Table('networks', meta, autoload=True) + networks.c.dns.alter(Column('dns1', String(255))) + networks.create_column(dns2) + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + + networks = Table('networks', meta, autoload=True) + networks.c.dns1.alter(Column('dns', String(255))) + networks.drop_column(dns2) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 2955b8780..7e35c2cba 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -21,7 +21,7 @@ SQLAlchemy models for nova data. from sqlalchemy.orm import relationship, backref, object_mapper from sqlalchemy import Column, Integer, String, schema -from sqlalchemy import ForeignKey, DateTime, Boolean, Text +from sqlalchemy import ForeignKey, DateTime, Boolean, Text, Float from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.schema import ForeignKeyConstraint @@ -31,6 +31,7 @@ from nova.db.sqlalchemy.session import get_session from nova import auth from nova import exception from nova import flags +from nova import ipv6 from nova import utils @@ -209,12 +210,12 @@ class Instance(BASE, NovaBase): hostname = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) + # aka flavor_id instance_type_id = Column(Integer) user_data = Column(Text) reservation_id = Column(String(255)) - mac_address = Column(String(255)) scheduled_at = Column(DateTime) launched_at = Column(DateTime) @@ -236,6 +237,8 @@ class Instance(BASE, NovaBase): vm_mode = Column(String(255)) uuid = Column(String(36)) + root_device_name = Column(String(255)) + # TODO(vish): see Ewan's email about state improvements, probably # should be in a driver base class or some such # vmstate_state = running, halted, suspended, paused @@ -526,7 +529,8 @@ class Migration(BASE, NovaBase): dest_host = Column(String(255)) old_flavor_id = Column(Integer()) new_flavor_id = Column(Integer()) - instance_id = Column(Integer, ForeignKey('instances.id'), nullable=True) + instance_uuid = Column(String(255), ForeignKey('instances.uuid'), + nullable=True) #TODO(_cerberus_): enum status = Column(String(255)) @@ -543,14 +547,17 @@ class Network(BASE, NovaBase): injected = Column(Boolean, default=False) cidr = Column(String(255), unique=True) cidr_v6 = Column(String(255), unique=True) + multi_host = Column(Boolean, default=False) gateway_v6 = Column(String(255)) netmask_v6 = Column(String(255)) netmask = Column(String(255)) bridge = Column(String(255)) + bridge_interface = Column(String(255)) gateway = Column(String(255)) broadcast = Column(String(255)) - dns = Column(String(255)) + dns1 = Column(String(255)) + dns2 = Column(String(255)) vlan = Column(Integer) vpn_public_address = Column(String(255)) @@ -558,26 +565,33 @@ class Network(BASE, NovaBase): vpn_private_address = Column(String(255)) dhcp_start = Column(String(255)) - # NOTE(vish): The unique constraint below helps avoid a race condition - # when associating a network, but it also means that we - # can't associate two networks with one project. - project_id = Column(String(255), unique=True) + project_id = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) -class AuthToken(BASE, NovaBase): - """Represents an authorization token for all API transactions. +class VirtualInterface(BASE, NovaBase): + """Represents a virtual interface on an instance.""" + __tablename__ = 'virtual_interfaces' + id = Column(Integer, primary_key=True) + address = Column(String(255), unique=True) + network_id = Column(Integer, ForeignKey('networks.id')) + network = relationship(Network, backref=backref('virtual_interfaces')) - Fields are a string representing the actual token and a user id for - mapping to the actual user + # TODO(tr3buchet): cut the cord, removed foreign key and backrefs + instance_id = Column(Integer, ForeignKey('instances.id'), nullable=False) + instance = relationship(Instance, backref=backref('virtual_interfaces')) - """ - __tablename__ = 'auth_tokens' - token_hash = Column(String(255), primary_key=True) - user_id = Column(String(255)) - server_management_url = Column(String(255)) - storage_url = Column(String(255)) - cdn_management_url = Column(String(255)) + @property + def fixed_ipv6(self): + cidr_v6 = self.network.cidr_v6 + if cidr_v6 is None: + ipv6_address = None + else: + project_id = self.instance.project_id + mac = self.address + ipv6_address = ipv6.to_global(cidr_v6, mac, project_id) + + return ipv6_address # TODO(vish): can these both come from the same baseclass? @@ -588,16 +602,56 @@ class FixedIp(BASE, NovaBase): address = Column(String(255)) network_id = Column(Integer, ForeignKey('networks.id'), nullable=True) network = relationship(Network, backref=backref('fixed_ips')) + virtual_interface_id = Column(Integer, ForeignKey('virtual_interfaces.id'), + nullable=True) + virtual_interface = relationship(VirtualInterface, + backref=backref('fixed_ips')) instance_id = Column(Integer, ForeignKey('instances.id'), nullable=True) instance = relationship(Instance, - backref=backref('fixed_ip', uselist=False), + backref=backref('fixed_ips'), foreign_keys=instance_id, primaryjoin='and_(' 'FixedIp.instance_id == Instance.id,' 'FixedIp.deleted == False)') + # associated means that a fixed_ip has its instance_id column set + # allocated means that a fixed_ip has a its virtual_interface_id column set allocated = Column(Boolean, default=False) + # leased means dhcp bridge has leased the ip leased = Column(Boolean, default=False) reserved = Column(Boolean, default=False) + host = Column(String(255)) + + +class FloatingIp(BASE, NovaBase): + """Represents a floating ip that dynamically forwards to a fixed ip.""" + __tablename__ = 'floating_ips' + id = Column(Integer, primary_key=True) + address = Column(String(255)) + fixed_ip_id = Column(Integer, ForeignKey('fixed_ips.id'), nullable=True) + fixed_ip = relationship(FixedIp, + backref=backref('floating_ips'), + foreign_keys=fixed_ip_id, + primaryjoin='and_(' + 'FloatingIp.fixed_ip_id == FixedIp.id,' + 'FloatingIp.deleted == False)') + project_id = Column(String(255)) + host = Column(String(255)) # , ForeignKey('hosts.id')) + auto_assigned = Column(Boolean, default=False, nullable=False) + + +class AuthToken(BASE, NovaBase): + """Represents an authorization token for all API transactions. + + Fields are a string representing the actual token and a user id for + mapping to the actual user + + """ + __tablename__ = 'auth_tokens' + token_hash = Column(String(255), primary_key=True) + user_id = Column(String(255)) + server_management_url = Column(String(255)) + storage_url = Column(String(255)) + cdn_management_url = Column(String(255)) class User(BASE, NovaBase): @@ -660,23 +714,6 @@ class UserProjectAssociation(BASE, NovaBase): project_id = Column(String(255), ForeignKey(Project.id), primary_key=True) -class FloatingIp(BASE, NovaBase): - """Represents a floating ip that dynamically forwards to a fixed ip.""" - __tablename__ = 'floating_ips' - id = Column(Integer, primary_key=True) - address = Column(String(255)) - fixed_ip_id = Column(Integer, ForeignKey('fixed_ips.id'), nullable=True) - fixed_ip = relationship(FixedIp, - backref=backref('floating_ips'), - foreign_keys=fixed_ip_id, - primaryjoin='and_(' - 'FloatingIp.fixed_ip_id == FixedIp.id,' - 'FloatingIp.deleted == False)') - project_id = Column(String(255)) - host = Column(String(255)) # , ForeignKey('hosts.id')) - auto_assigned = Column(Boolean, default=False, nullable=False) - - class ConsolePool(BASE, NovaBase): """Represents pool of consoles on the same physical node.""" __tablename__ = 'console_pools' @@ -738,6 +775,8 @@ class Zone(BASE, NovaBase): api_url = Column(String(255)) username = Column(String(255)) password = Column(String(255)) + weight_offset = Column(Float(), default=0.0) + weight_scale = Column(Float(), default=1.0) class AgentBuild(BASE, NovaBase): diff --git a/nova/exception.py b/nova/exception.py index ef50b144f..38e705417 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -24,8 +24,9 @@ SHOULD include dedicated exception logging. """ -from nova import log as logging +from functools import wraps +from nova import log as logging LOG = logging.getLogger('nova.exception') @@ -77,23 +78,53 @@ def wrap_db_error(f): except Exception, e: LOG.exception(_('DB exception wrapped.')) raise DBError(e) - return _wrap _wrap.func_name = f.func_name + return _wrap -def wrap_exception(f): - def _wrap(*args, **kw): - try: - return f(*args, **kw) - except Exception, e: - if not isinstance(e, Error): - #exc_type, exc_value, exc_traceback = sys.exc_info() - LOG.exception(_('Uncaught exception')) - #logging.error(traceback.extract_stack(exc_traceback)) - raise Error(str(e)) - raise - _wrap.func_name = f.func_name - return _wrap +def wrap_exception(notifier=None, publisher_id=None, event_type=None, + level=None): + """This decorator wraps a method to catch any exceptions that may + get thrown. It logs the exception as well as optionally sending + it to the notification system. + """ + # TODO(sandy): Find a way to import nova.notifier.api so we don't have + # to pass it in as a parameter. Otherwise we get a cyclic import of + # nova.notifier.api -> nova.utils -> nova.exception :( + def inner(f): + def wrapped(*args, **kw): + try: + return f(*args, **kw) + except Exception, e: + if notifier: + payload = dict(args=args, exception=e) + payload.update(kw) + + # Use a temp vars so we don't shadow + # our outer definitions. + temp_level = level + if not temp_level: + temp_level = notifier.ERROR + + temp_type = event_type + if not temp_type: + # If f has multiple decorators, they must use + # functools.wraps to ensure the name is + # propagated. + temp_type = f.__name__ + + notifier.notify(publisher_id, temp_type, temp_level, + payload) + + if not isinstance(e, Error): + #exc_type, exc_value, exc_traceback = sys.exc_info() + LOG.exception(_('Uncaught exception')) + #logging.error(traceback.extract_stack(exc_traceback)) + raise Error(str(e)) + raise + + return wraps(f)(wrapped) + return inner class NovaException(Exception): @@ -118,6 +149,15 @@ class NovaException(Exception): return self._error_string +class VirtualInterfaceCreateException(NovaException): + message = _("Virtual Interface creation failed") + + +class VirtualInterfaceMacAddressException(NovaException): + message = _("5 attempts to create virtual interface" + "with unique mac address failed") + + class NotAuthorized(NovaException): message = _("Not authorized.") @@ -356,34 +396,67 @@ class DatastoreNotFound(NotFound): message = _("Could not find the datastore reference(s) which the VM uses.") -class NoFixedIpsFoundForInstance(NotFound): +class FixedIpNotFound(NotFound): + message = _("No fixed IP associated with id %(id)s.") + + +class FixedIpNotFoundForAddress(FixedIpNotFound): + message = _("Fixed ip not found for address %(address)s.") + + +class FixedIpNotFoundForInstance(FixedIpNotFound): message = _("Instance %(instance_id)s has zero fixed ips.") -class FloatingIpNotFound(NotFound): - message = _("Floating ip %(floating_ip)s not found") +class FixedIpNotFoundForNetworkHost(FixedIpNotFound): + message = _("Network host %(host)s has zero fixed ips " + "in network %(network_id)s.") -class FloatingIpNotFoundForFixedAddress(NotFound): - message = _("Floating ip not found for fixed address %(fixed_ip)s.") +class FixedIpNotFoundForSpecificInstance(FixedIpNotFound): + message = _("Instance %(instance_id)s doesn't have fixed ip '%(ip)s'.") -class NoFloatingIpsDefined(NotFound): - message = _("Zero floating ips could be found.") +class FixedIpNotFoundForVirtualInterface(FixedIpNotFound): + message = _("Virtual interface %(vif_id)s has zero associated fixed ips.") -class NoFloatingIpsDefinedForHost(NoFloatingIpsDefined): - message = _("Zero floating ips defined for host %(host)s.") +class FixedIpNotFoundForHost(FixedIpNotFound): + message = _("Host %(host)s has zero fixed ips.") -class NoFloatingIpsDefinedForInstance(NoFloatingIpsDefined): - message = _("Zero floating ips defined for instance %(instance_id)s.") +class NoMoreFixedIps(Error): + message = _("Zero fixed ips available.") -class NoMoreFloatingIps(NotFound): +class NoFixedIpsDefined(NotFound): + message = _("Zero fixed ips could be found.") + + +class FloatingIpNotFound(NotFound): + message = _("Floating ip not found for id %(id)s.") + + +class FloatingIpNotFoundForAddress(FloatingIpNotFound): + message = _("Floating ip not found for address %(address)s.") + + +class FloatingIpNotFoundForProject(FloatingIpNotFound): + message = _("Floating ip not found for project %(project_id)s.") + + +class FloatingIpNotFoundForHost(FloatingIpNotFound): + message = _("Floating ip not found for host %(host)s.") + + +class NoMoreFloatingIps(FloatingIpNotFound): message = _("Zero floating ips available.") +class NoFloatingIpsDefined(NotFound): + message = _("Zero floating ips exist.") + + class KeypairNotFound(NotFound): message = _("Keypair %(keypair_name)s not found for user %(user_id)s") @@ -558,6 +631,14 @@ class GlobalRoleNotAllowed(NotAllowed): message = _("Unable to use global role %(role_id)s") +class ImageRotationNotAllowed(NovaException): + message = _("Rotation is not allowed for snapshots") + + +class RotationRequiredForBackup(NovaException): + message = _("Rotation param is required for backup image_type") + + #TODO(bcwaldon): EOL this exception! class Duplicate(NovaException): pass diff --git a/nova/flags.py b/nova/flags.py index 57a4ecf2f..49355b436 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -305,6 +305,8 @@ DEFINE_string('rabbit_virtual_host', '/', 'rabbit virtual host') DEFINE_integer('rabbit_retry_interval', 10, 'rabbit connection retry interval') DEFINE_integer('rabbit_max_retries', 12, 'rabbit connection attempts') DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to') +DEFINE_list('enabled_apis', ['ec2', 'osapi'], + 'list of APIs to enable by default') DEFINE_string('ec2_host', '$my_ip', 'ip of api server') DEFINE_string('ec2_dmz_host', '$my_ip', 'internal ip of api server') DEFINE_integer('ec2_port', 8773, 'cloud controller port') diff --git a/nova/image/fake.py b/nova/image/fake.py index c4b3d5fd6..28e912534 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -137,7 +137,11 @@ class _FakeImageService(service.BaseImageService): try: image_id = metadata['id'] except KeyError: - image_id = random.randint(0, 2 ** 31 - 1) + while True: + image_id = random.randint(0, 2 ** 31 - 1) + if not self.images.get(str(image_id)): + break + image_id = str(image_id) if self.images.get(image_id): @@ -176,3 +180,8 @@ _fakeImageService = _FakeImageService() def FakeImageService(): return _fakeImageService + + +def FakeImageService_reset(): + global _fakeImageService + _fakeImageService = _FakeImageService() diff --git a/nova/image/glance.py b/nova/image/glance.py index 55d948a32..5c2dc957b 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -89,6 +89,10 @@ class GlanceImageService(service.BaseImageService): # `get_images` here because we need `is_public` and `properties` # included so we can filter by user filtered = [] + filters = filters or {} + if 'is_public' not in filters: + # NOTE(vish): don't filter out private images + filters['is_public'] = 'none' image_metas = self.client.get_images_detailed(filters=filters, marker=marker, limit=limit) @@ -101,6 +105,10 @@ class GlanceImageService(service.BaseImageService): def detail(self, context, filters=None, marker=None, limit=None): """Calls out to Glance for a list of detailed image information.""" filtered = [] + filters = filters or {} + if 'is_public' not in filters: + # NOTE(vish): don't filter out private images + filters['is_public'] = 'none' image_metas = self.client.get_images_detailed(filters=filters, marker=marker, limit=limit) diff --git a/nova/image/s3.py b/nova/image/s3.py index 9e95bd698..c313c7a13 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -102,18 +102,7 @@ class S3ImageService(service.BaseImageService): key.get_contents_to_filename(local_filename) return local_filename - def _s3_create(self, context, metadata): - """Gets a manifext from s3 and makes an image.""" - - image_path = tempfile.mkdtemp(dir=FLAGS.image_decryption_dir) - - image_location = metadata['properties']['image_location'] - bucket_name = image_location.split('/')[0] - manifest_path = image_location[len(bucket_name) + 1:] - bucket = self._conn(context).get_bucket(bucket_name) - key = bucket.get_key(manifest_path) - manifest = key.get_contents_as_string() - + def _s3_parse_manifest(self, context, metadata, manifest): manifest = ElementTree.fromstring(manifest) image_format = 'ami' image_type = 'machine' @@ -141,6 +130,28 @@ class S3ImageService(service.BaseImageService): except Exception: arch = 'x86_64' + # NOTE(yamahata): + # EC2 ec2-budlne-image --block-device-mapping accepts + # <virtual name>=<device name> where + # virtual name = {ami, root, swap, ephemeral<N>} + # where N is no negative integer + # device name = the device name seen by guest kernel. + # They are converted into + # block_device_mapping/mapping/{virtual, device} + # + # Do NOT confuse this with ec2-register's block device mapping + # argument. + mappings = [] + try: + block_device_mapping = manifest.findall('machine_configuration/' + 'block_device_mapping/' + 'mapping') + for bdm in block_device_mapping: + mappings.append({'virtual': bdm.find('virtual').text, + 'device': bdm.find('device').text}) + except Exception: + mappings = [] + properties = metadata['properties'] properties['project_id'] = context.project_id properties['architecture'] = arch @@ -151,13 +162,31 @@ class S3ImageService(service.BaseImageService): if ramdisk_id: properties['ramdisk_id'] = ec2utils.ec2_id_to_id(ramdisk_id) + if mappings: + properties['mappings'] = mappings + metadata.update({'disk_format': image_format, 'container_format': image_format, 'status': 'queued', - 'is_public': True, + 'is_public': False, 'properties': properties}) metadata['properties']['image_state'] = 'pending' image = self.service.create(context, metadata) + return manifest, image + + def _s3_create(self, context, metadata): + """Gets a manifext from s3 and makes an image.""" + + image_path = tempfile.mkdtemp(dir=FLAGS.image_decryption_dir) + + image_location = metadata['properties']['image_location'] + bucket_name = image_location.split('/')[0] + manifest_path = image_location[len(bucket_name) + 1:] + bucket = self._conn(context).get_bucket(bucket_name) + key = bucket.get_key(manifest_path) + manifest = key.get_contents_as_string() + + manifest, image = self._s3_parse_manifest(context, metadata, manifest) image_id = image['id'] def delayed_create(): diff --git a/nova/network/api.py b/nova/network/api.py index dd3ed1709..33a9fe239 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -18,11 +18,9 @@ """Handles all requests relating to instances (guest vms).""" -from nova import db from nova import exception from nova import flags from nova import log as logging -from nova import quota from nova import rpc from nova.db import base @@ -39,7 +37,7 @@ class API(base.Base): return dict(rv.iteritems()) def get_floating_ip_by_ip(self, context, address): - res = self.db.floating_ip_get_by_ip(context, address) + res = self.db.floating_ip_get_by_address(context, address) return dict(res.iteritems()) def list_floating_ips(self, context): @@ -47,13 +45,12 @@ class API(base.Base): context.project_id) return ips + def get_vifs_by_instance(self, context, instance_id): + vifs = self.db.virtual_interface_get_by_instance(context, instance_id) + return vifs + def allocate_floating_ip(self, context): - if quota.allowed_floating_ips(context, 1) < 1: - LOG.warn(_('Quota exceeeded for %s, tried to allocate ' - 'address'), - context.project_id) - raise quota.QuotaError(_('Address quota exceeded. You cannot ' - 'allocate any more addresses')) + """Adds a floating ip to a project.""" # NOTE(vish): We don't know which network host should get the ip # when we allocate, so just send it to any one. This # will probably need to move into a network supervisor @@ -65,7 +62,11 @@ class API(base.Base): def release_floating_ip(self, context, address, affect_auto_assigned=False): + """Removes floating ip with address from a project.""" floating_ip = self.db.floating_ip_get_by_address(context, address) + if floating_ip['fixed_ip']: + raise exception.ApiError(_('Floating ip is in use. ' + 'Disassociate it before releasing.')) if not affect_auto_assigned and floating_ip.get('auto_assigned'): return # NOTE(vish): We don't know which network host should get the ip @@ -78,8 +79,19 @@ class API(base.Base): 'args': {'floating_address': floating_ip['address']}}) def associate_floating_ip(self, context, floating_ip, fixed_ip, - affect_auto_assigned=False): - if isinstance(fixed_ip, str) or isinstance(fixed_ip, unicode): + affect_auto_assigned=False): + """Associates a floating ip with a fixed ip. + + ensures floating ip is allocated to the project in context + + :param fixed_ip: is either fixed_ip object or a string fixed ip address + :param floating_ip: is a string floating ip address + """ + # NOTE(tr3buchet): i don't like the "either or" argument type + # funcationility but i've left it alone for now + # TODO(tr3buchet): this function needs to be rewritten to move + # the network related db lookups into the network host code + if isinstance(fixed_ip, basestring): fixed_ip = self.db.fixed_ip_get_by_address(context, fixed_ip) floating_ip = self.db.floating_ip_get_by_address(context, floating_ip) if not affect_auto_assigned and floating_ip.get('auto_assigned'): @@ -99,9 +111,11 @@ class API(base.Base): '(%(project)s)') % {'address': floating_ip['address'], 'project': context.project_id}) - # NOTE(vish): Perhaps we should just pass this on to compute and - # let compute communicate with network. - host = fixed_ip['network']['host'] + # NOTE(vish): if we are multi_host, send to the instances host + if fixed_ip['network']['multi_host']: + host = fixed_ip['instance']['host'] + else: + host = fixed_ip['network']['host'] rpc.cast(context, self.db.queue_get_for(context, FLAGS.network_topic, host), {'method': 'associate_floating_ip', @@ -110,15 +124,73 @@ class API(base.Base): def disassociate_floating_ip(self, context, address, affect_auto_assigned=False): + """Disassociates a floating ip from fixed ip it is associated with.""" floating_ip = self.db.floating_ip_get_by_address(context, address) if not affect_auto_assigned and floating_ip.get('auto_assigned'): return if not floating_ip.get('fixed_ip'): raise exception.ApiError('Address is not associated.') - # NOTE(vish): Get the topic from the host name of the network of - # the associated fixed ip. - host = floating_ip['fixed_ip']['network']['host'] - rpc.cast(context, + # NOTE(vish): if we are multi_host, send to the instances host + if floating_ip['fixed_ip']['network']['multi_host']: + host = floating_ip['fixed_ip']['instance']['host'] + else: + host = floating_ip['fixed_ip']['network']['host'] + rpc.call(context, self.db.queue_get_for(context, FLAGS.network_topic, host), {'method': 'disassociate_floating_ip', 'args': {'floating_address': floating_ip['address']}}) + + def allocate_for_instance(self, context, instance, **kwargs): + """Allocates all network structures for an instance. + + :returns: network info as from get_instance_nw_info() below + """ + args = kwargs + args['instance_id'] = instance['id'] + args['project_id'] = instance['project_id'] + args['host'] = instance['host'] + args['instance_type_id'] = instance['instance_type_id'] + + return rpc.call(context, FLAGS.network_topic, + {'method': 'allocate_for_instance', + 'args': args}) + + def deallocate_for_instance(self, context, instance, **kwargs): + """Deallocates all network structures related to instance.""" + args = kwargs + args['instance_id'] = instance['id'] + args['project_id'] = instance['project_id'] + rpc.cast(context, FLAGS.network_topic, + {'method': 'deallocate_for_instance', + 'args': args}) + + def add_fixed_ip_to_instance(self, context, instance_id, network_id): + """Adds a fixed ip to instance from specified network.""" + args = {'instance_id': instance_id, + 'network_id': network_id} + rpc.cast(context, FLAGS.network_topic, + {'method': 'add_fixed_ip_to_instance', + 'args': args}) + + def remove_fixed_ip_from_instance(self, context, instance_id, address): + """Removes a fixed ip from instance from specified network.""" + args = {'instance_id': instance_id, + 'address': address} + rpc.cast(context, FLAGS.network_topic, + {'method': 'remove_fixed_ip_from_instance', + 'args': args}) + + def add_network_to_project(self, context, project_id): + """Force adds another network to a project.""" + rpc.cast(context, FLAGS.network_topic, + {'method': 'add_network_to_project', + 'args': {'project_id': project_id}}) + + def get_instance_nw_info(self, context, instance): + """Returns all network info related to an instance.""" + args = {'instance_id': instance['id'], + 'instance_type_id': instance['instance_type_id'], + 'host': instance['host']} + return rpc.call(context, FLAGS.network_topic, + {'method': 'get_instance_nw_info', + 'args': args}) diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 6c5a6f1ce..8ace07884 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -451,20 +451,21 @@ def floating_forward_rules(floating_ip, fixed_ip): '-s %s -j SNAT --to %s' % (fixed_ip, floating_ip))] -def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None): +def ensure_vlan_bridge(vlan_num, bridge, bridge_interface, net_attrs=None): """Create a vlan and bridge unless they already exist.""" - interface = ensure_vlan(vlan_num) + interface = ensure_vlan(vlan_num, bridge_interface) ensure_bridge(bridge, interface, net_attrs) + return interface @utils.synchronized('ensure_vlan', external=True) -def ensure_vlan(vlan_num): +def ensure_vlan(vlan_num, bridge_interface): """Create a vlan unless it already exists.""" interface = 'vlan%s' % vlan_num if not _device_exists(interface): LOG.debug(_('Starting VLAN inteface %s'), interface) _execute('sudo', 'vconfig', 'set_name_type', 'VLAN_PLUS_VID_NO_PAD') - _execute('sudo', 'vconfig', 'add', FLAGS.vlan_interface, vlan_num) + _execute('sudo', 'vconfig', 'add', bridge_interface, vlan_num) _execute('sudo', 'ip', 'link', 'set', interface, 'up') return interface @@ -497,7 +498,7 @@ def ensure_bridge(bridge, interface, net_attrs=None): suffix = net_attrs['cidr'].rpartition('/')[2] out, err = _execute('sudo', 'ip', 'addr', 'add', '%s/%s' % - (net_attrs['gateway'], suffix), + (net_attrs['dhcp_server'], suffix), 'brd', net_attrs['broadcast'], 'dev', @@ -551,21 +552,27 @@ def ensure_bridge(bridge, interface, net_attrs=None): bridge) -def get_dhcp_leases(context, network_id): +def get_dhcp_leases(context, network_ref): """Return a network's hosts config in dnsmasq leasefile format.""" hosts = [] - for fixed_ip_ref in db.network_get_associated_fixed_ips(context, - network_id): - hosts.append(_host_lease(fixed_ip_ref)) + for fixed_ref in db.network_get_associated_fixed_ips(context, + network_ref['id']): + host = fixed_ref['instance']['host'] + if network_ref['multi_host'] and FLAGS.host != host: + continue + hosts.append(_host_lease(fixed_ref)) return '\n'.join(hosts) -def get_dhcp_hosts(context, network_id): +def get_dhcp_hosts(context, network_ref): """Get network's hosts config in dhcp-host format.""" hosts = [] - for fixed_ip_ref in db.network_get_associated_fixed_ips(context, - network_id): - hosts.append(_host_dhcp(fixed_ip_ref)) + for fixed_ref in db.network_get_associated_fixed_ips(context, + network_ref['id']): + host = fixed_ref['instance']['host'] + if network_ref['multi_host'] and FLAGS.host != host: + continue + hosts.append(_host_dhcp(fixed_ref)) return '\n'.join(hosts) @@ -573,18 +580,16 @@ def get_dhcp_hosts(context, network_id): # configuration options (like dchp-range, vlan, ...) # aren't reloaded. @utils.synchronized('dnsmasq_start') -def update_dhcp(context, network_id): +def update_dhcp(context, network_ref): """(Re)starts a dnsmasq server for a given network. If a dnsmasq instance is already running then send a HUP signal causing it to reload, otherwise spawn a new instance. """ - network_ref = db.network_get(context, network_id) - conffile = _dhcp_file(network_ref['bridge'], 'conf') with open(conffile, 'w') as f: - f.write(get_dhcp_hosts(context, network_id)) + f.write(get_dhcp_hosts(context, network_ref)) # Make sure dnsmasq can actually read it (it setuid()s to "nobody") os.chmod(conffile, 0644) @@ -612,9 +617,7 @@ def update_dhcp(context, network_id): @utils.synchronized('radvd_start') -def update_ra(context, network_id): - network_ref = db.network_get(context, network_id) - +def update_ra(context, network_ref): conffile = _ra_file(network_ref['bridge'], 'conf') with open(conffile, 'w') as f: conf_str = """ @@ -650,9 +653,6 @@ interface %s LOG.debug(_('Pid %d is stale, relaunching radvd'), pid) command = _ra_cmd(network_ref) _execute(*command) - db.network_update(context, network_id, - {'gateway_v6': - utils.get_my_linklocal(network_ref['bridge'])}) def _host_lease(fixed_ip_ref): @@ -666,7 +666,7 @@ def _host_lease(fixed_ip_ref): seconds_since_epoch = calendar.timegm(timestamp.utctimetuple()) return '%d %s %s %s *' % (seconds_since_epoch + FLAGS.dhcp_lease_time, - instance_ref['mac_address'], + fixed_ip_ref['virtual_interface']['address'], fixed_ip_ref['address'], instance_ref['hostname'] or '*') @@ -674,7 +674,7 @@ def _host_lease(fixed_ip_ref): def _host_dhcp(fixed_ip_ref): """Return a host string for an address in dhcp-host format.""" instance_ref = fixed_ip_ref['instance'] - return '%s,%s.%s,%s' % (instance_ref['mac_address'], + return '%s,%s.%s,%s' % (fixed_ip_ref['virtual_interface']['address'], instance_ref['hostname'], FLAGS.dhcp_domain, fixed_ip_ref['address']) @@ -701,10 +701,11 @@ def _dnsmasq_cmd(net): cmd = ['sudo', '-E', 'dnsmasq', '--strict-order', '--bind-interfaces', + '--interface=%s' % net['bridge'], '--conf-file=%s' % FLAGS.dnsmasq_config_file, '--domain=%s' % FLAGS.dhcp_domain, '--pid-file=%s' % _dhcp_file(net['bridge'], 'pid'), - '--listen-address=%s' % net['gateway'], + '--listen-address=%s' % net['dhcp_server'], '--except-interface=lo', '--dhcp-range=%s,static,120s' % net['dhcp_start'], '--dhcp-lease-max=%s' % len(netaddr.IPNetwork(net['cidr'])), diff --git a/nova/network/manager.py b/nova/network/manager.py index bf0456522..6f7573f66 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -28,7 +28,6 @@ topologies. All of the network commands are issued to a subclass of :flat_network_bridge: Bridge device for simple network instances :flat_interface: FlatDhcp will bridge into this interface if set :flat_network_dns: Dns for simple network -:flat_network_dhcp_start: Dhcp start for FlatDhcp :vlan_start: First VLAN for private networks :vpn_ip: Public IP for the cloudpipe VPN servers :vpn_start: First Vpn port for private networks @@ -40,6 +39,8 @@ topologies. All of the network commands are issued to a subclass of is disassociated :fixed_ip_disassociate_timeout: Seconds after which a deallocated ip is disassociated +:create_unique_mac_address_attempts: Number of times to attempt creating + a unique mac address """ @@ -47,15 +48,20 @@ import datetime import math import netaddr import socket +from eventlet import greenpool from nova import context from nova import db from nova import exception from nova import flags +from nova import ipv6 from nova import log as logging from nova import manager +from nova import quota from nova import utils from nova import rpc +from nova.network import api as network_api +import random LOG = logging.getLogger("nova.network.manager") @@ -70,15 +76,15 @@ flags.DEFINE_bool('flat_injected', True, 'Whether to attempt to inject network setup into guest') flags.DEFINE_string('flat_interface', None, 'FlatDhcp will bridge into this interface if set') -flags.DEFINE_string('flat_network_dhcp_start', '10.0.0.2', - 'Dhcp start for FlatDhcp') flags.DEFINE_integer('vlan_start', 100, 'First VLAN for private networks') -flags.DEFINE_string('vlan_interface', 'eth0', - 'network device for vlans') +flags.DEFINE_string('vlan_interface', None, + 'vlans will bridge into this interface if set') flags.DEFINE_integer('num_networks', 1, 'Number of networks to support') flags.DEFINE_string('vpn_ip', '$my_ip', 'Public IP for the cloudpipe VPN servers') flags.DEFINE_integer('vpn_start', 1000, 'First Vpn port for private networks') +flags.DEFINE_bool('multi_host', False, + 'Default value for multi_host in networks') flags.DEFINE_integer('network_size', 256, 'Number of addresses in each private subnet') flags.DEFINE_string('floating_range', '4.4.4.0/24', @@ -94,7 +100,10 @@ flags.DEFINE_bool('update_dhcp_on_disassociate', False, 'Whether to update dhcp when fixed_ip is disassociated') flags.DEFINE_integer('fixed_ip_disassociate_timeout', 600, 'Seconds after which a deallocated ip is disassociated') - +flags.DEFINE_integer('create_unique_mac_address_attempts', 5, + 'Number of attempts to create unique mac address') +flags.DEFINE_bool('auto_assign_floating_ip', False, + 'Autoassigning floating ip to VM') flags.DEFINE_bool('use_ipv6', False, 'use the ipv6') flags.DEFINE_string('network_host', socket.gethostname(), @@ -108,41 +117,235 @@ class AddressAlreadyAllocated(exception.Error): pass +class RPCAllocateFixedIP(object): + """Mixin class originally for FlatDCHP and VLAN network managers. + + used since they share code to RPC.call allocate_fixed_ip on the + correct network host to configure dnsmasq + """ + def _allocate_fixed_ips(self, context, instance_id, host, networks, + **kwargs): + """Calls allocate_fixed_ip once for each network.""" + green_pool = greenpool.GreenPool() + + vpn = kwargs.pop('vpn') + for network in networks: + # NOTE(vish): if we are not multi_host pass to the network host + if not network['multi_host']: + host = network['host'] + # NOTE(vish): if there is no network host, set one + if host == None: + host = rpc.call(context, FLAGS.network_topic, + {'method': 'set_network_host', + 'args': {'network_ref': network}}) + if host != self.host: + # need to call allocate_fixed_ip to correct network host + topic = self.db.queue_get_for(context, + FLAGS.network_topic, + host) + args = {} + args['instance_id'] = instance_id + args['network_id'] = network['id'] + args['vpn'] = vpn + + green_pool.spawn_n(rpc.call, context, topic, + {'method': '_rpc_allocate_fixed_ip', + 'args': args}) + else: + # i am the correct host, run here + self.allocate_fixed_ip(context, instance_id, network, vpn=vpn) + + # wait for all of the allocates (if any) to finish + green_pool.waitall() + + def _rpc_allocate_fixed_ip(self, context, instance_id, network_id, + **kwargs): + """Sits in between _allocate_fixed_ips and allocate_fixed_ip to + perform network lookup on the far side of rpc. + """ + network = self.db.network_get(context, network_id) + self.allocate_fixed_ip(context, instance_id, network, **kwargs) + + +class FloatingIP(object): + """Mixin class for adding floating IP functionality to a manager.""" + def init_host_floating_ips(self): + """Configures floating ips owned by host.""" + + admin_context = context.get_admin_context() + try: + floating_ips = self.db.floating_ip_get_all_by_host(admin_context, + self.host) + except exception.NotFound: + return + + for floating_ip in floating_ips: + if floating_ip.get('fixed_ip', None): + fixed_address = floating_ip['fixed_ip']['address'] + # NOTE(vish): The False here is because we ignore the case + # that the ip is already bound. + self.driver.bind_floating_ip(floating_ip['address'], False) + self.driver.ensure_floating_forward(floating_ip['address'], + fixed_address) + + def allocate_for_instance(self, context, **kwargs): + """Handles allocating the floating IP resources for an instance. + + calls super class allocate_for_instance() as well + + rpc.called by network_api + """ + instance_id = kwargs.get('instance_id') + project_id = kwargs.get('project_id') + LOG.debug(_("floating IP allocation for instance |%s|"), instance_id, + context=context) + # call the next inherited class's allocate_for_instance() + # which is currently the NetworkManager version + # do this first so fixed ip is already allocated + ips = super(FloatingIP, self).allocate_for_instance(context, **kwargs) + if FLAGS.auto_assign_floating_ip: + # allocate a floating ip (public_ip is just the address string) + public_ip = self.allocate_floating_ip(context, project_id) + # set auto_assigned column to true for the floating ip + self.db.floating_ip_set_auto_assigned(context, public_ip) + # get the floating ip object from public_ip string + floating_ip = self.db.floating_ip_get_by_address(context, + public_ip) + + # get the first fixed_ip belonging to the instance + fixed_ips = self.db.fixed_ip_get_by_instance(context, instance_id) + fixed_ip = fixed_ips[0] if fixed_ips else None + + # call to correct network host to associate the floating ip + self.network_api.associate_floating_ip(context, + floating_ip, + fixed_ip, + affect_auto_assigned=True) + return ips + + def deallocate_for_instance(self, context, **kwargs): + """Handles deallocating floating IP resources for an instance. + + calls super class deallocate_for_instance() as well. + + rpc.called by network_api + """ + instance_id = kwargs.get('instance_id') + LOG.debug(_("floating IP deallocation for instance |%s|"), instance_id, + context=context) + + fixed_ips = self.db.fixed_ip_get_by_instance(context, instance_id) + # add to kwargs so we can pass to super to save a db lookup there + kwargs['fixed_ips'] = fixed_ips + for fixed_ip in fixed_ips: + # disassociate floating ips related to fixed_ip + for floating_ip in fixed_ip.floating_ips: + address = floating_ip['address'] + self.network_api.disassociate_floating_ip(context, address) + # deallocate if auto_assigned + if floating_ip['auto_assigned']: + self.network_api.release_floating_ip(context, + address, + True) + + # call the next inherited class's deallocate_for_instance() + # which is currently the NetworkManager version + # call this after so floating IPs are handled first + super(FloatingIP, self).deallocate_for_instance(context, **kwargs) + + def allocate_floating_ip(self, context, project_id): + """Gets an floating ip from the pool.""" + # NOTE(tr3buchet): all networks hosts in zone now use the same pool + LOG.debug("QUOTA: %s" % quota.allowed_floating_ips(context, 1)) + if quota.allowed_floating_ips(context, 1) < 1: + LOG.warn(_('Quota exceeded for %s, tried to allocate ' + 'address'), + context.project_id) + raise quota.QuotaError(_('Address quota exceeded. You cannot ' + 'allocate any more addresses')) + # TODO(vish): add floating ips through manage command + return self.db.floating_ip_allocate_address(context, + project_id) + + def associate_floating_ip(self, context, floating_address, fixed_address): + """Associates an floating ip to a fixed ip.""" + self.db.floating_ip_fixed_ip_associate(context, + floating_address, + fixed_address) + self.driver.bind_floating_ip(floating_address) + self.driver.ensure_floating_forward(floating_address, fixed_address) + + def disassociate_floating_ip(self, context, floating_address): + """Disassociates a floating ip.""" + fixed_address = self.db.floating_ip_disassociate(context, + floating_address) + self.driver.unbind_floating_ip(floating_address) + self.driver.remove_floating_forward(floating_address, fixed_address) + + def deallocate_floating_ip(self, context, floating_address): + """Returns an floating ip to the pool.""" + self.db.floating_ip_deallocate(context, floating_address) + + class NetworkManager(manager.SchedulerDependentManager): """Implements common network manager functionality. This class must be subclassed to support specific topologies. + host management: + hosts configure themselves for networks they are assigned to in the + table upon startup. If there are networks in the table which do not + have hosts, those will be filled in and have hosts configured + as the hosts pick them up one at time during their periodic task. + The one at a time part is to flatten the layout to help scale """ + # If True, this manager requires VIF to create a bridge. + SHOULD_CREATE_BRIDGE = False + + # If True, this manager requires VIF to create VLAN tag. + SHOULD_CREATE_VLAN = False + timeout_fixed_ips = True def __init__(self, network_driver=None, *args, **kwargs): if not network_driver: network_driver = FLAGS.network_driver self.driver = utils.import_object(network_driver) + self.network_api = network_api.API() super(NetworkManager, self).__init__(service_name='network', *args, **kwargs) + @utils.synchronized('get_dhcp') + def _get_dhcp_ip(self, context, network_ref, host=None): + """Get the proper dhcp address to listen on.""" + # NOTE(vish): this is for compatibility + if not network_ref['multi_host']: + return network_ref['gateway'] + + if not host: + host = self.host + network_id = network_ref['id'] + try: + fip = self.db.fixed_ip_get_by_network_host(context, + network_id, + host) + return fip['address'] + except exception.FixedIpNotFoundForNetworkHost: + elevated = context.elevated() + return self.db.fixed_ip_associate_pool(elevated, + network_id, + host=host) + def init_host(self): - """Do any initialization for a standalone service.""" - self.driver.init_host() - self.driver.ensure_metadata_ip() - # Set up networking for the projects for which we're already - # the designated network host. + """Do any initialization that needs to be run if this is a + standalone service. + """ + # NOTE(vish): Set up networks for which this host already has + # an ip address. ctxt = context.get_admin_context() - for network in self.db.host_get_networks(ctxt, self.host): - self._on_set_network_host(ctxt, network['id']) - floating_ips = self.db.floating_ip_get_all_by_host(ctxt, - self.host) - for floating_ip in floating_ips: - if floating_ip.get('fixed_ip', None): - fixed_address = floating_ip['fixed_ip']['address'] - # NOTE(vish): The False here is because we ignore the case - # that the ip is already bound. - self.driver.bind_floating_ip(floating_ip['address'], False) - self.driver.ensure_floating_forward(floating_ip['address'], - fixed_address) + for network in self.db.network_get_all_by_host(ctxt, self.host): + self._setup_network(ctxt, network) def periodic_tasks(self, context=None): """Tasks to be run at a periodic interval.""" @@ -157,148 +360,263 @@ class NetworkManager(manager.SchedulerDependentManager): if num: LOG.debug(_('Dissassociated %s stale fixed ip(s)'), num) - def set_network_host(self, context, network_id): + def set_network_host(self, context, network_ref): """Safely sets the host of the network.""" LOG.debug(_('setting network host'), context=context) host = self.db.network_set_host(context, - network_id, + network_ref['id'], self.host) - self._on_set_network_host(context, network_id) return host - def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): + def _get_networks_for_instance(self, context, instance_id, project_id): + """Determine & return which networks an instance should connect to.""" + # TODO(tr3buchet) maybe this needs to be updated in the future if + # there is a better way to determine which networks + # a non-vlan instance should connect to + try: + networks = self.db.network_get_all(context) + except exception.NoNetworksFound: + return [] + + # return only networks which are not vlan networks + return [network for network in networks if + not network['vlan']] + + def allocate_for_instance(self, context, **kwargs): + """Handles allocating the various network resources for an instance. + + rpc.called by network_api + """ + instance_id = kwargs.pop('instance_id') + host = kwargs.pop('host') + project_id = kwargs.pop('project_id') + type_id = kwargs.pop('instance_type_id') + vpn = kwargs.pop('vpn') + admin_context = context.elevated() + LOG.debug(_("network allocations for instance %s"), instance_id, + context=context) + networks = self._get_networks_for_instance(admin_context, instance_id, + project_id) + LOG.warn(networks) + self._allocate_mac_addresses(context, instance_id, networks) + self._allocate_fixed_ips(admin_context, instance_id, host, networks, + vpn=vpn) + return self.get_instance_nw_info(context, instance_id, type_id, host) + + def deallocate_for_instance(self, context, **kwargs): + """Handles deallocating various network resources for an instance. + + rpc.called by network_api + kwargs can contain fixed_ips to circumvent another db lookup + """ + instance_id = kwargs.pop('instance_id') + fixed_ips = kwargs.get('fixed_ips') or \ + self.db.fixed_ip_get_by_instance(context, instance_id) + LOG.debug(_("network deallocation for instance |%s|"), instance_id, + context=context) + # deallocate fixed ips + for fixed_ip in fixed_ips: + self.deallocate_fixed_ip(context, fixed_ip['address'], **kwargs) + + # deallocate vifs (mac addresses) + self.db.virtual_interface_delete_by_instance(context, instance_id) + + def get_instance_nw_info(self, context, instance_id, + instance_type_id, host): + """Creates network info list for instance. + + called by allocate_for_instance and netowrk_api + context needs to be elevated + :returns: network info list [(network,info),(network,info)...] + where network = dict containing pertinent data from a network db object + and info = dict containing pertinent networking data + """ + # TODO(tr3buchet) should handle floating IPs as well? + try: + fixed_ips = self.db.fixed_ip_get_by_instance(context, instance_id) + except exception.FixedIpNotFoundForInstance: + LOG.warn(_('No fixed IPs for instance %s'), instance_id) + fixed_ips = [] + + vifs = self.db.virtual_interface_get_by_instance(context, instance_id) + flavor = self.db.instance_type_get(context, instance_type_id) + network_info = [] + # a vif has an address, instance_id, and network_id + # it is also joined to the instance and network given by those IDs + for vif in vifs: + network = vif['network'] + + # determine which of the instance's IPs belong to this network + network_IPs = [fixed_ip['address'] for fixed_ip in fixed_ips if + fixed_ip['network_id'] == network['id']] + + # TODO(tr3buchet) eventually "enabled" should be determined + def ip_dict(ip): + return { + "ip": ip, + "netmask": network["netmask"], + "enabled": "1"} + + def ip6_dict(): + return { + "ip": ipv6.to_global(network['cidr_v6'], + vif['address'], + network['project_id']), + "netmask": network['netmask_v6'], + "enabled": "1"} + network_dict = { + 'bridge': network['bridge'], + 'id': network['id'], + 'cidr': network['cidr'], + 'cidr_v6': network['cidr_v6'], + 'injected': network['injected'], + 'vlan': network['vlan'], + 'bridge_interface': network['bridge_interface'], + 'multi_host': network['multi_host']} + if network['multi_host']: + dhcp_server = self._get_dhcp_ip(context, network, host) + else: + dhcp_server = self._get_dhcp_ip(context, + network, + network['host']) + info = { + 'label': network['label'], + 'gateway': network['gateway'], + 'dhcp_server': dhcp_server, + 'broadcast': network['broadcast'], + 'mac': vif['address'], + 'rxtx_cap': flavor['rxtx_cap'], + 'dns': [], + 'ips': [ip_dict(ip) for ip in network_IPs], + 'should_create_bridge': self.SHOULD_CREATE_BRIDGE, + 'should_create_vlan': self.SHOULD_CREATE_VLAN} + + if network['cidr_v6']: + info['ip6s'] = [ip6_dict()] + # TODO(tr3buchet): handle ip6 routes here as well + if network['gateway_v6']: + info['gateway6'] = network['gateway_v6'] + if network['dns1']: + info['dns'].append(network['dns1']) + if network['dns2']: + info['dns'].append(network['dns2']) + + network_info.append((network_dict, info)) + return network_info + + def _allocate_mac_addresses(self, context, instance_id, networks): + """Generates mac addresses and creates vif rows in db for them.""" + for network in networks: + vif = {'address': self.generate_mac_address(), + 'instance_id': instance_id, + 'network_id': network['id']} + # try FLAG times to create a vif record with a unique mac_address + for i in range(FLAGS.create_unique_mac_address_attempts): + try: + self.db.virtual_interface_create(context, vif) + break + except exception.VirtualInterfaceCreateException: + vif['address'] = self.generate_mac_address() + else: + self.db.virtual_interface_delete_by_instance(context, + instance_id) + raise exception.VirtualInterfaceMacAddressException() + + def generate_mac_address(self): + """Generate a mac address for a vif on an instance.""" + mac = [0x02, 0x16, 0x3e, + random.randint(0x00, 0x7f), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff)] + return ':'.join(map(lambda x: "%02x" % x, mac)) + + def add_fixed_ip_to_instance(self, context, instance_id, host, network_id): + """Adds a fixed ip to an instance from specified network.""" + networks = [self.db.network_get(context, network_id)] + self._allocate_fixed_ips(context, instance_id, host, networks) + + def remove_fixed_ip_from_instance(self, context, instance_id, address): + """Removes a fixed ip from an instance from specified network.""" + fixed_ips = self.db.fixed_ip_get_by_instance(context, instance_id) + for fixed_ip in fixed_ips: + if fixed_ip['address'] == address: + self.deallocate_fixed_ip(context, address) + return + raise exception.FixedIpNotFoundForSpecificInstance( + instance_id=instance_id, ip=address) + + def allocate_fixed_ip(self, context, instance_id, network, **kwargs): """Gets a fixed ip from the pool.""" # TODO(vish): when this is called by compute, we can associate compute # with a network, or a cluster of computes with a network # and use that network here with a method like # network_get_by_compute_host - network_ref = self.db.network_get_by_bridge(context.elevated(), - FLAGS.flat_network_bridge) address = self.db.fixed_ip_associate_pool(context.elevated(), - network_ref['id'], + network['id'], instance_id) - self.db.fixed_ip_update(context, address, {'allocated': True}) + vif = self.db.virtual_interface_get_by_instance_and_network(context, + instance_id, + network['id']) + values = {'allocated': True, + 'virtual_interface_id': vif['id']} + self.db.fixed_ip_update(context, address, values) + self._setup_network(context, network) return address - def deallocate_fixed_ip(self, context, address, *args, **kwargs): + def deallocate_fixed_ip(self, context, address, **kwargs): """Returns a fixed ip to the pool.""" - self.db.fixed_ip_update(context, address, {'allocated': False}) - self.db.fixed_ip_disassociate(context.elevated(), address) - - def setup_fixed_ip(self, context, address): - """Sets up rules for fixed ip.""" - raise NotImplementedError() - - def _on_set_network_host(self, context, network_id): - """Called when this host becomes the host for a network.""" - raise NotImplementedError() - - def setup_compute_network(self, context, instance_id): - """Sets up matching network for compute hosts.""" - raise NotImplementedError() - - def allocate_floating_ip(self, context, project_id): - """Gets an floating ip from the pool.""" - # TODO(vish): add floating ips through manage command - return self.db.floating_ip_allocate_address(context, - self.host, - project_id) - - def associate_floating_ip(self, context, floating_address, fixed_address): - """Associates an floating ip to a fixed ip.""" - self.db.floating_ip_fixed_ip_associate(context, - floating_address, - fixed_address) - self.driver.bind_floating_ip(floating_address) - self.driver.ensure_floating_forward(floating_address, fixed_address) + self.db.fixed_ip_update(context, address, + {'allocated': False, + 'virtual_interface_id': None}) - def disassociate_floating_ip(self, context, floating_address): - """Disassociates a floating ip.""" - fixed_address = self.db.floating_ip_disassociate(context, - floating_address) - self.driver.unbind_floating_ip(floating_address) - self.driver.remove_floating_forward(floating_address, fixed_address) - - def deallocate_floating_ip(self, context, floating_address): - """Returns an floating ip to the pool.""" - self.db.floating_ip_deallocate(context, floating_address) - - def lease_fixed_ip(self, context, mac, address): + def lease_fixed_ip(self, context, address): """Called by dhcp-bridge when ip is leased.""" - LOG.debug(_('Leasing IP %s'), address, context=context) - fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) - instance_ref = fixed_ip_ref['instance'] - if not instance_ref: + LOG.debug(_('Leased IP |%(address)s|'), locals(), context=context) + fixed_ip = self.db.fixed_ip_get_by_address(context, address) + instance = fixed_ip['instance'] + if not instance: raise exception.Error(_('IP %s leased that is not associated') % address) - if instance_ref['mac_address'] != mac: - inst_addr = instance_ref['mac_address'] - raise exception.Error(_('IP %(address)s leased to bad mac' - ' %(inst_addr)s vs %(mac)s') % locals()) now = utils.utcnow() self.db.fixed_ip_update(context, - fixed_ip_ref['address'], + fixed_ip['address'], {'leased': True, 'updated_at': now}) - if not fixed_ip_ref['allocated']: - LOG.warn(_('IP %s leased that was already deallocated'), address, + if not fixed_ip['allocated']: + LOG.warn(_('IP |%s| leased that isn\'t allocated'), address, context=context) - def release_fixed_ip(self, context, mac, address): + def release_fixed_ip(self, context, address): """Called by dhcp-bridge when ip is released.""" - LOG.debug(_('Releasing IP %s'), address, context=context) - fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) - instance_ref = fixed_ip_ref['instance'] - if not instance_ref: + LOG.debug(_('Released IP |%(address)s|'), locals(), context=context) + fixed_ip = self.db.fixed_ip_get_by_address(context, address) + instance = fixed_ip['instance'] + if not instance: raise exception.Error(_('IP %s released that is not associated') % address) - if instance_ref['mac_address'] != mac: - inst_addr = instance_ref['mac_address'] - raise exception.Error(_('IP %(address)s released from bad mac' - ' %(inst_addr)s vs %(mac)s') % locals()) - if not fixed_ip_ref['leased']: + if not fixed_ip['leased']: LOG.warn(_('IP %s released that was not leased'), address, context=context) self.db.fixed_ip_update(context, - fixed_ip_ref['address'], + fixed_ip['address'], {'leased': False}) - if not fixed_ip_ref['allocated']: + if not fixed_ip['allocated']: self.db.fixed_ip_disassociate(context, address) # NOTE(vish): dhcp server isn't updated until next setup, this # means there will stale entries in the conf file # the code below will update the file if necessary if FLAGS.update_dhcp_on_disassociate: network_ref = self.db.fixed_ip_get_network(context, address) - self.driver.update_dhcp(context, network_ref['id']) - - def get_network_host(self, context): - """Get the network host for the current context.""" - network_ref = self.db.network_get_by_bridge(context, - FLAGS.flat_network_bridge) - # NOTE(vish): If the network has no host, use the network_host flag. - # This could eventually be a a db lookup of some sort, but - # a flag is easy to handle for now. - host = network_ref['host'] - if not host: - topic = self.db.queue_get_for(context, - FLAGS.network_topic, - FLAGS.network_host) - if FLAGS.fake_call: - return self.set_network_host(context, network_ref['id']) - host = rpc.call(context, - FLAGS.network_topic, - {'method': 'set_network_host', - 'args': {'network_id': network_ref['id']}}) - return host + self._setup_network(context, network_ref) - def create_networks(self, context, cidr, num_networks, network_size, - cidr_v6, gateway_v6, label, *args, **kwargs): + def create_networks(self, context, label, cidr, multi_host, num_networks, + network_size, cidr_v6, gateway_v6, bridge, + bridge_interface, dns1=None, dns2=None, **kwargs): """Create networks based on parameters.""" fixed_net = netaddr.IPNetwork(cidr) fixed_net_v6 = netaddr.IPNetwork(cidr_v6) significant_bits_v6 = 64 network_size_v6 = 1 << 64 - count = 1 for index in range(num_networks): start = index * network_size start_v6 = index * network_size_v6 @@ -306,20 +624,22 @@ class NetworkManager(manager.SchedulerDependentManager): cidr = '%s/%s' % (fixed_net[start], significant_bits) project_net = netaddr.IPNetwork(cidr) net = {} - net['bridge'] = FLAGS.flat_network_bridge - net['dns'] = FLAGS.flat_network_dns + net['bridge'] = bridge + net['bridge_interface'] = bridge_interface + net['dns1'] = dns1 + net['dns2'] = dns2 net['cidr'] = cidr + net['multi_host'] = multi_host net['netmask'] = str(project_net.netmask) - net['gateway'] = str(list(project_net)[1]) + net['gateway'] = str(project_net[1]) net['broadcast'] = str(project_net.broadcast) - net['dhcp_start'] = str(list(project_net)[2]) + net['dhcp_start'] = str(project_net[2]) if num_networks > 1: - net['label'] = '%s_%d' % (label, count) + net['label'] = '%s_%d' % (label, index) else: net['label'] = label - count += 1 - if(FLAGS.use_ipv6): + if FLAGS.use_ipv6: cidr_v6 = '%s/%s' % (fixed_net_v6[start_v6], significant_bits_v6) net['cidr_v6'] = cidr_v6 @@ -328,16 +648,34 @@ class NetworkManager(manager.SchedulerDependentManager): if gateway_v6: # use a pre-defined gateway if one is provided - net['gateway_v6'] = str(list(gateway_v6)[1]) + net['gateway_v6'] = str(gateway_v6) else: - net['gateway_v6'] = str(list(project_net_v6)[1]) + net['gateway_v6'] = str(project_net_v6[1]) net['netmask_v6'] = str(project_net_v6._prefixlen) - network_ref = self.db.network_create_safe(context, net) - - if network_ref: - self._create_fixed_ips(context, network_ref['id']) + if kwargs.get('vpn', False): + # this bit here is for vlan-manager + del net['dns1'] + del net['dns2'] + vlan = kwargs['vlan_start'] + index + net['vpn_private_address'] = str(project_net[2]) + net['dhcp_start'] = str(project_net[3]) + net['vlan'] = vlan + net['bridge'] = 'br%s' % vlan + + # NOTE(vish): This makes ports unique accross the cloud, a more + # robust solution would be to make them uniq per ip + net['vpn_public_port'] = kwargs['vpn_start'] + index + + # None if network with cidr or cidr_v6 already exists + network = self.db.network_create_safe(context, net) + + if network: + self._create_fixed_ips(context, network['id']) + else: + raise ValueError(_('Network with cidr %s already exists') % + cidr) @property def _bottom_reserved_ips(self): # pylint: disable=R0201 @@ -351,12 +689,12 @@ class NetworkManager(manager.SchedulerDependentManager): def _create_fixed_ips(self, context, network_id): """Create all fixed ips for network.""" - network_ref = self.db.network_get(context, network_id) + network = self.db.network_get(context, network_id) # NOTE(vish): Should these be properties of the network as opposed # to properties of the manager class? bottom_reserved = self._bottom_reserved_ips top_reserved = self._top_reserved_ips - project_net = netaddr.IPNetwork(network_ref['cidr']) + project_net = netaddr.IPNetwork(network['cidr']) num_ips = len(project_net) for index in range(num_ips): address = str(project_net[index]) @@ -368,6 +706,15 @@ class NetworkManager(manager.SchedulerDependentManager): 'address': address, 'reserved': reserved}) + def _allocate_fixed_ips(self, context, instance_id, host, networks, + **kwargs): + """Calls allocate_fixed_ip once for each network.""" + raise NotImplementedError() + + def _setup_network(self, context, network_ref): + """Sets up network on this host.""" + raise NotImplementedError() + class FlatManager(NetworkManager): """Basic network where no vlans are used. @@ -399,93 +746,64 @@ class FlatManager(NetworkManager): timeout_fixed_ips = False - def init_host(self): - """Do any initialization for a standalone service.""" - #Fix for bug 723298 - do not call init_host on superclass - #Following code has been copied for NetworkManager.init_host - ctxt = context.get_admin_context() - for network in self.db.host_get_networks(ctxt, self.host): - self._on_set_network_host(ctxt, network['id']) + def _allocate_fixed_ips(self, context, instance_id, host, networks, + **kwargs): + """Calls allocate_fixed_ip once for each network.""" + for network in networks: + self.allocate_fixed_ip(context, instance_id, network) - def setup_compute_network(self, context, instance_id): - """Network is created manually.""" - pass + def deallocate_fixed_ip(self, context, address, **kwargs): + """Returns a fixed ip to the pool.""" + super(FlatManager, self).deallocate_fixed_ip(context, address, + **kwargs) + self.db.fixed_ip_disassociate(context, address) - def _on_set_network_host(self, context, network_id): - """Called when this host becomes the host for a network.""" + def _setup_network(self, context, network_ref): + """Setup Network on this host.""" net = {} net['injected'] = FLAGS.flat_injected - net['dns'] = FLAGS.flat_network_dns - self.db.network_update(context, network_id, net) - - def allocate_floating_ip(self, context, project_id): - #Fix for bug 723298 - raise NotImplementedError() - - def associate_floating_ip(self, context, floating_address, fixed_address): - #Fix for bug 723298 - raise NotImplementedError() - - def disassociate_floating_ip(self, context, floating_address): - #Fix for bug 723298 - raise NotImplementedError() - - def deallocate_floating_ip(self, context, floating_address): - #Fix for bug 723298 - raise NotImplementedError() + self.db.network_update(context, network_ref['id'], net) -class FlatDHCPManager(NetworkManager): +class FlatDHCPManager(FloatingIP, RPCAllocateFixedIP, NetworkManager): """Flat networking with dhcp. FlatDHCPManager will start up one dhcp server to give out addresses. - It never injects network settings into the guest. Otherwise it behaves - like FlatDHCPManager. + It never injects network settings into the guest. It also manages bridges. + Otherwise it behaves like FlatManager. """ - def init_host(self): - """Do any initialization for a standalone service.""" - super(FlatDHCPManager, self).init_host() - self.driver.metadata_forward() + SHOULD_CREATE_BRIDGE = True - def setup_compute_network(self, context, instance_id): - """Sets up matching network for compute hosts.""" - network_ref = db.network_get_by_instance(context, instance_id) - self.driver.ensure_bridge(network_ref['bridge'], - FLAGS.flat_interface) + def init_host(self): + """Do any initialization that needs to be run if this is a + standalone service. + """ + self.driver.init_host() + self.driver.ensure_metadata_ip() - def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): - """Setup dhcp for this network.""" - address = super(FlatDHCPManager, self).allocate_fixed_ip(context, - instance_id, - *args, - **kwargs) - network_ref = db.fixed_ip_get_network(context, address) - if not FLAGS.fake_network: - self.driver.update_dhcp(context, network_ref['id']) - return address + super(FlatDHCPManager, self).init_host() + self.init_host_floating_ips() - def deallocate_fixed_ip(self, context, address, *args, **kwargs): - """Returns a fixed ip to the pool.""" - self.db.fixed_ip_update(context, address, {'allocated': False}) + self.driver.metadata_forward() - def _on_set_network_host(self, context, network_id): - """Called when this host becomes the host for a project.""" - net = {} - net['dhcp_start'] = FLAGS.flat_network_dhcp_start - self.db.network_update(context, network_id, net) - network_ref = db.network_get(context, network_id) + def _setup_network(self, context, network_ref): + """Sets up network on this host.""" + network_ref['dhcp_server'] = self._get_dhcp_ip(context, network_ref) self.driver.ensure_bridge(network_ref['bridge'], - FLAGS.flat_interface, + network_ref['bridge_interface'], network_ref) if not FLAGS.fake_network: - self.driver.update_dhcp(context, network_id) + self.driver.update_dhcp(context, network_ref) if(FLAGS.use_ipv6): - self.driver.update_ra(context, network_id) + self.driver.update_ra(context, network_ref) + gateway = utils.get_my_linklocal(network_ref['bridge']) + self.db.network_update(context, network_ref['id'], + {'gateway_v6': gateway}) -class VlanManager(NetworkManager): +class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): """Vlan network with dhcp. VlanManager is the most complicated. It will create a host-managed @@ -500,141 +818,98 @@ class VlanManager(NetworkManager): """ + SHOULD_CREATE_BRIDGE = True + SHOULD_CREATE_VLAN = True + def init_host(self): - """Do any initialization for a standalone service.""" - super(VlanManager, self).init_host() + """Do any initialization that needs to be run if this is a + standalone service. + """ + + self.driver.init_host() + self.driver.ensure_metadata_ip() + + NetworkManager.init_host(self) + self.init_host_floating_ips() + self.driver.metadata_forward() - def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): + def allocate_fixed_ip(self, context, instance_id, network, **kwargs): """Gets a fixed ip from the pool.""" - # TODO(vish): This should probably be getting project_id from - # the instance, but it is another trip to the db. - # Perhaps this method should take an instance_ref. - ctxt = context.elevated() - network_ref = self.db.project_get_network(ctxt, - context.project_id) if kwargs.get('vpn', None): - address = network_ref['vpn_private_address'] - self.db.fixed_ip_associate(ctxt, + address = network['vpn_private_address'] + self.db.fixed_ip_associate(context, address, instance_id) else: - address = self.db.fixed_ip_associate_pool(ctxt, - network_ref['id'], + address = self.db.fixed_ip_associate_pool(context, + network['id'], instance_id) - self.db.fixed_ip_update(context, address, {'allocated': True}) - if not FLAGS.fake_network: - self.driver.update_dhcp(context, network_ref['id']) + + vif = self.db.virtual_interface_get_by_instance_and_network(context, + instance_id, + network['id']) + values = {'allocated': True, + 'virtual_interface_id': vif['id']} + self.db.fixed_ip_update(context, address, values) + self._setup_network(context, network) return address - def deallocate_fixed_ip(self, context, address, *args, **kwargs): - """Returns a fixed ip to the pool.""" - self.db.fixed_ip_update(context, address, {'allocated': False}) + def add_network_to_project(self, context, project_id): + """Force adds another network to a project.""" + self.db.network_associate(context, project_id, force=True) - def setup_compute_network(self, context, instance_id): - """Sets up matching network for compute hosts.""" - network_ref = db.network_get_by_instance(context, instance_id) - self.driver.ensure_vlan_bridge(network_ref['vlan'], - network_ref['bridge']) + def _get_networks_for_instance(self, context, instance_id, project_id): + """Determine which networks an instance should connect to.""" + # get networks associated with project + return self.db.project_get_networks(context, project_id) - def create_networks(self, context, cidr, num_networks, network_size, - cidr_v6, vlan_start, vpn_start, **kwargs): + def create_networks(self, context, **kwargs): """Create networks based on parameters.""" # Check that num_networks + vlan_start is not > 4094, fixes lp708025 - if num_networks + vlan_start > 4094: + if kwargs['num_networks'] + kwargs['vlan_start'] > 4094: raise ValueError(_('The sum between the number of networks and' ' the vlan start cannot be greater' ' than 4094')) - fixed_net = netaddr.IPNetwork(cidr) - if len(fixed_net) < num_networks * network_size: + # check that num networks and network size fits in fixed_net + fixed_net = netaddr.IPNetwork(kwargs['cidr']) + if len(fixed_net) < kwargs['num_networks'] * kwargs['network_size']: raise ValueError(_('The network range is not big enough to fit ' - '%(num_networks)s. Network size is %(network_size)s' % - locals())) + '%(num_networks)s. Network size is %(network_size)s') % + kwargs) - fixed_net_v6 = netaddr.IPNetwork(cidr_v6) - network_size_v6 = 1 << 64 - significant_bits_v6 = 64 - for index in range(num_networks): - vlan = vlan_start + index - start = index * network_size - start_v6 = index * network_size_v6 - significant_bits = 32 - int(math.log(network_size, 2)) - cidr = "%s/%s" % (fixed_net[start], significant_bits) - project_net = netaddr.IPNetwork(cidr) - net = {} - net['cidr'] = cidr - net['netmask'] = str(project_net.netmask) - net['gateway'] = str(list(project_net)[1]) - net['broadcast'] = str(project_net.broadcast) - net['vpn_private_address'] = str(list(project_net)[2]) - net['dhcp_start'] = str(list(project_net)[3]) - net['vlan'] = vlan - net['bridge'] = 'br%s' % vlan - if(FLAGS.use_ipv6): - cidr_v6 = '%s/%s' % (fixed_net_v6[start_v6], - significant_bits_v6) - net['cidr_v6'] = cidr_v6 - - # NOTE(vish): This makes ports unique accross the cloud, a more - # robust solution would be to make them unique per ip - net['vpn_public_port'] = vpn_start + index - network_ref = None - try: - network_ref = db.network_get_by_cidr(context, cidr) - except exception.NotFound: - pass - - if network_ref is not None: - raise ValueError(_('Network with cidr %s already exists' % - cidr)) - - network_ref = self.db.network_create_safe(context, net) - if network_ref: - self._create_fixed_ips(context, network_ref['id']) - - def get_network_host(self, context): - """Get the network for the current context.""" - network_ref = self.db.project_get_network(context.elevated(), - context.project_id) - # NOTE(vish): If the network has no host, do a call to get an - # available host. This should be changed to go through - # the scheduler at some point. - host = network_ref['host'] - if not host: - if FLAGS.fake_call: - return self.set_network_host(context, network_ref['id']) - host = rpc.call(context, - FLAGS.network_topic, - {'method': 'set_network_host', - 'args': {'network_id': network_ref['id']}}) - - return host + NetworkManager.create_networks(self, context, vpn=True, **kwargs) - def _on_set_network_host(self, context, network_id): - """Called when this host becomes the host for a network.""" - network_ref = self.db.network_get(context, network_id) + def _setup_network(self, context, network_ref): + """Sets up network on this host.""" if not network_ref['vpn_public_address']: net = {} address = FLAGS.vpn_ip net['vpn_public_address'] = address - db.network_update(context, network_id, net) + network_ref = db.network_update(context, network_ref['id'], net) else: address = network_ref['vpn_public_address'] + network_ref['dhcp_server'] = self._get_dhcp_ip(context, network_ref) self.driver.ensure_vlan_bridge(network_ref['vlan'], network_ref['bridge'], + network_ref['bridge_interface'], network_ref) # NOTE(vish): only ensure this forward if the address hasn't been set # manually. - if address == FLAGS.vpn_ip: + if address == FLAGS.vpn_ip and hasattr(self.driver, + "ensure_vlan_forward"): self.driver.ensure_vlan_forward(FLAGS.vpn_ip, network_ref['vpn_public_port'], network_ref['vpn_private_address']) if not FLAGS.fake_network: - self.driver.update_dhcp(context, network_id) + self.driver.update_dhcp(context, network_ref) if(FLAGS.use_ipv6): - self.driver.update_ra(context, network_id) + self.driver.update_ra(context, network_ref) + gateway = utils.get_my_linklocal(network_ref['bridge']) + self.db.network_update(context, network_ref['id'], + {'gateway_v6': gateway}) @property def _bottom_reserved_ips(self): diff --git a/nova/network/vmwareapi_net.py b/nova/network/vmwareapi_net.py deleted file mode 100644 index 04210c011..000000000 --- a/nova/network/vmwareapi_net.py +++ /dev/null @@ -1,82 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 Citrix Systems, Inc. -# Copyright 2011 OpenStack LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Implements vlans for vmwareapi.""" - -from nova import db -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils -from nova.virt.vmwareapi_conn import VMWareAPISession -from nova.virt.vmwareapi import network_utils - - -LOG = logging.getLogger("nova.network.vmwareapi_net") - - -FLAGS = flags.FLAGS -FLAGS['vlan_interface'].SetDefault('vmnic0') - - -def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None): - """Create a vlan and bridge unless they already exist.""" - # Open vmwareapi session - host_ip = FLAGS.vmwareapi_host_ip - host_username = FLAGS.vmwareapi_host_username - host_password = FLAGS.vmwareapi_host_password - if not host_ip or host_username is None or host_password is None: - raise Exception(_('Must specify vmwareapi_host_ip, ' - 'vmwareapi_host_username ' - 'and vmwareapi_host_password to use ' - 'connection_type=vmwareapi')) - session = VMWareAPISession(host_ip, host_username, host_password, - FLAGS.vmwareapi_api_retry_count) - vlan_interface = FLAGS.vlan_interface - # Check if the vlan_interface physical network adapter exists on the host - if not network_utils.check_if_vlan_interface_exists(session, - vlan_interface): - raise exception.NetworkAdapterNotFound(adapter=vlan_interface) - - # Get the vSwitch associated with the Physical Adapter - vswitch_associated = network_utils.get_vswitch_for_vlan_interface( - session, vlan_interface) - if vswitch_associated is None: - raise exception.SwicthNotFoundForNetworkAdapter(adapter=vlan_interface) - # Check whether bridge already exists and retrieve the the ref of the - # network whose name_label is "bridge" - network_ref = network_utils.get_network_with_the_name(session, bridge) - if network_ref is None: - # Create a port group on the vSwitch associated with the vlan_interface - # corresponding physical network adapter on the ESX host - network_utils.create_port_group(session, bridge, vswitch_associated, - vlan_num) - else: - # Get the vlan id and vswitch corresponding to the port group - pg_vlanid, pg_vswitch = \ - network_utils.get_vlanid_and_vswitch_for_portgroup(session, bridge) - - # Check if the vswitch associated is proper - if pg_vswitch != vswitch_associated: - raise exception.InvalidVLANPortGroup(bridge=bridge, - expected=vswitch_associated, - actual=pg_vswitch) - - # Check if the vlan id is proper for the port group - if pg_vlanid != vlan_num: - raise exception.InvalidVLANTag(bridge=bridge, tag=vlan_num, - pgroup=pg_vlanid) diff --git a/nova/network/xenapi_net.py b/nova/network/xenapi_net.py deleted file mode 100644 index af295a4f8..000000000 --- a/nova/network/xenapi_net.py +++ /dev/null @@ -1,87 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 Citrix Systems, Inc. -# Copyright 2011 OpenStack LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Implements vlans, bridges, and iptables rules using linux utilities.""" - -import os - -from nova import db -from nova import exception -from nova import flags -from nova import log as logging -from nova import utils -from nova.virt import xenapi_conn -from nova.virt.xenapi import network_utils - - -LOG = logging.getLogger("nova.xenapi_net") - - -FLAGS = flags.FLAGS - - -def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None): - """Create a vlan and bridge unless they already exist.""" - # Open xenapi session - LOG.debug('ENTERING ensure_vlan_bridge in xenapi net') - url = FLAGS.xenapi_connection_url - username = FLAGS.xenapi_connection_username - password = FLAGS.xenapi_connection_password - session = xenapi_conn.XenAPISession(url, username, password) - # Check whether bridge already exists - # Retrieve network whose name_label is "bridge" - network_ref = network_utils.NetworkHelper.find_network_with_name_label( - session, - bridge) - if network_ref is None: - # If bridge does not exists - # 1 - create network - description = 'network for nova bridge %s' % bridge - network_rec = {'name_label': bridge, - 'name_description': description, - 'other_config': {}} - network_ref = session.call_xenapi('network.create', network_rec) - # 2 - find PIF for VLAN - # NOTE(salvatore-orlando): using double quotes inside single quotes - # as xapi filter only support tokens in double quotes - expr = 'field "device" = "%s" and \ - field "VLAN" = "-1"' % FLAGS.vlan_interface - pifs = session.call_xenapi('PIF.get_all_records_where', expr) - pif_ref = None - # Multiple PIF are ok: we are dealing with a pool - if len(pifs) == 0: - raise Exception( - _('Found no PIF for device %s') % FLAGS.vlan_interface) - # 3 - create vlan for network - for pif_ref in pifs.keys(): - session.call_xenapi('VLAN.create', - pif_ref, - str(vlan_num), - network_ref) - else: - # Check VLAN tag is appropriate - network_rec = session.call_xenapi('network.get_record', network_ref) - # Retrieve PIFs from network - for pif_ref in network_rec['PIFs']: - # Retrieve VLAN from PIF - pif_rec = session.call_xenapi('PIF.get_record', pif_ref) - pif_vlan = int(pif_rec['VLAN']) - # Raise an exception if VLAN != vlan_num - if pif_vlan != vlan_num: - raise Exception(_("PIF %(pif_rec['uuid'])s for network " - "%(bridge)s has VLAN id %(pif_vlan)d. " - "Expected %(vlan_num)d") % locals()) diff --git a/nova/notifier/api.py b/nova/notifier/api.py index d49517c8b..98969fd3e 100644 --- a/nova/notifier/api.py +++ b/nova/notifier/api.py @@ -17,7 +17,9 @@ import uuid from nova import flags from nova import utils +from nova import log as logging +LOG = logging.getLogger('nova.exception') FLAGS = flags.FLAGS @@ -37,6 +39,12 @@ class BadPriorityException(Exception): pass +def publisher_id(service, host=None): + if not host: + host = FLAGS.host + return "%s.%s" % (service, host) + + def notify(publisher_id, event_type, priority, payload): """ Sends a notification using the specified driver @@ -79,4 +87,8 @@ def notify(publisher_id, event_type, priority, payload): priority=priority, payload=payload, timestamp=str(utils.utcnow())) - driver.notify(msg) + try: + driver.notify(msg) + except Exception, e: + LOG.exception(_("Problem '%(e)s' attempting to " + "send to notification system." % locals())) diff --git a/nova/notifier/test_notifier.py b/nova/notifier/test_notifier.py new file mode 100644 index 000000000..d43f43e48 --- /dev/null +++ b/nova/notifier/test_notifier.py @@ -0,0 +1,28 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from nova import flags +from nova import log as logging + +FLAGS = flags.FLAGS + +NOTIFICATIONS = [] + + +def notify(message): + """Test notifier, stores notifications in memory for unittests.""" + NOTIFICATIONS.append(message) diff --git a/nova/rpc.py b/nova/rpc.py index 2e78a31e7..e2771ca88 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -219,7 +219,7 @@ class AdapterConsumer(Consumer): return self.pool.spawn_n(self._process_data, msg_id, ctxt, method, args) - @exception.wrap_exception + @exception.wrap_exception() def _process_data(self, msg_id, ctxt, method, args): """Thread that maigcally looks for a method on the proxy object and calls it. @@ -275,6 +275,11 @@ class FanoutAdapterConsumer(AdapterConsumer): unique = uuid.uuid4().hex self.queue = '%s_fanout_%s' % (topic, unique) self.durable = False + # Fanout creates unique queue names, so we should auto-remove + # them when done, so they're not left around on restart. + # Also, we're the only one that should be consuming. exclusive + # implies auto_delete, so we'll just set that.. + self.exclusive = True LOG.info(_('Created "%(exchange)s" fanout exchange ' 'with "%(key)s" routing key'), dict(exchange=self.exchange, key=self.routing_key)) @@ -355,6 +360,7 @@ class FanoutPublisher(Publisher): self.exchange = '%s_fanout' % topic self.queue = '%s_fanout' % topic self.durable = False + self.auto_delete = True LOG.info(_('Creating "%(exchange)s" fanout exchange'), dict(exchange=self.exchange)) super(FanoutPublisher, self).__init__(connection=connection) diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py index 1bb047e2e..137b671c0 100644 --- a/nova/scheduler/api.py +++ b/nova/scheduler/api.py @@ -51,6 +51,11 @@ def _call_scheduler(method, context, params=None): return rpc.call(context, queue, kwargs) +def get_host_list(context): + """Return a list of hosts associated with this zone.""" + return _call_scheduler('get_host_list', context) + + def get_zone_list(context): """Return a list of zones assoicated with this zone.""" items = _call_scheduler('get_zone_list', context) @@ -114,7 +119,8 @@ def _process(func, zone): def call_zone_method(context, method_name, errors_to_ignore=None, - novaclient_collection_name='zones', *args, **kwargs): + novaclient_collection_name='zones', zones=None, + *args, **kwargs): """Returns a list of (zone, call_result) objects.""" if not isinstance(errors_to_ignore, (list, tuple)): # This will also handle the default None @@ -122,7 +128,9 @@ def call_zone_method(context, method_name, errors_to_ignore=None, pool = greenpool.GreenPool() results = [] - for zone in db.zone_get_all(context): + if zones is None: + zones = db.zone_get_all(context) + for zone in zones: try: nova = novaclient.OpenStack(zone.username, zone.password, None, zone.api_url) @@ -162,32 +170,53 @@ def child_zone_helper(zone_list, func): _wrap_method(_process, func), zone_list)] -def _issue_novaclient_command(nova, zone, collection, method_name, item_id): +def _issue_novaclient_command(nova, zone, collection, + method_name, *args, **kwargs): """Use novaclient to issue command to a single child zone. - One of these will be run in parallel for each child zone.""" + One of these will be run in parallel for each child zone. + """ manager = getattr(nova, collection) - result = None - try: + + # NOTE(comstud): This is not ideal, but we have to do this based on + # how novaclient is implemented right now. + # 'find' is special cased as novaclient requires kwargs for it to + # filter on a 'get_all'. + # Every other method first needs to do a 'get' on the first argument + # passed, which should be a UUID. If it's 'get' itself that we want, + # we just return the result. Otherwise, we next call the real method + # that's wanted... passing other arguments that may or may not exist. + if method_name in ['find', 'findall']: try: - result = manager.get(int(item_id)) - except ValueError, e: - result = manager.find(name=item_id) + return getattr(manager, method_name)(**kwargs) + except novaclient.NotFound: + url = zone.api_url + LOG.debug(_("%(collection)s.%(method_name)s didn't find " + "anything matching '%(kwargs)s' on '%(url)s'" % + locals())) + return None + + args = list(args) + # pop off the UUID to look up + item = args.pop(0) + try: + result = manager.get(item) except novaclient.NotFound: url = zone.api_url - LOG.debug(_("%(collection)s '%(item_id)s' not found on '%(url)s'" % + LOG.debug(_("%(collection)s '%(item)s' not found on '%(url)s'" % locals())) return None - if method_name.lower() not in ['get', 'find']: - result = getattr(result, method_name)() + if method_name.lower() != 'get': + # if we're doing something other than 'get', call it passing args. + result = getattr(result, method_name)(*args, **kwargs) return result -def wrap_novaclient_function(f, collection, method_name, item_id): - """Appends collection, method_name and item_id to the incoming +def wrap_novaclient_function(f, collection, method_name, *args, **kwargs): + """Appends collection, method_name and arguments to the incoming (nova, zone) call from child_zone_helper.""" def inner(nova, zone): - return f(nova, zone, collection, method_name, item_id) + return f(nova, zone, collection, method_name, *args, **kwargs) return inner @@ -220,7 +249,7 @@ class reroute_compute(object): the wrapped method. (This ensures that zone-local code can continue to use integer IDs). - 4. If the item was not found, we delgate the call to a child zone + 4. If the item was not found, we delegate the call to a child zone using the UUID. """ def __init__(self, method_name): diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index 0b257c5d8..1bfa7740a 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -31,6 +31,7 @@ from nova import rpc from nova import utils from nova.compute import power_state + FLAGS = flags.FLAGS flags.DEFINE_integer('service_down_time', 60, 'maximum time since last checkin for up service') @@ -129,8 +130,7 @@ class Scheduler(object): # Checking instance is running. if (power_state.RUNNING != instance_ref['state'] or \ 'running' != instance_ref['state_description']): - ec2_id = instance_ref['hostname'] - raise exception.InstanceNotRunning(instance_id=ec2_id) + raise exception.InstanceNotRunning(instance_id=instance_ref['id']) # Checing volume node is running when any volumes are mounted # to the instance. @@ -168,9 +168,9 @@ class Scheduler(object): # and dest is not same. src = instance_ref['host'] if dest == src: - ec2_id = instance_ref['hostname'] - raise exception.UnableToMigrateToSelf(instance_id=ec2_id, - host=dest) + raise exception.UnableToMigrateToSelf( + instance_id=instance_ref['id'], + host=dest) # Checking dst host still has enough capacities. self.assert_compute_node_has_enough_resources(context, @@ -245,7 +245,7 @@ class Scheduler(object): """ # Getting instance information - ec2_id = instance_ref['hostname'] + hostname = instance_ref['hostname'] # Getting host information service_refs = db.service_get_all_compute_by_host(context, dest) @@ -256,8 +256,9 @@ class Scheduler(object): mem_avail = mem_total - mem_used mem_inst = instance_ref['memory_mb'] if mem_avail <= mem_inst: - reason = _("Unable to migrate %(ec2_id)s to destination: %(dest)s " - "(host:%(mem_avail)s <= instance:%(mem_inst)s)") + reason = _("Unable to migrate %(hostname)s to destination: " + "%(dest)s (host:%(mem_avail)s <= instance:" + "%(mem_inst)s)") raise exception.MigrationError(reason=reason % locals()) def mounted_on_same_shared_storage(self, context, instance_ref, dest): diff --git a/nova/scheduler/host_filter.py b/nova/scheduler/host_filter.py index 5f61f9456..b7bbbbcb8 100644 --- a/nova/scheduler/host_filter.py +++ b/nova/scheduler/host_filter.py @@ -251,8 +251,7 @@ class JsonFilter(HostFilter): required_disk = instance_type['local_gb'] query = ['and', ['>=', '$compute.host_memory_free', required_ram], - ['>=', '$compute.disk_available', required_disk], - ] + ['>=', '$compute.disk_available', required_disk]] return (self._full_name(), json.dumps(query)) def _parse_string(self, string, host, services): @@ -329,8 +328,9 @@ class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): 'instance_type': <InstanceType dict>} """ - def filter_hosts(self, num, request_spec): + def filter_hosts(self, topic, request_spec, hosts=None): """Filter the full host list (from the ZoneManager)""" + filter_name = request_spec.get('filter', None) host_filter = choose_host_filter(filter_name) @@ -341,8 +341,9 @@ class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler): name, query = host_filter.instance_type_to_filter(instance_type) return host_filter.filter_hosts(self.zone_manager, query) - def weigh_hosts(self, num, request_spec, hosts): + def weigh_hosts(self, topic, request_spec, hosts): """Derived classes must override this method and return a lists of hosts in [{weight, hostname}] format. """ - return [dict(weight=1, hostname=host) for host, caps in hosts] + return [dict(weight=1, hostname=hostname, capabilities=caps) + for hostname, caps in hosts] diff --git a/nova/scheduler/least_cost.py b/nova/scheduler/least_cost.py index 629fe2e42..6f5eb66fd 100644 --- a/nova/scheduler/least_cost.py +++ b/nova/scheduler/least_cost.py @@ -48,25 +48,43 @@ def noop_cost_fn(host): return 1 -flags.DEFINE_integer('fill_first_cost_fn_weight', 1, +flags.DEFINE_integer('compute_fill_first_cost_fn_weight', 1, 'How much weight to give the fill-first cost function') -def fill_first_cost_fn(host): +def compute_fill_first_cost_fn(host): """Prefer hosts that have less ram available, filter_hosts will exclude hosts that don't have enough ram""" hostname, caps = host - free_mem = caps['compute']['host_memory_free'] + free_mem = caps['host_memory_free'] return free_mem class LeastCostScheduler(zone_aware_scheduler.ZoneAwareScheduler): - def get_cost_fns(self): + def __init__(self, *args, **kwargs): + self.cost_fns_cache = {} + super(LeastCostScheduler, self).__init__(*args, **kwargs) + + def get_cost_fns(self, topic): """Returns a list of tuples containing weights and cost functions to use for weighing hosts """ + + if topic in self.cost_fns_cache: + return self.cost_fns_cache[topic] + cost_fns = [] for cost_fn_str in FLAGS.least_cost_scheduler_cost_functions: + if '.' in cost_fn_str: + short_name = cost_fn_str.split('.')[-1] + else: + short_name = cost_fn_str + cost_fn_str = "%s.%s.%s" % ( + __name__, self.__class__.__name__, short_name) + + if not (short_name.startswith('%s_' % topic) or + short_name.startswith('noop')): + continue try: # NOTE(sirp): import_class is somewhat misnamed since it can @@ -84,23 +102,23 @@ class LeastCostScheduler(zone_aware_scheduler.ZoneAwareScheduler): cost_fns.append((weight, cost_fn)) + self.cost_fns_cache[topic] = cost_fns return cost_fns - def weigh_hosts(self, num, request_spec, hosts): + def weigh_hosts(self, topic, request_spec, hosts): """Returns a list of dictionaries of form: - [ {weight: weight, hostname: hostname} ]""" - - # FIXME(sirp): weigh_hosts should handle more than just instances - hostnames = [hostname for hostname, caps in hosts] + [ {weight: weight, hostname: hostname, capabilities: capabs} ] + """ - cost_fns = self.get_cost_fns() + cost_fns = self.get_cost_fns(topic) costs = weighted_sum(domain=hosts, weighted_fns=cost_fns) weighted = [] weight_log = [] - for cost, hostname in zip(costs, hostnames): + for cost, (hostname, caps) in zip(costs, hosts): weight_log.append("%s: %s" % (hostname, "%.2f" % cost)) - weight_dict = dict(weight=cost, hostname=hostname) + weight_dict = dict(weight=cost, hostname=hostname, + capabilities=caps) weighted.append(weight_dict) LOG.debug(_("Weighted Costs => %s") % weight_log) @@ -127,7 +145,8 @@ def weighted_sum(domain, weighted_fns, normalize=True): weighted_fns - list of weights and functions like: [(weight, objective-functions)] - Returns an unsorted of scores. To pair with hosts do: zip(scores, hosts) + Returns an unsorted list of scores. To pair with hosts do: + zip(scores, hosts) """ # Table of form: # { domain1: [score1, score2, ..., scoreM] @@ -150,7 +169,6 @@ def weighted_sum(domain, weighted_fns, normalize=True): domain_scores = [] for idx in sorted(score_table): elem_score = sum(score_table[idx]) - elem = domain[idx] domain_scores.append(elem_score) return domain_scores diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index 6cb75aa8d..749d66cad 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -56,6 +56,10 @@ class SchedulerManager(manager.Manager): """Poll child zones periodically to get status.""" self.zone_manager.ping(context) + def get_host_list(self, context=None): + """Get a list of hosts from the ZoneManager.""" + return self.zone_manager.get_host_list() + def get_zone_list(self, context=None): """Get a list of zones from the ZoneManager.""" return self.zone_manager.get_zone_list() diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py index e7bff2faa..c429fdfcc 100644 --- a/nova/scheduler/zone_aware_scheduler.py +++ b/nova/scheduler/zone_aware_scheduler.py @@ -33,6 +33,7 @@ from nova import flags from nova import log as logging from nova import rpc +from nova.compute import api as compute_api from nova.scheduler import api from nova.scheduler import driver @@ -48,14 +49,25 @@ class InvalidBlob(exception.NovaException): class ZoneAwareScheduler(driver.Scheduler): """Base class for creating Zone Aware Schedulers.""" - def _call_zone_method(self, context, method, specs): + def _call_zone_method(self, context, method, specs, zones): """Call novaclient zone method. Broken out for testing.""" - return api.call_zone_method(context, method, specs=specs) + return api.call_zone_method(context, method, specs=specs, zones=zones) - def _provision_resource_locally(self, context, item, instance_id, kwargs): + def _provision_resource_locally(self, context, build_plan_item, + request_spec, kwargs): """Create the requested resource in this Zone.""" - host = item['hostname'] + host = build_plan_item['hostname'] + base_options = request_spec['instance_properties'] + + # TODO(sandy): I guess someone needs to add block_device_mapping + # support at some point? Also, OS API has no concept of security + # groups. + instance = compute_api.API().create_db_entry_for_new_instance(context, + base_options, None, []) + + instance_id = instance['id'] kwargs['instance_id'] = instance_id + rpc.cast(context, db.queue_get_for(context, "compute", host), {"method": "run_instance", @@ -115,8 +127,8 @@ class ZoneAwareScheduler(driver.Scheduler): nova.servers.create(name, image_ref, flavor_id, ipgroup, meta, files, child_blob, reservation_id=reservation_id) - def _provision_resource_from_blob(self, context, item, instance_id, - request_spec, kwargs): + def _provision_resource_from_blob(self, context, build_plan_item, + instance_id, request_spec, kwargs): """Create the requested resource locally or in a child zone based on what is stored in the zone blob info. @@ -132,12 +144,12 @@ class ZoneAwareScheduler(driver.Scheduler): request.""" host_info = None - if "blob" in item: + if "blob" in build_plan_item: # Request was passed in from above. Is it for us? - host_info = self._decrypt_blob(item['blob']) - elif "child_blob" in item: + host_info = self._decrypt_blob(build_plan_item['blob']) + elif "child_blob" in build_plan_item: # Our immediate child zone provided this info ... - host_info = item + host_info = build_plan_item if not host_info: raise InvalidBlob() @@ -147,19 +159,46 @@ class ZoneAwareScheduler(driver.Scheduler): self._ask_child_zone_to_create_instance(context, host_info, request_spec, kwargs) else: - self._provision_resource_locally(context, host_info, - instance_id, kwargs) + self._provision_resource_locally(context, host_info, request_spec, + kwargs) - def _provision_resource(self, context, item, instance_id, request_spec, - kwargs): + def _provision_resource(self, context, build_plan_item, instance_id, + request_spec, kwargs): """Create the requested resource in this Zone or a child zone.""" - if "hostname" in item: - self._provision_resource_locally(context, item, instance_id, - kwargs) + if "hostname" in build_plan_item: + self._provision_resource_locally(context, build_plan_item, + request_spec, kwargs) return - self._provision_resource_from_blob(context, item, instance_id, - request_spec, kwargs) + self._provision_resource_from_blob(context, build_plan_item, + instance_id, request_spec, kwargs) + + def _adjust_child_weights(self, child_results, zones): + """Apply the Scale and Offset values from the Zone definition + to adjust the weights returned from the child zones. Alters + child_results in place. + """ + for zone_id, result in child_results: + if not result: + continue + + assert isinstance(zone_id, int) + + for zone_rec in zones: + if zone_rec['id'] != zone_id: + continue + + for item in result: + try: + offset = zone_rec['weight_offset'] + scale = zone_rec['weight_scale'] + raw_weight = item['weight'] + cooked_weight = offset + scale * raw_weight + item['weight'] = cooked_weight + item['raw_weight'] = raw_weight + except KeyError: + LOG.exception(_("Bad child zone scaling values " + "for Zone: %(zone_id)s") % locals()) def schedule_run_instance(self, context, instance_id, request_spec, *args, **kwargs): @@ -180,18 +219,22 @@ class ZoneAwareScheduler(driver.Scheduler): request_spec, kwargs) return None + num_instances = request_spec.get('num_instances', 1) + LOG.debug(_("Attempting to build %(num_instances)d instance(s)") % + locals()) + # Create build plan and provision ... build_plan = self.select(context, request_spec) if not build_plan: raise driver.NoValidHost(_('No hosts were available')) - for num in xrange(request_spec['num_instances']): + for num in xrange(num_instances): if not build_plan: break - item = build_plan.pop(0) - self._provision_resource(context, item, instance_id, request_spec, - kwargs) + build_plan_item = build_plan.pop(0) + self._provision_resource(context, build_plan_item, instance_id, + request_spec, kwargs) # Returning None short-circuits the routing to Compute (since # we've already done it here) @@ -224,23 +267,43 @@ class ZoneAwareScheduler(driver.Scheduler): raise NotImplemented(_("Zone Aware Scheduler only understands " "Compute nodes (for now)")) - #TODO(sandy): how to infer this from OS API params? - num_instances = 1 - - # Filter local hosts based on requirements ... - host_list = self.filter_hosts(num_instances, request_spec) + num_instances = request_spec.get('num_instances', 1) + instance_type = request_spec['instance_type'] - # TODO(sirp): weigh_hosts should also be a function of 'topic' or - # resources, so that we can apply different objective functions to it + weighted = [] + host_list = None + + for i in xrange(num_instances): + # Filter local hosts based on requirements ... + # + # The first pass through here will pass 'None' as the + # host_list.. which tells the filter to build the full + # list of hosts. + # On a 2nd pass, the filter can modify the host_list with + # any updates it needs to make based on resources that + # may have been consumed from a previous build.. + host_list = self.filter_hosts(topic, request_spec, host_list) + if not host_list: + LOG.warn(_("Filter returned no hosts after processing " + "%(i)d of %(num_instances)d instances") % locals()) + break - # then weigh the selected hosts. - # weighted = [{weight=weight, name=hostname}, ...] - weighted = self.weigh_hosts(num_instances, request_spec, host_list) + # then weigh the selected hosts. + # weighted = [{weight=weight, hostname=hostname, + # capabilities=capabs}, ...] + weights = self.weigh_hosts(topic, request_spec, host_list) + weights.sort(key=operator.itemgetter('weight')) + best_weight = weights[0] + weighted.append(best_weight) + self.consume_resources(topic, best_weight['capabilities'], + instance_type) # Next, tack on the best weights from the child zones ... json_spec = json.dumps(request_spec) + all_zones = db.zone_get_all(context) child_results = self._call_zone_method(context, "select", - specs=json_spec) + specs=json_spec, zones=all_zones) + self._adjust_child_weights(child_results, all_zones) for child_zone, result in child_results: for weighting in result: # Remember the child_zone so we can get back to @@ -254,18 +317,65 @@ class ZoneAwareScheduler(driver.Scheduler): weighted.sort(key=operator.itemgetter('weight')) return weighted - def filter_hosts(self, num, request_spec): - """Derived classes must override this method and return - a list of hosts in [(hostname, capability_dict)] format. + def compute_filter(self, hostname, capabilities, request_spec): + """Return whether or not we can schedule to this compute node. + Derived classes should override this and return True if the host + is acceptable for scheduling. """ - # NOTE(sirp): The default logic is the equivalent to AllHostsFilter - service_states = self.zone_manager.service_states - return [(host, services) - for host, services in service_states.iteritems()] + instance_type = request_spec['instance_type'] + requested_mem = instance_type['memory_mb'] * 1024 * 1024 + return capabilities['host_memory_free'] >= requested_mem + + def filter_hosts(self, topic, request_spec, host_list=None): + """Return a list of hosts which are acceptable for scheduling. + Return value should be a list of (hostname, capability_dict)s. + Derived classes may override this, but may find the + '<topic>_filter' function more appropriate. + """ + + def _default_filter(self, hostname, capabilities, request_spec): + """Default filter function if there's no <topic>_filter""" + # NOTE(sirp): The default logic is the equivalent to + # AllHostsFilter + return True + + filter_func = getattr(self, '%s_filter' % topic, _default_filter) - def weigh_hosts(self, num, request_spec, hosts): + if host_list is None: + first_run = True + host_list = self.zone_manager.service_states.iteritems() + else: + first_run = False + + filtered_hosts = [] + for host, services in host_list: + if first_run: + if topic not in services: + continue + services = services[topic] + if filter_func(host, services, request_spec): + filtered_hosts.append((host, services)) + return filtered_hosts + + def weigh_hosts(self, topic, request_spec, hosts): """Derived classes may override this to provide more sophisticated scheduling objectives """ # NOTE(sirp): The default logic is the same as the NoopCostFunction - return [dict(weight=1, hostname=host) for host, caps in hosts] + return [dict(weight=1, hostname=hostname, capabilities=capabilities) + for hostname, capabilities in hosts] + + def compute_consume(self, capabilities, instance_type): + """Consume compute resources for selected host""" + + requested_mem = max(instance_type['memory_mb'], 0) * 1024 * 1024 + capabilities['host_memory_free'] -= requested_mem + + def consume_resources(self, topic, capabilities, instance_type): + """Consume resources for a specific host. 'host' is a tuple + of the hostname and the services""" + + consume_func = getattr(self, '%s_consume' % topic, None) + if not consume_func: + return + consume_func(capabilities, instance_type) diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py index ba7403c15..efdac06e1 100644 --- a/nova/scheduler/zone_manager.py +++ b/nova/scheduler/zone_manager.py @@ -115,6 +115,18 @@ class ZoneManager(object): """Return the list of zones we know about.""" return [zone.to_dict() for zone in self.zone_states.values()] + def get_host_list(self): + """Returns a list of dicts for each host that the Zone Manager + knows about. Each dict contains the host_name and the service + for that host. + """ + all_hosts = self.service_states.keys() + ret = [] + for host in self.service_states: + for svc in self.service_states[host]: + ret.append({"service": svc, "host_name": host}) + return ret + def get_zone_capabilities(self, context): """Roll up all the individual host info to generic 'service' capabilities. Each capability is aggregated into @@ -125,15 +137,30 @@ class ZoneManager(object): # But it's likely to change once we understand what the Best-Match # code will need better. combined = {} # { <service>_<cap> : (min, max), ... } + stale_host_services = {} # { host1 : [svc1, svc2], host2 :[svc1]} for host, host_dict in hosts_dict.iteritems(): for service_name, service_dict in host_dict.iteritems(): + if not service_dict.get("enabled", True): + # Service is disabled; do no include it + continue + + #Check if the service capabilities became stale + if self.host_service_caps_stale(host, service_name): + if host not in stale_host_services: + stale_host_services[host] = [] # Adding host key once + stale_host_services[host].append(service_name) + continue for cap, value in service_dict.iteritems(): + if cap == "timestamp": # Timestamp is not needed + continue key = "%s_%s" % (service_name, cap) min_value, max_value = combined.get(key, (value, value)) min_value = min(min_value, value) max_value = max(max_value, value) combined[key] = (min_value, max_value) + # Delete the expired host services + self.delete_expired_host_services(stale_host_services) return combined def _refresh_from_db(self, context): @@ -172,5 +199,24 @@ class ZoneManager(object): logging.debug(_("Received %(service_name)s service update from " "%(host)s: %(capabilities)s") % locals()) service_caps = self.service_states.get(host, {}) + capabilities["timestamp"] = utils.utcnow() # Reported time service_caps[service_name] = capabilities self.service_states[host] = service_caps + + def host_service_caps_stale(self, host, service): + """Check if host service capabilites are not recent enough.""" + allowed_time_diff = FLAGS.periodic_interval * 3 + caps = self.service_states[host][service] + if (utils.utcnow() - caps["timestamp"]) <= \ + datetime.timedelta(seconds=allowed_time_diff): + return False + return True + + def delete_expired_host_services(self, host_services_dict): + """Delete all the inactive host services information.""" + for host, services in host_services_dict.iteritems(): + service_caps = self.service_states[host] + for service in services: + del service_caps[service] + if len(service_caps) == 0: # Delete host if no services + del self.service_states[host] diff --git a/nova/test.py b/nova/test.py index ab1eaf5fd..9790b0aa1 100644 --- a/nova/test.py +++ b/nova/test.py @@ -30,11 +30,15 @@ import uuid import unittest import mox +import nose.plugins.skip +import nova.image.fake +import shutil import stubout from eventlet import greenthread from nova import fakerabbit from nova import flags +from nova import log from nova import rpc from nova import utils from nova import service @@ -47,6 +51,22 @@ flags.DEFINE_string('sqlite_clean_db', 'clean.sqlite', flags.DEFINE_bool('fake_tests', True, 'should we use everything for testing') +LOG = log.getLogger('nova.tests') + + +class skip_test(object): + """Decorator that skips a test.""" + def __init__(self, msg): + self.message = msg + + def __call__(self, func): + def _skipper(*args, **kw): + """Wrapped skipper function.""" + raise nose.SkipTest(self.message) + _skipper.__name__ = func.__name__ + _skipper.__doc__ = func.__doc__ + return _skipper + def skip_if_fake(func): """Decorator that skips a test if running in fake mode.""" @@ -100,6 +120,9 @@ class TestCase(unittest.TestCase): if hasattr(fake.FakeConnection, '_instance'): del fake.FakeConnection._instance + if FLAGS.image_service == 'nova.image.fake.FakeImageService': + nova.image.fake.FakeImageService_reset() + # Reset any overriden flags self.reset_flags() @@ -229,3 +252,15 @@ class TestCase(unittest.TestCase): for d1, d2 in zip(L1, L2): self.assertDictMatch(d1, d2, approx_equal=approx_equal, tolerance=tolerance) + + def assertSubDictMatch(self, sub_dict, super_dict): + """Assert a sub_dict is subset of super_dict.""" + self.assertTrue(set(sub_dict.keys()).issubset(set(super_dict.keys()))) + for k, sub_value in sub_dict.items(): + super_value = super_dict[k] + if isinstance(sub_value, dict): + self.assertSubDictMatch(sub_value, super_value) + elif 'DONTCARE' in (sub_value, super_value): + continue + else: + self.assertEqual(sub_value, super_value) diff --git a/nova/tests/__init__.py b/nova/tests/__init__.py index 5e0cb718e..720d5b0e6 100644 --- a/nova/tests/__init__.py +++ b/nova/tests/__init__.py @@ -42,6 +42,7 @@ def setup(): from nova import context from nova import flags + from nova import db from nova.db import migration from nova.network import manager as network_manager from nova.tests import fake_flags @@ -53,14 +54,23 @@ def setup(): return migration.db_sync() ctxt = context.get_admin_context() - network_manager.VlanManager().create_networks(ctxt, - FLAGS.fixed_range, - FLAGS.num_networks, - FLAGS.network_size, - FLAGS.fixed_range_v6, - FLAGS.vlan_start, - FLAGS.vpn_start, - ) + network = network_manager.VlanManager() + bridge_interface = FLAGS.flat_interface or FLAGS.vlan_interface + network.create_networks(ctxt, + label='test', + cidr=FLAGS.fixed_range, + multi_host=FLAGS.multi_host, + num_networks=FLAGS.num_networks, + network_size=FLAGS.network_size, + cidr_v6=FLAGS.fixed_range_v6, + gateway_v6=FLAGS.gateway_v6, + bridge=FLAGS.flat_network_bridge, + bridge_interface=bridge_interface, + vpn_start=FLAGS.vpn_start, + vlan_start=FLAGS.vlan_start, + dns1=FLAGS.flat_network_dns) + for net in db.network_get_all(ctxt): + network.set_network_host(ctxt, net) cleandb = os.path.join(FLAGS.state_path, FLAGS.sqlite_clean_db) shutil.copyfile(testdb, cleandb) diff --git a/nova/tests/api/__init__.py b/nova/tests/api/__init__.py index e69de29bb..6dab802f2 100644 --- a/nova/tests/api/__init__.py +++ b/nova/tests/api/__init__.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Openstack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work +from nova.tests import * diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py index bac7181f7..bfb424afe 100644 --- a/nova/tests/api/openstack/__init__.py +++ b/nova/tests/api/openstack/__init__.py @@ -15,6 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. +# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work +from nova.tests import * + import webob.dec from nova import test diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py index de1eb2f53..de006d088 100644 --- a/nova/tests/api/openstack/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/contrib/test_floating_ips.py @@ -139,7 +139,9 @@ class FloatingIpTest(test.TestCase): def test_floating_ip_allocate(self): req = webob.Request.blank('/v1.1/os-floating-ips') req.method = 'POST' + req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app()) + print res self.assertEqual(res.status_int, 200) ip = json.loads(res.body)['allocated'] expected = { @@ -177,6 +179,7 @@ class FloatingIpTest(test.TestCase): def test_floating_ip_disassociate(self): req = webob.Request.blank('/v1.1/os-floating-ips/1/disassociate') req.method = 'POST' + req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) ip = json.loads(res.body)['disassociated'] diff --git a/nova/tests/api/openstack/contrib/test_multinic_xs.py b/nova/tests/api/openstack/contrib/test_multinic_xs.py new file mode 100644 index 000000000..b0a9f7676 --- /dev/null +++ b/nova/tests/api/openstack/contrib/test_multinic_xs.py @@ -0,0 +1,115 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import stubout +import webob + +from nova import compute +from nova import context +from nova import test +from nova.tests.api.openstack import fakes + + +last_add_fixed_ip = (None, None) +last_remove_fixed_ip = (None, None) + + +def compute_api_add_fixed_ip(self, context, instance_id, network_id): + global last_add_fixed_ip + + last_add_fixed_ip = (instance_id, network_id) + + +def compute_api_remove_fixed_ip(self, context, instance_id, address): + global last_remove_fixed_ip + + last_remove_fixed_ip = (instance_id, address) + + +class FixedIpTest(test.TestCase): + def setUp(self): + super(FixedIpTest, self).setUp() + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.reset_fake_data() + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + self.stubs.Set(compute.api.API, "add_fixed_ip", + compute_api_add_fixed_ip) + self.stubs.Set(compute.api.API, "remove_fixed_ip", + compute_api_remove_fixed_ip) + self.context = context.get_admin_context() + + def tearDown(self): + self.stubs.UnsetAll() + super(FixedIpTest, self).tearDown() + + def test_add_fixed_ip(self): + global last_add_fixed_ip + last_add_fixed_ip = (None, None) + + body = dict(addFixedIp=dict(networkId='test_net')) + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) + self.assertEqual(last_add_fixed_ip, ('test_inst', 'test_net')) + + def test_add_fixed_ip_no_network(self): + global last_add_fixed_ip + last_add_fixed_ip = (None, None) + + body = dict(addFixedIp=dict()) + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 422) + self.assertEqual(last_add_fixed_ip, (None, None)) + + def test_remove_fixed_ip(self): + global last_remove_fixed_ip + last_remove_fixed_ip = (None, None) + + body = dict(removeFixedIp=dict(address='10.10.10.1')) + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) + self.assertEqual(last_remove_fixed_ip, ('test_inst', '10.10.10.1')) + + def test_remove_fixed_ip_no_address(self): + global last_remove_fixed_ip + last_remove_fixed_ip = (None, None) + + body = dict(removeFixedIp=dict()) + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = 'POST' + req.body = json.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 422) + self.assertEqual(last_remove_fixed_ip, (None, None)) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index eeb96cb12..26b1de818 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -147,6 +147,16 @@ def stub_out_compute_api_snapshot(stubs): stubs.Set(nova.compute.API, 'snapshot', snapshot) +def stub_out_compute_api_backup(stubs): + def backup(self, context, instance_id, name, backup_type, rotation, + extra_properties=None): + props = dict(instance_id=instance_id, instance_ref=instance_id, + backup_type=backup_type, rotation=rotation) + props.update(extra_properties or {}) + return dict(id='123', status='ACTIVE', name=name, properties=props) + stubs.Set(nova.compute.API, 'backup', backup) + + def stub_out_glance_add_image(stubs, sent_to_glance): """ We return the metadata sent to glance by modifying the sent_to_glance dict diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py index 29cb8b944..f09270b34 100644 --- a/nova/tests/api/openstack/test_common.py +++ b/nova/tests/api/openstack/test_common.py @@ -190,3 +190,78 @@ class PaginationParamsTest(test.TestCase): req = Request.blank('/?limit=20&marker=40') self.assertEqual(common.get_pagination_params(req), {'marker': 40, 'limit': 20}) + + +class MiscFunctionsTest(test.TestCase): + + def test_remove_version_from_href(self): + fixture = 'http://www.testsite.com/v1.1/images' + expected = 'http://www.testsite.com/images' + actual = common.remove_version_from_href(fixture) + self.assertEqual(actual, expected) + + def test_remove_version_from_href_2(self): + fixture = 'http://www.testsite.com/v1.1/' + expected = 'http://www.testsite.com/' + actual = common.remove_version_from_href(fixture) + self.assertEqual(actual, expected) + + def test_remove_version_from_href_3(self): + fixture = 'http://www.testsite.com/v10.10' + expected = 'http://www.testsite.com' + actual = common.remove_version_from_href(fixture) + self.assertEqual(actual, expected) + + def test_remove_version_from_href_4(self): + fixture = 'http://www.testsite.com/v1.1/images/v10.5' + expected = 'http://www.testsite.com/images/v10.5' + actual = common.remove_version_from_href(fixture) + self.assertEqual(actual, expected) + + def test_remove_version_from_href_bad_request(self): + fixture = 'http://www.testsite.com/1.1/images' + self.assertRaises(ValueError, + common.remove_version_from_href, + fixture) + + def test_remove_version_from_href_bad_request_2(self): + fixture = 'http://www.testsite.com/v/images' + self.assertRaises(ValueError, + common.remove_version_from_href, + fixture) + + def test_remove_version_from_href_bad_request_3(self): + fixture = 'http://www.testsite.com/v1.1images' + self.assertRaises(ValueError, + common.remove_version_from_href, + fixture) + + def test_get_id_from_href(self): + fixture = 'http://www.testsite.com/dir/45' + actual = common.get_id_from_href(fixture) + expected = 45 + self.assertEqual(actual, expected) + + def test_get_id_from_href_bad_request(self): + fixture = 'http://45' + self.assertRaises(ValueError, + common.get_id_from_href, + fixture) + + def test_get_version_from_href(self): + fixture = 'http://www.testsite.com/v1.1/images' + expected = '1.1' + actual = common.get_version_from_href(fixture) + self.assertEqual(actual, expected) + + def test_get_version_from_href_2(self): + fixture = 'http://www.testsite.com/v1.1' + expected = '1.1' + actual = common.get_version_from_href(fixture) + self.assertEqual(actual, expected) + + def test_get_version_from_href_default(self): + fixture = 'http://www.testsite.com/images' + expected = '1.0' + actual = common.get_version_from_href(fixture) + self.assertEqual(actual, expected) diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index 697c62e5c..d459c694f 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -16,10 +16,11 @@ # under the License. import json +import os.path import stubout import unittest import webob -import os.path +from xml.etree import ElementTree from nova import context from nova import flags @@ -30,7 +31,8 @@ from nova.api.openstack import wsgi from nova.tests.api.openstack import fakes FLAGS = flags.FLAGS - +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" response_body = "Try to say this Mr. Knox, sir..." @@ -80,20 +82,99 @@ class StubExtensionManager(object): class ExtensionControllerTest(unittest.TestCase): - def test_index(self): + def setUp(self): + FLAGS.osapi_extensions_path = os.path.join( + os.path.dirname(__file__), "extensions") + + def test_list_extensions_json(self): app = openstack.APIRouterV11() ext_midware = extensions.ExtensionMiddleware(app) request = webob.Request.blank("/extensions") response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) - def test_get_by_alias(self): + # Make sure we have all the extensions. + data = json.loads(response.body) + names = [x['name'] for x in data['extensions']] + names.sort() + self.assertEqual(names, ["FlavorExtraSpecs", "Floating_ips", + "Fox In Socks", "Hosts", "Multinic", "Volumes"]) + + # Make sure that at least Fox in Sox is correct. + (fox_ext,) = [ + x for x in data['extensions'] if x['alias'] == 'FOXNSOX'] + self.assertEqual(fox_ext, { + 'namespace': 'http://www.fox.in.socks/api/ext/pie/v1.0', + 'name': 'Fox In Socks', + 'updated': '2011-01-22T13:25:27-06:00', + 'description': 'The Fox In Socks Extension', + 'alias': 'FOXNSOX', + 'links': [] + } + ) + + def test_get_extension_json(self): app = openstack.APIRouterV11() ext_midware = extensions.ExtensionMiddleware(app) request = webob.Request.blank("/extensions/FOXNSOX") response = request.get_response(ext_midware) self.assertEqual(200, response.status_int) + data = json.loads(response.body) + self.assertEqual(data['extension'], { + "namespace": "http://www.fox.in.socks/api/ext/pie/v1.0", + "name": "Fox In Socks", + "updated": "2011-01-22T13:25:27-06:00", + "description": "The Fox In Socks Extension", + "alias": "FOXNSOX", + "links": [] + } + ) + + def test_list_extensions_xml(self): + app = openstack.APIRouterV11() + ext_midware = extensions.ExtensionMiddleware(app) + request = webob.Request.blank("/extensions") + request.accept = "application/xml" + response = request.get_response(ext_midware) + self.assertEqual(200, response.status_int) + print response.body + + root = ElementTree.XML(response.body) + self.assertEqual(root.tag.split('extensions')[0], NS) + + # Make sure we have all the extensions. + exts = root.findall('{0}extension'.format(NS)) + self.assertEqual(len(exts), 6) + + # Make sure that at least Fox in Sox is correct. + (fox_ext,) = [x for x in exts if x.get('alias') == 'FOXNSOX'] + self.assertEqual(fox_ext.get('name'), 'Fox In Socks') + self.assertEqual(fox_ext.get('namespace'), + 'http://www.fox.in.socks/api/ext/pie/v1.0') + self.assertEqual(fox_ext.get('updated'), '2011-01-22T13:25:27-06:00') + self.assertEqual(fox_ext.findtext('{0}description'.format(NS)), + 'The Fox In Socks Extension') + + def test_get_extension_xml(self): + app = openstack.APIRouterV11() + ext_midware = extensions.ExtensionMiddleware(app) + request = webob.Request.blank("/extensions/FOXNSOX") + request.accept = "application/xml" + response = request.get_response(ext_midware) + self.assertEqual(200, response.status_int) + print response.body + + root = ElementTree.XML(response.body) + self.assertEqual(root.tag.split('extension')[0], NS) + self.assertEqual(root.get('alias'), 'FOXNSOX') + self.assertEqual(root.get('name'), 'Fox In Socks') + self.assertEqual(root.get('namespace'), + 'http://www.fox.in.socks/api/ext/pie/v1.0') + self.assertEqual(root.get('updated'), '2011-01-22T13:25:27-06:00') + self.assertEqual(root.findtext('{0}description'.format(NS)), + 'The Fox In Socks Extension') + class ResourceExtensionTest(unittest.TestCase): @@ -192,7 +273,7 @@ class ActionExtensionTest(unittest.TestCase): def test_invalid_action(self): body = dict(blah=dict(name="test")) - response = self._send_server_action_request("/asdf/1/action", body) + response = self._send_server_action_request("/fdsa/1/action", body) self.assertEqual(404, response.status_int) @@ -244,3 +325,109 @@ class RequestExtensionTest(unittest.TestCase): response_data = json.loads(response.body) self.assertEqual('newblue', response_data['flavor']['googoose']) self.assertEqual("Pig Bands!", response_data['big_bands']) + + +class ExtensionsXMLSerializerTest(unittest.TestCase): + + def test_serialize_extenstion(self): + serializer = extensions.ExtensionsXMLSerializer() + data = { + 'extension': { + 'name': 'ext1', + 'namespace': 'http://docs.rack.com/servers/api/ext/pie/v1.0', + 'alias': 'RS-PIE', + 'updated': '2011-01-22T13:25:27-06:00', + 'description': 'Adds the capability to share an image.', + 'links': [ + { + 'rel': 'describedby', + 'type': 'application/pdf', + 'href': 'http://docs.rack.com/servers/api/ext/cs.pdf' + }, + { + 'rel': 'describedby', + 'type': 'application/vnd.sun.wadl+xml', + 'href': 'http://docs.rack.com/servers/api/ext/cs.wadl' + } + ] + } + } + + xml = serializer.serialize(data, 'show') + root = ElementTree.XML(xml) + ext_dict = data['extension'] + self.assertEqual(root.findtext('{0}description'.format(NS)), + ext_dict['description']) + + for key in ['name', 'namespace', 'alias', 'updated']: + self.assertEqual(root.get(key), ext_dict[key]) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(ext_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_serialize_extensions(self): + serializer = extensions.ExtensionsXMLSerializer() + data = { + "extensions": [ + { + "name": "Public Image Extension", + "namespace": "http://foo.com/api/ext/pie/v1.0", + "alias": "RS-PIE", + "updated": "2011-01-22T13:25:27-06:00", + "description": "Adds the capability to share an image.", + "links": [ + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://foo.com/api/ext/cs-pie.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://foo.com/api/ext/cs-pie.wadl" + } + ] + }, + { + "name": "Cloud Block Storage", + "namespace": "http://foo.com/api/ext/cbs/v1.0", + "alias": "RS-CBS", + "updated": "2011-01-12T11:22:33-06:00", + "description": "Allows mounting cloud block storage.", + "links": [ + { + "rel": "describedby", + "type": "application/pdf", + "href": "http://foo.com/api/ext/cs-cbs.pdf" + }, + { + "rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://foo.com/api/ext/cs-cbs.wadl" + } + ] + } + ] + } + + xml = serializer.serialize(data, 'index') + print xml + root = ElementTree.XML(xml) + ext_elems = root.findall('{0}extension'.format(NS)) + self.assertEqual(len(ext_elems), 2) + for i, ext_elem in enumerate(ext_elems): + ext_dict = data['extensions'][i] + self.assertEqual(ext_elem.findtext('{0}description'.format(NS)), + ext_dict['description']) + + for key in ['name', 'namespace', 'alias', 'updated']: + self.assertEqual(ext_elem.get(key), ext_dict[key]) + + link_nodes = ext_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(ext_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) diff --git a/nova/tests/api/openstack/test_faults.py b/nova/tests/api/openstack/test_faults.py index 4d86ffb26..6da27540a 100644 --- a/nova/tests/api/openstack/test_faults.py +++ b/nova/tests/api/openstack/test_faults.py @@ -16,6 +16,7 @@ # under the License. import json +from xml.dom import minidom import webob import webob.dec @@ -24,6 +25,7 @@ import webob.exc from nova import test from nova.api.openstack import common from nova.api.openstack import faults +from nova.api.openstack import wsgi class TestFaults(test.TestCase): @@ -139,3 +141,113 @@ class TestFaults(test.TestCase): self.assertEqual(resp.content_type, "application/xml") self.assertEqual(resp.status_int, 404) self.assertTrue('whut?' in resp.body) + + def test_fault_has_status_int(self): + """Ensure the status_int is set correctly on faults""" + fault = faults.Fault(webob.exc.HTTPBadRequest(explanation='what?')) + self.assertEqual(fault.status_int, 400) + + def test_v10_xml_serializer(self): + """Ensure that a v1.0 request responds with a v1.0 xmlns""" + request = webob.Request.blank('/', + headers={"Accept": "application/xml"}) + + fault = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + response = request.get_response(fault) + + self.assertTrue(common.XML_NS_V10 in response.body) + self.assertEqual(response.content_type, "application/xml") + self.assertEqual(response.status_int, 400) + + def test_v11_xml_serializer(self): + """Ensure that a v1.1 request responds with a v1.1 xmlns""" + request = webob.Request.blank('/v1.1', + headers={"Accept": "application/xml"}) + + fault = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + response = request.get_response(fault) + + self.assertTrue(common.XML_NS_V11 in response.body) + self.assertEqual(response.content_type, "application/xml") + self.assertEqual(response.status_int, 400) + + +class FaultsXMLSerializationTestV11(test.TestCase): + """Tests covering `nova.api.openstack.faults:Fault` class.""" + + def _prepare_xml(self, xml_string): + xml_string = xml_string.replace(" ", "") + xml_string = xml_string.replace("\n", "") + xml_string = xml_string.replace("\t", "") + return xml_string + + def test_400_fault(self): + metadata = {'attributes': {"badRequest": 'code'}} + serializer = wsgi.XMLDictSerializer(metadata=metadata, + xmlns=common.XML_NS_V11) + + fixture = { + "badRequest": { + "message": "scram", + "code": 400, + }, + } + + output = serializer.serialize(fixture) + actual = minidom.parseString(self._prepare_xml(output)) + + expected = minidom.parseString(self._prepare_xml(""" + <badRequest code="400" xmlns="%s"> + <message>scram</message> + </badRequest> + """) % common.XML_NS_V11) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_413_fault(self): + metadata = {'attributes': {"overLimit": 'code'}} + serializer = wsgi.XMLDictSerializer(metadata=metadata, + xmlns=common.XML_NS_V11) + + fixture = { + "overLimit": { + "message": "sorry", + "code": 413, + "retryAfter": 4, + }, + } + + output = serializer.serialize(fixture) + actual = minidom.parseString(self._prepare_xml(output)) + + expected = minidom.parseString(self._prepare_xml(""" + <overLimit code="413" xmlns="%s"> + <message>sorry</message> + <retryAfter>4</retryAfter> + </overLimit> + """) % common.XML_NS_V11) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_404_fault(self): + metadata = {'attributes': {"itemNotFound": 'code'}} + serializer = wsgi.XMLDictSerializer(metadata=metadata, + xmlns=common.XML_NS_V11) + + fixture = { + "itemNotFound": { + "message": "sorry", + "code": 404, + }, + } + + output = serializer.serialize(fixture) + actual = minidom.parseString(self._prepare_xml(output)) + + expected = minidom.parseString(self._prepare_xml(""" + <itemNotFound code="404" xmlns="%s"> + <message>sorry</message> + </itemNotFound> + """) % common.XML_NS_V11) + + self.assertEqual(expected.toxml(), actual.toxml()) diff --git a/nova/tests/api/openstack/test_flavors.py b/nova/tests/api/openstack/test_flavors.py index d1c62e454..4ac35b26b 100644 --- a/nova/tests/api/openstack/test_flavors.py +++ b/nova/tests/api/openstack/test_flavors.py @@ -18,12 +18,14 @@ import json import stubout import webob +import xml.dom.minidom as minidom +from nova.api.openstack import flavors import nova.db.api -from nova import context from nova import exception from nova import test from nova.tests.api.openstack import fakes +from nova import wsgi def stub_flavor(flavorid, name, memory_mb="256", local_gb="10"): @@ -64,7 +66,6 @@ class FlavorsTest(test.TestCase): return_instance_types) self.stubs.Set(nova.db.api, "instance_type_get_by_flavor_id", return_instance_type_by_flavor_id) - self.context = context.get_admin_context() def tearDown(self): self.stubs.UnsetAll() @@ -87,6 +88,19 @@ class FlavorsTest(test.TestCase): ] self.assertEqual(flavors, expected) + def test_get_empty_flavor_list_v1_0(self): + def _return_empty(self): + return {} + self.stubs.Set(nova.db.api, "instance_type_get_all", + _return_empty) + + req = webob.Request.blank('/v1.0/flavors') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + flavors = json.loads(res.body)["flavors"] + expected = [] + self.assertEqual(flavors, expected) + def test_get_flavor_list_detail_v1_0(self): req = webob.Request.blank('/v1.0/flavors/detail') res = req.get_response(fakes.wsgi_app()) @@ -133,79 +147,65 @@ class FlavorsTest(test.TestCase): req.environ['api.version'] = '1.1' res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) - flavor = json.loads(res.body)["flavor"] + flavor = json.loads(res.body) expected = { - "id": "12", - "name": "flavor 12", - "ram": "256", - "disk": "10", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/flavors/12", - }, - { - "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/flavors/12", - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/flavors/12", - }, - ], - } - self.assertEqual(flavor, expected) - - def test_get_flavor_list_v1_1(self): - req = webob.Request.blank('/v1.1/flavors') - req.environ['api.version'] = '1.1' - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - flavor = json.loads(res.body)["flavors"] - expected = [ - { - "id": "1", - "name": "flavor 1", - "links": [ - { - "rel": "self", - "href": "http://localhost/v1.1/flavors/1", - }, - { - "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/flavors/1", - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/flavors/1", - }, - ], - }, - { - "id": "2", - "name": "flavor 2", + "flavor": { + "id": "12", + "name": "flavor 12", + "ram": "256", + "disk": "10", "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/2", - }, - { - "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/flavors/2", + "href": "http://localhost/v1.1/flavors/12", }, { "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/flavors/2", + "href": "http://localhost/flavors/12", }, ], }, - ] + } + self.assertEqual(flavor, expected) + + def test_get_flavor_list_v1_1(self): + req = webob.Request.blank('/v1.1/flavors') + req.environ['api.version'] = '1.1' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + flavor = json.loads(res.body) + expected = { + "flavors": [ + { + "id": "1", + "name": "flavor 1", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost/flavors/2", + }, + ], + }, + ], + } self.assertEqual(flavor, expected) def test_get_flavor_list_detail_v1_1(self): @@ -213,51 +213,273 @@ class FlavorsTest(test.TestCase): req.environ['api.version'] = '1.1' res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) - flavor = json.loads(res.body)["flavors"] - expected = [ - { - "id": "1", - "name": "flavor 1", + flavor = json.loads(res.body) + expected = { + "flavors": [ + { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "ram": "256", + "disk": "10", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost/flavors/2", + }, + ], + }, + ], + } + self.assertEqual(flavor, expected) + + def test_get_empty_flavor_list_v1_1(self): + def _return_empty(self): + return {} + self.stubs.Set(nova.db.api, "instance_type_get_all", _return_empty) + + req = webob.Request.blank('/v1.1/flavors') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + flavors = json.loads(res.body)["flavors"] + expected = [] + self.assertEqual(flavors, expected) + + +class FlavorsXMLSerializationTest(test.TestCase): + + def test_show(self): + serializer = flavors.FlavorXMLSerializer() + + input = { + "flavor": { + "id": "12", + "name": "asdf", "ram": "256", "disk": "10", "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/1", + "href": "http://localhost/v1.1/flavors/12", }, { "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/flavors/1", - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/flavors/1", + "href": "http://localhost/flavors/12", }, ], }, - { - "id": "2", - "name": "flavor 2", - "ram": "256", - "disk": "10", + } + + output = serializer.serialize(input, 'show') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <flavor xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom" + id="12" + name="asdf" + ram="256" + disk="10"> + <atom:link href="http://localhost/v1.1/flavors/12" rel="self"/> + <atom:link href="http://localhost/flavors/12" rel="bookmark"/> + </flavor> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_show_handles_integers(self): + serializer = flavors.FlavorXMLSerializer() + + input = { + "flavor": { + "id": 12, + "name": "asdf", + "ram": 256, + "disk": 10, "links": [ { "rel": "self", - "href": "http://localhost/v1.1/flavors/2", + "href": "http://localhost/v1.1/flavors/12", }, { "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/flavors/2", - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/flavors/2", + "href": "http://localhost/flavors/12", }, ], }, - ] - self.assertEqual(flavor, expected) + } + + output = serializer.serialize(input, 'show') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <flavor xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom" + id="12" + name="asdf" + ram="256" + disk="10"> + <atom:link href="http://localhost/v1.1/flavors/12" rel="self"/> + <atom:link href="http://localhost/flavors/12" rel="bookmark"/> + </flavor> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_detail(self): + serializer = flavors.FlavorXMLSerializer() + + input = { + "flavors": [ + { + "id": "23", + "name": "flavor 23", + "ram": "512", + "disk": "20", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/flavors/23", + }, + { + "rel": "bookmark", + "href": "http://localhost/flavors/23", + }, + ], + }, { + "id": "13", + "name": "flavor 13", + "ram": "256", + "disk": "10", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/flavors/13", + }, + { + "rel": "bookmark", + "href": "http://localhost/flavors/13", + }, + ], + }, + ], + } + + output = serializer.serialize(input, 'detail') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <flavors xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom"> + <flavor id="23" + name="flavor 23" + ram="512" + disk="20"> + <atom:link href="http://localhost/v1.1/flavors/23" rel="self"/> + <atom:link href="http://localhost/flavors/23" rel="bookmark"/> + </flavor> + <flavor id="13" + name="flavor 13" + ram="256" + disk="10"> + <atom:link href="http://localhost/v1.1/flavors/13" rel="self"/> + <atom:link href="http://localhost/flavors/13" rel="bookmark"/> + </flavor> + </flavors> + """.replace(" ", "") % locals()) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_index(self): + serializer = flavors.FlavorXMLSerializer() + + input = { + "flavors": [ + { + "id": "23", + "name": "flavor 23", + "ram": "512", + "disk": "20", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/flavors/23", + }, + { + "rel": "bookmark", + "href": "http://localhost/flavors/23", + }, + ], + }, { + "id": "13", + "name": "flavor 13", + "ram": "256", + "disk": "10", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/flavors/13", + }, + { + "rel": "bookmark", + "href": "http://localhost/flavors/13", + }, + ], + }, + ], + } + + output = serializer.serialize(input, 'index') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <flavors xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom"> + <flavor id="23" name="flavor 23"> + <atom:link href="http://localhost/v1.1/flavors/23" rel="self"/> + <atom:link href="http://localhost/flavors/23" rel="bookmark"/> + </flavor> + <flavor id="13" name="flavor 13"> + <atom:link href="http://localhost/v1.1/flavors/13" rel="self"/> + <atom:link href="http://localhost/flavors/13" rel="bookmark"/> + </flavor> + </flavors> + """.replace(" ", "") % locals()) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_index_empty(self): + serializer = flavors.FlavorXMLSerializer() + + input = { + "flavors": [], + } + + output = serializer.serialize(input, 'index') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <flavors xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom" /> + """.replace(" ", "") % locals()) + + self.assertEqual(expected.toxml(), actual.toxml()) diff --git a/nova/tests/api/openstack/test_image_metadata.py b/nova/tests/api/openstack/test_image_metadata.py index 730af3665..31ca18497 100644 --- a/nova/tests/api/openstack/test_image_metadata.py +++ b/nova/tests/api/openstack/test_image_metadata.py @@ -24,6 +24,7 @@ import xml.dom.minidom as minidom from nova import flags from nova.api import openstack +from nova import test from nova.tests.api.openstack import fakes import nova.wsgi @@ -31,7 +32,7 @@ import nova.wsgi FLAGS = flags.FLAGS -class ImageMetaDataTest(unittest.TestCase): +class ImageMetaDataTest(test.TestCase): IMAGE_FIXTURES = [ {'status': 'active', @@ -102,8 +103,7 @@ class ImageMetaDataTest(unittest.TestCase): super(ImageMetaDataTest, self).tearDown() def test_index(self): - req = webob.Request.blank('/v1.1/images/1/meta') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/1/metadata') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) @@ -112,33 +112,8 @@ class ImageMetaDataTest(unittest.TestCase): for (key, value) in res_dict['metadata'].items(): self.assertEqual(value, res_dict['metadata'][key]) - def test_index_xml(self): - serializer = openstack.image_metadata.ImageMetadataXMLSerializer() - fixture = { - 'metadata': { - 'one': 'two', - 'three': 'four', - }, - } - output = serializer.index(fixture) - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> - <meta key="three"> - four - </meta> - <meta key="one"> - two - </meta> - </metadata> - """.replace(" ", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) - def test_show(self): - req = webob.Request.blank('/v1.1/images/1/meta/key1') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/1/metadata/key1') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) @@ -146,116 +121,75 @@ class ImageMetaDataTest(unittest.TestCase): self.assertEqual(len(res_dict['meta']), 1) self.assertEqual('value1', res_dict['meta']['key1']) - def test_show_xml(self): - serializer = openstack.image_metadata.ImageMetadataXMLSerializer() - fixture = { - 'meta': { - 'one': 'two', - }, - } - output = serializer.show(fixture) - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <meta xmlns="http://docs.openstack.org/compute/api/v1.1" key="one"> - two - </meta> - """.replace(" ", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) - def test_show_not_found(self): - req = webob.Request.blank('/v1.1/images/1/meta/key9') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/1/metadata/key9') res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) def test_create(self): - req = webob.Request.blank('/v1.1/images/2/meta') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/2/metadata') req.method = 'POST' req.body = '{"metadata": {"key9": "value9"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) - res_dict = json.loads(res.body) + self.assertEqual(200, res.status_int) - self.assertEqual('value9', res_dict['metadata']['key9']) - # other items should not be modified - self.assertEqual('value1', res_dict['metadata']['key1']) - self.assertEqual('value2', res_dict['metadata']['key2']) - self.assertEqual(1, len(res_dict)) + actual_output = json.loads(res.body) - def test_create_xml(self): - serializer = openstack.image_metadata.ImageMetadataXMLSerializer() - fixture = { + expected_output = { 'metadata': { - 'key9': 'value9', - 'key2': 'value2', 'key1': 'value1', + 'key2': 'value2', + 'key9': 'value9', }, } - output = serializer.create(fixture) - actual = minidom.parseString(output.replace(" ", "")) - expected = minidom.parseString(""" - <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> - <meta key="key2"> - value2 - </meta> - <meta key="key9"> - value9 - </meta> - <meta key="key1"> - value1 - </meta> - </metadata> - """.replace(" ", "")) + self.assertEqual(expected_output, actual_output) - self.assertEqual(expected.toxml(), actual.toxml()) + def test_update_all(self): + req = webob.Request.blank('/v1.1/images/2/metadata') + req.method = 'PUT' + req.body = '{"metadata": {"key9": "value9"}}' + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(200, res.status_int) + actual_output = json.loads(res.body) + + expected_output = { + 'metadata': { + 'key9': 'value9', + }, + } + + self.assertEqual(expected_output, actual_output) def test_update_item(self): - req = webob.Request.blank('/v1.1/images/1/meta/key1') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/1/metadata/key1') req.method = 'PUT' req.body = '{"meta": {"key1": "zz"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) + self.assertEqual(200, res.status_int) - res_dict = json.loads(res.body) - self.assertTrue('meta' in res_dict) - self.assertEqual(len(res_dict['meta']), 1) - self.assertEqual('zz', res_dict['meta']['key1']) + actual_output = json.loads(res.body) + expected_output = { + 'meta': { + 'key1': 'zz', + }, + } + self.assertEqual(actual_output, expected_output) def test_update_item_bad_body(self): - req = webob.Request.blank('/v1.1/images/1/meta/key1') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/1/metadata/key1') req.method = 'PUT' req.body = '{"key1": "zz"}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) - def test_update_item_xml(self): - serializer = openstack.image_metadata.ImageMetadataXMLSerializer() - fixture = { - 'meta': { - 'one': 'two', - }, - } - output = serializer.update(fixture) - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - <meta xmlns="http://docs.openstack.org/compute/api/v1.1" key="one"> - two - </meta> - """.replace(" ", "")) - - self.assertEqual(expected.toxml(), actual.toxml()) - def test_update_item_too_many_keys(self): - req = webob.Request.blank('/v1.1/images/1/meta/key1') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/1/metadata/key1') req.method = 'PUT' req.body = '{"meta": {"key1": "value1", "key2": "value2"}}' req.headers["content-type"] = "application/json" @@ -263,24 +197,38 @@ class ImageMetaDataTest(unittest.TestCase): self.assertEqual(400, res.status_int) def test_update_item_body_uri_mismatch(self): - req = webob.Request.blank('/v1.1/images/1/meta/bad') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/1/metadata/bad') req.method = 'PUT' req.body = '{"meta": {"key1": "value1"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) + def test_update_item_xml(self): + req = webob.Request.blank('/v1.1/images/1/metadata/key1') + req.method = 'PUT' + req.body = '<meta key="key1">five</meta>' + req.headers["content-type"] = "application/xml" + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(200, res.status_int) + actual_output = json.loads(res.body) + expected_output = { + 'meta': { + 'key1': 'five', + }, + } + self.assertEqual(actual_output, expected_output) + def test_delete(self): - req = webob.Request.blank('/v1.1/images/2/meta/key1') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/2/metadata/key1') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) - self.assertEqual(200, res.status_int) + self.assertEqual(204, res.status_int) + self.assertEqual('', res.body) def test_delete_not_found(self): - req = webob.Request.blank('/v1.1/images/2/meta/blah') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/2/metadata/blah') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) @@ -290,8 +238,7 @@ class ImageMetaDataTest(unittest.TestCase): for num in range(FLAGS.quota_metadata_items + 1): data['metadata']['key%i' % num] = "blah" json_string = str(data).replace("\'", "\"") - req = webob.Request.blank('/v1.1/images/2/meta') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/2/metadata') req.method = 'POST' req.body = json_string req.headers["content-type"] = "application/json" @@ -299,10 +246,209 @@ class ImageMetaDataTest(unittest.TestCase): self.assertEqual(400, res.status_int) def test_too_many_metadata_items_on_put(self): - req = webob.Request.blank('/v1.1/images/3/meta/blah') - req.environ['api.version'] = '1.1' + req = webob.Request.blank('/v1.1/images/3/metadata/blah') req.method = 'PUT' req.body = '{"meta": {"blah": "blah"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) + + +class ImageMetadataXMLDeserializationTest(test.TestCase): + + deserializer = openstack.image_metadata.ImageMetadataXMLDeserializer() + + def test_create(self): + request_body = """ + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> + <meta key='123'>asdf</meta> + <meta key='567'>jkl;</meta> + </metadata>""" + output = self.deserializer.deserialize(request_body, 'create') + expected = {"body": {"metadata": {"123": "asdf", "567": "jkl;"}}} + self.assertEquals(output, expected) + + def test_create_empty(self): + request_body = """ + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"/>""" + output = self.deserializer.deserialize(request_body, 'create') + expected = {"body": {"metadata": {}}} + self.assertEquals(output, expected) + + def test_update_all(self): + request_body = """ + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> + <meta key='123'>asdf</meta> + <meta key='567'>jkl;</meta> + </metadata>""" + output = self.deserializer.deserialize(request_body, 'update_all') + expected = {"body": {"metadata": {"123": "asdf", "567": "jkl;"}}} + self.assertEquals(output, expected) + + def test_update(self): + request_body = """ + <meta xmlns="http://docs.openstack.org/compute/api/v1.1" + key='123'>asdf</meta>""" + output = self.deserializer.deserialize(request_body, 'update') + expected = {"body": {"meta": {"123": "asdf"}}} + self.assertEquals(output, expected) + + +class ImageMetadataXMLSerializationTest(test.TestCase): + + def test_index(self): + serializer = openstack.image_metadata.ImageMetadataXMLSerializer() + fixture = { + 'metadata': { + 'one': 'two', + 'three': 'four', + }, + } + output = serializer.serialize(fixture, 'index') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> + <meta key="three"> + four + </meta> + <meta key="one"> + two + </meta> + </metadata> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_index_null(self): + serializer = openstack.image_metadata.ImageMetadataXMLSerializer() + fixture = { + 'metadata': { + None: None, + }, + } + output = serializer.serialize(fixture, 'index') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> + <meta key="None"> + None + </meta> + </metadata> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_index_unicode(self): + serializer = openstack.image_metadata.ImageMetadataXMLSerializer() + fixture = { + 'metadata': { + u'three': u'Jos\xe9', + }, + } + output = serializer.serialize(fixture, 'index') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(u""" + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> + <meta key="three"> + Jos\xe9 + </meta> + </metadata> + """.encode("UTF-8").replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_show(self): + serializer = openstack.image_metadata.ImageMetadataXMLSerializer() + fixture = { + 'meta': { + 'one': 'two', + }, + } + output = serializer.serialize(fixture, 'show') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <meta xmlns="http://docs.openstack.org/compute/api/v1.1" key="one"> + two + </meta> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_update_all(self): + serializer = openstack.image_metadata.ImageMetadataXMLSerializer() + fixture = { + 'metadata': { + 'key6': 'value6', + 'key4': 'value4', + }, + } + output = serializer.serialize(fixture, 'update_all') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> + <meta key="key6"> + value6 + </meta> + <meta key="key4"> + value4 + </meta> + </metadata> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_update_item(self): + serializer = openstack.image_metadata.ImageMetadataXMLSerializer() + fixture = { + 'meta': { + 'one': 'two', + }, + } + output = serializer.serialize(fixture, 'update') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <meta xmlns="http://docs.openstack.org/compute/api/v1.1" key="one"> + two + </meta> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_create(self): + serializer = openstack.image_metadata.ImageMetadataXMLSerializer() + fixture = { + 'metadata': { + 'key9': 'value9', + 'key2': 'value2', + 'key1': 'value1', + }, + } + output = serializer.serialize(fixture, 'create') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> + <meta key="key2"> + value2 + </meta> + <meta key="key9"> + value9 + </meta> + <meta key="key1"> + value1 + </meta> + </metadata> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_delete(self): + serializer = openstack.image_metadata.ImageMetadataXMLSerializer() + output = serializer.serialize(None, 'delete') + self.assertEqual(output, '') diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index fc4fc84e2..87a695dde 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -340,6 +340,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.fixtures = self._make_image_fixtures() fakes.stub_out_glance(self.stubs, initial_fixtures=self.fixtures) fakes.stub_out_compute_api_snapshot(self.stubs) + fakes.stub_out_compute_api_backup(self.stubs) def tearDown(self): """Run after each test.""" @@ -364,10 +365,10 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response_list = response_dict["images"] expected = [{'id': 123, 'name': 'public image'}, - {'id': 124, 'name': 'queued backup'}, - {'id': 125, 'name': 'saving backup'}, - {'id': 126, 'name': 'active backup'}, - {'id': 127, 'name': 'killed backup'}, + {'id': 124, 'name': 'queued snapshot'}, + {'id': 125, 'name': 'saving snapshot'}, + {'id': 126, 'name': 'active snapshot'}, + {'id': 127, 'name': 'killed snapshot'}, {'id': 129, 'name': None}] self.assertDictListMatch(response_list, expected) @@ -393,33 +394,45 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(expected_image, actual_image) def test_get_image_v1_1(self): - request = webob.Request.blank('/v1.1/images/123') + request = webob.Request.blank('/v1.1/images/124') response = request.get_response(fakes.wsgi_app()) actual_image = json.loads(response.body) - href = "http://localhost/v1.1/images/123" + href = "http://localhost/v1.1/images/124" + bookmark = "http://localhost/images/124" + server_href = "http://localhost/v1.1/servers/42" + server_bookmark = "http://localhost/servers/42" expected_image = { "image": { - "id": 123, - "name": "public image", + "id": 124, + "name": "queued snapshot", "updated": self.NOW_API_FORMAT, "created": self.NOW_API_FORMAT, - "status": "ACTIVE", + "status": "QUEUED", + 'server': { + 'id': 42, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "metadata": { + "instance_ref": "http://localhost/v1.1/servers/42", + "user_id": "1", + }, "links": [{ "rel": "self", "href": href, }, { "rel": "bookmark", - "type": "application/json", - "href": href, - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": href, + "href": bookmark, }], }, } @@ -464,34 +477,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(expected_image.toxml(), actual_image.toxml()) - def test_get_image_v1_1_xml(self): - request = webob.Request.blank('/v1.1/images/123') - request.accept = "application/xml" - response = request.get_response(fakes.wsgi_app()) - - actual_image = minidom.parseString(response.body.replace(" ", "")) - - expected_href = "http://localhost/v1.1/images/123" - expected_now = self.NOW_API_FORMAT - expected_image = minidom.parseString(""" - <image id="123" - name="public image" - updated="%(expected_now)s" - created="%(expected_now)s" - status="ACTIVE" - xmlns="http://docs.openstack.org/compute/api/v1.1"> - <links> - <link href="%(expected_href)s" rel="self"/> - <link href="%(expected_href)s" rel="bookmark" - type="application/json" /> - <link href="%(expected_href)s" rel="bookmark" - type="application/xml" /> - </links> - </image> - """.replace(" ", "") % (locals())) - - self.assertEqual(expected_image.toxml(), actual_image.toxml()) - def test_get_image_404_json(self): request = webob.Request.blank('/v1.0/images/NonExistantImage') response = request.get_response(fakes.wsgi_app()) @@ -553,7 +538,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): # because the element hasn't changed definition expected = minidom.parseString(""" <itemNotFound code="404" - xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"> + xmlns="http://docs.openstack.org/compute/api/v1.1"> <message> Image not found. </message> @@ -579,23 +564,20 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): continue href = "http://localhost/v1.1/images/%s" % image["id"] + bookmark = "http://localhost/images/%s" % image["id"] test_image = { "id": image["id"], "name": image["name"], - "links": [{ - "rel": "self", - "href": "http://localhost/v1.1/images/%s" % image["id"], - }, - { - "rel": "bookmark", - "type": "application/json", - "href": href, - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": href, - }], + "links": [ + { + "rel": "self", + "href": href, + }, + { + "rel": "bookmark", + "href": bookmark, + }, + ], } self.assertTrue(test_image in response_list) @@ -617,14 +599,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 124, - 'name': 'queued backup', + 'name': 'queued snapshot', 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'QUEUED', }, { 'id': 125, - 'name': 'saving backup', + 'name': 'saving snapshot', 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'SAVING', @@ -632,14 +614,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 126, - 'name': 'active backup', + 'name': 'active snapshot', 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE' }, { 'id': 127, - 'name': 'killed backup', + 'name': 'killed snapshot', 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'FAILED', @@ -660,10 +642,13 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response_dict = json.loads(response.body) response_list = response_dict["images"] + server_href = "http://localhost/v1.1/servers/42" + server_bookmark = "http://localhost/servers/42" expected = [{ 'id': 123, 'name': 'public image', + 'metadata': {}, 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', @@ -673,107 +658,134 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/images/123", - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/images/123", + "href": "http://localhost/images/123", }], }, { 'id': 124, - 'name': 'queued backup', - 'serverRef': "http://localhost:8774/v1.1/servers/42", + 'name': 'queued snapshot', + 'metadata': { + u'instance_ref': u'http://localhost/v1.1/servers/42', + u'user_id': u'1', + }, 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'QUEUED', + 'server': { + 'id': 42, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/124", }, { "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/images/124", - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/images/124", + "href": "http://localhost/images/124", }], }, { 'id': 125, - 'name': 'saving backup', - 'serverRef': "http://localhost:8774/v1.1/servers/42", + 'name': 'saving snapshot', + 'metadata': { + u'instance_ref': u'http://localhost/v1.1/servers/42', + u'user_id': u'1', + }, 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'SAVING', 'progress': 0, + 'server': { + 'id': 42, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/125", }, { "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/images/125", - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/images/125", + "href": "http://localhost/images/125", }], }, { 'id': 126, - 'name': 'active backup', - 'serverRef': "http://localhost:8774/v1.1/servers/42", + 'name': 'active snapshot', + 'metadata': { + u'instance_ref': u'http://localhost/v1.1/servers/42', + u'user_id': u'1', + }, 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + 'server': { + 'id': 42, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/126", }, { "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/images/126", - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/images/126", + "href": "http://localhost/images/126", }], }, { 'id': 127, - 'name': 'killed backup', - 'serverRef': "http://localhost:8774/v1.1/servers/42", + 'name': 'killed snapshot', + 'metadata': { + u'instance_ref': u'http://localhost/v1.1/servers/42', + u'user_id': u'1', + }, 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'FAILED', + 'server': { + 'id': 42, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, "links": [{ "rel": "self", "href": "http://localhost/v1.1/images/127", }, { "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/images/127", - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/images/127", + "href": "http://localhost/images/127", }], }, { 'id': 129, 'name': None, + 'metadata': {}, 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', @@ -783,13 +795,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/images/129", - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/images/129", + "href": "http://localhost/images/129", }], }, ] @@ -797,154 +803,206 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertDictListMatch(expected, response_list) def test_image_filter_with_name(self): - mocker = mox.Mox() - image_service = mocker.CreateMockAnything() + image_service = self.mox.CreateMockAnything() context = object() filters = {'name': 'testname'} - image_service.index( - context, filters=filters).AndReturn([]) - mocker.ReplayAll() - request = webob.Request.blank( - '/v1.1/images?name=testname') + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images?name=testname') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.index(request) - mocker.VerifyAll() + self.mox.VerifyAll() def test_image_filter_with_status(self): - mocker = mox.Mox() - image_service = mocker.CreateMockAnything() + image_service = self.mox.CreateMockAnything() context = object() filters = {'status': 'ACTIVE'} - image_service.index( - context, filters=filters).AndReturn([]) - mocker.ReplayAll() - request = webob.Request.blank( - '/v1.1/images?status=ACTIVE') + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images?status=ACTIVE') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.index(request) - mocker.VerifyAll() + self.mox.VerifyAll() def test_image_filter_with_property(self): - mocker = mox.Mox() - image_service = mocker.CreateMockAnything() + image_service = self.mox.CreateMockAnything() context = object() filters = {'property-test': '3'} - image_service.index( - context, filters=filters).AndReturn([]) - mocker.ReplayAll() - request = webob.Request.blank( - '/v1.1/images?property-test=3') + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images?property-test=3') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_server(self): + image_service = self.mox.CreateMockAnything() + context = object() + # 'server' should be converted to 'property-instance_ref' + filters = {'property-instance_ref': 'http://localhost:8774/servers/12'} + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images?server=' + 'http://localhost:8774/servers/12') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_changes_since(self): + image_service = self.mox.CreateMockAnything() + context = object() + filters = {'changes-since': '2011-01-24T17:08Z'} + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images?changes-since=' + '2011-01-24T17:08Z') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_filter_with_type(self): + image_service = self.mox.CreateMockAnything() + context = object() + filters = {'property-image_type': 'BASE'} + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images?type=BASE') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.index(request) - mocker.VerifyAll() + self.mox.VerifyAll() def test_image_filter_not_supported(self): - mocker = mox.Mox() - image_service = mocker.CreateMockAnything() + image_service = self.mox.CreateMockAnything() context = object() filters = {'status': 'ACTIVE'} - image_service.index( - context, filters=filters).AndReturn([]) - mocker.ReplayAll() - request = webob.Request.blank( - '/v1.1/images?status=ACTIVE&UNSUPPORTEDFILTER=testname') + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images?status=ACTIVE&' + 'UNSUPPORTEDFILTER=testname') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) - controller.index(request) - mocker.VerifyAll() + controller.detail(request) + self.mox.VerifyAll() def test_image_no_filters(self): - mocker = mox.Mox() - image_service = mocker.CreateMockAnything() + image_service = self.mox.CreateMockAnything() context = object() filters = {} image_service.index( context, filters=filters).AndReturn([]) - mocker.ReplayAll() + self.mox.ReplayAll() request = webob.Request.blank( '/v1.1/images') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.index(request) - mocker.VerifyAll() + self.mox.VerifyAll() def test_image_detail_filter_with_name(self): - mocker = mox.Mox() - image_service = mocker.CreateMockAnything() + image_service = self.mox.CreateMockAnything() context = object() filters = {'name': 'testname'} - image_service.detail( - context, filters=filters).AndReturn([]) - mocker.ReplayAll() - request = webob.Request.blank( - '/v1.1/images/detail?name=testname') + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images/detail?name=testname') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.detail(request) - mocker.VerifyAll() + self.mox.VerifyAll() def test_image_detail_filter_with_status(self): - mocker = mox.Mox() - image_service = mocker.CreateMockAnything() + image_service = self.mox.CreateMockAnything() context = object() filters = {'status': 'ACTIVE'} - image_service.detail( - context, filters=filters).AndReturn([]) - mocker.ReplayAll() - request = webob.Request.blank( - '/v1.1/images/detail?status=ACTIVE') + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images/detail?status=ACTIVE') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.detail(request) - mocker.VerifyAll() + self.mox.VerifyAll() def test_image_detail_filter_with_property(self): - mocker = mox.Mox() - image_service = mocker.CreateMockAnything() + image_service = self.mox.CreateMockAnything() context = object() filters = {'property-test': '3'} - image_service.detail( - context, filters=filters).AndReturn([]) - mocker.ReplayAll() - request = webob.Request.blank( - '/v1.1/images/detail?property-test=3') + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images/detail?property-test=3') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.detail(request) - mocker.VerifyAll() + self.mox.VerifyAll() + + def test_image_detail_filter_server(self): + image_service = self.mox.CreateMockAnything() + context = object() + # 'server' should be converted to 'property-instance_ref' + filters = {'property-instance_ref': 'http://localhost:8774/servers/12'} + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images/detail?server=' + 'http://localhost:8774/servers/12') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_detail_filter_changes_since(self): + image_service = self.mox.CreateMockAnything() + context = object() + filters = {'changes-since': '2011-01-24T17:08Z'} + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images/detail?changes-since=' + '2011-01-24T17:08Z') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() + + def test_image_detail_filter_with_type(self): + image_service = self.mox.CreateMockAnything() + context = object() + filters = {'property-image_type': 'BASE'} + image_service.index(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images/detail?type=BASE') + request.environ['nova.context'] = context + controller = images.ControllerV11(image_service=image_service) + controller.index(request) + self.mox.VerifyAll() def test_image_detail_filter_not_supported(self): - mocker = mox.Mox() - image_service = mocker.CreateMockAnything() + image_service = self.mox.CreateMockAnything() context = object() filters = {'status': 'ACTIVE'} - image_service.detail( - context, filters=filters).AndReturn([]) - mocker.ReplayAll() - request = webob.Request.blank( - '/v1.1/images/detail?status=ACTIVE&UNSUPPORTEDFILTER=testname') + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images/detail?status=ACTIVE&' + 'UNSUPPORTEDFILTER=testname') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.detail(request) - mocker.VerifyAll() + self.mox.VerifyAll() def test_image_detail_no_filters(self): - mocker = mox.Mox() - image_service = mocker.CreateMockAnything() + image_service = self.mox.CreateMockAnything() context = object() filters = {} - image_service.detail( - context, filters=filters).AndReturn([]) - mocker.ReplayAll() - request = webob.Request.blank( - '/v1.1/images/detail') + image_service.detail(context, filters=filters).AndReturn([]) + self.mox.ReplayAll() + request = webob.Request.blank('/v1.1/images/detail') request.environ['nova.context'] = context controller = images.ControllerV11(image_service=image_service) controller.detail(request) - mocker.VerifyAll() + self.mox.VerifyAll() def test_get_image_found(self): req = webob.Request.blank('/v1.0/images/123') @@ -969,8 +1027,48 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(res.status_int, 404) def test_create_image(self): + body = dict(image=dict(serverId='123', name='Snapshot 1')) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(200, response.status_int) + + def test_create_snapshot_no_name(self): + """Name is required for snapshots""" + body = dict(image=dict(serverId='123')) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + + def test_create_backup_no_name(self): + """Name is also required for backups""" + body = dict(image=dict(serverId='123', image_type='backup', + backup_type='daily', rotation=1)) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + + def test_create_backup_with_rotation_and_backup_type(self): + """The happy path for creating backups + + Creating a backup is an admin-only operation, as opposed to snapshots + which are available to anybody. + """ + # FIXME(sirp): teardown needed? + FLAGS.allow_admin_api = True - body = dict(image=dict(serverId='123', name='Backup 1')) + # FIXME(sirp): should the fact that backups are admin_only be a FLAG + body = dict(image=dict(serverId='123', image_type='backup', + name='Backup 1', + backup_type='daily', rotation=1)) req = webob.Request.blank('/v1.0/images') req.method = 'POST' req.body = json.dumps(body) @@ -978,9 +1076,54 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(200, response.status_int) + def test_create_backup_no_rotation(self): + """Rotation is required for backup requests""" + # FIXME(sirp): teardown needed? + FLAGS.allow_admin_api = True + + # FIXME(sirp): should the fact that backups are admin_only be a FLAG + body = dict(image=dict(serverId='123', name='daily', + image_type='backup', backup_type='daily')) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + + def test_create_backup_no_backup_type(self): + """Backup Type (daily or weekly) is required for backup requests""" + # FIXME(sirp): teardown needed? + FLAGS.allow_admin_api = True + + # FIXME(sirp): should the fact that backups are admin_only be a FLAG + body = dict(image=dict(serverId='123', name='daily', + image_type='backup', rotation=1)) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + + def test_create_image_with_invalid_image_type(self): + """Valid image_types are snapshot | daily | weekly""" + # FIXME(sirp): teardown needed? + FLAGS.allow_admin_api = True + + # FIXME(sirp): should the fact that backups are admin_only be a FLAG + body = dict(image=dict(serverId='123', image_type='monthly', + rotation=1)) + req = webob.Request.blank('/v1.0/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + def test_create_image_no_server_id(self): - body = dict(image=dict(name='Backup 1')) + body = dict(image=dict(name='Snapshot 1')) req = webob.Request.blank('/v1.0/images') req.method = 'POST' req.body = json.dumps(body) @@ -990,7 +1133,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_create_image_v1_1(self): - body = dict(image=dict(serverRef='123', name='Backup 1')) + body = dict(image=dict(serverRef='123', name='Snapshot 1')) req = webob.Request.blank('/v1.1/images') req.method = 'POST' req.body = json.dumps(body) @@ -1001,6 +1144,34 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_create_image_v1_1_actual_server_ref(self): serverRef = 'http://localhost/v1.1/servers/1' + serverBookmark = 'http://localhost/servers/1' + body = dict(image=dict(serverRef=serverRef, name='Backup 1')) + req = webob.Request.blank('/v1.1/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(200, response.status_int) + result = json.loads(response.body) + expected = { + 'id': 1, + 'links': [ + { + 'rel': 'self', + 'href': serverRef, + }, + { + 'rel': 'bookmark', + 'href': serverBookmark, + }, + ] + } + self.assertEqual(result['image']['server'], expected) + + def test_create_image_v1_1_actual_server_ref_port(self): + + serverRef = 'http://localhost:8774/v1.1/servers/1' + serverBookmark = 'http://localhost:8774/servers/1' body = dict(image=dict(serverRef=serverRef, name='Backup 1')) req = webob.Request.blank('/v1.1/images') req.method = 'POST' @@ -1009,7 +1180,20 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(200, response.status_int) result = json.loads(response.body) - self.assertEqual(result['image']['serverRef'], serverRef) + expected = { + 'id': 1, + 'links': [ + { + 'rel': 'self', + 'href': serverRef, + }, + { + 'rel': 'bookmark', + 'href': serverBookmark, + }, + ] + } + self.assertEqual(result['image']['server'], expected) def test_create_image_v1_1_server_ref_bad_hostname(self): @@ -1022,42 +1206,31 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(400, response.status_int) - def test_create_image_v1_1_xml_serialization(self): + def test_create_image_v1_1_no_server_ref(self): - body = dict(image=dict(serverRef='123', name='Backup 1')) + body = dict(image=dict(name='Snapshot 1')) req = webob.Request.blank('/v1.1/images') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" - req.headers["accept"] = "application/xml" response = req.get_response(fakes.wsgi_app()) - self.assertEqual(200, response.status_int) - resp_xml = minidom.parseString(response.body.replace(" ", "")) - expected_href = "http://localhost/v1.1/images/123" - expected_image = minidom.parseString(""" - <image - created="None" - id="123" - name="Backup 1" - serverRef="http://localhost/v1.1/servers/123" - status="ACTIVE" - updated="None" - xmlns="http://docs.openstack.org/compute/api/v1.1"> - <links> - <link href="%(expected_href)s" rel="self"/> - <link href="%(expected_href)s" rel="bookmark" - type="application/json" /> - <link href="%(expected_href)s" rel="bookmark" - type="application/xml" /> - </links> - </image> - """.replace(" ", "") % (locals())) + self.assertEqual(400, response.status_int) - self.assertEqual(expected_image.toxml(), resp_xml.toxml()) + def test_create_image_v1_1_server_ref_missing_version(self): - def test_create_image_v1_1_no_server_ref(self): + serverRef = 'http://localhost/servers/1' + body = dict(image=dict(serverRef=serverRef, name='Backup 1')) + req = webob.Request.blank('/v1.1/images') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + response = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, response.status_int) + + def test_create_image_v1_1_server_ref_missing_id(self): - body = dict(image=dict(name='Backup 1')) + serverRef = 'http://localhost/v1.1/servers' + body = dict(image=dict(serverRef=serverRef, name='Backup 1')) req = webob.Request.blank('/v1.1/images') req.method = 'POST' req.body = json.dumps(body) @@ -1084,19 +1257,21 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): status='active', properties={}) image_id += 1 - # Backup for User 1 - server_ref = 'http://localhost:8774/v1.1/servers/42' - backup_properties = {'instance_ref': server_ref, 'user_id': '1'} + # Snapshot for User 1 + server_ref = 'http://localhost/v1.1/servers/42' + snapshot_properties = {'instance_ref': server_ref, 'user_id': '1'} for status in ('queued', 'saving', 'active', 'killed'): - add_fixture(id=image_id, name='%s backup' % status, + add_fixture(id=image_id, name='%s snapshot' % status, is_public=False, status=status, - properties=backup_properties) + properties=snapshot_properties) image_id += 1 - # Backup for User 2 - other_backup_properties = {'instance_id': '43', 'user_id': '2'} - add_fixture(id=image_id, name='someone elses backup', is_public=False, - status='active', properties=other_backup_properties) + # Snapshot for User 2 + other_snapshot_properties = {'instance_id': '43', 'user_id': '2'} + add_fixture(id=image_id, name='someone elses snapshot', + is_public=False, status='active', + properties=other_snapshot_properties) + image_id += 1 # Image without a name @@ -1105,3 +1280,512 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): image_id += 1 return fixtures + + +class ImageXMLSerializationTest(test.TestCase): + + TIMESTAMP = "2010-10-11T10:30:22Z" + SERVER_HREF = 'http://localhost/v1.1/servers/123' + SERVER_BOOKMARK = 'http://localhost/servers/123' + IMAGE_HREF = 'http://localhost/v1.1/images/%s' + IMAGE_BOOKMARK = 'http://localhost/images/%s' + + def test_show(self): + serializer = images.ImageXMLSerializer() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'server': { + 'id': 1, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture, 'show') + actual = minidom.parseString(output.replace(" ", "")) + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_href = self.IMAGE_HREF % 1 + expected_bookmark = self.IMAGE_BOOKMARK % 1 + expected_now = self.TIMESTAMP + expected = minidom.parseString(""" + <image id="1" + xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom" + name="Image1" + updated="%(expected_now)s" + created="%(expected_now)s" + status="ACTIVE" + progress="80"> + <server id="1"> + <atom:link rel="self" href="%(expected_server_href)s"/> + <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/> + </server> + <metadata> + <meta key="key1"> + value1 + </meta> + </metadata> + <atom:link href="%(expected_href)s" rel="self"/> + <atom:link href="%(expected_bookmark)s" rel="bookmark"/> + </image> + """.replace(" ", "") % (locals())) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_show_zero_metadata(self): + serializer = images.ImageXMLSerializer() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'server': { + 'id': 1, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': {}, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture, 'show') + actual = minidom.parseString(output.replace(" ", "")) + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_href = self.IMAGE_HREF % 1 + expected_bookmark = self.IMAGE_BOOKMARK % 1 + expected_now = self.TIMESTAMP + expected = minidom.parseString(""" + <image id="1" + xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom" + name="Image1" + updated="%(expected_now)s" + created="%(expected_now)s" + status="ACTIVE"> + <server id="1"> + <atom:link rel="self" href="%(expected_server_href)s"/> + <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/> + </server> + <atom:link href="%(expected_href)s" rel="self"/> + <atom:link href="%(expected_bookmark)s" rel="bookmark"/> + </image> + """.replace(" ", "") % (locals())) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_show_image_no_metadata_key(self): + serializer = images.ImageXMLSerializer() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'server': { + 'id': 1, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture, 'show') + actual = minidom.parseString(output.replace(" ", "")) + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_href = self.IMAGE_HREF % 1 + expected_bookmark = self.IMAGE_BOOKMARK % 1 + expected_now = self.TIMESTAMP + expected = minidom.parseString(""" + <image id="1" + xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom" + name="Image1" + updated="%(expected_now)s" + created="%(expected_now)s" + status="ACTIVE"> + <server id="1"> + <atom:link rel="self" href="%(expected_server_href)s"/> + <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/> + </server> + <atom:link href="%(expected_href)s" rel="self"/> + <atom:link href="%(expected_bookmark)s" rel="bookmark"/> + </image> + """.replace(" ", "") % (locals())) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_show_no_server(self): + serializer = images.ImageXMLSerializer() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture, 'show') + actual = minidom.parseString(output.replace(" ", "")) + + expected_href = self.IMAGE_HREF % 1 + expected_bookmark = self.IMAGE_BOOKMARK % 1 + expected_now = self.TIMESTAMP + expected = minidom.parseString(""" + <image id="1" + xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom" + name="Image1" + updated="%(expected_now)s" + created="%(expected_now)s" + status="ACTIVE"> + <metadata> + <meta key="key1"> + value1 + </meta> + </metadata> + <atom:link href="%(expected_href)s" rel="self"/> + <atom:link href="%(expected_bookmark)s" rel="bookmark"/> + </image> + """.replace(" ", "") % (locals())) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_index(self): + serializer = images.ImageXMLSerializer() + + fixture = { + 'images': [ + { + 'id': 1, + 'name': 'Image1', + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + ], + }, + { + 'id': 2, + 'name': 'Image2', + 'links': [ + { + 'href': self.IMAGE_HREF % 2, + 'rel': 'self', + }, + ], + }, + ] + } + + output = serializer.serialize(fixture, 'index') + actual = minidom.parseString(output.replace(" ", "")) + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_href = self.IMAGE_HREF % 1 + expected_bookmark = self.IMAGE_BOOKMARK % 1 + expected_href_two = self.IMAGE_HREF % 2 + expected_bookmark_two = self.IMAGE_BOOKMARK % 2 + expected_now = self.TIMESTAMP + expected = minidom.parseString(""" + <images + xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom"> + <image id="1" name="Image1"> + <atom:link href="%(expected_href)s" rel="self"/> + </image> + <image id="2" name="Image2"> + <atom:link href="%(expected_href_two)s" rel="self"/> + </image> + </images> + """.replace(" ", "") % (locals())) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_index_zero_images(self): + serializer = images.ImageXMLSerializer() + + fixtures = { + 'images': [], + } + + output = serializer.serialize(fixtures, 'index') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <images + xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom" /> + """.replace(" ", "") % (locals())) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_detail(self): + serializer = images.ImageXMLSerializer() + + fixture = { + 'images': [ + { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'server': { + 'id': 1, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + { + 'id': 2, + 'name': 'Image2', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'SAVING', + 'progress': 80, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 2, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 2, + 'rel': 'bookmark', + }, + ], + }, + ] + } + + output = serializer.serialize(fixture, 'detail') + actual = minidom.parseString(output.replace(" ", "")) + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_href = self.IMAGE_HREF % 1 + expected_bookmark = self.IMAGE_BOOKMARK % 1 + expected_href_two = self.IMAGE_HREF % 2 + expected_bookmark_two = self.IMAGE_BOOKMARK % 2 + expected_now = self.TIMESTAMP + expected = minidom.parseString(""" + <images + xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom"> + <image id="1" + name="Image1" + updated="%(expected_now)s" + created="%(expected_now)s" + status="ACTIVE"> + <server id="1"> + <atom:link rel="self" href="%(expected_server_href)s"/> + <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/> + </server> + <atom:link href="%(expected_href)s" rel="self"/> + <atom:link href="%(expected_bookmark)s" rel="bookmark"/> + </image> + <image id="2" + name="Image2" + updated="%(expected_now)s" + created="%(expected_now)s" + status="SAVING" + progress="80"> + <metadata> + <meta key="key1"> + value1 + </meta> + </metadata> + <atom:link href="%(expected_href_two)s" rel="self"/> + <atom:link href="%(expected_bookmark_two)s" rel="bookmark"/> + </image> + </images> + """.replace(" ", "") % (locals())) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_create(self): + serializer = images.ImageXMLSerializer() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'SAVING', + 'progress': 80, + 'server': { + 'id': 1, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture, 'create') + actual = minidom.parseString(output.replace(" ", "")) + + expected_server_href = self.SERVER_HREF + expected_server_bookmark = self.SERVER_BOOKMARK + expected_href = self.IMAGE_HREF % 1 + expected_bookmark = self.IMAGE_BOOKMARK % 1 + expected_now = self.TIMESTAMP + expected = minidom.parseString(""" + <image id="1" + xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom" + name="Image1" + updated="%(expected_now)s" + created="%(expected_now)s" + status="SAVING" + progress="80"> + <server id="1"> + <atom:link rel="self" href="%(expected_server_href)s"/> + <atom:link rel="bookmark" href="%(expected_server_bookmark)s"/> + </server> + <metadata> + <meta key="key1"> + value1 + </meta> + </metadata> + <atom:link href="%(expected_href)s" rel="self"/> + <atom:link href="%(expected_bookmark)s" rel="bookmark"/> + </image> + """.replace(" ", "") % (locals())) + + self.assertEqual(expected.toxml(), actual.toxml()) diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py index 38c959fae..8a3fe681a 100644 --- a/nova/tests/api/openstack/test_limits.py +++ b/nova/tests/api/openstack/test_limits.py @@ -24,11 +24,12 @@ import stubout import time import unittest import webob - -from xml.dom.minidom import parseString +from xml.dom import minidom import nova.context from nova.api.openstack import limits +from nova.api.openstack import views +from nova import test TEST_LIMITS = [ @@ -166,7 +167,7 @@ class LimitsControllerV10Test(BaseLimitTestSuite): request = self._get_index_request("application/xml") response = request.get_response(self.controller) - expected = parseString(""" + expected = minidom.parseString(""" <limits xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"> <rate/> @@ -174,7 +175,7 @@ class LimitsControllerV10Test(BaseLimitTestSuite): </limits> """.replace(" ", "")) - body = parseString(response.body.replace(" ", "")) + body = minidom.parseString(response.body.replace(" ", "")) self.assertEqual(expected.toxml(), body.toxml()) @@ -184,7 +185,7 @@ class LimitsControllerV10Test(BaseLimitTestSuite): request = self._populate_limits(request) response = request.get_response(self.controller) - expected = parseString(""" + expected = minidom.parseString(""" <limits xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"> <rate> @@ -196,7 +197,7 @@ class LimitsControllerV10Test(BaseLimitTestSuite): <absolute/> </limits> """.replace(" ", "")) - body = parseString(response.body.replace(" ", "")) + body = minidom.parseString(response.body.replace(" ", "")) self.assertEqual(expected.toxml(), body.toxml()) @@ -210,6 +211,7 @@ class LimitsControllerV11Test(BaseLimitTestSuite): """Run before each test.""" BaseLimitTestSuite.setUp(self) self.controller = limits.create_resource('1.1') + self.maxDiff = None def _get_index_request(self, accept_header="application/json"): """Helper to set routing arguments.""" @@ -266,14 +268,14 @@ class LimitsControllerV11Test(BaseLimitTestSuite): "limit": [ { "verb": "GET", - "next-available": 0, + "next-available": "1970-01-01T00:00:00Z", "unit": "MINUTE", "value": 10, "remaining": 10, }, { "verb": "POST", - "next-available": 0, + "next-available": "1970-01-01T00:00:00Z", "unit": "HOUR", "value": 5, "remaining": 5, @@ -286,7 +288,7 @@ class LimitsControllerV11Test(BaseLimitTestSuite): "limit": [ { "verb": "GET", - "next-available": 0, + "next-available": "1970-01-01T00:00:00Z", "unit": "MINUTE", "value": 5, "remaining": 5, @@ -328,7 +330,7 @@ class LimitsControllerV11Test(BaseLimitTestSuite): "limit": [ { "verb": "GET", - "next-available": 0, + "next-available": "1970-01-01T00:00:00Z", "unit": "MINUTE", "value": 10, "remaining": 10, @@ -341,7 +343,7 @@ class LimitsControllerV11Test(BaseLimitTestSuite): "limit": [ { "verb": "GET", - "next-available": 0, + "next-available": "1970-01-01T00:00:00Z", "unit": "MINUTE", "value": 10, "remaining": 10, @@ -400,6 +402,10 @@ class LimitsControllerV11Test(BaseLimitTestSuite): self._test_index_absolute_limits_json(expected) +class TestLimiter(limits.Limiter): + pass + + class LimitMiddlewareTest(BaseLimitTestSuite): """ Tests for the `limits.RateLimitingMiddleware` class. @@ -413,10 +419,14 @@ class LimitMiddlewareTest(BaseLimitTestSuite): def setUp(self): """Prepare middleware for use through fake WSGI app.""" BaseLimitTestSuite.setUp(self) - _limits = [ - limits.Limit("GET", "*", ".*", 1, 60), - ] - self.app = limits.RateLimitingMiddleware(self._empty_app, _limits) + _limits = '(GET, *, .*, 1, MINUTE)' + self.app = limits.RateLimitingMiddleware(self._empty_app, _limits, + "%s.TestLimiter" % + self.__class__.__module__) + + def test_limit_class(self): + """Test that middleware selected correct limiter class.""" + assert isinstance(self.app._limiter, TestLimiter) def test_good_request(self): """Test successful GET request through middleware.""" @@ -450,7 +460,7 @@ class LimitMiddlewareTest(BaseLimitTestSuite): response = request.get_response(self.app) self.assertEqual(response.status_int, 403) - root = parseString(response.body).childNodes[0] + root = minidom.parseString(response.body).childNodes[0] expected = "Only 1 GET request(s) can be made to * every minute." details = root.getElementsByTagName("details") @@ -492,6 +502,72 @@ class LimitTest(BaseLimitTestSuite): self.assertEqual(4, limit.last_request) +class ParseLimitsTest(BaseLimitTestSuite): + """ + Tests for the default limits parser in the in-memory + `limits.Limiter` class. + """ + + def test_invalid(self): + """Test that parse_limits() handles invalid input correctly.""" + self.assertRaises(ValueError, limits.Limiter.parse_limits, + ';;;;;') + + def test_bad_rule(self): + """Test that parse_limits() handles bad rules correctly.""" + self.assertRaises(ValueError, limits.Limiter.parse_limits, + 'GET, *, .*, 20, minute') + + def test_missing_arg(self): + """Test that parse_limits() handles missing args correctly.""" + self.assertRaises(ValueError, limits.Limiter.parse_limits, + '(GET, *, .*, 20)') + + def test_bad_value(self): + """Test that parse_limits() handles bad values correctly.""" + self.assertRaises(ValueError, limits.Limiter.parse_limits, + '(GET, *, .*, foo, minute)') + + def test_bad_unit(self): + """Test that parse_limits() handles bad units correctly.""" + self.assertRaises(ValueError, limits.Limiter.parse_limits, + '(GET, *, .*, 20, lightyears)') + + def test_multiple_rules(self): + """Test that parse_limits() handles multiple rules correctly.""" + try: + l = limits.Limiter.parse_limits('(get, *, .*, 20, minute);' + '(PUT, /foo*, /foo.*, 10, hour);' + '(POST, /bar*, /bar.*, 5, second);' + '(Say, /derp*, /derp.*, 1, day)') + except ValueError, e: + assert False, str(e) + + # Make sure the number of returned limits are correct + self.assertEqual(len(l), 4) + + # Check all the verbs... + expected = ['GET', 'PUT', 'POST', 'SAY'] + self.assertEqual([t.verb for t in l], expected) + + # ...the URIs... + expected = ['*', '/foo*', '/bar*', '/derp*'] + self.assertEqual([t.uri for t in l], expected) + + # ...the regexes... + expected = ['.*', '/foo.*', '/bar.*', '/derp.*'] + self.assertEqual([t.regex for t in l], expected) + + # ...the values... + expected = [20, 10, 5, 1] + self.assertEqual([t.value for t in l], expected) + + # ...and the units... + expected = [limits.PER_MINUTE, limits.PER_HOUR, + limits.PER_SECOND, limits.PER_DAY] + self.assertEqual([t.unit for t in l], expected) + + class LimiterTest(BaseLimitTestSuite): """ Tests for the in-memory `limits.Limiter` class. @@ -500,7 +576,8 @@ class LimiterTest(BaseLimitTestSuite): def setUp(self): """Run before each test.""" BaseLimitTestSuite.setUp(self) - self.limiter = limits.Limiter(TEST_LIMITS) + userlimits = {'user:user3': ''} + self.limiter = limits.Limiter(TEST_LIMITS, **userlimits) def _check(self, num, verb, url, username=None): """Check and yield results from checks.""" @@ -605,6 +682,12 @@ class LimiterTest(BaseLimitTestSuite): results = list(self._check(10, "PUT", "/anything")) self.assertEqual(expected, results) + def test_user_limit(self): + """ + Test user-specific limits. + """ + self.assertEqual(self.limiter.levels['user3'], []) + def test_multiple_users(self): """ Tests involving multiple users. @@ -619,6 +702,11 @@ class LimiterTest(BaseLimitTestSuite): results = list(self._check(15, "PUT", "/anything", "user2")) self.assertEqual(expected, results) + # User3 + expected = [None] * 20 + results = list(self._check(20, "PUT", "/anything", "user3")) + self.assertEqual(expected, results) + self.time += 1.0 # User1 again @@ -818,3 +906,195 @@ class WsgiLimiterProxyTest(BaseLimitTestSuite): "made to /delayed every minute.") self.assertEqual((delay, error), expected) + + +class LimitsViewBuilderV11Test(test.TestCase): + + def setUp(self): + self.view_builder = views.limits.ViewBuilderV11() + self.rate_limits = [ + { + "URI": "*", + "regex": ".*", + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "resetTime": 1311272226 + }, + { + "URI": "*/servers", + "regex": "^/servers", + "value": 50, + "verb": "POST", + "remaining": 10, + "unit": "DAY", + "resetTime": 1311272226 + }, + ] + self.absolute_limits = { + "metadata_items": 1, + "injected_files": 5, + "injected_file_content_bytes": 5, + } + + def tearDown(self): + pass + + def test_build_limits(self): + expected_limits = { + "limits": { + "rate": [ + { + "uri": "*", + "regex": ".*", + "limit": [ + { + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-07-21T18:17:06Z" + }, + ] + }, + { + "uri": "*/servers", + "regex": "^/servers", + "limit": [ + { + "value": 50, + "verb": "POST", + "remaining": 10, + "unit": "DAY", + "next-available": "2011-07-21T18:17:06Z" + }, + ] + }, + ], + "absolute": { + "maxServerMeta": 1, + "maxImageMeta": 1, + "maxPersonality": 5, + "maxPersonalitySize": 5 + } + } + } + + output = self.view_builder.build(self.rate_limits, + self.absolute_limits) + self.assertDictMatch(output, expected_limits) + + def test_build_limits_empty_limits(self): + expected_limits = { + "limits": { + "rate": [], + "absolute": {} + } + } + + abs_limits = {} + rate_limits = [] + output = self.view_builder.build(rate_limits, abs_limits) + self.assertDictMatch(output, expected_limits) + + +class LimitsXMLSerializationTest(test.TestCase): + + def setUp(self): + self.maxDiff = None + + def tearDown(self): + pass + + def test_index(self): + serializer = limits.LimitsXMLSerializer() + + fixture = { + "limits": { + "rate": [ + { + "uri": "*", + "regex": ".*", + "limit": [ + { + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + }, + ] + }, + { + "uri": "*/servers", + "regex": "^/servers", + "limit": [ + { + "value": 50, + "verb": "POST", + "remaining": 10, + "unit": "DAY", + "next-available": "2011-12-15T22:42:45Z" + }, + ] + }, + ], + "absolute": { + "maxServerMeta": 1, + "maxImageMeta": 1, + "maxPersonality": 5, + "maxPersonalitySize": 10240 + } + } + } + + output = serializer.serialize(fixture, 'index') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <limits xmlns="http://docs.openstack.org/compute/api/v1.1"> + <rates> + <rate uri="*" regex=".*"> + <limit value="10" verb="POST" remaining="2" + unit="MINUTE" + next-available="2011-12-15T22:42:45Z"/> + </rate> + <rate uri="*/servers" regex="^/servers"> + <limit value="50" verb="POST" remaining="10" + unit="DAY" + next-available="2011-12-15T22:42:45Z"/> + </rate> + </rates> + <absolute> + <limit name="maxServerMeta" value="1"/> + <limit name="maxPersonality" value="5"/> + <limit name="maxImageMeta" value="1"/> + <limit name="maxPersonalitySize" value="10240"/> + </absolute> + </limits> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_index_no_limits(self): + serializer = limits.LimitsXMLSerializer() + + fixture = { + "limits": { + "rate": [], + "absolute": {} + } + } + + output = serializer.serialize(fixture, 'index') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <limits xmlns="http://docs.openstack.org/compute/api/v1.1"> + <rates /> + <absolute /> + </limits> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index b53c6c9be..56bc2cf10 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -30,8 +30,9 @@ from nova import flags from nova import test from nova import utils import nova.api.openstack -from nova.api.openstack import servers from nova.api.openstack import create_instance_helper +from nova.api.openstack import servers +from nova.api.openstack import wsgi import nova.compute.api from nova.compute import instance_types from nova.compute import power_state @@ -65,6 +66,18 @@ def return_server_by_uuid(context, uuid): return stub_instance(id, uuid=uuid) +def return_virtual_interface_by_instance(interfaces): + def _return_virtual_interface_by_instance(context, instance_id): + return interfaces + return _return_virtual_interface_by_instance + + +def return_virtual_interface_instance_nonexistant(interfaces): + def _return_virtual_interface_by_instance(context, instance_id): + raise exception.InstanceNotFound(instance_id=instance_id) + return _return_virtual_interface_by_instance + + def return_server_with_addresses(private, public): def _return_server(context, id): return stub_instance(id, private_address=private, @@ -72,6 +85,12 @@ def return_server_with_addresses(private, public): return _return_server +def return_server_with_interfaces(interfaces): + def _return_server(context, id): + return stub_instance(id, interfaces=interfaces) + return _return_server + + def return_server_with_power_state(power_state): def _return_server(context, id): return stub_instance(id, power_state=power_state) @@ -118,16 +137,19 @@ def instance_update(context, instance_id, kwargs): return stub_instance(instance_id) -def instance_address(context, instance_id): +def instance_addresses(context, instance_id): return None def stub_instance(id, user_id=1, private_address=None, public_addresses=None, host=None, power_state=0, reservation_id="", - uuid=FAKE_UUID): + uuid=FAKE_UUID, interfaces=None): metadata = [] metadata.append(InstanceMetadata(key='seq', value=id)) + if interfaces is None: + interfaces = [] + inst_type = instance_types.get_instance_type_by_flavor_id(1) if public_addresses is None: @@ -171,9 +193,10 @@ def stub_instance(id, user_id=1, private_address=None, public_addresses=None, "display_description": "", "locked": False, "metadata": metadata, - "uuid": uuid} + "uuid": uuid, + "virtual_interfaces": interfaces} - instance["fixed_ip"] = { + instance["fixed_ips"] = { "address": private_address, "floating_ips": [{"address":ip} for ip in public_addresses]} @@ -220,10 +243,10 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.db.api, 'instance_add_security_group', return_security_group) self.stubs.Set(nova.db.api, 'instance_update', instance_update) - self.stubs.Set(nova.db.api, 'instance_get_fixed_address', - instance_address) + self.stubs.Set(nova.db.api, 'instance_get_fixed_addresses', + instance_addresses) self.stubs.Set(nova.db.api, 'instance_get_floating_address', - instance_address) + instance_addresses) self.stubs.Set(nova.compute.API, 'pause', fake_compute_api) self.stubs.Set(nova.compute.API, 'unpause', fake_compute_api) self.stubs.Set(nova.compute.API, 'suspend', fake_compute_api) @@ -290,13 +313,7 @@ class ServersTest(test.TestCase): }, { "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/servers/1", - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/servers/1", + "href": "http://localhost/servers/1", }, ] @@ -417,22 +434,203 @@ class ServersTest(test.TestCase): self.assertEquals(ip.getAttribute('addr'), private) def test_get_server_by_id_with_addresses_v1_1(self): - private = "192.168.0.3" - public = ["1.2.3.4"] - new_return_server = return_server_with_addresses(private, public) + FLAGS.use_ipv6 = True + interfaces = [ + { + 'network': {'label': 'network_1'}, + 'fixed_ips': [ + {'address': '192.168.0.3'}, + {'address': '192.168.0.4'}, + ], + }, + { + 'network': {'label': 'network_2'}, + 'fixed_ips': [ + {'address': '172.19.0.1'}, + {'address': '172.19.0.2'}, + ], + 'fixed_ipv6': '2001:4860::12', + }, + ] + new_return_server = return_server_with_interfaces(interfaces) self.stubs.Set(nova.db.api, 'instance_get', new_return_server) + req = webob.Request.blank('/v1.1/servers/1') res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) self.assertEqual(res_dict['server']['id'], 1) self.assertEqual(res_dict['server']['name'], 'server1') addresses = res_dict['server']['addresses'] - self.assertEqual(len(addresses["public"]), len(public)) - self.assertEqual(addresses["public"][0], - {"version": 4, "addr": public[0]}) - self.assertEqual(len(addresses["private"]), 1) - self.assertEqual(addresses["private"][0], - {"version": 4, "addr": private}) + expected = { + 'network_1': [ + {'addr': '192.168.0.3', 'version': 4}, + {'addr': '192.168.0.4', 'version': 4}, + ], + 'network_2': [ + {'addr': '172.19.0.1', 'version': 4}, + {'addr': '172.19.0.2', 'version': 4}, + {'addr': '2001:4860::12', 'version': 6}, + ], + } + + self.assertEqual(addresses, expected) + + def test_get_server_by_id_with_addresses_v1_1_ipv6_disabled(self): + FLAGS.use_ipv6 = False + interfaces = [ + { + 'network': {'label': 'network_1'}, + 'fixed_ips': [ + {'address': '192.168.0.3'}, + {'address': '192.168.0.4'}, + ], + }, + { + 'network': {'label': 'network_2'}, + 'fixed_ips': [ + {'address': '172.19.0.1'}, + {'address': '172.19.0.2'}, + ], + 'fixed_ipv6': '2001:4860::12', + }, + ] + new_return_server = return_server_with_interfaces(interfaces) + self.stubs.Set(nova.db.api, 'instance_get', new_return_server) + + req = webob.Request.blank('/v1.1/servers/1') + res = req.get_response(fakes.wsgi_app()) + + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['name'], 'server1') + addresses = res_dict['server']['addresses'] + expected = { + 'network_1': [ + {'addr': '192.168.0.3', 'version': 4}, + {'addr': '192.168.0.4', 'version': 4}, + ], + 'network_2': [ + {'addr': '172.19.0.1', 'version': 4}, + {'addr': '172.19.0.2', 'version': 4}, + ], + } + + self.assertEqual(addresses, expected) + + def test_get_server_addresses_v1_1(self): + FLAGS.use_ipv6 = True + interfaces = [ + { + 'network': {'label': 'network_1'}, + 'fixed_ips': [ + {'address': '192.168.0.3'}, + {'address': '192.168.0.4'}, + ], + }, + { + 'network': {'label': 'network_2'}, + 'fixed_ips': [ + { + 'address': '172.19.0.1', + 'floating_ips': [ + {'address': '1.2.3.4'}, + ], + }, + {'address': '172.19.0.2'}, + ], + 'fixed_ipv6': '2001:4860::12', + }, + ] + + _return_vifs = return_virtual_interface_by_instance(interfaces) + self.stubs.Set(nova.db.api, + 'virtual_interface_get_by_instance', + _return_vifs) + + req = webob.Request.blank('/v1.1/servers/1/ips') + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + + expected = { + 'addresses': { + 'network_1': [ + {'version': 4, 'addr': '192.168.0.3'}, + {'version': 4, 'addr': '192.168.0.4'}, + ], + 'network_2': [ + {'version': 4, 'addr': '172.19.0.1'}, + {'version': 4, 'addr': '1.2.3.4'}, + {'version': 4, 'addr': '172.19.0.2'}, + {'version': 6, 'addr': '2001:4860::12'}, + ], + }, + } + + self.assertEqual(res_dict, expected) + + def test_get_server_addresses_single_network_v1_1(self): + FLAGS.use_ipv6 = True + interfaces = [ + { + 'network': {'label': 'network_1'}, + 'fixed_ips': [ + {'address': '192.168.0.3'}, + {'address': '192.168.0.4'}, + ], + }, + { + 'network': {'label': 'network_2'}, + 'fixed_ips': [ + { + 'address': '172.19.0.1', + 'floating_ips': [ + {'address': '1.2.3.4'}, + ], + }, + {'address': '172.19.0.2'}, + ], + 'fixed_ipv6': '2001:4860::12', + }, + ] + _return_vifs = return_virtual_interface_by_instance(interfaces) + self.stubs.Set(nova.db.api, + 'virtual_interface_get_by_instance', + _return_vifs) + + req = webob.Request.blank('/v1.1/servers/1/ips/network_2') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + expected = { + 'network_2': [ + {'version': 4, 'addr': '172.19.0.1'}, + {'version': 4, 'addr': '1.2.3.4'}, + {'version': 4, 'addr': '172.19.0.2'}, + {'version': 6, 'addr': '2001:4860::12'}, + ], + } + self.assertEqual(res_dict, expected) + + def test_get_server_addresses_nonexistant_network_v1_1(self): + _return_vifs = return_virtual_interface_by_instance([]) + self.stubs.Set(nova.db.api, + 'virtual_interface_get_by_instance', + _return_vifs) + + req = webob.Request.blank('/v1.1/servers/1/ips/network_0') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 404) + + def test_get_server_addresses_nonexistant_server_v1_1(self): + _return_vifs = return_virtual_interface_instance_nonexistant([]) + self.stubs.Set(nova.db.api, + 'virtual_interface_get_by_instance', + _return_vifs) + + req = webob.Request.blank('/v1.1/servers/600/ips') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 404) def test_get_server_list(self): req = webob.Request.blank('/v1.0/servers') @@ -514,13 +712,7 @@ class ServersTest(test.TestCase): }, { "rel": "bookmark", - "type": "application/json", - "href": "http://localhost/v1.1/servers/%d" % (i,), - }, - { - "rel": "bookmark", - "type": "application/xml", - "href": "http://localhost/v1.1/servers/%d" % (i,), + "href": "http://localhost/servers/%d" % (i,), }, ] @@ -596,7 +788,7 @@ class ServersTest(test.TestCase): def fake_method(*args, **kwargs): pass - def project_get_network(context, user_id): + def project_get_networks(context, user_id): return dict(id='1', host='localhost') def queue_get_for(context, *args): @@ -608,7 +800,8 @@ class ServersTest(test.TestCase): def image_id_from_hash(*args, **kwargs): return 2 - self.stubs.Set(nova.db.api, 'project_get_network', project_get_network) + self.stubs.Set(nova.db.api, 'project_get_networks', + project_get_networks) self.stubs.Set(nova.db.api, 'instance_create', instance_create) self.stubs.Set(nova.rpc, 'cast', fake_method) self.stubs.Set(nova.rpc, 'call', fake_method) @@ -749,6 +942,18 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_create_instance_no_server_entity(self): + self._setup_for_create_instance() + + body = {} + + req = webob.Request.blank('/v1.0/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 422) + def test_create_instance_whitespace_name(self): self._setup_for_create_instance() @@ -797,13 +1002,45 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) server = json.loads(res.body)['server'] self.assertEqual(16, len(server['adminPass'])) self.assertEqual('server_test', server['name']) self.assertEqual(1, server['id']) self.assertEqual(flavor_ref, server['flavorRef']) self.assertEqual(image_href, server['imageRef']) - self.assertEqual(res.status_int, 200) + + def test_create_instance_v1_1_invalid_flavor_href(self): + self._setup_for_create_instance() + + image_href = 'http://localhost/v1.1/images/2' + flavor_ref = 'http://localhost/v1.1/flavors/asdf' + body = dict(server=dict( + name='server_test', imageRef=image_href, flavorRef=flavor_ref, + metadata={'hello': 'world', 'open': 'stack'}, + personality={})) + req = webob.Request.blank('/v1.1/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_create_instance_v1_1_bad_flavor_href(self): + self._setup_for_create_instance() + + image_href = 'http://localhost/v1.1/images/2' + flavor_ref = 'http://localhost/v1.1/flavors/17' + body = dict(server=dict( + name='server_test', imageRef=image_href, flavorRef=flavor_ref, + metadata={'hello': 'world', 'open': 'stack'}, + personality={})) + req = webob.Request.blank('/v1.1/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) def test_create_instance_v1_1_bad_href(self): self._setup_for_create_instance() @@ -911,11 +1148,11 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) - def test_update_no_body(self): + def test_update_server_no_body(self): req = webob.Request.blank('/v1.0/servers/1') req.method = 'PUT' res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 422) + self.assertEqual(res.status_int, 400) def test_update_nonstring_name(self): """ Confirm that update is filtering params """ @@ -977,6 +1214,21 @@ class ServersTest(test.TestCase): self.assertEqual(mock_method.instance_id, '1') self.assertEqual(mock_method.password, 'bacon') + def test_update_server_no_body_v1_1(self): + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_update_server_name_v1_1(self): + req = webob.Request.blank('/v1.1/servers/1') + req.method = 'PUT' + req.content_type = 'application/json' + req.body = json.dumps({'server': {'name': 'new-name'}}) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 204) + self.assertEqual(res.body, '') + def test_update_server_adminPass_ignored_v1_1(self): inst_dict = dict(name='server_test', adminPass='bacon') self.body = json.dumps(dict(server=inst_dict)) @@ -995,6 +1247,7 @@ class ServersTest(test.TestCase): req.body = self.body res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 204) + self.assertEqual(res.body, '') def test_create_backup_schedules(self): req = webob.Request.blank('/v1.0/servers/1/backup_schedule') @@ -1443,6 +1696,57 @@ class ServersTest(test.TestCase): self.assertEqual(res.status, '202 Accepted') self.assertEqual(self.server_delete_called, True) + def test_rescue_accepted(self): + FLAGS.allow_admin_api = True + body = {} + + self.called = False + + def rescue_mock(*args, **kwargs): + self.called = True + + self.stubs.Set(nova.compute.api.API, 'rescue', rescue_mock) + req = webob.Request.blank('/v1.0/servers/1/rescue') + req.method = 'POST' + req.content_type = 'application/json' + + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(self.called, True) + self.assertEqual(res.status_int, 202) + + def test_rescue_raises_handled(self): + FLAGS.allow_admin_api = True + body = {} + + def rescue_mock(*args, **kwargs): + raise Exception('Who cares?') + + self.stubs.Set(nova.compute.api.API, 'rescue', rescue_mock) + req = webob.Request.blank('/v1.0/servers/1/rescue') + req.method = 'POST' + req.content_type = 'application/json' + + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 422) + + def test_delete_server_instance_v1_1(self): + req = webob.Request.blank('/v1.1/servers/1') + req.method = 'DELETE' + + self.server_delete_called = False + + def instance_destroy_mock(context, id): + self.server_delete_called = True + + self.stubs.Set(nova.db.api, 'instance_destroy', + instance_destroy_mock) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 204) + self.assertEqual(self.server_delete_called, True) + def test_resize_server(self): req = self.webreq('/1/action', 'POST', dict(resize=dict(flavorId=3))) @@ -1499,7 +1803,7 @@ class ServersTest(test.TestCase): self.stubs.Set(nova.compute.api.API, 'resize', resize_mock) res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 400) + self.assertEqual(res.status_int, 500) def test_resized_server_has_correct_status(self): req = self.webreq('/1', 'GET') @@ -1567,6 +1871,23 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_migrate_server(self): + """This is basically the same as resize, only we provide the `migrate` + attribute in the body's dict. + """ + req = self.webreq('/1/action', 'POST', dict(migrate=None)) + + self.resize_called = False + + def resize_mock(*args): + self.resize_called = True + + self.stubs.Set(nova.compute.api.API, 'resize', resize_mock) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + self.assertEqual(self.resize_called, True) + def test_shutdown_status(self): new_server = return_server_with_power_state(power_state.SHUTDOWN) self.stubs.Set(nova.db.api, 'instance_get', new_server) @@ -1601,7 +1922,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): "imageId": "1", "flavorId": "1", }} - self.assertEquals(request, expected) + self.assertEquals(request['body'], expected) def test_request_with_empty_metadata(self): serial_request = """ @@ -1616,7 +1937,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): "flavorId": "1", "metadata": {}, }} - self.assertEquals(request, expected) + self.assertEquals(request['body'], expected) def test_request_with_empty_personality(self): serial_request = """ @@ -1631,7 +1952,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): "flavorId": "1", "personality": [], }} - self.assertEquals(request, expected) + self.assertEquals(request['body'], expected) def test_request_with_empty_metadata_and_personality(self): serial_request = """ @@ -1648,7 +1969,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): "metadata": {}, "personality": [], }} - self.assertEquals(request, expected) + self.assertEquals(request['body'], expected) def test_request_with_empty_metadata_and_personality_reversed(self): serial_request = """ @@ -1665,7 +1986,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): "metadata": {}, "personality": [], }} - self.assertEquals(request, expected) + self.assertEquals(request['body'], expected) def test_request_with_one_personality(self): serial_request = """ @@ -1677,7 +1998,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): </server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = [{"path": "/etc/conf", "contents": "aabbccdd"}] - self.assertEquals(request["server"]["personality"], expected) + self.assertEquals(request['body']["server"]["personality"], expected) def test_request_with_two_personalities(self): serial_request = """ @@ -1688,7 +2009,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): request = self.deserializer.deserialize(serial_request, 'create') expected = [{"path": "/etc/conf", "contents": "aabbccdd"}, {"path": "/etc/sudoers", "contents": "abcd"}] - self.assertEquals(request["server"]["personality"], expected) + self.assertEquals(request['body']["server"]["personality"], expected) def test_request_second_personality_node_ignored(self): serial_request = """ @@ -1703,7 +2024,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): </server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = [{"path": "/etc/conf", "contents": "aabbccdd"}] - self.assertEquals(request["server"]["personality"], expected) + self.assertEquals(request['body']["server"]["personality"], expected) def test_request_with_one_personality_missing_path(self): serial_request = """ @@ -1712,7 +2033,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): <personality><file>aabbccdd</file></personality></server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = [{"contents": "aabbccdd"}] - self.assertEquals(request["server"]["personality"], expected) + self.assertEquals(request['body']["server"]["personality"], expected) def test_request_with_one_personality_empty_contents(self): serial_request = """ @@ -1721,7 +2042,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): <personality><file path="/etc/conf"></file></personality></server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = [{"path": "/etc/conf", "contents": ""}] - self.assertEquals(request["server"]["personality"], expected) + self.assertEquals(request['body']["server"]["personality"], expected) def test_request_with_one_personality_empty_contents_variation(self): serial_request = """ @@ -1730,7 +2051,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): <personality><file path="/etc/conf"/></personality></server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = [{"path": "/etc/conf", "contents": ""}] - self.assertEquals(request["server"]["personality"], expected) + self.assertEquals(request['body']["server"]["personality"], expected) def test_request_with_one_metadata(self): serial_request = """ @@ -1742,7 +2063,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): </server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = {"alpha": "beta"} - self.assertEquals(request["server"]["metadata"], expected) + self.assertEquals(request['body']["server"]["metadata"], expected) def test_request_with_two_metadata(self): serial_request = """ @@ -1755,7 +2076,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): </server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = {"alpha": "beta", "foo": "bar"} - self.assertEquals(request["server"]["metadata"], expected) + self.assertEquals(request['body']["server"]["metadata"], expected) def test_request_with_metadata_missing_value(self): serial_request = """ @@ -1767,7 +2088,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): </server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = {"alpha": ""} - self.assertEquals(request["server"]["metadata"], expected) + self.assertEquals(request['body']["server"]["metadata"], expected) def test_request_with_two_metadata_missing_value(self): serial_request = """ @@ -1780,7 +2101,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): </server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = {"alpha": "", "delta": ""} - self.assertEquals(request["server"]["metadata"], expected) + self.assertEquals(request['body']["server"]["metadata"], expected) def test_request_with_metadata_missing_key(self): serial_request = """ @@ -1792,7 +2113,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): </server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = {"": "beta"} - self.assertEquals(request["server"]["metadata"], expected) + self.assertEquals(request['body']["server"]["metadata"], expected) def test_request_with_two_metadata_missing_key(self): serial_request = """ @@ -1805,7 +2126,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): </server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = {"": "gamma"} - self.assertEquals(request["server"]["metadata"], expected) + self.assertEquals(request['body']["server"]["metadata"], expected) def test_request_with_metadata_duplicate_key(self): serial_request = """ @@ -1818,7 +2139,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): </server>""" request = self.deserializer.deserialize(serial_request, 'create') expected = {"foo": "baz"} - self.assertEquals(request["server"]["metadata"], expected) + self.assertEquals(request['body']["server"]["metadata"], expected) def test_canonical_request_from_docs(self): serial_request = """ @@ -1864,7 +2185,7 @@ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==""", ], }} request = self.deserializer.deserialize(serial_request, 'create') - self.assertEqual(request, expected) + self.assertEqual(request['body'], expected) def test_request_xmlser_with_flavor_image_href(self): serial_request = """ @@ -1874,12 +2195,68 @@ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==""", flavorRef="http://localhost:8774/v1.1/flavors/1"> </server>""" request = self.deserializer.deserialize(serial_request, 'create') - self.assertEquals(request["server"]["flavorRef"], + self.assertEquals(request['body']["server"]["flavorRef"], "http://localhost:8774/v1.1/flavors/1") - self.assertEquals(request["server"]["imageRef"], + self.assertEquals(request['body']["server"]["imageRef"], "http://localhost:8774/v1.1/images/1") +class TextAddressesXMLSerialization(test.TestCase): + + serializer = nova.api.openstack.ips.IPXMLSerializer() + + def test_show(self): + fixture = { + 'network_2': [ + {'addr': '192.168.0.1', 'version': 4}, + {'addr': 'fe80::beef', 'version': 6}, + ], + } + output = self.serializer.serialize(fixture, 'show') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <network xmlns="http://docs.openstack.org/compute/api/v1.1" + id="network_2"> + <ip version="4" addr="192.168.0.1"/> + <ip version="6" addr="fe80::beef"/> + </network> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_index(self): + fixture = { + 'addresses': { + 'network_1': [ + {'addr': '192.168.0.3', 'version': 4}, + {'addr': '192.168.0.5', 'version': 4}, + ], + 'network_2': [ + {'addr': '192.168.0.1', 'version': 4}, + {'addr': 'fe80::beef', 'version': 6}, + ], + }, + } + output = self.serializer.serialize(fixture, 'index') + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <addresses xmlns="http://docs.openstack.org/compute/api/v1.1"> + <network id="network_2"> + <ip version="4" addr="192.168.0.1"/> + <ip version="6" addr="fe80::beef"/> + </network> + <network id="network_1"> + <ip version="4" addr="192.168.0.3"/> + <ip version="4" addr="192.168.0.5"/> + </network> + </addresses> + """.replace(" ", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + class TestServerInstanceCreation(test.TestCase): def setUp(self): @@ -1941,7 +2318,7 @@ class TestServerInstanceCreation(test.TestCase): def _get_create_request_json(self, body_dict): req = webob.Request.blank('/v1.0/servers') - req.content_type = 'application/json' + req.headers['Content-Type'] = 'application/json' req.method = 'POST' req.body = json.dumps(body_dict) return req diff --git a/nova/tests/api/openstack/test_versions.py b/nova/tests/api/openstack/test_versions.py index fd8d50904..da964ee1f 100644 --- a/nova/tests/api/openstack/test_versions.py +++ b/nova/tests/api/openstack/test_versions.py @@ -21,6 +21,7 @@ import webob from nova import context from nova import test from nova.tests.api.openstack import fakes +from nova.api.openstack import versions from nova.api.openstack import views @@ -43,19 +44,21 @@ class VersionsTest(test.TestCase): { "id": "v1.1", "status": "CURRENT", + "updated": "2011-07-18T11:30:00Z", "links": [ { "rel": "self", - "href": "http://localhost/v1.1", + "href": "http://localhost/v1.1/", }], }, { "id": "v1.0", "status": "DEPRECATED", + "updated": "2010-10-09T11:30:00Z", "links": [ { "rel": "self", - "href": "http://localhost/v1.0", + "href": "http://localhost/v1.0/", }], }, ] @@ -69,15 +72,12 @@ class VersionsTest(test.TestCase): self.assertEqual(res.content_type, "application/xml") expected = """<versions> - <version id="v1.1" status="CURRENT"> - <links> - <link href="http://localhost/v1.1" rel="self"/> - </links> + <version id="v1.1" status="CURRENT" updated="2011-07-18T11:30:00Z"> + <atom:link href="http://localhost/v1.1/" rel="self"/> </version> - <version id="v1.0" status="DEPRECATED"> - <links> - <link href="http://localhost/v1.0" rel="self"/> - </links> + <version id="v1.0" status="DEPRECATED" + updated="2010-10-09T11:30:00Z"> + <atom:link href="http://localhost/v1.0/" rel="self"/> </version> </versions>""".replace(" ", "").replace("\n", "") @@ -85,21 +85,64 @@ class VersionsTest(test.TestCase): self.assertEqual(expected, actual) + def test_get_version_list_atom(self): + req = webob.Request.blank('/') + req.accept = "application/atom+xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/atom+xml") + + expected = """ + <feed xmlns="http://www.w3.org/2005/Atom"> + <title type="text">Available API Versions</title> + <updated>2011-07-18T11:30:00Z</updated> + <id>http://localhost/</id> + <author> + <name>Rackspace</name> + <uri>http://www.rackspace.com/</uri> + </author> + <link href="http://localhost/" rel="self"/> + <entry> + <id>http://localhost/v1.1/</id> + <title type="text">Version v1.1</title> + <updated>2011-07-18T11:30:00Z</updated> + <link href="http://localhost/v1.1/" rel="self"/> + <content type="text"> + Version v1.1 CURRENT (2011-07-18T11:30:00Z) + </content> + </entry> + <entry> + <id>http://localhost/v1.0/</id> + <title type="text">Version v1.0</title> + <updated>2010-10-09T11:30:00Z</updated> + <link href="http://localhost/v1.0/" rel="self"/> + <content type="text"> + Version v1.0 DEPRECATED (2010-10-09T11:30:00Z) + </content> + </entry> + </feed> + """.replace(" ", "").replace("\n", "") + + actual = res.body.replace(" ", "").replace("\n", "") + + self.assertEqual(expected, actual) + def test_view_builder(self): base_url = "http://example.org/" version_data = { "id": "3.2.1", "status": "CURRENT", - } + "updated": "2011-07-18T11:30:00Z"} expected = { "id": "3.2.1", "status": "CURRENT", + "updated": "2011-07-18T11:30:00Z", "links": [ { "rel": "self", - "href": "http://example.org/3.2.1", + "href": "http://example.org/3.2.1/", }, ], } @@ -113,9 +156,99 @@ class VersionsTest(test.TestCase): base_url = "http://example.org/app/" version_number = "v1.4.6" - expected = "http://example.org/app/v1.4.6" + expected = "http://example.org/app/v1.4.6/" builder = views.versions.ViewBuilder(base_url) actual = builder.generate_href(version_number) self.assertEqual(actual, expected) + + def test_xml_serializer(self): + versions_data = { + 'versions': [ + { + "id": "2.7.1", + "updated": "2011-07-18T11:30:00Z", + "status": "DEPRECATED", + "links": [ + { + "rel": "self", + "href": "http://test/2.7.1", + }, + ], + }, + ] + } + + expected = """ + <versions> + <version id="2.7.1" status="DEPRECATED" + updated="2011-07-18T11:30:00Z"> + <atom:link href="http://test/2.7.1" rel="self"/> + </version> + </versions>""".replace(" ", "").replace("\n", "") + + serializer = versions.VersionsXMLSerializer() + response = serializer.default(versions_data) + response = response.replace(" ", "").replace("\n", "") + self.assertEqual(expected, response) + + def test_atom_serializer(self): + versions_data = { + 'versions': [ + { + "id": "2.9.8", + "updated": "2011-07-20T11:40:00Z", + "status": "CURRENT", + "links": [ + { + "rel": "self", + "href": "http://test/2.9.8", + }, + ], + }, + ] + } + + expected = """ + <feed xmlns="http://www.w3.org/2005/Atom"> + <title type="text"> + Available API Versions + </title> + <updated> + 2011-07-20T11:40:00Z + </updated> + <id> + http://test/ + </id> + <author> + <name> + Rackspace + </name> + <uri> + http://www.rackspace.com/ + </uri> + </author> + <link href="http://test/" rel="self"/> + <entry> + <id> + http://test/2.9.8 + </id> + <title type="text"> + Version 2.9.8 + </title> + <updated> + 2011-07-20T11:40:00Z + </updated> + <link href="http://test/2.9.8" rel="self"/> + <content type="text"> + Version 2.9.8 CURRENT (2011-07-20T11:40:00Z) + </content> + </entry> + </feed>""".replace(" ", "").replace("\n", "") + + serializer = versions.VersionsAtomSerializer() + response = serializer.default(versions_data) + print response + response = response.replace(" ", "").replace("\n", "") + self.assertEqual(expected, response) diff --git a/nova/tests/api/openstack/test_wsgi.py b/nova/tests/api/openstack/test_wsgi.py index 73a26a087..6dea78d17 100644 --- a/nova/tests/api/openstack/test_wsgi.py +++ b/nova/tests/api/openstack/test_wsgi.py @@ -12,8 +12,7 @@ class RequestTest(test.TestCase): def test_content_type_missing(self): request = wsgi.Request.blank('/tests/123', method='POST') request.body = "<body />" - self.assertRaises(exception.InvalidContentType, - request.get_content_type) + self.assertEqual(None, request.get_content_type()) def test_content_type_unsupported(self): request = wsgi.Request.blank('/tests/123', method='POST') @@ -76,24 +75,48 @@ class RequestTest(test.TestCase): self.assertEqual(result, "application/json") -class DictSerializerTest(test.TestCase): +class ActionDispatcherTest(test.TestCase): def test_dispatch(self): - serializer = wsgi.DictSerializer() + serializer = wsgi.ActionDispatcher() + serializer.create = lambda x: 'pants' + self.assertEqual(serializer.dispatch({}, action='create'), 'pants') + + def test_dispatch_action_None(self): + serializer = wsgi.ActionDispatcher() serializer.create = lambda x: 'pants' serializer.default = lambda x: 'trousers' - self.assertEqual(serializer.serialize({}, 'create'), 'pants') + self.assertEqual(serializer.dispatch({}, action=None), 'trousers') def test_dispatch_default(self): - serializer = wsgi.DictSerializer() + serializer = wsgi.ActionDispatcher() serializer.create = lambda x: 'pants' serializer.default = lambda x: 'trousers' - self.assertEqual(serializer.serialize({}, 'update'), 'trousers') + self.assertEqual(serializer.dispatch({}, action='update'), 'trousers') - def test_dispatch_action_None(self): + +class ResponseHeadersSerializerTest(test.TestCase): + def test_default(self): + serializer = wsgi.ResponseHeadersSerializer() + response = webob.Response() + serializer.serialize(response, {'v': '123'}, 'asdf') + self.assertEqual(response.status_int, 200) + + def test_custom(self): + class Serializer(wsgi.ResponseHeadersSerializer): + def update(self, response, data): + response.status_int = 404 + response.headers['X-Custom-Header'] = data['v'] + serializer = Serializer() + response = webob.Response() + serializer.serialize(response, {'v': '123'}, 'update') + self.assertEqual(response.status_int, 404) + self.assertEqual(response.headers['X-Custom-Header'], '123') + + +class DictSerializerTest(test.TestCase): + def test_dispatch_default(self): serializer = wsgi.DictSerializer() - serializer.create = lambda x: 'pants' - serializer.default = lambda x: 'trousers' - self.assertEqual(serializer.serialize({}, None), 'trousers') + self.assertEqual(serializer.serialize({}, 'update'), '') class XMLDictSerializerTest(test.TestCase): @@ -117,23 +140,9 @@ class JSONDictSerializerTest(test.TestCase): class TextDeserializerTest(test.TestCase): - def test_dispatch(self): - deserializer = wsgi.TextDeserializer() - deserializer.create = lambda x: 'pants' - deserializer.default = lambda x: 'trousers' - self.assertEqual(deserializer.deserialize({}, 'create'), 'pants') - def test_dispatch_default(self): deserializer = wsgi.TextDeserializer() - deserializer.create = lambda x: 'pants' - deserializer.default = lambda x: 'trousers' - self.assertEqual(deserializer.deserialize({}, 'update'), 'trousers') - - def test_dispatch_action_None(self): - deserializer = wsgi.TextDeserializer() - deserializer.create = lambda x: 'pants' - deserializer.default = lambda x: 'trousers' - self.assertEqual(deserializer.deserialize({}, None), 'trousers') + self.assertEqual(deserializer.deserialize({}, 'update'), {}) class JSONDeserializerTest(test.TestCase): @@ -144,12 +153,17 @@ class JSONDeserializerTest(test.TestCase): "bs": ["1", "2", "3", {"c": {"c1": "1"}}], "d": {"e": "1"}, "f": "1"}}""" - as_dict = dict(a={ - 'a1': '1', - 'a2': '2', - 'bs': ['1', '2', '3', {'c': dict(c1='1')}], - 'd': {'e': '1'}, - 'f': '1'}) + as_dict = { + 'body': { + 'a': { + 'a1': '1', + 'a2': '2', + 'bs': ['1', '2', '3', {'c': {'c1': '1'}}], + 'd': {'e': '1'}, + 'f': '1', + }, + }, + } deserializer = wsgi.JSONDeserializer() self.assertEqual(deserializer.deserialize(data), as_dict) @@ -163,23 +177,44 @@ class XMLDeserializerTest(test.TestCase): <f>1</f> </a> """.strip() - as_dict = dict(a={ - 'a1': '1', - 'a2': '2', - 'bs': ['1', '2', '3', {'c': dict(c1='1')}], - 'd': {'e': '1'}, - 'f': '1'}) + as_dict = { + 'body': { + 'a': { + 'a1': '1', + 'a2': '2', + 'bs': ['1', '2', '3', {'c': {'c1': '1'}}], + 'd': {'e': '1'}, + 'f': '1', + }, + }, + } metadata = {'plurals': {'bs': 'b', 'ts': 't'}} deserializer = wsgi.XMLDeserializer(metadata=metadata) self.assertEqual(deserializer.deserialize(xml), as_dict) def test_xml_empty(self): xml = """<a></a>""" - as_dict = {"a": {}} + as_dict = {"body": {"a": {}}} deserializer = wsgi.XMLDeserializer() self.assertEqual(deserializer.deserialize(xml), as_dict) +class RequestHeadersDeserializerTest(test.TestCase): + def test_default(self): + deserializer = wsgi.RequestHeadersDeserializer() + req = wsgi.Request.blank('/') + self.assertEqual(deserializer.deserialize(req, 'asdf'), {}) + + def test_custom(self): + class Deserializer(wsgi.RequestHeadersDeserializer): + def update(self, request): + return {'a': request.headers['X-Custom-Header']} + deserializer = Deserializer() + req = wsgi.Request.blank('/') + req.headers['X-Custom-Header'] = 'b' + self.assertEqual(deserializer.deserialize(req, 'update'), {'a': 'b'}) + + class ResponseSerializerTest(test.TestCase): def setUp(self): class JSONSerializer(object): @@ -190,29 +225,43 @@ class ResponseSerializerTest(test.TestCase): def serialize(self, data, action='default'): return 'pew_xml' - self.serializers = { + class HeadersSerializer(object): + def serialize(self, response, data, action): + response.status_int = 404 + + self.body_serializers = { 'application/json': JSONSerializer(), 'application/XML': XMLSerializer(), } - self.serializer = wsgi.ResponseSerializer(serializers=self.serializers) + self.serializer = wsgi.ResponseSerializer(self.body_serializers, + HeadersSerializer()) def tearDown(self): pass def test_get_serializer(self): - self.assertEqual(self.serializer.get_serializer('application/json'), - self.serializers['application/json']) + ctype = 'application/json' + self.assertEqual(self.serializer.get_body_serializer(ctype), + self.body_serializers[ctype]) def test_get_serializer_unknown_content_type(self): self.assertRaises(exception.InvalidContentType, - self.serializer.get_serializer, + self.serializer.get_body_serializer, 'application/unknown') def test_serialize_response(self): response = self.serializer.serialize({}, 'application/json') self.assertEqual(response.headers['Content-Type'], 'application/json') self.assertEqual(response.body, 'pew_json') + self.assertEqual(response.status_int, 404) + + def test_serialize_response_None(self): + response = self.serializer.serialize(None, 'application/json') + print response + self.assertEqual(response.headers['Content-Type'], 'application/json') + self.assertEqual(response.body, '') + self.assertEqual(response.status_int, 404) def test_serialize_response_dict_to_unknown_content_type(self): self.assertRaises(exception.InvalidContentType, @@ -230,24 +279,23 @@ class RequestDeserializerTest(test.TestCase): def deserialize(self, data, action='default'): return 'pew_xml' - self.deserializers = { + self.body_deserializers = { 'application/json': JSONDeserializer(), 'application/XML': XMLDeserializer(), } - self.deserializer = wsgi.RequestDeserializer( - deserializers=self.deserializers) + self.deserializer = wsgi.RequestDeserializer(self.body_deserializers) def tearDown(self): pass def test_get_deserializer(self): - expected = self.deserializer.get_deserializer('application/json') - self.assertEqual(expected, self.deserializers['application/json']) + expected = self.deserializer.get_body_deserializer('application/json') + self.assertEqual(expected, self.body_deserializers['application/json']) def test_get_deserializer_unknown_content_type(self): self.assertRaises(exception.InvalidContentType, - self.deserializer.get_deserializer, + self.deserializer.get_body_deserializer, 'application/unknown') def test_get_expected_content_type(self): diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py index 098577e4c..6a6e13d93 100644 --- a/nova/tests/api/openstack/test_zones.py +++ b/nova/tests/api/openstack/test_zones.py @@ -34,7 +34,7 @@ FLAGS.verbose = True def zone_get(context, zone_id): return dict(id=1, api_url='http://example.com', username='bob', - password='xxx') + password='xxx', weight_scale=1.0, weight_offset=0.0) def zone_create(context, values): @@ -57,9 +57,9 @@ def zone_delete(context, zone_id): def zone_get_all_scheduler(*args): return [ dict(id=1, api_url='http://example.com', username='bob', - password='xxx'), + password='xxx', weight_scale=1.0, weight_offset=0.0), dict(id=2, api_url='http://example.org', username='alice', - password='qwerty'), + password='qwerty', weight_scale=1.0, weight_offset=0.0), ] @@ -70,9 +70,9 @@ def zone_get_all_scheduler_empty(*args): def zone_get_all_db(context): return [ dict(id=1, api_url='http://example.com', username='bob', - password='xxx'), + password='xxx', weight_scale=1.0, weight_offset=0.0), dict(id=2, api_url='http://example.org', username='alice', - password='qwerty'), + password='qwerty', weight_scale=1.0, weight_offset=0.0), ] diff --git a/nova/tests/db/fakes.py b/nova/tests/db/fakes.py index 8bdea359a..19028a451 100644 --- a/nova/tests/db/fakes.py +++ b/nova/tests/db/fakes.py @@ -20,10 +20,327 @@ import time from nova import db +from nova import exception from nova import test from nova import utils +class FakeModel(object): + """Stubs out for model.""" + def __init__(self, values): + self.values = values + + def __getattr__(self, name): + return self.values[name] + + def __getitem__(self, key): + if key in self.values: + return self.values[key] + else: + raise NotImplementedError() + + def __repr__(self): + return '<FakeModel: %s>' % self.values + + +def stub_out(stubs, funcs): + """Set the stubs in mapping in the db api.""" + for func in funcs: + func_name = '_'.join(func.__name__.split('_')[1:]) + stubs.Set(db, func_name, func) + + +def stub_out_db_network_api(stubs): + network_fields = {'id': 0, + 'cidr': '192.168.0.0/24', + 'netmask': '255.255.255.0', + 'cidr_v6': 'dead:beef::/64', + 'netmask_v6': '64', + 'project_id': 'fake', + 'label': 'fake', + 'gateway': '192.168.0.1', + 'bridge': 'fa0', + 'bridge_interface': 'fake_fa0', + 'broadcast': '192.168.0.255', + 'gateway_v6': 'dead:beef::1', + 'dns': '192.168.0.1', + 'vlan': None, + 'host': None, + 'injected': False, + 'vpn_public_address': '192.168.0.2'} + + fixed_ip_fields = {'id': 0, + 'network_id': 0, + 'network': FakeModel(network_fields), + 'address': '192.168.0.100', + 'instance': False, + 'instance_id': 0, + 'allocated': False, + 'virtual_interface_id': 0, + 'virtual_interface': None, + 'floating_ips': []} + + flavor_fields = {'id': 0, + 'rxtx_cap': 3} + + floating_ip_fields = {'id': 0, + 'address': '192.168.1.100', + 'fixed_ip_id': None, + 'fixed_ip': None, + 'project_id': None, + 'auto_assigned': False} + + virtual_interface_fields = {'id': 0, + 'address': 'DE:AD:BE:EF:00:00', + 'network_id': 0, + 'instance_id': 0, + 'network': FakeModel(network_fields)} + + fixed_ips = [fixed_ip_fields] + floating_ips = [floating_ip_fields] + virtual_interfacees = [virtual_interface_fields] + networks = [network_fields] + + def fake_floating_ip_allocate_address(context, project_id): + ips = filter(lambda i: i['fixed_ip_id'] == None \ + and i['project_id'] == None, + floating_ips) + if not ips: + raise exception.NoMoreFloatingIps() + ips[0]['project_id'] = project_id + return FakeModel(ips[0]) + + def fake_floating_ip_deallocate(context, address): + ips = filter(lambda i: i['address'] == address, + floating_ips) + if ips: + ips[0]['project_id'] = None + ips[0]['auto_assigned'] = False + + def fake_floating_ip_disassociate(context, address): + ips = filter(lambda i: i['address'] == address, + floating_ips) + if ips: + fixed_ip_address = None + if ips[0]['fixed_ip']: + fixed_ip_address = ips[0]['fixed_ip']['address'] + ips[0]['fixed_ip'] = None + return fixed_ip_address + + def fake_floating_ip_fixed_ip_associate(context, floating_address, + fixed_address): + float = filter(lambda i: i['address'] == floating_address, + floating_ips) + fixed = filter(lambda i: i['address'] == fixed_address, + fixed_ips) + if float and fixed: + float[0]['fixed_ip'] = fixed[0] + float[0]['fixed_ip_id'] = fixed[0]['id'] + + def fake_floating_ip_get_all_by_host(context, host): + # TODO(jkoelker): Once we get the patches that remove host from + # the floating_ip table, we'll need to stub + # this out + pass + + def fake_floating_ip_get_by_address(context, address): + if isinstance(address, FakeModel): + # NOTE(tr3buchet): yo dawg, i heard you like addresses + address = address['address'] + ips = filter(lambda i: i['address'] == address, + floating_ips) + if not ips: + raise exception.FloatingIpNotFoundForAddress(address=address) + return FakeModel(ips[0]) + + def fake_floating_ip_set_auto_assigned(contex, address): + ips = filter(lambda i: i['address'] == address, + floating_ips) + if ips: + ips[0]['auto_assigned'] = True + + def fake_fixed_ip_associate(context, address, instance_id): + ips = filter(lambda i: i['address'] == address, + fixed_ips) + if not ips: + raise exception.NoMoreFixedIps() + ips[0]['instance'] = True + ips[0]['instance_id'] = instance_id + + def fake_fixed_ip_associate_pool(context, network_id, instance_id): + ips = filter(lambda i: (i['network_id'] == network_id \ + or i['network_id'] is None) \ + and not i['instance'], + fixed_ips) + if not ips: + raise exception.NoMoreFixedIps() + ips[0]['instance'] = True + ips[0]['instance_id'] = instance_id + return ips[0]['address'] + + def fake_fixed_ip_create(context, values): + ip = dict(fixed_ip_fields) + ip['id'] = max([i['id'] for i in fixed_ips] or [-1]) + 1 + for key in values: + ip[key] = values[key] + return ip['address'] + + def fake_fixed_ip_disassociate(context, address): + ips = filter(lambda i: i['address'] == address, + fixed_ips) + if ips: + ips[0]['instance_id'] = None + ips[0]['instance'] = None + ips[0]['virtual_interface'] = None + ips[0]['virtual_interface_id'] = None + + def fake_fixed_ip_disassociate_all_by_timeout(context, host, time): + return 0 + + def fake_fixed_ip_get_by_instance(context, instance_id): + ips = filter(lambda i: i['instance_id'] == instance_id, + fixed_ips) + return [FakeModel(i) for i in ips] + + def fake_fixed_ip_get_by_address(context, address): + ips = filter(lambda i: i['address'] == address, + fixed_ips) + if ips: + return FakeModel(ips[0]) + + def fake_fixed_ip_get_network(context, address): + ips = filter(lambda i: i['address'] == address, + fixed_ips) + if ips: + nets = filter(lambda n: n['id'] == ips[0]['network_id'], + networks) + if nets: + return FakeModel(nets[0]) + + def fake_fixed_ip_update(context, address, values): + ips = filter(lambda i: i['address'] == address, + fixed_ips) + if ips: + for key in values: + ips[0][key] = values[key] + if key == 'virtual_interface_id': + vif = filter(lambda x: x['id'] == values[key], + virtual_interfacees) + if not vif: + continue + fixed_ip_fields['virtual_interface'] = FakeModel(vif[0]) + + def fake_instance_type_get(context, id): + if flavor_fields['id'] == id: + return FakeModel(flavor_fields) + + def fake_virtual_interface_create(context, values): + vif = dict(virtual_interface_fields) + vif['id'] = max([m['id'] for m in virtual_interfacees] or [-1]) + 1 + for key in values: + vif[key] = values[key] + return FakeModel(vif) + + def fake_virtual_interface_delete_by_instance(context, instance_id): + addresses = [m for m in virtual_interfacees \ + if m['instance_id'] == instance_id] + try: + for address in addresses: + virtual_interfacees.remove(address) + except ValueError: + pass + + def fake_virtual_interface_get_by_instance(context, instance_id): + return [FakeModel(m) for m in virtual_interfacees \ + if m['instance_id'] == instance_id] + + def fake_virtual_interface_get_by_instance_and_network(context, + instance_id, + network_id): + vif = filter(lambda m: m['instance_id'] == instance_id and \ + m['network_id'] == network_id, + virtual_interfacees) + if not vif: + return None + return FakeModel(vif[0]) + + def fake_network_create_safe(context, values): + net = dict(network_fields) + net['id'] = max([n['id'] for n in networks] or [-1]) + 1 + for key in values: + net[key] = values[key] + return FakeModel(net) + + def fake_network_get(context, network_id): + net = filter(lambda n: n['id'] == network_id, networks) + if not net: + return None + return FakeModel(net[0]) + + def fake_network_get_all(context): + return [FakeModel(n) for n in networks] + + def fake_network_get_all_by_host(context, host): + nets = filter(lambda n: n['host'] == host, networks) + return [FakeModel(n) for n in nets] + + def fake_network_get_all_by_instance(context, instance_id): + nets = filter(lambda n: n['instance_id'] == instance_id, networks) + return [FakeModel(n) for n in nets] + + def fake_network_set_host(context, network_id, host_id): + nets = filter(lambda n: n['id'] == network_id, networks) + for net in nets: + net['host'] = host_id + return host_id + + def fake_network_update(context, network_id, values): + nets = filter(lambda n: n['id'] == network_id, networks) + for net in nets: + for key in values: + net[key] = values[key] + + def fake_project_get_networks(context, project_id): + return [FakeModel(n) for n in networks \ + if n['project_id'] == project_id] + + def fake_queue_get_for(context, topic, node): + return "%s.%s" % (topic, node) + + funcs = [fake_floating_ip_allocate_address, + fake_floating_ip_deallocate, + fake_floating_ip_disassociate, + fake_floating_ip_fixed_ip_associate, + fake_floating_ip_get_all_by_host, + fake_floating_ip_get_by_address, + fake_floating_ip_set_auto_assigned, + fake_fixed_ip_associate, + fake_fixed_ip_associate_pool, + fake_fixed_ip_create, + fake_fixed_ip_disassociate, + fake_fixed_ip_disassociate_all_by_timeout, + fake_fixed_ip_get_by_instance, + fake_fixed_ip_get_by_address, + fake_fixed_ip_get_network, + fake_fixed_ip_update, + fake_instance_type_get, + fake_virtual_interface_create, + fake_virtual_interface_delete_by_instance, + fake_virtual_interface_get_by_instance, + fake_virtual_interface_get_by_instance_and_network, + fake_network_create_safe, + fake_network_get, + fake_network_get_all, + fake_network_get_all_by_host, + fake_network_get_all_by_instance, + fake_network_set_host, + fake_network_update, + fake_project_get_networks, + fake_queue_get_for] + + stub_out(stubs, funcs) + + def stub_out_db_instance_api(stubs, injected=True): """Stubs out the db API for creating Instances.""" @@ -92,27 +409,13 @@ def stub_out_db_instance_api(stubs, injected=True): 'address_v6': 'fe80::a00:3', 'network_id': 'fake_flat'} - class FakeModel(object): - """Stubs out for model.""" - def __init__(self, values): - self.values = values - - def __getattr__(self, name): - return self.values[name] - - def __getitem__(self, key): - if key in self.values: - return self.values[key] - else: - raise NotImplementedError() - def fake_instance_type_get_all(context, inactive=0): return INSTANCE_TYPES def fake_instance_type_get_by_name(context, name): return INSTANCE_TYPES[name] - def fake_instance_type_get_by_id(context, id): + def fake_instance_type_get(context, id): for name, inst_type in INSTANCE_TYPES.iteritems(): if str(inst_type['id']) == str(id): return inst_type @@ -132,26 +435,22 @@ def stub_out_db_instance_api(stubs, injected=True): else: return [FakeModel(flat_network_fields)] - def fake_instance_get_fixed_address(context, instance_id): - return FakeModel(fixed_ip_fields).address + def fake_instance_get_fixed_addresses(context, instance_id): + return [FakeModel(fixed_ip_fields).address] - def fake_instance_get_fixed_address_v6(context, instance_id): - return FakeModel(fixed_ip_fields).address + def fake_instance_get_fixed_addresses_v6(context, instance_id): + return [FakeModel(fixed_ip_fields).address] - def fake_fixed_ip_get_all_by_instance(context, instance_id): + def fake_fixed_ip_get_by_instance(context, instance_id): return [FakeModel(fixed_ip_fields)] - stubs.Set(db, 'network_get_by_instance', fake_network_get_by_instance) - stubs.Set(db, 'network_get_all_by_instance', - fake_network_get_all_by_instance) - stubs.Set(db, 'instance_type_get_all', fake_instance_type_get_all) - stubs.Set(db, 'instance_type_get_by_name', fake_instance_type_get_by_name) - stubs.Set(db, 'instance_type_get_by_id', fake_instance_type_get_by_id) - stubs.Set(db, 'instance_get_fixed_address', - fake_instance_get_fixed_address) - stubs.Set(db, 'instance_get_fixed_address_v6', - fake_instance_get_fixed_address_v6) - stubs.Set(db, 'network_get_all_by_instance', - fake_network_get_all_by_instance) - stubs.Set(db, 'fixed_ip_get_all_by_instance', - fake_fixed_ip_get_all_by_instance) + funcs = [fake_network_get_by_instance, + fake_network_get_all_by_instance, + fake_instance_type_get_all, + fake_instance_type_get_by_name, + fake_instance_type_get, + fake_instance_get_fixed_addresses, + fake_instance_get_fixed_addresses_v6, + fake_network_get_all_by_instance, + fake_fixed_ip_get_by_instance] + stub_out(stubs, funcs) diff --git a/nova/tests/glance/stubs.py b/nova/tests/glance/stubs.py index 1e0b90d82..aac3ff330 100644 --- a/nova/tests/glance/stubs.py +++ b/nova/tests/glance/stubs.py @@ -64,8 +64,8 @@ class FakeGlance(object): pass def get_image_meta(self, image_id): - return self.IMAGE_FIXTURES[image_id]['image_meta'] + return self.IMAGE_FIXTURES[int(image_id)]['image_meta'] def get_image(self, image_id): - image = self.IMAGE_FIXTURES[image_id] + image = self.IMAGE_FIXTURES[int(image_id)] return image['image_meta'], image['image_data'] diff --git a/nova/tests/image/__init__.py b/nova/tests/image/__init__.py index b94e2e54e..6dab802f2 100644 --- a/nova/tests/image/__init__.py +++ b/nova/tests/image/__init__.py @@ -14,3 +14,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + +# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work +from nova.tests import * diff --git a/nova/tests/image/test_s3.py b/nova/tests/image/test_s3.py new file mode 100644 index 000000000..231e109f8 --- /dev/null +++ b/nova/tests/image/test_s3.py @@ -0,0 +1,122 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Isaku Yamahata +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova import context +from nova import flags +from nova import test +from nova.image import s3 + +FLAGS = flags.FLAGS + + +ami_manifest_xml = """<?xml version="1.0" ?> +<manifest> + <version>2011-06-17</version> + <bundler> + <name>test-s3</name> + <version>0</version> + <release>0</release> + </bundler> + <machine_configuration> + <architecture>x86_64</architecture> + <block_device_mapping> + <mapping> + <virtual>ami</virtual> + <device>sda1</device> + </mapping> + <mapping> + <virtual>root</virtual> + <device>/dev/sda1</device> + </mapping> + <mapping> + <virtual>ephemeral0</virtual> + <device>sda2</device> + </mapping> + <mapping> + <virtual>swap</virtual> + <device>sda3</device> + </mapping> + </block_device_mapping> + </machine_configuration> +</manifest> +""" + + +class TestS3ImageService(test.TestCase): + def setUp(self): + super(TestS3ImageService, self).setUp() + self.orig_image_service = FLAGS.image_service + FLAGS.image_service = 'nova.image.fake.FakeImageService' + self.image_service = s3.S3ImageService() + self.context = context.RequestContext(None, None) + + def tearDown(self): + super(TestS3ImageService, self).tearDown() + FLAGS.image_service = self.orig_image_service + + def _assertEqualList(self, list0, list1, keys): + self.assertEqual(len(list0), len(list1)) + key = keys[0] + for x in list0: + self.assertEqual(len(x), len(keys)) + self.assertTrue(key in x) + for y in list1: + self.assertTrue(key in y) + if x[key] == y[key]: + for k in keys: + self.assertEqual(x[k], y[k]) + + def test_s3_create(self): + metadata = {'properties': { + 'root_device_name': '/dev/sda1', + 'block_device_mapping': [ + {'device_name': '/dev/sda1', + 'snapshot_id': 'snap-12345678', + 'delete_on_termination': True}, + {'device_name': '/dev/sda2', + 'virutal_name': 'ephemeral0'}, + {'device_name': '/dev/sdb0', + 'no_device': True}]}} + _manifest, image = self.image_service._s3_parse_manifest( + self.context, metadata, ami_manifest_xml) + image_id = image['id'] + + ret_image = self.image_service.show(self.context, image_id) + self.assertTrue('properties' in ret_image) + properties = ret_image['properties'] + + self.assertTrue('mappings' in properties) + mappings = properties['mappings'] + expected_mappings = [ + {"device": "sda1", "virtual": "ami"}, + {"device": "/dev/sda1", "virtual": "root"}, + {"device": "sda2", "virtual": "ephemeral0"}, + {"device": "sda3", "virtual": "swap"}] + self._assertEqualList(mappings, expected_mappings, + ['device', 'virtual']) + + self.assertTrue('block_device_mapping', properties) + block_device_mapping = properties['block_device_mapping'] + expected_bdm = [ + {'device_name': '/dev/sda1', + 'snapshot_id': 'snap-12345678', + 'delete_on_termination': True}, + {'device_name': '/dev/sda2', + 'virutal_name': 'ephemeral0'}, + {'device_name': '/dev/sdb0', + 'no_device': True}] + self.assertEqual(block_device_mapping, expected_bdm) diff --git a/nova/tests/integrated/__init__.py b/nova/tests/integrated/__init__.py index 10e0a91d7..430af8754 100644 --- a/nova/tests/integrated/__init__.py +++ b/nova/tests/integrated/__init__.py @@ -18,3 +18,5 @@ :mod:`integrated` -- Tests whole systems, using mock services where needed ================================= """ +# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work +from nova.tests import * diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py index 76c03c5fa..035a35aab 100644 --- a/nova/tests/integrated/api/client.py +++ b/nova/tests/integrated/api/client.py @@ -71,8 +71,8 @@ class TestOpenStackClient(object): self.auth_uri = auth_uri def request(self, url, method='GET', body=None, headers=None): - if headers is None: - headers = {} + _headers = {'Content-Type': 'application/json'} + _headers.update(headers or {}) parsed_url = urlparse.urlparse(url) port = parsed_url.port @@ -94,9 +94,8 @@ class TestOpenStackClient(object): LOG.info(_("Doing %(method)s on %(relative_url)s") % locals()) if body: LOG.info(_("Body: %s") % body) - headers.setdefault('Content-Type', 'application/json') - conn.request(method, relative_url, body, headers) + conn.request(method, relative_url, body, _headers) response = conn.getresponse() return response @@ -173,9 +172,20 @@ class TestOpenStackClient(object): response = self.api_request(relative_uri, **kwargs) return self._decode_json(response) + def api_put(self, relative_uri, body, **kwargs): + kwargs['method'] = 'PUT' + if body: + headers = kwargs.setdefault('headers', {}) + headers['Content-Type'] = 'application/json' + kwargs['body'] = json.dumps(body) + + kwargs.setdefault('check_response_status', [200, 202, 204]) + response = self.api_request(relative_uri, **kwargs) + return self._decode_json(response) + def api_delete(self, relative_uri, **kwargs): kwargs['method'] = 'DELETE' - kwargs.setdefault('check_response_status', [200, 202]) + kwargs.setdefault('check_response_status', [200, 202, 204]) return self.api_request(relative_uri, **kwargs) def get_server(self, server_id): @@ -188,6 +198,9 @@ class TestOpenStackClient(object): def post_server(self, server): return self.api_post('/servers', server)['server'] + def put_server(self, server_id, server): + return self.api_put('/servers/%s' % server_id, server) + def post_server_action(self, server_id, data): return self.api_post('/servers/%s/action' % server_id, data) diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index fcb517cf5..4e8e85c7b 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -285,6 +285,25 @@ class ServersTest(integrated_helpers._IntegratedTestBase): # Cleanup self._delete_server(created_server_id) + def test_rename_server(self): + """Test building and renaming a server.""" + + # Create a server + server = self._build_minimal_create_server_request() + created_server = self.api.post_server({'server': server}) + LOG.debug("created_server: %s" % created_server) + server_id = created_server['id'] + self.assertTrue(server_id) + + # Rename the server to 'new-name' + self.api.put_server(server_id, {'server': {'name': 'new-name'}}) + + # Check the name of the server + created_server = self.api.get_server(server_id) + self.assertEqual(created_server['name'], 'new-name') + + # Cleanup + self._delete_server(server_id) if __name__ == "__main__": unittest.main() diff --git a/nova/tests/network/__init__.py b/nova/tests/network/__init__.py deleted file mode 100644 index 97f96b6fa..000000000 --- a/nova/tests/network/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" -Utility methods -""" -import os - -from nova import context -from nova import db -from nova import flags -from nova import log as logging -from nova import utils - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.tests.network') - - -def binpath(script): - """Returns the absolute path to a script in bin""" - return os.path.abspath(os.path.join(__file__, "../../../../bin", script)) - - -def lease_ip(private_ip): - """Run add command on dhcpbridge""" - network_ref = db.fixed_ip_get_network(context.get_admin_context(), - private_ip) - instance_ref = db.fixed_ip_get_instance(context.get_admin_context(), - private_ip) - cmd = (binpath('nova-dhcpbridge'), 'add', - instance_ref['mac_address'], - private_ip, 'fake') - env = {'DNSMASQ_INTERFACE': network_ref['bridge'], - 'TESTING': '1', - 'FLAGFILE': FLAGS.dhcpbridge_flagfile} - (out, err) = utils.execute(*cmd, addl_env=env) - LOG.debug("ISSUE_IP: %s, %s ", out, err) - - -def release_ip(private_ip): - """Run del command on dhcpbridge""" - network_ref = db.fixed_ip_get_network(context.get_admin_context(), - private_ip) - instance_ref = db.fixed_ip_get_instance(context.get_admin_context(), - private_ip) - cmd = (binpath('nova-dhcpbridge'), 'del', - instance_ref['mac_address'], - private_ip, 'fake') - env = {'DNSMASQ_INTERFACE': network_ref['bridge'], - 'TESTING': '1', - 'FLAGFILE': FLAGS.dhcpbridge_flagfile} - (out, err) = utils.execute(*cmd, addl_env=env) - LOG.debug("RELEASE_IP: %s, %s ", out, err) diff --git a/nova/tests/network/base.py b/nova/tests/network/base.py deleted file mode 100644 index f65416824..000000000 --- a/nova/tests/network/base.py +++ /dev/null @@ -1,155 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" -Base class of Unit Tests for all network models -""" -import netaddr -import os - -from nova import context -from nova import db -from nova import exception -from nova import flags -from nova import ipv6 -from nova import log as logging -from nova import test -from nova import utils -from nova.auth import manager - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.tests.network') - - -class NetworkTestCase(test.TestCase): - """Test cases for network code""" - def setUp(self): - super(NetworkTestCase, self).setUp() - # NOTE(vish): if you change these flags, make sure to change the - # flags in the corresponding section in nova-dhcpbridge - self.flags(connection_type='fake', - fake_call=True, - fake_network=True) - self.manager = manager.AuthManager() - self.user = self.manager.create_user('netuser', 'netuser', 'netuser') - self.projects = [] - self.network = utils.import_object(FLAGS.network_manager) - self.context = context.RequestContext(project=None, user=self.user) - for i in range(FLAGS.num_networks): - name = 'project%s' % i - project = self.manager.create_project(name, 'netuser', name) - self.projects.append(project) - # create the necessary network data for the project - user_context = context.RequestContext(project=self.projects[i], - user=self.user) - host = self.network.get_network_host(user_context.elevated()) - instance_ref = self._create_instance(0) - self.instance_id = instance_ref['id'] - instance_ref = self._create_instance(1) - self.instance2_id = instance_ref['id'] - - def tearDown(self): - # TODO(termie): this should really be instantiating clean datastores - # in between runs, one failure kills all the tests - db.instance_destroy(context.get_admin_context(), self.instance_id) - db.instance_destroy(context.get_admin_context(), self.instance2_id) - for project in self.projects: - self.manager.delete_project(project) - self.manager.delete_user(self.user) - super(NetworkTestCase, self).tearDown() - - def _create_instance(self, project_num, mac=None): - if not mac: - mac = utils.generate_mac() - project = self.projects[project_num] - self.context._project = project - self.context.project_id = project.id - return db.instance_create(self.context, - {'project_id': project.id, - 'mac_address': mac}) - - def _create_address(self, project_num, instance_id=None): - """Create an address in given project num""" - if instance_id is None: - instance_id = self.instance_id - self.context._project = self.projects[project_num] - self.context.project_id = self.projects[project_num].id - return self.network.allocate_fixed_ip(self.context, instance_id) - - def _deallocate_address(self, project_num, address): - self.context._project = self.projects[project_num] - self.context.project_id = self.projects[project_num].id - self.network.deallocate_fixed_ip(self.context, address) - - def _is_allocated_in_project(self, address, project_id): - """Returns true if address is in specified project""" - project_net = db.network_get_by_bridge(context.get_admin_context(), - FLAGS.flat_network_bridge) - network = db.fixed_ip_get_network(context.get_admin_context(), - address) - instance = db.fixed_ip_get_instance(context.get_admin_context(), - address) - # instance exists until release - return instance is not None and network['id'] == project_net['id'] - - def test_private_ipv6(self): - """Make sure ipv6 is OK""" - if FLAGS.use_ipv6: - instance_ref = self._create_instance(0) - address = self._create_address(0, instance_ref['id']) - network_ref = db.project_get_network( - context.get_admin_context(), - self.context.project_id) - address_v6 = db.instance_get_fixed_address_v6( - context.get_admin_context(), - instance_ref['id']) - self.assertEqual(instance_ref['mac_address'], - ipv6.to_mac(address_v6)) - instance_ref2 = db.fixed_ip_get_instance_v6( - context.get_admin_context(), - address_v6) - self.assertEqual(instance_ref['id'], instance_ref2['id']) - self.assertEqual(address_v6, - ipv6.to_global(network_ref['cidr_v6'], - instance_ref['mac_address'], - 'test')) - self._deallocate_address(0, address) - db.instance_destroy(context.get_admin_context(), - instance_ref['id']) - - def test_available_ips(self): - """Make sure the number of available ips for the network is correct - - The number of available IP addresses depends on the test - environment's setup. - - Network size is set in test fixture's setUp method. - - There are ips reserved at the bottom and top of the range. - services (network, gateway, CloudPipe, broadcast) - """ - network = db.project_get_network(context.get_admin_context(), - self.projects[0].id) - net_size = flags.FLAGS.network_size - admin_context = context.get_admin_context() - total_ips = (db.network_count_available_ips(admin_context, - network['id']) + - db.network_count_reserved_ips(admin_context, - network['id']) + - db.network_count_allocated_ips(admin_context, - network['id'])) - self.assertEqual(total_ips, net_size) diff --git a/nova/tests/scheduler/__init__.py b/nova/tests/scheduler/__init__.py index e69de29bb..6dab802f2 100644 --- a/nova/tests/scheduler/__init__.py +++ b/nova/tests/scheduler/__init__.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Openstack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work +from nova.tests import * diff --git a/nova/tests/scheduler/test_least_cost_scheduler.py b/nova/tests/scheduler/test_least_cost_scheduler.py index 9a5318aee..49791053e 100644 --- a/nova/tests/scheduler/test_least_cost_scheduler.py +++ b/nova/tests/scheduler/test_least_cost_scheduler.py @@ -122,15 +122,16 @@ class LeastCostSchedulerTestCase(test.TestCase): for hostname, caps in hosts] self.assertWeights(expected, num, request_spec, hosts) - def test_fill_first_cost_fn(self): + def test_compute_fill_first_cost_fn(self): FLAGS.least_cost_scheduler_cost_functions = [ - 'nova.scheduler.least_cost.fill_first_cost_fn', + 'nova.scheduler.least_cost.compute_fill_first_cost_fn', ] - FLAGS.fill_first_cost_fn_weight = 1 + FLAGS.compute_fill_first_cost_fn_weight = 1 num = 1 - request_spec = {} - hosts = self.sched.filter_hosts(num, request_spec) + instance_type = {'memory_mb': 1024} + request_spec = {'instance_type': instance_type} + hosts = self.sched.filter_hosts('compute', request_spec, None) expected = [] for idx, (hostname, caps) in enumerate(hosts): diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index 4be59d411..daea826fd 100644 --- a/nova/tests/scheduler/test_scheduler.py +++ b/nova/tests/scheduler/test_scheduler.py @@ -268,7 +268,6 @@ class SimpleDriverTestCase(test.TestCase): inst['user_id'] = self.user.id inst['project_id'] = self.project.id inst['instance_type_id'] = '1' - inst['mac_address'] = utils.generate_mac() inst['vcpus'] = kwargs.get('vcpus', 1) inst['ami_launch_index'] = 0 inst['availability_zone'] = kwargs.get('availability_zone', None) @@ -1074,7 +1073,7 @@ class DynamicNovaClientTest(test.TestCase): self.assertEquals(api._issue_novaclient_command( FakeNovaClient(FakeServerCollection()), - zone, "servers", "find", "name").b, 22) + zone, "servers", "find", name="test").b, 22) self.assertEquals(api._issue_novaclient_command( FakeNovaClient(FakeServerCollection()), @@ -1088,7 +1087,7 @@ class DynamicNovaClientTest(test.TestCase): self.assertEquals(api._issue_novaclient_command( FakeNovaClient(FakeEmptyServerCollection()), - zone, "servers", "find", "name"), None) + zone, "servers", "find", name="test"), None) self.assertEquals(api._issue_novaclient_command( FakeNovaClient(FakeEmptyServerCollection()), diff --git a/nova/tests/scheduler/test_zone_aware_scheduler.py b/nova/tests/scheduler/test_zone_aware_scheduler.py index 37c6488cc..d74b71fb6 100644 --- a/nova/tests/scheduler/test_zone_aware_scheduler.py +++ b/nova/tests/scheduler/test_zone_aware_scheduler.py @@ -16,6 +16,8 @@ Tests For Zone Aware Scheduler. """ +import nova.db + from nova import exception from nova import test from nova.scheduler import driver @@ -55,29 +57,21 @@ def fake_zone_manager_service_states(num_hosts): class FakeZoneAwareScheduler(zone_aware_scheduler.ZoneAwareScheduler): - def filter_hosts(self, num, specs): - # NOTE(sirp): this is returning [(hostname, services)] - return self.zone_manager.service_states.items() - - def weigh_hosts(self, num, specs, hosts): - fake_weight = 99 - weighted = [] - for hostname, caps in hosts: - weighted.append(dict(weight=fake_weight, name=hostname)) - return weighted + # No need to stub anything at the moment + pass class FakeZoneManager(zone_manager.ZoneManager): def __init__(self): self.service_states = { 'host1': { - 'compute': {'ram': 1000}, + 'compute': {'host_memory_free': 1073741824}, }, 'host2': { - 'compute': {'ram': 2000}, + 'compute': {'host_memory_free': 2147483648}, }, 'host3': { - 'compute': {'ram': 3000}, + 'compute': {'host_memory_free': 3221225472}, }, } @@ -87,7 +81,7 @@ class FakeEmptyZoneManager(zone_manager.ZoneManager): self.service_states = {} -def fake_empty_call_zone_method(context, method, specs): +def fake_empty_call_zone_method(context, method, specs, zones): return [] @@ -106,7 +100,7 @@ def fake_ask_child_zone_to_create_instance(context, zone_info, was_called = True -def fake_provision_resource_locally(context, item, instance_id, kwargs): +def fake_provision_resource_locally(context, build_plan, request_spec, kwargs): global was_called was_called = True @@ -126,21 +120,21 @@ def fake_decrypt_blob_returns_child_info(blob): 'child_blob': True} # values aren't important. Keys are. -def fake_call_zone_method(context, method, specs): +def fake_call_zone_method(context, method, specs, zones): return [ - ('zone1', [ + (1, [ dict(weight=1, blob='AAAAAAA'), dict(weight=111, blob='BBBBBBB'), dict(weight=112, blob='CCCCCCC'), dict(weight=113, blob='DDDDDDD'), ]), - ('zone2', [ + (2, [ dict(weight=120, blob='EEEEEEE'), dict(weight=2, blob='FFFFFFF'), dict(weight=122, blob='GGGGGGG'), dict(weight=123, blob='HHHHHHH'), ]), - ('zone3', [ + (3, [ dict(weight=130, blob='IIIIIII'), dict(weight=131, blob='JJJJJJJ'), dict(weight=132, blob='KKKKKKK'), @@ -149,28 +143,67 @@ def fake_call_zone_method(context, method, specs): ] +def fake_zone_get_all(context): + return [ + dict(id=1, api_url='zone1', + username='admin', password='password', + weight_offset=0.0, weight_scale=1.0), + dict(id=2, api_url='zone2', + username='admin', password='password', + weight_offset=1000.0, weight_scale=1.0), + dict(id=3, api_url='zone3', + username='admin', password='password', + weight_offset=0.0, weight_scale=1000.0), + ] + + class ZoneAwareSchedulerTestCase(test.TestCase): """Test case for Zone Aware Scheduler.""" def test_zone_aware_scheduler(self): """ - Create a nested set of FakeZones, ensure that a select call returns the - appropriate build plan. + Create a nested set of FakeZones, try to build multiple instances + and ensure that a select call returns the appropriate build plan. """ sched = FakeZoneAwareScheduler() self.stubs.Set(sched, '_call_zone_method', fake_call_zone_method) + self.stubs.Set(nova.db, 'zone_get_all', fake_zone_get_all) zm = FakeZoneManager() sched.set_zone_manager(zm) fake_context = {} - build_plan = sched.select(fake_context, {}) - - self.assertEqual(15, len(build_plan)) - - hostnames = [plan_item['name'] - for plan_item in build_plan if 'name' in plan_item] - self.assertEqual(3, len(hostnames)) + build_plan = sched.select(fake_context, + {'instance_type': {'memory_mb': 512}, + 'num_instances': 4}) + + # 4 from local zones, 12 from remotes + self.assertEqual(16, len(build_plan)) + + hostnames = [plan_item['hostname'] + for plan_item in build_plan if 'hostname' in plan_item] + # 4 local hosts + self.assertEqual(4, len(hostnames)) + + def test_adjust_child_weights(self): + """Make sure the weights returned by child zones are + properly adjusted based on the scale/offset in the zone + db entries. + """ + sched = FakeZoneAwareScheduler() + child_results = fake_call_zone_method(None, None, None, None) + zones = fake_zone_get_all(None) + sched._adjust_child_weights(child_results, zones) + scaled = [130000, 131000, 132000, 3000] + for zone, results in child_results: + for item in results: + w = item['weight'] + if zone == 'zone1': # No change + self.assertTrue(w < 1000.0) + if zone == 'zone2': # Offset +1000 + self.assertTrue(w >= 1000.0 and w < 2000) + if zone == 'zone3': # Scale x1000 + self.assertEqual(scaled.pop(0), w) def test_empty_zone_aware_scheduler(self): """ @@ -178,6 +211,7 @@ class ZoneAwareSchedulerTestCase(test.TestCase): """ sched = FakeZoneAwareScheduler() self.stubs.Set(sched, '_call_zone_method', fake_empty_call_zone_method) + self.stubs.Set(nova.db, 'zone_get_all', fake_zone_get_all) zm = FakeEmptyZoneManager() sched.set_zone_manager(zm) @@ -185,8 +219,7 @@ class ZoneAwareSchedulerTestCase(test.TestCase): fake_context = {} self.assertRaises(driver.NoValidHost, sched.schedule_run_instance, fake_context, 1, - dict(host_filter=None, - request_spec={'instance_type': {}})) + dict(host_filter=None, instance_type={})) def test_schedule_do_not_schedule_with_hint(self): """ diff --git a/nova/tests/test_adminapi.py b/nova/tests/test_adminapi.py index ce826fd5b..877cf4ea1 100644 --- a/nova/tests/test_adminapi.py +++ b/nova/tests/test_adminapi.py @@ -56,7 +56,6 @@ class AdminApiTestCase(test.TestCase): self.project = self.manager.create_project('proj', 'admin', 'proj') self.context = context.RequestContext(user=self.user, project=self.project) - host = self.network.get_network_host(self.context.elevated()) def fake_show(meh, context, id): return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1, @@ -75,9 +74,6 @@ class AdminApiTestCase(test.TestCase): self.stubs.Set(rpc, 'cast', finish_cast) def tearDown(self): - network_ref = db.project_get_network(self.context, - self.project.id) - db.network_disassociate(self.context, network_ref['id']) self.manager.delete_project(self.project) self.manager.delete_user(self.user) super(AdminApiTestCase, self).tearDown() diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py index 20b20fcbf..26ac5ff24 100644 --- a/nova/tests/test_api.py +++ b/nova/tests/test_api.py @@ -92,7 +92,9 @@ class XmlConversionTestCase(test.TestCase): conv = ec2utils._try_convert self.assertEqual(conv('None'), None) self.assertEqual(conv('True'), True) + self.assertEqual(conv('true'), True) self.assertEqual(conv('False'), False) + self.assertEqual(conv('false'), False) self.assertEqual(conv('0'), 0) self.assertEqual(conv('42'), 42) self.assertEqual(conv('3.14'), 3.14) @@ -107,6 +109,8 @@ class Ec2utilsTestCase(test.TestCase): def test_ec2_id_to_id(self): self.assertEqual(ec2utils.ec2_id_to_id('i-0000001e'), 30) self.assertEqual(ec2utils.ec2_id_to_id('ami-1d'), 29) + self.assertEqual(ec2utils.ec2_id_to_id('snap-0000001c'), 28) + self.assertEqual(ec2utils.ec2_id_to_id('vol-0000001b'), 27) def test_bad_ec2_id(self): self.assertRaises(exception.InvalidEc2Id, @@ -116,6 +120,72 @@ class Ec2utilsTestCase(test.TestCase): def test_id_to_ec2_id(self): self.assertEqual(ec2utils.id_to_ec2_id(30), 'i-0000001e') self.assertEqual(ec2utils.id_to_ec2_id(29, 'ami-%08x'), 'ami-0000001d') + self.assertEqual(ec2utils.id_to_ec2_snap_id(28), 'snap-0000001c') + self.assertEqual(ec2utils.id_to_ec2_vol_id(27), 'vol-0000001b') + + def test_dict_from_dotted_str(self): + in_str = [('BlockDeviceMapping.1.DeviceName', '/dev/sda1'), + ('BlockDeviceMapping.1.Ebs.SnapshotId', 'snap-0000001c'), + ('BlockDeviceMapping.1.Ebs.VolumeSize', '80'), + ('BlockDeviceMapping.1.Ebs.DeleteOnTermination', 'false'), + ('BlockDeviceMapping.2.DeviceName', '/dev/sdc'), + ('BlockDeviceMapping.2.VirtualName', 'ephemeral0')] + expected_dict = { + 'block_device_mapping': { + '1': {'device_name': '/dev/sda1', + 'ebs': {'snapshot_id': 'snap-0000001c', + 'volume_size': 80, + 'delete_on_termination': False}}, + '2': {'device_name': '/dev/sdc', + 'virtual_name': 'ephemeral0'}}} + out_dict = ec2utils.dict_from_dotted_str(in_str) + + self.assertDictMatch(out_dict, expected_dict) + + def test_properties_root_defice_name(self): + mappings = [{"device": "/dev/sda1", "virtual": "root"}] + properties0 = {'mappings': mappings} + properties1 = {'root_device_name': '/dev/sdb', 'mappings': mappings} + + root_device_name = ec2utils.properties_root_device_name(properties0) + self.assertEqual(root_device_name, '/dev/sda1') + + root_device_name = ec2utils.properties_root_device_name(properties1) + self.assertEqual(root_device_name, '/dev/sdb') + + def test_mapping_prepend_dev(self): + mappings = [ + {'virtual': 'ami', + 'device': 'sda1'}, + {'virtual': 'root', + 'device': '/dev/sda1'}, + + {'virtual': 'swap', + 'device': 'sdb1'}, + {'virtual': 'swap', + 'device': '/dev/sdb2'}, + + {'virtual': 'ephemeral0', + 'device': 'sdc1'}, + {'virtual': 'ephemeral1', + 'device': '/dev/sdc1'}] + expected_result = [ + {'virtual': 'ami', + 'device': 'sda1'}, + {'virtual': 'root', + 'device': '/dev/sda1'}, + + {'virtual': 'swap', + 'device': '/dev/sdb1'}, + {'virtual': 'swap', + 'device': '/dev/sdb2'}, + + {'virtual': 'ephemeral0', + 'device': '/dev/sdc1'}, + {'virtual': 'ephemeral1', + 'device': '/dev/sdc1'}] + self.assertDictListMatch(ec2utils.mappings_prepend_dev(mappings), + expected_result) class ApiEc2TestCase(test.TestCase): diff --git a/nova/tests/test_bdm.py b/nova/tests/test_bdm.py new file mode 100644 index 000000000..b258f6a75 --- /dev/null +++ b/nova/tests/test_bdm.py @@ -0,0 +1,233 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Isaku Yamahata +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Tests for Block Device Mapping Code. +""" + +from nova.api.ec2 import cloud +from nova import test + + +class BlockDeviceMappingEc2CloudTestCase(test.TestCase): + """Test Case for Block Device Mapping""" + + def setUp(self): + super(BlockDeviceMappingEc2CloudTestCase, self).setUp() + + def tearDown(self): + super(BlockDeviceMappingEc2CloudTestCase, self).tearDown() + + def _assertApply(self, action, bdm_list): + for bdm, expected_result in bdm_list: + self.assertDictMatch(action(bdm), expected_result) + + def test_parse_block_device_mapping(self): + bdm_list = [ + ({'device_name': '/dev/fake0', + 'ebs': {'snapshot_id': 'snap-12345678', + 'volume_size': 1}}, + {'device_name': '/dev/fake0', + 'snapshot_id': 0x12345678, + 'volume_size': 1, + 'delete_on_termination': True}), + + ({'device_name': '/dev/fake1', + 'ebs': {'snapshot_id': 'snap-23456789', + 'delete_on_termination': False}}, + {'device_name': '/dev/fake1', + 'snapshot_id': 0x23456789, + 'delete_on_termination': False}), + + ({'device_name': '/dev/fake2', + 'ebs': {'snapshot_id': 'vol-87654321', + 'volume_size': 2}}, + {'device_name': '/dev/fake2', + 'volume_id': 0x87654321, + 'volume_size': 2, + 'delete_on_termination': True}), + + ({'device_name': '/dev/fake3', + 'ebs': {'snapshot_id': 'vol-98765432', + 'delete_on_termination': False}}, + {'device_name': '/dev/fake3', + 'volume_id': 0x98765432, + 'delete_on_termination': False}), + + ({'device_name': '/dev/fake4', + 'ebs': {'no_device': True}}, + {'device_name': '/dev/fake4', + 'no_device': True}), + + ({'device_name': '/dev/fake5', + 'virtual_name': 'ephemeral0'}, + {'device_name': '/dev/fake5', + 'virtual_name': 'ephemeral0'}), + + ({'device_name': '/dev/fake6', + 'virtual_name': 'swap'}, + {'device_name': '/dev/fake6', + 'virtual_name': 'swap'}), + ] + self._assertApply(cloud._parse_block_device_mapping, bdm_list) + + def test_format_block_device_mapping(self): + bdm_list = [ + ({'device_name': '/dev/fake0', + 'snapshot_id': 0x12345678, + 'volume_size': 1, + 'delete_on_termination': True}, + {'deviceName': '/dev/fake0', + 'ebs': {'snapshotId': 'snap-12345678', + 'volumeSize': 1, + 'deleteOnTermination': True}}), + + ({'device_name': '/dev/fake1', + 'snapshot_id': 0x23456789}, + {'deviceName': '/dev/fake1', + 'ebs': {'snapshotId': 'snap-23456789'}}), + + ({'device_name': '/dev/fake2', + 'snapshot_id': 0x23456789, + 'delete_on_termination': False}, + {'deviceName': '/dev/fake2', + 'ebs': {'snapshotId': 'snap-23456789', + 'deleteOnTermination': False}}), + + ({'device_name': '/dev/fake3', + 'volume_id': 0x12345678, + 'volume_size': 1, + 'delete_on_termination': True}, + {'deviceName': '/dev/fake3', + 'ebs': {'snapshotId': 'vol-12345678', + 'volumeSize': 1, + 'deleteOnTermination': True}}), + + ({'device_name': '/dev/fake4', + 'volume_id': 0x23456789}, + {'deviceName': '/dev/fake4', + 'ebs': {'snapshotId': 'vol-23456789'}}), + + ({'device_name': '/dev/fake5', + 'volume_id': 0x23456789, + 'delete_on_termination': False}, + {'deviceName': '/dev/fake5', + 'ebs': {'snapshotId': 'vol-23456789', + 'deleteOnTermination': False}}), + ] + self._assertApply(cloud._format_block_device_mapping, bdm_list) + + def test_format_mapping(self): + properties = { + 'mappings': [ + {'virtual': 'ami', + 'device': 'sda1'}, + {'virtual': 'root', + 'device': '/dev/sda1'}, + + {'virtual': 'swap', + 'device': 'sdb1'}, + {'virtual': 'swap', + 'device': 'sdb2'}, + {'virtual': 'swap', + 'device': 'sdb3'}, + {'virtual': 'swap', + 'device': 'sdb4'}, + + {'virtual': 'ephemeral0', + 'device': 'sdc1'}, + {'virtual': 'ephemeral1', + 'device': 'sdc2'}, + {'virtual': 'ephemeral2', + 'device': 'sdc3'}, + ], + + 'block_device_mapping': [ + # root + {'device_name': '/dev/sda1', + 'snapshot_id': 0x12345678, + 'delete_on_termination': False}, + + + # overwrite swap + {'device_name': '/dev/sdb2', + 'snapshot_id': 0x23456789, + 'delete_on_termination': False}, + {'device_name': '/dev/sdb3', + 'snapshot_id': 0x3456789A}, + {'device_name': '/dev/sdb4', + 'no_device': True}, + + # overwrite ephemeral + {'device_name': '/dev/sdc2', + 'snapshot_id': 0x3456789A, + 'delete_on_termination': False}, + {'device_name': '/dev/sdc3', + 'snapshot_id': 0x456789AB}, + {'device_name': '/dev/sdc4', + 'no_device': True}, + + # volume + {'device_name': '/dev/sdd1', + 'snapshot_id': 0x87654321, + 'delete_on_termination': False}, + {'device_name': '/dev/sdd2', + 'snapshot_id': 0x98765432}, + {'device_name': '/dev/sdd3', + 'snapshot_id': 0xA9875463}, + {'device_name': '/dev/sdd4', + 'no_device': True}]} + + expected_result = { + 'blockDeviceMapping': [ + # root + {'deviceName': '/dev/sda1', + 'ebs': {'snapshotId': 'snap-12345678', + 'deleteOnTermination': False}}, + + # swap + {'deviceName': '/dev/sdb1', + 'virtualName': 'swap'}, + {'deviceName': '/dev/sdb2', + 'ebs': {'snapshotId': 'snap-23456789', + 'deleteOnTermination': False}}, + {'deviceName': '/dev/sdb3', + 'ebs': {'snapshotId': 'snap-3456789a'}}, + + # ephemeral + {'deviceName': '/dev/sdc1', + 'virtualName': 'ephemeral0'}, + {'deviceName': '/dev/sdc2', + 'ebs': {'snapshotId': 'snap-3456789a', + 'deleteOnTermination': False}}, + {'deviceName': '/dev/sdc3', + 'ebs': {'snapshotId': 'snap-456789ab'}}, + + # volume + {'deviceName': '/dev/sdd1', + 'ebs': {'snapshotId': 'snap-87654321', + 'deleteOnTermination': False}}, + {'deviceName': '/dev/sdd2', + 'ebs': {'snapshotId': 'snap-98765432'}}, + {'deviceName': '/dev/sdd3', + 'ebs': {'snapshotId': 'snap-a9875463'}}]} + + result = {} + cloud._format_mappings(properties, result) + print result + self.assertEqual(result['blockDeviceMapping'].sort(), + expected_result['blockDeviceMapping'].sort()) diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index 6327734f5..136082cc1 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -15,6 +15,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import mox from base64 import b64decode from M2Crypto import BIO @@ -29,6 +30,7 @@ from nova import db from nova import exception from nova import flags from nova import log as logging +from nova import network from nova import rpc from nova import test from nova import utils @@ -45,7 +47,8 @@ LOG = logging.getLogger('nova.tests.cloud') class CloudTestCase(test.TestCase): def setUp(self): super(CloudTestCase, self).setUp() - self.flags(connection_type='fake') + self.flags(connection_type='fake', + stub_network=True) self.conn = rpc.Connection.instance() @@ -64,10 +67,11 @@ class CloudTestCase(test.TestCase): self.project = self.manager.create_project('proj', 'admin', 'proj') self.context = context.RequestContext(user=self.user, project=self.project) - host = self.network.get_network_host(self.context.elevated()) + host = self.network.host def fake_show(meh, context, id): - return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1, + return {'id': 1, 'container_format': 'ami', + 'properties': {'kernel_id': 1, 'ramdisk_id': 1, 'type': 'machine', 'image_state': 'available'}} self.stubs.Set(fake._FakeImageService, 'show', fake_show) @@ -83,9 +87,10 @@ class CloudTestCase(test.TestCase): self.stubs.Set(rpc, 'cast', finish_cast) def tearDown(self): - network_ref = db.project_get_network(self.context, - self.project.id) - db.network_disassociate(self.context, network_ref['id']) + networks = db.project_get_networks(self.context, self.project.id, + associate=False) + for network in networks: + db.network_disassociate(self.context, network['id']) self.manager.delete_project(self.project) self.manager.delete_user(self.user) super(CloudTestCase, self).tearDown() @@ -116,6 +121,7 @@ class CloudTestCase(test.TestCase): public_ip=address) db.floating_ip_destroy(self.context, address) + @test.skip_test("Skipping this pending future merge") def test_allocate_address(self): address = "10.10.10.10" allocate = self.cloud.allocate_address @@ -128,6 +134,34 @@ class CloudTestCase(test.TestCase): allocate, self.context) + def test_release_address(self): + address = "10.10.10.10" + allocate = self.cloud.allocate_address + db.floating_ip_create(self.context, + {'address': address, + 'host': self.network.host}) + result = self.cloud.release_address(self.context, address) + self.assertEqual(result['releaseResponse'], ['Address released.']) + + def test_release_address_still_associated(self): + address = "10.10.10.10" + fixed_ip = {'instance': {'id': 1}} + floating_ip = {'id': 0, + 'address': address, + 'fixed_ip_id': 0, + 'fixed_ip': fixed_ip, + 'project_id': None, + 'auto_assigned': False} + network_api = network.api.API() + self.mox.StubOutWithMock(network_api.db, 'floating_ip_get_by_address') + network_api.db.floating_ip_get_by_address(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(floating_ip) + self.mox.ReplayAll() + release = self.cloud.release_address + # ApiError: Floating ip is in use. Disassociate it before releasing. + self.assertRaises(exception.ApiError, release, self.context, address) + + @test.skip_test("Skipping this pending future merge") def test_associate_disassociate_address(self): """Verifies associate runs cleanly without raising an exception""" address = "10.10.10.10" @@ -135,8 +169,27 @@ class CloudTestCase(test.TestCase): {'address': address, 'host': self.network.host}) self.cloud.allocate_address(self.context) - inst = db.instance_create(self.context, {'host': self.compute.host}) - fixed = self.network.allocate_fixed_ip(self.context, inst['id']) + # TODO(jkoelker) Probably need to query for instance_type_id and + # make sure we get a valid one + inst = db.instance_create(self.context, {'host': self.compute.host, + 'instance_type_id': 1}) + networks = db.network_get_all(self.context) + for network in networks: + self.network.set_network_host(self.context, network['id']) + project_id = self.context.project_id + type_id = inst['instance_type_id'] + ips = self.network.allocate_for_instance(self.context, + instance_id=inst['id'], + instance_type_id=type_id, + project_id=project_id) + # TODO(jkoelker) Make this mas bueno + self.assertTrue(ips) + self.assertTrue('ips' in ips[0][1]) + self.assertTrue(ips[0][1]['ips']) + self.assertTrue('ip' in ips[0][1]['ips'][0]) + + fixed = ips[0][1]['ips'][0]['ip'] + ec2_id = ec2utils.id_to_ec2_id(inst['id']) self.cloud.associate_address(self.context, instance_id=ec2_id, @@ -165,13 +218,148 @@ class CloudTestCase(test.TestCase): sec['name']) db.security_group_destroy(self.context, sec['id']) + def test_describe_security_groups_by_id(self): + sec = db.security_group_create(self.context, + {'project_id': self.context.project_id, + 'name': 'test'}) + result = self.cloud.describe_security_groups(self.context, + group_id=[sec['id']]) + self.assertEqual(len(result['securityGroupInfo']), 1) + self.assertEqual( + result['securityGroupInfo'][0]['groupName'], + sec['name']) + default = db.security_group_get_by_name(self.context, + self.context.project_id, + 'default') + result = self.cloud.describe_security_groups(self.context, + group_id=[default['id']]) + self.assertEqual(len(result['securityGroupInfo']), 1) + self.assertEqual( + result['securityGroupInfo'][0]['groupName'], + 'default') + db.security_group_destroy(self.context, sec['id']) + + def test_create_delete_security_group(self): + descript = 'test description' + create = self.cloud.create_security_group + result = create(self.context, 'testgrp', descript) + group_descript = result['securityGroupSet'][0]['groupDescription'] + self.assertEqual(descript, group_descript) + delete = self.cloud.delete_security_group + self.assertTrue(delete(self.context, 'testgrp')) + + def test_delete_security_group_by_id(self): + sec = db.security_group_create(self.context, + {'project_id': self.context.project_id, + 'name': 'test'}) + delete = self.cloud.delete_security_group + self.assertTrue(delete(self.context, group_id=sec['id'])) + + def test_delete_security_group_with_bad_name(self): + delete = self.cloud.delete_security_group + notfound = exception.SecurityGroupNotFound + self.assertRaises(notfound, delete, self.context, 'badname') + + def test_delete_security_group_with_bad_group_id(self): + delete = self.cloud.delete_security_group + notfound = exception.SecurityGroupNotFound + self.assertRaises(notfound, delete, self.context, group_id=999) + + def test_delete_security_group_no_params(self): + delete = self.cloud.delete_security_group + self.assertRaises(exception.ApiError, delete, self.context) + + def test_authorize_security_group_ingress(self): + kwargs = {'project_id': self.context.project_id, 'name': 'test'} + sec = db.security_group_create(self.context, kwargs) + authz = self.cloud.authorize_security_group_ingress + kwargs = {'to_port': '999', 'from_port': '999', 'ip_protocol': 'tcp'} + self.assertTrue(authz(self.context, group_name=sec['name'], **kwargs)) + + def test_authorize_security_group_ingress_ip_permissions_ip_ranges(self): + kwargs = {'project_id': self.context.project_id, 'name': 'test'} + sec = db.security_group_create(self.context, kwargs) + authz = self.cloud.authorize_security_group_ingress + kwargs = {'ip_permissions': [{'to_port': 81, 'from_port': 81, + 'ip_ranges': + {'1': {'cidr_ip': u'0.0.0.0/0'}, + '2': {'cidr_ip': u'10.10.10.10/32'}}, + 'ip_protocol': u'tcp'}]} + self.assertTrue(authz(self.context, group_name=sec['name'], **kwargs)) + + def test_authorize_security_group_ingress_ip_permissions_groups(self): + kwargs = {'project_id': self.context.project_id, 'name': 'test'} + sec = db.security_group_create(self.context, kwargs) + authz = self.cloud.authorize_security_group_ingress + kwargs = {'ip_permissions': [{'to_port': 81, 'from_port': 81, + 'ip_ranges':{'1': {'cidr_ip': u'0.0.0.0/0'}, + '2': {'cidr_ip': u'10.10.10.10/32'}}, + 'groups': {'1': {'user_id': u'someuser', + 'group_name': u'somegroup1'}, + '2': {'user_id': u'someuser', + 'group_name': u'othergroup2'}}, + 'ip_protocol': u'tcp'}]} + self.assertTrue(authz(self.context, group_name=sec['name'], **kwargs)) + + def test_revoke_security_group_ingress(self): + kwargs = {'project_id': self.context.project_id, 'name': 'test'} + sec = db.security_group_create(self.context, kwargs) + authz = self.cloud.authorize_security_group_ingress + kwargs = {'to_port': '999', 'from_port': '999', 'ip_protocol': 'tcp'} + authz(self.context, group_id=sec['id'], **kwargs) + revoke = self.cloud.revoke_security_group_ingress + self.assertTrue(revoke(self.context, group_name=sec['name'], **kwargs)) + + def test_revoke_security_group_ingress_by_id(self): + kwargs = {'project_id': self.context.project_id, 'name': 'test'} + sec = db.security_group_create(self.context, kwargs) + authz = self.cloud.authorize_security_group_ingress + kwargs = {'to_port': '999', 'from_port': '999', 'ip_protocol': 'tcp'} + authz(self.context, group_id=sec['id'], **kwargs) + revoke = self.cloud.revoke_security_group_ingress + self.assertTrue(revoke(self.context, group_id=sec['id'], **kwargs)) + + def test_authorize_security_group_ingress_by_id(self): + sec = db.security_group_create(self.context, + {'project_id': self.context.project_id, + 'name': 'test'}) + authz = self.cloud.authorize_security_group_ingress + kwargs = {'to_port': '999', 'from_port': '999', 'ip_protocol': 'tcp'} + self.assertTrue(authz(self.context, group_id=sec['id'], **kwargs)) + + def test_authorize_security_group_ingress_missing_protocol_params(self): + sec = db.security_group_create(self.context, + {'project_id': self.context.project_id, + 'name': 'test'}) + authz = self.cloud.authorize_security_group_ingress + self.assertRaises(exception.ApiError, authz, self.context, 'test') + + def test_authorize_security_group_ingress_missing_group_name_or_id(self): + kwargs = {'project_id': self.context.project_id, 'name': 'test'} + authz = self.cloud.authorize_security_group_ingress + self.assertRaises(exception.ApiError, authz, self.context, **kwargs) + + def test_authorize_security_group_ingress_already_exists(self): + kwargs = {'project_id': self.context.project_id, 'name': 'test'} + sec = db.security_group_create(self.context, kwargs) + authz = self.cloud.authorize_security_group_ingress + kwargs = {'to_port': '999', 'from_port': '999', 'ip_protocol': 'tcp'} + authz(self.context, group_name=sec['name'], **kwargs) + self.assertRaises(exception.ApiError, authz, self.context, + group_name=sec['name'], **kwargs) + + def test_revoke_security_group_ingress_missing_group_name_or_id(self): + kwargs = {'to_port': '999', 'from_port': '999', 'ip_protocol': 'tcp'} + revoke = self.cloud.revoke_security_group_ingress + self.assertRaises(exception.ApiError, revoke, self.context, **kwargs) + def test_describe_volumes(self): """Makes sure describe_volumes works and filters results.""" vol1 = db.volume_create(self.context, {}) vol2 = db.volume_create(self.context, {}) result = self.cloud.describe_volumes(self.context) self.assertEqual(len(result['volumeSet']), 2) - volume_id = ec2utils.id_to_ec2_id(vol2['id'], 'vol-%08x') + volume_id = ec2utils.id_to_ec2_vol_id(vol2['id']) result = self.cloud.describe_volumes(self.context, volume_id=[volume_id]) self.assertEqual(len(result['volumeSet']), 1) @@ -187,7 +375,7 @@ class CloudTestCase(test.TestCase): snap = db.snapshot_create(self.context, {'volume_id': vol['id'], 'volume_size': vol['size'], 'status': "available"}) - snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x') + snapshot_id = ec2utils.id_to_ec2_snap_id(snap['id']) result = self.cloud.create_volume(self.context, snapshot_id=snapshot_id) @@ -217,6 +405,8 @@ class CloudTestCase(test.TestCase): db.service_destroy(self.context, service1['id']) db.service_destroy(self.context, service2['id']) + # NOTE(jkoelker): this test relies on fixed_ip being in instances + @test.skip_test("EC2 stuff needs fixed_ip in instance_ref") def test_describe_snapshots(self): """Makes sure describe_snapshots works and filters results.""" vol = db.volume_create(self.context, {}) @@ -224,7 +414,7 @@ class CloudTestCase(test.TestCase): snap2 = db.snapshot_create(self.context, {'volume_id': vol['id']}) result = self.cloud.describe_snapshots(self.context) self.assertEqual(len(result['snapshotSet']), 2) - snapshot_id = ec2utils.id_to_ec2_id(snap2['id'], 'snap-%08x') + snapshot_id = ec2utils.id_to_ec2_snap_id(snap2['id']) result = self.cloud.describe_snapshots(self.context, snapshot_id=[snapshot_id]) self.assertEqual(len(result['snapshotSet']), 1) @@ -238,7 +428,7 @@ class CloudTestCase(test.TestCase): def test_create_snapshot(self): """Makes sure create_snapshot works.""" vol = db.volume_create(self.context, {'status': "available"}) - volume_id = ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x') + volume_id = ec2utils.id_to_ec2_vol_id(vol['id']) result = self.cloud.create_snapshot(self.context, volume_id=volume_id) @@ -255,7 +445,7 @@ class CloudTestCase(test.TestCase): vol = db.volume_create(self.context, {'status': "available"}) snap = db.snapshot_create(self.context, {'volume_id': vol['id'], 'status': "available"}) - snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x') + snapshot_id = ec2utils.id_to_ec2_snap_id(snap['id']) result = self.cloud.delete_snapshot(self.context, snapshot_id=snapshot_id) @@ -294,11 +484,191 @@ class CloudTestCase(test.TestCase): db.service_destroy(self.context, comp1['id']) db.service_destroy(self.context, comp2['id']) + def _block_device_mapping_create(self, instance_id, mappings): + volumes = [] + for bdm in mappings: + db.block_device_mapping_create(self.context, bdm) + if 'volume_id' in bdm: + values = {'id': bdm['volume_id']} + for bdm_key, vol_key in [('snapshot_id', 'snapshot_id'), + ('snapshot_size', 'volume_size'), + ('delete_on_termination', + 'delete_on_termination')]: + if bdm_key in bdm: + values[vol_key] = bdm[bdm_key] + vol = db.volume_create(self.context, values) + db.volume_attached(self.context, vol['id'], + instance_id, bdm['device_name']) + volumes.append(vol) + return volumes + + def _setUpBlockDeviceMapping(self): + inst1 = db.instance_create(self.context, + {'image_ref': 1, + 'root_device_name': '/dev/sdb1'}) + inst2 = db.instance_create(self.context, + {'image_ref': 2, + 'root_device_name': '/dev/sdc1'}) + + instance_id = inst1['id'] + mappings0 = [ + {'instance_id': instance_id, + 'device_name': '/dev/sdb1', + 'snapshot_id': '1', + 'volume_id': '2'}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb2', + 'volume_id': '3', + 'volume_size': 1}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb3', + 'delete_on_termination': True, + 'snapshot_id': '4', + 'volume_id': '5'}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb4', + 'delete_on_termination': False, + 'snapshot_id': '6', + 'volume_id': '7'}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb5', + 'snapshot_id': '8', + 'volume_id': '9', + 'volume_size': 0}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb6', + 'snapshot_id': '10', + 'volume_id': '11', + 'volume_size': 1}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb7', + 'no_device': True}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb8', + 'virtual_name': 'swap'}, + {'instance_id': instance_id, + 'device_name': '/dev/sdb9', + 'virtual_name': 'ephemeral3'}] + + volumes = self._block_device_mapping_create(instance_id, mappings0) + return (inst1, inst2, volumes) + + def _tearDownBlockDeviceMapping(self, inst1, inst2, volumes): + for vol in volumes: + db.volume_destroy(self.context, vol['id']) + for id in (inst1['id'], inst2['id']): + for bdm in db.block_device_mapping_get_all_by_instance( + self.context, id): + db.block_device_mapping_destroy(self.context, bdm['id']) + db.instance_destroy(self.context, inst2['id']) + db.instance_destroy(self.context, inst1['id']) + + _expected_instance_bdm1 = { + 'instanceId': 'i-00000001', + 'rootDeviceName': '/dev/sdb1', + 'rootDeviceType': 'ebs'} + + _expected_block_device_mapping0 = [ + {'deviceName': '/dev/sdb1', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 2, + }}, + {'deviceName': '/dev/sdb2', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 3, + }}, + {'deviceName': '/dev/sdb3', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': True, + 'volumeId': 5, + }}, + {'deviceName': '/dev/sdb4', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 7, + }}, + {'deviceName': '/dev/sdb5', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 9, + }}, + {'deviceName': '/dev/sdb6', + 'ebs': {'status': 'in-use', + 'deleteOnTermination': False, + 'volumeId': 11, }}] + # NOTE(yamahata): swap/ephemeral device case isn't supported yet. + + _expected_instance_bdm2 = { + 'instanceId': 'i-00000002', + 'rootDeviceName': '/dev/sdc1', + 'rootDeviceType': 'instance-store'} + + def test_format_instance_bdm(self): + (inst1, inst2, volumes) = self._setUpBlockDeviceMapping() + + result = {} + self.cloud._format_instance_bdm(self.context, inst1['id'], '/dev/sdb1', + result) + self.assertSubDictMatch( + {'rootDeviceType': self._expected_instance_bdm1['rootDeviceType']}, + result) + self._assertEqualBlockDeviceMapping( + self._expected_block_device_mapping0, result['blockDeviceMapping']) + + result = {} + self.cloud._format_instance_bdm(self.context, inst2['id'], '/dev/sdc1', + result) + self.assertSubDictMatch( + {'rootDeviceType': self._expected_instance_bdm2['rootDeviceType']}, + result) + + self._tearDownBlockDeviceMapping(inst1, inst2, volumes) + + def _assertInstance(self, instance_id): + ec2_instance_id = ec2utils.id_to_ec2_id(instance_id) + result = self.cloud.describe_instances(self.context, + instance_id=[ec2_instance_id]) + result = result['reservationSet'][0] + self.assertEqual(len(result['instancesSet']), 1) + result = result['instancesSet'][0] + self.assertEqual(result['instanceId'], ec2_instance_id) + return result + + def _assertEqualBlockDeviceMapping(self, expected, result): + self.assertEqual(len(expected), len(result)) + for x in expected: + found = False + for y in result: + if x['deviceName'] == y['deviceName']: + self.assertSubDictMatch(x, y) + found = True + break + self.assertTrue(found) + + def test_describe_instances_bdm(self): + """Make sure describe_instances works with root_device_name and + block device mappings + """ + (inst1, inst2, volumes) = self._setUpBlockDeviceMapping() + + result = self._assertInstance(inst1['id']) + self.assertSubDictMatch(self._expected_instance_bdm1, result) + self._assertEqualBlockDeviceMapping( + self._expected_block_device_mapping0, result['blockDeviceMapping']) + + result = self._assertInstance(inst2['id']) + self.assertSubDictMatch(self._expected_instance_bdm2, result) + + self._tearDownBlockDeviceMapping(inst1, inst2, volumes) + def test_describe_images(self): describe_images = self.cloud.describe_images def fake_detail(meh, context): - return [{'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1, + return [{'id': 1, 'container_format': 'ami', + 'properties': {'kernel_id': 1, 'ramdisk_id': 1, 'type': 'machine'}}] def fake_show_none(meh, context, id): @@ -323,12 +693,168 @@ class CloudTestCase(test.TestCase): self.assertRaises(exception.ImageNotFound, describe_images, self.context, ['ami-fake']) + def assertDictListUnorderedMatch(self, L1, L2, key): + self.assertEqual(len(L1), len(L2)) + for d1 in L1: + self.assertTrue(key in d1) + for d2 in L2: + self.assertTrue(key in d2) + if d1[key] == d2[key]: + self.assertDictMatch(d1, d2) + + def _setUpImageSet(self, create_volumes_and_snapshots=False): + mappings1 = [ + {'device': '/dev/sda1', 'virtual': 'root'}, + + {'device': 'sdb0', 'virtual': 'ephemeral0'}, + {'device': 'sdb1', 'virtual': 'ephemeral1'}, + {'device': 'sdb2', 'virtual': 'ephemeral2'}, + {'device': 'sdb3', 'virtual': 'ephemeral3'}, + {'device': 'sdb4', 'virtual': 'ephemeral4'}, + + {'device': 'sdc0', 'virtual': 'swap'}, + {'device': 'sdc1', 'virtual': 'swap'}, + {'device': 'sdc2', 'virtual': 'swap'}, + {'device': 'sdc3', 'virtual': 'swap'}, + {'device': 'sdc4', 'virtual': 'swap'}] + block_device_mapping1 = [ + {'device_name': '/dev/sdb1', 'snapshot_id': 01234567}, + {'device_name': '/dev/sdb2', 'volume_id': 01234567}, + {'device_name': '/dev/sdb3', 'virtual_name': 'ephemeral5'}, + {'device_name': '/dev/sdb4', 'no_device': True}, + + {'device_name': '/dev/sdc1', 'snapshot_id': 12345678}, + {'device_name': '/dev/sdc2', 'volume_id': 12345678}, + {'device_name': '/dev/sdc3', 'virtual_name': 'ephemeral6'}, + {'device_name': '/dev/sdc4', 'no_device': True}] + image1 = { + 'id': 1, + 'properties': { + 'kernel_id': 1, + 'type': 'machine', + 'image_state': 'available', + 'mappings': mappings1, + 'block_device_mapping': block_device_mapping1, + } + } + + mappings2 = [{'device': '/dev/sda1', 'virtual': 'root'}] + block_device_mapping2 = [{'device_name': '/dev/sdb1', + 'snapshot_id': 01234567}] + image2 = { + 'id': 2, + 'properties': { + 'kernel_id': 2, + 'type': 'machine', + 'root_device_name': '/dev/sdb1', + 'mappings': mappings2, + 'block_device_mapping': block_device_mapping2}} + + def fake_show(meh, context, image_id): + for i in [image1, image2]: + if i['id'] == image_id: + return i + raise exception.ImageNotFound(image_id=image_id) + + def fake_detail(meh, context): + return [image1, image2] + + self.stubs.Set(fake._FakeImageService, 'show', fake_show) + self.stubs.Set(fake._FakeImageService, 'detail', fake_detail) + + volumes = [] + snapshots = [] + if create_volumes_and_snapshots: + for bdm in block_device_mapping1: + if 'volume_id' in bdm: + vol = self._volume_create(bdm['volume_id']) + volumes.append(vol['id']) + if 'snapshot_id' in bdm: + snap = db.snapshot_create(self.context, + {'id': bdm['snapshot_id'], + 'volume_id': 76543210, + 'status': "available", + 'volume_size': 1}) + snapshots.append(snap['id']) + return (volumes, snapshots) + + def _assertImageSet(self, result, root_device_type, root_device_name): + self.assertEqual(1, len(result['imagesSet'])) + result = result['imagesSet'][0] + self.assertTrue('rootDeviceType' in result) + self.assertEqual(result['rootDeviceType'], root_device_type) + self.assertTrue('rootDeviceName' in result) + self.assertEqual(result['rootDeviceName'], root_device_name) + self.assertTrue('blockDeviceMapping' in result) + + return result + + _expected_root_device_name1 = '/dev/sda1' + # NOTE(yamahata): noDevice doesn't make sense when returning mapping + # It makes sense only when user overriding existing + # mapping. + _expected_bdms1 = [ + {'deviceName': '/dev/sdb0', 'virtualName': 'ephemeral0'}, + {'deviceName': '/dev/sdb1', 'ebs': {'snapshotId': + 'snap-00053977'}}, + {'deviceName': '/dev/sdb2', 'ebs': {'snapshotId': + 'vol-00053977'}}, + {'deviceName': '/dev/sdb3', 'virtualName': 'ephemeral5'}, + # {'deviceName': '/dev/sdb4', 'noDevice': True}, + + {'deviceName': '/dev/sdc0', 'virtualName': 'swap'}, + {'deviceName': '/dev/sdc1', 'ebs': {'snapshotId': + 'snap-00bc614e'}}, + {'deviceName': '/dev/sdc2', 'ebs': {'snapshotId': + 'vol-00bc614e'}}, + {'deviceName': '/dev/sdc3', 'virtualName': 'ephemeral6'}, + # {'deviceName': '/dev/sdc4', 'noDevice': True} + ] + + _expected_root_device_name2 = '/dev/sdb1' + _expected_bdms2 = [{'deviceName': '/dev/sdb1', + 'ebs': {'snapshotId': 'snap-00053977'}}] + + # NOTE(yamahata): + # InstanceBlockDeviceMappingItemType + # rootDeviceType + # rootDeviceName + # blockDeviceMapping + # deviceName + # virtualName + # ebs + # snapshotId + # volumeSize + # deleteOnTermination + # noDevice + def test_describe_image_mapping(self): + """test for rootDeviceName and blockDeiceMapping""" + describe_images = self.cloud.describe_images + self._setUpImageSet() + + result = describe_images(self.context, ['ami-00000001']) + result = self._assertImageSet(result, 'instance-store', + self._expected_root_device_name1) + + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], + self._expected_bdms1, 'deviceName') + + result = describe_images(self.context, ['ami-00000002']) + result = self._assertImageSet(result, 'ebs', + self._expected_root_device_name2) + + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], + self._expected_bdms2, 'deviceName') + + self.stubs.UnsetAll() + def test_describe_image_attribute(self): describe_image_attribute = self.cloud.describe_image_attribute def fake_show(meh, context, id): return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1, - 'type': 'machine'}, 'is_public': True} + 'type': 'machine'}, 'container_format': 'ami', + 'is_public': True} self.stubs.Set(fake._FakeImageService, 'show', fake_show) self.stubs.Set(fake._FakeImageService, 'show_by_name', fake_show) @@ -336,11 +862,38 @@ class CloudTestCase(test.TestCase): 'launchPermission') self.assertEqual([{'group': 'all'}], result['launchPermission']) + def test_describe_image_attribute_root_device_name(self): + describe_image_attribute = self.cloud.describe_image_attribute + self._setUpImageSet() + + result = describe_image_attribute(self.context, 'ami-00000001', + 'rootDeviceName') + self.assertEqual(result['rootDeviceName'], + self._expected_root_device_name1) + result = describe_image_attribute(self.context, 'ami-00000002', + 'rootDeviceName') + self.assertEqual(result['rootDeviceName'], + self._expected_root_device_name2) + + def test_describe_image_attribute_block_device_mapping(self): + describe_image_attribute = self.cloud.describe_image_attribute + self._setUpImageSet() + + result = describe_image_attribute(self.context, 'ami-00000001', + 'blockDeviceMapping') + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], + self._expected_bdms1, 'deviceName') + result = describe_image_attribute(self.context, 'ami-00000002', + 'blockDeviceMapping') + self.assertDictListUnorderedMatch(result['blockDeviceMapping'], + self._expected_bdms2, 'deviceName') + def test_modify_image_attribute(self): modify_image_attribute = self.cloud.modify_image_attribute def fake_show(meh, context, id): - return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1, + return {'id': 1, 'container_format': 'ami', + 'properties': {'kernel_id': 1, 'ramdisk_id': 1, 'type': 'machine'}, 'is_public': False} def fake_update(meh, context, image_id, metadata, data=None): @@ -374,6 +927,16 @@ class CloudTestCase(test.TestCase): self.assertRaises(exception.ImageNotFound, deregister_image, self.context, 'ami-bad001') + def test_deregister_image_wrong_container_type(self): + deregister_image = self.cloud.deregister_image + + def fake_delete(self, context, id): + return None + + self.stubs.Set(fake._FakeImageService, 'delete', fake_delete) + self.assertRaises(exception.NotFound, deregister_image, self.context, + 'aki-00000001') + def _run_instance(self, **kwargs): rv = self.cloud.run_instances(self.context, **kwargs) instance_id = rv['instancesSet'][0]['instanceId'] @@ -384,6 +947,21 @@ class CloudTestCase(test.TestCase): self._wait_for_running(ec2_instance_id) return ec2_instance_id + def test_rescue_unrescue_instance(self): + instance_id = self._run_instance( + image_id='ami-1', + instance_type=FLAGS.default_instance_type, + max_count=1) + self.cloud.rescue_instance(context=self.context, + instance_id=instance_id) + # NOTE(vish): This currently does no validation, it simply makes sure + # that the code path doesn't throw an exception. + self.cloud.unrescue_instance(context=self.context, + instance_id=instance_id) + # TODO(soren): We need this until we can stop polling in the rpc code + # for unit tests. + self.cloud.terminate_instances(self.context, [instance_id]) + def test_console_output(self): instance_id = self._run_instance( image_id='ami-1', @@ -489,7 +1067,7 @@ class CloudTestCase(test.TestCase): def fake_show_no_state(self, context, id): return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1, - 'type': 'machine'}} + 'type': 'machine'}, 'container_format': 'ami'} self.stubs.UnsetAll() self.stubs.Set(fake._FakeImageService, 'show', fake_show_no_state) @@ -503,7 +1081,8 @@ class CloudTestCase(test.TestCase): run_instances = self.cloud.run_instances def fake_show_decrypt(self, context, id): - return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1, + return {'id': 1, 'container_format': 'ami', + 'properties': {'kernel_id': 1, 'ramdisk_id': 1, 'type': 'machine', 'image_state': 'decrypting'}} self.stubs.UnsetAll() @@ -518,7 +1097,8 @@ class CloudTestCase(test.TestCase): run_instances = self.cloud.run_instances def fake_show_stat_active(self, context, id): - return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1, + return {'id': 1, 'container_format': 'ami', + 'properties': {'kernel_id': 1, 'ramdisk_id': 1, 'type': 'machine'}, 'status': 'active'} self.stubs.Set(fake._FakeImageService, 'show', fake_show_stat_active) @@ -548,6 +1128,8 @@ class CloudTestCase(test.TestCase): self.assertEqual('c00l 1m4g3', inst['display_name']) db.instance_destroy(self.context, inst['id']) + # NOTE(jkoelker): This test relies on mac_address in instance + @test.skip_test("EC2 stuff needs mac_address in instance_ref") def test_update_of_instance_wont_update_private_fields(self): inst = db.instance_create(self.context, {}) ec2_id = ec2utils.id_to_ec2_id(inst['id']) @@ -561,7 +1143,7 @@ class CloudTestCase(test.TestCase): def test_update_of_volume_display_fields(self): vol = db.volume_create(self.context, {}) self.cloud.update_volume(self.context, - ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x'), + ec2utils.id_to_ec2_vol_id(vol['id']), display_name='c00l v0lum3') vol = db.volume_get(self.context, vol['id']) self.assertEqual('c00l v0lum3', vol['display_name']) @@ -570,7 +1152,7 @@ class CloudTestCase(test.TestCase): def test_update_of_volume_wont_update_private_fields(self): vol = db.volume_create(self.context, {}) self.cloud.update_volume(self.context, - ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x'), + ec2utils.id_to_ec2_vol_id(vol['id']), mountpoint='/not/here') vol = db.volume_get(self.context, vol['id']) self.assertEqual(None, vol['mountpoint']) @@ -611,6 +1193,7 @@ class CloudTestCase(test.TestCase): elevated = self.context.elevated(read_deleted=True) self._wait_for_state(elevated, instance_id, is_deleted) + @test.skip_test("skipping, test is hanging with multinic for rpc reasons") def test_stop_start_instance(self): """Makes sure stop/start instance works""" # enforce periodic tasks run in short time to avoid wait for 60s. @@ -647,11 +1230,13 @@ class CloudTestCase(test.TestCase): self._restart_compute_service() - def _volume_create(self): + def _volume_create(self, volume_id=None): kwargs = {'status': 'available', 'host': self.volume.host, 'size': 1, 'attach_status': 'detached', } + if volume_id: + kwargs['id'] = volume_id return db.volume_create(self.context, kwargs) def _assert_volume_attached(self, vol, instance_id, mountpoint): @@ -666,6 +1251,7 @@ class CloudTestCase(test.TestCase): self.assertEqual(vol['status'], "available") self.assertEqual(vol['attach_status'], "detached") + @test.skip_test("skipping, test is hanging with multinic for rpc reasons") def test_stop_start_with_volume(self): """Make sure run instance with block device mapping works""" @@ -679,10 +1265,10 @@ class CloudTestCase(test.TestCase): 'max_count': 1, 'block_device_mapping': [{'device_name': '/dev/vdb', 'volume_id': vol1['id'], - 'delete_on_termination': False, }, + 'delete_on_termination': False}, {'device_name': '/dev/vdc', 'volume_id': vol2['id'], - 'delete_on_termination': True, }, + 'delete_on_termination': True}, ]} ec2_instance_id = self._run_instance_wait(**kwargs) instance_id = ec2utils.ec2_id_to_id(ec2_instance_id) @@ -734,6 +1320,7 @@ class CloudTestCase(test.TestCase): self._restart_compute_service() + @test.skip_test("skipping, test is hanging with multinic for rpc reasons") def test_stop_with_attached_volume(self): """Make sure attach info is reflected to block device mapping""" # enforce periodic tasks run in short time to avoid wait for 60s. @@ -809,10 +1396,11 @@ class CloudTestCase(test.TestCase): greenthread.sleep(0.3) return result['snapshotId'] + @test.skip_test("skipping, test is hanging with multinic for rpc reasons") def test_run_with_snapshot(self): """Makes sure run/stop/start instance with snapshot works.""" vol = self._volume_create() - ec2_volume_id = ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x') + ec2_volume_id = ec2utils.id_to_ec2_vol_id(vol['id']) ec2_snapshot1_id = self._create_snapshot(ec2_volume_id) snapshot1_id = ec2utils.ec2_id_to_id(ec2_snapshot1_id) @@ -871,3 +1459,33 @@ class CloudTestCase(test.TestCase): self.cloud.delete_snapshot(self.context, snapshot_id) greenthread.sleep(0.3) db.volume_destroy(self.context, vol['id']) + + def test_create_image(self): + """Make sure that CreateImage works""" + # enforce periodic tasks run in short time to avoid wait for 60s. + self._restart_compute_service(periodic_interval=0.3) + + (volumes, snapshots) = self._setUpImageSet( + create_volumes_and_snapshots=True) + + kwargs = {'image_id': 'ami-1', + 'instance_type': FLAGS.default_instance_type, + 'max_count': 1} + ec2_instance_id = self._run_instance_wait(**kwargs) + + # TODO(yamahata): s3._s3_create() can't be tested easily by unit test + # as there is no unit test for s3.create() + ## result = self.cloud.create_image(self.context, ec2_instance_id, + ## no_reboot=True) + ## ec2_image_id = result['imageId'] + ## created_image = self.cloud.describe_images(self.context, + ## [ec2_image_id]) + + self.cloud.terminate_instances(self.context, [ec2_instance_id]) + for vol in volumes: + db.volume_destroy(self.context, vol) + for snap in snapshots: + db.snapshot_destroy(self.context, snap) + # TODO(yamahata): clean up snapshot created by CreateImage. + + self._restart_compute_service() diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 439508b27..2a8f33dd3 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -37,6 +37,7 @@ from nova import log as logging from nova import rpc from nova import test from nova import utils +from nova.notifier import test_notifier LOG = logging.getLogger('nova.tests.compute') FLAGS = flags.FLAGS @@ -62,6 +63,7 @@ class ComputeTestCase(test.TestCase): super(ComputeTestCase, self).setUp() self.flags(connection_type='fake', stub_network=True, + notification_driver='nova.notifier.test_notifier', network_manager='nova.network.manager.FlatManager') self.compute = utils.import_object(FLAGS.compute_manager) self.compute_api = compute.API() @@ -69,6 +71,7 @@ class ComputeTestCase(test.TestCase): self.user = self.manager.create_user('fake', 'fake', 'fake') self.project = self.manager.create_project('fake', 'fake', 'fake') self.context = context.RequestContext('fake', 'fake', False) + test_notifier.NOTIFICATIONS = [] def fake_show(meh, context, id): return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}} @@ -90,7 +93,6 @@ class ComputeTestCase(test.TestCase): inst['project_id'] = self.project.id type_id = instance_types.get_instance_type_by_name('m1.tiny')['id'] inst['instance_type_id'] = type_id - inst['mac_address'] = utils.generate_mac() inst['ami_launch_index'] = 0 inst.update(params) return db.instance_create(self.context, inst)['id'] @@ -128,7 +130,7 @@ class ComputeTestCase(test.TestCase): instance_ref = models.Instance() instance_ref['id'] = 1 instance_ref['volumes'] = [vol1, vol2] - instance_ref['hostname'] = 'i-00000001' + instance_ref['hostname'] = 'hostname-1' instance_ref['host'] = 'dummy' return instance_ref @@ -160,6 +162,18 @@ class ComputeTestCase(test.TestCase): db.security_group_destroy(self.context, group['id']) db.instance_destroy(self.context, ref[0]['id']) + def test_default_hostname_generator(self): + cases = [(None, 'server_1'), ('Hello, Server!', 'hello_server'), + ('<}\x1fh\x10e\x08l\x02l\x05o\x12!{>', 'hello')] + for display_name, hostname in cases: + ref = self.compute_api.create(self.context, + instance_types.get_default_instance_type(), None, + display_name=display_name) + try: + self.assertEqual(ref[0]['hostname'], hostname) + finally: + db.instance_destroy(self.context, ref[0]['id']) + def test_destroy_instance_disassociates_security_groups(self): """Make sure destroying disassociates security groups""" group = self._create_group() @@ -327,6 +341,50 @@ class ComputeTestCase(test.TestCase): self.assert_(console) self.compute.terminate_instance(self.context, instance_id) + def test_run_instance_usage_notification(self): + """Ensure run instance generates apropriate usage notification""" + instance_id = self._create_instance() + self.compute.run_instance(self.context, instance_id) + self.assertEquals(len(test_notifier.NOTIFICATIONS), 1) + msg = test_notifier.NOTIFICATIONS[0] + self.assertEquals(msg['priority'], 'INFO') + self.assertEquals(msg['event_type'], 'compute.instance.create') + payload = msg['payload'] + self.assertEquals(payload['tenant_id'], self.project.id) + self.assertEquals(payload['user_id'], self.user.id) + self.assertEquals(payload['instance_id'], instance_id) + self.assertEquals(payload['instance_type'], 'm1.tiny') + type_id = instance_types.get_instance_type_by_name('m1.tiny')['id'] + self.assertEquals(str(payload['instance_type_id']), str(type_id)) + self.assertTrue('display_name' in payload) + self.assertTrue('created_at' in payload) + self.assertTrue('launched_at' in payload) + self.assertEquals(payload['image_ref'], '1') + self.compute.terminate_instance(self.context, instance_id) + + def test_terminate_usage_notification(self): + """Ensure terminate_instance generates apropriate usage notification""" + instance_id = self._create_instance() + self.compute.run_instance(self.context, instance_id) + test_notifier.NOTIFICATIONS = [] + self.compute.terminate_instance(self.context, instance_id) + + self.assertEquals(len(test_notifier.NOTIFICATIONS), 1) + msg = test_notifier.NOTIFICATIONS[0] + self.assertEquals(msg['priority'], 'INFO') + self.assertEquals(msg['event_type'], 'compute.instance.delete') + payload = msg['payload'] + self.assertEquals(payload['tenant_id'], self.project.id) + self.assertEquals(payload['user_id'], self.user.id) + self.assertEquals(payload['instance_id'], instance_id) + self.assertEquals(payload['instance_type'], 'm1.tiny') + type_id = instance_types.get_instance_type_by_name('m1.tiny')['id'] + self.assertEquals(str(payload['instance_type_id']), str(type_id)) + self.assertTrue('display_name' in payload) + self.assertTrue('created_at' in payload) + self.assertTrue('launched_at' in payload) + self.assertEquals(payload['image_ref'], '1') + def test_run_instance_existing(self): """Ensure failure when running an instance that already exists""" instance_id = self._create_instance() @@ -363,13 +421,15 @@ class ComputeTestCase(test.TestCase): pass self.stubs.Set(self.compute.driver, 'finish_resize', fake) + self.stubs.Set(self.compute.network_api, 'get_instance_nw_info', fake) context = self.context.elevated() instance_id = self._create_instance() - self.compute.prep_resize(context, instance_id, 1) + instance_ref = db.instance_get(context, instance_id) + self.compute.prep_resize(context, instance_ref['uuid'], 1) migration_ref = db.migration_get_by_instance_and_status(context, - instance_id, 'pre-migrating') + instance_ref['uuid'], 'pre-migrating') try: - self.compute.finish_resize(context, instance_id, + self.compute.finish_resize(context, instance_ref['uuid'], int(migration_ref['id']), {}) except KeyError, e: # Only catch key errors. We want other reasons for the test to @@ -378,17 +438,50 @@ class ComputeTestCase(test.TestCase): self.compute.terminate_instance(self.context, instance_id) + def test_resize_instance_notification(self): + """Ensure notifications on instance migrate/resize""" + instance_id = self._create_instance() + context = self.context.elevated() + inst_ref = db.instance_get(context, instance_id) + + self.compute.run_instance(self.context, instance_id) + test_notifier.NOTIFICATIONS = [] + + db.instance_update(self.context, instance_id, {'host': 'foo'}) + self.compute.prep_resize(context, inst_ref['uuid'], 1) + migration_ref = db.migration_get_by_instance_and_status(context, + inst_ref['uuid'], 'pre-migrating') + + self.assertEquals(len(test_notifier.NOTIFICATIONS), 1) + msg = test_notifier.NOTIFICATIONS[0] + self.assertEquals(msg['priority'], 'INFO') + self.assertEquals(msg['event_type'], 'compute.instance.resize.prep') + payload = msg['payload'] + self.assertEquals(payload['tenant_id'], self.project.id) + self.assertEquals(payload['user_id'], self.user.id) + self.assertEquals(payload['instance_id'], instance_id) + self.assertEquals(payload['instance_type'], 'm1.tiny') + type_id = instance_types.get_instance_type_by_name('m1.tiny')['id'] + self.assertEquals(str(payload['instance_type_id']), str(type_id)) + self.assertTrue('display_name' in payload) + self.assertTrue('created_at' in payload) + self.assertTrue('launched_at' in payload) + self.assertEquals(payload['image_ref'], '1') + self.compute.terminate_instance(context, instance_id) + def test_resize_instance(self): """Ensure instance can be migrated/resized""" instance_id = self._create_instance() context = self.context.elevated() + inst_ref = db.instance_get(context, instance_id) self.compute.run_instance(self.context, instance_id) - db.instance_update(self.context, instance_id, {'host': 'foo'}) - self.compute.prep_resize(context, instance_id, 1) + db.instance_update(self.context, inst_ref['uuid'], + {'host': 'foo'}) + self.compute.prep_resize(context, inst_ref['uuid'], 1) migration_ref = db.migration_get_by_instance_and_status(context, - instance_id, 'pre-migrating') - self.compute.resize_instance(context, instance_id, + inst_ref['uuid'], 'pre-migrating') + self.compute.resize_instance(context, inst_ref['uuid'], migration_ref['id']) self.compute.terminate_instance(context, instance_id) @@ -430,6 +523,57 @@ class ComputeTestCase(test.TestCase): self.compute.terminate_instance(context, instance_id) + def test_finish_revert_resize(self): + """Ensure that the flavor is reverted to the original on revert""" + context = self.context.elevated() + instance_id = self._create_instance() + + def fake(*args, **kwargs): + pass + + self.stubs.Set(self.compute.driver, 'finish_resize', fake) + self.stubs.Set(self.compute.driver, 'revert_resize', fake) + self.stubs.Set(self.compute.network_api, 'get_instance_nw_info', fake) + + self.compute.run_instance(self.context, instance_id) + + # Confirm the instance size before the resize starts + inst_ref = db.instance_get(context, instance_id) + instance_type_ref = db.instance_type_get(context, + inst_ref['instance_type_id']) + self.assertEqual(instance_type_ref['flavorid'], 1) + + db.instance_update(self.context, instance_id, {'host': 'foo'}) + + self.compute.prep_resize(context, inst_ref['uuid'], 3) + + migration_ref = db.migration_get_by_instance_and_status(context, + inst_ref['uuid'], 'pre-migrating') + + self.compute.resize_instance(context, inst_ref['uuid'], + migration_ref['id']) + self.compute.finish_resize(context, inst_ref['uuid'], + int(migration_ref['id']), {}) + + # Prove that the instance size is now the new size + inst_ref = db.instance_get(context, instance_id) + instance_type_ref = db.instance_type_get(context, + inst_ref['instance_type_id']) + self.assertEqual(instance_type_ref['flavorid'], 3) + + # Finally, revert and confirm the old flavor has been applied + self.compute.revert_resize(context, inst_ref['uuid'], + migration_ref['id']) + self.compute.finish_revert_resize(context, inst_ref['uuid'], + migration_ref['id']) + + inst_ref = db.instance_get(context, instance_id) + instance_type_ref = db.instance_type_get(context, + inst_ref['instance_type_id']) + self.assertEqual(instance_type_ref['flavorid'], 1) + + self.compute.terminate_instance(context, instance_id) + def test_get_by_flavor_id(self): type = instance_types.get_instance_type_by_flavor_id(1) self.assertEqual(type['name'], 'm1.tiny') @@ -443,6 +587,14 @@ class ComputeTestCase(test.TestCase): self.context, instance_id, 1) self.compute.terminate_instance(self.context, instance_id) + def test_migrate(self): + context = self.context.elevated() + instance_id = self._create_instance() + self.compute.run_instance(self.context, instance_id) + # Migrate simply calls resize() without a flavor_id. + self.compute_api.resize(context, instance_id, None) + self.compute.terminate_instance(context, instance_id) + def _setup_other_managers(self): self.volume_manager = utils.import_object(FLAGS.volume_manager) self.network_manager = utils.import_object(FLAGS.network_manager) @@ -456,7 +608,7 @@ class ComputeTestCase(test.TestCase): dbmock = self.mox.CreateMock(db) dbmock.instance_get(c, i_id).AndReturn(instance_ref) - dbmock.instance_get_fixed_address(c, i_id).AndReturn(None) + dbmock.instance_get_fixed_addresses(c, i_id).AndReturn(None) self.compute.db = dbmock self.mox.ReplayAll() @@ -472,20 +624,18 @@ class ComputeTestCase(test.TestCase): self._setup_other_managers() dbmock = self.mox.CreateMock(db) volmock = self.mox.CreateMock(self.volume_manager) - netmock = self.mox.CreateMock(self.network_manager) drivermock = self.mox.CreateMock(self.compute_driver) dbmock.instance_get(c, i_ref['id']).AndReturn(i_ref) - dbmock.instance_get_fixed_address(c, i_ref['id']).AndReturn('dummy') + dbmock.instance_get_fixed_addresses(c, i_ref['id']).AndReturn('dummy') for i in range(len(i_ref['volumes'])): vid = i_ref['volumes'][i]['id'] volmock.setup_compute_volume(c, vid).InAnyOrder('g1') - netmock.setup_compute_network(c, i_ref['id']) + drivermock.plug_vifs(i_ref, []) drivermock.ensure_filtering_rules_for_instance(i_ref) self.compute.db = dbmock self.compute.volume_manager = volmock - self.compute.network_manager = netmock self.compute.driver = drivermock self.mox.ReplayAll() @@ -500,18 +650,16 @@ class ComputeTestCase(test.TestCase): self._setup_other_managers() dbmock = self.mox.CreateMock(db) - netmock = self.mox.CreateMock(self.network_manager) drivermock = self.mox.CreateMock(self.compute_driver) dbmock.instance_get(c, i_ref['id']).AndReturn(i_ref) - dbmock.instance_get_fixed_address(c, i_ref['id']).AndReturn('dummy') + dbmock.instance_get_fixed_addresses(c, i_ref['id']).AndReturn('dummy') self.mox.StubOutWithMock(compute_manager.LOG, 'info') compute_manager.LOG.info(_("%s has no volume."), i_ref['hostname']) - netmock.setup_compute_network(c, i_ref['id']) + drivermock.plug_vifs(i_ref, []) drivermock.ensure_filtering_rules_for_instance(i_ref) self.compute.db = dbmock - self.compute.network_manager = netmock self.compute.driver = drivermock self.mox.ReplayAll() @@ -532,18 +680,20 @@ class ComputeTestCase(test.TestCase): dbmock = self.mox.CreateMock(db) netmock = self.mox.CreateMock(self.network_manager) volmock = self.mox.CreateMock(self.volume_manager) + drivermock = self.mox.CreateMock(self.compute_driver) dbmock.instance_get(c, i_ref['id']).AndReturn(i_ref) - dbmock.instance_get_fixed_address(c, i_ref['id']).AndReturn('dummy') + dbmock.instance_get_fixed_addresses(c, i_ref['id']).AndReturn('dummy') for i in range(len(i_ref['volumes'])): volmock.setup_compute_volume(c, i_ref['volumes'][i]['id']) for i in range(FLAGS.live_migration_retry_count): - netmock.setup_compute_network(c, i_ref['id']).\ + drivermock.plug_vifs(i_ref, []).\ AndRaise(exception.ProcessExecutionError()) self.compute.db = dbmock self.compute.network_manager = netmock self.compute.volume_manager = volmock + self.compute.driver = drivermock self.mox.ReplayAll() self.assertRaises(exception.ProcessExecutionError, @@ -678,7 +828,7 @@ class ComputeTestCase(test.TestCase): for v in i_ref['volumes']: self.compute.volume_manager.remove_compute_volume(c, v['id']) self.mox.StubOutWithMock(self.compute.driver, 'unfilter_instance') - self.compute.driver.unfilter_instance(i_ref) + self.compute.driver.unfilter_instance(i_ref, []) # executing self.mox.ReplayAll() @@ -721,3 +871,114 @@ class ComputeTestCase(test.TestCase): LOG.info(_("After force-killing instances: %s"), instances) self.assertEqual(len(instances), 1) self.assertEqual(power_state.SHUTOFF, instances[0]['state']) + + @staticmethod + def _parse_db_block_device_mapping(bdm_ref): + attr_list = ('delete_on_termination', 'device_name', 'no_device', + 'virtual_name', 'volume_id', 'volume_size', 'snapshot_id') + bdm = {} + for attr in attr_list: + val = bdm_ref.get(attr, None) + if val: + bdm[attr] = val + + return bdm + + def test_update_block_device_mapping(self): + instance_id = self._create_instance() + mappings = [ + {'virtual': 'ami', 'device': 'sda1'}, + {'virtual': 'root', 'device': '/dev/sda1'}, + + {'virtual': 'swap', 'device': 'sdb1'}, + {'virtual': 'swap', 'device': 'sdb2'}, + {'virtual': 'swap', 'device': 'sdb3'}, + {'virtual': 'swap', 'device': 'sdb4'}, + + {'virtual': 'ephemeral0', 'device': 'sdc1'}, + {'virtual': 'ephemeral1', 'device': 'sdc2'}, + {'virtual': 'ephemeral2', 'device': 'sdc3'}] + block_device_mapping = [ + # root + {'device_name': '/dev/sda1', + 'snapshot_id': 0x12345678, + 'delete_on_termination': False}, + + + # overwrite swap + {'device_name': '/dev/sdb2', + 'snapshot_id': 0x23456789, + 'delete_on_termination': False}, + {'device_name': '/dev/sdb3', + 'snapshot_id': 0x3456789A}, + {'device_name': '/dev/sdb4', + 'no_device': True}, + + # overwrite ephemeral + {'device_name': '/dev/sdc2', + 'snapshot_id': 0x456789AB, + 'delete_on_termination': False}, + {'device_name': '/dev/sdc3', + 'snapshot_id': 0x56789ABC}, + {'device_name': '/dev/sdc4', + 'no_device': True}, + + # volume + {'device_name': '/dev/sdd1', + 'snapshot_id': 0x87654321, + 'delete_on_termination': False}, + {'device_name': '/dev/sdd2', + 'snapshot_id': 0x98765432}, + {'device_name': '/dev/sdd3', + 'snapshot_id': 0xA9875463}, + {'device_name': '/dev/sdd4', + 'no_device': True}] + + self.compute_api._update_image_block_device_mapping( + self.context, instance_id, mappings) + + bdms = [self._parse_db_block_device_mapping(bdm_ref) + for bdm_ref in db.block_device_mapping_get_all_by_instance( + self.context, instance_id)] + expected_result = [ + {'virtual_name': 'swap', 'device_name': '/dev/sdb1'}, + {'virtual_name': 'swap', 'device_name': '/dev/sdb2'}, + {'virtual_name': 'swap', 'device_name': '/dev/sdb3'}, + {'virtual_name': 'swap', 'device_name': '/dev/sdb4'}, + {'virtual_name': 'ephemeral0', 'device_name': '/dev/sdc1'}, + {'virtual_name': 'ephemeral1', 'device_name': '/dev/sdc2'}, + {'virtual_name': 'ephemeral2', 'device_name': '/dev/sdc3'}] + bdms.sort() + expected_result.sort() + self.assertDictListMatch(bdms, expected_result) + + self.compute_api._update_block_device_mapping( + self.context, instance_id, block_device_mapping) + bdms = [self._parse_db_block_device_mapping(bdm_ref) + for bdm_ref in db.block_device_mapping_get_all_by_instance( + self.context, instance_id)] + expected_result = [ + {'snapshot_id': 0x12345678, 'device_name': '/dev/sda1'}, + + {'virtual_name': 'swap', 'device_name': '/dev/sdb1'}, + {'snapshot_id': 0x23456789, 'device_name': '/dev/sdb2'}, + {'snapshot_id': 0x3456789A, 'device_name': '/dev/sdb3'}, + {'no_device': True, 'device_name': '/dev/sdb4'}, + + {'virtual_name': 'ephemeral0', 'device_name': '/dev/sdc1'}, + {'snapshot_id': 0x456789AB, 'device_name': '/dev/sdc2'}, + {'snapshot_id': 0x56789ABC, 'device_name': '/dev/sdc3'}, + {'no_device': True, 'device_name': '/dev/sdc4'}, + + {'snapshot_id': 0x87654321, 'device_name': '/dev/sdd1'}, + {'snapshot_id': 0x98765432, 'device_name': '/dev/sdd2'}, + {'snapshot_id': 0xA9875463, 'device_name': '/dev/sdd3'}, + {'no_device': True, 'device_name': '/dev/sdd4'}] + bdms.sort() + expected_result.sort() + self.assertDictListMatch(bdms, expected_result) + + for bdm in db.block_device_mapping_get_all_by_instance( + self.context, instance_id): + db.block_device_mapping_destroy(self.context, bdm['id']) + self.compute.terminate_instance(self.context, instance_id) diff --git a/nova/tests/test_console.py b/nova/tests/test_console.py index 831e7670f..1806cc1ea 100644 --- a/nova/tests/test_console.py +++ b/nova/tests/test_console.py @@ -61,7 +61,6 @@ class ConsoleTestCase(test.TestCase): inst['user_id'] = self.user.id inst['project_id'] = self.project.id inst['instance_type_id'] = 1 - inst['mac_address'] = utils.generate_mac() inst['ami_launch_index'] = 0 return db.instance_create(self.context, inst)['id'] diff --git a/nova/tests/test_direct.py b/nova/tests/test_direct.py index 588a24b35..4ed0c2aa5 100644 --- a/nova/tests/test_direct.py +++ b/nova/tests/test_direct.py @@ -105,24 +105,25 @@ class DirectTestCase(test.TestCase): self.assertEqual(rv['data'], 'baz') -class DirectCloudTestCase(test_cloud.CloudTestCase): - def setUp(self): - super(DirectCloudTestCase, self).setUp() - compute_handle = compute.API(image_service=self.cloud.image_service) - volume_handle = volume.API() - network_handle = network.API() - direct.register_service('compute', compute_handle) - direct.register_service('volume', volume_handle) - direct.register_service('network', network_handle) - - self.router = direct.JsonParamsMiddleware(direct.Router()) - proxy = direct.Proxy(self.router) - self.cloud.compute_api = proxy.compute - self.cloud.volume_api = proxy.volume - self.cloud.network_api = proxy.network - compute_handle.volume_api = proxy.volume - compute_handle.network_api = proxy.network - - def tearDown(self): - super(DirectCloudTestCase, self).tearDown() - direct.ROUTES = {} +# NOTE(jkoelker): This fails using the EC2 api +#class DirectCloudTestCase(test_cloud.CloudTestCase): +# def setUp(self): +# super(DirectCloudTestCase, self).setUp() +# compute_handle = compute.API(image_service=self.cloud.image_service) +# volume_handle = volume.API() +# network_handle = network.API() +# direct.register_service('compute', compute_handle) +# direct.register_service('volume', volume_handle) +# direct.register_service('network', network_handle) +# +# self.router = direct.JsonParamsMiddleware(direct.Router()) +# proxy = direct.Proxy(self.router) +# self.cloud.compute_api = proxy.compute +# self.cloud.volume_api = proxy.volume +# self.cloud.network_api = proxy.network +# compute_handle.volume_api = proxy.volume +# compute_handle.network_api = proxy.network +# +# def tearDown(self): +# super(DirectCloudTestCase, self).tearDown() +# direct.ROUTES = {} diff --git a/nova/tests/test_exception.py b/nova/tests/test_exception.py index 4d3b9cc73..cd74f8871 100644 --- a/nova/tests/test_exception.py +++ b/nova/tests/test_exception.py @@ -32,3 +32,66 @@ class ApiErrorTestCase(test.TestCase): self.assertEqual(err.__str__(), 'blah code: fake error') self.assertEqual(err.code, 'blah code') self.assertEqual(err.msg, 'fake error') + + +class FakeNotifier(object): + """Acts like the nova.notifier.api module.""" + ERROR = 88 + + def __init__(self): + self.provided_publisher = None + self.provided_event = None + self.provided_priority = None + self.provided_payload = None + + def notify(self, publisher, event, priority, payload): + self.provided_publisher = publisher + self.provided_event = event + self.provided_priority = priority + self.provided_payload = payload + + +def good_function(): + return 99 + + +def bad_function_error(): + raise exception.Error() + + +def bad_function_exception(): + raise Exception() + + +class WrapExceptionTestCase(test.TestCase): + def test_wrap_exception_good_return(self): + wrapped = exception.wrap_exception() + self.assertEquals(99, wrapped(good_function)()) + + def test_wrap_exception_throws_error(self): + wrapped = exception.wrap_exception() + self.assertRaises(exception.Error, wrapped(bad_function_error)) + + def test_wrap_exception_throws_exception(self): + wrapped = exception.wrap_exception() + # Note that Exception is converted to Error ... + self.assertRaises(exception.Error, wrapped(bad_function_exception)) + + def test_wrap_exception_with_notifier(self): + notifier = FakeNotifier() + wrapped = exception.wrap_exception(notifier, "publisher", "event", + "level") + self.assertRaises(exception.Error, wrapped(bad_function_exception)) + self.assertEquals(notifier.provided_publisher, "publisher") + self.assertEquals(notifier.provided_event, "event") + self.assertEquals(notifier.provided_priority, "level") + for key in ['exception', 'args']: + self.assertTrue(key in notifier.provided_payload.keys()) + + def test_wrap_exception_with_notifier_defaults(self): + notifier = FakeNotifier() + wrapped = exception.wrap_exception(notifier) + self.assertRaises(exception.Error, wrapped(bad_function_exception)) + self.assertEquals(notifier.provided_publisher, None) + self.assertEquals(notifier.provided_event, "bad_function_exception") + self.assertEquals(notifier.provided_priority, notifier.ERROR) diff --git a/nova/tests/test_flat_network.py b/nova/tests/test_flat_network.py deleted file mode 100644 index 8544019c0..000000000 --- a/nova/tests/test_flat_network.py +++ /dev/null @@ -1,161 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" -Unit Tests for flat network code -""" -import netaddr -import os -import unittest - -from nova import context -from nova import db -from nova import exception -from nova import flags -from nova import log as logging -from nova import test -from nova import utils -from nova.auth import manager -from nova.tests.network import base - - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.tests.network') - - -class FlatNetworkTestCase(base.NetworkTestCase): - """Test cases for network code""" - def test_public_network_association(self): - """Makes sure that we can allocate a public ip""" - # TODO(vish): better way of adding floating ips - - self.context._project = self.projects[0] - self.context.project_id = self.projects[0].id - pubnet = netaddr.IPRange(flags.FLAGS.floating_range) - address = str(list(pubnet)[0]) - try: - db.floating_ip_get_by_address(context.get_admin_context(), address) - except exception.NotFound: - db.floating_ip_create(context.get_admin_context(), - {'address': address, - 'host': FLAGS.host}) - - self.assertRaises(NotImplementedError, - self.network.allocate_floating_ip, - self.context, self.projects[0].id) - - fix_addr = self._create_address(0) - float_addr = address - self.assertRaises(NotImplementedError, - self.network.associate_floating_ip, - self.context, float_addr, fix_addr) - - address = db.instance_get_floating_address(context.get_admin_context(), - self.instance_id) - self.assertEqual(address, None) - - self.assertRaises(NotImplementedError, - self.network.disassociate_floating_ip, - self.context, float_addr) - - address = db.instance_get_floating_address(context.get_admin_context(), - self.instance_id) - self.assertEqual(address, None) - - self.assertRaises(NotImplementedError, - self.network.deallocate_floating_ip, - self.context, float_addr) - - self.network.deallocate_fixed_ip(self.context, fix_addr) - db.floating_ip_destroy(context.get_admin_context(), float_addr) - - def test_allocate_deallocate_fixed_ip(self): - """Makes sure that we can allocate and deallocate a fixed ip""" - address = self._create_address(0) - self.assertTrue(self._is_allocated_in_project(address, - self.projects[0].id)) - self._deallocate_address(0, address) - - # check if the fixed ip address is really deallocated - self.assertFalse(self._is_allocated_in_project(address, - self.projects[0].id)) - - def test_side_effects(self): - """Ensures allocating and releasing has no side effects""" - address = self._create_address(0) - address2 = self._create_address(1, self.instance2_id) - - self.assertTrue(self._is_allocated_in_project(address, - self.projects[0].id)) - self.assertTrue(self._is_allocated_in_project(address2, - self.projects[1].id)) - - self._deallocate_address(0, address) - self.assertFalse(self._is_allocated_in_project(address, - self.projects[0].id)) - - # First address release shouldn't affect the second - self.assertTrue(self._is_allocated_in_project(address2, - self.projects[0].id)) - - self._deallocate_address(1, address2) - self.assertFalse(self._is_allocated_in_project(address2, - self.projects[1].id)) - - def test_ips_are_reused(self): - """Makes sure that ip addresses that are deallocated get reused""" - address = self._create_address(0) - self.network.deallocate_fixed_ip(self.context, address) - - address2 = self._create_address(0) - self.assertEqual(address, address2) - - self.network.deallocate_fixed_ip(self.context, address2) - - def test_too_many_addresses(self): - """Test for a NoMoreAddresses exception when all fixed ips are used. - """ - admin_context = context.get_admin_context() - network = db.project_get_network(admin_context, self.projects[0].id) - num_available_ips = db.network_count_available_ips(admin_context, - network['id']) - addresses = [] - instance_ids = [] - for i in range(num_available_ips): - instance_ref = self._create_instance(0) - instance_ids.append(instance_ref['id']) - address = self._create_address(0, instance_ref['id']) - addresses.append(address) - - ip_count = db.network_count_available_ips(context.get_admin_context(), - network['id']) - self.assertEqual(ip_count, 0) - self.assertRaises(db.NoMoreAddresses, - self.network.allocate_fixed_ip, - self.context, - 'foo') - - for i in range(num_available_ips): - self.network.deallocate_fixed_ip(self.context, addresses[i]) - db.instance_destroy(context.get_admin_context(), instance_ids[i]) - ip_count = db.network_count_available_ips(context.get_admin_context(), - network['id']) - self.assertEqual(ip_count, num_available_ips) - - def run(self, result=None): - if(FLAGS.network_manager == 'nova.network.manager.FlatManager'): - super(FlatNetworkTestCase, self).run(result) diff --git a/nova/tests/test_hosts.py b/nova/tests/test_hosts.py new file mode 100644 index 000000000..548f81f8b --- /dev/null +++ b/nova/tests/test_hosts.py @@ -0,0 +1,102 @@ +# Copyright (c) 2011 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import stubout +import webob.exc + +from nova import context +from nova import exception +from nova import flags +from nova import log as logging +from nova import test +from nova.api.openstack.contrib import hosts as os_hosts +from nova.scheduler import api as scheduler_api + + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.tests.hosts') +# Simulate the hosts returned by the zone manager. +HOST_LIST = [ + {"host_name": "host_c1", "service": "compute"}, + {"host_name": "host_c2", "service": "compute"}, + {"host_name": "host_v1", "service": "volume"}, + {"host_name": "host_v2", "service": "volume"}] + + +def stub_get_host_list(req): + return HOST_LIST + + +def stub_set_host_enabled(context, host, enabled): + # We'll simulate success and failure by assuming + # that 'host_c1' always succeeds, and 'host_c2' + # always fails + fail = (host == "host_c2") + status = "enabled" if (enabled ^ fail) else "disabled" + return status + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + + +class HostTestCase(test.TestCase): + """Test Case for hosts.""" + + def setUp(self): + super(HostTestCase, self).setUp() + self.controller = os_hosts.HostController() + self.req = FakeRequest() + self.stubs.Set(scheduler_api, 'get_host_list', stub_get_host_list) + self.stubs.Set(self.controller.compute_api, 'set_host_enabled', + stub_set_host_enabled) + + def test_list_hosts(self): + """Verify that the compute hosts are returned.""" + hosts = os_hosts._list_hosts(self.req) + self.assertEqual(hosts, HOST_LIST) + + compute_hosts = os_hosts._list_hosts(self.req, "compute") + expected = [host for host in HOST_LIST + if host["service"] == "compute"] + self.assertEqual(compute_hosts, expected) + + def test_disable_host(self): + dis_body = {"status": "disable"} + result_c1 = self.controller.update(self.req, "host_c1", body=dis_body) + self.assertEqual(result_c1["status"], "disabled") + result_c2 = self.controller.update(self.req, "host_c2", body=dis_body) + self.assertEqual(result_c2["status"], "enabled") + + def test_enable_host(self): + en_body = {"status": "enable"} + result_c1 = self.controller.update(self.req, "host_c1", body=en_body) + self.assertEqual(result_c1["status"], "enabled") + result_c2 = self.controller.update(self.req, "host_c2", body=en_body) + self.assertEqual(result_c2["status"], "disabled") + + def test_bad_status_value(self): + bad_body = {"status": "bad"} + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + self.req, "host_c1", body=bad_body) + + def test_bad_update_key(self): + bad_body = {"crazy": "bad"} + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + self.req, "host_c1", body=bad_body) + + def test_bad_host(self): + self.assertRaises(exception.HostNotFound, self.controller.update, + self.req, "bogus_host_name", body={"status": "disable"}) diff --git a/nova/tests/test_instance_types_extra_specs.py b/nova/tests/test_instance_types_extra_specs.py index c26cf82ff..393ed1e36 100644 --- a/nova/tests/test_instance_types_extra_specs.py +++ b/nova/tests/test_instance_types_extra_specs.py @@ -105,8 +105,8 @@ class InstanceTypeExtraSpecsTestCase(test.TestCase): self.instance_type_id) self.assertEquals(expected_specs, actual_specs) - def test_instance_type_get_by_id_with_extra_specs(self): - instance_type = db.api.instance_type_get_by_id( + def test_instance_type_get_with_extra_specs(self): + instance_type = db.api.instance_type_get( context.get_admin_context(), self.instance_type_id) self.assertEquals(instance_type['extra_specs'], @@ -115,7 +115,7 @@ class InstanceTypeExtraSpecsTestCase(test.TestCase): xpu_arch="fermi", xpus="2", xpu_model="Tesla 2050")) - instance_type = db.api.instance_type_get_by_id( + instance_type = db.api.instance_type_get( context.get_admin_context(), 5) self.assertEquals(instance_type['extra_specs'], {}) @@ -136,7 +136,7 @@ class InstanceTypeExtraSpecsTestCase(test.TestCase): "m1.small") self.assertEquals(instance_type['extra_specs'], {}) - def test_instance_type_get_by_id_with_extra_specs(self): + def test_instance_type_get_with_extra_specs(self): instance_type = db.api.instance_type_get_by_flavor_id( context.get_admin_context(), 105) diff --git a/nova/tests/test_iptables_network.py b/nova/tests/test_iptables_network.py new file mode 100644 index 000000000..918034269 --- /dev/null +++ b/nova/tests/test_iptables_network.py @@ -0,0 +1,164 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Unit Tests for network code.""" + +import os + +from nova import test +from nova.network import linux_net + + +class IptablesManagerTestCase(test.TestCase): + sample_filter = ['#Generated by iptables-save on Fri Feb 18 15:17:05 2011', + '*filter', + ':INPUT ACCEPT [2223527:305688874]', + ':FORWARD ACCEPT [0:0]', + ':OUTPUT ACCEPT [2172501:140856656]', + ':nova-compute-FORWARD - [0:0]', + ':nova-compute-INPUT - [0:0]', + ':nova-compute-local - [0:0]', + ':nova-compute-OUTPUT - [0:0]', + ':nova-filter-top - [0:0]', + '-A FORWARD -j nova-filter-top ', + '-A OUTPUT -j nova-filter-top ', + '-A nova-filter-top -j nova-compute-local ', + '-A INPUT -j nova-compute-INPUT ', + '-A OUTPUT -j nova-compute-OUTPUT ', + '-A FORWARD -j nova-compute-FORWARD ', + '-A INPUT -i virbr0 -p udp -m udp --dport 53 -j ACCEPT ', + '-A INPUT -i virbr0 -p tcp -m tcp --dport 53 -j ACCEPT ', + '-A INPUT -i virbr0 -p udp -m udp --dport 67 -j ACCEPT ', + '-A INPUT -i virbr0 -p tcp -m tcp --dport 67 -j ACCEPT ', + '-A FORWARD -s 192.168.122.0/24 -i virbr0 -j ACCEPT ', + '-A FORWARD -i virbr0 -o virbr0 -j ACCEPT ', + '-A FORWARD -o virbr0 -j REJECT --reject-with ' + 'icmp-port-unreachable ', + '-A FORWARD -i virbr0 -j REJECT --reject-with ' + 'icmp-port-unreachable ', + 'COMMIT', + '# Completed on Fri Feb 18 15:17:05 2011'] + + sample_nat = ['# Generated by iptables-save on Fri Feb 18 15:17:05 2011', + '*nat', + ':PREROUTING ACCEPT [3936:762355]', + ':INPUT ACCEPT [2447:225266]', + ':OUTPUT ACCEPT [63491:4191863]', + ':POSTROUTING ACCEPT [63112:4108641]', + ':nova-compute-OUTPUT - [0:0]', + ':nova-compute-floating-ip-snat - [0:0]', + ':nova-compute-SNATTING - [0:0]', + ':nova-compute-PREROUTING - [0:0]', + ':nova-compute-POSTROUTING - [0:0]', + ':nova-postrouting-bottom - [0:0]', + '-A PREROUTING -j nova-compute-PREROUTING ', + '-A OUTPUT -j nova-compute-OUTPUT ', + '-A POSTROUTING -j nova-compute-POSTROUTING ', + '-A POSTROUTING -j nova-postrouting-bottom ', + '-A nova-postrouting-bottom -j nova-compute-SNATTING ', + '-A nova-compute-SNATTING -j nova-compute-floating-ip-snat ', + 'COMMIT', + '# Completed on Fri Feb 18 15:17:05 2011'] + + def setUp(self): + super(IptablesManagerTestCase, self).setUp() + self.manager = linux_net.IptablesManager() + + def test_filter_rules_are_wrapped(self): + current_lines = self.sample_filter + + table = self.manager.ipv4['filter'] + table.add_rule('FORWARD', '-s 1.2.3.4/5 -j DROP') + new_lines = self.manager._modify_rules(current_lines, table) + self.assertTrue('-A run_tests.py-FORWARD ' + '-s 1.2.3.4/5 -j DROP' in new_lines) + + table.remove_rule('FORWARD', '-s 1.2.3.4/5 -j DROP') + new_lines = self.manager._modify_rules(current_lines, table) + self.assertTrue('-A run_tests.py-FORWARD ' + '-s 1.2.3.4/5 -j DROP' not in new_lines) + + def test_nat_rules(self): + current_lines = self.sample_nat + new_lines = self.manager._modify_rules(current_lines, + self.manager.ipv4['nat']) + + for line in [':nova-compute-OUTPUT - [0:0]', + ':nova-compute-floating-ip-snat - [0:0]', + ':nova-compute-SNATTING - [0:0]', + ':nova-compute-PREROUTING - [0:0]', + ':nova-compute-POSTROUTING - [0:0]']: + self.assertTrue(line in new_lines, "One of nova-compute's chains " + "went missing.") + + seen_lines = set() + for line in new_lines: + line = line.strip() + self.assertTrue(line not in seen_lines, + "Duplicate line: %s" % line) + seen_lines.add(line) + + last_postrouting_line = '' + + for line in new_lines: + if line.startswith('-A POSTROUTING'): + last_postrouting_line = line + + self.assertTrue('-j nova-postrouting-bottom' in last_postrouting_line, + "Last POSTROUTING rule does not jump to " + "nova-postouting-bottom: %s" % last_postrouting_line) + + for chain in ['POSTROUTING', 'PREROUTING', 'OUTPUT']: + self.assertTrue('-A %s -j run_tests.py-%s' \ + % (chain, chain) in new_lines, + "Built-in chain %s not wrapped" % (chain,)) + + def test_filter_rules(self): + current_lines = self.sample_filter + new_lines = self.manager._modify_rules(current_lines, + self.manager.ipv4['filter']) + + for line in [':nova-compute-FORWARD - [0:0]', + ':nova-compute-INPUT - [0:0]', + ':nova-compute-local - [0:0]', + ':nova-compute-OUTPUT - [0:0]']: + self.assertTrue(line in new_lines, "One of nova-compute's chains" + " went missing.") + + seen_lines = set() + for line in new_lines: + line = line.strip() + self.assertTrue(line not in seen_lines, + "Duplicate line: %s" % line) + seen_lines.add(line) + + for chain in ['FORWARD', 'OUTPUT']: + for line in new_lines: + if line.startswith('-A %s' % chain): + self.assertTrue('-j nova-filter-top' in line, + "First %s rule does not " + "jump to nova-filter-top" % chain) + break + + self.assertTrue('-A nova-filter-top ' + '-j run_tests.py-local' in new_lines, + "nova-filter-top does not jump to wrapped local chain") + + for chain in ['INPUT', 'OUTPUT', 'FORWARD']: + self.assertTrue('-A %s -j run_tests.py-%s' \ + % (chain, chain) in new_lines, + "Built-in chain %s not wrapped" % (chain,)) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index d12e21063..ad0931a89 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -54,12 +54,17 @@ def _create_network_info(count=1, ipv6=None): fake_ip = '0.0.0.0/0' fake_ip_2 = '0.0.0.1/0' fake_ip_3 = '0.0.0.1/0' - network = {'gateway': fake, - 'gateway_v6': fake, - 'bridge': fake, + fake_vlan = 100 + fake_bridge_interface = 'eth0' + network = {'bridge': fake, 'cidr': fake_ip, - 'cidr_v6': fake_ip} + 'cidr_v6': fake_ip, + 'vlan': fake_vlan, + 'bridge_interface': fake_bridge_interface} mapping = {'mac': fake, + 'dhcp_server': fake, + 'gateway': fake, + 'gateway6': fake, 'ips': [{'ip': fake_ip}, {'ip': fake_ip}]} if ipv6: mapping['ip6s'] = [{'ip': fake_ip}, @@ -68,6 +73,24 @@ def _create_network_info(count=1, ipv6=None): return [(network, mapping) for x in xrange(0, count)] +def _setup_networking(instance_id, ip='1.2.3.4'): + ctxt = context.get_admin_context() + network_ref = db.project_get_networks(ctxt, + 'fake', + associate=True)[0] + vif = {'address': '56:12:12:12:12:12', + 'network_id': network_ref['id'], + 'instance_id': instance_id} + vif_ref = db.virtual_interface_create(ctxt, vif) + + fixed_ip = {'address': ip, + 'network_id': network_ref['id'], + 'virtual_interface_id': vif_ref['id']} + db.fixed_ip_create(ctxt, fixed_ip) + db.fixed_ip_update(ctxt, ip, {'allocated': True, + 'instance_id': instance_id}) + + class CacheConcurrencyTestCase(test.TestCase): def setUp(self): super(CacheConcurrencyTestCase, self).setUp() @@ -155,11 +178,15 @@ class LibvirtConnTestCase(test.TestCase): FLAGS.instances_path = '' self.call_libvirt_dependant_setup = False + def tearDown(self): + self.manager.delete_project(self.project) + self.manager.delete_user(self.user) + super(LibvirtConnTestCase, self).tearDown() + test_ip = '10.11.12.13' test_instance = {'memory_kb': '1024000', 'basepath': '/some/path', 'bridge_name': 'br100', - 'mac_address': '02:12:34:46:56:67', 'vcpus': 2, 'project_id': 'fake', 'bridge': 'br101', @@ -196,9 +223,19 @@ class LibvirtConnTestCase(test.TestCase): def setattr(self, key, val): self.__setattr__(key, val) + # A fake VIF driver + class FakeVIFDriver(object): + + def __init__(self, **kwargs): + pass + + def setattr(self, key, val): + self.__setattr__(key, val) + # Creating mocks fake = FakeLibvirtConnection() fakeip = FakeIptablesFirewallDriver + fakevif = FakeVIFDriver() # Customizing above fake if necessary for key, val in kwargs.items(): fake.__setattr__(key, val) @@ -206,6 +243,8 @@ class LibvirtConnTestCase(test.TestCase): # Inevitable mocks for connection.LibvirtConnection self.mox.StubOutWithMock(connection.utils, 'import_class') connection.utils.import_class(mox.IgnoreArg()).AndReturn(fakeip) + self.mox.StubOutWithMock(connection.utils, 'import_object') + connection.utils.import_object(mox.IgnoreArg()).AndReturn(fakevif) self.mox.StubOutWithMock(connection.LibvirtConnection, '_conn') connection.LibvirtConnection._conn = fake @@ -241,6 +280,7 @@ class LibvirtConnTestCase(test.TestCase): return db.service_create(context.get_admin_context(), service_ref) + @test.skip_test("Please review this test to ensure intent") def test_preparing_xml_info(self): conn = connection.LibvirtConnection(True) instance_ref = db.instance_create(self.context, self.test_instance) @@ -256,39 +296,27 @@ class LibvirtConnTestCase(test.TestCase): _create_network_info(2)) self.assertTrue(len(result['nics']) == 2) - def test_get_nic_for_xml_v4(self): - conn = connection.LibvirtConnection(True) - network, mapping = _create_network_info()[0] - self.flags(use_ipv6=False) - params = conn._get_nic_for_xml(network, mapping)['extra_params'] - self.assertTrue(params.find('PROJNETV6') == -1) - self.assertTrue(params.find('PROJMASKV6') == -1) - - def test_get_nic_for_xml_v6(self): - conn = connection.LibvirtConnection(True) - network, mapping = _create_network_info()[0] - self.flags(use_ipv6=True) - params = conn._get_nic_for_xml(network, mapping)['extra_params'] - self.assertTrue(params.find('PROJNETV6') > -1) - self.assertTrue(params.find('PROJMASKV6') > -1) - + @test.skip_test("skipping libvirt tests depends on get_network_info shim") def test_xml_and_uri_no_ramdisk_no_kernel(self): instance_data = dict(self.test_instance) self._check_xml_and_uri(instance_data, expect_kernel=False, expect_ramdisk=False) + @test.skip_test("skipping libvirt tests depends on get_network_info shim") def test_xml_and_uri_no_ramdisk(self): instance_data = dict(self.test_instance) instance_data['kernel_id'] = 'aki-deadbeef' self._check_xml_and_uri(instance_data, expect_kernel=True, expect_ramdisk=False) + @test.skip_test("skipping libvirt tests depends on get_network_info shim") def test_xml_and_uri_no_kernel(self): instance_data = dict(self.test_instance) instance_data['ramdisk_id'] = 'ari-deadbeef' self._check_xml_and_uri(instance_data, expect_kernel=False, expect_ramdisk=False) + @test.skip_test("skipping libvirt tests depends on get_network_info shim") def test_xml_and_uri(self): instance_data = dict(self.test_instance) instance_data['ramdisk_id'] = 'ari-deadbeef' @@ -296,6 +324,7 @@ class LibvirtConnTestCase(test.TestCase): self._check_xml_and_uri(instance_data, expect_kernel=True, expect_ramdisk=True) + @test.skip_test("skipping libvirt tests depends on get_network_info shim") def test_xml_and_uri_rescue(self): instance_data = dict(self.test_instance) instance_data['ramdisk_id'] = 'ari-deadbeef' @@ -303,6 +332,7 @@ class LibvirtConnTestCase(test.TestCase): self._check_xml_and_uri(instance_data, expect_kernel=True, expect_ramdisk=True, rescue=True) + @test.skip_test("skipping libvirt tests depends on get_network_info shim") def test_lxc_container_and_uri(self): instance_data = dict(self.test_instance) self._check_xml_and_container(instance_data) @@ -402,12 +432,18 @@ class LibvirtConnTestCase(test.TestCase): user_context = context.RequestContext(project=self.project, user=self.user) instance_ref = db.instance_create(user_context, instance) - host = self.network.get_network_host(user_context.elevated()) - network_ref = db.project_get_network(context.get_admin_context(), - self.project.id) - + # Re-get the instance so it's bound to an actual session + instance_ref = db.instance_get(user_context, instance_ref['id']) + network_ref = db.project_get_networks(context.get_admin_context(), + self.project.id)[0] + + vif = {'address': '56:12:12:12:12:12', + 'network_id': network_ref['id'], + 'instance_id': instance_ref['id']} + vif_ref = db.virtual_interface_create(self.context, vif) fixed_ip = {'address': self.test_ip, - 'network_id': network_ref['id']} + 'network_id': network_ref['id'], + 'virtual_interface_id': vif_ref['id']} ctxt = context.get_admin_context() fixed_ip_ref = db.fixed_ip_create(ctxt, fixed_ip) @@ -442,18 +478,10 @@ class LibvirtConnTestCase(test.TestCase): user_context = context.RequestContext(project=self.project, user=self.user) instance_ref = db.instance_create(user_context, instance) - host = self.network.get_network_host(user_context.elevated()) - network_ref = db.project_get_network(context.get_admin_context(), - self.project.id) - - fixed_ip = {'address': self.test_ip, - 'network_id': network_ref['id']} + network_ref = db.project_get_networks(context.get_admin_context(), + self.project.id)[0] - ctxt = context.get_admin_context() - fixed_ip_ref = db.fixed_ip_create(ctxt, fixed_ip) - db.fixed_ip_update(ctxt, self.test_ip, - {'allocated': True, - 'instance_id': instance_ref['id']}) + _setup_networking(instance_ref['id'], ip=self.test_ip) type_uri_map = {'qemu': ('qemu:///system', [(lambda t: t.find('.').get('type'), 'qemu'), @@ -694,6 +722,9 @@ class LibvirtConnTestCase(test.TestCase): return vdmock self.create_fake_libvirt_mock(lookupByName=fake_lookup) + self.mox.StubOutWithMock(self.compute, "recover_live_migration") + self.compute.recover_live_migration(self.context, instance_ref, + dest='dest') # Start test self.mox.ReplayAll() @@ -712,6 +743,7 @@ class LibvirtConnTestCase(test.TestCase): db.volume_destroy(self.context, volume_ref['id']) db.instance_destroy(self.context, instance_ref['id']) + @test.skip_test("test needs rewrite: instance no longer has mac_address") def test_spawn_with_network_info(self): # Skip if non-libvirt environment if not self.lazy_load_library_exists(): @@ -730,8 +762,8 @@ class LibvirtConnTestCase(test.TestCase): conn.firewall_driver.setattr('setup_basic_filtering', fake_none) conn.firewall_driver.setattr('prepare_instance_filter', fake_none) - network = db.project_get_network(context.get_admin_context(), - self.project.id) + network = db.project_get_networks(context.get_admin_context(), + self.project.id)[0] ip_dict = {'ip': self.test_ip, 'netmask': network['netmask'], 'enabled': '1'} @@ -756,11 +788,6 @@ class LibvirtConnTestCase(test.TestCase): ip = conn.get_host_ip_addr() self.assertEquals(ip, FLAGS.my_ip) - def tearDown(self): - self.manager.delete_project(self.project) - self.manager.delete_user(self.user) - super(LibvirtConnTestCase, self).tearDown() - class NWFilterFakes: def __init__(self): @@ -866,19 +893,24 @@ class IptablesFirewallTestCase(test.TestCase): return db.instance_create(self.context, {'user_id': 'fake', 'project_id': 'fake', - 'mac_address': '56:12:12:12:12:12', 'instance_type_id': 1}) + @test.skip_test("skipping libvirt tests depends on get_network_info shim") def test_static_filters(self): instance_ref = self._create_instance_ref() ip = '10.11.12.13' - network_ref = db.project_get_network(self.context, - 'fake') + network_ref = db.project_get_networks(self.context, + 'fake', + associate=True)[0] + vif = {'address': '56:12:12:12:12:12', + 'network_id': network_ref['id'], + 'instance_id': instance_ref['id']} + vif_ref = db.virtual_interface_create(self.context, vif) fixed_ip = {'address': ip, - 'network_id': network_ref['id']} - + 'network_id': network_ref['id'], + 'virtual_interface_id': vif_ref['id']} admin_ctxt = context.get_admin_context() db.fixed_ip_create(admin_ctxt, fixed_ip) db.fixed_ip_update(admin_ctxt, ip, {'allocated': True, @@ -1015,6 +1047,7 @@ class IptablesFirewallTestCase(test.TestCase): self.assertEquals(ipv6_network_rules, ipv6_rules_per_network * networks_count) + @test.skip_test("skipping libvirt tests") def test_do_refresh_security_group_rules(self): instance_ref = self._create_instance_ref() self.mox.StubOutWithMock(self.fw, @@ -1025,6 +1058,7 @@ class IptablesFirewallTestCase(test.TestCase): self.mox.ReplayAll() self.fw.do_refresh_security_group_rules("fake") + @test.skip_test("skip libvirt test project_get_network no longer exists") def test_unfilter_instance_undefines_nwfilter(self): # Skip if non-libvirt environment if not self.lazy_load_library_exists(): @@ -1058,6 +1092,7 @@ class IptablesFirewallTestCase(test.TestCase): db.instance_destroy(admin_ctxt, instance_ref['id']) + @test.skip_test("skip libvirt test project_get_network no longer exists") def test_provider_firewall_rules(self): # setup basic instance data instance_ref = self._create_instance_ref() @@ -1207,7 +1242,6 @@ class NWFilterTestCase(test.TestCase): return db.instance_create(self.context, {'user_id': 'fake', 'project_id': 'fake', - 'mac_address': '00:A0:C9:14:C8:29', 'instance_type_id': 1}) def _create_instance_type(self, params={}): @@ -1225,6 +1259,7 @@ class NWFilterTestCase(test.TestCase): inst.update(params) return db.instance_type_create(context, inst)['id'] + @test.skip_test('Skipping this test') def test_creates_base_rule_first(self): # These come pre-defined by libvirt self.defined_filters = ['no-mac-spoofing', @@ -1258,13 +1293,15 @@ class NWFilterTestCase(test.TestCase): ip = '10.11.12.13' - network_ref = db.project_get_network(self.context, 'fake') - fixed_ip = {'address': ip, 'network_id': network_ref['id']} + #network_ref = db.project_get_networks(self.context, 'fake')[0] + #fixed_ip = {'address': ip, 'network_id': network_ref['id']} - admin_ctxt = context.get_admin_context() - db.fixed_ip_create(admin_ctxt, fixed_ip) - db.fixed_ip_update(admin_ctxt, ip, {'allocated': True, - 'instance_id': inst_id}) + #admin_ctxt = context.get_admin_context() + #db.fixed_ip_create(admin_ctxt, fixed_ip) + #db.fixed_ip_update(admin_ctxt, ip, {'allocated': True, + # 'instance_id': inst_id}) + + self._setup_networking(instance_ref['id'], ip=ip) def _ensure_all_called(): instance_filter = 'nova-instance-%s-%s' % (instance_ref['name'], @@ -1299,6 +1336,7 @@ class NWFilterTestCase(test.TestCase): "fake") self.assertEquals(len(result), 3) + @test.skip_test("skip libvirt test project_get_network no longer exists") def test_unfilter_instance_undefines_nwfilters(self): admin_ctxt = context.get_admin_context() diff --git a/nova/tests/test_metadata.py b/nova/tests/test_metadata.py new file mode 100644 index 000000000..c862726ab --- /dev/null +++ b/nova/tests/test_metadata.py @@ -0,0 +1,76 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Tests for the testing the metadata code.""" + +import base64 +import httplib + +import webob + +from nova import test +from nova import wsgi +from nova.api.ec2 import metadatarequesthandler +from nova.db.sqlalchemy import api + + +class MetadataTestCase(test.TestCase): + """Test that metadata is returning proper values.""" + + def setUp(self): + super(MetadataTestCase, self).setUp() + self.instance = ({'id': 1, + 'project_id': 'test', + 'key_name': None, + 'host': 'test', + 'launch_index': 1, + 'instance_type': 'm1.tiny', + 'reservation_id': 'r-xxxxxxxx', + 'user_data': '', + 'image_ref': 7, + 'hostname': 'test'}) + + def instance_get(*args, **kwargs): + return self.instance + + def floating_get(*args, **kwargs): + return '99.99.99.99' + + self.stubs.Set(api, 'instance_get', instance_get) + self.stubs.Set(api, 'fixed_ip_get_instance', instance_get) + self.stubs.Set(api, 'instance_get_floating_address', floating_get) + self.app = metadatarequesthandler.MetadataRequestHandler() + + def request(self, relative_url): + request = webob.Request.blank(relative_url) + request.remote_addr = "127.0.0.1" + return request.get_response(self.app).body + + def test_base(self): + self.assertEqual(self.request('/'), 'meta-data/\nuser-data') + + def test_user_data(self): + self.instance['user_data'] = base64.b64encode('happy') + self.assertEqual(self.request('/user-data'), 'happy') + + def test_security_groups(self): + def sg_get(*args, **kwargs): + return [{'name': 'default'}, {'name': 'other'}] + self.stubs.Set(api, 'security_group_get_by_instance', sg_get) + self.assertEqual(self.request('/meta-data/security-groups'), + 'default\nother') diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py index 9327c7129..28f50d328 100644 --- a/nova/tests/test_network.py +++ b/nova/tests/test_network.py @@ -1,196 +1,269 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Rackspace # All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" -Unit Tests for network code -""" -import netaddr -import os +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from nova import db +from nova import exception +from nova import flags +from nova import log as logging from nova import test -from nova.network import linux_net - - -class IptablesManagerTestCase(test.TestCase): - sample_filter = ['#Generated by iptables-save on Fri Feb 18 15:17:05 2011', - '*filter', - ':INPUT ACCEPT [2223527:305688874]', - ':FORWARD ACCEPT [0:0]', - ':OUTPUT ACCEPT [2172501:140856656]', - ':nova-compute-FORWARD - [0:0]', - ':nova-compute-INPUT - [0:0]', - ':nova-compute-local - [0:0]', - ':nova-compute-OUTPUT - [0:0]', - ':nova-filter-top - [0:0]', - '-A FORWARD -j nova-filter-top ', - '-A OUTPUT -j nova-filter-top ', - '-A nova-filter-top -j nova-compute-local ', - '-A INPUT -j nova-compute-INPUT ', - '-A OUTPUT -j nova-compute-OUTPUT ', - '-A FORWARD -j nova-compute-FORWARD ', - '-A INPUT -i virbr0 -p udp -m udp --dport 53 -j ACCEPT ', - '-A INPUT -i virbr0 -p tcp -m tcp --dport 53 -j ACCEPT ', - '-A INPUT -i virbr0 -p udp -m udp --dport 67 -j ACCEPT ', - '-A INPUT -i virbr0 -p tcp -m tcp --dport 67 -j ACCEPT ', - '-A FORWARD -s 192.168.122.0/24 -i virbr0 -j ACCEPT ', - '-A FORWARD -i virbr0 -o virbr0 -j ACCEPT ', - '-A FORWARD -o virbr0 -j REJECT --reject-with ' - 'icmp-port-unreachable ', - '-A FORWARD -i virbr0 -j REJECT --reject-with ' - 'icmp-port-unreachable ', - 'COMMIT', - '# Completed on Fri Feb 18 15:17:05 2011'] - - sample_nat = ['# Generated by iptables-save on Fri Feb 18 15:17:05 2011', - '*nat', - ':PREROUTING ACCEPT [3936:762355]', - ':INPUT ACCEPT [2447:225266]', - ':OUTPUT ACCEPT [63491:4191863]', - ':POSTROUTING ACCEPT [63112:4108641]', - ':nova-compute-OUTPUT - [0:0]', - ':nova-compute-floating-ip-snat - [0:0]', - ':nova-compute-SNATTING - [0:0]', - ':nova-compute-PREROUTING - [0:0]', - ':nova-compute-POSTROUTING - [0:0]', - ':nova-postrouting-bottom - [0:0]', - '-A PREROUTING -j nova-compute-PREROUTING ', - '-A OUTPUT -j nova-compute-OUTPUT ', - '-A POSTROUTING -j nova-compute-POSTROUTING ', - '-A POSTROUTING -j nova-postrouting-bottom ', - '-A nova-postrouting-bottom -j nova-compute-SNATTING ', - '-A nova-compute-SNATTING -j nova-compute-floating-ip-snat ', - 'COMMIT', - '# Completed on Fri Feb 18 15:17:05 2011'] +from nova.network import manager as network_manager + +import mox + + +FLAGS = flags.FLAGS +LOG = logging.getLogger('nova.tests.network') + + +HOST = "testhost" + + +class FakeModel(dict): + """Represent a model from the db""" + def __init__(self, *args, **kwargs): + self.update(kwargs) + + def __getattr__(self, name): + return self[name] + + +networks = [{'id': 0, + 'label': 'test0', + 'injected': False, + 'multi_host': False, + 'cidr': '192.168.0.0/24', + 'cidr_v6': '2001:db8::/64', + 'gateway_v6': '2001:db8::1', + 'netmask_v6': '64', + 'netmask': '255.255.255.0', + 'bridge': 'fa0', + 'bridge_interface': 'fake_fa0', + 'gateway': '192.168.0.1', + 'broadcast': '192.168.0.255', + 'dns1': '192.168.0.1', + 'dns2': '192.168.0.2', + 'vlan': None, + 'host': None, + 'project_id': 'fake_project', + 'vpn_public_address': '192.168.0.2'}, + {'id': 1, + 'label': 'test1', + 'injected': False, + 'multi_host': False, + 'cidr': '192.168.1.0/24', + 'cidr_v6': '2001:db9::/64', + 'gateway_v6': '2001:db9::1', + 'netmask_v6': '64', + 'netmask': '255.255.255.0', + 'bridge': 'fa1', + 'bridge_interface': 'fake_fa1', + 'gateway': '192.168.1.1', + 'broadcast': '192.168.1.255', + 'dns1': '192.168.0.1', + 'dns2': '192.168.0.2', + 'vlan': None, + 'host': None, + 'project_id': 'fake_project', + 'vpn_public_address': '192.168.1.2'}] + + +fixed_ips = [{'id': 0, + 'network_id': 0, + 'address': '192.168.0.100', + 'instance_id': 0, + 'allocated': False, + 'virtual_interface_id': 0, + 'floating_ips': []}, + {'id': 0, + 'network_id': 1, + 'address': '192.168.1.100', + 'instance_id': 0, + 'allocated': False, + 'virtual_interface_id': 0, + 'floating_ips': []}] + + +flavor = {'id': 0, + 'rxtx_cap': 3} + + +floating_ip_fields = {'id': 0, + 'address': '192.168.10.100', + 'fixed_ip_id': 0, + 'project_id': None, + 'auto_assigned': False} + +vifs = [{'id': 0, + 'address': 'DE:AD:BE:EF:00:00', + 'network_id': 0, + 'network': FakeModel(**networks[0]), + 'instance_id': 0}, + {'id': 1, + 'address': 'DE:AD:BE:EF:00:01', + 'network_id': 1, + 'network': FakeModel(**networks[1]), + 'instance_id': 0}] + + +class FlatNetworkTestCase(test.TestCase): + def setUp(self): + super(FlatNetworkTestCase, self).setUp() + self.network = network_manager.FlatManager(host=HOST) + self.network.db = db + + def test_get_instance_nw_info(self): + self.mox.StubOutWithMock(db, 'fixed_ip_get_by_instance') + self.mox.StubOutWithMock(db, 'virtual_interface_get_by_instance') + self.mox.StubOutWithMock(db, 'instance_type_get') + + db.fixed_ip_get_by_instance(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(fixed_ips) + db.virtual_interface_get_by_instance(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(vifs) + db.instance_type_get(mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(flavor) + self.mox.ReplayAll() + + nw_info = self.network.get_instance_nw_info(None, 0, 0, None) + + self.assertTrue(nw_info) + + for i, nw in enumerate(nw_info): + i8 = i + 8 + check = {'bridge': 'fa%s' % i, + 'cidr': '192.168.%s.0/24' % i, + 'cidr_v6': '2001:db%s::/64' % i8, + 'id': i, + 'multi_host': False, + 'injected': 'DONTCARE', + 'bridge_interface': 'fake_fa%s' % i, + 'vlan': None} + + self.assertDictMatch(nw[0], check) + + check = {'broadcast': '192.168.%s.255' % i, + 'dhcp_server': '192.168.%s.1' % i, + 'dns': 'DONTCARE', + 'gateway': '192.168.%s.1' % i, + 'gateway6': '2001:db%s::1' % i8, + 'ip6s': 'DONTCARE', + 'ips': 'DONTCARE', + 'label': 'test%s' % i, + 'mac': 'DE:AD:BE:EF:00:0%s' % i, + 'rxtx_cap': 'DONTCARE', + 'should_create_vlan': False, + 'should_create_bridge': False} + self.assertDictMatch(nw[1], check) + + check = [{'enabled': 'DONTCARE', + 'ip': '2001:db%s::dcad:beff:feef:%s' % (i8, i), + 'netmask': '64'}] + self.assertDictListMatch(nw[1]['ip6s'], check) + + check = [{'enabled': '1', + 'ip': '192.168.%s.100' % i, + 'netmask': '255.255.255.0'}] + self.assertDictListMatch(nw[1]['ips'], check) + + +class VlanNetworkTestCase(test.TestCase): def setUp(self): - super(IptablesManagerTestCase, self).setUp() - self.manager = linux_net.IptablesManager() - - def test_filter_rules_are_wrapped(self): - current_lines = self.sample_filter - - table = self.manager.ipv4['filter'] - table.add_rule('FORWARD', '-s 1.2.3.4/5 -j DROP') - new_lines = self.manager._modify_rules(current_lines, table) - self.assertTrue('-A run_tests.py-FORWARD ' - '-s 1.2.3.4/5 -j DROP' in new_lines) - - table.remove_rule('FORWARD', '-s 1.2.3.4/5 -j DROP') - new_lines = self.manager._modify_rules(current_lines, table) - self.assertTrue('-A run_tests.py-FORWARD ' - '-s 1.2.3.4/5 -j DROP' not in new_lines) - - def test_nat_rules(self): - current_lines = self.sample_nat - new_lines = self.manager._modify_rules(current_lines, - self.manager.ipv4['nat']) - - for line in [':nova-compute-OUTPUT - [0:0]', - ':nova-compute-floating-ip-snat - [0:0]', - ':nova-compute-SNATTING - [0:0]', - ':nova-compute-PREROUTING - [0:0]', - ':nova-compute-POSTROUTING - [0:0]']: - self.assertTrue(line in new_lines, "One of nova-compute's chains " - "went missing.") - - seen_lines = set() - for line in new_lines: - line = line.strip() - self.assertTrue(line not in seen_lines, - "Duplicate line: %s" % line) - seen_lines.add(line) - - last_postrouting_line = '' - - for line in new_lines: - if line.startswith('-A POSTROUTING'): - last_postrouting_line = line - - self.assertTrue('-j nova-postrouting-bottom' in last_postrouting_line, - "Last POSTROUTING rule does not jump to " - "nova-postouting-bottom: %s" % last_postrouting_line) - - for chain in ['POSTROUTING', 'PREROUTING', 'OUTPUT']: - self.assertTrue('-A %s -j run_tests.py-%s' \ - % (chain, chain) in new_lines, - "Built-in chain %s not wrapped" % (chain,)) - - def test_filter_rules(self): - current_lines = self.sample_filter - new_lines = self.manager._modify_rules(current_lines, - self.manager.ipv4['filter']) - - for line in [':nova-compute-FORWARD - [0:0]', - ':nova-compute-INPUT - [0:0]', - ':nova-compute-local - [0:0]', - ':nova-compute-OUTPUT - [0:0]']: - self.assertTrue(line in new_lines, "One of nova-compute's chains" - " went missing.") - - seen_lines = set() - for line in new_lines: - line = line.strip() - self.assertTrue(line not in seen_lines, - "Duplicate line: %s" % line) - seen_lines.add(line) - - for chain in ['FORWARD', 'OUTPUT']: - for line in new_lines: - if line.startswith('-A %s' % chain): - self.assertTrue('-j nova-filter-top' in line, - "First %s rule does not " - "jump to nova-filter-top" % chain) - break - - self.assertTrue('-A nova-filter-top ' - '-j run_tests.py-local' in new_lines, - "nova-filter-top does not jump to wrapped local chain") - - for chain in ['INPUT', 'OUTPUT', 'FORWARD']: - self.assertTrue('-A %s -j run_tests.py-%s' \ - % (chain, chain) in new_lines, - "Built-in chain %s not wrapped" % (chain,)) - - def test_will_empty_chain(self): - self.manager.ipv4['filter'].add_chain('test-chain') - self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP') - old_count = len(self.manager.ipv4['filter'].rules) - self.manager.ipv4['filter'].empty_chain('test-chain') - self.assertEqual(old_count - 1, len(self.manager.ipv4['filter'].rules)) - - def test_will_empty_unwrapped_chain(self): - self.manager.ipv4['filter'].add_chain('test-chain', wrap=False) - self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP', - wrap=False) - old_count = len(self.manager.ipv4['filter'].rules) - self.manager.ipv4['filter'].empty_chain('test-chain', wrap=False) - self.assertEqual(old_count - 1, len(self.manager.ipv4['filter'].rules)) - - def test_will_not_empty_wrapped_when_unwrapped(self): - self.manager.ipv4['filter'].add_chain('test-chain') - self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP') - old_count = len(self.manager.ipv4['filter'].rules) - self.manager.ipv4['filter'].empty_chain('test-chain', wrap=False) - self.assertEqual(old_count, len(self.manager.ipv4['filter'].rules)) - - def test_will_not_empty_unwrapped_when_wrapped(self): - self.manager.ipv4['filter'].add_chain('test-chain', wrap=False) - self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP', - wrap=False) - old_count = len(self.manager.ipv4['filter'].rules) - self.manager.ipv4['filter'].empty_chain('test-chain') - self.assertEqual(old_count, len(self.manager.ipv4['filter'].rules)) + super(VlanNetworkTestCase, self).setUp() + self.network = network_manager.VlanManager(host=HOST) + self.network.db = db + + def test_vpn_allocate_fixed_ip(self): + self.mox.StubOutWithMock(db, 'fixed_ip_associate') + self.mox.StubOutWithMock(db, 'fixed_ip_update') + self.mox.StubOutWithMock(db, + 'virtual_interface_get_by_instance_and_network') + + db.fixed_ip_associate(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn('192.168.0.1') + db.fixed_ip_update(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + db.virtual_interface_get_by_instance_and_network(mox.IgnoreArg(), + mox.IgnoreArg(), mox.IgnoreArg()).AndReturn({'id': 0}) + self.mox.ReplayAll() + + network = dict(networks[0]) + network['vpn_private_address'] = '192.168.0.2' + self.network.allocate_fixed_ip(None, 0, network, vpn=True) + + def test_allocate_fixed_ip(self): + self.mox.StubOutWithMock(db, 'fixed_ip_associate_pool') + self.mox.StubOutWithMock(db, 'fixed_ip_update') + self.mox.StubOutWithMock(db, + 'virtual_interface_get_by_instance_and_network') + + db.fixed_ip_associate_pool(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn('192.168.0.1') + db.fixed_ip_update(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + db.virtual_interface_get_by_instance_and_network(mox.IgnoreArg(), + mox.IgnoreArg(), mox.IgnoreArg()).AndReturn({'id': 0}) + self.mox.ReplayAll() + + network = dict(networks[0]) + network['vpn_private_address'] = '192.168.0.2' + self.network.allocate_fixed_ip(None, 0, network) + + def test_create_networks_too_big(self): + self.assertRaises(ValueError, self.network.create_networks, None, + num_networks=4094, vlan_start=1) + + def test_create_networks_too_many(self): + self.assertRaises(ValueError, self.network.create_networks, None, + num_networks=100, vlan_start=1, + cidr='192.168.0.1/24', network_size=100) + + +class CommonNetworkTestCase(test.TestCase): + + class FakeNetworkManager(network_manager.NetworkManager): + """This NetworkManager doesn't call the base class so we can bypass all + inherited service cruft and just perform unit tests. + """ + + class FakeDB: + def fixed_ip_get_by_instance(self, context, instance_id): + return [dict(address='10.0.0.0'), dict(address='10.0.0.1'), + dict(address='10.0.0.2')] + + def __init__(self): + self.db = self.FakeDB() + self.deallocate_called = None + + def deallocate_fixed_ip(self, context, address): + self.deallocate_called = address + + def test_remove_fixed_ip_from_instance(self): + manager = self.FakeNetworkManager() + manager.remove_fixed_ip_from_instance(None, 99, '10.0.0.1') + + self.assertEquals(manager.deallocate_called, '10.0.0.1') + + def test_remove_fixed_ip_from_instance_bad_input(self): + manager = self.FakeNetworkManager() + self.assertRaises(exception.FixedIpNotFoundForSpecificInstance, + manager.remove_fixed_ip_from_instance, + None, 99, 'bad input') diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py index 0691231e4..69d2deafe 100644 --- a/nova/tests/test_quota.py +++ b/nova/tests/test_quota.py @@ -51,7 +51,7 @@ class QuotaTestCase(test.TestCase): self.manager = manager.AuthManager() self.user = self.manager.create_user('admin', 'admin', 'admin', True) self.project = self.manager.create_project('admin', 'admin', 'admin') - self.network = utils.import_object(FLAGS.network_manager) + self.network = self.network = self.start_service('network') self.context = context.RequestContext(project=self.project, user=self.user) @@ -69,7 +69,6 @@ class QuotaTestCase(test.TestCase): inst['project_id'] = self.project.id inst['instance_type_id'] = '3' # m1.large inst['vcpus'] = cores - inst['mac_address'] = utils.generate_mac() return db.instance_create(self.context, inst)['id'] def _create_volume(self, size=10): @@ -270,19 +269,16 @@ class QuotaTestCase(test.TestCase): for volume_id in volume_ids: db.volume_destroy(self.context, volume_id) + @test.skip_test def test_too_many_addresses(self): address = '192.168.0.100' db.floating_ip_create(context.get_admin_context(), - {'address': address, 'host': FLAGS.host}) - float_addr = self.network.allocate_floating_ip(self.context, - self.project.id) - # NOTE(vish): This assert never fails. When cloud attempts to - # make an rpc.call, the test just finishes with OK. It - # appears to be something in the magic inline callbacks - # that is breaking. + {'address': address, 'host': FLAGS.host, + 'project_id': self.project.id}) self.assertRaises(quota.QuotaError, - network.API().allocate_floating_ip, - self.context) + self.network.allocate_floating_ip, + self.context, + self.project.id) db.floating_ip_destroy(context.get_admin_context(), address) def test_too_many_metadata_items(self): diff --git a/nova/tests/test_utils.py b/nova/tests/test_utils.py index 3a3f914e4..0c359e981 100644 --- a/nova/tests/test_utils.py +++ b/nova/tests/test_utils.py @@ -276,6 +276,19 @@ class GenericUtilsTestCase(test.TestCase): result = utils.parse_server_string('www.exa:mple.com:8443') self.assertEqual(('', ''), result) + def test_bool_from_str(self): + self.assertTrue(utils.bool_from_str('1')) + self.assertTrue(utils.bool_from_str('2')) + self.assertTrue(utils.bool_from_str('-1')) + self.assertTrue(utils.bool_from_str('true')) + self.assertTrue(utils.bool_from_str('True')) + self.assertTrue(utils.bool_from_str('tRuE')) + self.assertFalse(utils.bool_from_str('False')) + self.assertFalse(utils.bool_from_str('false')) + self.assertFalse(utils.bool_from_str('0')) + self.assertFalse(utils.bool_from_str(None)) + self.assertFalse(utils.bool_from_str('junk')) + class IsUUIDLikeTestCase(test.TestCase): def assertUUIDLike(self, val, expected): diff --git a/nova/tests/test_vlan_network.py b/nova/tests/test_vlan_network.py deleted file mode 100644 index a1c8ab11c..000000000 --- a/nova/tests/test_vlan_network.py +++ /dev/null @@ -1,242 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -""" -Unit Tests for vlan network code -""" -import netaddr -import os - -from nova import context -from nova import db -from nova import exception -from nova import flags -from nova import log as logging -from nova import test -from nova import utils -from nova.auth import manager -from nova.tests.network import base -from nova.tests.network import binpath,\ - lease_ip, release_ip - -FLAGS = flags.FLAGS -LOG = logging.getLogger('nova.tests.network') - - -class VlanNetworkTestCase(base.NetworkTestCase): - """Test cases for network code""" - def test_public_network_association(self): - """Makes sure that we can allocaate a public ip""" - # TODO(vish): better way of adding floating ips - self.context._project = self.projects[0] - self.context.project_id = self.projects[0].id - pubnet = netaddr.IPNetwork(flags.FLAGS.floating_range) - address = str(list(pubnet)[0]) - try: - db.floating_ip_get_by_address(context.get_admin_context(), address) - except exception.NotFound: - db.floating_ip_create(context.get_admin_context(), - {'address': address, - 'host': FLAGS.host}) - float_addr = self.network.allocate_floating_ip(self.context, - self.projects[0].id) - fix_addr = self._create_address(0) - lease_ip(fix_addr) - self.assertEqual(float_addr, str(pubnet[0])) - self.network.associate_floating_ip(self.context, float_addr, fix_addr) - address = db.instance_get_floating_address(context.get_admin_context(), - self.instance_id) - self.assertEqual(address, float_addr) - self.network.disassociate_floating_ip(self.context, float_addr) - address = db.instance_get_floating_address(context.get_admin_context(), - self.instance_id) - self.assertEqual(address, None) - self.network.deallocate_floating_ip(self.context, float_addr) - self.network.deallocate_fixed_ip(self.context, fix_addr) - release_ip(fix_addr) - db.floating_ip_destroy(context.get_admin_context(), float_addr) - - def test_allocate_deallocate_fixed_ip(self): - """Makes sure that we can allocate and deallocate a fixed ip""" - address = self._create_address(0) - self.assertTrue(self._is_allocated_in_project(address, - self.projects[0].id)) - lease_ip(address) - self._deallocate_address(0, address) - - # Doesn't go away until it's dhcp released - self.assertTrue(self._is_allocated_in_project(address, - self.projects[0].id)) - - release_ip(address) - self.assertFalse(self._is_allocated_in_project(address, - self.projects[0].id)) - - def test_side_effects(self): - """Ensures allocating and releasing has no side effects""" - address = self._create_address(0) - address2 = self._create_address(1, self.instance2_id) - - self.assertTrue(self._is_allocated_in_project(address, - self.projects[0].id)) - self.assertTrue(self._is_allocated_in_project(address2, - self.projects[1].id)) - self.assertFalse(self._is_allocated_in_project(address, - self.projects[1].id)) - - # Addresses are allocated before they're issued - lease_ip(address) - lease_ip(address2) - - self._deallocate_address(0, address) - release_ip(address) - self.assertFalse(self._is_allocated_in_project(address, - self.projects[0].id)) - - # First address release shouldn't affect the second - self.assertTrue(self._is_allocated_in_project(address2, - self.projects[1].id)) - - self._deallocate_address(1, address2) - release_ip(address2) - self.assertFalse(self._is_allocated_in_project(address2, - self.projects[1].id)) - - def test_subnet_edge(self): - """Makes sure that private ips don't overlap""" - first = self._create_address(0) - lease_ip(first) - instance_ids = [] - for i in range(1, FLAGS.num_networks): - instance_ref = self._create_instance(i, mac=utils.generate_mac()) - instance_ids.append(instance_ref['id']) - address = self._create_address(i, instance_ref['id']) - instance_ref = self._create_instance(i, mac=utils.generate_mac()) - instance_ids.append(instance_ref['id']) - address2 = self._create_address(i, instance_ref['id']) - instance_ref = self._create_instance(i, mac=utils.generate_mac()) - instance_ids.append(instance_ref['id']) - address3 = self._create_address(i, instance_ref['id']) - lease_ip(address) - lease_ip(address2) - lease_ip(address3) - self.context._project = self.projects[i] - self.context.project_id = self.projects[i].id - self.assertFalse(self._is_allocated_in_project(address, - self.projects[0].id)) - self.assertFalse(self._is_allocated_in_project(address2, - self.projects[0].id)) - self.assertFalse(self._is_allocated_in_project(address3, - self.projects[0].id)) - self.network.deallocate_fixed_ip(self.context, address) - self.network.deallocate_fixed_ip(self.context, address2) - self.network.deallocate_fixed_ip(self.context, address3) - release_ip(address) - release_ip(address2) - release_ip(address3) - for instance_id in instance_ids: - db.instance_destroy(context.get_admin_context(), instance_id) - self.context._project = self.projects[0] - self.context.project_id = self.projects[0].id - self.network.deallocate_fixed_ip(self.context, first) - self._deallocate_address(0, first) - release_ip(first) - - def test_vpn_ip_and_port_looks_valid(self): - """Ensure the vpn ip and port are reasonable""" - self.assert_(self.projects[0].vpn_ip) - self.assert_(self.projects[0].vpn_port >= FLAGS.vpn_start) - self.assert_(self.projects[0].vpn_port <= FLAGS.vpn_start + - FLAGS.num_networks) - - def test_too_many_networks(self): - """Ensure error is raised if we run out of networks""" - projects = [] - networks_left = (FLAGS.num_networks - - db.network_count(context.get_admin_context())) - for i in range(networks_left): - project = self.manager.create_project('many%s' % i, self.user) - projects.append(project) - db.project_get_network(context.get_admin_context(), project.id) - project = self.manager.create_project('last', self.user) - projects.append(project) - self.assertRaises(db.NoMoreNetworks, - db.project_get_network, - context.get_admin_context(), - project.id) - for project in projects: - self.manager.delete_project(project) - - def test_ips_are_reused(self): - """Makes sure that ip addresses that are deallocated get reused""" - address = self._create_address(0) - lease_ip(address) - self.network.deallocate_fixed_ip(self.context, address) - release_ip(address) - - address2 = self._create_address(0) - self.assertEqual(address, address2) - lease_ip(address) - self.network.deallocate_fixed_ip(self.context, address2) - release_ip(address) - - def test_too_many_addresses(self): - """Test for a NoMoreAddresses exception when all fixed ips are used. - """ - admin_context = context.get_admin_context() - network = db.project_get_network(admin_context, self.projects[0].id) - num_available_ips = db.network_count_available_ips(admin_context, - network['id']) - addresses = [] - instance_ids = [] - for i in range(num_available_ips): - instance_ref = self._create_instance(0) - instance_ids.append(instance_ref['id']) - address = self._create_address(0, instance_ref['id']) - addresses.append(address) - lease_ip(address) - - ip_count = db.network_count_available_ips(context.get_admin_context(), - network['id']) - self.assertEqual(ip_count, 0) - self.assertRaises(db.NoMoreAddresses, - self.network.allocate_fixed_ip, - self.context, - 'foo') - - for i in range(num_available_ips): - self.network.deallocate_fixed_ip(self.context, addresses[i]) - release_ip(addresses[i]) - db.instance_destroy(context.get_admin_context(), instance_ids[i]) - ip_count = db.network_count_available_ips(context.get_admin_context(), - network['id']) - self.assertEqual(ip_count, num_available_ips) - - def _is_allocated_in_project(self, address, project_id): - """Returns true if address is in specified project""" - project_net = db.project_get_network(context.get_admin_context(), - project_id) - network = db.fixed_ip_get_network(context.get_admin_context(), - address) - instance = db.fixed_ip_get_instance(context.get_admin_context(), - address) - # instance exists until release - return instance is not None and network['id'] == project_net['id'] - - def run(self, result=None): - if(FLAGS.network_manager == 'nova.network.manager.VlanManager'): - super(VlanNetworkTestCase, self).run(result) diff --git a/nova/tests/test_vmwareapi.py b/nova/tests/test_vmwareapi.py index eddf01e9f..cbf7801cf 100644 --- a/nova/tests/test_vmwareapi.py +++ b/nova/tests/test_vmwareapi.py @@ -1,251 +1,276 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2011 Citrix Systems, Inc.
-# Copyright 2011 OpenStack LLC.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""
-Test suite for VMWareAPI.
-"""
-
-import stubout
-
-from nova import context
-from nova import db
-from nova import flags
-from nova import test
-from nova import utils
-from nova.auth import manager
-from nova.compute import power_state
-from nova.tests.glance import stubs as glance_stubs
-from nova.tests.vmwareapi import db_fakes
-from nova.tests.vmwareapi import stubs
-from nova.virt import vmwareapi_conn
-from nova.virt.vmwareapi import fake as vmwareapi_fake
-
-
-FLAGS = flags.FLAGS
-
-
-class VMWareAPIVMTestCase(test.TestCase):
- """Unit tests for Vmware API connection calls."""
-
- def setUp(self):
- super(VMWareAPIVMTestCase, self).setUp()
- self.flags(vmwareapi_host_ip='test_url',
- vmwareapi_host_username='test_username',
- vmwareapi_host_password='test_pass')
- self.manager = manager.AuthManager()
- self.user = self.manager.create_user('fake', 'fake', 'fake',
- admin=True)
- self.project = self.manager.create_project('fake', 'fake', 'fake')
- self.network = utils.import_object(FLAGS.network_manager)
- self.stubs = stubout.StubOutForTesting()
- vmwareapi_fake.reset()
- db_fakes.stub_out_db_instance_api(self.stubs)
- stubs.set_stubs(self.stubs)
- glance_stubs.stubout_glance_client(self.stubs)
- self.conn = vmwareapi_conn.get_connection(False)
-
- def _create_instance_in_the_db(self):
- values = {'name': 1,
- 'id': 1,
- 'project_id': self.project.id,
- 'user_id': self.user.id,
- 'image_ref': "1",
- 'kernel_id': "1",
- 'ramdisk_id': "1",
- 'instance_type': 'm1.large',
- 'mac_address': 'aa:bb:cc:dd:ee:ff',
- }
- self.instance = db.instance_create(None, values)
-
- def _create_vm(self):
- """Create and spawn the VM."""
- self._create_instance_in_the_db()
- self.type_data = db.instance_type_get_by_name(None, 'm1.large')
- self.conn.spawn(self.instance)
- self._check_vm_record()
-
- def _check_vm_record(self):
- """
- Check if the spawned VM's properties correspond to the instance in
- the db.
- """
- instances = self.conn.list_instances()
- self.assertEquals(len(instances), 1)
-
- # Get Nova record for VM
- vm_info = self.conn.get_info(1)
-
- # Get record for VM
- vms = vmwareapi_fake._get_objects("VirtualMachine")
- vm = vms[0]
-
- # Check that m1.large above turned into the right thing.
- mem_kib = long(self.type_data['memory_mb']) << 10
- vcpus = self.type_data['vcpus']
- self.assertEquals(vm_info['max_mem'], mem_kib)
- self.assertEquals(vm_info['mem'], mem_kib)
- self.assertEquals(vm.get("summary.config.numCpu"), vcpus)
- self.assertEquals(vm.get("summary.config.memorySizeMB"),
- self.type_data['memory_mb'])
-
- # Check that the VM is running according to Nova
- self.assertEquals(vm_info['state'], power_state.RUNNING)
-
- # Check that the VM is running according to vSphere API.
- self.assertEquals(vm.get("runtime.powerState"), 'poweredOn')
-
- def _check_vm_info(self, info, pwr_state=power_state.RUNNING):
- """
- Check if the get_info returned values correspond to the instance
- object in the db.
- """
- mem_kib = long(self.type_data['memory_mb']) << 10
- self.assertEquals(info["state"], pwr_state)
- self.assertEquals(info["max_mem"], mem_kib)
- self.assertEquals(info["mem"], mem_kib)
- self.assertEquals(info["num_cpu"], self.type_data['vcpus'])
-
- def test_list_instances(self):
- instances = self.conn.list_instances()
- self.assertEquals(len(instances), 0)
-
- def test_list_instances_1(self):
- self._create_vm()
- instances = self.conn.list_instances()
- self.assertEquals(len(instances), 1)
-
- def test_spawn(self):
- self._create_vm()
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
-
- def test_snapshot(self):
- self._create_vm()
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
- self.conn.snapshot(self.instance, "Test-Snapshot")
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
-
- def test_snapshot_non_existent(self):
- self._create_instance_in_the_db()
- self.assertRaises(Exception, self.conn.snapshot, self.instance,
- "Test-Snapshot")
-
- def test_reboot(self):
- self._create_vm()
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
- self.conn.reboot(self.instance)
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
-
- def test_reboot_non_existent(self):
- self._create_instance_in_the_db()
- self.assertRaises(Exception, self.conn.reboot, self.instance)
-
- def test_reboot_not_poweredon(self):
- self._create_vm()
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
- self.conn.suspend(self.instance, self.dummy_callback_handler)
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.PAUSED)
- self.assertRaises(Exception, self.conn.reboot, self.instance)
-
- def test_suspend(self):
- self._create_vm()
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
- self.conn.suspend(self.instance, self.dummy_callback_handler)
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.PAUSED)
-
- def test_suspend_non_existent(self):
- self._create_instance_in_the_db()
- self.assertRaises(Exception, self.conn.suspend, self.instance,
- self.dummy_callback_handler)
-
- def test_resume(self):
- self._create_vm()
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
- self.conn.suspend(self.instance, self.dummy_callback_handler)
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.PAUSED)
- self.conn.resume(self.instance, self.dummy_callback_handler)
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
-
- def test_resume_non_existent(self):
- self._create_instance_in_the_db()
- self.assertRaises(Exception, self.conn.resume, self.instance,
- self.dummy_callback_handler)
-
- def test_resume_not_suspended(self):
- self._create_vm()
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
- self.assertRaises(Exception, self.conn.resume, self.instance,
- self.dummy_callback_handler)
-
- def test_get_info(self):
- self._create_vm()
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
-
- def test_destroy(self):
- self._create_vm()
- info = self.conn.get_info(1)
- self._check_vm_info(info, power_state.RUNNING)
- instances = self.conn.list_instances()
- self.assertEquals(len(instances), 1)
- self.conn.destroy(self.instance)
- instances = self.conn.list_instances()
- self.assertEquals(len(instances), 0)
-
- def test_destroy_non_existent(self):
- self._create_instance_in_the_db()
- self.assertEquals(self.conn.destroy(self.instance), None)
-
- def test_pause(self):
- pass
-
- def test_unpause(self):
- pass
-
- def test_diagnostics(self):
- pass
-
- def test_get_console_output(self):
- pass
-
- def test_get_ajax_console(self):
- pass
-
- def dummy_callback_handler(self, ret):
- """
- Dummy callback function to be passed to suspend, resume, etc., calls.
- """
- pass
-
- def tearDown(self):
- super(VMWareAPIVMTestCase, self).tearDown()
- vmwareapi_fake.cleanup()
- self.manager.delete_project(self.project)
- self.manager.delete_user(self.user)
- self.stubs.UnsetAll()
+# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Citrix Systems, Inc. +# Copyright 2011 OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test suite for VMWareAPI. +""" + +import stubout + +from nova import context +from nova import db +from nova import flags +from nova import test +from nova import utils +from nova.auth import manager +from nova.compute import power_state +from nova.tests.glance import stubs as glance_stubs +from nova.tests.vmwareapi import db_fakes +from nova.tests.vmwareapi import stubs +from nova.virt import vmwareapi_conn +from nova.virt.vmwareapi import fake as vmwareapi_fake + + +FLAGS = flags.FLAGS + + +class VMWareAPIVMTestCase(test.TestCase): + """Unit tests for Vmware API connection calls.""" + + # NOTE(jkoelker): This is leaking stubs into the db module. + # Commenting out until updated for multi-nic. + #def setUp(self): + # super(VMWareAPIVMTestCase, self).setUp() + # self.flags(vmwareapi_host_ip='test_url', + # vmwareapi_host_username='test_username', + # vmwareapi_host_password='test_pass') + # self.manager = manager.AuthManager() + # self.user = self.manager.create_user('fake', 'fake', 'fake', + # admin=True) + # self.project = self.manager.create_project('fake', 'fake', 'fake') + # self.network = utils.import_object(FLAGS.network_manager) + # self.stubs = stubout.StubOutForTesting() + # vmwareapi_fake.reset() + # db_fakes.stub_out_db_instance_api(self.stubs) + # stubs.set_stubs(self.stubs) + # glance_stubs.stubout_glance_client(self.stubs, + # glance_stubs.FakeGlance) + # self.conn = vmwareapi_conn.get_connection(False) + + #def tearDown(self): + # super(VMWareAPIVMTestCase, self).tearDown() + # vmwareapi_fake.cleanup() + # self.manager.delete_project(self.project) + # self.manager.delete_user(self.user) + # self.stubs.UnsetAll() + + def _create_instance_in_the_db(self): + values = {'name': 1, + 'id': 1, + 'project_id': self.project.id, + 'user_id': self.user.id, + 'image_id': "1", + 'kernel_id': "1", + 'ramdisk_id': "1", + 'instance_type': 'm1.large', + 'mac_address': 'aa:bb:cc:dd:ee:ff', + } + self.instance = db.instance_create(values) + + def _create_vm(self): + """Create and spawn the VM.""" + self._create_instance_in_the_db() + self.type_data = db.instance_type_get_by_name(None, 'm1.large') + self.conn.spawn(self.instance) + self._check_vm_record() + + def _check_vm_record(self): + """ + Check if the spawned VM's properties correspond to the instance in + the db. + """ + instances = self.conn.list_instances() + self.assertEquals(len(instances), 1) + + # Get Nova record for VM + vm_info = self.conn.get_info(1) + + # Get record for VM + vms = vmwareapi_fake._get_objects("VirtualMachine") + vm = vms[0] + + # Check that m1.large above turned into the right thing. + mem_kib = long(self.type_data['memory_mb']) << 10 + vcpus = self.type_data['vcpus'] + self.assertEquals(vm_info['max_mem'], mem_kib) + self.assertEquals(vm_info['mem'], mem_kib) + self.assertEquals(vm.get("summary.config.numCpu"), vcpus) + self.assertEquals(vm.get("summary.config.memorySizeMB"), + self.type_data['memory_mb']) + + # Check that the VM is running according to Nova + self.assertEquals(vm_info['state'], power_state.RUNNING) + + # Check that the VM is running according to vSphere API. + self.assertEquals(vm.get("runtime.powerState"), 'poweredOn') + + def _check_vm_info(self, info, pwr_state=power_state.RUNNING): + """ + Check if the get_info returned values correspond to the instance + object in the db. + """ + mem_kib = long(self.type_data['memory_mb']) << 10 + self.assertEquals(info["state"], pwr_state) + self.assertEquals(info["max_mem"], mem_kib) + self.assertEquals(info["mem"], mem_kib) + self.assertEquals(info["num_cpu"], self.type_data['vcpus']) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_list_instances(self): + instances = self.conn.list_instances() + self.assertEquals(len(instances), 0) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_list_instances_1(self): + self._create_vm() + instances = self.conn.list_instances() + self.assertEquals(len(instances), 1) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_spawn(self): + self._create_vm() + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_snapshot(self): + self._create_vm() + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + self.conn.snapshot(self.instance, "Test-Snapshot") + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_snapshot_non_existent(self): + self._create_instance_in_the_db() + self.assertRaises(Exception, self.conn.snapshot, self.instance, + "Test-Snapshot") + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_reboot(self): + self._create_vm() + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + self.conn.reboot(self.instance) + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_reboot_non_existent(self): + self._create_instance_in_the_db() + self.assertRaises(Exception, self.conn.reboot, self.instance) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_reboot_not_poweredon(self): + self._create_vm() + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + self.conn.suspend(self.instance, self.dummy_callback_handler) + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.PAUSED) + self.assertRaises(Exception, self.conn.reboot, self.instance) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_suspend(self): + self._create_vm() + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + self.conn.suspend(self.instance, self.dummy_callback_handler) + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.PAUSED) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_suspend_non_existent(self): + self._create_instance_in_the_db() + self.assertRaises(Exception, self.conn.suspend, self.instance, + self.dummy_callback_handler) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_resume(self): + self._create_vm() + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + self.conn.suspend(self.instance, self.dummy_callback_handler) + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.PAUSED) + self.conn.resume(self.instance, self.dummy_callback_handler) + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_resume_non_existent(self): + self._create_instance_in_the_db() + self.assertRaises(Exception, self.conn.resume, self.instance, + self.dummy_callback_handler) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_resume_not_suspended(self): + self._create_vm() + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + self.assertRaises(Exception, self.conn.resume, self.instance, + self.dummy_callback_handler) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_get_info(self): + self._create_vm() + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_destroy(self): + self._create_vm() + info = self.conn.get_info(1) + self._check_vm_info(info, power_state.RUNNING) + instances = self.conn.list_instances() + self.assertEquals(len(instances), 1) + self.conn.destroy(self.instance) + instances = self.conn.list_instances() + self.assertEquals(len(instances), 0) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_destroy_non_existent(self): + self._create_instance_in_the_db() + self.assertEquals(self.conn.destroy(self.instance), None) + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_pause(self): + pass + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_unpause(self): + pass + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_diagnostics(self): + pass + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_get_console_output(self): + pass + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def test_get_ajax_console(self): + pass + + @test.skip_test("DB stubbing not removed, needs updating for multi-nic") + def dummy_callback_handler(self, ret): + """ + Dummy callback function to be passed to suspend, resume, etc., calls. + """ + pass diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py index 4f10ee6af..c0f89601f 100644 --- a/nova/tests/test_volume.py +++ b/nova/tests/test_volume.py @@ -27,8 +27,10 @@ from nova import exception from nova import db from nova import flags from nova import log as logging +from nova import rpc from nova import test from nova import utils +from nova import volume FLAGS = flags.FLAGS LOG = logging.getLogger('nova.tests.volume') @@ -43,6 +45,11 @@ class VolumeTestCase(test.TestCase): self.flags(connection_type='fake') self.volume = utils.import_object(FLAGS.volume_manager) self.context = context.get_admin_context() + self.instance_id = db.instance_create(self.context, {})['id'] + + def tearDown(self): + db.instance_destroy(self.context, self.instance_id) + super(VolumeTestCase, self).tearDown() @staticmethod def _create_volume(size='0', snapshot_id=None): @@ -127,7 +134,6 @@ class VolumeTestCase(test.TestCase): inst['user_id'] = 'fake' inst['project_id'] = 'fake' inst['instance_type_id'] = '2' # m1.tiny - inst['mac_address'] = utils.generate_mac() inst['ami_launch_index'] = 0 instance_id = db.instance_create(self.context, inst)['id'] mountpoint = "/dev/sdf" @@ -224,6 +230,30 @@ class VolumeTestCase(test.TestCase): snapshot_id) self.volume.delete_volume(self.context, volume_id) + def test_create_snapshot_force(self): + """Test snapshot in use can be created forcibly.""" + + def fake_cast(ctxt, topic, msg): + pass + self.stubs.Set(rpc, 'cast', fake_cast) + + volume_id = self._create_volume() + self.volume.create_volume(self.context, volume_id) + db.volume_attached(self.context, volume_id, self.instance_id, + '/dev/sda1') + + volume_api = volume.api.API() + self.assertRaises(exception.ApiError, + volume_api.create_snapshot, + self.context, volume_id, + 'fake_name', 'fake_description') + snapshot_ref = volume_api.create_snapshot_force(self.context, + volume_id, + 'fake_name', + 'fake_description') + db.snapshot_destroy(self.context, snapshot_ref['id']) + db.volume_destroy(self.context, volume_id) + class DriverTestCase(test.TestCase): """Base Test class for Drivers.""" diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index d9a514745..199a8bc52 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -83,7 +83,6 @@ class XenAPIVolumeTestCase(test.TestCase): 'kernel_id': 2, 'ramdisk_id': 3, 'instance_type_id': '3', # m1.large - 'mac_address': 'aa:bb:cc:dd:ee:ff', 'os_type': 'linux', 'architecture': 'x86-64'} @@ -211,11 +210,24 @@ class XenAPIVMTestCase(test.TestCase): 'kernel_id': 2, 'ramdisk_id': 3, 'instance_type_id': '3', # m1.large - 'mac_address': 'aa:bb:cc:dd:ee:ff', 'os_type': 'linux', 'architecture': 'x86-64'} + network_info = [({'bridge': 'fa0', 'id': 0, 'injected': False}, + {'broadcast': '192.168.0.255', + 'dns': ['192.168.0.1'], + 'gateway': '192.168.0.1', + 'gateway6': 'dead:beef::1', + 'ip6s': [{'enabled': '1', + 'ip': 'dead:beef::dcad:beff:feef:0', + 'netmask': '64'}], + 'ips': [{'enabled': '1', + 'ip': '192.168.0.100', + 'netmask': '255.255.255.0'}], + 'label': 'fake', + 'mac': 'DE:AD:BE:EF:00:00', + 'rxtx_cap': 3})] instance = db.instance_create(self.context, values) - self.conn.spawn(instance) + self.conn.spawn(instance, network_info) gt1 = eventlet.spawn(_do_build, 1, self.project.id, self.user.id) gt2 = eventlet.spawn(_do_build, 2, self.project.id, self.user.id) @@ -320,22 +332,22 @@ class XenAPIVMTestCase(test.TestCase): if check_injection: xenstore_data = self.vm['xenstore_data'] - key = 'vm-data/networking/aabbccddeeff' + key = 'vm-data/networking/DEADBEEF0000' xenstore_value = xenstore_data[key] tcpip_data = ast.literal_eval(xenstore_value) self.assertEquals(tcpip_data, - {'label': 'fake_flat_network', - 'broadcast': '10.0.0.255', - 'ips': [{'ip': '10.0.0.3', - 'netmask':'255.255.255.0', - 'enabled':'1'}], - 'ip6s': [{'ip': 'fe80::a8bb:ccff:fedd:eeff', - 'netmask': '120', - 'enabled': '1'}], - 'mac': 'aa:bb:cc:dd:ee:ff', - 'dns': ['10.0.0.2'], - 'gateway': '10.0.0.1', - 'gateway6': 'fe80::a00:1'}) + {'broadcast': '192.168.0.255', + 'dns': ['192.168.0.1'], + 'gateway': '192.168.0.1', + 'gateway6': 'dead:beef::1', + 'ip6s': [{'enabled': '1', + 'ip': 'dead:beef::dcad:beff:feef:0', + 'netmask': '64'}], + 'ips': [{'enabled': '1', + 'ip': '192.168.0.100', + 'netmask': '255.255.255.0'}], + 'label': 'fake', + 'mac': 'DE:AD:BE:EF:00:00'}) def check_vm_params_for_windows(self): self.assertEquals(self.vm['platform']['nx'], 'true') @@ -369,6 +381,18 @@ class XenAPIVMTestCase(test.TestCase): self.assertEquals(self.vm['HVM_boot_params'], {}) self.assertEquals(self.vm['HVM_boot_policy'], '') + def _list_vdis(self): + url = FLAGS.xenapi_connection_url + username = FLAGS.xenapi_connection_username + password = FLAGS.xenapi_connection_password + session = xenapi_conn.XenAPISession(url, username, password) + return session.call_xenapi('VDI.get_all') + + def _check_vdis(self, start_list, end_list): + for vdi_ref in end_list: + if not vdi_ref in start_list: + self.fail('Found unexpected VDI:%s' % vdi_ref) + def _test_spawn(self, image_ref, kernel_id, ramdisk_id, instance_type_id="3", os_type="linux", architecture="x86-64", instance_id=1, @@ -381,11 +405,24 @@ class XenAPIVMTestCase(test.TestCase): 'kernel_id': kernel_id, 'ramdisk_id': ramdisk_id, 'instance_type_id': instance_type_id, - 'mac_address': 'aa:bb:cc:dd:ee:ff', 'os_type': os_type, 'architecture': architecture} instance = db.instance_create(self.context, values) - self.conn.spawn(instance) + network_info = [({'bridge': 'fa0', 'id': 0, 'injected': True}, + {'broadcast': '192.168.0.255', + 'dns': ['192.168.0.1'], + 'gateway': '192.168.0.1', + 'gateway6': 'dead:beef::1', + 'ip6s': [{'enabled': '1', + 'ip': 'dead:beef::dcad:beff:feef:0', + 'netmask': '64'}], + 'ips': [{'enabled': '1', + 'ip': '192.168.0.100', + 'netmask': '255.255.255.0'}], + 'label': 'fake', + 'mac': 'DE:AD:BE:EF:00:00', + 'rxtx_cap': 3})] + self.conn.spawn(instance, network_info) self.create_vm_record(self.conn, os_type, instance_id) self.check_vm_record(self.conn, check_injection) self.assertTrue(instance.os_type) @@ -397,6 +434,36 @@ class XenAPIVMTestCase(test.TestCase): self._test_spawn, 1, 2, 3, "4") # m1.xlarge + def test_spawn_fail_cleanup_1(self): + """Simulates an error while downloading an image. + + Verifies that VDIs created are properly cleaned up. + + """ + vdi_recs_start = self._list_vdis() + FLAGS.xenapi_image_service = 'glance' + stubs.stubout_fetch_image_glance_disk(self.stubs) + self.assertRaises(xenapi_fake.Failure, + self._test_spawn, 1, 2, 3) + # No additional VDI should be found. + vdi_recs_end = self._list_vdis() + self._check_vdis(vdi_recs_start, vdi_recs_end) + + def test_spawn_fail_cleanup_2(self): + """Simulates an error while creating VM record. + + It verifies that VDIs created are properly cleaned up. + + """ + vdi_recs_start = self._list_vdis() + FLAGS.xenapi_image_service = 'glance' + stubs.stubout_create_vm(self.stubs) + self.assertRaises(xenapi_fake.Failure, + self._test_spawn, 1, 2, 3) + # No additional VDI should be found. + vdi_recs_end = self._list_vdis() + self._check_vdis(vdi_recs_start, vdi_recs_end) + def test_spawn_raw_objectstore(self): FLAGS.xenapi_image_service = 'objectstore' self._test_spawn(1, None, None) @@ -467,11 +534,11 @@ class XenAPIVMTestCase(test.TestCase): index = config.index('auto eth0') self.assertEquals(config[index + 1:index + 8], [ 'iface eth0 inet static', - 'address 10.0.0.3', + 'address 192.168.0.100', 'netmask 255.255.255.0', - 'broadcast 10.0.0.255', - 'gateway 10.0.0.1', - 'dns-nameservers 10.0.0.2', + 'broadcast 192.168.0.255', + 'gateway 192.168.0.1', + 'dns-nameservers 192.168.0.1', '']) self._tee_executed = True return '', '' @@ -532,23 +599,37 @@ class XenAPIVMTestCase(test.TestCase): # guest agent is detected self.assertFalse(self._tee_executed) + @test.skip_test("Never gets an address, not sure why") def test_spawn_vlanmanager(self): self.flags(xenapi_image_service='glance', network_manager='nova.network.manager.VlanManager', network_driver='nova.network.xenapi_net', vlan_interface='fake0') + + def dummy(*args, **kwargs): + pass + + self.stubs.Set(VMOps, 'create_vifs', dummy) # Reset network table xenapi_fake.reset_table('network') # Instance id = 2 will use vlan network (see db/fakes.py) - fake_instance_id = 2 + ctxt = self.context.elevated() + instance_ref = self._create_instance(2) network_bk = self.network # Ensure we use xenapi_net driver self.network = utils.import_object(FLAGS.network_manager) - self.network.setup_compute_network(None, fake_instance_id) + networks = self.network.db.network_get_all(ctxt) + for network in networks: + self.network.set_network_host(ctxt, network['id']) + + self.network.allocate_for_instance(ctxt, instance_id=instance_ref.id, + instance_type_id=1, project_id=self.project.id) + self.network.setup_compute_network(ctxt, instance_ref.id) self._test_spawn(glance_stubs.FakeGlance.IMAGE_MACHINE, glance_stubs.FakeGlance.IMAGE_KERNEL, glance_stubs.FakeGlance.IMAGE_RAMDISK, - instance_id=fake_instance_id) + instance_id=instance_ref.id, + create_record=False) # TODO(salvatore-orlando): a complete test here would require # a check for making sure the bridge for the VM's VIF is # consistent with bridge specified in nova db @@ -560,13 +641,13 @@ class XenAPIVMTestCase(test.TestCase): vif_rec = xenapi_fake.get_record('VIF', vif_ref) self.assertEquals(vif_rec['qos_algorithm_type'], 'ratelimit') self.assertEquals(vif_rec['qos_algorithm_params']['kbps'], - str(4 * 1024)) + str(3 * 1024)) def test_rescue(self): self.flags(xenapi_inject_image=False) instance = self._create_instance() conn = xenapi_conn.get_connection(False) - conn.rescue(instance, None) + conn.rescue(instance, None, []) def test_unrescue(self): instance = self._create_instance() @@ -582,22 +663,35 @@ class XenAPIVMTestCase(test.TestCase): self.vm = None self.stubs.UnsetAll() - def _create_instance(self): + def _create_instance(self, instance_id=1): """Creates and spawns a test instance.""" stubs.stubout_loopingcall_start(self.stubs) values = { - 'id': 1, + 'id': instance_id, 'project_id': self.project.id, 'user_id': self.user.id, 'image_ref': 1, 'kernel_id': 2, 'ramdisk_id': 3, 'instance_type_id': '3', # m1.large - 'mac_address': 'aa:bb:cc:dd:ee:ff', 'os_type': 'linux', 'architecture': 'x86-64'} instance = db.instance_create(self.context, values) - self.conn.spawn(instance) + network_info = [({'bridge': 'fa0', 'id': 0, 'injected': False}, + {'broadcast': '192.168.0.255', + 'dns': ['192.168.0.1'], + 'gateway': '192.168.0.1', + 'gateway6': 'dead:beef::1', + 'ip6s': [{'enabled': '1', + 'ip': 'dead:beef::dcad:beff:feef:0', + 'netmask': '64'}], + 'ips': [{'enabled': '1', + 'ip': '192.168.0.100', + 'netmask': '255.255.255.0'}], + 'label': 'fake', + 'mac': 'DE:AD:BE:EF:00:00', + 'rxtx_cap': 3})] + self.conn.spawn(instance, network_info) return instance @@ -669,7 +763,6 @@ class XenAPIMigrateInstance(test.TestCase): 'ramdisk_id': None, 'local_gb': 5, 'instance_type_id': '3', # m1.large - 'mac_address': 'aa:bb:cc:dd:ee:ff', 'os_type': 'linux', 'architecture': 'x86-64'} @@ -695,7 +788,22 @@ class XenAPIMigrateInstance(test.TestCase): stubs.stubout_session(self.stubs, stubs.FakeSessionForMigrationTests) stubs.stubout_loopingcall_start(self.stubs) conn = xenapi_conn.get_connection(False) - conn.finish_resize(instance, dict(base_copy='hurr', cow='durr')) + network_info = [({'bridge': 'fa0', 'id': 0, 'injected': False}, + {'broadcast': '192.168.0.255', + 'dns': ['192.168.0.1'], + 'gateway': '192.168.0.1', + 'gateway6': 'dead:beef::1', + 'ip6s': [{'enabled': '1', + 'ip': 'dead:beef::dcad:beff:feef:0', + 'netmask': '64'}], + 'ips': [{'enabled': '1', + 'ip': '192.168.0.100', + 'netmask': '255.255.255.0'}], + 'label': 'fake', + 'mac': 'DE:AD:BE:EF:00:00', + 'rxtx_cap': 3})] + conn.finish_resize(instance, dict(base_copy='hurr', cow='durr'), + network_info) class XenAPIDetermineDiskImageTestCase(test.TestCase): diff --git a/nova/tests/test_zones.py b/nova/tests/test_zones.py index e132809dc..a943fee27 100644 --- a/nova/tests/test_zones.py +++ b/nova/tests/test_zones.py @@ -198,3 +198,178 @@ class ZoneManagerTestCase(test.TestCase): self.assertEquals(zone_state.attempt, 3) self.assertFalse(zone_state.is_active) self.assertEquals(zone_state.name, None) + + def test_host_service_caps_stale_no_stale_service(self): + zm = zone_manager.ZoneManager() + + # services just updated capabilities + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4)) + self.assertFalse(zm.host_service_caps_stale("host1", "svc1")) + self.assertFalse(zm.host_service_caps_stale("host1", "svc2")) + + def test_host_service_caps_stale_all_stale_services(self): + zm = zone_manager.ZoneManager() + expiry_time = (FLAGS.periodic_interval * 3) + 1 + + # Both services became stale + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4)) + time_future = utils.utcnow() + datetime.timedelta(seconds=expiry_time) + utils.set_time_override(time_future) + self.assertTrue(zm.host_service_caps_stale("host1", "svc1")) + self.assertTrue(zm.host_service_caps_stale("host1", "svc2")) + utils.clear_time_override() + + def test_host_service_caps_stale_one_stale_service(self): + zm = zone_manager.ZoneManager() + expiry_time = (FLAGS.periodic_interval * 3) + 1 + + # One service became stale + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4)) + caps = zm.service_states["host1"]["svc1"] + caps["timestamp"] = utils.utcnow() - \ + datetime.timedelta(seconds=expiry_time) + self.assertTrue(zm.host_service_caps_stale("host1", "svc1")) + self.assertFalse(zm.host_service_caps_stale("host1", "svc2")) + + def test_delete_expired_host_services_del_one_service(self): + zm = zone_manager.ZoneManager() + + # Delete one service in a host + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4)) + stale_host_services = {"host1": ["svc1"]} + zm.delete_expired_host_services(stale_host_services) + self.assertFalse("svc1" in zm.service_states["host1"]) + self.assertTrue("svc2" in zm.service_states["host1"]) + + def test_delete_expired_host_services_del_all_hosts(self): + zm = zone_manager.ZoneManager() + + # Delete all services in a host + zm.update_service_capabilities("svc2", "host1", dict(a=3, b=4)) + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + stale_host_services = {"host1": ["svc1", "svc2"]} + zm.delete_expired_host_services(stale_host_services) + self.assertFalse("host1" in zm.service_states) + + def test_delete_expired_host_services_del_one_service_per_host(self): + zm = zone_manager.ZoneManager() + + # Delete one service per host + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4)) + stale_host_services = {"host1": ["svc1"], "host2": ["svc1"]} + zm.delete_expired_host_services(stale_host_services) + self.assertFalse("host1" in zm.service_states) + self.assertFalse("host2" in zm.service_states) + + def test_get_zone_capabilities_one_host(self): + zm = zone_manager.ZoneManager() + + # Service capabilities recent + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + caps = zm.get_zone_capabilities(None) + self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2))) + + def test_get_zone_capabilities_expired_host(self): + zm = zone_manager.ZoneManager() + expiry_time = (FLAGS.periodic_interval * 3) + 1 + + # Service capabilities stale + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + time_future = utils.utcnow() + datetime.timedelta(seconds=expiry_time) + utils.set_time_override(time_future) + caps = zm.get_zone_capabilities(None) + self.assertEquals(caps, {}) + utils.clear_time_override() + + def test_get_zone_capabilities_multiple_hosts(self): + zm = zone_manager.ZoneManager() + + # Both host service capabilities recent + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4)) + caps = zm.get_zone_capabilities(None) + self.assertEquals(caps, dict(svc1_a=(1, 3), svc1_b=(2, 4))) + + def test_get_zone_capabilities_one_stale_host(self): + zm = zone_manager.ZoneManager() + expiry_time = (FLAGS.periodic_interval * 3) + 1 + + # One host service capabilities become stale + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4)) + serv_caps = zm.service_states["host1"]["svc1"] + serv_caps["timestamp"] = utils.utcnow() - \ + datetime.timedelta(seconds=expiry_time) + caps = zm.get_zone_capabilities(None) + self.assertEquals(caps, dict(svc1_a=(3, 3), svc1_b=(4, 4))) + + def test_get_zone_capabilities_multiple_service_per_host(self): + zm = zone_manager.ZoneManager() + + # Multiple services per host + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4)) + zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6)) + zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8)) + caps = zm.get_zone_capabilities(None) + self.assertEquals(caps, dict(svc1_a=(1, 3), svc1_b=(2, 4), + svc2_a=(5, 7), svc2_b=(6, 8))) + + def test_get_zone_capabilities_one_stale_service_per_host(self): + zm = zone_manager.ZoneManager() + expiry_time = (FLAGS.periodic_interval * 3) + 1 + + # Two host services among four become stale + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4)) + zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6)) + zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8)) + serv_caps_1 = zm.service_states["host1"]["svc2"] + serv_caps_1["timestamp"] = utils.utcnow() - \ + datetime.timedelta(seconds=expiry_time) + serv_caps_2 = zm.service_states["host2"]["svc1"] + serv_caps_2["timestamp"] = utils.utcnow() - \ + datetime.timedelta(seconds=expiry_time) + caps = zm.get_zone_capabilities(None) + self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2), + svc2_a=(7, 7), svc2_b=(8, 8))) + + def test_get_zone_capabilities_three_stale_host_services(self): + zm = zone_manager.ZoneManager() + expiry_time = (FLAGS.periodic_interval * 3) + 1 + + # Three host services among four become stale + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4)) + zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6)) + zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8)) + serv_caps_1 = zm.service_states["host1"]["svc2"] + serv_caps_1["timestamp"] = utils.utcnow() - \ + datetime.timedelta(seconds=expiry_time) + serv_caps_2 = zm.service_states["host2"]["svc1"] + serv_caps_2["timestamp"] = utils.utcnow() - \ + datetime.timedelta(seconds=expiry_time) + serv_caps_3 = zm.service_states["host2"]["svc2"] + serv_caps_3["timestamp"] = utils.utcnow() - \ + datetime.timedelta(seconds=expiry_time) + caps = zm.get_zone_capabilities(None) + self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2))) + + def test_get_zone_capabilities_all_stale_host_services(self): + zm = zone_manager.ZoneManager() + expiry_time = (FLAGS.periodic_interval * 3) + 1 + + # All the host services become stale + zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2)) + zm.update_service_capabilities("svc1", "host2", dict(a=3, b=4)) + zm.update_service_capabilities("svc2", "host1", dict(a=5, b=6)) + zm.update_service_capabilities("svc2", "host2", dict(a=7, b=8)) + time_future = utils.utcnow() + datetime.timedelta(seconds=expiry_time) + utils.set_time_override(time_future) + caps = zm.get_zone_capabilities(None) + self.assertEquals(caps, {}) diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py index 151a3e909..66c79d465 100644 --- a/nova/tests/xenapi/stubs.py +++ b/nova/tests/xenapi/stubs.py @@ -98,6 +98,42 @@ def stubout_is_vdi_pv(stubs): stubs.Set(vm_utils, '_is_vdi_pv', f) +def stubout_determine_is_pv_objectstore(stubs): + """Assumes VMs never have PV kernels""" + + @classmethod + def f(cls, *args): + return False + stubs.Set(vm_utils.VMHelper, '_determine_is_pv_objectstore', f) + + +def stubout_lookup_image(stubs): + """Simulates a failure in lookup image.""" + def f(_1, _2, _3, _4): + raise Exception("Test Exception raised by fake lookup_image") + stubs.Set(vm_utils, 'lookup_image', f) + + +def stubout_fetch_image_glance_disk(stubs): + """Simulates a failure in fetch image_glance_disk.""" + + @classmethod + def f(cls, *args): + raise fake.Failure("Test Exception raised by " + + "fake fetch_image_glance_disk") + stubs.Set(vm_utils.VMHelper, '_fetch_image_glance_disk', f) + + +def stubout_create_vm(stubs): + """Simulates a failure in create_vm.""" + + @classmethod + def f(cls, *args): + raise fake.Failure("Test Exception raised by " + + "fake create_vm") + stubs.Set(vm_utils.VMHelper, 'create_vm', f) + + def stubout_loopingcall_start(stubs): def fake_start(self, interval, now=True): self.f(*self.args, **self.kw) @@ -120,6 +156,9 @@ class FakeSessionForVMTests(fake.SessionBase): super(FakeSessionForVMTests, self).__init__(uri) def host_call_plugin(self, _1, _2, plugin, method, _5): + # If the call is for 'copy_kernel_vdi' return None. + if method == 'copy_kernel_vdi': + return sr_ref = fake.get_all('SR')[0] vdi_ref = fake.create_vdi('', False, sr_ref, False) vdi_rec = fake.get_record('VDI', vdi_ref) diff --git a/nova/utils.py b/nova/utils.py index 918541224..8784a227d 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -262,14 +262,6 @@ def generate_uid(topic, size=8): return '%s-%s' % (topic, ''.join(choices)) -def generate_mac(): - mac = [0x02, 0x16, 0x3e, - random.randint(0x00, 0x7f), - random.randint(0x00, 0xff), - random.randint(0x00, 0xff)] - return ':'.join(map(lambda x: '%02x' % x, mac)) - - # Default symbols to use for passwords. Avoids visually confusing characters. # ~6 bits per symbol DEFAULT_PASSWORD_SYMBOLS = ('23456789' # Removed: 0,1 @@ -282,6 +274,22 @@ EASIER_PASSWORD_SYMBOLS = ('23456789' # Removed: 0, 1 'ABCDEFGHJKLMNPQRSTUVWXYZ') # Removed: I, O +def usage_from_instance(instance_ref, **kw): + usage_info = dict( + tenant_id=instance_ref['project_id'], + user_id=instance_ref['user_id'], + instance_id=instance_ref['id'], + instance_type=instance_ref['instance_type']['name'], + instance_type_id=instance_ref['instance_type_id'], + display_name=instance_ref['display_name'], + created_at=str(instance_ref['created_at']), + launched_at=str(instance_ref['launched_at']) \ + if instance_ref['launched_at'] else '', + image_ref=instance_ref['image_ref']) + usage_info.update(kw) + return usage_info + + def generate_password(length=20, symbols=DEFAULT_PASSWORD_SYMBOLS): """Generate a random password from the supplied symbols. @@ -756,6 +764,17 @@ def is_uuid_like(val): return (len(val) == 36) and (val.count('-') == 4) +def bool_from_str(val): + """Convert a string representation of a bool into a bool value""" + + if not val: + return False + try: + return True if int(val) else False + except ValueError: + return val.lower() == 'true' + + class Bootstrapper(object): """Provides environment bootstrapping capabilities for entry points.""" diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 2c7c0cfcc..34dc5f544 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -61,11 +61,11 @@ class ComputeDriver(object): """Return a list of InstanceInfo for all registered VMs""" raise NotImplementedError() - def spawn(self, instance, network_info=None, block_device_mapping=None): + def spawn(self, instance, network_info, block_device_mapping=None): """Launch a VM for the specified instance""" raise NotImplementedError() - def destroy(self, instance, cleanup=True): + def destroy(self, instance, network_info, cleanup=True): """Destroy (shutdown and delete) the specified instance. The given parameter is an instance of nova.compute.service.Instance, @@ -81,7 +81,7 @@ class ComputeDriver(object): """ raise NotImplementedError() - def reboot(self, instance): + def reboot(self, instance, network_info): """Reboot specified VM""" raise NotImplementedError() @@ -146,11 +146,11 @@ class ComputeDriver(object): """resume the specified instance""" raise NotImplementedError() - def rescue(self, instance, callback): + def rescue(self, instance, callback, network_info): """Rescue the specified instance""" raise NotImplementedError() - def unrescue(self, instance, callback): + def unrescue(self, instance, callback, network_info): """Unrescue the specified instance""" raise NotImplementedError() @@ -197,7 +197,7 @@ class ComputeDriver(object): def reset_network(self, instance): """reset networking for specified instance""" - raise NotImplementedError() + pass def ensure_filtering_rules_for_instance(self, instance_ref): """Setting up filtering rules and waiting for its completion. @@ -224,7 +224,7 @@ class ComputeDriver(object): """ raise NotImplementedError() - def unfilter_instance(self, instance): + def unfilter_instance(self, instance, network_info): """Stop filtering instance""" raise NotImplementedError() @@ -242,10 +242,18 @@ class ComputeDriver(object): """Update agent on the VM instance.""" raise NotImplementedError() - def inject_network_info(self, instance): + def inject_network_info(self, instance, nw_info): """inject network info for specified instance""" - raise NotImplementedError() + pass def poll_rescued_instances(self, timeout): """Poll for rescued instances""" raise NotImplementedError() + + def set_host_enabled(self, host, enabled): + """Sets the specified host's ability to accept new instances.""" + raise NotImplementedError() + + def plug_vifs(self, instance, network_info): + """Plugs in VIFs to networks.""" + raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index f78c29bd0..26bc421c0 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -129,7 +129,7 @@ class FakeConnection(driver.ComputeDriver): info_list.append(self._map_to_instance_info(instance)) return info_list - def spawn(self, instance, network_info=None, block_device_mapping=None): + def spawn(self, instance, network_info, block_device_mapping=None): """ Create a new instance/VM/domain on the virtualization platform. @@ -167,7 +167,7 @@ class FakeConnection(driver.ComputeDriver): """ pass - def reboot(self, instance): + def reboot(self, instance, network_info): """ Reboot the specified instance. @@ -240,13 +240,13 @@ class FakeConnection(driver.ComputeDriver): """ pass - def rescue(self, instance): + def rescue(self, instance, callback, network_info): """ Rescue the specified instance. """ pass - def unrescue(self, instance): + def unrescue(self, instance, callback, network_info): """ Unrescue the specified instance. """ @@ -293,7 +293,7 @@ class FakeConnection(driver.ComputeDriver): """ pass - def destroy(self, instance): + def destroy(self, instance, network_info): key = instance.name if key in self.instances: del self.instances[key] @@ -499,7 +499,7 @@ class FakeConnection(driver.ComputeDriver): """This method is supported only by libvirt.""" return - def unfilter_instance(self, instance_ref): + def unfilter_instance(self, instance_ref, network_info=None): """This method is supported only by libvirt.""" raise NotImplementedError('This method is supported only by libvirt.') @@ -514,3 +514,7 @@ class FakeConnection(driver.ComputeDriver): def get_host_stats(self, refresh=False): """Return fake Host Status of ram, disk, network.""" return self.host_status + + def set_host_enabled(self, host, enabled): + """Sets the specified host's ability to accept new instances.""" + pass diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index 772e7eb59..81c7dea58 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -139,7 +139,7 @@ class HyperVConnection(driver.ComputeDriver): return instance_infos - def spawn(self, instance, network_info=None, block_device_mapping=None): + def spawn(self, instance, network_info, block_device_mapping=None): """ Create a new VM and start it.""" vm = self._lookup(instance.name) if vm is not None: @@ -157,7 +157,12 @@ class HyperVConnection(driver.ComputeDriver): self._create_vm(instance) self._create_disk(instance['name'], vhdfile) - self._create_nic(instance['name'], instance['mac_address']) + + mac_address = None + if instance['mac_addresses']: + mac_address = instance['mac_addresses'][0]['address'] + + self._create_nic(instance['name'], mac_address) LOG.debug(_('Starting VM %s '), instance.name) self._set_vm_state(instance['name'], 'Enabled') @@ -363,14 +368,14 @@ class HyperVConnection(driver.ComputeDriver): wmi_obj.Properties_.Item(prop).Value return newinst - def reboot(self, instance): + def reboot(self, instance, network_info): """Reboot the specified instance.""" vm = self._lookup(instance.name) if vm is None: raise exception.InstanceNotFound(instance_id=instance.id) self._set_vm_state(instance.name, 'Reboot') - def destroy(self, instance): + def destroy(self, instance, network_info): """Destroy the VM. Also destroy the associated VHD disk files""" LOG.debug(_("Got request to destroy vm %s"), instance.name) vm = self._lookup(instance.name) @@ -494,3 +499,7 @@ class HyperVConnection(driver.ComputeDriver): def get_host_stats(self, refresh=False): """See xenapi_conn.py implementation.""" pass + + def set_host_enabled(self, host, enabled): + """Sets the specified host's ability to accept new instances.""" + pass diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index e1a683da8..a75636390 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -82,9 +82,13 @@ </disk> #end if #for $vol in $volumes - <disk type='block'> + <disk type='${vol.type}'> <driver type='raw'/> + #if $vol.type == 'network' + <source protocol='${vol.protocol}' name='${vol.name}'/> + #else <source dev='${vol.device_path}'/> + #end if <target dev='${vol.mount_device}' bus='${disk_bus}'/> </disk> #end for @@ -92,6 +96,22 @@ #end if #for $nic in $nics + #if $vif_type == 'ethernet' + <interface type='ethernet'> + <target dev='${nic.name}' /> + <mac address='${nic.mac_address}' /> + <script path='${nic.script}' /> + </interface> + #else if $vif_type == '802.1Qbh' + <interface type='direct'> + <mac address='${nic.mac_address}'/> + <source dev='${nic.device_name}' mode='private'/> + <virtualport type='802.1Qbh'> + <parameters profileid='${nic.profile_name}'/> + </virtualport> + <model type='virtio'/> + </interface> + #else <interface type='bridge'> <source bridge='${nic.bridge_name}'/> <mac address='${nic.mac_address}'/> @@ -107,6 +127,8 @@ #end if </filterref> </interface> + #end if + #end for <!-- The order is significant here. File must be defined first --> <serial type="file"> diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index fadf77629..96f9c41f9 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -123,6 +123,11 @@ flags.DEFINE_string('qemu_img', 'qemu-img', 'binary to use for qemu-img commands') flags.DEFINE_bool('start_guests_on_host_boot', False, 'Whether to restart guests when the host reboots') +flags.DEFINE_string('libvirt_vif_type', 'bridge', + 'Type of VIF to create.') +flags.DEFINE_string('libvirt_vif_driver', + 'nova.virt.libvirt.vif.LibvirtBridgeDriver', + 'The libvirt VIF driver to configure the VIFs.') def get_connection(read_only): @@ -165,6 +170,7 @@ class LibvirtConnection(driver.ComputeDriver): fw_class = utils.import_class(FLAGS.firewall_driver) self.firewall_driver = fw_class(get_connection=self._get_connection) + self.vif_driver = utils.import_object(FLAGS.libvirt_vif_driver) def init_host(self, host): # Adopt existing VM's running here @@ -256,7 +262,12 @@ class LibvirtConnection(driver.ComputeDriver): infos.append(info) return infos - def destroy(self, instance, cleanup=True): + def plug_vifs(self, instance, network_info): + """Plugin VIFs into networks.""" + for (network, mapping) in network_info: + self.vif_driver.plug(instance, network, mapping) + + def destroy(self, instance, network_info, cleanup=True): instance_name = instance['name'] try: @@ -300,6 +311,9 @@ class LibvirtConnection(driver.ComputeDriver): locals()) raise + for (network, mapping) in network_info: + self.vif_driver.unplug(instance, network, mapping) + def _wait_for_destroy(): """Called at an interval until the VM is gone.""" instance_name = instance['name'] @@ -314,7 +328,8 @@ class LibvirtConnection(driver.ComputeDriver): timer = utils.LoopingCall(_wait_for_destroy) timer.start(interval=0.5, now=True) - self.firewall_driver.unfilter_instance(instance) + self.firewall_driver.unfilter_instance(instance, + network_info=network_info) if cleanup: self._cleanup(instance) @@ -331,25 +346,24 @@ class LibvirtConnection(driver.ComputeDriver): if os.path.exists(target): shutil.rmtree(target) - @exception.wrap_exception + @exception.wrap_exception() def attach_volume(self, instance_name, device_path, mountpoint): virt_dom = self._lookup_by_name(instance_name) mount_device = mountpoint.rpartition("/")[2] - if device_path.startswith('/dev/'): + (type, protocol, name) = \ + self._get_volume_device_info(vol['device_path']) + if type == 'block': xml = """<disk type='block'> <driver name='qemu' type='raw'/> <source dev='%s'/> <target dev='%s' bus='virtio'/> </disk>""" % (device_path, mount_device) - elif ':' in device_path: - (protocol, name) = device_path.split(':') + elif type == 'network': xml = """<disk type='network'> <driver name='qemu' type='raw'/> <source protocol='%s' name='%s'/> <target dev='%s' bus='virtio'/> - </disk>""" % (protocol, - name, - mount_device) + </disk>""" % (protocol, name, mount_device) else: raise exception.InvalidDevicePath(path=device_path) @@ -375,7 +389,7 @@ class LibvirtConnection(driver.ComputeDriver): if doc is not None: doc.freeDoc() - @exception.wrap_exception + @exception.wrap_exception() def detach_volume(self, instance_name, mountpoint): virt_dom = self._lookup_by_name(instance_name) mount_device = mountpoint.rpartition("/")[2] @@ -384,7 +398,7 @@ class LibvirtConnection(driver.ComputeDriver): raise exception.DiskNotFound(location=mount_device) virt_dom.detachDevice(xml) - @exception.wrap_exception + @exception.wrap_exception() def snapshot(self, instance, image_href): """Create snapshot from a running VM instance. @@ -460,8 +474,8 @@ class LibvirtConnection(driver.ComputeDriver): # Clean up shutil.rmtree(temp_dir) - @exception.wrap_exception - def reboot(self, instance): + @exception.wrap_exception() + def reboot(self, instance, network_info): """Reboot a virtual machine, given an instance reference. This method actually destroys and re-creates the domain to ensure the @@ -476,7 +490,8 @@ class LibvirtConnection(driver.ComputeDriver): # NOTE(itoumsn): self.shutdown() and wait instead of self.destroy() is # better because we cannot ensure flushing dirty buffers # in the guest OS. But, in case of KVM, shutdown() does not work... - self.destroy(instance, False) + self.destroy(instance, network_info, cleanup=False) + self.plug_vifs(instance, network_info) self.firewall_driver.setup_basic_filtering(instance) self.firewall_driver.prepare_instance_filter(instance) self._create_new_domain(xml) @@ -501,32 +516,32 @@ class LibvirtConnection(driver.ComputeDriver): timer = utils.LoopingCall(_wait_for_reboot) return timer.start(interval=0.5, now=True) - @exception.wrap_exception + @exception.wrap_exception() def pause(self, instance, callback): """Pause VM instance""" dom = self._lookup_by_name(instance.name) dom.suspend() - @exception.wrap_exception + @exception.wrap_exception() def unpause(self, instance, callback): """Unpause paused VM instance""" dom = self._lookup_by_name(instance.name) dom.resume() - @exception.wrap_exception + @exception.wrap_exception() def suspend(self, instance, callback): """Suspend the specified instance""" dom = self._lookup_by_name(instance.name) dom.managedSave(0) - @exception.wrap_exception + @exception.wrap_exception() def resume(self, instance, callback): """resume the specified instance""" dom = self._lookup_by_name(instance.name) dom.create() - @exception.wrap_exception - def rescue(self, instance): + @exception.wrap_exception() + def rescue(self, instance, callback, network_info): """Loads a VM using rescue images. A rescue is normally performed when something goes wrong with the @@ -535,7 +550,7 @@ class LibvirtConnection(driver.ComputeDriver): data recovery. """ - self.destroy(instance, False) + self.destroy(instance, network_info, cleanup=False) xml = self.to_xml(instance, rescue=True) rescue_images = {'image_id': FLAGS.rescue_image_id, @@ -563,24 +578,24 @@ class LibvirtConnection(driver.ComputeDriver): timer = utils.LoopingCall(_wait_for_rescue) return timer.start(interval=0.5, now=True) - @exception.wrap_exception - def unrescue(self, instance): + @exception.wrap_exception() + def unrescue(self, instance, network_info): """Reboot the VM which is being rescued back into primary images. Because reboot destroys and re-creates instances, unresue should simply call reboot. """ - self.reboot(instance) + self.reboot(instance, network_info) - @exception.wrap_exception + @exception.wrap_exception() def poll_rescued_instances(self, timeout): pass # NOTE(ilyaalekseyev): Implementation like in multinics # for xenapi(tr3buchet) - @exception.wrap_exception - def spawn(self, instance, network_info=None, block_device_mapping=None): + @exception.wrap_exception() + def spawn(self, instance, network_info, block_device_mapping=None): xml = self.to_xml(instance, False, network_info=network_info, block_device_mapping=block_device_mapping) block_device_mapping = block_device_mapping or [] @@ -642,7 +657,7 @@ class LibvirtConnection(driver.ComputeDriver): LOG.info(_('Contents of file %(fpath)s: %(contents)r') % locals()) return contents - @exception.wrap_exception + @exception.wrap_exception() def get_console_output(self, instance): console_log = os.path.join(FLAGS.instances_path, instance['name'], 'console.log') @@ -663,7 +678,7 @@ class LibvirtConnection(driver.ComputeDriver): return self._dump_file(fpath) - @exception.wrap_exception + @exception.wrap_exception() def get_ajax_console(self, instance): def get_open_port(): start_port, end_port = FLAGS.ajaxterm_portrange.split("-") @@ -704,7 +719,7 @@ class LibvirtConnection(driver.ComputeDriver): def get_host_ip_addr(self): return FLAGS.my_ip - @exception.wrap_exception + @exception.wrap_exception() def get_vnc_console(self, instance): def get_vnc_port_for_instance(instance_name): virt_dom = self._lookup_by_name(instance_name) @@ -771,8 +786,6 @@ class LibvirtConnection(driver.ComputeDriver): def _create_image(self, inst, libvirt_xml, suffix='', disk_images=None, network_info=None, block_device_mapping=None): block_device_mapping = block_device_mapping or [] - if not network_info: - network_info = netutils.get_network_info(inst) if not suffix: suffix = '' @@ -881,18 +894,23 @@ class LibvirtConnection(driver.ComputeDriver): have_injected_networks = True address = mapping['ips'][0]['ip'] + netmask = mapping['ips'][0]['netmask'] address_v6 = None + gateway_v6 = None + netmask_v6 = None if FLAGS.use_ipv6: address_v6 = mapping['ip6s'][0]['ip'] + netmask_v6 = mapping['ip6s'][0]['netmask'] + gateway_v6 = mapping['gateway6'] net_info = {'name': 'eth%d' % ifc_num, 'address': address, - 'netmask': network_ref['netmask'], - 'gateway': network_ref['gateway'], - 'broadcast': network_ref['broadcast'], - 'dns': network_ref['dns'], + 'netmask': netmask, + 'gateway': mapping['gateway'], + 'broadcast': mapping['broadcast'], + 'dns': mapping['dns'], 'address_v6': address_v6, - 'gateway_v6': network_ref['gateway_v6'], - 'netmask_v6': network_ref['netmask_v6']} + 'gateway6': gateway_v6, + 'netmask_v6': netmask_v6} nets.append(net_info) if have_injected_networks: @@ -926,40 +944,6 @@ class LibvirtConnection(driver.ComputeDriver): if FLAGS.libvirt_type == 'uml': utils.execute('sudo', 'chown', 'root', basepath('disk')) - def _get_nic_for_xml(self, network, mapping): - # Assume that the gateway also acts as the dhcp server. - dhcp_server = network['gateway'] - gateway_v6 = network['gateway_v6'] - mac_id = mapping['mac'].replace(':', '') - - if FLAGS.allow_project_net_traffic: - template = "<parameter name=\"%s\"value=\"%s\" />\n" - net, mask = netutils.get_net_and_mask(network['cidr']) - values = [("PROJNET", net), ("PROJMASK", mask)] - if FLAGS.use_ipv6: - net_v6, prefixlen_v6 = netutils.get_net_and_prefixlen( - network['cidr_v6']) - values.extend([("PROJNETV6", net_v6), - ("PROJMASKV6", prefixlen_v6)]) - - extra_params = "".join([template % value for value in values]) - else: - extra_params = "\n" - - result = { - 'id': mac_id, - 'bridge_name': network['bridge'], - 'mac_address': mapping['mac'], - 'ip_address': mapping['ips'][0]['ip'], - 'dhcp_server': dhcp_server, - 'extra_params': extra_params, - } - - if gateway_v6: - result['gateway_v6'] = gateway_v6 + "/128" - - return result - root_mount_device = 'vda' # FIXME for now. it's hard coded. local_mount_device = 'vdb' # FIXME for now. it's hard coded. @@ -971,6 +955,16 @@ class LibvirtConnection(driver.ComputeDriver): return True return False + @exception.wrap_exception + def _get_volume_device_info(self, device_path): + if device_path.startswith('/dev/'): + return ('block', None, None) + elif ':' in device_path: + (protocol, name) = device_path.split(':') + return ('network', protocol, name) + else: + raise exception.InvalidDevicePath(path=device_path) + def _prepare_xml_info(self, instance, rescue=False, network_info=None, block_device_mapping=None): block_device_mapping = block_device_mapping or [] @@ -981,7 +975,7 @@ class LibvirtConnection(driver.ComputeDriver): nics = [] for (network, mapping) in network_info: - nics.append(self._get_nic_for_xml(network, mapping)) + nics.append(self.vif_driver.plug(instance, network, mapping)) # FIXME(vish): stick this in db inst_type_id = instance['instance_type_id'] inst_type = instance_types.get_instance_type(inst_type_id) @@ -993,6 +987,9 @@ class LibvirtConnection(driver.ComputeDriver): for vol in block_device_mapping: vol['mount_device'] = _strip_dev(vol['mount_device']) + (vol['type'], vol['protocol'], vol['name']) = \ + self._get_volume_device_info(vol['device_path']) + ebs_root = self._volume_in_mapping(self.root_mount_device, block_device_mapping) if self._volume_in_mapping(self.local_mount_device, @@ -1010,14 +1007,14 @@ class LibvirtConnection(driver.ComputeDriver): 'rescue': rescue, 'local': local_gb, 'driver_type': driver_type, + 'vif_type': FLAGS.libvirt_vif_type, 'nics': nics, 'ebs_root': ebs_root, 'volumes': block_device_mapping} - if FLAGS.vnc_enabled: - if FLAGS.libvirt_type != 'lxc': - xml_info['vncserver_host'] = FLAGS.vncserver_host - xml_info['vnc_keymap'] = FLAGS.vnc_keymap + if FLAGS.vnc_enabled and FLAGS.libvirt_type not in ('lxc', 'uml'): + xml_info['vncserver_host'] = FLAGS.vncserver_host + xml_info['vnc_keymap'] = FLAGS.vnc_keymap if not rescue: if instance['kernel_id']: xml_info['kernel'] = xml_info['basepath'] + "/kernel" @@ -1580,9 +1577,10 @@ class LibvirtConnection(driver.ComputeDriver): timer.f = wait_for_live_migration timer.start(interval=0.5, now=True) - def unfilter_instance(self, instance_ref): + def unfilter_instance(self, instance_ref, network_info): """See comments of same method in firewall_driver.""" - self.firewall_driver.unfilter_instance(instance_ref) + self.firewall_driver.unfilter_instance(instance_ref, + network_info=network_info) def update_host_status(self): """See xenapi_conn.py implementation.""" @@ -1591,3 +1589,7 @@ class LibvirtConnection(driver.ComputeDriver): def get_host_stats(self, refresh=False): """See xenapi_conn.py implementation.""" pass + + def set_host_enabled(self, host, enabled): + """Sets the specified host's ability to accept new instances.""" + pass diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py index b99f2ffb0..9ce57b6c9 100644 --- a/nova/virt/libvirt/firewall.py +++ b/nova/virt/libvirt/firewall.py @@ -46,7 +46,7 @@ class FirewallDriver(object): At this point, the instance isn't running yet.""" raise NotImplementedError() - def unfilter_instance(self, instance): + def unfilter_instance(self, instance, network_info=None): """Stop filtering instance""" raise NotImplementedError() @@ -300,9 +300,10 @@ class NWFilterFirewall(FirewallDriver): # execute in a native thread and block current greenthread until done tpool.execute(self._conn.nwfilterDefineXML, xml) - def unfilter_instance(self, instance): + def unfilter_instance(self, instance, network_info=None): """Clear out the nwfilter rules.""" - network_info = netutils.get_network_info(instance) + if not network_info: + network_info = netutils.get_network_info(instance) instance_name = instance.name for (network, mapping) in network_info: nic_id = mapping['mac'].replace(':', '') @@ -542,11 +543,11 @@ class IptablesFirewallDriver(FirewallDriver): """No-op. Everything is done in prepare_instance_filter""" pass - def unfilter_instance(self, instance): + def unfilter_instance(self, instance, network_info=None): if self.instances.pop(instance['id'], None): self.remove_filters_for_instance(instance) self.iptables.apply() - self.nwfilter.unfilter_instance(instance) + self.nwfilter.unfilter_instance(instance, network_info) else: LOG.info(_('Attempted to unfilter instance %s which is not ' 'filtered'), instance['id']) @@ -620,7 +621,7 @@ class IptablesFirewallDriver(FirewallDriver): ipv4_rules += ['-j $provider'] ipv6_rules += ['-j $provider'] - dhcp_servers = [network['gateway'] for (network, _m) in network_info] + dhcp_servers = [info['gateway'] for (_n, info) in network_info] for dhcp_server in dhcp_servers: ipv4_rules.append('-s %s -p udp --sport 67 --dport 68 ' @@ -637,7 +638,7 @@ class IptablesFirewallDriver(FirewallDriver): # they're not worth the clutter. if FLAGS.use_ipv6: # Allow RA responses - gateways_v6 = [network['gateway_v6'] for (network, _m) in + gateways_v6 = [mapping['gateway6'] for (_n, mapping) in network_info] for gateway_v6 in gateways_v6: ipv6_rules.append( @@ -645,8 +646,8 @@ class IptablesFirewallDriver(FirewallDriver): #Allow project network traffic if FLAGS.allow_project_net_traffic: - cidrv6s = [network['cidr_v6'] for (network, _m) - in network_info] + cidrv6s = [network['cidr_v6'] for (network, _m) in + network_info] for cidrv6 in cidrv6s: ipv6_rules.append('-s %s -j ACCEPT' % (cidrv6,)) diff --git a/nova/virt/libvirt/netutils.py b/nova/virt/libvirt/netutils.py index 0bad84f7c..041eacb2d 100644 --- a/nova/virt/libvirt/netutils.py +++ b/nova/virt/libvirt/netutils.py @@ -49,31 +49,36 @@ def get_ip_version(cidr): def get_network_info(instance): + # TODO(tr3buchet): this function needs to go away! network info + # MUST be passed down from compute # TODO(adiantum) If we will keep this function # we should cache network_info admin_context = context.get_admin_context() - ip_addresses = db.fixed_ip_get_all_by_instance(admin_context, - instance['id']) + fixed_ips = db.fixed_ip_get_by_instance(admin_context, instance['id']) + vifs = db.virtual_interface_get_by_instance(admin_context, instance['id']) networks = db.network_get_all_by_instance(admin_context, instance['id']) - flavor = db.instance_type_get_by_id(admin_context, + flavor = db.instance_type_get(admin_context, instance['instance_type_id']) network_info = [] - for network in networks: - network_ips = [ip for ip in ip_addresses - if ip['network_id'] == network['id']] + for vif in vifs: + network = vif['network'] + + # determine which of the instance's IPs belong to this network + network_ips = [fixed_ip['address'] for fixed_ip in fixed_ips if + fixed_ip['network_id'] == network['id']] def ip_dict(ip): return { - 'ip': ip['address'], + 'ip': ip, 'netmask': network['netmask'], 'enabled': '1'} def ip6_dict(): prefix = network['cidr_v6'] - mac = instance['mac_address'] + mac = vif['address'] project_id = instance['project_id'] return { 'ip': ipv6.to_global(prefix, mac, project_id), @@ -84,11 +89,16 @@ def get_network_info(instance): 'label': network['label'], 'gateway': network['gateway'], 'broadcast': network['broadcast'], - 'mac': instance['mac_address'], + 'mac': vif['address'], 'rxtx_cap': flavor['rxtx_cap'], - 'dns': [network['dns']], + 'dns': [], 'ips': [ip_dict(ip) for ip in network_ips]} + if network['dns1']: + mapping['dns'].append(network['dns1']) + if network['dns2']: + mapping['dns'].append(network['dns2']) + if FLAGS.use_ipv6: mapping['ip6s'] = [ip6_dict()] mapping['gateway6'] = network['gateway_v6'] diff --git a/nova/virt/libvirt/vif.py b/nova/virt/libvirt/vif.py new file mode 100644 index 000000000..24d45d1a7 --- /dev/null +++ b/nova/virt/libvirt/vif.py @@ -0,0 +1,134 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2011 Midokura KK +# Copyright (C) 2011 Nicira, Inc +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""VIF drivers for libvirt.""" + +from nova import flags +from nova import log as logging +from nova.network import linux_net +from nova.virt.libvirt import netutils +from nova import utils +from nova.virt.vif import VIFDriver + +LOG = logging.getLogger('nova.virt.libvirt.vif') + +FLAGS = flags.FLAGS + +flags.DEFINE_string('libvirt_ovs_bridge', 'br-int', + 'Name of Integration Bridge used by Open vSwitch') + + +class LibvirtBridgeDriver(VIFDriver): + """VIF driver for Linux bridge.""" + + def _get_configurations(self, network, mapping): + """Get a dictionary of VIF configurations for bridge type.""" + # Assume that the gateway also acts as the dhcp server. + gateway6 = mapping.get('gateway6') + mac_id = mapping['mac'].replace(':', '') + + if FLAGS.allow_project_net_traffic: + template = "<parameter name=\"%s\"value=\"%s\" />\n" + net, mask = netutils.get_net_and_mask(network['cidr']) + values = [("PROJNET", net), ("PROJMASK", mask)] + if FLAGS.use_ipv6: + net_v6, prefixlen_v6 = netutils.get_net_and_prefixlen( + network['cidr_v6']) + values.extend([("PROJNETV6", net_v6), + ("PROJMASKV6", prefixlen_v6)]) + + extra_params = "".join([template % value for value in values]) + else: + extra_params = "\n" + + result = { + 'id': mac_id, + 'bridge_name': network['bridge'], + 'mac_address': mapping['mac'], + 'ip_address': mapping['ips'][0]['ip'], + 'dhcp_server': mapping['dhcp_server'], + 'extra_params': extra_params, + } + + if gateway6: + result['gateway6'] = gateway6 + "/128" + + return result + + def plug(self, instance, network, mapping): + """Ensure that the bridge exists, and add VIF to it.""" + if (not network.get('multi_host') and + mapping.get('should_create_bridge')): + if mapping.get('should_create_vlan'): + LOG.debug(_('Ensuring vlan %(vlan)s and bridge %(bridge)s'), + {'vlan': network['vlan'], + 'bridge': network['bridge']}) + linux_net.ensure_vlan_bridge(network['vlan'], + network['bridge'], + network['bridge_interface']) + else: + LOG.debug(_("Ensuring bridge %s"), network['bridge']) + linux_net.ensure_bridge(network['bridge'], + network['bridge_interface']) + + return self._get_configurations(network, mapping) + + def unplug(self, instance, network, mapping): + """No manual unplugging required.""" + pass + + +class LibvirtOpenVswitchDriver(VIFDriver): + """VIF driver for Open vSwitch.""" + + def plug(self, instance, network, mapping): + vif_id = str(instance['id']) + "-" + str(network['id']) + dev = "tap-%s" % vif_id + iface_id = "nova-" + vif_id + if not linux_net._device_exists(dev): + utils.execute('sudo', 'ip', 'tuntap', 'add', dev, 'mode', 'tap') + utils.execute('sudo', 'ip', 'link', 'set', dev, 'up') + utils.execute('sudo', 'ovs-vsctl', '--', '--may-exist', 'add-port', + FLAGS.libvirt_ovs_bridge, dev, + '--', 'set', 'Interface', dev, + "external-ids:iface-id=%s" % iface_id, + '--', 'set', 'Interface', dev, + "external-ids:iface-status=active", + '--', 'set', 'Interface', dev, + "external-ids:attached-mac=%s" % mapping['mac']) + + result = { + 'script': '', + 'name': dev, + 'mac_address': mapping['mac']} + return result + + def unplug(self, instance, network, mapping): + """Unplug the VIF from the network by deleting the port from + the bridge.""" + vif_id = str(instance['id']) + "-" + str(network['id']) + dev = "tap-%s" % vif_id + try: + utils.execute('sudo', 'ovs-vsctl', 'del-port', + FLAGS.flat_network_bridge, dev) + utils.execute('sudo', 'ip', 'link', 'delete', dev) + except: + LOG.warning(_("Failed while unplugging vif of instance '%s'"), + instance['name']) + raise diff --git a/nova/virt/vif.py b/nova/virt/vif.py new file mode 100644 index 000000000..b78689957 --- /dev/null +++ b/nova/virt/vif.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2011 Midokura KK +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""VIF module common to all virt layers.""" + + +class VIFDriver(object): + """Abstract class that defines generic interfaces for all VIF drivers.""" + + def plug(self, instance, network, mapping): + """Plug VIF into network.""" + raise NotImplementedError() + + def unplug(self, instance, network, mapping): + """Unplug VIF from network.""" + raise NotImplementedError() diff --git a/nova/virt/vmwareapi/network_utils.py b/nova/virt/vmwareapi/network_utils.py index e77842535..08e3bf0b1 100644 --- a/nova/virt/vmwareapi/network_utils.py +++ b/nova/virt/vmwareapi/network_utils.py @@ -45,10 +45,30 @@ def get_network_with_the_name(session, network_name="vmnet0"): networks = session._call_method(vim_util,
"get_properties_for_a_collection_of_objects",
"Network", vm_networks, ["summary.name"])
- for network in networks:
- if network.propSet[0].val == network_name:
- return network.obj
- return None
+ network_obj = {}
+ for network in vm_networks:
+ # Get network properties
+ if network._type == 'DistributedVirtualPortgroup':
+ props = session._call_method(vim_util,
+ "get_dynamic_property", network,
+ "DistributedVirtualPortgroup", "config")
+ # NOTE(asomya): This only works on ESXi if the port binding is
+ # set to ephemeral
+ if props.name == network_name:
+ network_obj['type'] = 'DistributedVirtualPortgroup'
+ network_obj['dvpg'] = props.key
+ network_obj['dvsw'] = props.distributedVirtualSwitch.value
+ else:
+ props = session._call_method(vim_util,
+ "get_dynamic_property", network,
+ "Network", "summary.name")
+ if props == network_name:
+ network_obj['type'] = 'Network'
+ network_obj['name'] = network_name
+ if (len(network_obj) > 0):
+ return network_obj
+ else:
+ return None
def get_vswitch_for_vlan_interface(session, vlan_interface):
diff --git a/nova/virt/vmwareapi/vif.py b/nova/virt/vmwareapi/vif.py new file mode 100644 index 000000000..b3e43b209 --- /dev/null +++ b/nova/virt/vmwareapi/vif.py @@ -0,0 +1,95 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Citrix Systems, Inc. +# Copyright 2011 OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""VIF drivers for VMWare.""" + +from nova import db +from nova import exception +from nova import flags +from nova import log as logging +from nova import utils +from nova.virt.vif import VIFDriver +from nova.virt.vmwareapi_conn import VMWareAPISession +from nova.virt.vmwareapi import network_utils + + +LOG = logging.getLogger("nova.virt.vmwareapi.vif") + +FLAGS = flags.FLAGS + + +class VMWareVlanBridgeDriver(VIFDriver): + """VIF Driver to setup bridge/VLAN networking using VMWare API.""" + + def plug(self, instance, network, mapping): + """Create a vlan and bridge unless they already exist.""" + vlan_num = network['vlan'] + bridge = network['bridge'] + bridge_interface = network['bridge_interface'] + + # Open vmwareapi session + host_ip = FLAGS.vmwareapi_host_ip + host_username = FLAGS.vmwareapi_host_username + host_password = FLAGS.vmwareapi_host_password + if not host_ip or host_username is None or host_password is None: + raise Exception(_('Must specify vmwareapi_host_ip, ' + 'vmwareapi_host_username ' + 'and vmwareapi_host_password to use ' + 'connection_type=vmwareapi')) + session = VMWareAPISession(host_ip, host_username, host_password, + FLAGS.vmwareapi_api_retry_count) + vlan_interface = bridge_interface + # Check if the vlan_interface physical network adapter exists on the + # host. + if not network_utils.check_if_vlan_interface_exists(session, + vlan_interface): + raise exception.NetworkAdapterNotFound(adapter=vlan_interface) + + # Get the vSwitch associated with the Physical Adapter + vswitch_associated = network_utils.get_vswitch_for_vlan_interface( + session, vlan_interface) + if vswitch_associated is None: + raise exception.SwicthNotFoundForNetworkAdapter( + adapter=vlan_interface) + # Check whether bridge already exists and retrieve the the ref of the + # network whose name_label is "bridge" + network_ref = network_utils.get_network_with_the_name(session, bridge) + if network_ref is None: + # Create a port group on the vSwitch associated with the + # vlan_interface corresponding physical network adapter on the ESX + # host. + network_utils.create_port_group(session, bridge, + vswitch_associated, vlan_num) + else: + # Get the vlan id and vswitch corresponding to the port group + pg_vlanid, pg_vswitch = \ + network_utils.get_vlanid_and_vswitch_for_portgroup(session, + bridge) + + # Check if the vswitch associated is proper + if pg_vswitch != vswitch_associated: + raise exception.InvalidVLANPortGroup( + bridge=bridge, expected=vswitch_associated, + actual=pg_vswitch) + + # Check if the vlan id is proper for the port group + if pg_vlanid != vlan_num: + raise exception.InvalidVLANTag(bridge=bridge, tag=vlan_num, + pgroup=pg_vlanid) + + def unplug(self, instance, network, mapping): + pass diff --git a/nova/virt/vmwareapi/vm_util.py b/nova/virt/vmwareapi/vm_util.py index a2fa7600c..55578dd3c 100644 --- a/nova/virt/vmwareapi/vm_util.py +++ b/nova/virt/vmwareapi/vm_util.py @@ -40,7 +40,7 @@ def split_datastore_path(datastore_path): def get_vm_create_spec(client_factory, instance, data_store_name,
network_name="vmnet0",
- os_type="otherGuest"):
+ os_type="otherGuest", network_ref=None):
"""Builds the VM Create spec."""
config_spec = client_factory.create('ns0:VirtualMachineConfigSpec')
config_spec.name = instance.name
@@ -61,8 +61,12 @@ def get_vm_create_spec(client_factory, instance, data_store_name, config_spec.numCPUs = int(instance.vcpus)
config_spec.memoryMB = int(instance.memory_mb)
+ mac_address = None
+ if instance['mac_addresses']:
+ mac_address = instance['mac_addresses'][0]['address']
+
nic_spec = create_network_spec(client_factory,
- network_name, instance.mac_address)
+ network_name, mac_address)
device_config_spec = [nic_spec]
@@ -89,7 +93,8 @@ def create_controller_spec(client_factory, key): return virtual_device_config
-def create_network_spec(client_factory, network_name, mac_address):
+def create_network_spec(client_factory, network_name, mac_address,
+ network_ref=None):
"""
Builds a config spec for the addition of a new network
adapter to the VM.
@@ -101,9 +106,24 @@ def create_network_spec(client_factory, network_name, mac_address): # Get the recommended card type for the VM based on the guest OS of the VM
net_device = client_factory.create('ns0:VirtualPCNet32')
- backing = \
- client_factory.create('ns0:VirtualEthernetCardNetworkBackingInfo')
- backing.deviceName = network_name
+ # NOTE(asomya): Only works on ESXi if the portgroup binding is set to
+ # ephemeral. Invalid configuration if set to static and the NIC does
+ # not come up on boot if set to dynamic.
+ backing = None
+ if (network_ref['type'] == "DistributedVirtualPortgroup"):
+ backing_name = \
+ 'ns0:VirtualEthernetCardDistributedVirtualPortBackingInfo'
+ backing = \
+ client_factory.create(backing_name)
+ portgroup = \
+ client_factory.create('ns0:DistributedVirtualSwitchPortConnection')
+ portgroup.switchUuid = network_ref['dvsw']
+ portgroup.portgroupKey = network_ref['dvpg']
+ backing.port = portgroup
+ else:
+ backing = \
+ client_factory.create('ns0:VirtualEthernetCardNetworkBackingInfo')
+ backing.deviceName = network_name
connectable_spec = \
client_factory.create('ns0:VirtualDeviceConnectInfo')
@@ -274,9 +294,11 @@ def get_dummy_vm_create_spec(client_factory, name, data_store_name): return config_spec
-def get_machine_id_change_spec(client_factory, mac, ip_addr, netmask, gateway):
+def get_machine_id_change_spec(client_factory, mac, ip_addr, netmask,
+ gateway, broadcast, dns):
"""Builds the machine id change config spec."""
- machine_id_str = "%s;%s;%s;%s" % (mac, ip_addr, netmask, gateway)
+ machine_id_str = "%s;%s;%s;%s;%s;%s" % (mac, ip_addr, netmask,
+ gateway, broadcast, dns)
virtual_machine_config_spec = \
client_factory.create('ns0:VirtualMachineConfigSpec')
diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index 5f76b0df5..7e7d2dac3 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -31,6 +31,7 @@ from nova import db from nova import exception
from nova import flags
from nova import log as logging
+from nova import utils
from nova.compute import power_state
from nova.virt.vmwareapi import vim_util
from nova.virt.vmwareapi import vm_util
@@ -38,6 +39,10 @@ from nova.virt.vmwareapi import vmware_images from nova.virt.vmwareapi import network_utils
FLAGS = flags.FLAGS
+flags.DEFINE_string('vmware_vif_driver',
+ 'nova.virt.vmwareapi.vif.VMWareVlanBridgeDriver',
+ 'The VMWare VIF driver to configure the VIFs.')
+
LOG = logging.getLogger("nova.virt.vmwareapi.vmops")
VMWARE_POWER_STATES = {
@@ -52,6 +57,7 @@ class VMWareVMOps(object): def __init__(self, session):
"""Initializer."""
self._session = session
+ self._vif_driver = utils.import_object(FLAGS.vmware_vif_driver)
def _wait_with_callback(self, instance_id, task, callback):
"""Waits for the task to finish and does a callback after."""
@@ -83,7 +89,7 @@ class VMWareVMOps(object): LOG.debug(_("Got total of %s instances") % str(len(lst_vm_names)))
return lst_vm_names
- def spawn(self, instance):
+ def spawn(self, instance, network_info):
"""
Creates a VM instance.
@@ -116,8 +122,10 @@ class VMWareVMOps(object): net_name)
if network_ref is None:
raise exception.NetworkNotFoundForBridge(bridge=net_name)
+ return network_ref
- _check_if_network_bridge_exists()
+ self.plug_vifs(instance, network_info)
+ network_obj = _check_if_network_bridge_exists()
def _get_datastore_ref():
"""Get the datastore list and choose the first local storage."""
@@ -175,8 +183,10 @@ class VMWareVMOps(object): vm_folder_mor, res_pool_mor = _get_vmfolder_and_res_pool_mors()
# Get the create vm config spec
- config_spec = vm_util.get_vm_create_spec(client_factory, instance,
- data_store_name, net_name, os_type)
+ config_spec = vm_util.get_vm_create_spec(
+ client_factory, instance,
+ data_store_name, net_name, os_type,
+ network_obj)
def _execute_create_vm():
"""Create VM on ESX host."""
@@ -472,11 +482,14 @@ class VMWareVMOps(object): _clean_temp_data()
- def reboot(self, instance):
+ def reboot(self, instance, network_info):
"""Reboot a VM instance."""
vm_ref = self._get_vm_ref_from_the_name(instance.name)
if vm_ref is None:
raise exception.InstanceNotFound(instance_id=instance.id)
+
+ self.plug_vifs(instance, network_info)
+
lst_properties = ["summary.guest.toolsStatus", "runtime.powerState",
"summary.guest.toolsRunningStatus"]
props = self._session._call_method(vim_util, "get_object_properties",
@@ -514,7 +527,7 @@ class VMWareVMOps(object): self._session._wait_for_task(instance.id, reset_task)
LOG.debug(_("Did hard reboot of VM %s") % instance.name)
- def destroy(self, instance):
+ def destroy(self, instance, network_info):
"""
Destroy a VM instance. Steps followed are:
1. Power off the VM, if it is in poweredOn state.
@@ -560,6 +573,8 @@ class VMWareVMOps(object): LOG.warn(_("In vmwareapi:vmops:destroy, got this exception"
" while un-registering the VM: %s") % str(excep))
+ self._unplug_vifs(instance, network_info)
+
# Delete the folder holding the VM related content on
# the datastore.
try:
@@ -706,19 +721,29 @@ class VMWareVMOps(object): Set the machine id of the VM for guest tools to pick up and change
the IP.
"""
+ admin_context = context.get_admin_context()
vm_ref = self._get_vm_ref_from_the_name(instance.name)
if vm_ref is None:
raise exception.InstanceNotFound(instance_id=instance.id)
network = db.network_get_by_instance(context.get_admin_context(),
instance['id'])
- mac_addr = instance.mac_address
+ mac_address = None
+ if instance['mac_addresses']:
+ mac_address = instance['mac_addresses'][0]['address']
+
net_mask = network["netmask"]
gateway = network["gateway"]
- ip_addr = db.instance_get_fixed_address(context.get_admin_context(),
- instance['id'])
+ broadcast = network["broadcast"]
+ dns = network["dns"]
+
+ addresses = db.instance_get_fixed_addresses(admin_context,
+ instance['id'])
+ ip_addr = addresses[0] if addresses else None
+
machine_id_chanfge_spec = \
- vm_util.get_machine_id_change_spec(client_factory, mac_addr,
- ip_addr, net_mask, gateway)
+ vm_util.get_machine_id_change_spec(client_factory, mac_address,
+ ip_addr, net_mask, gateway,
+ broadcast, dns)
LOG.debug(_("Reconfiguring VM instance %(name)s to set the machine id "
"with ip - %(ip_addr)s") %
({'name': instance.name,
@@ -778,3 +803,13 @@ class VMWareVMOps(object): if vm.propSet[0].val == vm_name:
return vm.obj
return None
+
+ def plug_vifs(self, instance, network_info):
+ """Plug VIFs into networks."""
+ for (network, mapping) in network_info:
+ self._vif_driver.plug(instance, network, mapping)
+
+ def _unplug_vifs(self, instance, network_info):
+ """Unplug VIFs from networks."""
+ for (network, mapping) in network_info:
+ self._vif_driver.unplug(instance, network, mapping)
diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index 3c6345ec8..ce57847b2 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -124,21 +124,21 @@ class VMWareESXConnection(driver.ComputeDriver): """List VM instances."""
return self._vmops.list_instances()
- def spawn(self, instance, network_info=None, block_device_mapping=None):
+ def spawn(self, instance, network_info, block_device_mapping=None):
"""Create VM instance."""
- self._vmops.spawn(instance)
+ self._vmops.spawn(instance, network_info)
def snapshot(self, instance, name):
"""Create snapshot from a running VM instance."""
self._vmops.snapshot(instance, name)
- def reboot(self, instance):
+ def reboot(self, instance, network_info):
"""Reboot VM instance."""
- self._vmops.reboot(instance)
+ self._vmops.reboot(instance, network_info)
- def destroy(self, instance):
+ def destroy(self, instance, network_info):
"""Destroy VM instance."""
- self._vmops.destroy(instance)
+ self._vmops.destroy(instance, network_info)
def pause(self, instance, callback):
"""Pause VM instance."""
@@ -190,6 +190,14 @@ class VMWareESXConnection(driver.ComputeDriver): """This method is supported only by libvirt."""
return
+ def set_host_enabled(self, host, enabled):
+ """Sets the specified host's ability to accept new instances."""
+ pass
+
+ def plug_vifs(self, instance, network_info):
+ """Plugs in VIFs to networks."""
+ self._vmops.plug_vifs(instance, network_info)
+
class VMWareAPISession(object):
"""
diff --git a/nova/virt/xenapi/vif.py b/nova/virt/xenapi/vif.py new file mode 100644 index 000000000..527602243 --- /dev/null +++ b/nova/virt/xenapi/vif.py @@ -0,0 +1,140 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Citrix Systems, Inc. +# Copyright 2011 OpenStack LLC. +# Copyright (C) 2011 Nicira, Inc +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""VIF drivers for XenAPI.""" + +from nova import flags +from nova import log as logging +from nova.virt.vif import VIFDriver +from nova.virt.xenapi.network_utils import NetworkHelper + +FLAGS = flags.FLAGS +flags.DEFINE_string('xenapi_ovs_integration_bridge', 'xapi1', + 'Name of Integration Bridge used by Open vSwitch') + +LOG = logging.getLogger("nova.virt.xenapi.vif") + + +class XenAPIBridgeDriver(VIFDriver): + """VIF Driver for XenAPI that uses XenAPI to create Networks.""" + + def plug(self, xenapi_session, vm_ref, instance, device, network, + network_mapping): + if network_mapping.get('should_create_vlan'): + network_ref = self.ensure_vlan_bridge(xenapi_session, network) + else: + network_ref = NetworkHelper.find_network_with_bridge( + xenapi_session, network['bridge']) + rxtx_cap = network_mapping.pop('rxtx_cap') + vif_rec = {} + vif_rec['device'] = str(device) + vif_rec['network'] = network_ref + vif_rec['VM'] = vm_ref + vif_rec['MAC'] = network_mapping['mac'] + vif_rec['MTU'] = '1500' + vif_rec['other_config'] = {} + vif_rec['qos_algorithm_type'] = "ratelimit" if rxtx_cap else '' + vif_rec['qos_algorithm_params'] = \ + {"kbps": str(rxtx_cap * 1024)} if rxtx_cap else {} + return vif_rec + + def ensure_vlan_bridge(self, xenapi_session, network): + """Ensure that a VLAN bridge exists""" + + vlan_num = network['vlan'] + bridge = network['bridge'] + bridge_interface = network['bridge_interface'] + # Check whether bridge already exists + # Retrieve network whose name_label is "bridge" + network_ref = NetworkHelper.find_network_with_name_label( + xenapi_session, bridge) + if network_ref is None: + # If bridge does not exists + # 1 - create network + description = 'network for nova bridge %s' % bridge + network_rec = {'name_label': bridge, + 'name_description': description, + 'other_config': {}} + network_ref = xenapi_session.call_xenapi('network.create', + network_rec) + # 2 - find PIF for VLAN NOTE(salvatore-orlando): using double + # quotes inside single quotes as xapi filter only support + # tokens in double quotes + expr = 'field "device" = "%s" and \ + field "VLAN" = "-1"' % bridge_interface + pifs = xenapi_session.call_xenapi('PIF.get_all_records_where', + expr) + pif_ref = None + # Multiple PIF are ok: we are dealing with a pool + if len(pifs) == 0: + raise Exception(_('Found no PIF for device %s') % \ + bridge_interface) + for pif_ref in pifs.keys(): + xenapi_session.call_xenapi('VLAN.create', + pif_ref, + str(vlan_num), + network_ref) + else: + # Check VLAN tag is appropriate + network_rec = xenapi_session.call_xenapi('network.get_record', + network_ref) + # Retrieve PIFs from network + for pif_ref in network_rec['PIFs']: + # Retrieve VLAN from PIF + pif_rec = xenapi_session.call_xenapi('PIF.get_record', + pif_ref) + pif_vlan = int(pif_rec['VLAN']) + # Raise an exception if VLAN != vlan_num + if pif_vlan != vlan_num: + raise Exception(_( + "PIF %(pif_rec['uuid'])s for network " + "%(bridge)s has VLAN id %(pif_vlan)d. " + "Expected %(vlan_num)d") % locals()) + + return network_ref + + def unplug(self, instance, network, mapping): + pass + + +class XenAPIOpenVswitchDriver(VIFDriver): + """VIF driver for Open vSwitch with XenAPI.""" + + def plug(self, xenapi_session, vm_ref, instance, device, network, + network_mapping): + # with OVS model, always plug into an OVS integration bridge + # that is already created + network_ref = NetworkHelper.find_network_with_bridge(xenapi_session, + FLAGS.xenapi_ovs_integration_bridge) + vif_rec = {} + vif_rec['device'] = str(device) + vif_rec['network'] = network_ref + vif_rec['VM'] = vm_ref + vif_rec['MAC'] = network_mapping['mac'] + vif_rec['MTU'] = '1500' + vif_id = "nova-" + str(instance['id']) + "-" + str(network['id']) + vif_rec['qos_algorithm_type'] = "" + vif_rec['qos_algorithm_params'] = {} + # OVS on the hypervisor monitors this key and uses it to + # set the iface-id attribute + vif_rec['other_config'] = {"nicira-iface-id": vif_id} + return vif_rec + + def unplug(self, instance, network, mapping): + pass diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index f91958c57..62863c6d8 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -23,6 +23,7 @@ import json import os import pickle import re +import sys import tempfile import time import urllib @@ -71,17 +72,51 @@ KERNEL_DIR = '/boot/guest' class ImageType: """ Enumeration class for distinguishing different image types - 0 - kernel/ramdisk image (goes on dom0's filesystem) - 1 - disk image (local SR, partitioned by objectstore plugin) - 2 - raw disk image (local SR, NOT partitioned by plugin) - 3 - vhd disk image (local SR, NOT inspected by XS, PV assumed for + 0 - kernel image (goes on dom0's filesystem) + 1 - ramdisk image (goes on dom0's filesystem) + 2 - disk image (local SR, partitioned by objectstore plugin) + 3 - raw disk image (local SR, NOT partitioned by plugin) + 4 - vhd disk image (local SR, NOT inspected by XS, PV assumed for linux, HVM assumed for Windows) """ - KERNEL_RAMDISK = 0 - DISK = 1 - DISK_RAW = 2 - DISK_VHD = 3 + KERNEL = 0 + RAMDISK = 1 + DISK = 2 + DISK_RAW = 3 + DISK_VHD = 4 + + KERNEL_STR = "kernel" + RAMDISK_STR = "ramdisk" + DISK_STR = "os" + DISK_RAW_STR = "os_raw" + DISK_VHD_STR = "vhd" + + @classmethod + def to_string(cls, image_type): + if image_type == ImageType.KERNEL: + return ImageType.KERNEL_STR + elif image_type == ImageType.RAMDISK: + return ImageType.RAMDISK_STR + elif image_type == ImageType.DISK: + return ImageType.DISK_STR + elif image_type == ImageType.DISK_RAW: + return ImageType.DISK_RAW_STR + elif image_type == ImageType.DISK_VHD: + return ImageType.VHD_STR + + @classmethod + def from_string(cls, image_type_str): + if image_type_str == ImageType.KERNEL_STR: + return ImageType.KERNEL + elif image_type == ImageType.RAMDISK_STR: + return ImageType.RAMDISK + elif image_type == ImageType.DISK_STR: + return ImageType.DISK + elif image_type == ImageType.DISK_RAW_STR: + return ImageType.DISK_RAW + elif image_type == ImageType.DISK_VHD_STR: + return ImageType.VHD class VMHelper(HelperBase): @@ -145,7 +180,6 @@ class VMHelper(HelperBase): 'VCPUs_max': vcpus, 'VCPUs_params': {}, 'xenstore_data': {}} - # Complete VM configuration record according to the image type # non-raw/raw with PV kernel/raw in HVM mode if use_pv_kernel: @@ -240,26 +274,13 @@ class VMHelper(HelperBase): raise StorageError(_('Unable to destroy VBD %s') % vbd_ref) @classmethod - def create_vif(cls, session, vm_ref, network_ref, mac_address, - dev, rxtx_cap=0): - """Create a VIF record. Returns a Deferred that gives the new - VIF reference.""" - vif_rec = {} - vif_rec['device'] = str(dev) - vif_rec['network'] = network_ref - vif_rec['VM'] = vm_ref - vif_rec['MAC'] = mac_address - vif_rec['MTU'] = '1500' - vif_rec['other_config'] = {} - vif_rec['qos_algorithm_type'] = "ratelimit" if rxtx_cap else '' - vif_rec['qos_algorithm_params'] = \ - {"kbps": str(rxtx_cap * 1024)} if rxtx_cap else {} - LOG.debug(_('Creating VIF for VM %(vm_ref)s,' - ' network %(network_ref)s.') % locals()) - vif_ref = session.call_xenapi('VIF.create', vif_rec) - LOG.debug(_('Created VIF %(vif_ref)s for VM %(vm_ref)s,' - ' network %(network_ref)s.') % locals()) - return vif_ref + def destroy_vdi(cls, session, vdi_ref): + try: + task = session.call_xenapi('Async.VDI.destroy', vdi_ref) + session.wait_for_task(task) + except cls.XenAPI.Failure, exc: + LOG.exception(exc) + raise StorageError(_('Unable to destroy VDI %s') % vdi_ref) @classmethod def create_vdi(cls, session, sr_ref, name_label, virtual_size, read_only): @@ -394,12 +415,12 @@ class VMHelper(HelperBase): """ LOG.debug(_("Asking xapi to fetch vhd image %(image)s") % locals()) - sr_ref = safe_find_sr(session) - # NOTE(sirp): The Glance plugin runs under Python 2.4 which does not - # have the `uuid` module. To work around this, we generate the uuids - # here (under Python 2.6+) and pass them as arguments + # NOTE(sirp): The Glance plugin runs under Python 2.4 + # which does not have the `uuid` module. To work around this, + # we generate the uuids here (under Python 2.6+) and + # pass them as arguments uuid_stack = [str(uuid.uuid4()) for i in xrange(2)] glance_host, glance_port = \ @@ -449,18 +470,20 @@ class VMHelper(HelperBase): # FIXME(sirp): Since the Glance plugin seems to be required for the # VHD disk, it may be worth using the plugin for both VHD and RAW and # DISK restores + LOG.debug(_("Fetching image %(image)s") % locals()) + LOG.debug(_("Image Type: %s"), ImageType.to_string(image_type)) sr_ref = safe_find_sr(session) glance_client, image_id = nova.image.get_glance_client(image) meta, image_file = glance_client.get_image(image_id) virtual_size = int(meta['size']) vdi_size = virtual_size - LOG.debug(_("Size for image %(image)s:%(virtual_size)d") % locals()) - + LOG.debug(_("Size for image %(image)s:" + + "%(virtual_size)d") % locals()) if image_type == ImageType.DISK: # Make room for MBR. vdi_size += MBR_SIZE_BYTES - elif image_type == ImageType.KERNEL_RAMDISK and \ + elif image_type in (ImageType.KERNEL, ImageType.RAMDISK) and \ vdi_size > FLAGS.max_kernel_ramdisk_size: max_size = FLAGS.max_kernel_ramdisk_size raise exception.Error( @@ -469,29 +492,45 @@ class VMHelper(HelperBase): name_label = get_name_label_for_image(image) vdi_ref = cls.create_vdi(session, sr_ref, name_label, vdi_size, False) - - with_vdi_attached_here(session, vdi_ref, False, - lambda dev: - _stream_disk(dev, image_type, - virtual_size, image_file)) - if image_type == ImageType.KERNEL_RAMDISK: - #we need to invoke a plugin for copying VDI's - #content into proper path - LOG.debug(_("Copying VDI %s to /boot/guest on dom0"), vdi_ref) - fn = "copy_kernel_vdi" - args = {} - args['vdi-ref'] = vdi_ref - #let the plugin copy the correct number of bytes - args['image-size'] = str(vdi_size) - task = session.async_call_plugin('glance', fn, args) - filename = session.wait_for_task(task, instance_id) - #remove the VDI as it is not needed anymore - session.get_xenapi().VDI.destroy(vdi_ref) - LOG.debug(_("Kernel/Ramdisk VDI %s destroyed"), vdi_ref) - return filename - else: + # From this point we have a VDI on Xen host; + # If anything goes wrong, we need to remember its uuid. + try: + filename = None vdi_uuid = session.get_xenapi().VDI.get_uuid(vdi_ref) - return [dict(vdi_type='os', vdi_uuid=vdi_uuid)] + with_vdi_attached_here(session, vdi_ref, False, + lambda dev: + _stream_disk(dev, image_type, + virtual_size, image_file)) + if image_type in (ImageType.KERNEL, ImageType.RAMDISK): + # We need to invoke a plugin for copying the + # content of the VDI into the proper path. + LOG.debug(_("Copying VDI %s to /boot/guest on dom0"), vdi_ref) + fn = "copy_kernel_vdi" + args = {} + args['vdi-ref'] = vdi_ref + # Let the plugin copy the correct number of bytes. + args['image-size'] = str(vdi_size) + task = session.async_call_plugin('glance', fn, args) + filename = session.wait_for_task(task, instance_id) + # Remove the VDI as it is not needed anymore. + session.get_xenapi().VDI.destroy(vdi_ref) + LOG.debug(_("Kernel/Ramdisk VDI %s destroyed"), vdi_ref) + return [dict(vdi_type=ImageType.to_string(image_type), + vdi_uuid=None, + file=filename)] + else: + return [dict(vdi_type=ImageType.to_string(image_type), + vdi_uuid=vdi_uuid, + file=None)] + except (cls.XenAPI.Failure, IOError, OSError) as e: + # We look for XenAPI and OS failures. + LOG.exception(_("instance %s: Failed to fetch glance image"), + instance_id, exc_info=sys.exc_info()) + e.args = e.args + ([dict(vdi_type=ImageType. + to_string(image_type), + vdi_uuid=vdi_uuid, + file=filename)],) + raise e @classmethod def determine_disk_image_type(cls, instance): @@ -506,7 +545,8 @@ class VMHelper(HelperBase): whether a kernel_id is specified. """ def log_disk_format(image_type): - pretty_format = {ImageType.KERNEL_RAMDISK: 'KERNEL_RAMDISK', + pretty_format = {ImageType.KERNEL: 'KERNEL', + ImageType.RAMDISK: 'RAMDISK', ImageType.DISK: 'DISK', ImageType.DISK_RAW: 'DISK_RAW', ImageType.DISK_VHD: 'DISK_VHD'} @@ -519,8 +559,8 @@ class VMHelper(HelperBase): def determine_from_glance(): glance_disk_format2nova_type = { 'ami': ImageType.DISK, - 'aki': ImageType.KERNEL_RAMDISK, - 'ari': ImageType.KERNEL_RAMDISK, + 'aki': ImageType.KERNEL, + 'ari': ImageType.RAMDISK, 'raw': ImageType.DISK_RAW, 'vhd': ImageType.DISK_VHD} image_ref = instance.image_ref @@ -553,7 +593,7 @@ class VMHelper(HelperBase): image_type): """Fetch image from glance based on image type. - Returns: A single filename if image_type is KERNEL_RAMDISK + Returns: A single filename if image_type is KERNEL or RAMDISK A list of dictionaries that describe VDIs, otherwise """ if image_type == ImageType.DISK_VHD: @@ -568,13 +608,13 @@ class VMHelper(HelperBase): secret, image_type): """Fetch an image from objectstore. - Returns: A single filename if image_type is KERNEL_RAMDISK + Returns: A single filename if image_type is KERNEL or RAMDISK A list of dictionaries that describe VDIs, otherwise """ url = "http://%s:%s/_images/%s/image" % (FLAGS.s3_host, FLAGS.s3_port, image) LOG.debug(_("Asking xapi to fetch %(url)s as %(access)s") % locals()) - if image_type == ImageType.KERNEL_RAMDISK: + if image_type in (ImageType.KERNEL, ImageType.RAMDISK): fn = 'get_kernel' else: fn = 'get_vdi' @@ -584,15 +624,20 @@ class VMHelper(HelperBase): args['password'] = secret args['add_partition'] = 'false' args['raw'] = 'false' - if image_type != ImageType.KERNEL_RAMDISK: + if not image_type in (ImageType.KERNEL, ImageType.RAMDISK): args['add_partition'] = 'true' if image_type == ImageType.DISK_RAW: args['raw'] = 'true' task = session.async_call_plugin('objectstore', fn, args) - uuid_or_fn = session.wait_for_task(task, instance_id) - if image_type != ImageType.KERNEL_RAMDISK: - return [dict(vdi_type='os', vdi_uuid=uuid_or_fn)] - return uuid_or_fn + vdi_uuid = None + filename = None + if image_type in (ImageType.KERNEL, ImageType.RAMDISK): + filename = session.wait_for_task(task, instance_id) + else: + vdi_uuid = session.wait_for_task(task, instance_id) + return [dict(vdi_type=ImageType.to_string(image_type), + vdi_uuid=vdi_uuid, + file=filename)] @classmethod def determine_is_pv(cls, session, instance_id, vdi_ref, disk_image_type, diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 2f4286184..0473abb97 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -24,7 +24,10 @@ import json import M2Crypto import os import pickle +import random import subprocess +import sys +import time import uuid from nova import context @@ -44,7 +47,14 @@ from nova.virt.xenapi.vm_utils import ImageType XenAPI = None LOG = logging.getLogger("nova.virt.xenapi.vmops") + FLAGS = flags.FLAGS +flags.DEFINE_integer('windows_version_timeout', 300, + 'number of seconds to wait for windows agent to be ' + 'fully operational') +flags.DEFINE_string('xenapi_vif_driver', + 'nova.virt.xenapi.vif.XenAPIBridgeDriver', + 'The XenAPI VIF driver using XenServer Network APIs.') def cmp_version(a, b): @@ -71,6 +81,7 @@ class VMOps(object): self._session = session self.poll_rescue_last_ran = None VMHelper.XenAPI = self.XenAPI + self.vif_driver = utils.import_object(FLAGS.xenapi_vif_driver) def list_instances(self): """List VM instances.""" @@ -103,11 +114,12 @@ class VMOps(object): vm_ref = VMHelper.lookup(self._session, instance.name) self._start(instance, vm_ref) - def finish_resize(self, instance, disk_info): + def finish_resize(self, instance, disk_info, network_info): vdi_uuid = self.link_disks(instance, disk_info['base_copy'], disk_info['cow']) vm_ref = self._create_vm(instance, - [dict(vdi_type='os', vdi_uuid=vdi_uuid)]) + [dict(vdi_type='os', vdi_uuid=vdi_uuid)], + network_info) self.resize_instance(instance, vdi_uuid) self._spawn(instance, vm_ref) @@ -130,16 +142,25 @@ class VMOps(object): disk_image_type) return vdis - def spawn(self, instance, network_info=None): - vdis = self._create_disks(instance) - vm_ref = self._create_vm(instance, vdis, network_info) - self._spawn(instance, vm_ref) + def spawn(self, instance, network_info): + vdis = None + try: + vdis = self._create_disks(instance) + vm_ref = self._create_vm(instance, vdis, network_info) + self._spawn(instance, vm_ref) + except (self.XenAPI.Failure, OSError, IOError) as spawn_error: + LOG.exception(_("instance %s: Failed to spawn"), + instance.id, exc_info=sys.exc_info()) + LOG.debug(_('Instance %s failed to spawn - performing clean-up'), + instance.id) + self._handle_spawn_error(vdis, spawn_error) + raise spawn_error def spawn_rescue(self, instance): """Spawn a rescue instance.""" self.spawn(instance) - def _create_vm(self, instance, vdis, network_info=None): + def _create_vm(self, instance, vdis, network_info): """Create VM instance.""" instance_name = instance.name vm_ref = VMHelper.lookup(self._session, instance_name) @@ -159,42 +180,64 @@ class VMOps(object): project = AuthManager().get_project(instance.project_id) disk_image_type = VMHelper.determine_disk_image_type(instance) - kernel = None - if instance.kernel_id: - kernel = VMHelper.fetch_image(self._session, instance.id, - instance.kernel_id, user, project, - ImageType.KERNEL_RAMDISK) - ramdisk = None - if instance.ramdisk_id: - ramdisk = VMHelper.fetch_image(self._session, instance.id, - instance.ramdisk_id, user, project, - ImageType.KERNEL_RAMDISK) - - # Create the VM ref and attach the first disk - first_vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', - vdis[0]['vdi_uuid']) - - vm_mode = instance.vm_mode and instance.vm_mode.lower() - if vm_mode == 'pv': - use_pv_kernel = True - elif vm_mode in ('hv', 'hvm'): - use_pv_kernel = False - vm_mode = 'hvm' # Normalize - else: - use_pv_kernel = VMHelper.determine_is_pv(self._session, - instance.id, first_vdi_ref, disk_image_type, - instance.os_type) - vm_mode = use_pv_kernel and 'pv' or 'hvm' - - if instance.vm_mode != vm_mode: - # Update database with normalized (or determined) value - db.instance_update(context.get_admin_context(), - instance['id'], {'vm_mode': vm_mode}) - - vm_ref = VMHelper.create_vm(self._session, instance, - kernel, ramdisk, use_pv_kernel) + try: + if instance.kernel_id: + kernel = VMHelper.fetch_image(self._session, instance.id, + instance.kernel_id, user, project, + ImageType.KERNEL)[0] + if instance.ramdisk_id: + ramdisk = VMHelper.fetch_image(self._session, instance.id, + instance.ramdisk_id, user, project, + ImageType.RAMDISK)[0] + # Create the VM ref and attach the first disk + first_vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', + vdis[0]['vdi_uuid']) + + vm_mode = instance.vm_mode and instance.vm_mode.lower() + if vm_mode == 'pv': + use_pv_kernel = True + elif vm_mode in ('hv', 'hvm'): + use_pv_kernel = False + vm_mode = 'hvm' # Normalize + else: + use_pv_kernel = VMHelper.determine_is_pv(self._session, + instance.id, first_vdi_ref, disk_image_type, + instance.os_type) + vm_mode = use_pv_kernel and 'pv' or 'hvm' + + if instance.vm_mode != vm_mode: + # Update database with normalized (or determined) value + db.instance_update(context.get_admin_context(), + instance['id'], {'vm_mode': vm_mode}) + vm_ref = VMHelper.create_vm(self._session, instance, + kernel and kernel.get('file', None) or None, + ramdisk and ramdisk.get('file', None) or None, + use_pv_kernel) + except (self.XenAPI.Failure, OSError, IOError) as vm_create_error: + # Collect VDI/file resources to clean up; + # These resources will be removed by _handle_spawn_error. + LOG.exception(_("instance %s: Failed to spawn - " + + "Unable to create VM"), + instance.id, exc_info=sys.exc_info()) + last_arg = None + resources = [] + + if vm_create_error.args: + last_arg = vm_create_error.args[-1] + if isinstance(last_arg, list): + resources = last_arg + else: + vm_create_error.args = vm_create_error.args + (resources,) + + if kernel: + resources.append(kernel) + if ramdisk: + resources.append(ramdisk) + + raise vm_create_error + VMHelper.create_vbd(session=self._session, vm_ref=vm_ref, vdi_ref=first_vdi_ref, userdevice=0, bootable=True) @@ -211,17 +254,12 @@ class VMOps(object): bootable=False) userdevice += 1 - # TODO(tr3buchet) - check to make sure we have network info, otherwise - # create it now. This goes away once nova-multi-nic hits. - if network_info is None: - network_info = self._get_network_info(instance) - # Alter the image before VM start for, e.g. network injection if FLAGS.xenapi_inject_image: VMHelper.preconfigure_instance(self._session, instance, first_vdi_ref, network_info) - self.create_vifs(vm_ref, network_info) + self.create_vifs(vm_ref, instance, network_info) self.inject_network_info(instance, network_info, vm_ref) return vm_ref @@ -247,7 +285,15 @@ class VMOps(object): 'architecture': instance.architecture}) def _check_agent_version(): - version = self.get_agent_version(instance) + if instance.os_type == 'windows': + # Windows will generally perform a setup process on first boot + # that can take a couple of minutes and then reboot. So we + # need to be more patient than normal as well as watch for + # domid changes + version = self.get_agent_version(instance, + timeout=FLAGS.windows_version_timeout) + else: + version = self.get_agent_version(instance) if not version: LOG.info(_('No agent version returned by instance')) return @@ -298,6 +344,7 @@ class VMOps(object): _check_agent_version() _inject_files() _set_admin_password() + self.reset_network(instance, vm_ref) return True except Exception, exc: LOG.warn(exc) @@ -307,11 +354,49 @@ class VMOps(object): timer.f = _wait_for_boot - # call to reset network to configure network from xenstore - self.reset_network(instance, vm_ref) - return timer.start(interval=0.5, now=True) + def _handle_spawn_error(self, vdis, spawn_error): + # Extract resource list from spawn_error. + resources = [] + if spawn_error.args: + last_arg = spawn_error.args[-1] + resources = last_arg + if vdis: + for vdi in vdis: + resources.append(dict(vdi_type=vdi['vdi_type'], + vdi_uuid=vdi['vdi_uuid'], + file=None)) + + LOG.debug(_("Resources to remove:%s"), resources) + kernel_file = None + ramdisk_file = None + + for item in resources: + vdi_type = item['vdi_type'] + vdi_to_remove = item['vdi_uuid'] + if vdi_to_remove: + try: + vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', + vdi_to_remove) + LOG.debug(_('Removing VDI %(vdi_ref)s' + + '(uuid:%(vdi_to_remove)s)'), locals()) + VMHelper.destroy_vdi(self._session, vdi_ref) + except self.XenAPI.Failure: + # Vdi has already been deleted + LOG.debug(_("Skipping VDI destroy for %s"), vdi_to_remove) + if item['file']: + # There is also a file to remove. + if vdi_type == ImageType.KERNEL_STR: + kernel_file = item['file'] + elif vdi_type == ImageType.RAMDISK_STR: + ramdisk_file = item['file'] + + if kernel_file or ramdisk_file: + LOG.debug(_("Removing kernel/ramdisk files from dom0")) + self._destroy_kernel_ramdisk_plugin_call(kernel_file, + ramdisk_file) + def _get_vm_opaque_ref(self, instance_or_vm): """ Refactored out the common code of many methods that receive either @@ -386,7 +471,7 @@ class VMOps(object): self._session, instance, template_vdi_uuids, image_id) finally: if template_vm_ref: - self._destroy(instance, template_vm_ref, + self._destroy(instance, template_vm_ref, None, shutdown=False, destroy_kernel_ramdisk=False) logging.debug(_("Finished snapshot and upload for VM %s"), instance) @@ -502,18 +587,43 @@ class VMOps(object): task = self._session.call_xenapi('Async.VM.clean_reboot', vm_ref) self._session.wait_for_task(task, instance.id) - def get_agent_version(self, instance): + def get_agent_version(self, instance, timeout=None): """Get the version of the agent running on the VM instance.""" - # Send the encrypted password - transaction_id = str(uuid.uuid4()) - args = {'id': transaction_id} - resp = self._make_agent_call('version', instance, '', args) - if resp is None: - # No response from the agent - return - resp_dict = json.loads(resp) - return resp_dict['message'] + def _call(): + # Send the encrypted password + transaction_id = str(uuid.uuid4()) + args = {'id': transaction_id} + resp = self._make_agent_call('version', instance, '', args) + if resp is None: + # No response from the agent + return + resp_dict = json.loads(resp) + # Some old versions of the Windows agent have a trailing \\r\\n + # (ie CRLF escaped) for some reason. Strip that off. + return resp_dict['message'].replace('\\r\\n', '') + + if timeout: + vm_ref = self._get_vm_opaque_ref(instance) + vm_rec = self._session.get_xenapi().VM.get_record(vm_ref) + + domid = vm_rec['domid'] + + expiration = time.time() + timeout + while time.time() < expiration: + ret = _call() + if ret: + return ret + + vm_rec = self._session.get_xenapi().VM.get_record(vm_ref) + if vm_rec['domid'] != domid: + LOG.info(_('domid changed from %(olddomid)s to ' + '%(newdomid)s') % { + 'olddomid': domid, + 'newdomid': vm_rec['domid']}) + domid = vm_rec['domid'] + else: + return _call() def agent_update(self, instance, url, md5sum): """Update agent on the VM instance.""" @@ -556,9 +666,13 @@ class VMOps(object): # There was some sort of error; the message will contain # a description of the error. raise RuntimeError(resp_dict['message']) - agent_pub = int(resp_dict['message']) + # Some old versions of the Windows agent have a trailing \\r\\n + # (ie CRLF escaped) for some reason. Strip that off. + agent_pub = int(resp_dict['message'].replace('\\r\\n', '')) dh.compute_shared(agent_pub) - enc_pass = dh.encrypt(new_pass) + # Some old versions of Linux and Windows agent expect trailing \n + # on password to work correctly. + enc_pass = dh.encrypt(new_pass + '\n') # Send the encrypted password password_transaction_id = str(uuid.uuid4()) password_args = {'id': password_transaction_id, 'enc_pass': enc_pass} @@ -666,6 +780,16 @@ class VMOps(object): VMHelper.unplug_vbd(self._session, vbd_ref) VMHelper.destroy_vbd(self._session, vbd_ref) + def _destroy_kernel_ramdisk_plugin_call(self, kernel, ramdisk): + args = {} + if kernel: + args['kernel-file'] = kernel + if ramdisk: + args['ramdisk-file'] = ramdisk + task = self._session.async_call_plugin( + 'glance', 'remove_kernel_ramdisk', args) + self._session.wait_for_task(task) + def _destroy_kernel_ramdisk(self, instance, vm_ref): """Three situations can occur: @@ -695,13 +819,7 @@ class VMOps(object): (kernel, ramdisk) = VMHelper.lookup_kernel_ramdisk(self._session, vm_ref) - LOG.debug(_("Removing kernel/ramdisk files")) - - args = {'kernel-file': kernel, 'ramdisk-file': ramdisk} - task = self._session.async_call_plugin( - 'glance', 'remove_kernel_ramdisk', args) - self._session.wait_for_task(task, instance.id) - + self._destroy_kernel_ramdisk_plugin_call(kernel, ramdisk) LOG.debug(_("kernel/ramdisk files removed")) def _destroy_vm(self, instance, vm_ref): @@ -723,7 +841,7 @@ class VMOps(object): self._session.call_xenapi("Async.VM.destroy", rescue_vm_ref) - def destroy(self, instance): + def destroy(self, instance, network_info): """Destroy VM instance. This is the method exposed by xenapi_conn.destroy(). The rest of the @@ -733,9 +851,9 @@ class VMOps(object): instance_id = instance.id LOG.info(_("Destroying VM for Instance %(instance_id)s") % locals()) vm_ref = VMHelper.lookup(self._session, instance.name) - return self._destroy(instance, vm_ref, shutdown=True) + return self._destroy(instance, vm_ref, network_info, shutdown=True) - def _destroy(self, instance, vm_ref, shutdown=True, + def _destroy(self, instance, vm_ref, network_info, shutdown=True, destroy_kernel_ramdisk=True): """Destroys VM instance by performing: @@ -757,6 +875,10 @@ class VMOps(object): self._destroy_kernel_ramdisk(instance, vm_ref) self._destroy_vm(instance, vm_ref) + if network_info: + for (network, mapping) in network_info: + self.vif_driver.unplug(instance, network, mapping) + def _wait_with_callback(self, instance_id, task, callback): ret = None try: @@ -900,76 +1022,44 @@ class VMOps(object): # TODO: implement this! return 'http://fakeajaxconsole/fake_url' - # TODO(tr3buchet) - remove this function after nova multi-nic - def _get_network_info(self, instance): - """Creates network info list for instance.""" - admin_context = context.get_admin_context() - ips = db.fixed_ip_get_all_by_instance(admin_context, - instance['id']) - networks = db.network_get_all_by_instance(admin_context, - instance['id']) - - inst_type = db.instance_type_get_by_id(admin_context, - instance['instance_type_id']) - - network_info = [] - for network in networks: - network_ips = [ip for ip in ips if ip.network_id == network.id] - - def ip_dict(ip): - return { - "ip": ip.address, - "netmask": network["netmask"], - "enabled": "1"} - - def ip6_dict(): - return { - "ip": ipv6.to_global(network['cidr_v6'], - instance['mac_address'], - instance['project_id']), - "netmask": network['netmask_v6'], - "enabled": "1"} - - info = { - 'label': network['label'], - 'gateway': network['gateway'], - 'broadcast': network['broadcast'], - 'mac': instance.mac_address, - 'rxtx_cap': inst_type['rxtx_cap'], - 'dns': [network['dns']], - 'ips': [ip_dict(ip) for ip in network_ips]} - if network['cidr_v6']: - info['ip6s'] = [ip6_dict()] - if network['gateway_v6']: - info['gateway6'] = network['gateway_v6'] - network_info.append((network, info)) - return network_info - - #TODO{tr3buchet) remove this shim with nova-multi-nic - def inject_network_info(self, instance, network_info=None, vm_ref=None): - """ - shim in place which makes inject_network_info work without being - passed network_info. - shim goes away after nova-multi-nic + def set_host_enabled(self, host, enabled): + """Sets the specified host's ability to accept new instances.""" + args = {"enabled": json.dumps(enabled)} + json_resp = self._call_xenhost("set_host_enabled", args) + resp = json.loads(json_resp) + return resp["status"] + + def _call_xenhost(self, method, arg_dict): + """There will be several methods that will need this general + handling for interacting with the xenhost plugin, so this abstracts + out that behavior. """ - if not network_info: - network_info = self._get_network_info(instance) - self._inject_network_info(instance, network_info, vm_ref) + # Create a task ID as something that won't match any instance ID + task_id = random.randint(-80000, -70000) + try: + task = self._session.async_call_plugin("xenhost", method, + args=arg_dict) + #args={"params": arg_dict}) + ret = self._session.wait_for_task(task, task_id) + except self.XenAPI.Failure as e: + ret = None + LOG.error(_("The call to %(method)s returned an error: %(e)s.") + % locals()) + return ret - def _inject_network_info(self, instance, network_info, vm_ref=None): + def inject_network_info(self, instance, network_info, vm_ref=None): """ Generate the network info and make calls to place it into the xenstore and the xenstore param list. vm_ref can be passed in because it will sometimes be different than what VMHelper.lookup(session, instance.name) will find (ex: rescue) """ - logging.debug(_("injecting network info to xs for vm: |%s|"), vm_ref) - if vm_ref: # this function raises if vm_ref is not a vm_opaque_ref self._session.get_xenapi().VM.get_record(vm_ref) else: vm_ref = VMHelper.lookup(self._session, instance.name) + logging.debug(_("injecting network info to xs for vm: |%s|"), vm_ref) for (network, info) in network_info: location = 'vm-data/networking/%s' % info['mac'].replace(':', '') @@ -984,22 +1074,28 @@ class VMOps(object): # catch KeyError for domid if instance isn't running pass - def create_vifs(self, vm_ref, network_info): + def create_vifs(self, vm_ref, instance, network_info): """Creates vifs for an instance.""" + logging.debug(_("creating vif(s) for vm: |%s|"), vm_ref) # this function raises if vm_ref is not a vm_opaque_ref self._session.get_xenapi().VM.get_record(vm_ref) for device, (network, info) in enumerate(network_info): - mac_address = info['mac'] - bridge = network['bridge'] - rxtx_cap = info.pop('rxtx_cap') - network_ref = \ - NetworkHelper.find_network_with_bridge(self._session, - bridge) - VMHelper.create_vif(self._session, vm_ref, network_ref, - mac_address, device, rxtx_cap) + vif_rec = self.vif_driver.plug(self._session, + vm_ref, instance, device, network, info) + network_ref = vif_rec['network'] + LOG.debug(_('Creating VIF for VM %(vm_ref)s,' \ + ' network %(network_ref)s.') % locals()) + vif_ref = self._session.call_xenapi('VIF.create', vif_rec) + LOG.debug(_('Created VIF %(vif_ref)s for VM %(vm_ref)s,' + ' network %(network_ref)s.') % locals()) + + def plug_vifs(instance, network_info): + """Set up VIF networking on the host.""" + for (network, mapping) in network_info: + self.vif_driver.plug(self._session, instance, network, mapping) def reset_network(self, instance, vm_ref=None): """Creates uuid arg to pass to make_agent_call and calls it.""" diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 5fcec1715..7c355a55b 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -194,23 +194,23 @@ class XenAPIConnection(driver.ComputeDriver): def list_instances_detail(self): return self._vmops.list_instances_detail() - def spawn(self, instance, network_info=None, block_device_mapping=None): + def spawn(self, instance, network_info, block_device_mapping=None): """Create VM instance""" - self._vmops.spawn(instance) + self._vmops.spawn(instance, network_info) def revert_resize(self, instance): """Reverts a resize, powering back on the instance""" self._vmops.revert_resize(instance) - def finish_resize(self, instance, disk_info): + def finish_resize(self, instance, disk_info, network_info): """Completes a resize, turning on the migrated instance""" - self._vmops.finish_resize(instance, disk_info) + self._vmops.finish_resize(instance, disk_info, network_info) def snapshot(self, instance, image_id): """ Create snapshot from a running VM instance """ self._vmops.snapshot(instance, image_id) - def reboot(self, instance): + def reboot(self, instance, network_info): """Reboot VM instance""" self._vmops.reboot(instance) @@ -224,9 +224,9 @@ class XenAPIConnection(driver.ComputeDriver): """ self._vmops.inject_file(instance, b64_path, b64_contents) - def destroy(self, instance): + def destroy(self, instance, network_info): """Destroy VM instance""" - self._vmops.destroy(instance) + self._vmops.destroy(instance, network_info) def pause(self, instance, callback): """Pause VM instance""" @@ -249,11 +249,11 @@ class XenAPIConnection(driver.ComputeDriver): """resume the specified instance""" self._vmops.resume(instance, callback) - def rescue(self, instance, callback): + def rescue(self, instance, callback, network_info): """Rescue the specified instance""" self._vmops.rescue(instance, callback) - def unrescue(self, instance, callback): + def unrescue(self, instance, callback, network_info): """Unrescue the specified instance""" self._vmops.unrescue(instance, callback) @@ -265,9 +265,12 @@ class XenAPIConnection(driver.ComputeDriver): """reset networking for specified instance""" self._vmops.reset_network(instance) - def inject_network_info(self, instance): + def inject_network_info(self, instance, network_info): """inject network info for specified instance""" - self._vmops.inject_network_info(instance) + self._vmops.inject_network_info(instance, network_info) + + def plug_vifs(self, instance_ref, network_info): + self._vmops.plug_vifs(instance_ref, network_info) def get_info(self, instance_id): """Return data about VM instance""" @@ -322,7 +325,7 @@ class XenAPIConnection(driver.ComputeDriver): """This method is supported only by libvirt.""" return - def unfilter_instance(self, instance_ref): + def unfilter_instance(self, instance_ref, network_info): """This method is supported only by libvirt.""" raise NotImplementedError('This method is supported only by libvirt.') @@ -336,6 +339,10 @@ class XenAPIConnection(driver.ComputeDriver): True, run the update first.""" return self.HostState.get_host_stats(refresh=refresh) + def set_host_enabled(self, host, enabled): + """Sets the specified host's ability to accept new instances.""" + return self._vmops.set_host_enabled(host, enabled) + class XenAPISession(object): """The session to invoke XenAPI SDK calls""" diff --git a/nova/volume/api.py b/nova/volume/api.py index 7d27abff9..52b3a9fed 100644 --- a/nova/volume/api.py +++ b/nova/volume/api.py @@ -52,7 +52,7 @@ class API(base.Base): if quota.allowed_volumes(context, 1, size) < 1: pid = context.project_id - LOG.warn(_("Quota exceeeded for %(pid)s, tried to create" + LOG.warn(_("Quota exceeded for %(pid)s, tried to create" " %(size)sG volume") % locals()) raise quota.QuotaError(_("Volume quota exceeded. You cannot " "create a volume of size %sG") % size) @@ -140,9 +140,10 @@ class API(base.Base): {"method": "remove_volume", "args": {'volume_id': volume_id}}) - def create_snapshot(self, context, volume_id, name, description): + def _create_snapshot(self, context, volume_id, name, description, + force=False): volume = self.get(context, volume_id) - if volume['status'] != "available": + if ((not force) and (volume['status'] != "available")): raise exception.ApiError(_("Volume status must be available")) options = { @@ -164,6 +165,14 @@ class API(base.Base): "snapshot_id": snapshot['id']}}) return snapshot + def create_snapshot(self, context, volume_id, name, description): + return self._create_snapshot(context, volume_id, name, description, + False) + + def create_snapshot_force(self, context, volume_id, name, description): + return self._create_snapshot(context, volume_id, name, description, + True) + def delete_snapshot(self, context, snapshot_id): snapshot = self.get_snapshot(context, snapshot_id) if snapshot['status'] != "available": diff --git a/nova/wsgi.py b/nova/wsgi.py index 23d29079f..eae3afcb4 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -67,6 +67,7 @@ class Server(object): self.host = host or "0.0.0.0" self.port = port or 0 self._server = None + self._tcp_server = None self._socket = None self._pool = eventlet.GreenPool(pool_size or self.default_pool_size) self._logger = logging.getLogger("eventlet.wsgi.server") @@ -106,6 +107,17 @@ class Server(object): """ LOG.info(_("Stopping WSGI server.")) self._server.kill() + if self._tcp_server is not None: + LOG.info(_("Stopping raw TCP server.")) + self._tcp_server.kill() + + def start_tcp(self, listener, port, host='0.0.0.0', key=None, backlog=128): + """Run a raw TCP server with the given application.""" + arg0 = sys.argv[0] + LOG.info(_('Starting TCP server %(arg0)s on %(host)s:%(port)s') + % locals()) + socket = eventlet.listen((host, port), backlog=backlog) + self._tcp_server = self._pool.spawn_n(self._run_tcp, listener, socket) def wait(self): """Block, until the server has stopped. @@ -120,6 +132,15 @@ class Server(object): except greenlet.GreenletExit: LOG.info(_("WSGI server has stopped.")) + def _run_tcp(self, listener, socket): + """Start a raw TCP server in a new green thread.""" + while True: + try: + new_sock, address = socket.accept() + self._pool.spawn_n(listener, new_sock) + except (SystemExit, KeyboardInterrupt): + pass + class Request(webob.Request): pass |
