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

New Features Added:
1. A new function to generate a URL with time to live (TTL) property for a key (object) is implemented, and it's test function is implemented.
2. list function is updated to accept a prefix for querying buckets, and it's test function is implemented.
3. A new function `exists` is added to bucket, which given a key, determines if the key exists in the bucket.

vahid 9 лет назад
Родитель
Сommit
7ee80daf64

+ 12 - 0
cloudbridge/cloud/interfaces/resources.py

@@ -247,6 +247,7 @@ class PageableObjectMixin(object):
 
     @abstractmethod
     def list(self, limit=None, marker=None):
+        # type: (object, object) -> object
         """
         Returns a list of objects up to a maximum limit.
 
@@ -2067,6 +2068,17 @@ class BucketObject(CloudResource):
         """
         pass
 
+    @abstractmethod
+    def generate_url(self, expires_in):
+        """
+        Generates a URL to this object. If the object is public, `expires_in`
+        argument is not necessary, but if the object is private, the life time
+        of URL is set using `expires_in` argument.
+        :param expires_in: time to live of the generated URL in seconds.
+        :return: A URL to access the object.
+        """
+        pass
+
 
 class Bucket(PageableObjectMixin, CloudResource):
 

+ 41 - 33
cloudbridge/cloud/providers/aws/resources.py

@@ -38,7 +38,6 @@ from retrying import retry
 
 
 class AWSMachineImage(BaseMachineImage):
-
     IMAGE_STATE_MAP = {
         'pending': MachineImageState.PENDING,
         'available': MachineImageState.AVAILABLE,
@@ -109,7 +108,6 @@ class AWSMachineImage(BaseMachineImage):
 
 
 class AWSPlacementZone(BasePlacementZone):
-
     def __init__(self, provider, zone, region):
         super(AWSPlacementZone, self).__init__(provider)
         if isinstance(zone, AWSPlacementZone):
@@ -152,7 +150,6 @@ class AWSPlacementZone(BasePlacementZone):
 
 
 class AWSInstanceType(BaseInstanceType):
-
     def __init__(self, provider, instance_dict):
         super(AWSInstanceType, self).__init__(provider)
         self._inst_dict = instance_dict
@@ -204,7 +201,6 @@ class AWSInstanceType(BaseInstanceType):
 
 
 class AWSInstance(BaseInstance):
-
     # ref:
     # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
     INSTANCE_STATE_MAP = {
@@ -373,7 +369,6 @@ class AWSInstance(BaseInstance):
 
 
 class AWSVolume(BaseVolume):
-
     # Ref:
     # http://docs.aws.amazon.com/AWSEC2/latest/CommandLineReference/
     # ApiReference-cmd-DescribeVolumes.html
@@ -498,7 +493,6 @@ class AWSVolume(BaseVolume):
 
 
 class AWSSnapshot(BaseSnapshot):
-
     # Ref: http://docs.aws.amazon.com/AWSEC2/latest/CommandLineReference/
     # ApiReference-cmd-DescribeSnapshots.html
     SNAPSHOT_STATE_MAP = {
@@ -587,7 +581,6 @@ class AWSSnapshot(BaseSnapshot):
 
 
 class AWSKeyPair(BaseKeyPair):
-
     def __init__(self, provider, key_pair):
         super(AWSKeyPair, self).__init__(provider, key_pair)
 
@@ -604,7 +597,6 @@ class AWSKeyPair(BaseKeyPair):
 
 
 class AWSSecurityGroup(BaseSecurityGroup):
-
     def __init__(self, provider, security_group):
         super(AWSSecurityGroup, self).__init__(provider, security_group)
 
@@ -672,17 +664,17 @@ class AWSSecurityGroup(BaseSecurityGroup):
                  cidr_ip=None, src_group=None):
         for rule in self._security_group.rules:
             if (rule.ip_protocol == ip_protocol and
-               rule.from_port == from_port and
-               rule.to_port == to_port and
-               rule.grants[0].cidr_ip == cidr_ip) or \
-               (rule.grants[0].group_id == src_group.id if src_group and
-               hasattr(rule.grants[0], 'group_id') else False):
+                        rule.from_port == from_port and
+                        rule.to_port == to_port and
+                        rule.grants[0].cidr_ip == cidr_ip) or \
+                    (rule.grants[0].group_id == src_group.id if src_group and
+                        hasattr(rule.grants[0], 'group_id') else False):
                 return AWSSecurityGroupRule(self._provider, rule, self)
         return None
 
     def to_json(self):
-        attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
-        js = {k: v for(k, v) in attr if not k.startswith('_')}
+        attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
+        js = {k: v for (k, v) in attr if not k.startswith('_')}
         json_rules = [r.to_json() for r in self.rules]
         js['rules'] = [json.loads(r) for r in json_rules]
         if js.get('network_id'):
@@ -691,7 +683,6 @@ class AWSSecurityGroup(BaseSecurityGroup):
 
 
 class AWSSecurityGroupRule(BaseSecurityGroupRule):
-
     def __init__(self, provider, rule, parent):
         super(AWSSecurityGroupRule, self).__init__(provider, rule, parent)
 
@@ -703,7 +694,7 @@ class AWSSecurityGroupRule(BaseSecurityGroupRule):
         md5 = hashlib.md5()
         md5.update("{0}-{1}-{2}-{3}".format(
             self.ip_protocol, self.from_port, self.to_port, self.cidr_ip)
-            .encode('ascii'))
+                   .encode('ascii'))
         return md5.hexdigest()
 
     @property
