diff --git a/ec2api/api/cloud.py b/ec2api/api/cloud.py index 5f473590..11edcd58 100644 --- a/ec2api/api/cloud.py +++ b/ec2api/api/cloud.py @@ -1005,9 +1005,12 @@ class CloudController(object): private_ip_address) def run_instances(self, context, image_id, min_count, max_count, - subnet_id=None, private_ip_address=None, - network_interface=None, security_group=None, - security_group_id=None, **kwargs): + key_name=None, security_group_id=None, + security_group=None, user_data=None, instance_type=None, + placement=None, kernel_id=None, ramdisk_id=None, + block_device_mapping=None, subnet_id=None, + private_ip_address=None, client_token=None, + network_interface=None, **kwargs): """Launches the specified number of instances using an AMI. Args: @@ -1021,10 +1024,33 @@ class CloudController(object): If you specify more instances than EC2 can launch in the target Availability Zone, EC2 launches the largest possible number of instances above max_count. + key_name (str): The name of the key pair. + security_group_id (list of str): One or more security group IDs. + security_group (list of str): One or more security group names. + For VPC mode, you must use security_group_id. + user_data (str): Base64-encoded MIME user data for the instances. + instance_type (str): The instance type. + placement (dict): Dict can contain: + availability_zone (str): Availability Zone for the instance. + kernel_id (str): The ID of the kernel. + ramdisk_id (str): The ID of the RAM disk. + block_device_mapping (list of dict): Dict can contain: + device_name (str): The device name exposed to the instance + (for example, /dev/sdh or xvdh). + virtual_name (str): The virtual device name (ephemeral[0..3]). + ebs (dict): Dict can contain: + volume_id (str): The ID of the volume (Nova extension). + snapshot_id (str): The ID of the snapshot. + volume_size (str): The size of the volume, in GiBs. + delete_on_termination (bool): Indicates whether to delete + the volume on instance termination. + no_device (str): Suppresses the device mapping. subnet_id (str): The ID of the subnet to launch the instance into. private_ip_address (str): The primary IP address. You must specify a value from the IP address range of the subnet. + client_token (str): Unique, case-sensitive identifier you provide + to ensure idempotency of the request. network_interface (list of dicts): Dict can contain: network_interface_id (str): An existing interface to attach to a single instance. Requires n=1 instances. @@ -1053,9 +1079,6 @@ class CloudController(object): IP address using private_ip_address. associate_public_ip_address (boolean): Indicates whether to assign a public IP address to an instance in a VPC. - security_group (list of str): One or more security group names. - For a nondefault VPC, you must use security_group_id. - security_group_id (list of str): One or more security group IDs. kwargs: Other arguments supported by AWS EC2. Returns: @@ -1065,9 +1088,12 @@ class CloudController(object): uses the default security group. """ return instance.run_instances(context, image_id, min_count, max_count, - subnet_id, private_ip_address, - network_interface, security_group, - security_group_id, **kwargs) + key_name, security_group_id, + security_group, user_data, instance_type, + placement, kernel_id, ramdisk_id, + block_device_mapping, subnet_id, + private_ip_address, client_token, + network_interface, **kwargs) def terminate_instances(self, context, instance_id): """Shuts down one or more instances. diff --git a/ec2api/api/ec2utils.py b/ec2api/api/ec2utils.py index 4d1a8b15..7729a907 100644 --- a/ec2api/api/ec2utils.py +++ b/ec2api/api/ec2utils.py @@ -175,6 +175,16 @@ def is_ec2_timestamp_expired(request, expires=None): return True +def id_to_glance_id(context, image_id): + """Convert an internal (db) id to a glance id.""" + return novadb.s3_image_get(context, image_id)['uuid'] + + +def ec2_id_to_glance_id(context, ec2_id): + image_id = ec2_id_to_id(ec2_id) + return id_to_glance_id(context, image_id) + + # TODO(Alex) This function is copied as is from original cloud.py. It doesn't # check for the prefix which allows any prefix used for any object. def ec2_id_to_id(ec2_id): @@ -221,6 +231,31 @@ def get_int_id_from_instance_uuid(context, instance_uuid): return novadb.ec2_instance_create(context, instance_uuid)['id'] +def get_volume_uuid_from_int_id(context, int_id): + return novadb.get_volume_uuid_by_ec2_id(context, int_id) + + +def ec2_vol_id_to_uuid(ec2_id): + """Get the corresponding UUID for the given ec2-id.""" + ctxt = context.get_admin_context() + + # NOTE(jgriffith) first strip prefix to get just the numeric + int_id = ec2_id_to_id(ec2_id) + return get_volume_uuid_from_int_id(ctxt, int_id) + + +def get_snapshot_uuid_from_int_id(context, int_id): + return novadb.get_snapshot_uuid_by_ec2_id(context, int_id) + + +def ec2_snap_id_to_uuid(ec2_id): + """Get the corresponding UUID for the given ec2-id.""" + ctxt = context.get_admin_context() + + # NOTE(jgriffith) first strip prefix to get just the numeric + int_id = ec2_id_to_id(ec2_id) + return get_snapshot_uuid_from_int_id(ctxt, int_id) + # NOTE(ft): extra functions to use in vpc specific code or instead of # malformed existed functions diff --git a/ec2api/api/instance.py b/ec2api/api/instance.py index 84bf3a42..647d93bd 100644 --- a/ec2api/api/instance.py +++ b/ec2api/api/instance.py @@ -15,6 +15,8 @@ import collections import copy +import random +import re from ec2api.api import clients from ec2api.api import ec2client @@ -35,9 +37,12 @@ from ec2api.openstack.common import timeutils def run_instances(context, image_id, min_count, max_count, - subnet_id=None, private_ip_address=None, - network_interface=None, security_group=None, - security_group_id=None, **kwargs): + key_name=None, security_group_id=None, + security_group=None, user_data=None, instance_type=None, + placement=None, kernel_id=None, ramdisk_id=None, + block_device_mapping=None, subnet_id=None, + private_ip_address=None, client_token=None, + network_interface=None, **kwargs): # TODO(ft): fix passing complex network parameters create_network_interface # TODO(ft): check the compatibility of complex network parameters and # multiple running @@ -45,7 +50,23 @@ def run_instances(context, image_id, min_count, max_count, # network interface params function _check_min_max_count(min_count, max_count) - (ec2_security_groups, + # TODO(ft): support client tokens + + os_image, os_kernel_id, os_ramdisk_id = _parse_image_parameters( + context, image_id, kernel_id, ramdisk_id) + + nova = clients.nova(context) + os_flavor = next((f for f in nova.flavors.list() + if f.name == instance_type), None) + if not os_flavor: + raise exception.InvalidParameterValue(value=instance_type, + parameter='InstanceType') + + bdm = _parse_block_device_mapping(block_device_mapping, os_image) + + # TODO(ft): support auto_assign_floating_ip + + (security_groups_names, vpc_network_parameters) = _merge_network_interface_parameters( security_group, subnet_id, private_ip_address, security_group_id, @@ -98,34 +119,44 @@ def run_instances(context, image_id, min_count, max_count, ec2 = ec2client.ec2client(context) # NOTE(ft): run instances one by one using created ports - ec2_instance_network_pairs = [] + network_interfaces_by_instances = {} + ec2_instance_ids = [] for network_interfaces in instance_network_interfaces: - arg_network_interfaces = [{'network_interface_id': eni['os_id']} - for eni in network_interfaces] - ec2_reservation = ec2.run_instances( - image_id=image_id, - min_count=1, max_count=1, - network_interface=arg_network_interfaces, - security_group=ec2_security_groups, - **kwargs) - ec2_instance = ec2_reservation['instancesSet'][0] + nics = [{'port-id': eni['os_id']} for eni in network_interfaces] + os_instance = nova.servers.create( + 'EC2 server', os_image.id, os_flavor, + min_count=1, max_count=1, + kernel_id=os_kernel_id, ramdisk_id=os_ramdisk_id, + availability_zone=(placement or {}).get('availability_zone'), + block_device_mapping=bdm, + security_group=security_groups_names, + nics=nics, + key_name=key_name, userdata=user_data) + + ec2_instance_id = ec2utils.id_to_ec2_inst_id(os_instance.id) cleaner.addCleanup(ec2.terminate_instances, - instance_id=ec2_instance['instanceId']) - ec2_instance_network_pairs.append((ec2_instance, - network_interfaces,)) + instance_id=ec2_instance_id) + nova.servers.update(os_instance, name=ec2_instance_id) + + network_interfaces_by_instances[ec2_instance_id] = ( + network_interfaces) + ec2_instance_ids.append(ec2_instance_id) # TODO(ft): receive port from a create_network_interface sub-function os_ports = neutron.list_ports()['ports'] os_ports = dict((p['id'], p) for p in os_ports) + ec2_instances = ec2.describe_instances(instance_id=ec2_instance_ids) + ec2_instances = [i for r in ec2_instances['reservationSet'] + for i in r['instancesSet']] attach_time = timeutils.isotime(None, True) # TODO(ft): Process min and max counts on running errors accordingly to # their meanings. Correct error messages are also critical - ec2_instances = [] - for ec2_instance, network_interfaces in ec2_instance_network_pairs: + for ec2_instance in ec2_instances: instance_ports_info = [] instance_id = ec2utils.ec2_id_to_id(ec2_instance['instanceId']) delete_on_termination = iter(delete_on_termination_flags) - for network_interface in network_interfaces: + for network_interface in network_interfaces_by_instances[ + ec2_instance['instanceId']]: # TODO(ft): implement update items in DB layer to prevent # record by record modification # Alternatively a create_network_interface sub-function can @@ -141,17 +172,16 @@ def run_instances(context, image_id, min_count, max_count, os_port = os_ports[network_interface['os_id']] instance_ports_info.append((network_interface, os_port, [],)) - _format_instance(context, ec2_instance, instance_ports_info, - security_groups) - ec2_instances.append(ec2_instance) + _format_instance(context, ec2_instance, + instance_ports_info, security_groups) # TODO(ft): since we run instances separately each instance has its # own ec2_reservation id. Now we return ec2_reservation id of # the last started instance # If we aren't able to update OpenStack to fit ec2 requirements, # we should have our own ec2_reservation id to use it instead of Nova's. - ec2_reservation['instancesSet'] = ec2_instances - return ec2_reservation + ec2_reservation_id = _generate_reservation_id() + return _format_reservation(context, ec2_reservation_id, ec2_instances) def terminate_instances(context, instance_id): @@ -267,7 +297,20 @@ def _format_instance(context, ec2_instance, ports_info, security_groups): return ec2_instance +def _format_reservation(context, ec2_reservation_id, ec2_instances): + return {'reservationId': ec2_reservation_id, + 'ownerId': context.project_id, + 'instancesSet': ec2_instances, + # TODO(ft): Check AWS behavior: can it start zero instances with + # successfull result? + 'groupSet': ec2_instances[0].get('groupSet')} + + def _check_min_max_count(min_count, max_count): + # TODO(ft): figure out appropriate aws message and use them + min_count = int(min_count) + max_count = int(max_count) + if min_count < 1: msg = _('Minimum instance count must be greater than zero') raise exception.InvalidParameterValue(msg) @@ -280,6 +323,52 @@ def _check_min_max_count(min_count, max_count): raise exception.InvalidParameterValue(msg) +def _parse_image_parameters(context, image_id, kernel_id, ramdisk_id): + glance = clients.glance(context) + if kernel_id: + os_kernel_id = ec2utils.ec2_id_to_glance_id(context, kernel_id) + glance.images.get(os_kernel_id) + if ramdisk_id: + os_ramdisk_id = ec2utils.ec2_id_to_glance_id(context, ramdisk_id) + glance.images.get(os_ramdisk_id) + os_image_id = ec2utils.ec2_id_to_glance_id(context, image_id) + os_image = glance.images.get(os_image_id) + + if _cloud_get_image_state(os_image) != 'available': + # TODO(ft): Change the message with the real AWS message + msg = _('Image must be available') + raise exception.ImageNotActive(message=msg) + + return os_image, kernel_id, ramdisk_id + + +def _parse_block_device_mapping(block_device_mapping, os_image): + # NOTE(ft): The following code allows reconfiguration of devices + # according to list of new parameters supplied in EC2 call. + # This code merges these parameters with information taken from image. + image_root_device_name = os_image.properties.get('root_device_name') + image_bdm = dict( + (_block_device_strip_dev(bd.get('device_name') or + image_root_device_name), + bd) + for bd in os_image.properties.get('block_device_mapping', []) + if bd.get('device_name') or bd.get('boot_index') == 0) + + for args_bd in (block_device_mapping or []): + _cloud_parse_block_device_mapping(args_bd) + dev_name = _block_device_strip_dev(args_bd.get('device_name')) + if (not dev_name or dev_name not in image_bdm or + 'snapshot_id' in args_bd or 'volume_id' in args_bd): + continue + image_bd = image_bdm[dev_name] + for key in ('device_name', 'delete_on_termination', 'virtual_name', + 'snapshot_id', 'volume_id', 'volume_size', + 'no_device'): + args_bd[key] = args_bd.get(key, image_bd.get(key)) + + return block_device_mapping + + def _merge_network_interface_parameters(security_group_names, subnet_id, private_ip_address, @@ -441,3 +530,57 @@ def _create_network_interfaces(context, cleaner, params): network_interfaces.append(network_interface) return network_interfaces + + +# NOTE(ft): following functions are copied from various parts of Nova + +_dev = re.compile('^/dev/') + + +def _block_device_strip_dev(device_name): + """remove leading '/dev/'.""" + return _dev.sub('', device_name) if device_name else device_name + + +def _cloud_parse_block_device_mapping(bdm): + """Parse BlockDeviceMappingItemType into flat hash + + BlockDevicedMapping..DeviceName + BlockDevicedMapping..Ebs.SnapshotId + BlockDevicedMapping..Ebs.VolumeSize + BlockDevicedMapping..Ebs.DeleteOnTermination + BlockDevicedMapping..Ebs.NoDevice + BlockDevicedMapping..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: + if ec2_id.startswith('snap-'): + bdm['snapshot_id'] = ec2utils.ec2_snap_id_to_uuid(ec2_id) + elif ec2_id.startswith('vol-'): + bdm['volume_id'] = ec2utils.ec2_vol_id_to_uuid(ec2_id) + else: + # NOTE(ft): AWS returns undocumented InvalidSnapshotID.NotFound + raise exception.InvalidSnapshotIDMalformed(snapshot_id=ec2_id) + ebs.setdefault('delete_on_termination', True) + bdm.update(ebs) + return bdm + + +def _utils_generate_uid(topic, size=8): + characters = '01234567890abcdefghijklmnopqrstuvwxyz' + choices = [random.choice(characters) for _x in xrange(size)] + return '%s-%s' % (topic, ''.join(choices)) + + +def _generate_reservation_id(): + return _utils_generate_uid('r') + + +def _cloud_get_image_state(image): + state = image.status + if state == 'active': + state = 'available' + return image.properties.get('image_state', state) diff --git a/ec2api/exception.py b/ec2api/exception.py index d521a5c4..96c79819 100644 --- a/ec2api/exception.py +++ b/ec2api/exception.py @@ -142,6 +142,21 @@ class EC2NotFound(NotFound): code = 400 +class ImageNotFound(EC2NotFound): + ec2_code = 'InvalidAMIID.NotFound' + msg_fmt = _("The image id '[%(image_id)s]' does not exist") + + +class VolumeNotFound(NotFound): + ec2_code = 'InvalidVolume.NotFound' + msg_fmt = _("Volume %(volume_id)s could not be found.") + + +class SnapshotNotFound(NotFound): + ec2_code = 'InvalidSnapshot.NotFound' + msg_fmt = _("Snapshot %(snapshot_id)s could not be found.") + + class InstanceNotFound(EC2NotFound): ec2_code = 'InvalidInstanceID.NotFound' msg_fmt = _("Instance %(instance_id)s could not be found.") @@ -300,3 +315,15 @@ class NetworkInterfaceLimitExceeded(Invalid): # TODO(Alex) Change next class with the real AWS exception class RuleAlreadyExists(Invalid): msg_fmt = _('The rule already exists.') + + +class ImageNotActive(Invalid): + ec2_code = 'InvalidAMIID.Unavailable' + # TODO(ft): Change the message with the real AWS message + msg_fmt = _("Image %(image_id)s is not active.") + + +class InvalidSnapshotIDMalformed(Invalid): + ec2_code = 'InvalidSnapshotID.Malformed' + # TODO(ft): Change the message with the real AWS message + msg_fmg = _('The snapshot %(snapshot_id)s ID is not valid') diff --git a/ec2api/novadb/api.py b/ec2api/novadb/api.py index 4f1b01dc..c50d1932 100644 --- a/ec2api/novadb/api.py +++ b/ec2api/novadb/api.py @@ -80,6 +80,25 @@ MAX_INT = 0x7FFFFFFF #################### +def s3_image_get(context, image_id): + """Find local s3 image represented by the provided id.""" + return IMPL.s3_image_get(context, image_id) + + +################### + + +def get_volume_uuid_by_ec2_id(context, ec2_id): + return IMPL.get_volume_uuid_by_ec2_id(context, ec2_id) + + +def get_snapshot_uuid_by_ec2_id(context, ec2_id): + return IMPL.get_snapshot_uuid_by_ec2_id(context, ec2_id) + + +################### + + def get_ec2_instance_id_by_uuid(context, instance_id): """Get ec2 id through uuid from instance_id_mappings table.""" return IMPL.get_ec2_instance_id_by_uuid(context, instance_id) diff --git a/ec2api/novadb/sqlalchemy/api.py b/ec2api/novadb/sqlalchemy/api.py index 95bede08..4aff4330 100644 --- a/ec2api/novadb/sqlalchemy/api.py +++ b/ec2api/novadb/sqlalchemy/api.py @@ -163,9 +163,52 @@ def model_query(context, model, *args, **kwargs): return query +#################### + + +def s3_image_get(context, image_id): + """Find local s3 image represented by the provided id.""" + result = (model_query(context, models.S3Image, read_deleted="yes"). + filter_by(id=image_id). + first()) + + if not result: + raise exception.ImageNotFound(image_id=image_id) + + return result + + ################## +@require_context +def get_volume_uuid_by_ec2_id(context, ec2_id): + result = (model_query(context, models.VolumeIdMapping, read_deleted='yes'). + filter_by(id=ec2_id). + first()) + + if not result: + raise exception.VolumeNotFound(volume_id=ec2_id) + + return result['uuid'] + + +@require_context +def get_snapshot_uuid_by_ec2_id(context, ec2_id): + result = (model_query(context, models.SnapshotIdMapping, + read_deleted='yes'). + filter_by(id=ec2_id). + first()) + + if not result: + raise exception.SnapshotNotFound(snapshot_id=ec2_id) + + return result['uuid'] + + +################### + + @require_context def ec2_instance_create(context, instance_uuid, id=None): """Create ec2 compatible instance by provided uuid.""" diff --git a/ec2api/novadb/sqlalchemy/models.py b/ec2api/novadb/sqlalchemy/models.py index 3f7f4c6e..ef717be5 100644 --- a/ec2api/novadb/sqlalchemy/models.py +++ b/ec2api/novadb/sqlalchemy/models.py @@ -49,6 +49,30 @@ class NovaBase(models.SoftDeleteMixin, super(NovaBase, self).save(session=session) +class S3Image(BASE, NovaBase): + """Compatibility layer for the S3 image service talking to Glance.""" + __tablename__ = 's3_images' + __table_args__ = () + id = Column(Integer, primary_key=True, nullable=False, autoincrement=True) + uuid = Column(String(36), nullable=False) + + +class VolumeIdMapping(BASE, NovaBase): + """Compatibility layer for the EC2 volume service.""" + __tablename__ = 'volume_id_mappings' + __table_args__ = () + id = Column(Integer, primary_key=True, nullable=False, autoincrement=True) + uuid = Column(String(36), nullable=False) + + +class SnapshotIdMapping(BASE, NovaBase): + """Compatibility layer for the EC2 snapshot service.""" + __tablename__ = 'snapshot_id_mappings' + __table_args__ = () + id = Column(Integer, primary_key=True, nullable=False, autoincrement=True) + uuid = Column(String(36), nullable=False) + + class InstanceIdMapping(BASE, NovaBase): """Compatibility layer for the EC2 instance service.""" __tablename__ = 'instance_id_mappings' diff --git a/ec2api/tests/base.py b/ec2api/tests/base.py index 55a33a74..6f3cbbef 100644 --- a/ec2api/tests/base.py +++ b/ec2api/tests/base.py @@ -36,9 +36,11 @@ class ApiTestCase(test_base.BaseTestCase): neutron_patcher = mock.patch('neutronclient.v2_0.client.Client') self.neutron = neutron_patcher.start().return_value self.addCleanup(neutron_patcher.stop) - nova_servers_patcher = mock.patch('novaclient.v1_1.client.Client') - self.nova_servers = nova_servers_patcher.start().return_value.servers - self.addCleanup(nova_servers_patcher.stop) + nova_patcher = mock.patch('novaclient.v1_1.client.Client') + nova_mock = nova_patcher.start() + self.nova_servers = nova_mock.return_value.servers + self.nova_flavors = nova_mock.return_value.flavors + self.addCleanup(nova_patcher.stop) db_api_patcher = mock.patch('ec2api.db.api.IMPL') self.db_api = db_api_patcher.start() self.addCleanup(db_api_patcher.stop) diff --git a/ec2api/tests/fakes.py b/ec2api/tests/fakes.py index 72a82d80..30e677de 100644 --- a/ec2api/tests/fakes.py +++ b/ec2api/tests/fakes.py @@ -160,7 +160,8 @@ ID_EC2_INSTANCE_1 = ec2utils.get_ec2_id(ID_DB_INSTANCE_1, 'i') ID_EC2_INSTANCE_2 = ec2utils.get_ec2_id(ID_DB_INSTANCE_2, 'i') ID_OS_INSTANCE_1 = random_os_id() ID_OS_INSTANCE_2 = random_os_id() - +ID_EC2_RESERVATION_1 = 'r-%s' % random_db_id() +ID_EC2_RESERVATION_2 = 'r-%s' % random_db_id() # DHCP options constants ID_DB_DHCP_OPTIONS_1 = random_db_id() @@ -838,7 +839,9 @@ def gen_ec2_instance(ec2_instance_id, private_ip_address='', return ec2_instance -def gen_ec2_reservation(ec2_instances): +def gen_ec2_reservation(ec2_reservation_id, ec2_instances): """Generate EC2 Reservation dictionary.""" - return {'instancesSet': [inst for inst in ec2_instances], - 'fakeKey': 'fakeValue'} + return {'reservationId': ec2_reservation_id, + 'ownerId': ID_OS_PROJECT, + 'instancesSet': [inst for inst in ec2_instances], + 'groupSet': []} diff --git a/ec2api/tests/test_instance.py b/ec2api/tests/test_instance.py index 6dc2d066..aa7cb9b4 100644 --- a/ec2api/tests/test_instance.py +++ b/ec2api/tests/test_instance.py @@ -13,6 +13,7 @@ # under the License. +import collections import copy import itertools @@ -36,6 +37,28 @@ class InstanceTestCase(base.ApiTestCase): self.create_network_interface = ( create_network_interface_patcher.start()) self.addCleanup(create_network_interface_patcher.stop) + glance_patcher = mock.patch('glanceclient.client.Client') + self.glance = glance_patcher.start().return_value + self.addCleanup(glance_patcher.stop) + ec2_id_to_glance_id_patcher = ( + mock.patch('ec2api.api.ec2utils.ec2_id_to_glance_id')) + self.ec2_id_to_glance_id = ec2_id_to_glance_id_patcher.start() + self.addCleanup(ec2_id_to_glance_id_patcher.stop) + id_to_ec2_inst_id_patcher = ( + mock.patch('ec2api.api.ec2utils.id_to_ec2_inst_id')) + self.id_to_ec2_inst_id = id_to_ec2_inst_id_patcher.start() + self.addCleanup(id_to_ec2_inst_id_patcher.stop) + utils_generate_uid_patcher = ( + mock.patch('ec2api.api.instance._utils_generate_uid')) + self.utils_generate_uid = utils_generate_uid_patcher.start() + self.addCleanup(utils_generate_uid_patcher.stop) + + self.fake_image_class = collections.namedtuple( + 'FakeImage', ['id', 'status', 'properties']) + self.fake_flavor_class = collections.namedtuple( + 'FakeFlavor', ['name']) + self.fake_instance_class = collections.namedtuple( + 'FakeInstance', ['id']) def test_run_instances(self): """Run instance with various network interface settings.""" @@ -48,13 +71,26 @@ class InstanceTestCase(base.ApiTestCase): {'ports': [fakes.OS_PORT_1, fakes.OS_PORT_2]}) self.create_network_interface.return_value = ( {'networkInterface': fakes.EC2_NETWORK_INTERFACE_1}) - self.ec2.run_instances.return_value = ( - fakes.gen_ec2_reservation([fakes.gen_ec2_instance( - fakes.ID_EC2_INSTANCE_1, private_ip_address=None)])) + self.ec2.describe_instances.return_value = { + 'reservationSet': [fakes.gen_ec2_reservation( + fakes.ID_EC2_RESERVATION_1, + [fakes.gen_ec2_instance(fakes.ID_EC2_INSTANCE_1, + private_ip_address=None)])]} self.isotime.return_value = fakes.TIME_ATTACH_NETWORK_INTERFACE + self.id_to_ec2_inst_id.return_value = fakes.ID_EC2_INSTANCE_1 + self.utils_generate_uid.return_value = fakes.ID_EC2_RESERVATION_1 + + self.glance.images.get.return_value = self.fake_image_class( + 'fake_image_id', 'active', {}) + self.ec2_id_to_glance_id.return_value = 'fake_image_id' + fake_flavor = self.fake_flavor_class('fake_flavor') + self.nova_flavors.list.return_value = [fake_flavor] + self.nova_servers.create.return_value = self.fake_instance_class( + fakes.ID_OS_INSTANCE_1) def do_check(params, new_port=True, delete_on_termination=None): - params.update({'ImageId': 'fake_image', + params.update({'ImageId': 'ami-00000001', + 'InstanceType': 'fake_flavor', 'MinCount': '1', 'MaxCount': '1'}) resp = self.execute('RunInstances', params) self.assertEqual(200, resp['status']) @@ -78,25 +114,30 @@ class InstanceTestCase(base.ApiTestCase): ec2_instance_id=fakes.ID_EC2_INSTANCE_1, delete_on_termination=delete_port_on_termination, for_instance_output=True) - expected_reservation = fakes.gen_ec2_reservation([ - fakes.gen_ec2_instance( + expected_reservation = fakes.gen_ec2_reservation( + fakes.ID_EC2_RESERVATION_1, + [fakes.gen_ec2_instance( fakes.ID_EC2_INSTANCE_1, private_ip_address=None, ec2_network_interfaces=[eni])]) self.assertThat(resp, matchers.DictMatches(expected_reservation)) if new_port: self.create_network_interface.assert_called_once_with( mock.ANY, fakes.EC2_SUBNET_1['subnetId']) - self.ec2.run_instances.assert_called_once_with( - image_id='fake_image', + self.nova_servers.create.assert_called_once_with( + 'EC2 server', 'fake_image_id', fake_flavor, min_count=1, max_count=1, + kernel_id=None, ramdisk_id=None, + availability_zone=None, + block_device_mapping=None, security_group=None, - network_interface=[ - {'network_interface_id': fakes.ID_OS_PORT_1}]) + nics=[{'port-id': fakes.ID_OS_PORT_1}], + key_name=None, userdata=None) self.db_api.update_item.assert_called_once_with( mock.ANY, db_attached_eni) self.isotime.assert_called_once_with(None, True) self.create_network_interface.reset_mock() + self.nova_servers.reset_mock() self.ec2.reset_mock() self.db_api.reset_mock() self.isotime.reset_mock() @@ -119,11 +160,14 @@ class InstanceTestCase(base.ApiTestCase): """Run 2 instances at once on 2 subnets in all combinations.""" self._build_multiple_data_model() - ec2os_reservations = [ - fakes.gen_ec2_reservation([ - fakes.gen_ec2_instance(ec2_instance_id, - private_ip_address=None)]) - for ec2_instance_id in self.IDS_EC2_INSTANCE] + self.id_to_ec2_inst_id.side_effect = self.IDS_EC2_INSTANCE + self.utils_generate_uid.return_value = fakes.ID_EC2_RESERVATION_1 + + self.glance.images.get.return_value = self.fake_image_class( + 'fake_image_id', 'active', {}) + self.ec2_id_to_glance_id.return_value = 'fake_image_id' + fake_flavor = self.fake_flavor_class('fake_flavor') + self.nova_flavors.list.return_value = [fake_flavor] ec2_instances = [ fakes.gen_ec2_instance( @@ -133,7 +177,8 @@ class InstanceTestCase(base.ApiTestCase): for ec2_instance_id, eni_pair in zip( self.IDS_EC2_INSTANCE, zip(*[iter(self.EC2_ATTACHED_ENIS)] * 2))] - ec2_reservation = fakes.gen_ec2_reservation(ec2_instances) + ec2_reservation = fakes.gen_ec2_reservation(fakes.ID_EC2_RESERVATION_1, + ec2_instances) fakes_db_items = dict((eni['id'], eni) for eni in self.DB_DETACHED_ENIS) @@ -145,16 +190,24 @@ class InstanceTestCase(base.ApiTestCase): self.create_network_interface.side_effect = ( [{'networkInterface': eni} for eni in self.EC2_DETACHED_ENIS]) - self.ec2.run_instances.side_effect = ( - [copy.deepcopy(r) - for r in ec2os_reservations]) + self.ec2.describe_instances.return_value = { + 'reservationSet': [ + fakes.gen_ec2_reservation( + fakes.ID_EC2_RESERVATION_1, + [fakes.gen_ec2_instance(ec2_instance_id, + private_ip_address=None) + for ec2_instance_id in self.IDS_EC2_INSTANCE])]} + self.nova_servers.create.side_effect = [ + self.fake_instance_class(os_instance_id) + for os_instance_id in self.IDS_OS_INSTANCE] self.neutron.list_ports.return_value = ( {'ports': self.OS_DETACHED_PORTS + [self.OS_FAKE_PORT]}) self.isotime.return_value = fakes.TIME_ATTACH_NETWORK_INTERFACE resp = self.execute( 'RunInstances', - {'ImageId': 'fake_image', + {'ImageId': 'ami-00000001', + 'InstanceType': 'fake_flavor', 'MinCount': '2', 'MaxCount': '2', 'NetworkInterface.1.SubnetId': fakes.ID_EC2_SUBNET_1, @@ -168,13 +221,17 @@ class InstanceTestCase(base.ApiTestCase): self.create_network_interface.assert_has_calls([ mock.call(mock.ANY, ec2_subnet_id) for ec2_subnet_id in self.IDS_EC2_SUBNET_BY_PORT]) - self.ec2.run_instances.assert_has_calls([ - mock.call(image_id='fake_image', - min_count=1, max_count=1, - security_group=None, - network_interface=[ - {'network_interface_id': port_id} - for port_id in port_ids]) + self.nova_servers.create.assert_has_calls([ + mock.call( + 'EC2 server', 'fake_image_id', fake_flavor, + min_count=1, max_count=1, + kernel_id=None, ramdisk_id=None, + availability_zone=None, + block_device_mapping=None, + security_group=None, + nics=[{'port-id': port_id} + for port_id in port_ids], + key_name=None, userdata=None) for port_ids in zip(*[iter(self.IDS_OS_PORT)] * 2)]) self.db_api.update_item.assert_has_calls([ mock.call(mock.ANY, eni) @@ -194,14 +251,27 @@ class InstanceTestCase(base.ApiTestCase): {'ports': [fakes.OS_PORT_1, fakes.OS_PORT_2]}) self.create_network_interface.return_value = ( {'networkInterface': fakes.EC2_NETWORK_INTERFACE_1}) - self.ec2.run_instances.return_value = ( - fakes.gen_ec2_reservation([fakes.gen_ec2_instance( - fakes.ID_EC2_INSTANCE_1, private_ip_address=None)])) + self.ec2.describe_instances.return_value = { + 'reservationSet': [fakes.gen_ec2_reservation( + fakes.ID_EC2_RESERVATION_1, + [fakes.gen_ec2_instance(fakes.ID_EC2_INSTANCE_1, + private_ip_address=None)])]} self.isotime.return_value = fakes.TIME_ATTACH_NETWORK_INTERFACE + self.id_to_ec2_inst_id.return_value = fakes.ID_EC2_INSTANCE_1 + self.utils_generate_uid.return_value = fakes.ID_EC2_RESERVATION_1 + + self.glance.images.get.return_value = self.fake_image_class( + 'fake_image_id', 'active', {}) + self.ec2_id_to_glance_id.return_value = 'fake_image_id' + fake_flavor = self.fake_flavor_class('fake_flavor') + self.nova_flavors.list.return_value = [fake_flavor] + self.nova_servers.create.return_value = self.fake_instance_class( + fakes.ID_OS_INSTANCE_1) format_instance.side_effect = Exception() def do_check(params, new_port=True, delete_on_termination=None): - params.update({'ImageId': 'fake_image', + params.update({'ImageId': 'ami-00000001', + 'InstanceType': 'fake_flavor', 'MinCount': '1', 'MaxCount': '1'}) self.execute('RunInstances', params) @@ -419,11 +489,14 @@ class InstanceTestCase(base.ApiTestCase): is_instance_ip_in_vpc_by_instance=[True, True]): def gen_reservation_set(instances): if separate_reservations: - return [fakes.gen_ec2_reservation([instances[0]]), - fakes.gen_ec2_reservation([instances[1]])] + return [fakes.gen_ec2_reservation( + fakes.ID_EC2_RESERVATION_1, [instances[0]]), + fakes.gen_ec2_reservation( + fakes.ID_EC2_RESERVATION_2, [instances[1]])] else: - return [fakes.gen_ec2_reservation([instances[0], - instances[1]])] + return [fakes.gen_ec2_reservation( + fakes.ID_EC2_RESERVATION_1, [instances[0], + instances[1]])] instances = [fakes.gen_ec2_instance(inst_id, private_ip_address=ip) for inst_id, ip in zip( @@ -634,29 +707,3 @@ class InstanceTestCase(base.ApiTestCase): self.assertIn('device_id', list_ports_kwargs) self.assertEqual(sorted(instance_ids), sorted(list_ports_kwargs['device_id'])) - - -class InstanceIntegrationTestCase(base.ApiTestCase): - - def test_run_instances(self): - self.db_api.get_item_by_id.side_effect = ( - fakes.get_db_api_get_item_by_id( - {fakes.ID_DB_SUBNET_1: fakes.DB_SUBNET_1, - fakes.ID_DB_VPC_1: fakes.DB_VPC_1, - fakes.ID_DB_NETWORK_INTERFACE_1: - fakes.DB_NETWORK_INTERFACE_1})) - self.db_api.add_item.return_value = fakes.DB_NETWORK_INTERFACE_1 - self.neutron.show_subnet.return_value = {'subnet': fakes.OS_SUBNET_1} - self.neutron.create_port.return_value = {'port': fakes.OS_PORT_1} - self.neutron.list_ports.return_value = {'ports': [fakes.OS_PORT_1]} - self.ec2.run_instances.return_value = ( - fakes.gen_ec2_reservation([fakes.gen_ec2_instance( - fakes.ID_EC2_INSTANCE_1, private_ip_address=None)])) - self.isotime.return_value = fakes.TIME_ATTACH_NETWORK_INTERFACE - - resp = self.execute('RunInstances', - {'ImageId': 'fake_image', - 'MinCount': '1', 'MaxCount': '1', - 'SubnetId': fakes.ID_EC2_SUBNET_1}) - - self.assertEqual(200, resp['status']) diff --git a/requirements.txt b/requirements.txt index b367dc72..6688e6e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ PasteDeploy>=1.5.0 pbr>=0.6,!=0.7,<1.0 pyasn1 python-keystoneclient>=0.9.0 +python-glanceclient>=0.14.0 python-neutronclient>=2.3.6,<3 python-novaclient>=2.17.0 Routes>=1.12.3,!=2.0