diff --git a/ec2api/api/instance.py b/ec2api/api/instance.py index fae205f4..3160718e 100644 --- a/ec2api/api/instance.py +++ b/ec2api/api/instance.py @@ -298,16 +298,20 @@ def describe_instances(context, instance_id=None, filter=None, def reboot_instances(context, instance_id): return _foreach_instance(context, instance_id, + (vm_states_ALLOW_SOFT_REBOOT + + vm_states_ALLOW_HARD_REBOOT), lambda instance: instance.reboot()) def stop_instances(context, instance_id, force=False): return _foreach_instance(context, instance_id, + [vm_states_ACTIVE, vm_states_RESCUED, + vm_states_ERROR], lambda instance: instance.stop()) def start_instances(context, instance_id): - return _foreach_instance(context, instance_id, + return _foreach_instance(context, instance_id, [vm_states_STOPPED], lambda instance: instance.start()) @@ -419,14 +423,10 @@ def _get_idempotent_run(context, client_token): instances_info = [] instance_ids = [] for os_instance in os_instances: - instance = instances.pop(os_instance['id']) + instance = instances[os_instance.id] novadb_instance = novadb.instance_get_by_uuid(context, os_instance.id) instances_info.append((instance, os_instance, novadb_instance,)) instance_ids.append(instance['id']) - - # NOTE(ft): delete obsolete instances - if instances: - _remove_instances(context, instances.itervalues()) if not instances_info: return ec2_network_interfaces = ( @@ -687,10 +687,15 @@ def _get_ip_info_for_instance(os_instance): return fixed_ip, fixed_ip6, floating_ip -def _foreach_instance(context, instance_ids, func): +def _foreach_instance(context, instance_ids, valid_states, func): instances = ec2utils.get_db_items(context, 'i', instance_ids) os_instances = _get_os_instances_by_instances(context, instances, exactly=True) + for os_instance in os_instances: + if getattr(os_instance, 'OS-EXT-STS:vm_state') not in valid_states: + raise exception.IncorrectInstanceState( + instance_id=next(inst['id'] for inst in instances + if inst['os_id'] == os_instance.id)) for os_instance in os_instances: func(os_instance) return True @@ -699,15 +704,17 @@ def _foreach_instance(context, instance_ids, func): def _get_os_instances_by_instances(context, instances, exactly=False): nova = clients.nova(context) os_instances = [] - found_obsolete_instance = False + obsolete_instances = [] for instance in instances: try: os_instances.append(nova.servers.get(instance['os_id'])) except nova_exception.NotFound: db_api.delete_item(context, instance['id']) - found_obsolete_instance = True - if found_obsolete_instance and exactly: - raise exception.InvalidInstanceIDNotFound(id=instance['id']) + obsolete_instances.append(instance) + if obsolete_instances: + _remove_instances(context, obsolete_instances) + if exactly: + raise exception.InvalidInstanceIDNotFound(id=instance['id']) return os_instances @@ -747,10 +754,6 @@ class InstanceEngineNeutron(object): private_ip_address=None, client_token=None, network_interface=None, iam_instance_profile=None, ebs_optimized=None): - # TODO(ft): fix passing complex network parameters to - # create_network_interface - # TODO(ft): check the compatibility of complex network parameters and - # multiple running os_image, os_kernel_id, os_ramdisk_id = _parse_image_parameters( context, image_id, kernel_id, ramdisk_id) @@ -906,6 +909,60 @@ class InstanceEngineNeutron(object): eni['attachment']['instanceId']].append(eni) return ec2_network_interfaces + def merge_network_interface_parameters(self, + security_group_names, + subnet_id, + private_ip_address, + security_group_ids, + network_interfaces): + network_interfaces = network_interfaces or [] + + if ((subnet_id or private_ip_address or security_group_ids or + security_group_names) and + (len(network_interfaces) > 1 or + # NOTE(ft): the only case in AWS when simple subnet_id + # and/or private_ip_address parameters are compatible with + # network_interface parameter is default behavior change of + # public IP association for passed subnet_id by specifying + # the only element in network_interfaces: + # {"device_index": 0, + # "associate_public_ip_address": } + # Both keys must be in the dict, and no other keys + # are allowed + # We should support such combination of parameters for + # compatibility purposes, even if we ignore device_index + # and associate_public_ip_address in all other code + len(network_interfaces) == 1 and + (len(network_interfaces[0]) != 2 or + 'associate_public_ip_address' not in network_interfaces[0] + or 'device_index' not in network_interfaces[0]))): + msg = _(' Network interfaces and an instance-level subnet ID or ' + 'private IP address or security groups may not be ' + 'specified on the same request') + raise exception.InvalidParameterCombination(msg) + + if subnet_id: + if security_group_names: + msg = _('The parameter groupName cannot be used with ' + 'the parameter subnet') + raise exception.InvalidParameterCombination(msg) + param = {'subnet_id': subnet_id} + if private_ip_address: + param['private_ip_address'] = private_ip_address + if security_group_ids: + param['security_group_id'] = security_group_ids + return None, [param] + elif private_ip_address: + msg = _('Specifying an IP address is only valid for VPC instances ' + 'and thus requires a subnet in which to launch') + raise exception.InvalidParameterCombination(msg) + elif security_group_ids: + msg = _('VPC security groups may not be used for a non-VPC launch') + raise exception.InvalidParameterCombination(msg) + else: + # NOTE(ft): only one of this variables is not empty + return security_group_names, network_interfaces + def check_network_interface_parameters(self, params, min_instance_count, max_instance_count): @@ -1052,60 +1109,6 @@ class InstanceEngineNeutron(object): 'for EC2 Classic mode')) return ec2_classic_os_networks[0] - def merge_network_interface_parameters(self, - security_group_names, - subnet_id, - private_ip_address, - security_group_ids, - network_interfaces): - network_interfaces = network_interfaces or [] - - if ((subnet_id or private_ip_address or security_group_ids or - security_group_names) and - (len(network_interfaces) > 1 or - # NOTE(ft): the only case in AWS when simple subnet_id - # and/or private_ip_address parameters are compatible with - # network_interface parameter is default behavior change of - # public IP association for passed subnet_id by specifying - # the only element in network_interfaces: - # {"device_index": 0, - # "associate_public_ip_address": } - # Both keys must be in the dict, and no other keys - # are allowed - # We should support such combination of parameters for - # compatibility purposes, even if we ignore device_index - # and associate_public_ip_address in all other code - len(network_interfaces) == 1 and - (len(network_interfaces[0]) != 2 or - 'associate_public_ip_address' not in network_interfaces[0] - or 'device_index' not in network_interfaces[0]))): - msg = _(' Network interfaces and an instance-level subnet ID or ' - 'private IP address or security groups may not be ' - 'specified on the same request') - raise exception.InvalidParameterCombination(msg) - - if subnet_id: - if security_group_names: - msg = _('The parameter groupName cannot be used with ' - 'the parameter subnet') - raise exception.InvalidParameterCombination(msg) - param = {'subnet_id': subnet_id} - if private_ip_address: - param['private_ip_address'] = private_ip_address - if security_group_ids: - param['security_group_id'] = security_group_ids - return None, [param] - elif private_ip_address: - msg = _('Specifying an IP address is only valid for VPC instances ' - 'and thus requires a subnet in which to launch') - raise exception.InvalidParameterCombination(msg) - elif security_group_ids: - msg = _('VPC security groups may not be used for a non-VPC launch') - raise exception.InvalidParameterCombination(msg) - else: - # NOTE(ft): only one of this variables is not empty - return security_group_names, network_interfaces - def format_network_interfaces(self, context, instances_network_interfaces): neutron = clients.neutron(context) @@ -1273,6 +1276,7 @@ def _cloud_format_ramdisk_id(context, instance_ref, image_ids=None): def _cloud_format_instance_type(context, os_instance): + # TODO(ft): cache flavors return clients.nova(context).flavors.get(os_instance.flavor['id']).name diff --git a/ec2api/exception.py b/ec2api/exception.py index d1ec9ddb..711b2761 100644 --- a/ec2api/exception.py +++ b/ec2api/exception.py @@ -257,6 +257,12 @@ class IncorrectState(EC2Exception): "'%(reason)s'") +class IncorrectInstanceState(IncorrectState): + ec2_code = 'IncorrectInstanceState' + msg_fmt = _("The instance '%(instance_id)s' is not in a state from which " + "the requested operation can be performed.") + + class InvalidVpcRange(Invalid): ec2_code = 'InvalidVpc.Range' msg_fmt = _("The CIDR '%(cidr_block)s' is invalid.") diff --git a/ec2api/tests/fakes.py b/ec2api/tests/fakes.py index 3a6db3e7..426534e4 100644 --- a/ec2api/tests/fakes.py +++ b/ec2api/tests/fakes.py @@ -150,6 +150,8 @@ ID_EC2_RESERVATION_2 = random_ec2_id('r') ROOT_DEVICE_NAME_INSTANCE_1 = '/dev/vda' ROOT_DEVICE_NAME_INSTANCE_2 = '/dev/sdb1' +IPV6_INSTANCE_2 = 'fe80:b33f::a8bb:ccff:fedd:eeff' +CLIENT_TOKEN_INSTANCE_2 = 'client-token-2' # DHCP options constants ID_EC2_DHCP_OPTIONS_1 = random_ec2_id('dopt') @@ -479,6 +481,7 @@ DB_INSTANCE_2 = { 'vpc_id': None, 'reservation_id': ID_EC2_RESERVATION_2, 'launch_index': 0, + 'client_token': CLIENT_TOKEN_INSTANCE_2, } NOVADB_INSTANCE_1 = { @@ -592,6 +595,7 @@ EC2_INSTANCE_2 = { 'amiLaunchIndex': 0, 'placement': {'availabilityZone': NAME_AVAILABILITY_ZONE}, 'dnsName': None, + 'dnsNameV6': IPV6_INSTANCE_2, 'instanceState': {'code': 0, 'name': 'pending'}, 'imageId': None, 'productCodesSet': [], @@ -607,6 +611,7 @@ EC2_INSTANCE_2 = { 'attachTime': None}}], 'instanceType': 'fake_flavor', 'rootDeviceName': ROOT_DEVICE_NAME_INSTANCE_2, + 'clientToken': CLIENT_TOKEN_INSTANCE_2, } EC2_RESERVATION_1 = { 'reservationId': ID_EC2_RESERVATION_1, @@ -646,11 +651,25 @@ class OSInstance(object): setattr(self, 'OS-EXT-AZ:availability_zone', availability_zone) def get(self): - None + pass def delete(self): - None + pass + def start(self): + pass + + def stop(self): + pass + + def reboot(self): + pass + + def get_password(self): + return None + + def get_console_output(self): + return None OS_INSTANCE_1 = OSInstance( ID_OS_INSTANCE_1, {'id': 'fakeFlavorId'}, @@ -674,7 +693,12 @@ OS_INSTANCE_2 = OSInstance( ID_OS_INSTANCE_2, {'id': 'fakeFlavorId'}, security_groups=[{'name': NAME_DEFAULT_OS_SECURITY_GROUP}, {'name': NAME_OTHER_OS_SECURITY_GROUP}], - availability_zone=NAME_AVAILABILITY_ZONE) + availability_zone=NAME_AVAILABILITY_ZONE, + addresses={ + ID_EC2_SUBNET_1: [{'addr': IPV6_INSTANCE_2, + 'version': 6, + 'OS-EXT-IPS:type': 'fixed'}]}, + ) # DHCP options objects diff --git a/ec2api/tests/test_instance.py b/ec2api/tests/test_instance.py index 6f670691..dd8c7b7d 100644 --- a/ec2api/tests/test_instance.py +++ b/ec2api/tests/test_instance.py @@ -13,12 +13,14 @@ # limitations under the License. import copy +import datetime import itertools import mock from oslotest import base as test_base from ec2api.api import instance as instance_api +from ec2api import exception from ec2api.tests import base from ec2api.tests import fakes from ec2api.tests import matchers @@ -292,6 +294,152 @@ class InstanceTestCase(base.ApiTestCase): mock.call(mock.ANY, 'i', tools.purge_dict(db_instance, ['id'])) for db_instance in self.DB_INSTANCES]) + @mock.patch('ec2api.api.instance._format_reservation') + @mock.patch('ec2api.api.instance.InstanceEngineNeutron.' + 'get_ec2_classic_os_network') + def test_run_instances_other_parameters(self, get_ec2_classic_os_network, + format_reservation): + self.glance.images.get.return_value = fakes.OSImage(fakes.OS_IMAGE_1) + get_ec2_classic_os_network.return_value = {'id': fakes.random_os_id()} + format_reservation.return_value = {} + + def do_check(engine, extra_kwargs={}, extra_db_instance={}): + instance_api.instance_engine = engine + + resp = self.execute('RunInstances', + {'ImageId': fakes.ID_EC2_IMAGE_1, + 'InstanceType': 'fake_flavor', + 'MinCount': '1', 'MaxCount': '1', + 'SecurityGroup.1': 'Default', + 'Placement.AvailabilityZone': 'fake_zone', + 'ClientToken': 'fake_client_token'}) + self.assertEqual(200, resp['http_status_code']) + + self.nova_servers.create.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, min_count=1, max_count=1, + userdata=None, block_device_mapping=None, + kernel_id=None, ramdisk_id=None, key_name=None, + availability_zone='fake_zone', security_groups=['Default'], + **extra_kwargs) + self.nova_servers.reset_mock() + db_instance = {'os_id': mock.ANY, + 'reservation_id': mock.ANY, + 'launch_index': 0, + 'client_token': 'fake_client_token'} + db_instance.update(extra_db_instance) + self.db_api.add_item.assert_called_once_with( + mock.ANY, 'i', db_instance) + self.db_api.reset_mock() + + do_check( + instance_api.InstanceEngineNeutron(), + extra_kwargs={ + 'nics': [ + {'net-id': get_ec2_classic_os_network.return_value['id']}], + }, + extra_db_instance={'vpc_id': None}) + do_check(instance_api.InstanceEngineNova()) + + @mock.patch('ec2api.api.instance._format_reservation') + @mock.patch('ec2api.api.instance._get_os_instances_by_instances') + def test_idempotent_run(self, get_os_instances_by_instances, + format_reservation): + instance_engine = mock.MagicMock() + instance_api.instance_engine = instance_engine + get_ec2_network_interfaces = instance_engine.get_ec2_network_interfaces + + instances = [{'id': fakes.random_ec2_id('i'), + 'os_id': fakes.random_os_id(), + 'reservation_id': fakes.random_ec2_id('r'), + 'client_token': 'client-token-%s' % ind} + for ind in range(3)] + os_instances = [fakes.OSInstance(inst['os_id']) + for inst in instances] + format_reservation.return_value = {'key': 'value'} + + # NOTE(ft): check select corresponding instance by client_token + self.db_api.get_items.return_value = [instances[0], instances[1]] + get_os_instances_by_instances.return_value = [os_instances[1]] + self.novadb.instance_get_by_uuid.return_value = 'novadb_instance' + get_ec2_network_interfaces.return_value = 'ec2_network_interfaces' + + resp = self.execute('RunInstances', + {'MinCount': '1', 'MaxCount': '1', + 'ImageId': fakes.ID_EC2_IMAGE_1, + 'InstanceType': 'fake_flavor', + 'ClientToken': 'client-token-1'}) + self.assertEqual({'http_status_code': 200, + 'key': 'value'}, + resp) + format_reservation.assert_called_once_with( + mock.ANY, instances[1]['reservation_id'], + [(instances[1], os_instances[1], 'novadb_instance')], + 'ec2_network_interfaces') + get_os_instances_by_instances.assert_called_once_with( + mock.ANY, {instances[1]['os_id']: instances[1]}) + self.novadb.instance_get_by_uuid.assert_called_once_with( + mock.ANY, os_instances[1].id) + get_ec2_network_interfaces.assert_called_once_with( + mock.ANY, [instances[1]['id']]) + + # NOTE(ft): check pass to general run_instances logic if no + # corresponding client_token is found + instance_engine.run_instances.return_value = {} + resp = self.execute('RunInstances', + {'MinCount': '1', 'MaxCount': '1', + 'ImageId': fakes.ID_EC2_IMAGE_1, + 'InstanceType': 'fake_flavor', + 'ClientToken': 'client-token-2'}) + self.assertTrue(instance_engine.run_instances.called) + + # NOTE(ft): check pass to general run_instances logic if no more + # corresponding OS instance exists + instance_engine.reset_mock() + get_os_instances_by_instances.return_value = [] + resp = self.execute('RunInstances', + {'MinCount': '1', 'MaxCount': '1', + 'ImageId': fakes.ID_EC2_IMAGE_1, + 'InstanceType': 'fake_flavor', + 'ClientToken': 'client-token-1'}) + self.assertTrue(instance_engine.run_instances.called) + + # NOTE(ft): check case for several instances with same client_token, + # but one no more exists in OS + format_reservation.reset_mock() + get_os_instances_by_instances.reset_mock() + instance_engine.reset_mock() + self.novadb.reset_mock() + for inst in instances: + inst['reservation_id'] = instances[0]['reservation_id'] + inst['client_token'] = 'client-token' + self.db_api.get_items.return_value = instances + get_os_instances_by_instances.return_value = [os_instances[0], + os_instances[2]] + self.novadb.instance_get_by_uuid.side_effect = ['novadb-instance-0', + 'novadb-instance-2'] + get_ec2_network_interfaces.return_value = 'ec2_network_interfaces' + + resp = self.execute('RunInstances', + {'MinCount': '1', 'MaxCount': '1', + 'ImageId': fakes.ID_EC2_IMAGE_1, + 'InstanceType': 'fake_flavor', + 'ClientToken': 'client-token'}) + self.assertEqual({'http_status_code': 200, + 'key': 'value'}, + resp) + format_reservation.assert_called_once_with( + mock.ANY, instances[0]['reservation_id'], + [(instances[0], os_instances[0], 'novadb-instance-0'), + (instances[2], os_instances[2], 'novadb-instance-2')], + 'ec2_network_interfaces') + get_os_instances_by_instances.assert_called_once_with( + mock.ANY, dict((inst['os_id'], inst) for inst in instances)) + self.assertEqual([mock.call(mock.ANY, os_instances[0].id), + mock.call(mock.ANY, os_instances[2].id)], + self.novadb.instance_get_by_uuid.mock_calls) + get_ec2_network_interfaces.assert_called_once_with( + mock.ANY, [instances[0]['id'], instances[2]['id']]) + def test_run_instances_rollback(self): instance_api.instance_engine = ( instance_api.InstanceEngineNeutron()) @@ -353,6 +501,31 @@ class InstanceTestCase(base.ApiTestCase): fakes.ID_EC2_NETWORK_INTERFACE_1}, new_port=False) + def test_run_instances_invalid_parameters(self): + resp = self.execute('RunInstances', + {'ImageId': fakes.ID_EC2_IMAGE_1, + 'MinCount': '0', 'MaxCount': '0'}) + self.assertEqual(400, resp['http_status_code']) + self.assertEqual('InvalidParameterValue', resp['Error']['Code']) + + resp = self.execute('RunInstances', + {'ImageId': fakes.ID_EC2_IMAGE_1, + 'MinCount': '1', 'MaxCount': '0'}) + self.assertEqual(400, resp['http_status_code']) + self.assertEqual('InvalidParameterValue', resp['Error']['Code']) + + resp = self.execute('RunInstances', + {'ImageId': fakes.ID_EC2_IMAGE_1, + 'MinCount': '0', 'MaxCount': '1'}) + self.assertEqual(400, resp['http_status_code']) + self.assertEqual('InvalidParameterValue', resp['Error']['Code']) + + resp = self.execute('RunInstances', + {'ImageId': fakes.ID_EC2_IMAGE_1, + 'MinCount': '2', 'MaxCount': '1'}) + self.assertEqual(400, resp['http_status_code']) + self.assertEqual('InvalidParameterValue', resp['Error']['Code']) + @mock.patch.object(fakes.OSInstance, 'delete', autospec=True) @mock.patch.object(fakes.OSInstance, 'get', autospec=True) def test_terminate_instances(self, os_instance_get, os_instance_delete): @@ -479,6 +652,95 @@ class InstanceTestCase(base.ApiTestCase): detached_enis=[self.DB_ATTACHED_ENIS[1]], deleted_enis=[]) + def test_terminate_instances_invalid_parameters(self): + resp = self.execute('TerminateInstances', + {'InstanceId.1': fakes.random_ec2_id('i')}) + self.assertEqual(400, resp['http_status_code']) + self.assertEqual('InvalidInstanceID.NotFound', resp['Error']['Code']) + + @mock.patch('ec2api.api.instance._get_os_instances_by_instances') + def _test_instances_operation(self, operation, os_instance_operation, + valid_state, invalid_state, + get_os_instances_by_instances): + os_instance_1 = copy.deepcopy(fakes.OS_INSTANCE_1) + os_instance_2 = copy.deepcopy(fakes.OS_INSTANCE_2) + for inst in (os_instance_1, os_instance_2): + setattr(inst, 'OS-EXT-STS:vm_state', valid_state) + + self.db_api.get_items_by_ids.return_value = [fakes.DB_INSTANCE_1, + fakes.DB_INSTANCE_2] + get_os_instances_by_instances.return_value = [os_instance_1, + os_instance_2] + + resp = self.execute(operation, + {'InstanceId.1': fakes.ID_EC2_INSTANCE_1, + 'InstanceId.2': fakes.ID_EC2_INSTANCE_2}) + self.assertEqual({'http_status_code': 200, + 'return': True}, + resp) + self.assertEqual([mock.call(os_instance_1), mock.call(os_instance_2)], + os_instance_operation.mock_calls) + self.db_api.get_items_by_ids.assert_called_once_with( + mock.ANY, 'i', set([fakes.ID_EC2_INSTANCE_1, + fakes.ID_EC2_INSTANCE_2])) + get_os_instances_by_instances.assert_called_once_with( + mock.ANY, [fakes.DB_INSTANCE_1, fakes.DB_INSTANCE_2], exactly=True) + + setattr(os_instance_2, 'OS-EXT-STS:vm_state', invalid_state) + os_instance_operation.reset_mock() + resp = self.execute('StartInstances', + {'InstanceId.1': fakes.ID_EC2_INSTANCE_1, + 'InstanceId.2': fakes.ID_EC2_INSTANCE_2}) + self.assertEqual(400, resp['http_status_code']) + self.assertEqual('IncorrectInstanceState', resp['Error']['Code']) + self.assertEqual(0, os_instance_operation.call_count) + + @mock.patch.object(fakes.OSInstance, 'start', autospec=True) + def test_start_instances(self, os_instance_start): + self._test_instances_operation('StartInstances', os_instance_start, + instance_api.vm_states_STOPPED, + instance_api.vm_states_ACTIVE) + + @mock.patch.object(fakes.OSInstance, 'stop', autospec=True) + def test_stop_instances(self, os_instance_stop): + self._test_instances_operation('StopInstances', os_instance_stop, + instance_api.vm_states_ACTIVE, + instance_api.vm_states_STOPPED) + + @mock.patch.object(fakes.OSInstance, 'reboot', autospec=True) + def test_reboot_instances(self, os_instance_reboot): + self._test_instances_operation('RebootInstances', os_instance_reboot, + instance_api.vm_states_ACTIVE, + instance_api.vm_states_BUILDING) + + @mock.patch('ec2api.openstack.common.timeutils.utcnow') + def _test_instance_get_operation(self, operation, getter, key, utcnow): + self.db_api.get_item_by_id.return_value = fakes.DB_INSTANCE_2 + self.nova_servers.get.return_value = fakes.OS_INSTANCE_2 + getter.return_value = 'fake_data' + utcnow.return_value = datetime.datetime(2015, 1, 19, 23, 34, 45, 123) + resp = self.execute(operation, + {'InstanceId': fakes.ID_EC2_INSTANCE_2}) + self.assertEqual({'http_status_code': 200, + 'instanceId': fakes.ID_EC2_INSTANCE_2, + 'timestamp': '2015-01-19T23:34:45.000Z', + key: 'fake_data'}, + resp) + self.db_api.get_item_by_id.assert_called_once_with( + mock.ANY, 'i', fakes.ID_EC2_INSTANCE_2) + self.nova_servers.get.assert_called_once_with(fakes.ID_OS_INSTANCE_2) + getter.assert_called_once_with(fakes.OS_INSTANCE_2) + + @mock.patch.object(fakes.OSInstance, 'get_password', autospec=True) + def test_get_password_data(self, get_password): + self._test_instance_get_operation('GetPasswordData', + get_password, 'passwordData') + + @mock.patch.object(fakes.OSInstance, 'get_console_output', autospec=True) + def test_console_output(self, get_console_output): + self._test_instance_get_operation('GetConsoleOutput', + get_console_output, 'output') + def test_describe_instances(self): """Describe 2 instances, one of which is vpc instance.""" instance_api.instance_engine = ( @@ -521,6 +783,16 @@ class InstanceTestCase(base.ApiTestCase): fakes.EC2_RESERVATION_2]}, orderless_lists=True)) + resp = self.execute('DescribeInstances', + {'Filter.1.Name': 'key-name', + 'Filter.1.Value.1': 'a', + 'Filter.1.Value.2': 'b', + 'Filter.2.Name': 'client-token', + 'Filter.2.Value.1': 'a string'}) + self.assertEqual({'http_status_code': 200, + 'reservationSet': []}, + resp) + self.db_api.get_items_by_ids.return_value = [fakes.DB_INSTANCE_2] resp = self.execute('DescribeInstances', {'InstanceId.1': fakes.ID_EC2_INSTANCE_2}) @@ -633,6 +905,44 @@ class InstanceTestCase(base.ApiTestCase): ec2_enis_by_instance=[[self.EC2_ATTACHED_ENIS[1]], []], ec2_instance_ips=[fakes.IP_FIRST_SUBNET_2, fakes.IP_LAST_SUBNET_2]) + @mock.patch('ec2api.api.instance._remove_instances') + def test_describe_instances_auto_remove(self, remove_instances): + instance_api.instance_engine = ( + instance_api.InstanceEngineNova()) + self.db_api.get_items.side_effect = ( + fakes.get_db_api_get_items( + {'i': [fakes.DB_INSTANCE_1, fakes.DB_INSTANCE_2], + 'ami': [], + 'vol': [fakes.DB_VOLUME_2]})) + self.nova_servers.list.return_value = [fakes.OS_INSTANCE_2] + self.novadb.instance_get_by_uuid.return_value = ( + fakes.NOVADB_INSTANCE_2) + self.novadb.block_device_mapping_get_all_by_instance.return_value = ( + fakes.NOVADB_BDM_INSTANCE_2) + + resp = self.execute('DescribeInstances', {}) + + self.assertThat(resp, + matchers.DictMatches( + {'http_status_code': 200, + 'reservationSet': [fakes.EC2_RESERVATION_2]}, + orderless_lists=True)) + remove_instances.assert_called_once_with( + mock.ANY, [fakes.DB_INSTANCE_1]) + + def test_describe_instances_invalid_parameters(self): + resp = self.execute('DescribeInstances', {'InstanceId.1': + fakes.random_ec2_id('i')}) + self.assertEqual(400, resp['http_status_code']) + self.assertEqual('InvalidInstanceID.NotFound', resp['Error']['Code']) + + self.db_api.get_items_by_ids.return_value = [fakes.DB_INSTANCE_2] + resp = self.execute('DescribeInstances', + {'InstanceId.1': fakes.ID_EC2_INSTANCE_2, + 'InstanceId.2': fakes.random_ec2_id('i')}) + self.assertEqual(400, resp['http_status_code']) + self.assertEqual('InvalidInstanceID.NotFound', resp['Error']['Code']) + def _build_multiple_data_model(self): # NOTE(ft): generate necessary fake data # We need 4 detached ports in 2 subnets. @@ -744,26 +1054,82 @@ class InstanceTestCase(base.ApiTestCase): fakes.random_os_id(), ec2_fake_eni, fakes.ID_OS_SUBNET_2, ['fake_ip']) - def _assert_list_ports_is_called_with_filter(self, instance_ids): - # NOTE(ft): compare manually due to the order of instance ids in - # list_ports call depends of values of instance EC2 ids - # But neither assert_any_called nor matchers.DictMatches can not - # compare lists excluding the order of elements - list_ports_calls = self.neutron.list_ports.mock_calls - self.assertEqual(1, len(list_ports_calls)) - self.assertEqual((), list_ports_calls[0][1]) - list_ports_kwargs = list_ports_calls[0][2] - self.assertEqual(len(list_ports_kwargs), 1) - self.assertIn('device_id', list_ports_kwargs) - self.assertEqual(sorted(instance_ids), - sorted(list_ports_kwargs['device_id'])) - -# TODO(ft): add tests for get_vpc_default_security_group_id -# and format_network_interfaces +# TODO(ft): add tests for get_vpc_default_security_group_id, +# format_network_interfaces, get_os_instances_by_instances, remove_instances, +# format_reservation class InstancePrivateTestCase(test_base.BaseTestCase): + @mock.patch('glanceclient.client.Client') + @mock.patch('ec2api.db.api.IMPL') + def test_parse_image_parameters(self, db_api, glance): + fake_context = mock.Mock(service_catalog=[{'type': 'fake'}]) + image_id = fakes.random_ec2_id('ami') + os_image_id = fakes.random_os_id() + db_api.get_public_items.return_value = [{'id': image_id, + 'os_id': os_image_id}] + os_image = fakes.OSImage({ + 'id': fakes.random_os_id(), + 'owner': fakes.ID_OS_PROJECT, + 'is_public': True, + 'status': None, + 'container_format': 'ami', + 'name': 'fake_name', + 'properties': {}}) + glance.return_value.images.get.return_value = os_image + + self.assertRaises(exception.ImageNotActive, + instance_api._parse_image_parameters, + fake_context, image_id, None, None) + + os_image.status = 'active' + os_image.properties['image_state'] = 'decrypting' + + self.assertRaises(exception.ImageNotActive, + instance_api._parse_image_parameters, + fake_context, image_id, None, None) + + @mock.patch('ec2api.api.instance.novadb') + @mock.patch('novaclient.v1_1.client.Client') + @mock.patch('ec2api.db.api.IMPL') + def test_format_instance(self, db_api, nova, novadb): + fake_context = mock.Mock(service_catalog=[{'type': 'fake'}]) + fake_flavor = mock.Mock() + fake_flavor.configure_mock(name='fake_flavor') + nova.return_value.flavors.get.return_value = fake_flavor + + instance = {'id': fakes.random_ec2_id('i'), + 'os_id': fakes.random_os_id(), + 'launch_index': 0} + os_instance = fakes.OSInstance(instance['os_id'], + flavor={'id': 'fakeFlavorId'}) + novadb_instance = {'kernel_id': None, + 'ramdisk_id': None, + 'hostname': instance['id']} + + setattr(os_instance, 'OS-EXT-STS:vm_state', + instance_api.vm_states_ACTIVE) + formatted_instance = instance_api._format_instance( + fake_context, instance, os_instance, novadb_instance, [], {}) + self.assertEqual({'name': instance_api.inst_state_RUNNING, + 'code': instance_api.inst_state_RUNNING_CODE}, + formatted_instance['instanceState']) + + setattr(os_instance, 'OS-EXT-STS:vm_state', + instance_api.vm_states_STOPPED) + formatted_instance = instance_api._format_instance( + fake_context, instance, os_instance, novadb_instance, [], {}) + self.assertEqual({'name': instance_api.inst_state_STOPPED, + 'code': instance_api.inst_state_STOPPED_CODE}, + formatted_instance['instanceState']) + + os_instance.image = {'id': fakes.random_os_id()} + formatted_instance = instance_api._format_instance( + fake_context, instance, os_instance, novadb_instance, [], {}) + db_api.add_item_id.assert_called_once_with( + mock.ANY, 'ami', os_instance.image['id']) + @mock.patch('cinderclient.v1.client.Client') @mock.patch('ec2api.api.instance.novadb') def test_format_instance_bdm(self, novadb, cinder): @@ -927,3 +1293,16 @@ class InstancePrivateTestCase(test_base.BaseTestCase): 'deleteOnTermination': False, 'volumeId': 'vol-00000002', 'attachTime': '', }}]})) + + def test_block_device_strip_dev(self): + self.assertEqual( + instance_api._block_device_strip_dev('/dev/sda'), 'sda') + self.assertEqual(instance_api._block_device_strip_dev('sda'), 'sda') + + def test_block_device_prepend_dev(self): + mapping = ['/dev/sda', 'sdb', 'sdc', 'sdd', 'sde'] + expected = ['/dev/sda', '/dev/sdb', '/dev/sdc', '/dev/sdd', '/dev/sde'] + + for m, e in zip(mapping, expected): + prepended = instance_api._block_device_prepend_dev(m) + self.assertEqual(e, prepended)