Просмотр исходного кода

Added block_store service to EC2 provider (volumes+snapshots). Some
general consistency improvements.

nuwan_ag 10 лет назад
Родитель
Сommit
17bd41b5a0

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

@@ -9,6 +9,7 @@ from boto.ec2.regioninfo import RegionInfo
 
 from cloudbridge.providers.base import BaseCloudProvider
 
+from .services import AWSBlockStoreService
 from .services import AWSComputeService
 from .services import AWSImageService
 from .services import AWSSecurityService
@@ -39,7 +40,7 @@ class AWSCloudProviderV1(BaseCloudProvider):
         self._compute = AWSComputeService(self)
         self._images = AWSImageService(self)
         self._security = AWSSecurityService(self)
-        self._block_store = None  # AWSBlockStoreService(self)
+        self._block_store = AWSBlockStoreService(self)
         self._object_store = None  # AWSObjectStore(self)
 
     @property

+ 181 - 1
cloudbridge/providers/aws/resources.py

@@ -6,9 +6,14 @@ from cloudbridge.providers.base import BaseInstance
 from cloudbridge.providers.base import BaseKeyPair
 from cloudbridge.providers.base import BaseMachineImage
 from cloudbridge.providers.base import BaseSecurityGroup
+from cloudbridge.providers.base import BaseSnapshot
+from cloudbridge.providers.base import BaseVolume
 from cloudbridge.providers.interfaces import InstanceState
 from cloudbridge.providers.interfaces import InstanceType
 from cloudbridge.providers.interfaces import MachineImageState
+from cloudbridge.providers.interfaces import SnapshotState
+from cloudbridge.providers.interfaces import VolumeState
+from cloudbridge.providers.interfaces.resources import PlacementZone
 
 
 class AWSMachineImage(BaseMachineImage):
@@ -76,6 +81,36 @@ class AWSMachineImage(BaseMachineImage):
             self.image_id)._ec2_image
 
 
+class AWSPlacementZone(PlacementZone):
+
+    def __init__(self, provider, zone):
+        self.provider = provider
+        if isinstance(zone, AWSPlacementZone):
+            self._aws_zone = zone._aws_zone
+        else:
+            self._aws_zone = zone
+
+    @property
+    def name(self):
+        """
+        Get the zone name.
+
+        :rtype: ``str``
+        :return: Name for this zone as returned by the cloud middleware.
+        """
+        return self._aws_zone
+
+    @property
+    def region(self):
+        """
+        Get the region that this zone belongs to.
+
+        :rtype: ``str``
+        :return: Name of this zone's region as returned by the cloud middleware
+        """
+        return self._aws_zone.region_name
+
+
 class AWSInstanceType(InstanceType):
 
     def __init__(self, instance_type):
@@ -177,7 +212,7 @@ class AWSInstance(BaseInstance):
         """
         Get the placement zone where this instance is running.
         """
-        return self._ec2_instance.placement
+        return AWSPlacementZone(self.provider, self._ec2_instance.placement)
 
     @property
     def mac_address(self):
