def close_par(par=None, par_uid=None, url_checksum=None): """Close the passed OSPar, which provides access to data in the passed bucket Args: par (OSPar, default=None): OSPar to close bucket par_uid (str, default=None): UID for OSPar url_checksum (str, default=None): Checksum to pass to PARRegistry Returns: None """ from Acquire.ObjectStore import OSParRegistry as _OSParRegistry if par is None: par = _OSParRegistry.get( par_uid=par_uid, details_function=_get_driver_details_from_data, url_checksum=url_checksum) from Acquire.ObjectStore import OSPar as _OSPar if not isinstance(par, _OSPar): raise TypeError("The OSPar must be of type OSPar") if par.driver() != "oci": raise ValueError("Cannot delete a OSPar that was not created " "by the OCI object store") # delete the PAR from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket par_bucket = par.driver_details()["bucket"] par_id = par.driver_details()["par_id"] bucket = _get_service_account_bucket() # now get the bucket accessed by the OSPar... bucket = OCI_ObjectStore.get_bucket(bucket=bucket, bucket_name=par_bucket) client = bucket["client"] try: response = client.delete_preauthenticated_request( client.get_namespace().data, bucket["bucket_name"], par_id) except Exception as e: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("Unable to delete a OSPar '%s' : Error %s" % (par_id, str(e))) if response.status not in [200, 204]: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "Unable to delete a OSPar '%s' : Status %s, Error %s" % (par_id, response.status, str(response.data))) # close the OSPar - this will trigger any close_function(s) _OSParRegistry.close(par=par)
def delete_bucket(bucket, force=False): """Delete the passed bucket. This should be used with caution. Normally you can only delete a bucket if it is empty. If 'force' is True then it will remove all objects/pars from the bucket first, and then delete the bucket. This can cause a LOSS OF DATA! Args: bucket (dict): Bucket to delete force (bool, default=False): If True, delete even if bucket is not empty. If False and bucket not empty raise PermissionError Returns: None """ is_empty = GCP_ObjectStore.is_bucket_empty(bucket=bucket) if not is_empty: if force: GCP_ObjectStore.delete_all_objects(bucket=bucket) else: raise PermissionError( "You cannot delete the bucket %s as it is not empty" % GCP_ObjectStore.get_bucket_name(bucket=bucket)) # the bucket is empty - delete it try: bucket['bucket'].delete() except Exception as e: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "Unable to delete bucket '%s'. Please check the " "access permissions: Error %s" % (bucket['bucket_name'], str(e)))
def create_bucket(bucket, bucket_name): """Create and return a new bucket in the object store called 'bucket_name'. This will raise an ObjectStoreError if this bucket already exists """ new_bucket = _copy.copy(bucket) try: from google.cloud import storage as _storage client = new_bucket["client"] bucket_name = _sanitise_bucket_name(bucket_name, bucket["unique_suffix"]) bucket_obj = _storage.Bucket(client, name=bucket_name) bucket_obj.location = bucket["bucket"].location bucket_obj.storage_class = "REGIONAL" new_bucket["bucket"] = client.create_bucket(bucket_obj) new_bucket["bucket_name"] = str(bucket_name) except Exception as e: # couldn't create the bucket - likely because it already # exists - try to connect to the existing bucket from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "Unable to create the bucket '%s', likely because it " "already exists: %s" % (bucket_name, str(e))) return new_bucket
def get_bucket(bucket, bucket_name, compartment=None, create_if_needed=True): """Find and return a new bucket in the object store called 'bucket_name', optionally placing it into the compartment identified by 'compartment'. If 'create_if_needed' is True then the bucket will be created if it doesn't exist. Otherwise, if the bucket does not exist then an exception will be raised. """ bucket_name = str(bucket_name) if compartment is not None: if compartment.endswith("/"): bucket = compartment else: bucket = "%s/" % compartment full_name = _os.path.join(_os.path.split(bucket)[0], bucket_name) if not _os.path.exists(full_name): if create_if_needed: _os.makedirs(full_name) else: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "There is no bucket available called '%s' in " "compartment '%s'" % (bucket_name, compartment)) return full_name
def get_size_and_checksum(bucket, key): """Return the object size (in bytes) and MD5 checksum of the object in the passed bucket at the specified key Args: bucket (dict): Bucket containing data key (str): Key for object Returns: tuple (int, str): Size and MD5 checksum of object """ key = _clean_key(key) try: response = bucket["client"].get_object(bucket["namespace"], bucket["bucket_name"], key) except: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("No data at key '%s'" % key) content_length = response.headers["Content-Length"] checksum = response.headers["Content-MD5"] # the checksum is a base64 encoded Content-MD5 header # described as standard part of HTTP RFC 2616. Need to # convert this back to a hexdigest import binascii as _binascii import base64 as _base64 md5sum = _binascii.hexlify(_base64.b64decode(checksum)).decode("utf-8") return (int(content_length), md5sum)
def get_bucket(bucket, bucket_name, create_if_needed=True): """Find and return a new bucket in the object store called 'bucket_name'. If 'create_if_needed' is True then the bucket will be created if it doesn't exist. Otherwise, if the bucket does not exist then an exception will be raised. """ new_bucket = _copy.copy(bucket) new_bucket["bucket_name"] = _sanitise_bucket_name(bucket_name) try: from oci.object_storage.models import CreateBucketDetails as \ _CreateBucketDetails except: raise ImportError( "Cannot import OCI. Please install OCI, e.g. via " "'pip install oci' so that you can connect to the " "Oracle Cloud Infrastructure") # try to get the existing bucket client = new_bucket["client"] namespace = client.get_namespace().data sanitised_name = _sanitise_bucket_name(bucket_name) try: existing_bucket = client.get_bucket(namespace, sanitised_name).data except: existing_bucket = None if existing_bucket: new_bucket["bucket"] = existing_bucket return new_bucket if create_if_needed: try: request = _CreateBucketDetails() request.compartment_id = new_bucket["compartment_id"] request.name = sanitised_name client.create_bucket(namespace, request) except: pass try: existing_bucket = client.get_bucket(namespace, sanitised_name).data except: existing_bucket = None if existing_bucket is None: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "There is not bucket called '%s'. Please check the " "compartment and access permissions." % bucket_name) new_bucket["bucket"] = existing_bucket return new_bucket
def delete_bucket(bucket, force=False): """Delete the passed bucket. This should be used with caution. Normally you can only delete a bucket if it is empty. If 'force' is True then it will remove all objects/pars from the bucket first, and then delete the bucket. This can cause a LOSS OF DATA! Args: bucket (dict): Bucket to delete force (bool, default=False): If True, delete even if bucket is not empty. If False and bucket not empty raise PermissionError Returns: None """ is_empty = OCI_ObjectStore.is_bucket_empty(bucket=bucket) if not is_empty: if force: OCI_ObjectStore.delete_all_objects(bucket=bucket) else: raise PermissionError( "You cannot delete the bucket %s as it is not empty" % OCI_ObjectStore.get_bucket_name(bucket=bucket)) # the bucket is empty - delete it client = bucket["client"] namespace = client.get_namespace().data bucket_name = bucket["bucket_name"] try: response = client.delete_bucket(namespace, bucket_name) except Exception as e: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "Unable to delete bucket '%s'. Please check the " "compartment and access permissions: Error %s" % (bucket_name, str(e))) if response.status not in [200, 204]: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "Unable to delete a bucket '%s' : Status %s, Error %s" % (bucket_name, response.status, str(response.data)))
def get_object(bucket, key): """Return the binary data contained in the key 'key' in the passed bucket Args: bucket (dict): Bucket containing data key (str): Key for data in bucket Returns: bytes: Binary data """ key = _clean_key(key) blob = bucket["bucket"].blob(key) try: response = blob.download_as_string() is_chunked = False except: try: blob = bucket["bucket"].blob("%s/1" % key) response = blob.download_as_string() is_chunked = True except: is_chunked = False pass # Raise the original error if not is_chunked: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("No data at key '%s'" % key) data = response if is_chunked: # keep going through to find more chunks next_chunk = 1 while True: next_chunk += 1 try: blob = bucket["bucket"].blob("%s/%s" % (key, next_chunk)) response = blob.download_as_string() except: response = None break if not data: data = response else: data += response return data
def get_object(bucket, key): """Return the binary data contained in the key 'key' in the passed bucket""" with _rlock: filepath = "%s/%s._data" % (bucket, key) if _os.path.exists(filepath): return open(filepath, "rb").read() else: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("No object at key '%s'" % key)
def take_object(bucket, key): """Take (delete) the object from the object store, returning the object """ with _rlock: filepath = "%s/%s._data" % (bucket, key) if _os.path.exists(filepath): data = open(filepath, "rb").read() _os.remove(filepath) return data else: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("No object at key '%s'" % key)
def get_size_and_checksum(bucket, key): """Return the object size (in bytes) and checksum of the object in the passed bucket at the specified key """ filepath = "%s/%s._data" % (bucket, key) if not _os.path.exists(filepath): from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("No object at key '%s'" % key) from Acquire.Access import get_filesize_and_checksum \ as _get_filesize_and_checksum return _get_filesize_and_checksum(filepath)
def set_object_store_backend(backend): """Set the backend that is used to actually connect to the object store. This can only be set once in the program! """ global _objstore_backend if backend == _objstore_backend: return if _objstore_backend is not None: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("You cannot change the object store " "backend once it has been already set!") _objstore_backend = backend
def create_bucket(bucket, bucket_name, compartment=None): """Create and return a new bucket in the object store called 'bucket_name', optionally placing it into the compartment identified by 'compartment'. This will raise an ObjectStoreError if this bucket already exists Args: bucket (dict): Bucket to hold data bucket_name (str): Name of bucket to create compartment (str): Compartment in which to create bucket Returns: dict: New bucket """ new_bucket = _copy.copy(bucket) new_bucket["bucket_name"] = str(bucket_name) if compartment is not None: new_bucket["compartment_id"] = str(compartment) try: from oci.object_storage.models import CreateBucketDetails as \ _CreateBucketDetails except: raise ImportError( "Cannot import OCI. Please install OCI, e.g. via " "'pip install oci' so that you can connect to the " "Oracle Cloud Infrastructure") try: request = _CreateBucketDetails() request.compartment_id = new_bucket["compartment_id"] client = new_bucket["client"] request.name = _sanitise_bucket_name(bucket_name) new_bucket["bucket"] = client.create_bucket( client.get_namespace().data, request).data except Exception as e: # couldn't create the bucket - likely because it already # exists - try to connect to the existing bucket from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "Unable to create the bucket '%s', likely because it " "already exists: %s" % (bucket_name, str(e))) return new_bucket
def get_object(bucket, key): """ Gets the object at key in the passed bucket Args: bucket (str): Bucket containing data key (str): Key for data in bucket Returns: Object: Object from store """ with rlock: filepath = Path(f"{bucket}/{key}._data") if filepath.exists(): return filepath.read_bytes() else: raise ObjectStoreError(f"No object at key '{key}'")
def create_bucket(bucket, bucket_name): """Create and return a new bucket in the object store called 'bucket_name'. This will raise an ObjectStoreError if this bucket already exists """ bucket_name = str(bucket_name) full_name = _os.path.join(_os.path.split(bucket)[0], bucket_name) if _os.path.exists(full_name): from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "CANNOT CREATE NEW BUCKET '%s': EXISTS!" % bucket_name) _os.makedirs(full_name) return full_name
def get_bucket(bucket, bucket_name, create_if_needed=True): """Find and return a new bucket in the object store called 'bucket_name'. If 'create_if_needed' is True then the bucket will be created if it doesn't exist. Otherwise, if the bucket does not exist then an exception will be raised. """ bucket_name = str(bucket_name) full_name = _os.path.join(_os.path.split(bucket)[0], bucket_name) if not _os.path.exists(full_name): if create_if_needed: _os.makedirs(full_name) else: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "There is no bucket available called '%s'" % (bucket_name)) return full_name
def get_bucket(bucket, bucket_name, create_if_needed=True): """Find and return a new bucket in the object store called 'bucket_name'. If 'create_if_needed' is True then the bucket will be created if it doesn't exist. Otherwise, if the bucket does not exist then an exception will be raised. """ new_bucket = _copy.copy(bucket) # try to get the existing bucket client = new_bucket["client"] sanitised_name = _sanitise_bucket_name(bucket_name, bucket["unique_suffix"]) new_bucket["bucket_name"] = sanitised_name try: existing_bucket = client.get_bucket(sanitised_name) except: existing_bucket = None if existing_bucket: new_bucket["bucket"] = existing_bucket return new_bucket if create_if_needed: try: new_bucket = GCP_ObjectStore.create_bucket(bucket, bucket_name) existing_bucket = new_bucket["bucket"] except: existing_bucket = None if existing_bucket is None: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "There is not bucket called '%s'. Please check the " "access permissions." % bucket_name) new_bucket["bucket"] = existing_bucket return new_bucket
def _clean_key(key): """This function cleans and returns a key so that it is suitable for use both as a key and a directory/file path e.g. it removes double-slashes Args: key (str): Key to clean Returns: str: Cleaned key """ key = _os.path.normpath(key) if len(key) > 1024: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "The object store does not support keys with longer than " "1024 characters (%s) - %s" % (len(key), key)) # if this becomes a problem then we will implement a 'tinyurl' # to shorten keys and use this function to lookup long keys return key
def create_bucket(bucket, bucket_name, compartment=None): """Create and return a new bucket in the object store called 'bucket_name', optionally placing it into the compartment identified by 'compartment'. This will raise an ObjectStoreError if this bucket already exists """ bucket_name = str(bucket_name) if compartment is not None: if compartment.endswith("/"): bucket = compartment else: bucket = "%s/" % compartment full_name = _os.path.join(_os.path.split(bucket)[0], bucket_name) if _os.path.exists(full_name): from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("CANNOT CREATE NEW BUCKET '%s': EXISTS!" % bucket_name) _os.makedirs(full_name) return full_name
def create_par(bucket, encrypt_key, key=None, readable=True, writeable=False, duration=3600, cleanup_function=None): """Create a pre-authenticated request for the passed bucket and key (if key is None then the request is for the entire bucket). This will return a OSPar object that will contain a URL that can be used to access the object/bucket. If writeable is true, then the URL will also allow the object/bucket to be written to. PARs are time-limited. Set the lifetime in seconds by passing in 'duration' (by default this is one hour) Args: bucket (dict): Bucket to create OSPar for encrypt_key (PublicKey): Public key to encrypt PAR key (str, default=None): Key readable (bool, default=True): If bucket is readable writeable (bool, default=False): If bucket is writeable duration (int, default=3600): Duration OSPar should be valid for in seconds cleanup_function (function, default=None): Cleanup function to be passed to PARRegistry Returns: OSPar: Pre-authenticated request for the bucket """ from Acquire.Crypto import PublicKey as _PublicKey if not isinstance(encrypt_key, _PublicKey): from Acquire.Client import PARError raise PARError("You must supply a valid PublicKey to encrypt the " "returned OSPar") is_bucket = (key is None) if writeable: method = "PUT" elif readable: method = "GET" else: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("Unsupported permissions model for OSPar!") try: # get the UTC datetime when this OSPar should expire from Acquire.ObjectStore import get_datetime_now as _get_datetime_now created_datetime = _get_datetime_now() expires_datetime = _get_datetime_now() + _datetime.timedelta( seconds=duration) bucket_obj = bucket["bucket"] if is_bucket: url = bucket_obj.generate_signed_url( version='v4', expiration=expires_datetime, method=method) else: blob = bucket_obj.blob(key) url = blob.generate_signed_url(version='v4', expiration=expires_datetime, method=method) except Exception as e: # couldn't create the preauthenticated request from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("Unable to create the OSPar '%s': %s" % (key, str(e))) if url is None: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("Unable to create the signed URL!") # get the checksum for this URL - used to validate the close # request from Acquire.ObjectStore import OSPar as _OSPar from Acquire.ObjectStore import OSParRegistry as _OSParRegistry url_checksum = _OSPar.checksum(url) bucket_name = bucket["bucket_name"] driver_details = { "driver": "gcp", "bucket": bucket_name, "created_datetime": created_datetime } par = _OSPar(url=url, encrypt_key=encrypt_key, key=key, expires_datetime=expires_datetime, is_readable=readable, is_writeable=writeable, driver_details=driver_details) _OSParRegistry.register(par=par, url_checksum=url_checksum, details_function=_get_driver_details_from_par, cleanup_function=cleanup_function) return par
def get(par_uid, details_function, url_checksum=None): """Return the PAR that matches the passed PAR_UID. If 'url_checksum' is supplied then this verifies that the checksum of the secret URL is correct. This returns the PAR with a completed 'driver_details'. The 'driver_details' is created from the dictionary of data saved with the PAR. The signature should be; driver_details = details_function(data) """ if par_uid is None or len(par_uid) == 0: return from Acquire.Service import is_running_service as _is_running_service if not _is_running_service(): return from Acquire.Service import get_service_account_bucket \ as _get_service_account_bucket from Acquire.ObjectStore import OSPar as _OSPar from Acquire.ObjectStore import ObjectStore as _ObjectStore from Acquire.ObjectStore import string_to_datetime \ as _string_to_datetime key = "%s/uid/%s" % (_registry_key, par_uid) bucket = _get_service_account_bucket() objs = _ObjectStore.get_all_objects_from_json(bucket=bucket, prefix=key) data = None for obj in objs.values(): if url_checksum is not None: if url_checksum == obj["url_checksum"]: data = obj break else: data = obj break if data is None: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "There is matching PAR available to close...") par = _OSPar.from_data(data["par"]) if "driver_details" in data: if details_function is not None: driver_details = details_function(data["driver_details"]) par._driver_details = driver_details else: par._driver_details = driver_details return par
def get_object(bucket, key): """Return the binary data contained in the key 'key' in the passed bucket Args: bucket (dict): Bucket containing data key (str): Key for data in bucket Returns: bytes: Binary data """ key = _clean_key(key) try: response = bucket["client"].get_object(bucket["namespace"], bucket["bucket_name"], key) is_chunked = False except: try: response = bucket["client"].get_object(bucket["namespace"], bucket["bucket_name"], "%s/1" % key) is_chunked = True except: is_chunked = False pass # Raise the original error if not is_chunked: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError("No data at key '%s'" % key) data = None for chunk in response.data.raw.stream(1024 * 1024, decode_content=False): if not data: data = chunk else: data += chunk if is_chunked: # keep going through to find more chunks next_chunk = 1 while True: next_chunk += 1 try: response = bucket["client"].get_object( bucket["namespace"], bucket["bucket_name"], "%s/%d" % (key, next_chunk)) except: response = None break for chunk in response.data.raw.stream(1024 * 1024, decode_content=False): if not data: data = chunk else: data += chunk return data
def create_par(bucket, encrypt_key, key=None, readable=True, writeable=False, duration=3600, cleanup_function=None): """Create a pre-authenticated request for the passed bucket and key (if key is None then the request is for the entire bucket). This will return a OSPar object that will contain a URL that can be used to access the object/bucket. If writeable is true, then the URL will also allow the object/bucket to be written to. PARs are time-limited. Set the lifetime in seconds by passing in 'duration' (by default this is one hour) Args: bucket (dict): Bucket to create OSPar for encrypt_key (PublicKey): Public key to encrypt PAR key (str, default=None): Key readable (bool, default=True): If bucket is readable writeable (bool, default=False): If bucket is writeable duration (int, default=3600): Duration OSPar should be valid for in seconds cleanup_function (function, default=None): Cleanup function to be passed to PARRegistry Returns: OSPar: Pre-authenticated request for the bucket """ from Acquire.Crypto import PublicKey as _PublicKey if not isinstance(encrypt_key, _PublicKey): from Acquire.Client import PARError raise PARError( "You must supply a valid PublicKey to encrypt the " "returned OSPar") # get the UTC datetime when this OSPar should expire from Acquire.ObjectStore import get_datetime_now as _get_datetime_now expires_datetime = _get_datetime_now() + \ _datetime.timedelta(seconds=duration) is_bucket = (key is None) # Limitation of OCI - cannot have a bucket OSPar with # read permissions! if is_bucket and readable: from Acquire.Client import PARError raise PARError( "You cannot create a Bucket OSPar that has read permissions " "due to a limitation in the underlying platform") try: from oci.object_storage.models import \ CreatePreauthenticatedRequestDetails as \ _CreatePreauthenticatedRequestDetails except: raise ImportError( "Cannot import OCI. Please install OCI, e.g. via " "'pip install oci' so that you can connect to the " "Oracle Cloud Infrastructure") oci_par = None request = _CreatePreauthenticatedRequestDetails() if is_bucket: request.access_type = "AnyObjectWrite" elif readable and writeable: request.access_type = "ObjectReadWrite" elif readable: request.access_type = "ObjectRead" elif writeable: request.access_type = "ObjectWrite" else: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "Unsupported permissions model for OSPar!") request.name = str(_uuid.uuid4()) if not is_bucket: request.object_name = _clean_key(key) request.time_expires = expires_datetime client = bucket["client"] try: response = client.create_preauthenticated_request( client.get_namespace().data, bucket["bucket_name"], request) except Exception as e: # couldn't create the preauthenticated request from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "Unable to create the OSPar '%s': %s" % (str(request), str(e))) if response.status != 200: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "Unable to create the OSPar '%s': Status %s, Error %s" % (str(request), response.status, str(response.data))) oci_par = response.data if oci_par is None: from Acquire.ObjectStore import ObjectStoreError raise ObjectStoreError( "Unable to create the preauthenticated request!") created_datetime = oci_par.time_created.replace( tzinfo=_datetime.timezone.utc) expires_datetime = oci_par.time_expires.replace( tzinfo=_datetime.timezone.utc) # the URI returned by OCI does not include the server. We need # to get the server based on the region of this bucket url = _get_object_url_for_region(bucket["region"], oci_par.access_uri) # get the checksum for this URL - used to validate the close # request from Acquire.ObjectStore import OSPar as _OSPar from Acquire.ObjectStore import OSParRegistry as _OSParRegistry url_checksum = _OSPar.checksum(url) driver_details = {"driver": "oci", "bucket": bucket["bucket_name"], "created_datetime": created_datetime, "par_id": oci_par.id, "par_name": oci_par.name} par = _OSPar(url=url, encrypt_key=encrypt_key, key=oci_par.object_name, expires_datetime=expires_datetime, is_readable=readable, is_writeable=writeable, driver_details=driver_details) _OSParRegistry.register(par=par, url_checksum=url_checksum, details_function=_get_driver_details_from_par, cleanup_function=cleanup_function) return par