Parcourir la source

Added support for LaunchConfiguration block_device_mappings

nuwan_ag il y a 10 ans
Parent
commit
b5c8ad9093

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

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

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

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

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

@@ -1,6 +1,10 @@
 """
 """
 Services implemented by the AWS provider.
 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
 from boto.exception import EC2ResponseError
 import requests
 import requests
 
 
@@ -10,6 +14,7 @@ from cloudbridge.providers.base import BaseImageService
 from cloudbridge.providers.base import BaseInstanceService
 from cloudbridge.providers.base import BaseInstanceService
 from cloudbridge.providers.base import BaseInstanceTypesService
 from cloudbridge.providers.base import BaseInstanceTypesService
 from cloudbridge.providers.base import BaseKeyPairService
 from cloudbridge.providers.base import BaseKeyPairService
+from cloudbridge.providers.base import BaseLaunchConfig
 from cloudbridge.providers.base import BaseObjectStoreService
 from cloudbridge.providers.base import BaseObjectStoreService
 from cloudbridge.providers.base import BaseRegionService
 from cloudbridge.providers.base import BaseRegionService
 from cloudbridge.providers.base import BaseSecurityGroupService
 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 MachineImage
 from cloudbridge.providers.interfaces import PlacementZone
 from cloudbridge.providers.interfaces import PlacementZone
 from cloudbridge.providers.interfaces import SecurityGroup
 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 AWSContainer
 from .resources import AWSInstance
 from .resources import AWSInstance
@@ -244,7 +252,7 @@ class AWSVolumeService(BaseVolumeService):
         Creates a new volume.
         Creates a new volume.
         """
         """
         zone_name = zone.name if isinstance(zone, PlacementZone) else zone
         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
             zone, AWSSnapshot) and snapshot else snapshot
 
 
         ec2_vol = self.provider.ec2_conn.create_volume(
         ec2_vol = self.provider.ec2_conn.create_volume(
@@ -405,7 +413,7 @@ class AWSInstanceService(BaseInstanceService):
 
 
     def create(self, name, image, instance_type, zone=None,
     def create(self, name, image, instance_type, zone=None,
                keypair=None, security_groups=None, user_data=None,
                keypair=None, security_groups=None, user_data=None,
-               block_device_mapping=None, network_interfaces=None,
+               launch_config=None,
                **kwargs):
                **kwargs):
         """
         """
         Creates a new virtual machine instance.
         Creates a new virtual machine instance.
@@ -425,18 +433,71 @@ class AWSInstanceService(BaseInstanceService):
                 security_groups_list = security_groups
                 security_groups_list = security_groups
         else:
         else:
             security_groups_list = None
             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(
         reservation = self.provider.ec2_conn.run_instances(
             image_id=image_id, instance_type=instance_size,
             image_id=image_id, instance_type=instance_size,
             min_count=1, max_count=1, placement=zone_name,
             min_count=1, max_count=1, placement=zone_name,
             key_name=keypair_name, security_groups=security_groups_list,
             key_name=keypair_name, security_groups=security_groups_list,
-            user_data=user_data
-        )
+            user_data=user_data, block_device_map=bdm)
         if reservation:
         if reservation:
             instance = AWSInstance(self.provider, reservation.instances[0])
             instance = AWSInstance(self.provider, reservation.instances[0])
             instance.name = name
             instance.name = name
         return instance
         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):
     def get(self, instance_id):
         """
         """
         Returns an instance given its id. Returns None
         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 logging
 import time
 import time
+import six
 
 
 from cloudbridge.providers.interfaces import CloudProvider
 from cloudbridge.providers.interfaces import CloudProvider
 from cloudbridge.providers.interfaces import Instance
 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 Volume
 from cloudbridge.providers.interfaces import VolumeState
 from cloudbridge.providers.interfaces import VolumeState
 from cloudbridge.providers.interfaces import WaitStateException
 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 BlockStoreService
 from cloudbridge.providers.interfaces.services import ComputeService
 from cloudbridge.providers.interfaces.services import ComputeService
 from cloudbridge.providers.interfaces.services import ImageService
 from cloudbridge.providers.interfaces.services import ImageService
 from cloudbridge.providers.interfaces.services import InstanceService
 from cloudbridge.providers.interfaces.services import InstanceService
 from cloudbridge.providers.interfaces.services import InstanceTypesService
 from cloudbridge.providers.interfaces.services import InstanceTypesService
 from cloudbridge.providers.interfaces.services import KeyPairService
 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 ObjectStoreService
 from cloudbridge.providers.interfaces.services import ProviderService
 from cloudbridge.providers.interfaces.services import ProviderService
 from cloudbridge.providers.interfaces.services import RegionService
 from cloudbridge.providers.interfaces.services import RegionService
@@ -174,6 +178,82 @@ class BaseInstance(BaseObjectLifeCycleMixin, Instance):
         return [InstanceState.TERMINATED, InstanceState.ERROR]
         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):
 class BaseMachineImage(BaseObjectLifeCycleMixin, MachineImage):
 
 
     @property
     @property

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

