Преглед изворни кода

Added support for LaunchConfiguration block_device_mappings

nuwan_ag пре 10 година
родитељ
комит
b5c8ad9093

+ 1 - 1
cloudbridge/providers/aws/impl.py

@@ -144,7 +144,7 @@ class MockAWSCloudProvider(AWSCloudProviderV1, TestMockHelperMixin):
       "size": 160
     },
     "max_bandwidth": 0,
-    "instance_type": "m1.small",
+    "instance_type": "t1.micro",
     "ECU": 1.0,
     "memory": 1.7
   }

+ 4 - 4
cloudbridge/providers/aws/resources.py

@@ -343,7 +343,7 @@ class AWSVolume(BaseVolume):
         self._volume = volume
 
     @property
-    def volume_id(self):
+    def id(self):
         return self._volume.id
 
     @property
@@ -412,7 +412,7 @@ class AWSVolume(BaseVolume):
             self._volume.status = 'unknown'
 
     def __repr__(self):
-        return "<CB-AWSVolume: {0} ({1})>".format(self.volume_id, self.name)
+        return "<CB-AWSVolume: {0} ({1})>".format(self.id, self.name)
 
 
 class AWSSnapshot(BaseSnapshot):
@@ -430,7 +430,7 @@ class AWSSnapshot(BaseSnapshot):
         self._snapshot = snapshot
 
     @property
-    def snapshot_id(self):
+    def id(self):
         return self._snapshot.id
 
     @property
@@ -483,7 +483,7 @@ class AWSSnapshot(BaseSnapshot):
         raise NotImplementedError('share not implemented by this provider')
 
     def __repr__(self):
-        return "<CB-AWSSnapshot: {0} ({1})>".format(self.snapshot_id,
+        return "<CB-AWSSnapshot: {0} ({1})>".format(self.id,
                                                     self.name)
 
 

+ 65 - 4
cloudbridge/providers/aws/services.py

@@ -1,6 +1,10 @@
 """
 Services implemented by the AWS provider.
 """
+import string
+
+from boto.ec2.blockdevicemapping import BlockDeviceMapping
+from boto.ec2.blockdevicemapping import BlockDeviceType
 from boto.exception import EC2ResponseError
 import requests
 
@@ -10,6 +14,7 @@ from cloudbridge.providers.base import BaseImageService
 from cloudbridge.providers.base import BaseInstanceService
 from cloudbridge.providers.base import BaseInstanceTypesService
 from cloudbridge.providers.base import BaseKeyPairService
+from cloudbridge.providers.base import BaseLaunchConfig
 from cloudbridge.providers.base import BaseObjectStoreService
 from cloudbridge.providers.base import BaseRegionService
 from cloudbridge.providers.base import BaseSecurityGroupService
@@ -21,6 +26,9 @@ from cloudbridge.providers.interfaces import KeyPair
 from cloudbridge.providers.interfaces import MachineImage
 from cloudbridge.providers.interfaces import PlacementZone
 from cloudbridge.providers.interfaces import SecurityGroup
+from cloudbridge.providers.interfaces.resources import Snapshot
+from cloudbridge.providers.interfaces.resources import Volume
+from cloudbridge.providers.interfaces.services import LaunchConfig
 
 from .resources import AWSContainer
 from .resources import AWSInstance
@@ -244,7 +252,7 @@ class AWSVolumeService(BaseVolumeService):
         Creates a new volume.
         """
         zone_name = zone.name if isinstance(zone, PlacementZone) else zone
-        snapshot_id = snapshot.snapshot_id if isinstance(
+        snapshot_id = snapshot.id if isinstance(
             zone, AWSSnapshot) and snapshot else snapshot
 
         ec2_vol = self.provider.ec2_conn.create_volume(
@@ -405,7 +413,7 @@ class AWSInstanceService(BaseInstanceService):
 
     def create(self, name, image, instance_type, zone=None,
                keypair=None, security_groups=None, user_data=None,
-               block_device_mapping=None, network_interfaces=None,
+               launch_config=None,
                **kwargs):
         """
         Creates a new virtual machine instance.
@@ -425,18 +433,71 @@ class AWSInstanceService(BaseInstanceService):
                 security_groups_list = security_groups
         else:
             security_groups_list = None
+        if launch_config:
+            bdm = self._process_block_device_mappings(launch_config, zone_name)
+        else:
+            bdm = None
 
         reservation = self.provider.ec2_conn.run_instances(
             image_id=image_id, instance_type=instance_size,
             min_count=1, max_count=1, placement=zone_name,
             key_name=keypair_name, security_groups=security_groups_list,
-            user_data=user_data
-        )
+            user_data=user_data, block_device_map=bdm)
         if reservation:
             instance = AWSInstance(self.provider, reservation.instances[0])
             instance.name = name
         return instance
 
