Implement BlockDeviceMappings for AWS::EC2::Instance

We should support the BlockDeviceMappings for
AWS::EC2::Instance resource to be compatible with
AWSCloudFormation.

Implements blueprint implement-ec2instance-bdm

Change-Id: Ib4d318b429494f312f4d6978631dd0a9721b32ee
This commit is contained in:
huangtianhua 2014-07-01 12:33:25 +08:00
parent 22095896d6
commit 73cca5dd2b
9 changed files with 324 additions and 17 deletions

View File

@ -120,14 +120,14 @@ class Instance(resource.Resource):
PLACEMENT_GROUP_NAME, PRIVATE_IP_ADDRESS, RAM_DISK_ID,
SECURITY_GROUPS, SECURITY_GROUP_IDS, NETWORK_INTERFACES,
SOURCE_DEST_CHECK, SUBNET_ID, TAGS, NOVA_SCHEDULER_HINTS, TENANCY,
USER_DATA, VOLUMES,
USER_DATA, VOLUMES, BLOCK_DEVICE_MAPPINGS
) = (
'ImageId', 'InstanceType', 'KeyName', 'AvailabilityZone',
'DisableApiTermination', 'KernelId', 'Monitoring',
'PlacementGroupName', 'PrivateIpAddress', 'RamDiskId',
'SecurityGroups', 'SecurityGroupIds', 'NetworkInterfaces',
'SourceDestCheck', 'SubnetId', 'Tags', 'NovaSchedulerHints', 'Tenancy',
'UserData', 'Volumes',
'UserData', 'Volumes', 'BlockDeviceMappings'
)
_TAG_KEYS = (
@ -148,6 +148,20 @@ class Instance(resource.Resource):
'Device', 'VolumeId',
)
_BLOCK_DEVICE_MAPPINGS_KEYS = (
DEVICE_NAME, EBS, NO_DEVICE, VIRTUAL_NAME,
) = (
'DeviceName', 'Ebs', 'NoDevice', 'VirtualName',
)
_EBS_KEYS = (
DELETE_ON_TERMINATION, IOPS, SNAPSHOT_ID, VOLUME_SIZE,
VOLUME_TYPE,
) = (
'DeleteOnTermination', 'Iops', 'SnapshotId', 'VolumeSize',
'VolumeType'
)
ATTRIBUTES = (
AVAILABILITY_ZONE_ATTR, PRIVATE_DNS_NAME, PUBLIC_DNS_NAME, PRIVATE_IP,
PUBLIC_IP,
@ -308,6 +322,70 @@ class Instance(resource.Resource):
}
)
),
BLOCK_DEVICE_MAPPINGS: properties.Schema(
properties.Schema.LIST,
_('Block device mappings to attach to instance.'),
schema=properties.Schema(
properties.Schema.MAP,
schema={
DEVICE_NAME: properties.Schema(
properties.Schema.STRING,
_('A device name where the volume will be '
'attached in the system at /dev/device_name.'
'e.g. vdb'),
required=True,
),
EBS: properties.Schema(
properties.Schema.MAP,
_('The ebs volume to attach to the instance.'),
schema={
DELETE_ON_TERMINATION: properties.Schema(
properties.Schema.BOOLEAN,
_('Indicate whether the volume should be '
'deleted when the instance is terminated.'),
default=True
),
IOPS: properties.Schema(
properties.Schema.NUMBER,
_('The number of I/O operations per second '
'that the volume supports.'),
implemented=False
),
SNAPSHOT_ID: properties.Schema(
properties.Schema.STRING,
_('The ID of the snapshot to create '
'a volume from.'),
),
VOLUME_SIZE: properties.Schema(
properties.Schema.STRING,
_('The size of the volume, in GB. Must be '
'equal or greater than the size of the '
'snapshot. It is safe to leave this blank '
'and have the Compute service infer '
'the size.'),
),
VOLUME_TYPE: properties.Schema(
properties.Schema.STRING,
_('The volume type.'),
implemented=False
),
},
),
NO_DEVICE: properties.Schema(
properties.Schema.MAP,
_('The can be used to unmap a defined device.'),
implemented=False
),
VIRTUAL_NAME: properties.Schema(
properties.Schema.STRING,
_('The name of the virtual device. The name must be '
'in the form ephemeralX where X is a number '
'starting from zero (0); for example, ephemeral0.'),
implemented=False
),
},
),
),
}
attributes_schema = {
@ -422,6 +500,33 @@ class Instance(resource.Resource):
security_groups = None
return security_groups
def _build_block_device_mapping(self, bdm):
if not bdm:
return None
bdm_dict = {}
for mapping in bdm:
device_name = mapping.get(self.DEVICE_NAME)
ebs = mapping.get(self.EBS)
if ebs:
mapping_parts = []
snapshot_id = ebs.get(self.SNAPSHOT_ID)
volume_size = ebs.get(self.VOLUME_SIZE)
delete = ebs.get(self.DELETE_ON_TERMINATION)
if snapshot_id:
mapping_parts.append(snapshot_id)
mapping_parts.append('snap')
if volume_size:
mapping_parts.append(str(volume_size))
else:
mapping_parts.append('')
if delete is not None:
mapping_parts.append(str(delete))
bdm_dict[device_name] = ':'.join(mapping_parts)
return bdm_dict
def _get_nova_metadata(self, properties):
if properties is None or properties.get(self.TAGS) is None:
return None
@ -460,6 +565,9 @@ class Instance(resource.Resource):
nics = self._build_nics(self.properties[self.NETWORK_INTERFACES],
security_groups=security_groups,
subnet_id=self.properties[self.SUBNET_ID])
block_device_mapping = self._build_block_device_mapping(
self.properties.get(self.BLOCK_DEVICE_MAPPINGS))
server = None
# FIXME(shadower): the instance_user config option is deprecated. Once
@ -482,7 +590,8 @@ class Instance(resource.Resource):
meta=self._get_nova_metadata(self.properties),
scheduler_hints=scheduler_hints,
nics=nics,
availability_zone=availability_zone)
availability_zone=availability_zone,
block_device_mapping=block_device_mapping)
finally:
# Avoid a race condition where the thread could be cancelled
# before the ID is stored
@ -670,6 +779,23 @@ class Instance(resource.Resource):
'/'.join([self.SECURITY_GROUPS, self.SECURITY_GROUP_IDS]),
self.NETWORK_INTERFACES)
# check bdm property
# now we don't support without snapshot_id in bdm
bdm = self.properties.get(self.BLOCK_DEVICE_MAPPINGS)
if bdm:
for mapping in bdm:
ebs = mapping.get(self.EBS)
if ebs:
snapshot_id = ebs.get(self.SNAPSHOT_ID)
if not snapshot_id:
msg = _("SnapshotId is missing, this is required "
"when specifying BlockDeviceMappings.")
raise exception.StackValidationFailed(message=msg)
else:
msg = _("Ebs is missing, this is required "
"when specifying BlockDeviceMappings.")
raise exception.StackValidationFailed(message=msg)
def _detach_volumes_task(self):
'''
Detach volumes from the instance

View File

@ -230,7 +230,8 @@ def setup_mocks(mocks, stack, mock_image_constraint=True):
security_groups=None,
userdata=server_userdata, scheduler_hints=None,
meta=None, nics=None,
availability_zone=None).AndReturn(
availability_zone=None,
block_device_mapping=None).AndReturn(
fc.servers.list()[4])
return fc

View File

@ -57,7 +57,13 @@ wp_template = '''
{"Key": "bar", "Value": "eggs"},
{"Key": "foo", "Value": "ham"},
{"Key": "foo", "Value": "baz"}],
"UserData" : "wordpress"
"UserData" : "wordpress",
"BlockDeviceMappings": [
{
"DeviceName": "vdb",
"Ebs": {"SnapshotId": "9ef5496e-7426-446a-bbc8-01f84d9c9972",
"DeleteOnTermination": "True"}
}]
}
}
}
@ -103,6 +109,7 @@ class InstancesTest(HeatTestCase):
tmpl, stack = self._get_test_template(stack_name, image_id)
resource_defns = tmpl.resource_definitions(stack)
instance = instances.Instance(name, resource_defns['WebServer'], stack)
bdm = {"vdb": "9ef5496e-7426-446a-bbc8-01f84d9c9972:snap::True"}
self._mock_get_image_id_success(image_id or 'CentOS 5.2', 1)
@ -120,7 +127,8 @@ class InstancesTest(HeatTestCase):
security_groups=None,
userdata=mox.IgnoreArg(),
scheduler_hints={'foo': ['spam', 'ham', 'baz'], 'bar': 'eggs'},
meta=None, nics=None, availability_zone=None).AndReturn(
meta=None, nics=None, availability_zone=None,
block_device_mapping=bdm).AndReturn(
return_server)
return instance
@ -147,6 +155,167 @@ class InstancesTest(HeatTestCase):
self.m.VerifyAll()
def test_instance_create_with_BlockDeviceMappings(self):
return_server = self.fc.servers.list()[4]
instance = self._create_test_instance(return_server,
'create_with_bdm')
# this makes sure the auto increment worked on instance creation
self.assertTrue(instance.id > 0)
expected_ip = return_server.networks['public'][0]
self.assertEqual(expected_ip, instance.FnGetAtt('PublicIp'))
self.assertEqual(expected_ip, instance.FnGetAtt('PrivateIp'))
self.assertEqual(expected_ip, instance.FnGetAtt('PrivateDnsName'))
self.assertEqual(expected_ip, instance.FnGetAtt('PrivateDnsName'))
self.m.VerifyAll()
def test_build_block_device_mapping(self):
return_server = self.fc.servers.list()[1]
instance = self._create_test_instance(return_server,
'test_build_bdm')
self.assertIsNone(instance._build_block_device_mapping([]))
self.assertIsNone(instance._build_block_device_mapping(None))
self.assertEqual({
'vdb': '1234:snap:',
'vdc': '5678:snap::False',
}, instance._build_block_device_mapping([
{'DeviceName': 'vdb', 'Ebs': {'SnapshotId': '1234'}},
{'DeviceName': 'vdc', 'Ebs': {'SnapshotId': '5678',
'DeleteOnTermination': False}},
]))
self.assertEqual({
'vdb': '1234:snap:1',
'vdc': '5678:snap:2:True',
}, instance._build_block_device_mapping([
{'DeviceName': 'vdb', 'Ebs': {'SnapshotId': '1234',
'VolumeSize': '1'}},
{'DeviceName': 'vdc', 'Ebs': {'SnapshotId': '5678',
'VolumeSize': '2',
'DeleteOnTermination': True}},
]))
def test_validate_BlockDeviceMappings_VolumeSize_valid_str(self):
stack_name = 'val_VolumeSize_valid'
tmpl, stack = self._setup_test_stack(stack_name)
bdm = [{'DeviceName': 'vdb',
'Ebs': {'SnapshotId': '1234',
'VolumeSize': '1'}}]
wsp = tmpl.t['Resources']['WebServer']['Properties']
wsp['BlockDeviceMappings'] = bdm
resource_defns = tmpl.resource_definitions(stack)
instance = instances.Instance('validate_volume_size',
resource_defns['WebServer'], stack)
self._mock_get_image_id_success('F17-x86_64-gold', 1)
self.m.StubOutWithMock(nova.NovaClientPlugin, '_create')
nova.NovaClientPlugin._create().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
self.assertIsNone(instance.validate())
self.m.VerifyAll()
def test_validate_BlockDeviceMappings_VolumeSize_invalid_str(self):
stack_name = 'val_VolumeSize_valid'
tmpl, stack = self._setup_test_stack(stack_name)
bdm = [{'DeviceName': 'vdb',
'Ebs': {'SnapshotId': '1234',
'VolumeSize': 10}}]
wsp = tmpl.t['Resources']['WebServer']['Properties']
wsp['BlockDeviceMappings'] = bdm
resource_defns = tmpl.resource_definitions(stack)
instance = instances.Instance('validate_volume_size',
resource_defns['WebServer'], stack)
self._mock_get_image_id_success('F17-x86_64-gold', 1)
self.m.StubOutWithMock(nova.NovaClientPlugin, '_create')
nova.NovaClientPlugin._create().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
exc = self.assertRaises(exception.StackValidationFailed,
instance.validate)
self.assertIn("Value must be a string", six.text_type(exc))
self.m.VerifyAll()
def test_validate_BlockDeviceMappings_without_Ebs_property(self):
stack_name = 'without_Ebs'
tmpl, stack = self._setup_test_stack(stack_name)
bdm = [{'DeviceName': 'vdb'}]
wsp = tmpl.t['Resources']['WebServer']['Properties']
wsp['BlockDeviceMappings'] = bdm
resource_defns = tmpl.resource_definitions(stack)
instance = instances.Instance('validate_without_Ebs',
resource_defns['WebServer'], stack)
self._mock_get_image_id_success('F17-x86_64-gold', 1)
self.m.StubOutWithMock(nova.NovaClientPlugin, '_create')
nova.NovaClientPlugin._create().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
exc = self.assertRaises(exception.StackValidationFailed,
instance.validate)
self.assertIn("Ebs is missing, this is required",
six.text_type(exc))
self.m.VerifyAll()
def test_validate_BlockDeviceMappings_without_SnapshotId_property(self):
stack_name = 'without_SnapshotId'
tmpl, stack = self._setup_test_stack(stack_name)
bdm = [{'DeviceName': 'vdb',
'Ebs': {'VolumeSize': '1'}}]
wsp = tmpl.t['Resources']['WebServer']['Properties']
wsp['BlockDeviceMappings'] = bdm
resource_defns = tmpl.resource_definitions(stack)
instance = instances.Instance('validate_without_SnapshotId',
resource_defns['WebServer'], stack)
self._mock_get_image_id_success('F17-x86_64-gold', 1)
self.m.StubOutWithMock(nova.NovaClientPlugin, '_create')
nova.NovaClientPlugin._create().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
exc = self.assertRaises(exception.StackValidationFailed,
instance.validate)
self.assertIn("SnapshotId is missing, this is required",
six.text_type(exc))
self.m.VerifyAll()
def test_validate_BlockDeviceMappings_without_DeviceName_property(self):
stack_name = 'without_DeviceName'
tmpl, stack = self._setup_test_stack(stack_name)
bdm = [{'Ebs': {'SnapshotId': '1234',
'VolumeSize': '1'}}]
wsp = tmpl.t['Resources']['WebServer']['Properties']
wsp['BlockDeviceMappings'] = bdm
resource_defns = tmpl.resource_definitions(stack)
instance = instances.Instance('validate_without_DeviceName',
resource_defns['WebServer'], stack)
self._mock_get_image_id_success('F17-x86_64-gold', 1)
self.m.StubOutWithMock(nova.NovaClientPlugin, '_create')
nova.NovaClientPlugin._create().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
exc = self.assertRaises(exception.StackValidationFailed,
instance.validate)
excepted_error = ('Property error : WebServer: BlockDeviceMappings '
'Property error : BlockDeviceMappings: 0 Property '
'error : 0: Property DeviceName not assigned')
self.assertIn(excepted_error, six.text_type(exc))
self.m.VerifyAll()
def test_instance_create_with_image_id(self):
return_server = self.fc.servers.list()[1]
instance = self._setup_test_instance(return_server,

View File

@ -197,7 +197,8 @@ class instancesTest(HeatTestCase):
security_groups=None,
userdata=server_userdata, scheduler_hints=None, meta=None,
nics=[{'port-id': '64d913c1-bcb1-42d2-8f0a-9593dbcaf251'}],
availability_zone=None).AndReturn(
availability_zone=None,
block_device_mapping=None).AndReturn(
return_server)
self.m.ReplayAll()
@ -250,7 +251,8 @@ class instancesTest(HeatTestCase):
security_groups=None,
userdata=server_userdata, scheduler_hints=None, meta=None,
nics=[{'port-id': '64d913c1-bcb1-42d2-8f0a-9593dbcaf251'}],
availability_zone=None).AndReturn(
availability_zone=None,
block_device_mapping=None).AndReturn(
return_server)
self.m.ReplayAll()

