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

Refactored - added support for objects with lifecycles.

nuwan_ag 10 лет назад
Родитель
Сommit
4b55b1a977

+ 43 - 47
cloudbridge/providers/base.py

@@ -8,12 +8,12 @@ import time
 from cloudbridge.providers.interfaces import CloudProvider
 from cloudbridge.providers.interfaces import Instance
 from cloudbridge.providers.interfaces import InstanceState
-from cloudbridge.providers.interfaces import InstanceWaitException
 from cloudbridge.providers.interfaces import KeyPair
 from cloudbridge.providers.interfaces import MachineImage
 from cloudbridge.providers.interfaces import MachineImageState
-from cloudbridge.providers.interfaces import MachineImageWaitException
+from cloudbridge.providers.interfaces import ObjectLifeCycleMixin
 from cloudbridge.providers.interfaces import SecurityGroup
+from cloudbridge.providers.interfaces import WaitStateException
 
 
 log = logging.getLogger(__name__)
@@ -62,18 +62,29 @@ class BaseCloudProvider(CloudProvider):
             return self.config.get(key, default_value)
         else:
             return getattr(self.config, key) if hasattr(
-                self.config, key) and getattr(self.config, key) else default_value
-
-
-class BaseInstance(Instance):
-
+                self.config, key) and getattr(self.config, key) else \
+                default_value
+
+
+class BaseObjectLifeCycleMixin(ObjectLifeCycleMixin):
+    """
+    A base implementation of an ObjectLifeCycleMixin.
+    This base implementation has an implementation of wait_till_ready
+    which refreshes the object's state till the desired ready states
+    are reached. Subclasses must implement two new properties - ready_states
+    and terminal_states, which return a list of states to wait for.
+    """
     @property
     def ready_states(self):
-        return [InstanceState.RUNNING]
+        raise NotImplementedError(
+            "ready_states not implemented by this object. Subclasses must"
+            " implement this method and return a valid set of ready states")
 
     @property
     def terminal_states(self):