+    def _process_block_device_mappings(self, launch_config, zone=None):
+        """
+        Processes block device mapping information
+        and returns a Boto BlockDeviceMapping object. If new volumes
+        are requested (source is None and destination is VOLUME), they will be
+        created and the relevant volume ids included in the mapping.
+        """
+        bdm = BlockDeviceMapping()
+        # Assign letters from f onwards
+        # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html
+        next_letter = iter(list(string.ascii_lowercase[6:]))
+        # assign ephemeral devices from 0 onwards
+        ephemeral_counter = 0
+        for device in launch_config.block_devices:
+            bd_type = BlockDeviceType()
+            if device.is_root:
+                bdm['/dev/sda1'] = bd_type
+            else:
+                bdm['sd' + next(next_letter)] = bd_type
+
+            if isinstance(device.source, Snapshot):
+                bd_type.snapshot_id = device.source.id
+            elif isinstance(device.source, Volume):
+                bd_type.volume_id = device.source.id
+            elif isinstance(device.source, MachineImage):
+                # Not supported
+                pass
+            else:
+                if device.dest_type == \
+                        LaunchConfig.DestinationType.VOLUME:
+                    # source is None, but destination is volume, therefore
+                    # create a blank volume. If the Zone is None, this
+                    # could fail since the volume and instance may be created
+                    # in two different zones.
+                    new_vol = self.provider.block_store.volumes.create(
+                        '',
+                        device.size,
+                        zone)
+                    bd_type.volume_id = new_vol.id
+                else:
+                    bd_type.ephemeral_name = 'ephemeral%s' % ephemeral_counter
+
+            bd_type.delete_on_terminate = device.delete_on_terminate
+            if device.size:
+                bd_type.size = device.size
+        return bdm
+
+    def create_launch_config(self):
+        return BaseLaunchConfig(self.provider)
+
     def get(self, instance_id):
         """
         Returns an instance given its id. Returns None

+ 80 - 0
cloudbridge/providers/base.py

@@ -4,6 +4,7 @@ Implementation of common methods across cloud providers.
 
 import logging
 import time
+import six
 
 from cloudbridge.providers.interfaces import CloudProvider
 from cloudbridge.providers.interfaces import Instance
@@ -21,12 +22,15 @@ from cloudbridge.providers.interfaces import SnapshotState
 from cloudbridge.providers.interfaces import Volume
 from cloudbridge.providers.interfaces import VolumeState
 from cloudbridge.providers.interfaces import WaitStateException
+from cloudbridge.providers.interfaces.resources \
+    import InvalidConfigurationException
 from cloudbridge.providers.interfaces.services import BlockStoreService
 from cloudbridge.providers.interfaces.services import ComputeService
 from cloudbridge.providers.interfaces.services import ImageService
 from cloudbridge.providers.interfaces.services import InstanceService
 from cloudbridge.providers.interfaces.services import InstanceTypesService
 from cloudbridge.providers.interfaces.services import KeyPairService
+from cloudbridge.providers.interfaces.services import LaunchConfig
 from cloudbridge.providers.interfaces.services import ObjectStoreService
 from cloudbridge.providers.interfaces.services import ProviderService
 from cloudbridge.providers.interfaces.services import RegionService
@@ -174,6 +178,82 @@ class BaseInstance(BaseObjectLifeCycleMixin, Instance):
         return [InstanceState.TERMINATED, InstanceState.ERROR]
 
 
+class BaseLaunchConfig(LaunchConfig):
+
+    block_devices = []
+
+    def __init__(self, provider):
+        self.provider = provider
+
+    class BlockDeviceMapping(object):
+        """
+        Represents a block device mapping
+        """
+
+        def __init__(self, dest_type, source=None, is_root=None,
+                     size=None, delete_on_terminate=None):
+            self.dest_type = dest_type
+            self.source = dest_type
+            self.is_root = is_root
+            self.size = size
+            self.delete_on_terminate = delete_on_terminate
+
+        def __repr__(self):
+            return "<CB-{0}: Dest: {1}, Src: {2}, IsRoot: {3}, Size: {4}>" \
+                .format(self.__class__.__name__, self.dest_type, self.source,
+                        self.is_root, self.size)
+
+    def add_block_device(self, dest_type, source=None, is_root=None,
+                         size=None, delete_on_terminate=None):
+
+        block_device = self._parse_block_device(
+            dest_type, source, is_root, size, delete_on_terminate)
+        self.block_devices.append(block_device)
+
+    def _parse_block_device(self, dest_type, source=None, is_root=None,
+                            size=None, delete_on_terminate=None):
+        """
+        Validates a block device and throws an InvalidConfigurationException
+        if the configuration is incorrect.
+        """
+        if source is None:
+            if dest_type == LaunchConfig.DestinationType.VOLUME and \
+                    not size:
+                raise InvalidConfigurationException(
+                    "A size must be specified if the destination is a blank"
+                    " new volume")
+
+        if source and \
+                not isinstance(source, (Snapshot, Volume, MachineImage)):
+            raise InvalidConfigurationException(
+                "Source must be a Snapshot, Volume, MachineImage or None")
+        if source and isinstance(source, (Snapshot, Volume)) and \
+                not dest_type == LaunchConfig.DestinationType.VOLUME:
+            raise InvalidConfigurationException(
+                "The destination must be Volume if the sources is of type"
+                " Snapshot or Volume")
+        if size:
+            if not isinstance(size, six.integer_types) or not size >= 0:
+                raise InvalidConfigurationException(
+                    "The size must be None or a number greater than 0")
+
+        if source and isinstance(source, MachineImage) and \
+                dest_type == LaunchConfig.DestinationType.LOCAL:
+            # When source is an image and destination is LOCAL, is_root=True
+            # is implied
+            is_root = True
+
+        if is_root:
+            for bd in self.block_devices:
+                if bd.is_root:
+                    raise InvalidConfigurationException(
+                        "An existing block device: {0} has already been"
+                        " marked as root. There can only be one root device.")
+
+        return BaseLaunchConfig.BlockDeviceMapping(dest_type, source, is_root,
+                                                   size, delete_on_terminate)
+
+
 class BaseMachineImage(BaseObjectLifeCycleMixin, MachineImage):
 
     @property

+ 33 - 3
cloudbridge/providers/interfaces/resources.py

@@ -24,7 +24,15 @@ class CloudProviderServiceType(object):
     OBJECTSTORE = 'object_store'
 
 
-class WaitStateException(Exception):
+class CloudBridgeBaseException(Exception):
+
+    """
+    Base class for all CloudBridge exceptions
+    """
+    pass
+
+
+class WaitStateException(CloudBridgeBaseException):
 
     """
     Marker interface for object wait exceptions.