@@ -738,8 +729,8 @@ class AWSSecurityGroupRule(BaseSecurityGroupRule):
         return None
 
     def to_json(self):
-        attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
-        js = {k: v for(k, v) in attr if not k.startswith('_')}
+        attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
+        js = {k: v for (k, v) in attr if not k.startswith('_')}
         js['group'] = self.group.id if self.group else ''
         js['parent'] = self.parent.id if self.parent else ''
         return json.dumps(js, sort_keys=True)
@@ -761,7 +752,6 @@ class AWSSecurityGroupRule(BaseSecurityGroupRule):
 
 
 class AWSBucketObject(BaseBucketObject):
-
     def __init__(self, provider, key):
         super(AWSBucketObject, self).__init__(provider)
         self._key = key
@@ -807,7 +797,6 @@ class AWSBucketObject(BaseBucketObject):
         """
         self._key.set_contents_from_string(data)
 
-
     def upload_from_file(self, path):
         """
         Stores the contents of the file pointed by the "path" variable.
@@ -835,9 +824,18 @@ class AWSBucketObject(BaseBucketObject):
         """
         self._key.delete()
 
+    def generate_url(self, expires_in=0):
+        """
+        Generates a URL to this object. If the object is public, `expires_in`
+        argument is not necessary, but if the object is private, the life time
+        of URL is set using `expires_in` argument.
+        :param expires_in: time to live of the generated URL in seconds.
+        :return: The URL to access the object.
+        """
+        return self._key.generate_url(expires_in=expires_in)
 
-class AWSBucket(BaseBucket):
 
+class AWSBucket(BaseBucket):
     def __init__(self, provider, bucket):
         super(AWSBucket, self).__init__(provider)
         self._bucket = bucket
@@ -858,19 +856,24 @@ class AWSBucket(BaseBucket):
         Retrieve a given object from this bucket.
         """
         key = Key(self._bucket, key)
-        if key.exists():
+        if key and key.exists():
             return AWSBucketObject(self._provider, key)
         return None
 
-    def list(self, limit=None, marker=None):
+    def list(self, limit=None, marker=None, prefix=None):
         """
         List all objects within this bucket.
 
         :rtype: BucketObject
         :return: List of all available BucketObjects within this bucket.
         """
