Browse Source

Added volume+snapshot support to openstack

nuwan_ag 10 years ago
parent
commit
888126dd2a

+ 14 - 3
cloudbridge/providers/openstack/impl.py

@@ -5,6 +5,7 @@ compatible clouds.
 
 import os
 
+from cinderclient import client as cinder_client
 from keystoneclient import client as keystone_client
 from keystoneclient import session
 from keystoneclient.auth.identity import Password
@@ -12,6 +13,7 @@ from novaclient import client as nova_client
 
 from cloudbridge.providers.base import BaseCloudProvider
 
+from .services import OpenStackBlockStoreService
 from .services import OpenStackComputeService
 from .services import OpenStackImageService
 from .services import OpenStackSecurityService
@@ -35,11 +37,12 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
 
         self.nova = self._connect_nova()
         self.keystone = self._connect_keystone()
+        self.cinder = self._connect_cinder()
 
         self._compute = OpenStackComputeService(self)
         self._images = OpenStackImageService(self)
         self._security = OpenStackSecurityService(self)
-        self._block_store = None  # OpenStackBlockStore(self)
+        self._block_store = OpenStackBlockStoreService(self)
         self._object_store = None  # OpenStackObjectStore(self)
 
     @property
@@ -64,7 +67,7 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
 
     def _connect_nova(self):
         """
-        Get an openstack client object for the given cloud.
+        Get an openstack nova client object for the given cloud.
         """
         return nova_client.Client(
             self.api_version, self.username, self.password, self.tenant_name,
@@ -72,7 +75,7 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
 
     def _connect_keystone(self):
         """
-        Get an openstack client object for the given cloud.
+        Get an openstack keystone client object for the given cloud.
         """
         auth = Password(self.auth_url, username=self.username,
                         password=self.password, tenant_name=self.tenant_name)
@@ -88,3 +91,11 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
             tenant_name=self.tenant_name)
         keystone.authenticate()
         return keystone
+
+    def _connect_cinder(self):
+        """
+        Get an openstack cinder client object for the given cloud.
+        """
+        return cinder_client.Client(
+            self.api_version, self.username, self.password, self.tenant_name,
+            self.auth_url)

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

@@ -2,15 +2,21 @@
 DataTypes used by this provider
 """
 
+from cinderclient.exceptions import NotFound
+
 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 PlacementZone
 from cloudbridge.providers.interfaces import Region
+from cloudbridge.providers.interfaces import SnapshotState
+from cloudbridge.providers.interfaces import VolumeState
 
 
 class OpenStackMachineImage(BaseMachineImage):
@@ -78,8 +84,13 @@ class OpenStackMachineImage(BaseMachineImage):
         Refreshes the state of this instance by re-querying the cloud provider
         for its latest state.
         """
-        image = self.provider.images.get_image(self.image_id)
-        self._os_image = image._os_image
+        try:
+            image = self.provider.images.get_image(self.image_id)
+            self._os_image = image._os_image
+        except NotFound:
+            # The image no longer exists and cannot be refreshed.
+            # set the status to unknown
+            self._os_image.status = 'unknown'
 
 
 class OpenStackPlacementZone(PlacementZone):
@@ -99,7 +110,8 @@ class OpenStackPlacementZone(PlacementZone):
         :rtype: ``str``
         :return: Name for this zone as returned by the cloud middleware.
         """
-        return self._os_zone.zoneName
+        # return self._os_zone.zoneName
+        return self._os_zone
 
     @property
     def region(self):
@@ -178,6 +190,7 @@ class OpenStackInstance(BaseInstance):
         Set the instance name.
         """
         self._os_instance.name = value
+        self._os_instance.update()
 
     @property
     def public_ips(self):
@@ -225,7 +238,8 @@ class OpenStackInstance(BaseInstance):
         Get the placement zone where this instance is running.
         """
         return OpenStackPlacementZone(
-            self.provider, self._os_instance.availability_zone)
+            self.provider,
+            getattr(self._os_instance, 'OS-EXT-AZ:availability_zone', None))
 
     @property
     def mac_address(self):
@@ -268,8 +282,13 @@ class OpenStackInstance(BaseInstance):
         Refreshes the state of this instance by re-querying the cloud provider
         for its latest state.
         """
-        self._os_instance = self.provider.compute.get_instance(
-            self.instance_id)._os_instance
+        try:
+            self._os_instance = self.provider.compute.get_instance(
+                self.instance_id)._os_instance
+        except NotFound:
+            # The instance no longer exists and cannot be refreshed.
+            # set the status to unknown
+            self._os_instance.status = 'unknown'
 
     def __repr__(self):
         return "<CB-OSInstance: {0}({1})>".format(self.name, self.instance_id)
@@ -287,3 +306,155 @@ class OpenStackRegion(Region):
 
     def __repr__(self):
         return "<CB-OSRegion: {0}>".format(self.name)