View File

@ -137,7 +137,8 @@ class LoadBalancerTest(HeatTestCase):
flavor=2, image=746, key_name=key_name,
meta=None, nics=None, name=server_name,
scheduler_hints=None, userdata=mox.IgnoreArg(),
security_groups=None, availability_zone=None).AndReturn(
security_groups=None, availability_zone=None,
block_device_mapping=None).AndReturn(
self.fc.servers.list()[1])
if stub_meta:
resource.Resource.metadata_set(mox.IgnoreArg()).AndReturn(None)

View File

@ -82,7 +82,8 @@ class nokeyTest(HeatTestCase):
name=utils.PhysName(stack_name, instance.name),
security_groups=None,
userdata=server_userdata, scheduler_hints=None,
meta=None, nics=None, availability_zone=None).AndReturn(
meta=None, nics=None, availability_zone=None,
block_device_mapping=None).AndReturn(
self.fc.servers.list()[1])
self.m.ReplayAll()

View File

@ -168,7 +168,8 @@ class ServerTagsTest(HeatTestCase):
name=utils.PhysName(stack_name, instance.name),
security_groups=None,
userdata=server_userdata, scheduler_hints=None,
meta=nova_tags, nics=None, availability_zone=None).AndReturn(
meta=nova_tags, nics=None, availability_zone=None,
block_device_mapping=None).AndReturn(
self.fc.servers.list()[1])
return instance
@ -242,7 +243,8 @@ class ServerTagsTest(HeatTestCase):
name=mox.IgnoreArg(),
security_groups=None,
userdata=mox.IgnoreArg(), scheduler_hints=None,
meta=nova_tags, nics=None, availability_zone=None).AndReturn(
meta=nova_tags, nics=None, availability_zone=None,
block_device_mapping=None).AndReturn(
self.fc.servers.list()[1])
return group
@ -291,7 +293,8 @@ class ServerTagsTest(HeatTestCase):
name=mox.IgnoreArg(),
security_groups=None,
userdata=mox.IgnoreArg(), scheduler_hints=None,
meta=nova_tags, nics=None, availability_zone=None).AndReturn(
meta=nova_tags, nics=None, availability_zone=None,
block_device_mapping=None).AndReturn(
self.fc.servers.list()[1])
return group

