diff --git a/ec2api/api/instance.py b/ec2api/api/instance.py index de99bbf5..5fd952df 100644 --- a/ec2api/api/instance.py +++ b/ec2api/api/instance.py @@ -1240,10 +1240,10 @@ def _cloud_parse_block_device_mapping(context, bdm): if ec2_id: if ec2_id.startswith('snap-'): snapshot = ec2utils.get_db_item(context, 'snap', ec2_id) - bdm['snapshot_id'] = snapshot['id'] + bdm['snapshot_id'] = snapshot['os_id'] elif ec2_id.startswith('vol-'): volume = ec2utils.get_db_item(context, 'vol', ec2_id) - bdm['volume_id'] = volume['id'] + bdm['volume_id'] = volume['os_id'] else: # NOTE(ft): AWS returns undocumented InvalidSnapshotID.NotFound raise exception.InvalidSnapshotIDMalformed(snapshot_id=ec2_id) diff --git a/ec2api/tests/base.py b/ec2api/tests/base.py index 56ab847e..40706a69 100644 --- a/ec2api/tests/base.py +++ b/ec2api/tests/base.py @@ -50,6 +50,7 @@ class ApiTestCase(test_base.BaseTestCase): self.nova_security_groups = nova_mock.return_value.security_groups self.nova_security_group_rules = ( nova_mock.return_value.security_group_rules) + self.nova_volumes = nova_mock.return_value.volumes self.addCleanup(nova_patcher.stop) glance_patcher = mock.patch('glanceclient.client.Client') diff --git a/ec2api/tests/fakes.py b/ec2api/tests/fakes.py index 3f3889da..829b662b 100644 --- a/ec2api/tests/fakes.py +++ b/ec2api/tests/fakes.py @@ -1119,7 +1119,8 @@ EC2_IMAGE_1 = { {'deviceName': '/dev/sdb0', 'virtualName': 'ephemeral0'}, {'deviceName': '/dev/sdb1', - 'ebs': {'snapshotId': ID_EC2_SNAPSHOT_1}}, + 'ebs': {'snapshotId': ID_EC2_SNAPSHOT_1, + 'volumeSize': 22}}, {'deviceName': '/dev/sdb2', 'ebs': {'snapshotId': ID_EC2_VOLUME_1}}, {'deviceName': '/dev/sdb3', @@ -1199,7 +1200,8 @@ OS_IMAGE_1 = { {'device': 'sdc4', 'virtual': 'swap'}], 'block_device_mapping': [ {'device_name': '/dev/sdb1', - 'snapshot_id': ID_OS_SNAPSHOT_1}, + 'snapshot_id': ID_OS_SNAPSHOT_1, + 'volume_size': 22}, {'device_name': '/dev/sdb2', 'volume_id': ID_OS_VOLUME_1}, {'device_name': '/dev/sdb3', 'virtual_name': 'ephemeral5'}, diff --git a/ec2api/tests/test_instance.py b/ec2api/tests/test_instance.py index 3de24a82..1ed5f4fc 100644 --- a/ec2api/tests/test_instance.py +++ b/ec2api/tests/test_instance.py @@ -294,31 +294,39 @@ 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._parse_block_device_mapping') @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): + format_reservation, + parse_block_device_mapping): 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 = {} + parse_block_device_mapping.return_value = 'fake_bdm' 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'}) + 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', + 'BlockDeviceMapping.1.DeviceName': '/dev/vdd', + 'BlockDeviceMapping.1.Ebs.SnapshotId': ( + fakes.ID_EC2_SNAPSHOT_1), + 'BlockDeviceMapping.1.Ebs.DeleteOnTermination': 'False'}) 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, + userdata=None, kernel_id=None, ramdisk_id=None, key_name=None, + block_device_mapping='fake_bdm', availability_zone='fake_zone', security_groups=['Default'], **extra_kwargs) self.nova_servers.reset_mock() @@ -330,6 +338,13 @@ class InstanceTestCase(base.ApiTestCase): self.db_api.add_item.assert_called_once_with( mock.ANY, 'i', db_instance) self.db_api.reset_mock() + parse_block_device_mapping.assert_called_once_with( + mock.ANY, + [{'device_name': '/dev/vdd', + 'ebs': {'snapshot_id': fakes.ID_EC2_SNAPSHOT_1, + 'delete_on_termination': False}}], + self.glance.images.get.return_value) + parse_block_device_mapping.reset_mock() do_check( instance_api.InstanceEngineNeutron(), @@ -1147,6 +1162,61 @@ class InstancePrivateTestCase(test_base.BaseTestCase): instance_api._parse_image_parameters, fake_context, image_id, None, None) + @mock.patch('ec2api.db.api.IMPL') + def test_parse_block_device_mapping(self, db_api): + fake_context = mock.Mock(service_catalog=[{'type': 'fake'}]) + os_image = fakes.OSImage(fakes.OS_IMAGE_1) + + db_api.get_item_by_id.side_effect = fakes.get_db_api_get_item_by_id({ + fakes.ID_EC2_VOLUME_1: fakes.DB_VOLUME_1, + fakes.ID_EC2_VOLUME_2: fakes.DB_VOLUME_2, + fakes.ID_EC2_VOLUME_3: fakes.DB_VOLUME_3, + fakes.ID_EC2_SNAPSHOT_1: fakes.DB_SNAPSHOT_1, + fakes.ID_EC2_SNAPSHOT_2: fakes.DB_SNAPSHOT_2}) + + res = instance_api._parse_block_device_mapping( + fake_context, [], os_image) + self.assertEqual([], res) + + res = instance_api._parse_block_device_mapping( + fake_context, [{'device_name': '/dev/vdf', + 'ebs': {'snapshot_id': fakes.ID_EC2_SNAPSHOT_1}}, + {'device_name': '/dev/vdg', + 'ebs': {'snapshot_id': fakes.ID_EC2_SNAPSHOT_2, + 'volume_size': 111, + 'delete_on_termination': False}}, + {'device_name': '/dev/vdh', + 'ebs': {'snapshot_id': fakes.ID_EC2_VOLUME_1}}, + {'device_name': '/dev/vdi', + 'ebs': {'snapshot_id': fakes.ID_EC2_VOLUME_2, + 'delete_on_termination': True}}, + {'device_name': '/dev/sdb1', + 'ebs': {'volume_size': 55}}], + os_image) + self.assertThat( + res, + matchers.ListMatches([{'device_name': '/dev/vdf', + 'snapshot_id': fakes.ID_OS_SNAPSHOT_1, + 'delete_on_termination': True}, + {'device_name': '/dev/vdg', + 'snapshot_id': fakes.ID_OS_SNAPSHOT_2, + 'volume_size': 111, + 'delete_on_termination': False}, + {'device_name': '/dev/vdh', + 'volume_id': fakes.ID_OS_VOLUME_1, + 'delete_on_termination': True}, + {'device_name': '/dev/vdi', + 'volume_id': fakes.ID_OS_VOLUME_2, + 'delete_on_termination': True}, + {'device_name': '/dev/sdb1', + 'snapshot_id': fakes.ID_OS_SNAPSHOT_1, + 'volume_size': 55, + 'volume_id': None, + 'delete_on_termination': None, + 'virtual_name': None, + 'no_device': None}], + orderless_lists=True)) + @mock.patch('ec2api.api.instance.novadb') @mock.patch('novaclient.v1_1.client.Client') @mock.patch('ec2api.db.api.IMPL') diff --git a/ec2api/tests/test_volume.py b/ec2api/tests/test_volume.py index 05aecf6a..f69c0bc5 100644 --- a/ec2api/tests/test_volume.py +++ b/ec2api/tests/test_volume.py @@ -149,3 +149,64 @@ class VolumeTestCase(base.ApiTestCase): resp = self.execute('DescribeVolumes', {}) self.assertEqual(200, resp['http_status_code']) self.assertEqual('banana', resp['volumeSet'][0]['status']) + + def test_attach_volume(self): + self.db_api.get_item_by_id.side_effect = ( + fakes.get_db_api_get_item_by_id({ + fakes.ID_EC2_INSTANCE_2: fakes.DB_INSTANCE_2, + fakes.ID_EC2_VOLUME_3: fakes.DB_VOLUME_3})) + os_volume = fakes.CinderVolume(fakes.OS_VOLUME_3) + os_volume.attachments.append({'device': '/dev/vdf', + 'server_id': fakes.ID_OS_INSTANCE_2}) + os_volume.status = 'attaching' + self.cinder.volumes.get.return_value = os_volume + + resp = self.execute('AttachVolume', + {'VolumeId': fakes.ID_EC2_VOLUME_3, + 'InstanceId': fakes.ID_EC2_INSTANCE_2, + 'Device': '/dev/vdf'}) + self.assertEqual({'http_status_code': 200, + 'attachTime': None, + 'device': '/dev/vdf', + 'instanceId': fakes.ID_EC2_INSTANCE_2, + 'status': 'attaching', + 'volumeId': fakes.ID_EC2_VOLUME_3}, + resp) + self.nova_volumes.create_server_volume.assert_called_once_with( + fakes.ID_OS_INSTANCE_2, fakes.ID_OS_VOLUME_3, '/dev/vdf') + + @mock.patch.object(fakes.CinderVolume, 'get', autospec=True) + def test_detach_volume(self, os_volume_get): + self.db_api.get_item_by_id.side_effect = ( + fakes.get_db_api_get_item_by_id({ + fakes.ID_EC2_INSTANCE_2: fakes.DB_INSTANCE_2, + fakes.ID_EC2_VOLUME_2: fakes.DB_VOLUME_2})) + self.db_api.get_items.return_value = [fakes.DB_INSTANCE_1, + fakes.DB_INSTANCE_2] + os_volume = fakes.CinderVolume(fakes.OS_VOLUME_2) + self.cinder.volumes.get.return_value = os_volume + os_volume_get.side_effect = ( + lambda vol: setattr(vol, 'status', 'detaching')) + + resp = self.execute('DetachVolume', + {'VolumeId': fakes.ID_EC2_VOLUME_2}) + self.assertEqual({'http_status_code': 200, + 'attachTime': None, + 'device': os_volume.attachments[0]['device'], + 'instanceId': fakes.ID_EC2_INSTANCE_2, + 'status': 'detaching', + 'volumeId': fakes.ID_EC2_VOLUME_2}, + resp) + self.nova_volumes.delete_server_volume.assert_called_once_with( + fakes.ID_OS_INSTANCE_2, fakes.ID_OS_VOLUME_2) + self.cinder.volumes.get.assert_called_once_with(fakes.ID_OS_VOLUME_2) + + def test_detach_volume_invalid_parameters(self): + self.db_api.get_item_by_id.return_value = fakes.DB_VOLUME_1 + self.cinder.volumes.get.return_value = ( + fakes.CinderVolume(fakes.OS_VOLUME_1)) + + resp = self.execute('DetachVolume', + {'VolumeId': fakes.ID_EC2_VOLUME_1}) + self.assertEqual(400, resp['http_status_code']) + self.assertEqual('IncorrectState', resp['Error']['Code'])