@@ -221,3 +256,148 @@ class AWSInstance(BaseInstance):
         for its latest state.
         """
         self._ec2_instance.update()
+
+    def __repr__(self):
+        return "<CB-AWSInstance: {0}({1})>".format(self.name, self.instance_id)
+
+
+class AWSVolume(BaseVolume):
+
+    # Ref:
+    # http://docs.aws.amazon.com/AWSEC2/latest/CommandLineReference/
+    # ApiReference-cmd-DescribeVolumes.html
+    VOLUME_STATE_MAP = {
+        'creating': VolumeState.CREATING,
+        'available': VolumeState.AVAILABLE,
+        'in-use': VolumeState.IN_USE,
+        'deleting': VolumeState.CONFIGURING,
+        'deleted': VolumeState.DELETED,
+        'error': VolumeState.ERROR
+    }
+
+    def __init__(self, provider, volume):
+        self.provider = provider
+        self._volume = volume
+
+    @property
+    def volume_id(self):
+        return self._volume.id
+
+    @property
+    def name(self):
+        """
+        Get the volume name.
+
+        .. note:: an instance must have a (case sensitive) tag ``Name``
+        """
+        return self._volume.tags.get('Name')
+
+    @name.setter
+    def name(self, value):
+        """
+        Set the volume name.
+        """
+        self._volume.add_tag('Name', value)
+
+    def attach(self, instance, device):
+        """
+        Attach this volume to an instance.
+        """
+        instance_id = instance.instance_id if isinstance(
+            instance,
+            AWSInstance) else instance
+        self._volume.attach(instance_id, device)
+
+    def detach(self, force=False):
+        """
+        Detach this volume from an instance.
+        """
+        self._volume.detach()
+
+    def create_snapshot(self, name, description=None):
+        """
+        Create a snapshot of this Volume.
+        """
+        snap = AWSSnapshot(
+            self.provider,
+            self._volume.create_snapshot(
+                description=description))
+        snap.name = name
+        return snap
+
+    def delete(self):
+        """
+        Delete this volume.
+        """
+        self._volume.delete()
+
+    @property
+    def state(self):
+        return AWSVolume.VOLUME_STATE_MAP.get(
+            self._volume.status, VolumeState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this volume by re-querying the cloud provider
+        for its latest state.
+        """
+        self._volume.update()
+
+    def __repr__(self):
+        return "<CB-AWSVolume: {0} ({1})>".format(self.volume_id, self.name)
+
+
+class AWSSnapshot(BaseSnapshot):
+
+    # Ref: http://docs.aws.amazon.com/AWSEC2/latest/CommandLineReference/
+    # ApiReference-cmd-DescribeSnapshots.html
+    SNAPSHOT_STATE_MAP = {
+        'pending': SnapshotState.PENDING,
+        'completed': SnapshotState.AVAILABLE,
+        'error': SnapshotState.ERROR
+    }
+
+    def __init__(self, provider, snapshot):
+        self.provider = provider
+        self._snapshot = snapshot
+
+    @property
+    def snapshot_id(self):
+        return self._snapshot.id
+
+    @property
+    def name(self):
+        """
+        Get the snapshot name.
+
+        .. note:: an instance must have a (case sensitive) tag ``Name``
+        """
+        return self._snapshot.tags.get('Name')
+
+    @name.setter
+    def name(self, value):
+        """
+        Set the snapshot name.
+        """
+        self._snapshot.add_tag('Name', value)
+
+    @property
+    def state(self):
+        return AWSSnapshot.SNAPSHOT_STATE_MAP.get(
+            self._snapshot.status, SnapshotState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this snapshot by re-querying the cloud provider
+        for its latest state.
+        """
+        self._snapshot.update()
+
+    def delete(self):
+        """
+        Delete this snapshot.
+        """
+        self._snapshot.delete()
+
+    def __repr__(self):
+        return "<CB-AWSVolume: {0} ({1}>".format(self.snapshot_id, self.name)

+ 110 - 0
cloudbridge/providers/aws/services.py

@@ -4,6 +4,7 @@ Services implemented by this provider
 
 from cloudbridge.providers.base import BaseKeyPair
 from cloudbridge.providers.base import BaseSecurityGroup
+from cloudbridge.providers.interfaces import BlockStoreService
 from cloudbridge.providers.interfaces import ComputeService
 from cloudbridge.providers.interfaces import ImageService
 from cloudbridge.providers.interfaces import InstanceType
@@ -12,9 +13,13 @@ from cloudbridge.providers.interfaces import MachineImage
 from cloudbridge.providers.interfaces import PlacementZone
 from cloudbridge.providers.interfaces import SecurityGroup
 from cloudbridge.providers.interfaces import SecurityService
+from cloudbridge.providers.interfaces import SnapshotService
+from cloudbridge.providers.interfaces import VolumeService
 
 from .resources import AWSInstance
 from .resources import AWSMachineImage
+from .resources import AWSSnapshot
+from .resources import AWSVolume
 
 
 class AWSSecurityService(SecurityService):
@@ -43,6 +48,111 @@ class AWSSecurityService(SecurityService):
         return [BaseSecurityGroup(group.name) for group in groups]
 
 
+class AWSBlockStoreService(BlockStoreService):
+
+    def __init__(self, provider):
+        self.provider = provider
+
+        # Initialize provider services
+        self._volumes = AWSVolumeService(self.provider)
+        self._snapshots = AWSSnapshotService(self.provider)
+
+    @property
+    def volumes(self):
+        return self._volumes
+
+    @property
+    def snapshots(self):
+        return self._snapshots
+
+
+class AWSVolumeService(VolumeService):
+
+    def __init__(self, provider):
+        self.provider = provider
+
+    def get_volume(self, volume_id):
+        """
+        Returns a volume given its id.
+        """
+        vols = self.provider.ec2_conn.get_all_volumes(volume_ids=[volume_id])
+        return AWSVolume(self.provider, vols[0]) if vols else None
+
+    def find_volume(self, name):
+        """
+        Searches for a volume by a given list of attributes.
+        """
+        raise NotImplementedError(
+            'find_volume not implemented by this provider')
+
+    def list_volumes(self):
+        """
+        List all volumes.
+        """
+        return [AWSVolume(self.provider, vol)
+                for vol in self.provider.ec2_conn.get_all_volumes()]
+
+    def create_volume(self, name, size, zone, snapshot=None):
+        """
+        Creates a new volume.
+        """
+        zone_name = zone.name if isinstance(zone, PlacementZone) else zone
+        snapshot_id = snapshot.id if isinstance(
+            zone, AWSSnapshot) and snapshot else snapshot
+
+        ec2_vol = self.provider.ec2_conn.create_volume(
+            size,
+            zone_name,
+            snapshot=snapshot_id)
+        cb_vol = AWSVolume(self.provider, ec2_vol)
+        cb_vol.name = name
+        return cb_vol
+
+
+class AWSSnapshotService(SnapshotService):
+
+    def __init__(self, provider):
+        self.provider = provider
+
+    def get_snapshot(self, snapshot_id):
+        """
+        Returns a snapshot given its id.
+        """
+        snaps = self.provider.ec2_conn.get_all_snapshots(
+            snapshot_ids=[snapshot_id])
+        return AWSSnapshot(self.provider, snaps[0]) if snaps else None
+
+    def find_snapshot(self, name):
+        """
+        Searches for a volume by a given list of attributes.
+        """
+        raise NotImplementedError(
+            'find_volume not implemented by this provider')
+
+    def list_snapshots(self):
+        """
+        List all snapshot.
+        """
+        # TODO: get_all_images returns too many images - some kind of filtering
+        # abilities are needed. Forced to "self" for now
+        return [AWSSnapshot(self.provider, snap)
+                for snap in
+                self.provider.ec2_conn.get_all_snapshots(owner="self")]
+
+    def create_snapshot(self, name, volume, description=None):
+        """
+        Creates a new snapshot of a given volume.
+        """
+        volume_id = volume.id if isinstance(volume, AWSVolume) else volume
+
+        ec2_snap = self.provider.ec2_conn.create_snapshot(
+            volume_id,
+            description=description)
+        cb_snap = AWSSnapshot(self.provider, ec2_snap)
+        cb_snap.name = name
+        return cb_snap
+
+
 class AWSImageService(ImageService):
 
     def __init__(self, provider):

+ 64 - 8
cloudbridge/providers/base.py

@@ -13,6 +13,10 @@ from cloudbridge.providers.interfaces import MachineImage
 from cloudbridge.providers.interfaces import MachineImageState
 from cloudbridge.providers.interfaces import ObjectLifeCycleMixin
 from cloudbridge.providers.interfaces import SecurityGroup
+from cloudbridge.providers.interfaces import Snapshot
+from cloudbridge.providers.interfaces import SnapshotState
+from cloudbridge.providers.interfaces import Volume
+from cloudbridge.providers.interfaces import VolumeState
 from cloudbridge.providers.interfaces import WaitStateException
 
 
@@ -94,25 +98,27 @@ class BaseObjectLifeCycleMixin(ObjectLifeCycleMixin):
             "terminal_states not implemented by this object. Subclasses must"
             " implement this method and return a valid set of terminal states")
 
-    def wait_till_ready(self, timeout=600, interval=5):
+    def wait_for(self, target_states, terminal_states=None, timeout=600,
+                 interval=5):
         assert timeout > 0
         assert timeout > interval
         assert interval > 0
 
         for time_left in range(timeout, 0, -interval):
-            if self.state in self.ready_states:
+            if self.state in target_states:
                 return True
-            elif self.state in self.terminal_states:
+            elif self.state in terminal_states:
                 raise WaitStateException(
                     "Object: {0} is in state: {1} which is a terminal state"
                     " and cannot be waited on.".format(self, self.state))
             else:
                 log.debug(
                     "Object {0} is in state: {1}. Waiting another {2} seconds"
-                    " to reach state a ready state...".format(
+                    " to reach target state(s): {3}...".format(
                         self,
                         self.state,
-                        time_left))
+                        time_left,
+                        target_states))
                 time.sleep(interval)
             self.refresh()
 
@@ -120,6 +126,13 @@ class BaseObjectLifeCycleMixin(ObjectLifeCycleMixin):
                                  " ready. It's still  in state: {1}".format(
                                      self, self.state))
 
+    def wait_till_ready(self, timeout=600, interval=5):
+        self.wait_for(
+            self.ready_states,
+            self.terminal_states,
+            timeout,
+            interval)
+
 
 class BaseInstance(BaseObjectLifeCycleMixin, Instance):
 
@@ -143,11 +156,47 @@ class BaseMachineImage(BaseObjectLifeCycleMixin, MachineImage):
         return [MachineImageState.ERROR]
 
 
+class BaseVolume(BaseObjectLifeCycleMixin, Volume):
+
+    @property
+    def ready_states(self):
+        return [VolumeState.AVAILABLE]
+
+    @property
+    def terminal_states(self):
+        return [VolumeState.ERROR, VolumeState.DELETED]
+
+
+class BaseSnapshot(BaseObjectLifeCycleMixin, Snapshot):
+
+    @property
+    def ready_states(self):
+        return [SnapshotState.AVAILABLE]
+
+    @property
+    def terminal_states(self):
+        return [SnapshotState.ERROR]
+
+
 class BaseKeyPair(KeyPair):
 
     def __init__(self, name, material=None):
-        self.name = name
-        self.material = material
+        self._name = name
+        self._material = material
+
+    @property
+    def name(self):
+        """
+        Return the name of this key pair.
+        """
+        return self._name
+
+    @property
+    def material(self):
+        """
+        Unencrypted private key.
+        """
+        return self._material
 
     def __repr__(self):
         return "<CBKeyPair: {0}>".format(self.name)
@@ -156,7 +205,14 @@ class BaseKeyPair(KeyPair):
 class BaseSecurityGroup(SecurityGroup):
 
     def __init__(self, name):
-        self.name = name
+        self._name = name
+
+    @property
+    def name(self):
+        """
+        Return the name of this key pair.
+        """
+        return self._name
 
     def __repr__(self):
         return "<CBSecurityGroup: {0}>".format(self.name)

+ 7 - 5
cloudbridge/providers/factory.py

@@ -26,7 +26,8 @@ class CloudProviderFactory(object):
         """
         return [{"name": ProviderList.OPENSTACK,
                  "implementations":
-                 [{"class": "cloudbridge.providers.openstack.OpenStackCloudProviderV1",
+                 [{"class":
+                   "cloudbridge.providers.openstack.OpenStackCloudProviderV1",
                    "version": 1}]},
                 {"name": ProviderList.AWS,
                  "implementations":
@@ -60,8 +61,9 @@ class CloudProviderFactory(object):
                         return None
                 else:
                     # Return latest available version
-                    return sorted((item for item in provider["implementations"]),
-                                  key=lambda x: x["version"])[-1]["class"]
+                    return sorted(
+                        (item for item in provider["implementations"]),
+                        key=lambda x: x["version"])[-1]["class"]
         return None
 
     def create_provider(self, name, config, version=None):
@@ -85,8 +87,8 @@ class CloudProviderFactory(object):
         impl = self.find_provider_impl(name, version=version)
         if impl is None:
             raise NotImplementedError(
-                'A provider by name {0} implementing interface v1 could not be '
-                'found'.format(name))
+                'A provider by name {0} implementing interface v1 could not be'
+                ' found'.format(name))
         provider_class = self._get_provider_class(impl)
         return provider_class(config)
 

+ 11 - 7
cloudbridge/providers/interfaces/__init__.py

@@ -2,13 +2,6 @@
 Public interface exports
 """
 from .impl import CloudProvider
-from .services import ComputeService
-from .services import ImageService
-from .services import InstanceTypesService
-from .services import ObjectStoreService
-from .services import ProviderService
-from .services import SecurityService
-from .services import VolumeService
 from .resources import CloudProviderServiceType
 from .resources import Instance
 from .resources import InstanceState
@@ -21,5 +14,16 @@ from .resources import PlacementZone
 from .resources import Region
 from .resources import SecurityGroup
 from .resources import Snapshot
+from .resources import SnapshotState
 from .resources import Volume
+from .resources import VolumeState
 from .resources import WaitStateException
+from .services import BlockStoreService
+from .services import ComputeService
+from .services import ImageService
+from .services import InstanceTypesService
+from .services import ObjectStoreService
+from .services import ProviderService
+from .services import SecurityService
+from .services import SnapshotService
+from .services import VolumeService

+ 5 - 5
cloudbridge/providers/interfaces/impl.py

@@ -96,16 +96,16 @@ class CloudProvider(object):
             'CloudProvider.security not implemented by this provider')
 
     @property
-    def volumes(self):
+    def block_store(self):
         """
-        Provides access to the volume/elastic block store services in this
+        Provides access to the volume and snapshot services in this
         provider.
 
-        :rtype: ``object`` of :class:`.VolumeService`
-        :return: a VolumeService object
+        :rtype: ``object`` of :class:`.BlockStoreService`
+        :return: a BlockStoreService object
         """
         raise NotImplementedError(
-            'CloudProvider.volumes not implemented by this provider')
+            'CloudProvider.block_store not implemented by this provider')
 
     @property
     def object_store(self):

+ 88 - 6
cloudbridge/providers/interfaces/resources.py

@@ -118,6 +118,7 @@ class InstanceState(object):
 
 class Instance(ObjectLifeCycleMixin):
 
+    @property
     def instance_id(self):
         """
         Get the instance identifier.
@@ -128,6 +129,7 @@ class Instance(ObjectLifeCycleMixin):
         raise NotImplementedError(
             'Instance.instance_id not implemented by this provider')
 
+    @property
     def name(self):
         """
         Get the instance name.
@@ -138,6 +140,7 @@ class Instance(ObjectLifeCycleMixin):
         raise NotImplementedError(
             'Instance.name not implemented by this provider')
 
+    @property
     def public_ips(self):
         """
         Get all the public IP addresses for this instance.
@@ -148,6 +151,7 @@ class Instance(ObjectLifeCycleMixin):
         raise NotImplementedError(
             'Instance.public_ips not implemented by this provider')
 
+    @property
     def private_ips(self):
         """
         Get all the private IP addresses for this instance.
@@ -158,6 +162,7 @@ class Instance(ObjectLifeCycleMixin):
         raise NotImplementedError(
             'Instance.private_ips not implemented by this provider')
 
+    @property
     def instance_type(self):
         """
         Get the instance type.
@@ -189,6 +194,7 @@ class Instance(ObjectLifeCycleMixin):
         raise NotImplementedError(
             'Instance.terminate not implemented by this provider')
 
+    @property
     def image_id(self):
         """
         Get the image ID for this insance.
@@ -199,6 +205,7 @@ class Instance(ObjectLifeCycleMixin):
         raise NotImplementedError(
             'Instance.image_id not implemented by this provider')
 
+    @property
     def placement_zone(self):
         """
         Get the placement zone where this instance is running.
@@ -209,6 +216,7 @@ class Instance(ObjectLifeCycleMixin):
         raise NotImplementedError(
             'Instance.placement not implemented by this provider')
 
+    @property
     def mac_address(self):
         """
         Get the MAC address for this instance.
@@ -219,6 +227,7 @@ class Instance(ObjectLifeCycleMixin):
         raise NotImplementedError(
             'Instance.mac_address not implemented by this provider')
 
+    @property
     def security_group_ids(self):
         """
         Get the security group IDs associated with this instance.
@@ -229,6 +238,7 @@ class Instance(ObjectLifeCycleMixin):
         raise NotImplementedError(
             'Instance.security_group_ids not implemented by this provider')
 
+    @property
     def key_pair_name(self):
         """
         Get the name of the key pair associated with this instance.
@@ -312,8 +322,53 @@ class MachineImage(ObjectLifeCycleMixin):
             'MachineImage.delete not implemented by this provider')
 
 
+class VolumeState(object):
+
+    """
+    Standard states for a volume
+
+    :cvar UNKNOWN: Volume state unknown.
+    :cvar CREATING: Volume is being created.
+    :cvar CONFIGURING: Volume is being configured in some way.
+    :cvar AVAILABLE: Volume is available and can be attached to an instance.
+    :cvar IN_USE: Volume is attached and in-use.
+    :cvar DELETED: Volume has been deleted. No further operations possible.
+    :cvar ERROR: Volume is in an error state. No further operations possible.
+
+    """
+    UNKNOWN = "unknown"
+    CREATING = "creating"
+    CONFIGURING = "configuring"
+    AVAILABLE = "available"
+    IN_USE = "in-use"
+    DELETED = "deleted"
+    ERROR = "error"
+
+
 class Volume(ObjectLifeCycleMixin):
 
+    @property
+    def volume_id(self):
+        """
+        Get the volume identifier.
+
+        :rtype: ``str``
+        :return: ID for this instance as returned by the cloud middleware.
+        """
+        raise NotImplementedError(
+            'Volume.volume_id not implemented by this provider')
+
+    @property
+    def name(self):
+        """
+        Get the volume name.
+
+        :rtype: ``str``
+        :return: Name for this volume as returned by the cloud middleware.
+        """
+        raise NotImplementedError(
+            'Volume.name not implemented by this provider')
+
     def attach(self, instance_id, device):
         """
         Attach this volume to an instance.
@@ -330,7 +385,7 @@ class Volume(ObjectLifeCycleMixin):
         :return: True if successful
         """
         raise NotImplementedError(
-            'attach not implemented by this provider')
+            'Volume.attach not implemented by this provider')
 
     def detach(self, force=False):
         """
@@ -350,9 +405,9 @@ class Volume(ObjectLifeCycleMixin):
         :return: True if successful
         """
         raise NotImplementedError(
-            'detach not implemented by this provider')
+            'Volume.detach not implemented by this provider')
 
-    def snapshot(self, description=None):
+    def create_snapshot(self, description=None):
         """
         Create a snapshot of this Volume.
 
@@ -364,7 +419,7 @@ class Volume(ObjectLifeCycleMixin):
         :return: The created Snapshot object
         """
         raise NotImplementedError(
-            'snapshot not implemented by this provider')
+            'Volume.snapshot not implemented by this provider')
 
     def delete(self):
         """
@@ -374,7 +429,26 @@ class Volume(ObjectLifeCycleMixin):
         :return: True if successful
         """
         raise NotImplementedError(
-            'delete not implemented by this provider')
+            'Volume.delete not implemented by this provider')
+
+
+class SnapshotState(object):
+
+    """
+    Standard states for a snapshot
+
+    :cvar UNKNOWN: Snapshot state unknown.
+    :cvar PENDING: Snapshot is pending.
+    :cvar CONFIGURING: Snapshot is being configured in some way.
+    :cvar AVAILABLE: Snapshot has been completed and is ready for use.
+    :cvar ERROR: Snapshot is in an error state. No further operations possible.
+
+    """
+    UNKNOWN = "unknown"
+    PENDING = "pending"
+    CONFIGURING = "configuring"
+    AVAILABLE = "available"
+    ERROR = "error"
 
 
 class Snapshot(ObjectLifeCycleMixin):
@@ -443,6 +517,7 @@ class Snapshot(ObjectLifeCycleMixin):
 
 class KeyPair(object):
 
+    @property
     def name(self):
         """
         Return the name of this key pair.
@@ -453,6 +528,7 @@ class KeyPair(object):
         raise NotImplementedError(
             'name not implemented by this provider')
 
+    @property
     def material(self):
         """
         Unencrypted private key.
@@ -471,6 +547,7 @@ class Region(object):
     contain at least one placement zone.
     """
 
+    @property
     def name(self):
         """
         Name of the region.
@@ -498,6 +575,7 @@ class PlacementZone(object):
     Represents a placement zone. A placement zone is contained within a Region.
     """
 
+    @property
     def name(self):
         """
         Name of the placement zone.
@@ -508,7 +586,8 @@ class PlacementZone(object):
         raise NotImplementedError(
             'PlacementZone.name not implemented by this provider')
 
-    def region_name(self):
+    @property
+    def region(self):
         """
         A region this placement zone is associated with.
 
@@ -525,10 +604,12 @@ class InstanceType(object):
     An instance type object.
     """
 
+    @property
     def id(self):
         raise NotImplementedError(
             'InstanceType.id not implemented by this provider')
 
+    @property
     def name(self):
         raise NotImplementedError(
             'InstanceType.name not implemented by this provider')
@@ -536,6 +617,7 @@ class InstanceType(object):
 
 class SecurityGroup(object):
 
+    @property
     def name(self):
         """
         Return the name of this security group.

+ 102 - 10
cloudbridge/providers/interfaces/services.py

@@ -148,8 +148,8 @@ class VolumeService(ProviderService):
         """
         Searches for a volume by a given list of attributes.
 
-        :rtype: ``object`` of :class:`.Instance`
-        :return: an Instance object or ``None`` if not found
+        :rtype: ``object`` of :class:`.Volume`
+        :return: a Volume object or ``None`` if not found
         """
         raise NotImplementedError(
             'find_volume not implemented by this provider')
@@ -164,25 +164,117 @@ class VolumeService(ProviderService):
         raise NotImplementedError(
             'list_volumes not implemented by this provider')
 
-    def list_volume_snapshots(self):
+    def create_volume(self, name, size, zone, snapshot=None, description=None):
+        """
+        Creates a new volume.
+
+        :type  name: ``str``
+        :param name: The name of the volume
+
+        :type  size: ``int``
+        :param size: The size of the volume (in GB)
+
+        :type  zone: ``str`` or ``PlacementZone``
+        :param zone: The availability zone in which the Volume will be created.
+
+        :type  description: ``str``
+        :param description: An optional description that may be supported by
+        some providers. Providers that do not support this property will return
+        None.
+
+        :rtype: ``object`` of :class:`.Volume`
+        :return: a newly created Volume object
+        """
+        raise NotImplementedError(
+            'create_volume not implemented by this provider')
+
+
+class SnapshotService(ProviderService):
+
+    """
+    Base interface for a Snapshot Service
+    """
+
+    def get_snapshot(self, volume_id):
+        """
+        Returns a snapshot given its id.
+
+        :rtype: ``object`` of :class:`.Snapshot`
+        :return: a Snapshot object
+        """
+        raise NotImplementedError(
+            'get_snapshot not implemented by this provider')
+
+    def find_snapshot(self, name):
+        """
+        Searches for a snapshot by a given list of attributes.
+
+        :rtype: ``object`` of :class:`.Snapshot`
+        :return: a Snapshot object or ``None`` if not found
+        """
+        raise NotImplementedError(
+            'find_snapshot not implemented by this provider')
+
+    def list_snapshots(self):
         """
-        List all volume snapshots.
+        List all snapshots.
 
         :rtype: ``list`` of :class:`.Snapshot`
         :return: a list of Snapshot objects
         """
         raise NotImplementedError(
-            'list_volume_snapshots not implemented by this provider')
+            'list_snapshots not implemented by this provider')
 
-    def create_volume(self):
+    def create_snapshot(self, name, volume, description=None):
         """
-        Creates a new volume.
+        Creates a new snapshot off a volume.
 
-        :rtype: ``list`` of :class:`.Volume`
-        :return: a newly created Volume object
+        :type  name: ``str``
+        :param name: The name of the snapshot
+
+        :type  volume: ``str`` or ``Volume``
+        :param volume: The volume to create a snapshot of.
+
+        :type  description: ``str``
+        :param description: An optional description that may be supported by
+        some providers. Providers that do not support this property will return
+        None.
+
+        :rtype: ``object`` of :class:`.Snapshot`
+        :return: a newly created Snapshot object
         """
         raise NotImplementedError(
-            'create_volume not implemented by this provider')
+            'create_snapshot not implemented by this provider')
+
+
+class BlockStoreService(ProviderService):
+
+    """
+    Base interface for a Block Store Service
+    """
+
+    @property
+    def volumes(self):
+        """
+        Provides access to the volume and snapshot services in this
+        provider.
+
+        :rtype: ``object`` of :class:`.BlockStoreService`
+        :return: a BlockStoreService object
+        """
+        raise NotImplementedError(
+            'CloudProvider.block_store not implemented by this provider')
+
+    @property
+    def snapshots(self):
+        """
+        Provides access to object storage services in this provider.
+
+        :rtype: ``object`` of :class:`.ObjectStoreService`
+        :return: an ObjectStoreService object
+        """
+        raise NotImplementedError(
+            'CloudProvider.object_store not implemented by this provider')
 
 
 class ImageService(ProviderService):

+ 0 - 5
cloudbridge/providers/openstack/impl.py

@@ -41,7 +41,6 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
         self._security = OpenStackSecurityService(self)
         self._block_store = None  # OpenStackBlockStore(self)
         self._object_store = None  # OpenStackObjectStore(self)
-        self._volumes = None  # OpenStackVolumeService(self)
 
     @property
     def compute(self):
@@ -63,10 +62,6 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
     def object_store(self):
         return self._object_store
 
-    @property
-    def volumes(self):
-        return self._volumes
-
     def _connect_nova(self):
         """
         Get an openstack client object for the given cloud.

+ 36 - 1
cloudbridge/providers/openstack/resources.py

@@ -9,6 +9,7 @@ from cloudbridge.providers.base import BaseSecurityGroup
 from cloudbridge.providers.interfaces import InstanceState
 from cloudbridge.providers.interfaces import InstanceType
 from cloudbridge.providers.interfaces import MachineImageState
+from cloudbridge.providers.interfaces import PlacementZone
 from cloudbridge.providers.interfaces import Region
 
 
@@ -81,6 +82,36 @@ class OpenStackMachineImage(BaseMachineImage):
         self._os_image = image._os_image
 
 
+class OpenStackPlacementZone(PlacementZone):
+
+    def __init__(self, provider, zone):
+        self.provider = provider
+        if isinstance(zone, OpenStackPlacementZone):
+            self._os_zone = zone._os_zone
+        else:
+            self._os_zone = zone
+
+    @property
+    def name(self):
+        """
+        Get the zone name.
+
+        :rtype: ``str``
+        :return: Name for this zone as returned by the cloud middleware.
+        """
+        return self._os_zone.zoneName
+
+    @property
+    def region(self):
+        """
+        Get the region that this zone belongs to.
+
+        :rtype: ``str``
+        :return: Name of this zone's region as returned by the cloud middleware
+        """
+        return self._os_zone.region_name
+
+
 class OpenStackInstanceType(InstanceType):
 
     def __init__(self, os_flavor):
@@ -193,7 +224,8 @@ class OpenStackInstance(BaseInstance):
         """
         Get the placement zone where this instance is running.
         """
-        return self._os_instance.availability_zone
+        return OpenStackPlacementZone(
+            self.provider, self._os_instance.availability_zone)
 
     @property
     def mac_address(self):
@@ -239,6 +271,9 @@ class OpenStackInstance(BaseInstance):
         self._os_instance = self.provider.compute.get_instance(
             self.instance_id)._os_instance
 
+    def __repr__(self):
+        return "<CB-OSInstance: {0}({1})>".format(self.name, self.instance_id)
+
 
 class OpenStackRegion(Region):
 

+ 4 - 1
test/__init__.py

@@ -28,6 +28,8 @@ as a base class to each combination).
 import cloudbridge
 from test.helpers import ProviderTestCaseGenerator
 from test.test_compute_service import ProviderComputeServiceTestCase
+from test.test_provider_block_store_service import \
+    ProviderBlockStoreServiceTestCase
 from test.test_provider_image_service import ProviderImageServiceTestCase
 from test.test_provider_interface import ProviderInterfaceTestCase
 from test.test_provider_security_service import ProviderSecurityServiceTestCase
@@ -37,7 +39,8 @@ PROVIDER_TESTS = [
     ProviderInterfaceTestCase,
     ProviderSecurityServiceTestCase,
     ProviderComputeServiceTestCase,
-    ProviderImageServiceTestCase
+    ProviderImageServiceTestCase,
+    ProviderBlockStoreServiceTestCase
 ]
 
 

+ 4 - 0
test/helpers.py

@@ -106,6 +106,10 @@ class ProviderTestCaseGenerator():
         provider_name = os.environ.get("CB_TEST_PROVIDER", None)
         if provider_name:
             provider_classes = [factory.get_provider_class(provider_name)]
+            if not provider_classes[0]:
+                raise ValueError(
+                    "Could not find specified test provider %s" %
+                    provider_name)
         else:
             provider_classes = factory.get_all_provider_classes()
         suite = unittest.TestSuite()

+ 1 - 1
test/test_compute_service.py

@@ -19,4 +19,4 @@ class ProviderComputeServiceTestCase(ProviderTestBase):
         """
         instance_state should return an object of type InstanceState
         """
-        pass
+        pass

+ 92 - 0
test/test_provider_block_store_service.py

@@ -0,0 +1,92 @@
+import uuid
+
+from cloudbridge.providers.interfaces import SnapshotState
+from cloudbridge.providers.interfaces import VolumeState
+from test.helpers import ProviderTestBase
+import test.helpers
+
+
+class ProviderBlockStoreServiceTestCase(ProviderTestBase):
+
+    def __init__(self, methodName, provider):
+        super(ProviderBlockStoreServiceTestCase, self).__init__(
+            methodName=methodName, provider=provider)
+
+    def setUp(self):
+        self.instance = test.helpers.get_test_instance(self.provider)
+
+    def tearDown(self):
+        self.instance.terminate()
+
+    def test_crud_volume(self):
+        """
+        Create a new volume, check whether the expected values are set,
+        and delete it
+        """
+        name = "CBUnitTestCreateVol-{0}".format(uuid.uuid4())
+        test_vol = self.provider.block_store.volumes.create_volume(
+            name,
+            1,
+            self.instance.placement_zone)
+        try:
+            test_vol.wait_till_ready()
+            volumes = self.provider.block_store.volumes.list_volumes()
+            found_volumes = [vol for vol in volumes if vol.name == name]
+            self.assertTrue(
+                len(found_volumes) == 1,
+                "List volumes does not return the expected volume %s" %
+                name)
+        finally:
+            test_vol.delete()
+
+    def test_attach_detach_volume(self):
+        """
+        Create a new volume, and attempt to attach it to an instance
+        """
+        name = "CBUnitTestAttachVol-{0}".format(uuid.uuid4())
+        test_vol = self.provider.block_store.volumes.create_volume(
+            name, 1, self.instance.placement_zone)
+        try:
+            test_vol.wait_till_ready()
+            test_vol.attach(self.instance, '/dev/sda2')
+            test_vol.wait_for(
+                [VolumeState.IN_USE], terminal_states=[VolumeState.ERROR,
+                                                       VolumeState.DELETED])
+            test_vol.detach()
+            test_vol.wait_for(
+                [VolumeState.AVAILABLE], terminal_states=[VolumeState.ERROR,
+                                                          VolumeState.DELETED])
+        finally:
+            test_vol.delete()
+
+    def test_crud_snapshot(self):
+        """
+        Create a new volume, create a snapshot of the volume, and check
+        whether list_snapshots properly detects the new snapshot.
+        Delete everything afterwards.
+        """
+        name = "CBUnitTestCreateSnap-{0}".format(uuid.uuid4())
+        test_vol = self.provider.block_store.volumes.create_volume(
+            name,
+            1,
+            self.instance.placement_zone)
+        try:
+            test_vol.wait_till_ready()
+            snap_name = "CBSnapshot-{0}".format(name)
+            test_snap = test_vol.create_snapshot(name=snap_name,
+                                                 description=snap_name)
+            try:
+                test_snap.wait_for(
+                    [SnapshotState.AVAILABLE],
+                    terminal_states=[SnapshotState.ERROR])
+                snaps = self.provider.block_store.snapshots.list_snapshots()
+                found_snaps = [snap for snap in snaps
+                               if snap.name == snap_name]
+                self.assertTrue(
+                    len(found_snaps) == 1,
+                    "List snapshots does not return the expected volume %s" %
+                    name)
+            finally:
+                test_snap.delete()
+        finally:
+            test_vol.delete()

+ 22 - 11
test/test_provider_factory.py

@@ -11,31 +11,42 @@ class ProviderFactoryTestCase(unittest.TestCase):
         Creating a provider with a known name should return
         a valid implementation
         """
-        self.assertIsInstance(CloudProviderFactory().create_provider(factory.ProviderList.AWS, {}, version=1),
-                              interfaces.CloudProvider,
-                              "create_provider did not return a valid instance type")
+        self.assertIsInstance(CloudProviderFactory().create_provider(
+            factory.ProviderList.AWS, {}, version=1),
+            interfaces.CloudProvider,
+            "create_provider did not return a valid instance type")
 
     def test_create_provider_invalid(self):
         """
-        Creating a provider with an invalid name should raise a NotImplementedError
+        Creating a provider with an invalid name should raise a
+        NotImplementedError
         """
         with self.assertRaises(NotImplementedError):
             CloudProviderFactory().create_provider("ec23", {})
         with self.assertRaises(NotImplementedError):
-            CloudProviderFactory().create_provider(factory.ProviderList.AWS, {}, version=100)
+            CloudProviderFactory().create_provider(
+                factory.ProviderList.AWS,
+                {},
+                version=100)
 
     def test_find_provider_impl_valid(self):
         """
-        Searching for a provider with a known name or version should return a valid implementation
+        Searching for a provider with a known name or version should return a
+        valid implementation
         """
-        self.assertTrue(CloudProviderFactory().find_provider_impl(factory.ProviderList.AWS))
-        self.assertEqual(CloudProviderFactory().find_provider_impl(factory.ProviderList.AWS, version=1),
-                         "cloudbridge.providers.aws.AWSCloudProviderV1")
+        self.assertTrue(
+            CloudProviderFactory().find_provider_impl(
+                factory.ProviderList.AWS))
+        self.assertEqual(CloudProviderFactory().find_provider_impl(
+            factory.ProviderList.AWS, version=1),
+            "cloudbridge.providers.aws.AWSCloudProviderV1")
 
     def test_find_provider_impl_invalid(self):
         """
-        Searching for a provider with an invalid name or version should return None
+        Searching for a provider with an invalid name or version should return
+        None
         """
-        self.assertIsNone(CloudProviderFactory().find_provider_impl("openstack1"))
+        self.assertIsNone(
+            CloudProviderFactory().find_provider_impl("openstack1"))
         self.assertIsNone(CloudProviderFactory().find_provider_impl(
             factory.ProviderList.AWS, version=100))

+ 1 - 1
test/test_provider_interface.py

@@ -29,4 +29,4 @@ class ProviderInterfaceTestCase(ProviderTestBase):
         """
         self.assertFalse(
             self.provider.has_service("NON_EXISTENT_SERVICE"),
-            "has_service should not return True for a non-existent service")
+            "has_service should not return True for a non-existent service")

+ 1 - 1
test/test_provider_security_service.py

@@ -15,7 +15,7 @@ class ProviderSecurityServiceTestCase(ProviderTestBase):
         self.assertIsNotNone(key_pairs[0].name)
 
     def test_crud_security_groups(self):
-        #groups = self.provider.security.create_security_group()
+        # groups = self.provider.security.create_security_group()
         groups = self.provider.security.list_security_groups()
         # Assume there's always one keypair at least
 #         self.assertIsInstance(groups[0], interfaces.KeyPair)