-        return [InstanceState.TERMINATED, InstanceState.ERROR]
+        raise NotImplementedError(
+            "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):
         assert timeout > 0
@@ -81,62 +92,47 @@ class BaseInstance(Instance):
         assert interval > 0
 
         for time_left in range(timeout, 0, -interval):
-            state = self.instance_state
-            if state in self.ready_states:
+            if self.state in self.ready_states:
                 return True
-            elif state in self.terminal_states:
-                raise InstanceWaitException(
-                    "Instance is in state: {0} which is a terminal state and cannot be waited on.".format(state))
+            elif self.state in self.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(
-                    "Instance is in state '{0}'. Waiting another {1} seconds to reach state a ready state...".format(
-                        state,
+                    "Object {0} is in state: {1}. Waiting another {2} seconds"
+                    " to reach state a ready state...".format(
+                        self,
+                        self.state,
                         time_left))
                 time.sleep(interval)
             self.refresh()
 
-        raise InstanceWaitException(
-            "Waited too long for instance to become ready. Instance Id: %s is in state: %s".format(
-                self.instance_id,
-                self.name,
-                self.instance_state))
+        raise WaitStateException("Waited too long for object: {0} to become"
+                                 " ready. It's still  in state: {1}".format(
+                                     self, self.state))
 
 
-class BaseMachineImage(MachineImage):
+class BaseInstance(BaseObjectLifeCycleMixin, Instance):
 
     @property
     def ready_states(self):
-        return [MachineImageState.AVAILABLE]
+        return [InstanceState.RUNNING]
 
     @property
     def terminal_states(self):
-        return [MachineImageState.ERROR]
+        return [InstanceState.TERMINATED, InstanceState.ERROR]
 
-    def wait_till_ready(self, timeout=600, interval=5):
-        assert timeout > 0
-        assert timeout > interval
-        assert interval > 0
 
-        for time_left in range(timeout, 0, -interval):
-            state = self.image_state
-            if state in self.ready_states:
-                return True
-            elif state in self.terminal_states:
-                raise MachineImageWaitException(
-                    "Image is in state: {0} which is a terminal state and cannot be waited on.".format(state))
-            else:
-                log.debug(
-                    "Image is in state '{0}'. Waiting another {1} seconds to reach a ready state...".format(
-                        state,
-                        time_left))
-                time.sleep(interval)
-            self.refresh()
+class BaseMachineImage(BaseObjectLifeCycleMixin, MachineImage):
 
-        raise MachineImageWaitException(
-            "Waited too long for image to become ready. Image: {0}({1}) is still in state: {2}".format(
-                self.name,
-                self.image_id,
-                self.image_state))
+    @property
+    def ready_states(self):
+        return [MachineImageState.AVAILABLE]
+
+    @property
+    def terminal_states(self):
+        return [MachineImageState.ERROR]
 
 
 class BaseKeyPair(KeyPair):

+ 4 - 2
cloudbridge/providers/ec2/services.py

@@ -80,8 +80,10 @@ class EC2ComputeService(ComputeService):
     def __init__(self, provider):
         self.provider = provider
 
-    def create_instance(self, name, image, instance_type, zone=None, keypair=None, security_groups=None,
-                        user_data=None, block_device_mapping=None, network_interfaces=None, **kwargs):
+    def create_instance(self, name, image, instance_type, zone=None,
+                        keypair=None, security_groups=None, user_data=None,
+                        block_device_mapping=None, network_interfaces=None,
+                        **kwargs):
         """
         Creates a new virtual machine instance.
         """

+ 8 - 5
cloudbridge/providers/ec2/types.py

@@ -63,7 +63,7 @@ class EC2MachineImage(BaseMachineImage):
         self._ec2_image.deregister()
 
     @property
-    def image_state(self):
+    def state(self):
         return EC2MachineImage.IMAGE_STATE_MAP.get(
             self._ec2_image.state, MachineImageState.UNKNOWN)
 
@@ -72,7 +72,8 @@ class EC2MachineImage(BaseMachineImage):
         Refreshes the state of this instance by re-querying the cloud provider
         for its latest state.
         """
-        self._ec2_image = self.provider.images.get_image(self.image_id)._ec2_image
+        self._ec2_image = self.provider.images.get_image(
+            self.image_id)._ec2_image
 
 
 class EC2InstanceType(InstanceType):
@@ -94,7 +95,8 @@ class EC2InstanceType(InstanceType):
 
 class EC2Instance(BaseInstance):
 
-    # ref: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
+    # ref:
+    # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
     INSTANCE_STATE_MAP = {
         'pending': InstanceState.PENDING,
         'running': InstanceState.RUNNING,
@@ -190,7 +192,8 @@ class EC2Instance(BaseInstance):
         """
         Get the security group IDs associated with this instance.
         """
-        return [BaseSecurityGroup(group.name) for group in self._ec2_instance.groups]
+        return [BaseSecurityGroup(group.name)
+                for group in self._ec2_instance.groups]
 
     @property
     def key_pair_name(self):
@@ -208,7 +211,7 @@ class EC2Instance(BaseInstance):
         return EC2MachineImage(self.provider, image)
 
     @property
-    def instance_state(self):
+    def state(self):
         return EC2Instance.INSTANCE_STATE_MAP.get(
             self._ec2_instance.state, InstanceState.UNKNOWN)
 

+ 2 - 14
cloudbridge/providers/interfaces/__init__.py

@@ -1,15 +1,7 @@
 """
 Public interface exports
 """
-
-"""
-Core implementation of a cloud provider
-"""
 from .impl import CloudProvider
-
-"""
-Optional services offered by a cloud provider
-"""
 from .services import ComputeService
 from .services import ImageService
 from .services import InstanceTypesService
@@ -17,21 +9,17 @@ from .services import ObjectStoreService
 from .services import ProviderService
 from .services import SecurityService
 from .services import VolumeService
-
-"""
-Data objects returned by cloud provider services
-"""
 from .types import CloudProviderServiceType
 from .types import Instance
 from .types import InstanceState
 from .types import InstanceType
-from .types import InstanceWaitException
 from .types import KeyPair
 from .types import MachineImage
 from .types import MachineImageState
-from .types import MachineImageWaitException
+from .types import ObjectLifeCycleMixin
 from .types import PlacementZone
 from .types import Region
 from .types import SecurityGroup
 from .types import Snapshot
 from .types import Volume
+from .types import WaitStateException

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

@@ -11,8 +11,8 @@ class CloudProvider(object):
 
     def __init__(self, config):
         """
-        Create a new provider implementation given a dictionary of configuration
-        attributes.
+        Create a new provider implementation given a dictionary of
+        configuration attributes.
 
         :type config: an object with required fields
         :param config: This can be a Bunch or any other object whose fields can

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

@@ -76,8 +76,10 @@ class ComputeService(ProviderService):
         raise NotImplementedError(
             'list_regions not implemented by this provider')
 
-    def create_instance(self, name, image, instance_type, zone=None, keypair=None, security_groups=None,
-                        user_data=None, block_device_mapping=None, network_interfaces=None, **kwargs):
+    def create_instance(self, name, image, instance_type, zone=None,
+                        keypair=None, security_groups=None, user_data=None,
+                        block_device_mapping=None, network_interfaces=None,
+                        **kwargs):
         """
         Creates a new virtual machine instance.
 
@@ -85,7 +87,8 @@ class ComputeService(ProviderService):
         :param name: The name of the virtual machine instance
 
         :type  image: ``MachineImage`` or ``str``
-        :param image: The MachineImage object or id to boot the virtual machine with
+        :param image: The MachineImage object or id to boot the virtual machine
+        with
 
         :type  instance_type: ``InstanceType`` or ``str``
         :param instance_type: The InstanceType or name, specifying the size of
@@ -95,7 +98,8 @@ class ComputeService(ProviderService):
         :param zone: The Zone or its name, where the instance should be placed.
 
         :type  keypair: ``KeyPair`` or ``str``
-        :param keypair: The KeyPair object or its name, to set for the instance.
+        :param keypair: The KeyPair object or its name, to set for the
+        instance.
 
         :type  security_groups: A ``list`` of ``SecurityGroup`` objects or a
                                 list of ``str`` names
@@ -341,4 +345,5 @@ class InstanceTypesService(object):
         :return: an Instance object
         """
         raise NotImplementedError(
-            'InstanceTypesService.find_instance not implemented by this provider')
+            'InstanceTypesService.find_instance not implemented by this'
+            'provider')

+ 66 - 80
cloudbridge/providers/interfaces/types.py

@@ -23,16 +23,74 @@ class CloudProviderServiceType(object):
     OBJECTSTORE = 'object_store'
 
 
-class InstanceWaitException(Exception):
+class WaitStateException(Exception):
 
     """
-    Marker interface for instance wait exceptions.
-    Thrown when a timeout or errors occurs waiting for an instance to reach
-    state RUNNING.
+    Marker interface for object wait exceptions.
+    Thrown when a timeout or errors occurs waiting for an object does not reach
+    the expected state within a specified time limit.
     """
     pass
 
 
+class ObjectLifeCycleMixin(object):
+
+    """
+    A mixin for an object with a defined life-cycle, such as an Instance,
+    Volume, Image or Snapshot. An object that supports ObjectLifeCycleMixin
+    will always have a state, defining which point in its lifecycle it is
+    currently at.
+
+    It also defines a wait_till_ready operation, which indicates that the
+    object is in a state in its lifecycle where it is ready to be used by an
+    end-user.
+
+    A refresh operation allows the object to synchronise its state with the
+    service provider.
+    """
+    @property
+    def state(self):
+        """
+        Get the current state of this object.
+
+        :rtype: ``str``
+        :return: The current state as a string
+        """
+        raise NotImplementedError(
+            'LifeCycleObject.state not implemented by this service')
+
+    def refresh(self):
+        """
+        Refreshs this object's state and synchronize it with the underlying
+        service provider.
+        """
+        raise NotImplementedError(
+            'LifeCycleObject.refresh not implemented by this service')
+
+    def wait_till_ready(self, timeout, interval):
+        """
+        Wait till the current object is in a ready state, which is any
+        state where the end-user can successfully interact with the object.
+        Will throw a WaitStateException if the object is not ready within
+        the specified timeout.
+
+        :type timeout: int
+        :param timeout: The maximum length of time (in seconds) to wait for the
+        object to become ready.
+
+        :type interval: int
+        :param interval: How frequently to poll the object's ready state (in
+        seconds)
+
+        :rtype: ``True``
+        :return: Returns True if successful. A WaitStateException exception may
+        be thrown by the underlying service if the object cannot get into a
+        ready state (e.g. If the object is in an error state)
+        """
+        raise NotImplementedError(
+            'LifeCycleObject.wait_till_ready not implemented by this service')
+
+
 class InstanceState(object):
 
     """
@@ -58,7 +116,7 @@ class InstanceState(object):
     ERROR = "error"
 
 
-class Instance(object):
+class Instance(ObjectLifeCycleMixin):
 
     def instance_id(self):
         """
@@ -110,16 +168,6 @@ class Instance(object):
         raise NotImplementedError(
             'Instance.instance_type not implemented by this provider')
 
-    def instance_state(self):
-        """
-        Get the current state of this instance.
-
-        :rtype: ``InstanceType``
-        :return: The instance state such as RUNNING, PENDING, TERMINATED etc.
-        """
-        raise NotImplementedError(
-            'Instance.instance_state not implemented by this provider')
-
     def reboot(self):
         """
         Reboot this instance (using the cloud middleware API).
@@ -191,24 +239,6 @@ class Instance(object):
         raise NotImplementedError(
             'Instance.key_pair_name not implemented by this provider')
 
-    def wait_till_ready(self, timeout=600, interval=5):
-        """
-        Wait until the instance is running or the timeout elapses.
-        Throws an exception if the instance reaches an error state.
-
-        :type timeout: int
-        :param timeout: Maximum timeout to wait before giving up
-
-        :type interval: int
-        :param interval: How frequently to poll instance state
-
-        :rtype: :bool
-        :return: True if instance is ready. False if the instance is
-        not ready within the specified timeout.
-        """
-        raise NotImplementedError(
-            'Instance.wait_till_ready not implemented by this provider')
-
     def create_image(self, name):
         """
         Create a new image based on this instance.
@@ -218,24 +248,6 @@ class Instance(object):
         raise NotImplementedError(
             'Instance.create_image not implemented by this provider')
 
-    def refresh(self):
-        """
-        Refreshes the state of this instance by re-querying the cloud provider
-        for its latest state.
-        """
-        raise NotImplementedError(
-            'Instance.refresh not implemented by this provider')
-
-
-class MachineImageWaitException(Exception):
-
-    """
-    Marker interface for image wait exceptions.
-    Thrown when a timeout or errors occurs waiting for an image to reach
-    state RUNNING.
-    """
-    pass
-
 
 class MachineImageState(object):
 
@@ -254,7 +266,7 @@ class MachineImageState(object):
     ERROR = "error"
 
 
-class MachineImage(object):
+class MachineImage(ObjectLifeCycleMixin):
 
     @property
     def image_id(self):
@@ -299,34 +311,8 @@ class MachineImage(object):
         raise NotImplementedError(
             'MachineImage.delete not implemented by this provider')
 
-    def wait_till_ready(self, timeout=600, interval=5):
-        """
-        Wait until the image is running or the timeout elapses.
-        Throws an exception if the image reaches an error state.
-
-        :type timeout: int
-        :param timeout: Maximum timeout to wait before giving up
-
-        :type interval: int
-        :param interval: How frequently to poll image state
-
-        :rtype: :bool
-        :return: True if image is ready. False if the image is
-        not ready within the specified timeout.
-        """
-        raise NotImplementedError(
-            'Image.wait_till_ready not implemented by this provider')
-
-    def refresh(self):
-        """
-        Refreshes the state of this image by re-querying the cloud provider
-        for its latest state.
-        """
-        raise NotImplementedError(
-            'Image.refresh not implemented by this provider')
-
 
-class Volume(object):
+class Volume(ObjectLifeCycleMixin):
 
     def attach(self, instance_id, device):
         """
@@ -391,7 +377,7 @@ class Volume(object):
             'delete not implemented by this provider')
 
 
-class Snapshot(object):
+class Snapshot(ObjectLifeCycleMixin):
 
     def create_volume(self, placement, size=None, volume_type=None, iops=None):
         """

+ 1 - 1
cloudbridge/providers/openstack/__init__.py

@@ -4,4 +4,4 @@ Exports from this provider
 
 from .impl import OpenStackCloudProviderV1
 from .types import OpenStackInstance
-from .types import OpenStackInstanceType
+from .types import OpenStackInstanceType

+ 22 - 12
cloudbridge/providers/openstack/impl.py

@@ -1,5 +1,6 @@
 """
-Provider implementation based on openstack python client for OpenStack compatible clouds.
+Provider implementation based on openstack python client for OpenStack
+compatible clouds.
 """
 
 import os
@@ -23,11 +24,14 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
 
         self.api_version = self._get_config_value(
             'api_version', os.environ.get('OS_COMPUTE_API_VERSION', 2))
-        self.username = self._get_config_value('username', os.environ.get('OS_USERNAME', None))
-        self.password = self._get_config_value('password', os.environ.get('OS_PASSWORD', None))
+        self.username = self._get_config_value(
+            'username', os.environ.get('OS_USERNAME', None))
+        self.password = self._get_config_value(
+            'password', os.environ.get('OS_PASSWORD', None))
         self.tenant_name = self._get_config_value(
             'tenant_name', os.environ.get('OS_TENANT_NAME', None))
-        self.auth_url = self._get_config_value('auth_url', os.environ.get('OS_AUTH_URL', None))
+        self.auth_url = self._get_config_value(
+            'auth_url', os.environ.get('OS_AUTH_URL', None))
 
         self.nova = self._connect_nova()
         self.keystone = self._connect_keystone()
@@ -43,18 +47,24 @@ class OpenStackCloudProviderV1(BaseCloudProvider):
         Get an openstack client object for the given cloud.
         """
         return nova_client.Client(
-            self.api_version, self.username, self.password, self.tenant_name, self.auth_url)
+            self.api_version, self.username, self.password, self.tenant_name,
+            self.auth_url)
 
     def _connect_keystone(self):
         """
         Get an openstack client object for the given cloud.
         """
-        auth = Password(self.auth_url, username=self.username, password=self.password,
-                        tenant_name=self.tenant_name)
-        # Wow, the internal keystoneV2 implementation is terribly buggy. It needs both a separate Session object
-        # and the username, password again for things to work correctly. Plus, a manual call to authenticate() is
-        # also required if the service  catalogue needs to be queried
-        keystone = keystone_client.Client(session=session.Session(auth=auth), auth_url=self.auth_url, username=self.username,
-                                          password=self.password, tenant_name=self.tenant_name)
+        auth = Password(self.auth_url, username=self.username,
+                        password=self.password, tenant_name=self.tenant_name)
+        # Wow, the internal keystoneV2 implementation is terribly buggy. It
+        # needs both a separate Session object and the username, password again
+        # for things to work correctly. Plus, a manual call to authenticate()
+        # is also required if the service  catalogue needs to be queried
+        keystone = keystone_client.Client(
+            session=session.Session(auth=auth),
+            auth_url=self.auth_url,
+            username=self.username,
+            password=self.password,
+            tenant_name=self.tenant_name)
         keystone.authenticate()
         return keystone

+ 17 - 14
cloudbridge/providers/openstack/services.py

@@ -97,15 +97,17 @@ class OpenStackComputeService(ComputeService):
         self.provider = provider
         self.instance_types = OpenStackInstanceTypesService(self.provider)
 
-    def create_instance(self, name, image, instance_type, zone=None, keypair=None, security_groups=None,
-                        user_data=None, block_device_mapping=None, network_interfaces=None, **kwargs):
+    def create_instance(self, name, image, instance_type, zone=None,
+                        keypair=None, security_groups=None, user_data=None,
+                        block_device_mapping=None, network_interfaces=None,
+                        **kwargs):
         """
         Creates a new virtual machine instance.
         """
         image_id = image.image_id if isinstance(image, MachineImage) else image
-        instance_size = instance_type.name if isinstance(
-            instance_type,
-            InstanceType) else self.instance_types.find_by_name(instance_type).id
+        instance_size = instance_type.name if \
+            isinstance(instance_type, InstanceType) else \
+            self.instance_types.find_by_name(instance_type).id
         zone_name = zone.name if isinstance(zone, PlacementZone) else zone
         keypair_name = keypair.name if isinstance(
             keypair,
@@ -119,15 +121,16 @@ class OpenStackComputeService(ComputeService):
         else:
             security_groups_list = None
 
-        os_instance = self.provider.nova.servers.create(name, image_id,
-                                                        instance_size,
-                                                        min_count=1,
-                                                        max_count=1,
-                                                        availability_zone=zone_name,
-                                                        key_name=keypair_name,
-                                                        security_groups=security_groups_list,
-                                                        userdata=user_data
-                                                        )
+        os_instance = self.provider.nova.servers.create(
+            name,
+            image_id,
+            instance_size,
+            min_count=1,
+            max_count=1,
+            availability_zone=zone_name,
+            key_name=keypair_name,
+            security_groups=security_groups_list,
+            userdata=user_data)
         return OpenStackInstance(self.provider, os_instance)
 
     def list_instances(self):

+ 8 - 5
cloudbridge/providers/openstack/types.py

@@ -68,7 +68,7 @@ class OpenStackMachineImage(BaseMachineImage):
         self._os_image.delete()
 
     @property
-    def image_state(self):
+    def state(self):
         return OpenStackMachineImage.IMAGE_STATE_MAP.get(
             self._os_image.status, MachineImageState.UNKNOWN)
 
@@ -207,7 +207,8 @@ class OpenStackInstance(BaseInstance):
         """
         Get the security group IDs associated with this instance.
         """
-        return [BaseSecurityGroup(group.name) for group in self._os_instance.security_groups]
+        return [BaseSecurityGroup(group.name)
+                for group in self._os_instance.security_groups]
 
     @property
     def key_pair_name(self):
@@ -221,10 +222,11 @@ class OpenStackInstance(BaseInstance):
         Create a new image based on this instance.
         """
         image_id = self._os_instance.create_image(name)
-        return OpenStackMachineImage(self.provider, self.provider.images.get_image(image_id))
+        return OpenStackMachineImage(
+            self.provider, self.provider.images.get_image(image_id))
 
     @property
-    def instance_state(self):
+    def state(self):
         return OpenStackInstance.INSTANCE_STATE_MAP.get(
             self._os_instance.status, InstanceState.UNKNOWN)
 
@@ -233,7 +235,8 @@ 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
+        self._os_instance = self.provider.compute.get_instance(
+            self.instance_id)._os_instance
 
 
 class OpenStackRegion(Region):