-        objects = [AWSBucketObject(self._provider, obj)
-                   for obj in self._bucket.list()]
+        if prefix:
+            objects = [AWSBucketObject(self._provider, obj)
+                       for obj in self._bucket.list(prefix=prefix)]
+        else:
+            objects = [AWSBucketObject(self._provider, obj)
+                       for obj in self._bucket.list()]
+
         return ClientPagedResultList(self._provider, objects,
                                      limit=limit, marker=marker)
 
@@ -884,9 +887,19 @@ class AWSBucket(BaseBucket):
         key = Key(self._bucket, name)
         return AWSBucketObject(self._provider, key)
 
+    def exists(self, key):
+        """
+        Determines if an object with given key exists in this bucket.
+        :param key: The key to be searched in the bucket.
+        :return: Boolean: True if the key exists, False, if it does not.
+        """
+        key = Key(self._bucket, key)
+        if key and key.exists():
+            return True
+        return False
 
-class AWSRegion(BaseRegion):
 
+class AWSRegion(BaseRegion):
     def __init__(self, provider, aws_region):
         super(AWSRegion, self).__init__(provider)
         self._aws_region = aws_region
@@ -920,7 +933,6 @@ class AWSRegion(BaseRegion):
 
 
 class AWSNetwork(BaseNetwork):
