Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ Storage
(GITHUB-1397)
[Clemens Wolff - @c-w]

- [Azure Blobs] Implement ``get_object_cdn_url`` for the Azure Storage driver.

Leveraging Azure storage service shared access signatures, the Azure Storage
driver can now be used to generate temporary URLs that grant clients read
access to objects. The URLs expire after a certain period of time, either
configured via the ``ex_expiry`` argument or the
``LIBCLOUD_AZURE_STORAGE_CDN_URL_EXPIRY_HOURS`` environment variable
(default: 24 hours).

Reported by @rvolykh.
(GITHUB-1403, GITHUB-1408)
[Clemens Wolff - @c-w]

Changes in Apache Libcloud v2.8.0
---------------------------------

Expand Down
80 changes: 79 additions & 1 deletion libcloud/storage/drivers/azure_blobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
from __future__ import with_statement

import base64
import hashlib
import hmac
import os
import binascii
from datetime import datetime, timedelta

from libcloud.utils.py3 import ET
from libcloud.utils.py3 import httplib
from libcloud.utils.py3 import urlencode
from libcloud.utils.py3 import urlquote
from libcloud.utils.py3 import tostring
from libcloud.utils.py3 import b
Expand Down Expand Up @@ -65,6 +69,16 @@

AZURE_STORAGE_HOST_SUFFIX = 'blob.core.windows.net'

AZURE_STORAGE_CDN_URL_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ'

AZURE_STORAGE_CDN_URL_START_MINUTES = float(
os.getenv('LIBCLOUD_AZURE_STORAGE_CDN_URL_START_MINUTES', '5')
)

AZURE_STORAGE_CDN_URL_EXPIRY_HOURS = float(
os.getenv('LIBCLOUD_AZURE_STORAGE_CDN_URL_EXPIRY_HOURS', '24')
)


class AzureBlobLease(object):
"""
Expand Down Expand Up @@ -180,7 +194,7 @@ def morph_action_hook(self, action):

return action

API_VERSION = '2016-05-31'
API_VERSION = '2018-11-09'


class AzureBlobsStorageDriver(StorageDriver):
Expand Down Expand Up @@ -489,6 +503,70 @@ def get_object(self, container_name, object_name):
raise ObjectDoesNotExistError(value=None, driver=self,
object_name=object_name)

def get_object_cdn_url(self, obj,
ex_expiry=AZURE_STORAGE_CDN_URL_EXPIRY_HOURS):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going forward, we can also start adding type annotations for various method arguments in the drivers when we introduce new functionality.

We now don't support Python 2.7 anymore, but perhaps we should still start with type annotations in the comments and move to putting them directly into the method signatures in the future.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a note to work on a follow-up PR with type annotations for the storage API.

"""
Return a SAS URL that enables reading the given object.

:param obj: Object instance.
:type obj: :class:`Object`

:param ex_expiry: The number of hours after which the URL expires.
Defaults to 24 hours.
:type ex_expiry: ``float``

:return: A SAS URL for the object.
:rtype: ``str``
"""
object_path = self._get_object_path(obj.container, obj.name)

now = datetime.utcnow()
start = now - timedelta(minutes=AZURE_STORAGE_CDN_URL_START_MINUTES)
expiry = now + timedelta(hours=ex_expiry)

params = {
'st': start.strftime(AZURE_STORAGE_CDN_URL_DATE_FORMAT),
'se': expiry.strftime(AZURE_STORAGE_CDN_URL_DATE_FORMAT),
'sp': 'r',
'spr': 'https' if self.secure else 'http,https',
'sv': self.connectionCls.API_VERSION,
'sr': 'b',
}

string_to_sign = '\n'.join((
params['sp'],
params['st'],
params['se'],
'/blob/{}{}'.format(self.key, object_path),
'', # signedIdentifier
'', # signedIP
params['spr'],
params['sv'],
params['sr'],
'', # snapshot
'', # rscc
'', # rscd
'', # rsce
'', # rscl
'', # rsct
))

params['sig'] = base64.b64encode(
hmac.new(
self.secret,
string_to_sign.encode('utf-8'),
hashlib.sha256
).digest()
).decode('utf-8')

return '{scheme}://{host}:{port}{action}?{sas_token}'.format(
scheme='https' if self.secure else 'http',
host=self.connection.host,
port=self.connection.port,
action=self.connection.morph_action_hook(object_path),
sas_token=urlencode(params),
)

def _get_container_path(self, container):
"""
Return a container path
Expand Down
10 changes: 10 additions & 0 deletions libcloud/test/storage/test_azure_blobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,16 @@ def test_get_container_success(self):
self.assertTrue(container.extra['lease']['state'], 'available')
self.assertTrue(container.extra['meta_data']['meta1'], 'value1')

def test_get_object_cdn_url(self):
obj = self.driver.get_object(container_name='test_container200',
object_name='test')

url = urlparse.urlparse(self.driver.get_object_cdn_url(obj))
query = urlparse.parse_qs(url.query)

self.assertEqual(len(query['sig']), 1)
self.assertGreater(len(query['sig'][0]), 0)

def test_get_object_container_doesnt_exist(self):
# This method makes two requests which makes mocking the response a bit
# trickier
Expand Down