diff --git a/ChangeLog.md b/ChangeLog.md index 2eb9904a..bec69210 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,6 +6,7 @@ ### All: - Added logging to the library, the name of the logger is 'azure.storage'. User must add handlers to the logger to output logs. +- Secondary endpoints may now be specified in connection strings, provided the corresponding primary endpoint is specified. See the connection string documentation for details. ## Version 0.36.0: diff --git a/azure/storage/_connection.py b/azure/storage/_connection.py index baa24b54..9c308f36 100644 --- a/azure/storage/_connection.py +++ b/azure/storage/_connection.py @@ -39,17 +39,24 @@ _EMULATOR_ENDPOINTS = { 'file': '', } -_CONNECTION_ENDPONTS = { +_CONNECTION_ENDPOINTS = { 'blob': 'BlobEndpoint', 'queue': 'QueueEndpoint', 'table': 'TableEndpoint', 'file': 'FileEndpoint', } +_CONNECTION_ENDPOINTS_SECONDARY = { + 'blob': 'BlobSecondaryEndpoint', + 'queue': 'QueueSecondaryEndpoint', + 'table': 'TableSecondaryEndpoint', + 'file': 'FileSecondaryEndpoint', +} + class _ServiceParameters(object): - def __init__(self, service, account_name=None, account_key=None, sas_token=None, - is_emulated=False, protocol=DEFAULT_PROTOCOL, endpoint_suffix=SERVICE_HOST_BASE, - custom_domain=None): + def __init__(self, service, account_name=None, account_key=None, sas_token=None, + is_emulated=False, protocol=DEFAULT_PROTOCOL, endpoint_suffix=SERVICE_HOST_BASE, + custom_domain=None, custom_domain_secondary=None): self.account_name = account_name self.account_key = account_key @@ -88,10 +95,21 @@ class _ServiceParameters(object): self.primary_endpoint = '{}.{}.{}'.format(self.account_name, service, endpoint_suffix) # Setup the secondary endpoint - if self.account_name: - self.secondary_endpoint = '{}-secondary.{}.{}'.format(self.account_name, service, endpoint_suffix) + if custom_domain_secondary: + if not custom_domain: + raise ValueError(_ERROR_STORAGE_MISSING_INFO) + + parsed_url = urlparse(custom_domain_secondary) + + # Trim any trailing slashes from the path + path = parsed_url.path.rstrip('/') + + self.secondary_endpoint = parsed_url.netloc + path else: - self.secondary_endpoint = None + if self.account_name: + self.secondary_endpoint = '{}-secondary.{}.{}'.format(self.account_name, service, endpoint_suffix) + else: + self.secondary_endpoint = None @staticmethod def get_service_parameters(service, account_name=None, account_key=None, sas_token=None, is_emulated=None, @@ -135,7 +153,8 @@ class _ServiceParameters(object): endpoint_suffix = config.get('EndpointSuffix') # Custom URLs - endpoint = config.get(_CONNECTION_ENDPONTS[service]) + endpoint = config.get(_CONNECTION_ENDPOINTS[service]) + endpoint_secondary = config.get(_CONNECTION_ENDPOINTS_SECONDARY[service]) return _ServiceParameters(service, account_name=account_name, @@ -144,4 +163,5 @@ class _ServiceParameters(object): is_emulated=is_emulated, protocol=protocol, endpoint_suffix=endpoint_suffix, - custom_domain=endpoint) + custom_domain=endpoint, + custom_domain_secondary=endpoint_secondary) diff --git a/tests/test_client.py b/tests/test_client.py index 98763891..069c25eb 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -36,6 +36,20 @@ SERVICES = { FileService: 'file' } +_CONNECTION_ENDPOINTS = { + 'blob': 'BlobEndpoint', + 'queue': 'QueueEndpoint', + 'table': 'TableEndpoint', + 'file': 'FileEndpoint', +} + +_CONNECTION_ENDPOINTS_SECONDARY = { + 'blob': 'BlobSecondaryEndpoint', + 'queue': 'QueueSecondaryEndpoint', + 'table': 'TableSecondaryEndpoint', + 'file': 'FileSecondaryEndpoint', +} + class StorageClientTest(StorageTestCase): def setUp(self): @@ -294,6 +308,32 @@ class StorageClientTest(StorageTestCase): self.assertEqual(service.primary_endpoint, 'www.mydomain.com') self.assertEqual(service.secondary_endpoint, self.account_name + '-secondary.blob.core.windows.net') + def test_create_service_with_connection_string_fails_if_secondary_without_primary(self): + for type in SERVICES.items(): + # Arrange + conn_string = 'AccountName={};AccountKey={};{}=www.mydomain.com;'.format(self.account_name, self.account_key, _CONNECTION_ENDPOINTS_SECONDARY.get(type[1])) + + # Act + + # Fails if primary excluded + with self.assertRaises(ValueError): + service = type[0](connection_string=conn_string) + + def test_create_service_with_connection_string_succeeds_if_secondary_with_primary(self): + for type in SERVICES.items(): + # Arrange + conn_string = 'AccountName={};AccountKey={};{}=www.mydomain.com;{}=www-sec.mydomain.com;'.format(self.account_name, self.account_key, _CONNECTION_ENDPOINTS.get(type[1]), _CONNECTION_ENDPOINTS_SECONDARY.get(type[1])) + + # Act + service = type[0](connection_string=conn_string) + + # Assert + self.assertIsNotNone(service) + self.assertEqual(service.account_name, self.account_name) + self.assertEqual(service.account_key, self.account_key) + self.assertEqual(service.primary_endpoint, 'www.mydomain.com') + self.assertEqual(service.secondary_endpoint, 'www-sec.mydomain.com') + @record def test_request_callback_signed_header(self): # Arrange