+
+
+class OpenStackVolume(BaseVolume):
+
+    # Ref: http://developer.openstack.org/api-ref-blockstorage-v2.html
+    VOLUME_STATE_MAP = {
+        'creating': VolumeState.CREATING,
+        'available': VolumeState.AVAILABLE,
+        'attaching': VolumeState.CONFIGURING,
+        'in-use': VolumeState.IN_USE,
+        'deleting': VolumeState.CONFIGURING,
+        'error': VolumeState.ERROR,
+        'error_deleting': VolumeState.ERROR,
+        'backing-up': VolumeState.CONFIGURING,
+        'restoring-backup': VolumeState.CONFIGURING,
+        'error_restoring': VolumeState.ERROR,
+        'error_extending': 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.
+        """
+        return self._volume.name
+
+    @name.setter
+    def name(self, value):
+        """
+        Set the volume name.
+        """
+        self._volume.name = value
+        self._volume.update()
+
+    def attach(self, instance, device):
+        """
+        Attach this volume to an instance.
+        """
+        instance_id = instance.instance_id if isinstance(
+            instance,
+            OpenStackInstance) 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.
+        """
+        return self.provider.block_store.snapshots.create_snapshot(
+            name, self, description=description)
+
+    def delete(self):
+        """
+        Delete this volume.
+        """
+        self._volume.delete()
+
+    @property
+    def state(self):
+        return OpenStackVolume.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.
+        """
+        try:
+            self._volume = self.provider.block_store.volumes.get_volume(
+                self.volume_id)._volume
+        except NotFound:
+            # The volume no longer exists and cannot be refreshed.
+            # set the status to unknown
+            self._volume.status = 'unknown'
+
+    def __repr__(self):
+        return "<CB-OSVolume: {0} ({1})>".format(self.volume_id, self.name)
+
+
+class OpenStackSnapshot(BaseSnapshot):
+
+    # Ref: http://developer.openstack.org/api-ref-blockstorage-v2.html
+    SNAPSHOT_STATE_MAP = {
+        'creating': SnapshotState.PENDING,
+        'available': SnapshotState.AVAILABLE,
+        'deleting': SnapshotState.CONFIGURING,
+        'error': SnapshotState.ERROR,
+        'error_deleting': 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.
+        """
+        return self._snapshot.name
+
+    @name.setter
+    def name(self, value):
+        """
+        Set the snapshot name.
+        """
+        self._snapshot.add_tag('Name', value)
+        self._snapshot.update()
+
+    @property
+    def state(self):
+        return OpenStackSnapshot.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.
+        """
+        try:
+            self._snapshot = self.provider.block_store.snapshots.get_snapshot(
+                self.snapshot_id)._snapshot
+        except NotFound:
+            # The snapshot no longer exists and cannot be refreshed.
+            # set the status to unknown
+            self._snapshot.status = 'unknown'
+
+    def delete(self):
+        """
+        Delete this snapshot.
+        """
+        self._snapshot.delete()
+
+    def __repr__(self):
+        return "<CB-OSSnapshot: {0} ({1}>".format(self.snapshot_id, self.name)

+ 102 - 0
cloudbridge/providers/openstack/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
@@ -13,11 +14,15 @@ 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 OpenStackInstance
 from .resources import OpenStackInstanceType
 from .resources import OpenStackMachineImage
 from .resources import OpenStackRegion
+from .resources import OpenStackSnapshot
+from .resources import OpenStackVolume
 
 
 class OpenStackSecurityService(SecurityService):
@@ -91,6 +96,103 @@ class OpenStackInstanceTypesService(InstanceTypesService):
             (itype for itype in self.list() if itype.name == name), None)
 
 
+class OpenStackBlockStoreService(BlockStoreService):
+
+    def __init__(self, provider):
+        self.provider = provider
+
+        # Initialize provider services
+        self._volumes = OpenStackVolumeService(self.provider)
+        self._snapshots = OpenStackSnapshotService(self.provider)
+
+    @property
+    def volumes(self):
+        return self._volumes
+
+    @property
+    def snapshots(self):
+        return self._snapshots
+
+
+class OpenStackVolumeService(VolumeService):
+
+    def __init__(self, provider):
+        self.provider = provider
+
+    def get_volume(self, volume_id):
+        """
+        Returns a volume given its id.
+        """
+        vol = self.provider.cinder.volumes.get(volume_id)
+        return OpenStackVolume(self.provider, vol) if vol 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 [OpenStackVolume(self.provider, vol)
+                for vol in self.provider.cinder.volumes.list()]
+
+    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.snapshot_id if isinstance(
+            zone, OpenStackSnapshot) and snapshot else snapshot
+
+        os_vol = self.provider.cinder.volumes.create(
+            size, name=name, availability_zone=zone_name,
+            snapshot_id=snapshot_id)
+        return OpenStackVolume(self.provider, os_vol)
+
+
+class OpenStackSnapshotService(SnapshotService):
+
+    def __init__(self, provider):
+        self.provider = provider
+
+    def get_snapshot(self, snapshot_id):
+        """
+        Returns a snapshot given its id.
+        """
+        snap = self.provider.cinder.volume_snapshots.get(snapshot_id)
+        return OpenStackSnapshot(self.provider, snap) if snap 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.
+        """
+        return [OpenStackSnapshot(self.provider, snap)
+                for snap in self.provider.cinder.volume_snapshots.list()]
+
+    def create_snapshot(self, name, volume, description=None):
+        """
+        Creates a new snapshot of a given volume.
+        """
+        volume_id = volume.volume_id if \
+            isinstance(volume, OpenStackVolume) else volume
+
+        os_snap = self.provider.cinder.volume_snapshots.create(
+            volume_id, name=name,
+            description=description)
+        return OpenStackSnapshot(self.provider, os_snap)
+
+
 class OpenStackComputeService(ComputeService):
 
     def __init__(self, provider):

+ 2 - 1
setup.py

@@ -9,7 +9,8 @@ setup(name='cloudbridge',
       author_email='support@genome.edu.au',
       url='http://cloudbridge.readthedocs.org/',
       install_requires=['bunch>=1.00', 'python-keystoneclient',
-                        'python-novaclient', 'boto'],
+                        'python-novaclient', 'python-cinderclient',
+                        'boto'],
       packages=find_packages(),
       license='MIT',
       classifiers=[