@@ -24,7 +24,15 @@ class CloudProviderServiceType(object):
     OBJECTSTORE = 'object_store'
     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.
     Marker interface for object wait exceptions.
@@ -34,6 +42,16 @@ class WaitStateException(Exception):
     pass
     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):
 class ObjectLifeCycleMixin(object):
 
 
     """
     """
@@ -343,12 +361,13 @@ class Volume(ObjectLifeCycleMixin):
     __metaclass__ = ABCMeta
     __metaclass__ = ABCMeta
 
 
     @abstractproperty
     @abstractproperty
-    def volume_id(self):
+    def id(self):
         """
         """
         Get the volume identifier.
         Get the volume identifier.
 
 
         :rtype: ``str``
         :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
         pass
 
 
@@ -448,6 +467,17 @@ class Snapshot(ObjectLifeCycleMixin):
 
 
     __metaclass__ = ABCMeta
     __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
     @abstractmethod
     def create_volume(self, placement, size=None, volume_type=None, iops=None):
     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
 Specifications for services available through a provider
 """
 """
 from abc import ABCMeta, abstractmethod, abstractproperty
 from abc import ABCMeta, abstractmethod, abstractproperty
+from enum import Enum
 
 
 
 
 class ProviderService(object):
 class ProviderService(object):
@@ -101,8 +102,7 @@ class InstanceService(ProviderService):
     @abstractmethod
     @abstractmethod
     def create(self, name, image, instance_type, zone=None,
     def create(self, name, image, instance_type, zone=None,
                keypair=None, security_groups=None, user_data=None,
                keypair=None, security_groups=None, user_data=None,
-               block_device_mapping=None, network_interfaces=None,
-               launch_configuration=None,
+               launch_config=None,
                **kwargs):
                **kwargs):
         """
         """
         Creates a new virtual machine instance.
         Creates a new virtual machine instance.
@@ -135,28 +135,133 @@ class InstanceService(ProviderService):
         :param user_data: An extra userdata object which is compatible with
         :param user_data: An extra userdata object which is compatible with
                           the provider.
                           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
         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
         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
         :return:  an instance of Instance class
         """
         """
         pass
         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):
 class VolumeService(ProviderService):
 
 

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

@@ -380,7 +380,7 @@ class OpenStackVolume(BaseVolume):
         self._volume = volume
         self._volume = volume
 
 
     @property
     @property
-    def volume_id(self):
+    def id(self):
         return self._volume.id
         return self._volume.id
 
 
     @property
     @property
@@ -437,7 +437,7 @@ class OpenStackVolume(BaseVolume):
         for its latest state.
         for its latest state.
         """
         """
         vol = self._provider.block_store.volumes.get(
         vol = self._provider.block_store.volumes.get(
-            self.volume_id)
+            self.id)
         if vol:
         if vol:
             self._volume = vol._volume
             self._volume = vol._volume
         else:
         else:
@@ -446,7 +446,7 @@ class OpenStackVolume(BaseVolume):
             self._volume.status = 'unknown'
             self._volume.status = 'unknown'
 
 
     def __repr__(self):
     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):
 class OpenStackSnapshot(BaseSnapshot):
@@ -465,7 +465,7 @@ class OpenStackSnapshot(BaseSnapshot):
         self._snapshot = snapshot
         self._snapshot = snapshot
 
 
     @property
     @property
-    def snapshot_id(self):
+    def id(self):
         return self._snapshot.id
         return self._snapshot.id
 
 
     @property
     @property
@@ -494,7 +494,7 @@ class OpenStackSnapshot(BaseSnapshot):
         for its latest state.
         for its latest state.
         """
         """
         snap = self._provider.block_store.snapshots.get(
         snap = self._provider.block_store.snapshots.get(
-            self.snapshot_id)
+            self.id)
         if snap:
         if snap:
             self._snapshot = snap._snapshot
             self._snapshot = snap._snapshot
         else:
         else:
@@ -519,7 +519,7 @@ class OpenStackSnapshot(BaseSnapshot):
         raise NotImplementedError('share not implemented by this provider')
         raise NotImplementedError('share not implemented by this provider')
 
 
     def __repr__(self):
     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):
 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 BaseInstanceService
 from cloudbridge.providers.base import BaseInstanceTypesService
 from cloudbridge.providers.base import BaseInstanceTypesService
 from cloudbridge.providers.base import BaseKeyPairService
 from cloudbridge.providers.base import BaseKeyPairService
+from cloudbridge.providers.base import BaseLaunchConfig
 from cloudbridge.providers.base import BaseObjectStoreService
 from cloudbridge.providers.base import BaseObjectStoreService
 from cloudbridge.providers.base import BaseRegionService
 from cloudbridge.providers.base import BaseRegionService
 from cloudbridge.providers.base import BaseSecurityGroupService
 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 MachineImage
 from cloudbridge.providers.interfaces import PlacementZone
 from cloudbridge.providers.interfaces import PlacementZone
 from cloudbridge.providers.interfaces import SecurityGroup
 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 OpenStackContainer
 from .resources import OpenStackInstance
 from .resources import OpenStackInstance