View File

@ -127,8 +127,9 @@ class SqlAlchemyTest(HeatTestCase):
security_groups=None,
userdata=mox.IgnoreArg(), scheduler_hints=None,
meta=None, nics=None,
availability_zone=None).MultipleTimes().AndReturn(
fc.servers.list()[4])
availability_zone=None,
block_device_mapping=None).MultipleTimes().\
AndReturn(fc.servers.list()[4])
return fc
def _mock_delete(self, mocks):

View File

@ -171,7 +171,10 @@ class FakeHTTPClient(base_client.HTTPClient):
{"version": 4, "addr": "5.6.9.8"}],
"private": [{"version": 4,
"addr": "10.13.12.13"}]},
"metadata": {"Server Label": "DB 1"}},
"metadata": {"Server Label": "DB 1"},
"os-extended-volumes:volumes_attached":
[{"id":
"66359157-dace-43ab-a7ed-a7e7cd7be59d"}]},
{"id": 56789,
"name": "server-with-metadata",
"OS-EXT-SRV-ATTR:instance_name":
@ -218,7 +221,7 @@ class FakeHTTPClient(base_client.HTTPClient):
return (202, None)
def get_servers_9999(self, **kw):
r = {'server': self.get_servers_detail()[1]['servers'][0]}
r = {'server': self.get_servers_detail()[1]['servers'][4]}
return (200, r)
def get_servers_9102(self, **kw):