@@ -34,6 +42,16 @@ class WaitStateException(Exception):
     pass
 
 
+class InvalidConfigurationException(CloudBridgeBaseException):
+
+    """
+    Marker interface for invalid launch configurations.
+    Thrown when a combination of parameters in a LaunchConfig
+    object results in an illegal state.
+    """
+    pass
+
+
 class ObjectLifeCycleMixin(object):
 
     """
@@ -343,12 +361,13 @@ class Volume(ObjectLifeCycleMixin):
     __metaclass__ = ABCMeta
 
     @abstractproperty
-    def volume_id(self):
+    def id(self):
         """
         Get the volume identifier.
 
         :rtype: ``str``
-        :return: ID for this instance as returned by the cloud middleware.
+        :return: ID for this volume. Will generally correspond to the cloud
+        middleware's ID, but should be treated as an opaque value.
         """
         pass
 
@@ -448,6 +467,17 @@ class Snapshot(ObjectLifeCycleMixin):
 
     __metaclass__ = ABCMeta
 
+    @abstractproperty
+    def id(self):
+        """
+        Get the snapshot identifier.
+
+        :rtype: ``str``
+        :return: ID for this snapshot. Will generally correspond to the cloud
+        middleware's ID, but should be treated as an opaque value.
+        """
+        pass
+
     @abstractmethod
     def create_volume(self, placement, size=None, volume_type=None, iops=None):
         """