@@ -297,7 +301,7 @@ class OpenStackVolumeService(BaseVolumeService):
         Creates a new volume.
         Creates a new volume.
         """
         """
         zone_name = zone.name if isinstance(zone, PlacementZone) else zone
         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
             zone, OpenStackSnapshot) and snapshot else snapshot
 
 
         os_vol = self._provider.cinder.volumes.create(
         os_vol = self._provider.cinder.volumes.create(
@@ -340,7 +344,7 @@ class OpenStackSnapshotService(BaseSnapshotService):
         """
         """
         Creates a new snapshot of a given volume.
         Creates a new snapshot of a given volume.
         """
         """
-        volume_id = volume.volume_id if \
+        volume_id = volume.id if \
             isinstance(volume, OpenStackVolume) else volume
             isinstance(volume, OpenStackVolume) else volume
 
 
         os_snap = self._provider.cinder.volume_snapshots.create(
         os_snap = self._provider.cinder.volume_snapshots.create(
@@ -442,7 +446,7 @@ class OpenStackInstanceService(BaseInstanceService):
 
 
     def create(self, name, image, instance_type, zone=None,
     def create(self, name, image, instance_type, zone=None,
                keypair=None, security_groups=None, user_data=None,
                keypair=None, security_groups=None, user_data=None,
-               block_device_mapping=None, network_interfaces=None,
+               launch_config=None,
                **kwargs):
                **kwargs):
         """
         """
         Creates a new virtual machine instance.
         Creates a new virtual machine instance.
@@ -462,6 +466,10 @@ class OpenStackInstanceService(BaseInstanceService):
                 security_groups_list = security_groups
                 security_groups_list = security_groups
         else:
         else:
             security_groups_list = None
             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(
         os_instance = self._provider.nova.servers.create(
             name,
             name,
@@ -472,9 +480,51 @@ class OpenStackInstanceService(BaseInstanceService):
             availability_zone=zone_name,
             availability_zone=zone_name,
             key_name=keypair_name,
             key_name=keypair_name,
             security_groups=security_groups_list,
             security_groups=security_groups_list,
-            userdata=user_data)
+            userdata=user_data,
+            block_device_mapping_v2=bdm)
         return OpenStackInstance(self._provider, os_instance)
         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):
     def find(self, name):
         """
         """
         Searches for an instance by a given list of attributes.
         Searches for an instance by a given list of attributes.

+ 1 - 1
setup.py

@@ -1,6 +1,6 @@
 from setuptools import setup, find_packages
 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',
 openstack_reqs = ['python-keystoneclient',
                   'python-novaclient', 'python-cinderclient',
                   'python-novaclient', 'python-cinderclient',
                   'python-swiftclient']
                   'python-swiftclient']

+ 3 - 2
test/helpers.py

@@ -64,11 +64,12 @@ def get_provider_test_data(provider, key):
     return None
     return None
 
 
 
 
-def create_test_instance(provider, instance_name):
+def create_test_instance(provider, instance_name, launch_config=None):
     return provider.compute.instances.create(
     return provider.compute.instances.create(
         instance_name,
         instance_name,
         get_provider_test_data(provider, 'image'),
         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):
 def get_provider_wait_interval(provider):

+ 12 - 12
test/test_provider_block_store_service.py

@@ -32,14 +32,14 @@ class ProviderBlockStoreServiceTestCase(ProviderTestBase):
                 name)
                 name)
 
 
             get_vol = self.provider.block_store.volumes.get(
             get_vol = self.provider.block_store.volumes.get(
-                test_vol.volume_id)
+                test_vol.id)
             self.assertTrue(
             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 "
                 "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(
             self.assertTrue(
                 found_volumes[0].name ==
                 found_volumes[0].name ==
                 get_vol.name == test_vol.name,
                 get_vol.name == test_vol.name,
@@ -123,14 +123,14 @@ class ProviderBlockStoreServiceTestCase(ProviderTestBase):
                     name)
                     name)
 
 
                 get_snap = self.provider.block_store.snapshots.get(
                 get_snap = self.provider.block_store.snapshots.get(
-                    test_snap.snapshot_id)
+                    test_snap.id)
                 self.assertTrue(
                 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 "
                     "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(
                 self.assertTrue(
                     found_snaps[0].name ==
                     found_snaps[0].name ==
                     get_snap.name == test_snap.name,
                     get_snap.name == test_snap.name,

+ 81 - 0
test/test_provider_compute_service.py

@@ -1,6 +1,11 @@
 import uuid
 import uuid
+
 import ipaddress
 import ipaddress
+
 from cloudbridge.providers.interfaces import InstanceState
 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
 from test.helpers import ProviderTestBase
 import test.helpers as helpers
 import test.helpers as helpers
 
 
@@ -92,3 +97,79 @@ class ProviderComputeServiceTestCase(ProviderTestBase):
                 self._is_valid_ip(ip_address),
                 self._is_valid_ip(ip_address),
                 "Instance must have a valid IP address")
                 "Instance must have a valid IP address")
             test_instance.terminate()
             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