diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py index 5441488d86a9..33639019dfa8 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py @@ -73,6 +73,32 @@ "file": {"primary": "FILEENDPOINT", "secondary": "FILESECONDARYENDPOINT"}, "dfs": {"primary": "BLOBENDPOINT", "secondary": "BLOBENDPOINT"}, } +_ACCOUNT_NAME_SUFFIXES = ( + "-secondary-dualstack", + "-secondary-ipv6", + "-secondary", + "-dualstack", + "-ipv6", +) + + +def _strip_account_name_suffix(account_name: str) -> str: + """Strip any well-known storage endpoint suffix from `account_name`. + + Azure Storage endpoints may include suffixes such as `-secondary`, + `-dualstack` or `-ipv6` after the real account name. This function + removes those suffixes so callers always get back the base account name. + + :param account_name: The raw account name segment extracted from a storage + endpoint hostname (i.e. everything before the first `.blob.core.`). + :type account_name: str + :return: The account name with any recognized suffix removed. + :rtype: str + """ + for suffix in _ACCOUNT_NAME_SUFFIXES: + if account_name.endswith(suffix): + return account_name[: -len(suffix)] + return account_name class StorageAccountHostsMixin(object): @@ -106,7 +132,7 @@ def __init__( service_name = service.split("-")[0] account = parsed_url.netloc.split(f".{service_name}.core.") - self.account_name = account[0] if len(account) > 1 else None + self.account_name = _strip_account_name_suffix(account[0]) if len(account) > 1 else None if ( not self.account_name and parsed_url.netloc.startswith("localhost") diff --git a/sdk/storage/azure-storage-blob/tests/test_blob_client.py b/sdk/storage/azure-storage-blob/tests/test_blob_client.py index 5ad959336629..c853c5364bf7 100644 --- a/sdk/storage/azure-storage-blob/tests/test_blob_client.py +++ b/sdk/storage/azure-storage-blob/tests/test_blob_client.py @@ -299,6 +299,34 @@ def test_create_service_with_socket_timeout(self, **kwargs): assert service._client._client._pipeline._transport.connection_config.timeout == 22 assert default_service._client._client._pipeline._transport.connection_config.timeout in [20, (20, 2000)] + @pytest.mark.parametrize( + "account_url", [ + "https://my-account.blob.core.windows.net/", + "https://my-account-secondary.blob.core.windows.net/", + "https://my-account-dualstack.blob.core.windows.net/", + "https://my-account-ipv6.blob.core.windows.net/", + "https://my-account-secondary-dualstack.blob.core.windows.net/", + "https://my-account-secondary-ipv6.blob.core.windows.net/", + ] + ) + @BlobPreparer() + def test_create_service_ipv6(self, account_url, **kwargs): + storage_account_name = "my-account" + storage_account_key = kwargs.pop("storage_account_key") + + for service_type in SERVICES.keys(): + service = service_type( + account_url, + credential=storage_account_key.secret, + container_name='foo', + blob_name='bar' + ) + + assert service is not None + assert service.scheme == "https" + assert service.account_name == storage_account_name + assert service.credential.account_key == storage_account_key.secret + # --Connection String Test Cases -------------------------------------------- @BlobPreparer() diff --git a/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py b/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py index 3873f8d00b27..f91a26f87503 100644 --- a/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py +++ b/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py @@ -287,6 +287,34 @@ def test_create_service_with_socket_timeout(self, **kwargs): assert service._client._client._pipeline._transport.connection_config.timeout == 22 assert default_service._client._client._pipeline._transport.connection_config.timeout in [20, (20, 2000)] + @pytest.mark.parametrize( + "account_url", [ + "https://my-account.blob.core.windows.net/", + "https://my-account-secondary.blob.core.windows.net/", + "https://my-account-dualstack.blob.core.windows.net/", + "https://my-account-ipv6.blob.core.windows.net/", + "https://my-account-secondary-dualstack.blob.core.windows.net/", + "https://my-account-secondary-ipv6.blob.core.windows.net/", + ] + ) + @BlobPreparer() + def test_create_service_ipv6(self, account_url, **kwargs): + storage_account_name = "my-account" + storage_account_key = kwargs.pop("storage_account_key") + + for service_type in SERVICES.keys(): + service = service_type( + account_url, + credential=storage_account_key.secret, + container_name='foo', + blob_name='bar' + ) + + assert service is not None + assert service.scheme == "https" + assert service.account_name == storage_account_name + assert service.credential.account_key == storage_account_key.secret + # --Connection String Test Cases -------------------------------------------- @BlobPreparer() def test_create_service_with_connection_string_key(self, **kwargs): diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py index 5441488d86a9..33639019dfa8 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py @@ -73,6 +73,32 @@ "file": {"primary": "FILEENDPOINT", "secondary": "FILESECONDARYENDPOINT"}, "dfs": {"primary": "BLOBENDPOINT", "secondary": "BLOBENDPOINT"}, } +_ACCOUNT_NAME_SUFFIXES = ( + "-secondary-dualstack", + "-secondary-ipv6", + "-secondary", + "-dualstack", + "-ipv6", +) + + +def _strip_account_name_suffix(account_name: str) -> str: + """Strip any well-known storage endpoint suffix from `account_name`. + + Azure Storage endpoints may include suffixes such as `-secondary`, + `-dualstack` or `-ipv6` after the real account name. This function + removes those suffixes so callers always get back the base account name. + + :param account_name: The raw account name segment extracted from a storage + endpoint hostname (i.e. everything before the first `.blob.core.`). + :type account_name: str + :return: The account name with any recognized suffix removed. + :rtype: str + """ + for suffix in _ACCOUNT_NAME_SUFFIXES: + if account_name.endswith(suffix): + return account_name[: -len(suffix)] + return account_name class StorageAccountHostsMixin(object): @@ -106,7 +132,7 @@ def __init__( service_name = service.split("-")[0] account = parsed_url.netloc.split(f".{service_name}.core.") - self.account_name = account[0] if len(account) > 1 else None + self.account_name = _strip_account_name_suffix(account[0]) if len(account) > 1 else None if ( not self.account_name and parsed_url.netloc.startswith("localhost") diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py index 5441488d86a9..33639019dfa8 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py @@ -73,6 +73,32 @@ "file": {"primary": "FILEENDPOINT", "secondary": "FILESECONDARYENDPOINT"}, "dfs": {"primary": "BLOBENDPOINT", "secondary": "BLOBENDPOINT"}, } +_ACCOUNT_NAME_SUFFIXES = ( + "-secondary-dualstack", + "-secondary-ipv6", + "-secondary", + "-dualstack", + "-ipv6", +) + + +def _strip_account_name_suffix(account_name: str) -> str: + """Strip any well-known storage endpoint suffix from `account_name`. + + Azure Storage endpoints may include suffixes such as `-secondary`, + `-dualstack` or `-ipv6` after the real account name. This function + removes those suffixes so callers always get back the base account name. + + :param account_name: The raw account name segment extracted from a storage + endpoint hostname (i.e. everything before the first `.blob.core.`). + :type account_name: str + :return: The account name with any recognized suffix removed. + :rtype: str + """ + for suffix in _ACCOUNT_NAME_SUFFIXES: + if account_name.endswith(suffix): + return account_name[: -len(suffix)] + return account_name class StorageAccountHostsMixin(object): @@ -106,7 +132,7 @@ def __init__( service_name = service.split("-")[0] account = parsed_url.netloc.split(f".{service_name}.core.") - self.account_name = account[0] if len(account) > 1 else None + self.account_name = _strip_account_name_suffix(account[0]) if len(account) > 1 else None if ( not self.account_name and parsed_url.netloc.startswith("localhost") diff --git a/sdk/storage/azure-storage-file-share/tests/test_file_client.py b/sdk/storage/azure-storage-file-share/tests/test_file_client.py index df8a607f27f8..62b01359e79a 100644 --- a/sdk/storage/azure-storage-file-share/tests/test_file_client.py +++ b/sdk/storage/azure-storage-file-share/tests/test_file_client.py @@ -176,6 +176,35 @@ def test_create_service_with_socket_timeout(self, **kwargs): assert service._client._client._pipeline._transport.connection_config.timeout == 22 assert default_service._client._client._pipeline._transport.connection_config.timeout in [20, (20, 2000)] + @pytest.mark.parametrize( + "account_url", [ + "https://my-account.file.core.windows.net/", + "https://my-account-secondary.file.core.windows.net/", + "https://my-account-dualstack.file.core.windows.net/", + "https://my-account-ipv6.file.core.windows.net/", + "https://my-account-secondary-dualstack.file.core.windows.net/", + "https://my-account-secondary-ipv6.file.core.windows.net/", + ] + ) + @FileSharePreparer() + def test_create_service_ipv6(self, account_url, **kwargs): + storage_account_name = "my-account" + storage_account_key = kwargs.pop("storage_account_key") + + for service_type in SERVICES.keys(): + service = service_type( + account_url, + credential=storage_account_key.secret, + share_name='foo', + directory_path='bar', + file_path='baz' + ) + + assert service is not None + assert service.scheme == "https" + assert service.account_name == storage_account_name + assert service.credential.account_key == storage_account_key.secret + # --Connection String Test Cases -------------------------------------------- @FileSharePreparer() diff --git a/sdk/storage/azure-storage-file-share/tests/test_file_client_async.py b/sdk/storage/azure-storage-file-share/tests/test_file_client_async.py index bd2dac98405b..48c997780860 100644 --- a/sdk/storage/azure-storage-file-share/tests/test_file_client_async.py +++ b/sdk/storage/azure-storage-file-share/tests/test_file_client_async.py @@ -176,6 +176,35 @@ async def test_create_service_with_socket_timeout(self, **kwargs): assert service._client._client._pipeline._transport.connection_config.timeout == 22 assert default_service._client._client._pipeline._transport.connection_config.timeout in [20, (20, 2000)] + @pytest.mark.parametrize( + "account_url", [ + "https://my-account.file.core.windows.net/", + "https://my-account-secondary.file.core.windows.net/", + "https://my-account-dualstack.file.core.windows.net/", + "https://my-account-ipv6.file.core.windows.net/", + "https://my-account-secondary-dualstack.file.core.windows.net/", + "https://my-account-secondary-ipv6.file.core.windows.net/", + ] + ) + @FileSharePreparer() + def test_create_service_ipv6(self, account_url, **kwargs): + storage_account_name = "my-account" + storage_account_key = kwargs.pop("storage_account_key") + + for service_type in SERVICES.keys(): + service = service_type( + account_url, + credential=storage_account_key.secret, + share_name='foo', + directory_path='bar', + file_path='baz' + ) + + assert service is not None + assert service.scheme == "https" + assert service.account_name == storage_account_name + assert service.credential.account_key == storage_account_key.secret + # --Connection String Test Cases -------------------------------------------- @FileSharePreparer() diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py index 3f7609b9f026..c3ca04b8f5f0 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py @@ -73,6 +73,32 @@ "file": {"primary": "FILEENDPOINT", "secondary": "FILESECONDARYENDPOINT"}, "dfs": {"primary": "BLOBENDPOINT", "secondary": "BLOBENDPOINT"}, } +_ACCOUNT_NAME_SUFFIXES = ( + "-secondary-dualstack", + "-secondary-ipv6", + "-secondary", + "-dualstack", + "-ipv6", +) + + +def _strip_account_name_suffix(account_name: str) -> str: + """Strip any well-known storage endpoint suffix from `account_name`. + + Azure Storage endpoints may include suffixes such as `-secondary`, + `-dualstack` or `-ipv6` after the real account name. This function + removes those suffixes so callers always get back the base account name. + + :param account_name: The raw account name segment extracted from a storage + endpoint hostname (i.e. everything before the first `.blob.core.`). + :type account_name: str + :return: The account name with any recognized suffix removed. + :rtype: str + """ + for suffix in _ACCOUNT_NAME_SUFFIXES: + if account_name.endswith(suffix): + return account_name[: -len(suffix)] + return account_name class StorageAccountHostsMixin(object): @@ -106,7 +132,7 @@ def __init__( service_name = service.split("-")[0] account = parsed_url.netloc.split(f".{service_name}.core.") - self.account_name = account[0] if len(account) > 1 else None + self.account_name = _strip_account_name_suffix(account[0]) if len(account) > 1 else None if ( not self.account_name and parsed_url.netloc.startswith("localhost") diff --git a/sdk/storage/azure-storage-queue/tests/test_queue_client.py b/sdk/storage/azure-storage-queue/tests/test_queue_client.py index eefb88c6ce13..b3aba069fe92 100644 --- a/sdk/storage/azure-storage-queue/tests/test_queue_client.py +++ b/sdk/storage/azure-storage-queue/tests/test_queue_client.py @@ -235,6 +235,30 @@ def test_create_service_with_socket_timeout(self, **kwargs): assert service._client._client._pipeline._transport.connection_config.timeout == 22 assert default_service._client._client._pipeline._transport.connection_config.timeout in [20, (20, 2000)] + @pytest.mark.parametrize( + "account_url", + [ + "https://my-account.queue.core.windows.net/", + "https://my-account-secondary.queue.core.windows.net/", + "https://my-account-dualstack.queue.core.windows.net/", + "https://my-account-ipv6.queue.core.windows.net/", + "https://my-account-secondary-dualstack.queue.core.windows.net/", + "https://my-account-secondary-ipv6.queue.core.windows.net/", + ], + ) + @QueuePreparer() + def test_create_service_ipv6(self, account_url, **kwargs): + storage_account_name = "my-account" + storage_account_key = kwargs.pop("storage_account_key") + + for service_type in SERVICES.keys(): + service = service_type(account_url, credential=storage_account_key.secret, queue_name="foo") + + assert service is not None + assert service.scheme == "https" + assert service.account_name == storage_account_name + assert service.credential.account_key == storage_account_key.secret + # --Connection String Test Cases -------------------------------------------- @QueuePreparer() def test_create_service_with_connection_string_key(self, **kwargs): diff --git a/sdk/storage/azure-storage-queue/tests/test_queue_client_async.py b/sdk/storage/azure-storage-queue/tests/test_queue_client_async.py index 1a2366e8dc41..2381a3cbfe10 100644 --- a/sdk/storage/azure-storage-queue/tests/test_queue_client_async.py +++ b/sdk/storage/azure-storage-queue/tests/test_queue_client_async.py @@ -226,6 +226,30 @@ def test_create_service_with_socket_timeout(self, **kwargs): assert service._client._client._pipeline._transport.connection_config.timeout == 22 assert default_service._client._client._pipeline._transport.connection_config.timeout in [20, (20, 2000)] + @pytest.mark.parametrize( + "account_url", + [ + "https://my-account.queue.core.windows.net/", + "https://my-account-secondary.queue.core.windows.net/", + "https://my-account-dualstack.queue.core.windows.net/", + "https://my-account-ipv6.queue.core.windows.net/", + "https://my-account-secondary-dualstack.queue.core.windows.net/", + "https://my-account-secondary-ipv6.queue.core.windows.net/", + ], + ) + @QueuePreparer() + def test_create_service_ipv6(self, account_url, **kwargs): + storage_account_name = "my-account" + storage_account_key = kwargs.pop("storage_account_key") + + for service_type in SERVICES.keys(): + service = service_type(account_url, credential=storage_account_key.secret, queue_name="foo") + + assert service is not None + assert service.scheme == "https" + assert service.account_name == storage_account_name + assert service.credential.account_key == storage_account_key.secret + # --Connection String Test Cases -------------------------------------------- @QueuePreparer() def test_create_service_with_connection_string_key(self, **kwargs):