+ 122 - 17
cloudbridge/providers/interfaces/services.py

@@ -2,6 +2,7 @@
 Specifications for services available through a provider
 """
 from abc import ABCMeta, abstractmethod, abstractproperty
+from enum import Enum
 
 
 class ProviderService(object):
@@ -101,8 +102,7 @@ class InstanceService(ProviderService):
     @abstractmethod
     def create(self, name, image, instance_type, zone=None,
                keypair=None, security_groups=None, user_data=None,
-               block_device_mapping=None, network_interfaces=None,
-               launch_configuration=None,
+               launch_config=None,
                **kwargs):
         """
         Creates a new virtual machine instance.
@@ -135,28 +135,133 @@ class InstanceService(ProviderService):
         :param user_data: An extra userdata object which is compatible with
                           the provider.
 
-        :type  block_device_mapping: ``BlockDeviceMapping`` object
-        :param block_device_mapping: A ``BlockDeviceMapping`` object which
-                                     describes additional block device mappings
-                                     for this instance.
-
-        :type  network_interfaces: ``NetworkInterfaceList`` object
-        :param network_interfaces: A ``NetworkInterfaceList`` object which
-                                   describes network interfaces for this
-                                   instance.
-
-        :type  launch_configuration: ``LaunchConfiguration`` object
-        :param launch_configuration: A ``LaunchConfiguration`` object which
+        :type  launch_config: ``LaunchConfig`` object
+        :param launch_config: A ``LaunchConfig`` object which
         describes advanced launch configuration options for an instance. This
-        include blck_device_mappings and network_interfaces. To construct a
+        include block_device_mappings and network_interfaces. To construct a
         launch configuration object, call
-        provider.compute.instances.create_launch_configuration()
+        provider.compute.instances.create_launch_config()
 
