diff --git a/ec2api/api/instance.py b/ec2api/api/instance.py index 909c5eb2..c3a41433 100644 --- a/ec2api/api/instance.py +++ b/ec2api/api/instance.py @@ -887,6 +887,10 @@ def _parse_image_parameters(context, image_id, kernel_id, ramdisk_id): def _parse_block_device_mapping(context, block_device_mapping): # TODO(ft): check block_device_mapping structure + # TODO(ft): leave only the last bdm if several bdms are occured with + # the same device name. If names differ in /dev/ prefix occurence, + # raise InvalidBlockDeviceMapping with the message + # The device '' is used in more than one block-device mapping # TODO(ft): support virtual devices # TODO(ft): support no_device bdms = [] @@ -927,18 +931,67 @@ def _parse_block_device_mapping(context, block_device_mapping): def _build_block_device_mapping(context, block_device_mapping, os_image): mappings = _parse_block_device_mapping(context, block_device_mapping) properties = ec2utils.deserialize_os_image_properties(os_image) + image_bdms = ec2utils.get_os_image_mappings(properties) root_device_name = ( ec2utils.block_device_properties_root_device_name(properties)) short_root_device_name = ec2utils.block_device_strip_dev(root_device_name) + image_bdm_dict = {} + for bdm in image_bdms: + if bdm.get('device_name'): + key = ec2utils.block_device_strip_dev(bdm['device_name']) + if key == short_root_device_name: + bdm.setdefault('boot_index', 0) + elif bdm.get('boot_index') == 0: + key = short_root_device_name + bdm.setdefault('device_name', root_device_name) + else: + continue + image_bdm_dict[key] = bdm result = [] + # NOTE(ft): only the last device definition in the list is considered + # by AWS. So get only the last definitions, despite Nova can do the same. + # Because this is not contracted in Nova. for bdm in mappings: - _populate_parsed_bdm_parameter(bdm, short_root_device_name) + short_device_name = ec2utils.block_device_strip_dev(bdm['device_name']) + if short_device_name not in image_bdm_dict: + _populate_parsed_bdm_parameter(bdm, short_root_device_name) + else: + image_bdm = image_bdm_dict[short_device_name] + if bdm['device_name'] != image_bdm['device_name']: + raise exception.InvalidBlockDeviceMapping( + _("The device '%s' is used in more than one " + "block-device mapping") % short_device_name) + if (image_bdm.get('boot_index') == 0 and 'snapshot_id' in bdm and + bdm['snapshot_id'] != image_bdm.get('snapshot_id')): + raise exception.InvalidBlockDeviceMapping( + _('snapshotId cannot be modified on root device')) + if ('volume_size' in bdm and 'volume_size' in image_bdm and + bdm['volume_size'] < image_bdm['volume_size']): + raise exception.InvalidBlockDeviceMapping( + _("Volume of size %(bdm_size)dGB is smaller than expected " + "size %(image_bdm_size)dGB for '(device_name)s'") % + {'bdm_size': bdm['volume_size'], + 'image_bdm_size': image_bdm['volume_size'], + 'device_name': bdm['device_name']}) + + if bdm.get('snapshot_id'): + if 'snapshot_id' not in image_bdm: + raise exception.InvalidBlockDeviceMapping( + _('snapshotId can only be modified on EBS devices')) + + _populate_parsed_bdm_parameter(bdm, short_root_device_name) + else: + image_bdm.update(bdm) + bdm = image_bdm + source_type = bdm.get('source_type') if source_type: uuid = bdm.pop('_'.join([source_type, 'id']), None) if uuid: bdm['uuid'] = uuid + result.append(bdm) + return result diff --git a/ec2api/exception.py b/ec2api/exception.py index fa92d67e..fa32b15b 100644 --- a/ec2api/exception.py +++ b/ec2api/exception.py @@ -239,6 +239,10 @@ class InvalidSnapshotIDMalformed(EC2InvalidException): msg_fmg = _('The snapshot %(id)s ID is not valid') +class InvalidBlockDeviceMapping(EC2InvalidException): + pass + + class IncorrectState(EC2IncorrectStateException): msg_fmt = _("The resource is in incorrect state for the request - reason: " "'%(reason)s'") diff --git a/ec2api/tests/unit/test_instance.py b/ec2api/tests/unit/test_instance.py index 19602f4b..8a93f537 100644 --- a/ec2api/tests/unit/test_instance.py +++ b/ec2api/tests/unit/test_instance.py @@ -1465,8 +1465,10 @@ class InstancePrivateTestCase(test_base.BaseTestCase): def test_build_block_device_mapping(self, db_api): fake_context = mock.Mock(service_catalog=[{'type': 'fake'}]) db_api.get_item_by_id.side_effect = tools.get_db_api_get_item_by_id( - fakes.DB_SNAPSHOT_1, fakes.DB_VOLUME_1) + fakes.DB_SNAPSHOT_1, fakes.DB_SNAPSHOT_2, + fakes.DB_VOLUME_1, fakes.DB_VOLUME_2) + # check bdm attributes' population bdms = [ {'device_name': '/dev/sda1', 'ebs': {'snapshot_id': fakes.ID_EC2_SNAPSHOT_1}}, @@ -1496,11 +1498,217 @@ class InstancePrivateTestCase(test_base.BaseTestCase): 'delete_on_termination': True, 'boot_index': -1}, ] - result = instance_api._build_block_device_mapping( fake_context, bdms, fakes.OSImage(fakes.OS_IMAGE_1)) self.assertEqual(expected, result) + fake_image_template = { + 'id': fakes.random_os_id(), + 'properties': {'root_device_name': '/dev/vda', + 'bdm_v2': True, + 'block_device_mapping': []}} + + # check merging with image bdms + fake_image_template['properties']['block_device_mapping'] = [ + {'boot_index': 0, + 'device_name': '/dev/vda', + 'source_type': 'snapshot', + 'snapshot_id': fakes.ID_OS_SNAPSHOT_1, + 'delete_on_termination': True}, + {'device_name': 'vdb', + 'source_type': 'snapshot', + 'snapshot_id': fakes.random_os_id(), + 'volume_size': 50}, + {'device_name': '/dev/vdc', + 'source_type': 'blank', + 'volume_size': 10}, + ] + bdms = [ + {'device_name': '/dev/vda', + 'ebs': {'volume_size': 15}}, + {'device_name': 'vdb', + 'ebs': {'snapshot_id': fakes.ID_EC2_SNAPSHOT_2, + 'delete_on_termination': False}}, + {'device_name': '/dev/vdc', + 'ebs': {'volume_size': 20}}, + ] + expected = [ + {'device_name': '/dev/vda', + 'source_type': 'snapshot', + 'destination_type': 'volume', + 'uuid': fakes.ID_OS_SNAPSHOT_1, + 'delete_on_termination': True, + 'volume_size': 15, + 'boot_index': 0}, + {'device_name': 'vdb', + 'source_type': 'snapshot', + 'destination_type': 'volume', + 'uuid': fakes.ID_OS_SNAPSHOT_2, + 'delete_on_termination': False, + 'boot_index': -1}, + {'device_name': '/dev/vdc', + 'source_type': 'blank', + 'destination_type': 'volume', + 'volume_size': 20, + 'delete_on_termination': False}, + ] + result = instance_api._build_block_device_mapping( + fake_context, bdms, fakes.OSImage(fake_image_template)) + self.assertEqual(expected, result) + + # check result order for adjusting some bdm of all + fake_image_template['properties']['block_device_mapping'] = [ + {'device_name': '/dev/vdc', + 'source_type': 'blank', + 'volume_size': 10}, + {'device_name': '/dev/vde', + 'source_type': 'blank', + 'volume_size': 10}, + {'device_name': '/dev/vdf', + 'source_type': 'blank', + 'volume_size': 10}, + {'boot_index': -1, + 'source_type': 'blank', + 'volume_size': 10}, + ] + bdms = [ + {'device_name': '/dev/vdh', + 'ebs': {'volume_size': 15}}, + {'device_name': '/dev/vde', + 'ebs': {'volume_size': 15}}, + {'device_name': '/dev/vdb', + 'ebs': {'volume_size': 15}}, + ] + expected = [ + {'device_name': '/dev/vdh', + 'source_type': 'blank', + 'destination_type': 'volume', + 'volume_size': 15, + 'delete_on_termination': True, + 'boot_index': -1}, + {'device_name': '/dev/vde', + 'source_type': 'blank', + 'destination_type': 'volume', + 'volume_size': 15, + 'delete_on_termination': False}, + {'device_name': '/dev/vdb', + 'source_type': 'blank', + 'destination_type': 'volume', + 'volume_size': 15, + 'delete_on_termination': True, + 'boot_index': -1}, + ] + result = instance_api._build_block_device_mapping( + fake_context, bdms, fakes.OSImage(fake_image_template)) + self.assertEqual(expected, result) + + # check conflict of short and full device names + fake_image_template['properties']['block_device_mapping'] = [ + {'device_name': '/dev/vdc', + 'source_type': 'blank', + 'volume_size': 10}, + ] + bdms = [ + {'device_name': 'vdc', + 'ebs': {'volume_size': 15}}, + ] + self.assertRaises(exception.InvalidBlockDeviceMapping, + instance_api._build_block_device_mapping, + fake_context, bdms, + fakes.OSImage(fake_image_template)) + + # opposit combination of the same case + fake_image_template['properties']['block_device_mapping'] = [ + {'device_name': 'vdc', + 'source_type': 'blank', + 'volume_size': 10}, + ] + bdms = [ + {'device_name': '/dev/vdc', + 'ebs': {'volume_size': 15}}, + ] + self.assertRaises(exception.InvalidBlockDeviceMapping, + instance_api._build_block_device_mapping, + fake_context, bdms, + fakes.OSImage(fake_image_template)) + + # check fault on root device snapshot changing + fake_image_template['properties']['block_device_mapping'] = [ + {'boot_index': 0, + 'source_type': 'snapshot', + 'snapshot_id': fakes.ID_EC2_SNAPSHOT_1}, + ] + bdms = [ + {'device_name': '/dev/vda', + 'ebs': {'snapshot_id': fakes.ID_EC2_SNAPSHOT_2}}, + ] + self.assertRaises(exception.InvalidBlockDeviceMapping, + instance_api._build_block_device_mapping, + fake_context, bdms, + fakes.OSImage(fake_image_template)) + + # same case for legacy bdm + fake_image_template['properties']['block_device_mapping'] = [ + {'device_name': '/dev/vda', + 'snapshot_id': fakes.ID_EC2_SNAPSHOT_1}, + ] + fake_image_template['properties']['bdm_v2'] = False + bdms = [ + {'device_name': '/dev/vda', + 'ebs': {'snapshot_id': fakes.ID_EC2_SNAPSHOT_2}}, + ] + self.assertRaises(exception.InvalidBlockDeviceMapping, + instance_api._build_block_device_mapping, + fake_context, bdms, + fakes.OSImage(fake_image_template)) + + # same case for legacy bdm with short names + fake_image_template['properties']['block_device_mapping'] = [ + {'device_name': 'vda', + 'snapshot_id': fakes.ID_EC2_SNAPSHOT_1}, + ] + fake_image_template['properties']['bdm_v2'] = False + bdms = [ + {'device_name': 'vda', + 'ebs': {'snapshot_id': fakes.ID_EC2_SNAPSHOT_2}}, + ] + self.assertRaises(exception.InvalidBlockDeviceMapping, + instance_api._build_block_device_mapping, + fake_context, bdms, + fakes.OSImage(fake_image_template)) + + fake_image_template['properties']['bdm_v2'] = True + + # check fault on reduce volume size + fake_image_template['properties']['block_device_mapping'] = [ + {'device_name': 'vdc', + 'source_type': 'blank', + 'volume_size': 15}, + ] + bdms = [ + {'device_name': '/dev/vdc', + 'ebs': {'volume_size': 10}}, + ] + self.assertRaises(exception.InvalidBlockDeviceMapping, + instance_api._build_block_device_mapping, + fake_context, bdms, + fakes.OSImage(fake_image_template)) + + # check fault on set snapshot id if bdm doesn't have one + fake_image_template['properties']['block_device_mapping'] = [ + {'device_name': 'vdc', + 'source_type': 'blank', + 'volume_size': 10}, + ] + bdms = [ + {'device_name': '/dev/vdc', + 'ebs': {'snapshot_id': fakes.ID_EC2_SNAPSHOT_1}}, + ] + self.assertRaises(exception.InvalidBlockDeviceMapping, + instance_api._build_block_device_mapping, + fake_context, bdms, + fakes.OSImage(fake_image_template)) + @mock.patch('cinderclient.client.Client') @mock.patch('novaclient.client.Client') @mock.patch('ec2api.db.api.IMPL')