Răsfoiți Sursa

Added volume+snapshot support to openstack

nuwan_ag 10 ani în urmă
părinte
comite
888126dd2a

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

@@ -5,6 +5,7 @@ compatible clouds.
 
 
 import os
 import os
 
 
+from cinderclient import client as cinder_client
 from keystoneclient import client as keystone_client
 from keystoneclient import client as keystone_client
 from keystoneclient import session
 from keystoneclient import session
 from keystoneclient.auth.identity import Password
 from keystoneclient.auth.identity import Password
@@ -12,6 +13,7 @@ from novaclient import client as nova_client
 
 
 from cloudbridge.providers.base import BaseCloudProvider
 from cloudbridge.providers.base import BaseCloudProvider
 
 
+from .services import OpenStackBlockStoreService
 from .services import OpenStackComputeService
 from .services import OpenStackComputeService
 from .services import OpenStackImageService
 from .services import OpenStackImageService
 from .services import OpenStackSecurityService
 from .services import OpenStackSecurityService
@@ -35,11 +37,12 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
 
 
         self.nova = self._connect_nova()
         self.nova = self._connect_nova()
         self.keystone = self._connect_keystone()
         self.keystone = self._connect_keystone()
+        self.cinder = self._connect_cinder()
 
 
         self._compute = OpenStackComputeService(self)
         self._compute = OpenStackComputeService(self)
         self._images = OpenStackImageService(self)
         self._images = OpenStackImageService(self)
         self._security = OpenStackSecurityService(self)
         self._security = OpenStackSecurityService(self)
-        self._block_store = None  # OpenStackBlockStore(self)
+        self._block_store = OpenStackBlockStoreService(self)
         self._object_store = None  # OpenStackObjectStore(self)
         self._object_store = None  # OpenStackObjectStore(self)
 
 
     @property
     @property
@@ -64,7 +67,7 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
 
 
     def _connect_nova(self):
     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(
         return nova_client.Client(
             self.api_version, self.username, self.password, self.tenant_name,
             self.api_version, self.username, self.password, self.tenant_name,
@@ -72,7 +75,7 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
 
 
     def _connect_keystone(self):
     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,
         auth = Password(self.auth_url, username=self.username,
                         password=self.password, tenant_name=self.tenant_name)
                         password=self.password, tenant_name=self.tenant_name)
@@ -88,3 +91,11 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
             tenant_name=self.tenant_name)
             tenant_name=self.tenant_name)
         keystone.authenticate()
         keystone.authenticate()
         return keystone
         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
 DataTypes used by this provider
 """
 """
 
 
+from cinderclient.exceptions import NotFound
+
 from cloudbridge.providers.base import BaseInstance
 from cloudbridge.providers.base import BaseInstance
 from cloudbridge.providers.base import BaseKeyPair
 from cloudbridge.providers.base import BaseKeyPair
 from cloudbridge.providers.base import BaseMachineImage
 from cloudbridge.providers.base import BaseMachineImage
 from cloudbridge.providers.base import BaseSecurityGroup
 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 InstanceState
 from cloudbridge.providers.interfaces import InstanceType
 from cloudbridge.providers.interfaces import InstanceType
 from cloudbridge.providers.interfaces import MachineImageState
 from cloudbridge.providers.interfaces import MachineImageState
 from cloudbridge.providers.interfaces import PlacementZone
 from cloudbridge.providers.interfaces import PlacementZone
 from cloudbridge.providers.interfaces import Region
 from cloudbridge.providers.interfaces import Region
+from cloudbridge.providers.interfaces import SnapshotState
+from cloudbridge.providers.interfaces import VolumeState
 
 
 
 
 class OpenStackMachineImage(BaseMachineImage):
 class OpenStackMachineImage(BaseMachineImage):
@@ -78,8 +84,13 @@ class OpenStackMachineImage(BaseMachineImage):
         Refreshes the state of this instance by re-querying the cloud provider
         Refreshes the state of this instance by re-querying the cloud provider
         for its latest state.
         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):
 class OpenStackPlacementZone(PlacementZone):
@@ -99,7 +110,8 @@ class OpenStackPlacementZone(PlacementZone):
         :rtype: ``str``
         :rtype: ``str``
         :return: Name for this zone as returned by the cloud middleware.
         :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
     @property
     def region(self):
     def region(self):
@@ -178,6 +190,7 @@ class OpenStackInstance(BaseInstance):
         Set the instance name.
         Set the instance name.
         """
         """
         self._os_instance.name = value
         self._os_instance.name = value
+        self._os_instance.update()
 
 
     @property
     @property
     def public_ips(self):
     def public_ips(self):
@@ -225,7 +238,8 @@ class OpenStackInstance(BaseInstance):
         Get the placement zone where this instance is running.
         Get the placement zone where this instance is running.
         """
         """
         return OpenStackPlacementZone(
         return OpenStackPlacementZone(
-            self.provider, self._os_instance.availability_zone)
+            self.provider,
+            getattr(self._os_instance, 'OS-EXT-AZ:availability_zone', None))
 
 
     @property
     @property
     def mac_address(self):
     def mac_address(self):
@@ -268,8 +282,13 @@ class OpenStackInstance(BaseInstance):
         Refreshes the state of this instance by re-querying the cloud provider
         Refreshes the state of this instance by re-querying the cloud provider
         for its latest state.
         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):
     def __repr__(self):
         return "<CB-OSInstance: {0}({1})>".format(self.name, self.instance_id)
         return "<CB-OSInstance: {0}({1})>".format(self.name, self.instance_id)
@@ -287,3 +306,155 @@ class OpenStackRegion(Region):
 
 
     def __repr__(self):
     def __repr__(self):
         return "<CB-OSRegion: {0}>".format(self.name)
         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 BaseKeyPair
 from cloudbridge.providers.base import BaseSecurityGroup
 from cloudbridge.providers.base import BaseSecurityGroup
+from cloudbridge.providers.interfaces import BlockStoreService
 from cloudbridge.providers.interfaces import ComputeService
 from cloudbridge.providers.interfaces import ComputeService
 from cloudbridge.providers.interfaces import ImageService
 from cloudbridge.providers.interfaces import ImageService
 from cloudbridge.providers.interfaces import InstanceType
 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 PlacementZone
 from cloudbridge.providers.interfaces import SecurityGroup
 from cloudbridge.providers.interfaces import SecurityGroup
 from cloudbridge.providers.interfaces import SecurityService
 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 OpenStackInstance
 from .resources import OpenStackInstanceType
 from .resources import OpenStackInstanceType
 from .resources import OpenStackMachineImage
 from .resources import OpenStackMachineImage
 from .resources import OpenStackRegion
 from .resources import OpenStackRegion
+from .resources import OpenStackSnapshot
+from .resources import OpenStackVolume
 
 
 
 
 class OpenStackSecurityService(SecurityService):
 class OpenStackSecurityService(SecurityService):
@@ -91,6 +96,103 @@ class OpenStackInstanceTypesService(InstanceTypesService):
             (itype for itype in self.list() if itype.name == name), None)
             (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):
 class OpenStackComputeService(ComputeService):
 
 
     def __init__(self, provider):
     def __init__(self, provider):

+ 2 - 1
setup.py

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