-
     # Ref:
     # docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcs.html
     _NETWORK_STATE_MAP = {
@@ -994,7 +1006,6 @@ class AWSNetwork(BaseNetwork):
 
 
 class AWSSubnet(BaseSubnet):
-
     def __init__(self, provider, subnet):
         super(AWSSubnet, self).__init__(provider)
         self._subnet = subnet
@@ -1033,7 +1044,6 @@ class AWSSubnet(BaseSubnet):
 
 
 class AWSFloatingIP(BaseFloatingIP):
-
     def __init__(self, provider, floating_ip):
         super(AWSFloatingIP, self).__init__(provider)
         self._ip = floating_ip
@@ -1058,7 +1068,6 @@ class AWSFloatingIP(BaseFloatingIP):
 
 
 class AWSRouter(BaseRouter):
-
     def __init__(self, provider, router):
         super(AWSRouter, self).__init__(provider)
         self._router = router
@@ -1111,7 +1120,7 @@ class AWSRouter(BaseRouter):
     def state(self):
         self.refresh()  # Explicitly refresh the local object
         if self._router.attachments and \
-           self._router.attachments[0].state == 'available':
+                        self._router.attachments[0].state == 'available':
             return RouterState.ATTACHED
         return RouterState.DETACHED
 
@@ -1159,6 +1168,5 @@ class AWSRouter(BaseRouter):
 
 
 class AWSLaunchConfig(BaseLaunchConfig):
-
     def __init__(self, provider):
         super(AWSLaunchConfig, self).__init__(provider)

+ 22 - 3
cloudbridge/cloud/providers/openstack/resources.py

@@ -1061,7 +1061,6 @@ class OpenStackBucketObject(BaseBucketObject):
         """
         raise NotImplementedError("This functionality is not implemented yet.")
 
-
     def upload_from_large_file(self, path):
         """
         Stores the contents of the large file pointed by the "path" variable.
@@ -1072,7 +1071,6 @@ class OpenStackBucketObject(BaseBucketObject):
         """
         raise NotImplementedError("This functionality is not implemented yet.")
 
-
     def delete(self):
         """
         Delete this object.
@@ -1088,6 +1086,16 @@ class OpenStackBucketObject(BaseBucketObject):
                 return True
         return False
 
+    def generate_url(self, expires_in=0):
+        """
+        Generates a URL to this object. If the object is public, `expires_in`
+        argument is not necessary, but if the object is private, the life time
+        of URL is set using `expires_in` argument.
+        :param expires_in: time to live of the generated URL in seconds.
+        :return: A URL to access the object.
+        """
+        raise NotImplementedError("This functionality is not implemented yet.")
+
 
 class OpenStackBucket(BaseBucket):
 
@@ -1118,13 +1126,16 @@ class OpenStackBucket(BaseBucket):
         else:
             return None
 
-    def list(self, limit=None, marker=None):
+    def list(self, limit=None, marker=None, prefix=None):
         """
         List all objects within this bucket.
 
         :rtype: BucketObject
         :return: List of all available BucketObjects within this bucket.
         """
+        if prefix is not None:
+            raise NotImplementedError("This functionality is not implemented yet.")
+
         _, object_list = self._provider.swift.get_container(
             self.name, limit=oshelpers.os_result_limit(self._provider, limit),
             marker=marker)
@@ -1145,3 +1156,11 @@ class OpenStackBucket(BaseBucket):
     def create_object(self, object_name):
         self._provider.swift.put_object(self.name, object_name, None)
         return self.get(object_name)
+
+    def exists(self, key):
+        """
+        Determines if an object with given key exists in this bucket.
+        :param key: The key to be searched in the bucket.
+        :return: Boolean: True if the key exists, False, if it does not.
+        """
+        raise NotImplementedError("This functionality is not implemented yet.")

+ 19 - 7
test/test_object_store_service.py

@@ -1,6 +1,8 @@
 from datetime import datetime
 from io import BytesIO
 import uuid
+import urllib
+import os
 
 from cloudbridge.cloud.interfaces.resources import BucketObject
 from test.helpers import ProviderTestBase
@@ -80,7 +82,8 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
         self.assertEqual([], objects)
 
         with helpers.cleanup_action(lambda: test_bucket.delete()):
-            obj_name = "hello_world.txt"
+            obj_name_prefix = "hello"
+            obj_name = obj_name_prefix + "_world.txt"
             obj = test_bucket.create_object(obj_name)
 
             self.assertTrue(
@@ -130,6 +133,13 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                     isinstance(obj_too, BucketObject),
                     "Did not get object {0} of expected type.".format(obj_too))
 
+                prefix_filtered_list = test_bucket.list(prefix=obj_name_prefix)
+                self.assertTrue(
+                    len(objs) == len(prefix_filtered_list) == 1,
+                    'The number of objects returned by list function, '
+                    'with and without a prefix, are expected to be equal, '
+                    'but its detected otherwise.')
+
             objs = test_bucket.list()
             found_objs = [o for o in objs if o.name == obj_name]
             self.assertTrue(
@@ -137,16 +147,14 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                 "Object %s should have been deleted but still exists." %
                 obj_name)
 
-
     def test_upload_download_bucket_content(self):
-
         name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
         test_bucket = self.provider.object_store.create(name)
 
         with helpers.cleanup_action(lambda: test_bucket.delete()):
             obj_name = "hello_upload_download.txt"
             obj = test_bucket.create_object(obj_name)
-            content = b"Hello World. Here's some content."
+            content = "Hello World. Here's some content."
 
             with helpers.cleanup_action(lambda: obj.delete()):
                 # TODO: Upload and download methods accept different parameter
@@ -160,9 +168,13 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                 for data in obj.iter_content():
                     target_stream2.write(data)
                 self.assertEqual(target_stream2.getvalue(), content)
+                url = obj.generate_url(100)
+                urllib.urlretrieve(url, "Downloaded_" + obj.name)
+                with open("Downloaded_" + obj.name) as tmpFile:
+                    for line in tmpFile:
+                        self.assertEqual(line, content)
+                os.remove("Downloaded_" + obj.name)
 
-
-            obj_name = "hello_upload_download.txt"
             obj = test_bucket.create_object(obj_name)
             with helpers.cleanup_action(lambda: obj.delete):
                 with open("hello_upload_download.txt", "w+") as tmpFile:
@@ -174,4 +186,4 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                     target_stream2 = BytesIO()
                     for data in obj.iter_content():
                         target_stream2.write(data)
-                    self.assertEqual(target_stream2.getvalue(), content)
+                    self.assertEqual(target_stream2.getvalue(), content)