-        :rtype: `object`` of :class:`.Instance`
+        :rtype: ``object`` of :class:`.Instance`
         :return:  an instance of Instance class
         """
         pass
 
+    def create_launch_config(self):
+        """
+        Creates a ``LaunchConfig`` object which can be used
+        to set additional options when launching an instance, such as
+        block device mappings and network interfaces.
+
+        :rtype: ``object`` of :class:`.LaunchConfig`
+        :return:  an instance of a LaunchConfig class
+        """
+        pass
+
+
+class LaunchConfig(object):
+    """
+    Represents an advanced launch configuration object, containing
+    information such as BlockDeviceMappings, NetworkInterface configurations,
+    and other advanced options which may be useful when launching an instance.
+
+    Typical Usage:
+    ```
+        lc = provider.compute.instances.create_launch_config()
+        lc.add_block_device(...)
+        lc.add_network_interface(...)
+
+        inst = provider.compute.instances.create(name, image, instance_type,
+                                               launch_configuration=lc)
+    ```
+    """
+    class DestinationType(Enum):
+        LOCAL = 'local'
+        VOLUME = 'volume'
+
+    def add_block_device(self, dest_type, source=None, is_root=None,
+                         size=None, delete_on_terminate=None):
+        """
+        Adds a new block device mapping to the boot configuration. The block
+        device can be based on a snapshot, image, existing volume or be a blank
+        new volume, and is specified by the source parameter.
+        The destination can be either a Volume or a Local ephemeral device.
+
+        The property is_root can be set to True to override any existing root
+        device mappings. Otherwise, the default behaviour is to add new block
+        devices to the instance. When source is an Image and destination is
+        LOCAL, is_root=True is implied and does not need to be manually
+        specified. Specifying more than one device as root is an error and the
+        behaviour is indeterminate.
+
+        If no source is specified, then the destination must be LOCAL, and can
+        be used to add available ephemeral devices. (The total number of
+        ephemeral devices available for a particular InstanceType can be
+        determined by querying the InstanceTypes service).
+
+        Note that the device name, such as /dev/sda1, cannot be selected at
+        present, since this tends to be provider and instance type specific.
+        However, the order of device addition coupled with device type will
+        generally determine naming order, with devices added first getting
+        lower letters than instances added later (except when is_root is set).
+
+        Examples:
+        ```
+        lc = provider.compute.instances.create_launch_config()
+
+        # 1. Create and attach an empty volume to the instance of size 100GB
+        lc.add_block_device(LaunchConfig.DestinationType.VOLUME,
+                            size=100)
+
+        # 2. Override the size of the root device with a larger size
+        img = provider.images.get('<my_image_id>')
+        lc.add_block_device(LaunchConfig.DestinationType.LOCAL,
+                            source=img, size=100)
+
+        # 3. Create and attach a volume based on a snapshot
+        snap = provider.block_store.snapshots.get('<my_snapshot_id>')
+        lc.add_block_device(LaunchConfig.DestinationType.VOLUME,
+                            source=snap)
+
+        # 4. Add all available ephemeral devices
+        inst_type = provider.compute.instance_types.find_by_name('m1.small')
+        for i in xrange(inst_type.num_ephemeral_disks):
+            lc.add_block_device(LaunchConfig.DestinationType.LOCAL)
+        ```
+
+        :type  source: ``Volume``, ``Snapshot``, ``Image`` or None.
+        :param source: The source block_device to add. If ``Volume``, the
+        volume will be attached directly to the instance. If ``Snapshot``, a
+        volume will be created based on the Snapshot and attached to the
+        instance. If ``Image``, a volume based on the Image will be attached to
+        the instance. If blank, the source is assumed to be an empty blank
+        volume.
+
+        :type  dest_type: an  enum of ``LaunchConfig.DestinationType``
+        :param dest_type: The dest_type can be DestinationType.LOCAL, in which
+        case a local, ephemeral disk is assumed. Otherwise, it can be
+        DestinationType.VOLUME, in which case a volume is used. Note however,
+        that not all source and destination types are compatible. Only
+        a source of type ``Image`` and ``None`` can be used with a destination
+        type of Local. The destination type ``Volume`` supports all valid
+        sources.
+
+        :type  is_root: ``bool``
+        :param is_root: Determines which device will serve as the root device.
+        If more than one device is defined as root, the behaviour is
+        indeterminate and provider specific.
+
+        :type  size: ``int``
+        :param size: The size of the destination volume. Only valid for
+        dest_type of 'volume'. An implementation may ignore this parameter
+        for certain sources like 'Volume'.
+
+        :type  delete_on_terminate: ``bool``
+        :param delete_on_terminate: Applies only if the dest_type is Volume,
+        and determines whether to delete the volume on instance termination.
+        """
+        pass
+
 
 class VolumeService(ProviderService):
 

+ 6 - 6
cloudbridge/providers/openstack/resources.py

@@ -380,7 +380,7 @@ class OpenStackVolume(BaseVolume):
         self._volume = volume
 
     @property
-    def volume_id(self):
+    def id(self):
         return self._volume.id
 
     @property
@@ -437,7 +437,7 @@ class OpenStackVolume(BaseVolume):
         for its latest state.
         """
         vol = self._provider.block_store.volumes.get(
-            self.volume_id)
+            self.id)
         if vol:
             self._volume = vol._volume
         else:
@@ -446,7 +446,7 @@ class OpenStackVolume(BaseVolume):
             self._volume.status = 'unknown'
 
     def __repr__(self):
-        return "<CB-OSVolume: {0} ({1})>".format(self.volume_id, self.name)
+        return "<CB-OSVolume: {0} ({1})>".format(self.id, self.name)
 
 
 class OpenStackSnapshot(BaseSnapshot):
@@ -465,7 +465,7 @@ class OpenStackSnapshot(BaseSnapshot):
         self._snapshot = snapshot
 
     @property
-    def snapshot_id(self):
+    def id(self):
         return self._snapshot.id
 
     @property
@@ -494,7 +494,7 @@ class OpenStackSnapshot(BaseSnapshot):
         for its latest state.
         """
         snap = self._provider.block_store.snapshots.get(
-            self.snapshot_id)
+            self.id)
         if snap:
             self._snapshot = snap._snapshot
         else:
@@ -519,7 +519,7 @@ class OpenStackSnapshot(BaseSnapshot):
         raise NotImplementedError('share not implemented by this provider')
 
     def __repr__(self):
-        return "<CB-OSSnapshot: {0} ({1}>".format(self.snapshot_id, self.name)
+        return "<CB-OSSnapshot: {0} ({1}>".format(self.id, self.name)
 
 
 class OpenStackKeyPair(BaseKeyPair):

+ 54 - 4
cloudbridge/providers/openstack/services.py

@@ -10,6 +10,7 @@ from cloudbridge.providers.base import BaseImageService
 from cloudbridge.providers.base import BaseInstanceService
 from cloudbridge.providers.base import BaseInstanceTypesService
 from cloudbridge.providers.base import BaseKeyPairService
+from cloudbridge.providers.base import BaseLaunchConfig
 from cloudbridge.providers.base import BaseObjectStoreService
 from cloudbridge.providers.base import BaseRegionService
 from cloudbridge.providers.base import BaseSecurityGroupService
@@ -21,6 +22,9 @@ from cloudbridge.providers.interfaces import KeyPair
 from cloudbridge.providers.interfaces import MachineImage
 from cloudbridge.providers.interfaces import PlacementZone
 from cloudbridge.providers.interfaces import SecurityGroup
+from cloudbridge.providers.interfaces.resources import Snapshot
+from cloudbridge.providers.interfaces.resources import Volume
+from cloudbridge.providers.interfaces.services import LaunchConfig
 
 from .resources import OpenStackContainer
 from .resources import OpenStackInstance
@@ -297,7 +301,7 @@ class OpenStackVolumeService(BaseVolumeService):
         Creates a new volume.
         """
         zone_name = zone.name if isinstance(zone, PlacementZone) else zone
-        snapshot_id = snapshot.snapshot_id if isinstance(
+        snapshot_id = snapshot.id if isinstance(
             zone, OpenStackSnapshot) and snapshot else snapshot
 
         os_vol = self._provider.cinder.volumes.create(
@@ -340,7 +344,7 @@ class OpenStackSnapshotService(BaseSnapshotService):
         """
         Creates a new snapshot of a given volume.
         """
-        volume_id = volume.volume_id if \
+        volume_id = volume.id if \
             isinstance(volume, OpenStackVolume) else volume
 
         os_snap = self._provider.cinder.volume_snapshots.create(
@@ -442,7 +446,7 @@ class OpenStackInstanceService(BaseInstanceService):
 
     def create(self, name, image, instance_type, zone=None,
                keypair=None, security_groups=None, user_data=None,
-               block_device_mapping=None, network_interfaces=None,
+               launch_config=None,
                **kwargs):
         """
         Creates a new virtual machine instance.
@@ -462,6 +466,10 @@ class OpenStackInstanceService(BaseInstanceService):
                 security_groups_list = security_groups
         else:
             security_groups_list = None
+        if launch_config:
+            bdm = self._to_block_device_mapping(launch_config)
+        else:
+            bdm = None
 
         os_instance = self._provider.nova.servers.create(
             name,
@@ -472,9 +480,51 @@ class OpenStackInstanceService(BaseInstanceService):
             availability_zone=zone_name,
             key_name=keypair_name,
             security_groups=security_groups_list,
-            userdata=user_data)
+            userdata=user_data,
+            block_device_mapping_v2=bdm)
         return OpenStackInstance(self._provider, os_instance)
 
+    def _to_block_device_mapping(self, launch_config):
+        """
+        Extracts block device mapping information
+        from a launch config and constructs a BlockDeviceMappingV2
+        object.
+        """
+        bdm = []
+        for device in launch_config.block_devices:
+            bdm_dict = {}
+            if device.is_root:
+                bdm_dict['device_name'] = '/dev/vda'
+            else:
+                # Let openstack auto assign device name
+                bdm_dict['device_name'] = None
+
+            if isinstance(device.source, Snapshot):
+                bdm_dict['source_type'] = 'snapshot'
+                bdm_dict['uuid'] = device.source.id
+            elif isinstance(device.source, Volume):
+                bdm_dict['source_type'] = 'volume'
+                bdm_dict['uuid'] = device.source.id
+            elif isinstance(device.source, MachineImage):
+                bdm_dict['source_type'] = 'image'
+                bdm_dict['uuid'] = device.source.id
+            else:
+                bdm_dict['source_type'] = 'blank'
+
+            bdm_dict['destination_type'] = \
+                'volume' if device.dest_type == \
+                LaunchConfig.DestinationType.LOCAL \
+                else 'local'
+            bdm_dict['delete_on_termination'] = device.delete_on_terminate
+            if device.size:
+                bdm_dict['size'] = device.size
+
+            bdm.append(bdm_dict)
+        return bdm
+
+    def create_launch_config(self):
+        return BaseLaunchConfig(self.provider)
+
     def find(self, name):
         """
         Searches for an instance by a given list of attributes.

+ 1 - 1
setup.py

@@ -1,6 +1,6 @@
 from setuptools import setup, find_packages
 
-base_reqs = ['bunch>=1.00', 'six>=1.9.0', 'retrying']
+base_reqs = ['bunch>=1.00', 'six>=1.9.0', 'retrying', 'enum34']
 openstack_reqs = ['python-keystoneclient',
                   'python-novaclient', 'python-cinderclient',
                   'python-swiftclient']

+ 3 - 2
test/helpers.py

@@ -64,11 +64,12 @@ def get_provider_test_data(provider, key):
     return None
 
 
-def create_test_instance(provider, instance_name):
+def create_test_instance(provider, instance_name, launch_config=None):
     return provider.compute.instances.create(
         instance_name,
         get_provider_test_data(provider, 'image'),
-        get_provider_test_data(provider, 'instance_type'))
+        get_provider_test_data(provider, 'instance_type'),
+        launch_config=launch_config)
 
 
 def get_provider_wait_interval(provider):

+ 12 - 12
test/test_provider_block_store_service.py

@@ -32,14 +32,14 @@ class ProviderBlockStoreServiceTestCase(ProviderTestBase):
                 name)
 
             get_vol = self.provider.block_store.volumes.get(
-                test_vol.volume_id)
+                test_vol.id)
             self.assertTrue(
-                found_volumes[0].volume_id ==
-                get_vol.volume_id == test_vol.volume_id,
+                found_volumes[0].id ==
+                get_vol.id == test_vol.id,
                 "Ids returned by list: {0} and get: {1} are not as "
-                " expected: {2}" .format(found_volumes[0].volume_id,
-                                         get_vol.volume_id,
-                                         test_vol.volume_id))
+                " expected: {2}" .format(found_volumes[0].id,
+                                         get_vol.id,
+                                         test_vol.id))
             self.assertTrue(
                 found_volumes[0].name ==
                 get_vol.name == test_vol.name,
@@ -123,14 +123,14 @@ class ProviderBlockStoreServiceTestCase(ProviderTestBase):
                     name)
 
                 get_snap = self.provider.block_store.snapshots.get(
-                    test_snap.snapshot_id)
+                    test_snap.id)
                 self.assertTrue(
-                    found_snaps[0].snapshot_id ==
-                    get_snap.snapshot_id == test_snap.snapshot_id,
+                    found_snaps[0].id ==
+                    get_snap.id == test_snap.id,
                     "Ids returned by list: {0} and get: {1} are not as "
-                    " expected: {2}" .format(found_snaps[0].snapshot_id,
-                                             get_snap.snapshot_id,
-                                             test_snap.snapshot_id))
+                    " expected: {2}" .format(found_snaps[0].id,
+                                             get_snap.id,
+                                             test_snap.id))
                 self.assertTrue(
                     found_snaps[0].name ==
                     get_snap.name == test_snap.name,

+ 81 - 0
test/test_provider_compute_service.py

@@ -1,6 +1,11 @@
 import uuid
+
 import ipaddress
+
 from cloudbridge.providers.interfaces import InstanceState
+from cloudbridge.providers.interfaces.resources \
+    import InvalidConfigurationException
+from cloudbridge.providers.interfaces.services import LaunchConfig
 from test.helpers import ProviderTestBase
 import test.helpers as helpers
 
@@ -92,3 +97,79 @@ class ProviderComputeServiceTestCase(ProviderTestBase):
                 self._is_valid_ip(ip_address),
                 "Instance must have a valid IP address")
             test_instance.terminate()
+
+    def test_block_device_mappings(self):
+        name = "CBInstBlkMap-{0}-{1}".format(
+            self.provider.name,
+            uuid.uuid4())
+
+        outer_inst = helpers.create_test_instance(self.provider, name)
+        with helpers.exception_action(lambda: outer_inst.terminate()):
+            img = outer_inst.create_image(name)
+            with helpers.exception_action(lambda: img.delete()):
+                lc = self.provider.compute.instances.create_launch_config()
+
+                # specifying no size with a destination of volume should raise
+                # an exception
+                with self.assertRaises(InvalidConfigurationException):
+                    lc.add_block_device(LaunchConfig.DestinationType.VOLUME)
+
+                # specifying an invalid source type should raise an error
+                with self.assertRaises(InvalidConfigurationException):
+                    lc.add_block_device(LaunchConfig.DestinationType.LOCAL,
+                                        source='1234')
+
+                # specifying an invalid size should raise an error
+                with self.assertRaises(InvalidConfigurationException):
+                    lc.add_block_device(LaunchConfig.DestinationType.LOCAL,
+                                        source=img, size=-1)
+
+                # block_devices should be empty so far
+                self.assertFalse(
+                    lc.block_devices, "No block devices should have been added"
+                    " to mappings list since the configuration was invalid")
+
+                # Add a new volume
+                lc.add_block_device(LaunchConfig.DestinationType.VOLUME,
+                                    size=1)
+                # Override root volume size
+                if img:
+                    lc.add_block_device(LaunchConfig.DestinationType.LOCAL,
+                                        source=img, size=11)
+
+                # Since the previous addition with destination=LOCAL with
+                # source=img implies a root volume, attempting to add another
+                # root volume should raise an exception.
+                with self.assertRaises(InvalidConfigurationException):
+                    lc.add_block_device(LaunchConfig.DestinationType.LOCAL,
+                                        size=1, is_root=True)
+
+                # Add all available ephemeral devices
+                instance_type_name = helpers.get_provider_test_data(
+                    self.provider,
+                    "instance_type")
+                inst_type = self.provider.compute.instance_types.find_by_name(
+                    instance_type_name)
+                for _ in range(inst_type.num_ephemeral_disks):
+                    lc.add_block_device(LaunchConfig.DestinationType.LOCAL)
+
+                # block_devices should be populated
+                self.assertTrue(
+                    len(lc.block_devices) >= 1,
+                    "Expected number of block devices %s not found" %
+                    len(lc.block_devices))
+
+                inst = helpers.create_test_instance(
+                    self.provider,
+                    name,
+                    launch_config=lc)
+                with helpers.exception_action(lambda: inst.terminate()):
+                    inst.wait_till_ready(
+                        interval=self.get_test_wait_interval())
+                    inst.terminate()
+                    inst.wait_for(
+                        [InstanceState.TERMINATED, InstanceState.UNKNOWN],
+                        terminal_states=[InstanceState.ERROR],
+                        interval=self.get_test_wait_interval())
+                    # TODO: Check instance attachments and make sure they
+                    # correspond to requested mappings