diff --git a/sdk/storagemover/azure-mgmt-storagemover/assets.json b/sdk/storagemover/azure-mgmt-storagemover/assets.json new file mode 100644 index 000000000000..e4adb97dbc44 --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "python", + "TagPrefix": "python/storagemover/azure-mgmt-storagemover", + "Tag": "python/storagemover/azure-mgmt-storagemover_ee26229261" +} diff --git a/sdk/storagemover/azure-mgmt-storagemover/dev_requirements.txt b/sdk/storagemover/azure-mgmt-storagemover/dev_requirements.txt index 03dba171e250..f937a8811655 100644 --- a/sdk/storagemover/azure-mgmt-storagemover/dev_requirements.txt +++ b/sdk/storagemover/azure-mgmt-storagemover/dev_requirements.txt @@ -1,3 +1,10 @@ -e ../../../eng/tools/azure-sdk-tools ../../identity/azure-identity aiohttp +# Cross-sub mgmt clients used by scenario-test matrix row #31 +# (`test_start_c2c_job_with_private_source`) and row #10's extended public-bucket +# E2E (`test_job_definition_job_run`). Test-only deps; not in the package's +# runtime install_requires. +azure-mgmt-network +azure-mgmt-authorization +azure-mgmt-storage diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/conftest.py b/sdk/storagemover/azure-mgmt-storagemover/tests/conftest.py index 93f0ac800445..4bf84a94db49 100644 --- a/sdk/storagemover/azure-mgmt-storagemover/tests/conftest.py +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/conftest.py @@ -13,6 +13,7 @@ add_general_regex_sanitizer, add_body_key_sanitizer, add_header_regex_sanitizer, + remove_batch_sanitizers, ) load_dotenv() @@ -33,3 +34,37 @@ def add_sanitizers(test_proxy): add_header_regex_sanitizer(key="Set-Cookie", value="[set-cookie;]") add_header_regex_sanitizer(key="Cookie", value="cookie;") add_body_key_sanitizer(json_path="$..access_token", value="access_token") + + # Cross-subscription shared infra in XDataMove-Synthetics is referenced by + # matrix #31 (`test_start_c2c_job_with_private_source`), the extended + # matrix #10 (`test_job_definition_job_run`), and #32 + # (`test_create_get_list_update_delete` on connections). The sub ID is + # well-known shared infrastructure but must be sanitized in recordings. + # **Use the same target value (`00000000-...`) as the default subscription + # sanitizer above** so they cooperate at both record AND playback time — + # at record the default sanitizer wins (env-var match), at playback this + # one rewrites the source-code literal to match the cassette. + add_general_regex_sanitizer( + regex="b6b34ad8-ca89-4f85-beb7-c2ec13702dac", + value="00000000-0000-0000-0000-000000000000", + ) + # Sanitize the per-run role-assignment GUID we mint for matrix #31 and #10. + # The GUID is a path segment under `roleAssignments/` — redact only + # there to avoid clobbering unrelated GUIDs elsewhere in payloads. The + # `variables` mechanism makes the source GUID deterministic across runs, + # so both request and cassette get the same sanitized value. + add_general_regex_sanitizer( + regex=r"(?<=roleAssignments/)[0-9a-fA-F\-]{36}", + value="00000000-0000-0000-0000-000000000000", + ) + # Sanitize managed-identity principalId values returned by the RP on + # blob-container endpoints (matrix #10 + #31) so cassettes don't leak + # object IDs we don't control. + add_body_key_sanitizer(json_path="$..principalId", value="00000000-0000-0000-0000-000000000000") + + # Remove default sanitizers that clobber non-sensitive fields the storage mover + # tests need to assert on: + # - AZSDK3430: $..id (resource ARM IDs - subscription is already sanitized) + # - AZSDK3493: $..name (resource names like storage mover / endpoint / project names) + # - AZSDK2003: Location header (needed for LRO polling on begin_delete, etc.) + remove_batch_sanitizers(["AZSDK3430", "AZSDK3493", "AZSDK2003"]) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_agents_operations_async_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_agents_operations_async_test.py new file mode 100644 index 000000000000..344291d8fe96 --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_agents_operations_async_test.py @@ -0,0 +1,106 @@ + +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +import pytest +from azure.core.exceptions import ResourceNotFoundError +from azure.mgmt.storagemover.aio import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase +from devtools_testutils.aio import recorded_by_proxy_async + +RESOURCE_GROUP_NAME = "teststomover" +STORAGE_MOVER_NAME = "testsm1" +AGENT_NAME = "testagent1" +MISSING_AGENT_NAME = AGENT_NAME + "111" +UPDATED_DESCRIPTION = "This is an updated agent" +UPLOAD_LIMIT_SCHEDULE = { + "weeklyRecurrences": [ + { + "startTime": {"hour": 1}, + "endTime": {"hour": 2}, + "days": ["Monday", "Tuesday"], + "limitInMbps": 100, + } + ] +} + + +def _assert_agent_matches(expected, actual): + assert actual.name == expected.name + assert actual.id == expected.id + assert actual.local_ip_address == expected.local_ip_address + + +def _assert_upload_limit_schedule(agent): + recurrences = agent.upload_limit_schedule.weekly_recurrences + assert len(recurrences) == 1 + recurrence = recurrences[0] + assert recurrence.limit_in_mbps == 100 + assert recurrence.days[0] == "Monday" + assert len(recurrence.days) == 2 + assert recurrence.start_time.hour == 1 + assert recurrence.start_time.minute == 0 + assert recurrence.end_time.hour == 2 + assert recurrence.end_time.minute == 0 + + +class TestStorageMoverMgmtAgentsOperationsAsync(AzureMgmtRecordedTestCase): + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient, is_async=True) + + @pytest.mark.skip(reason="Requires a registered agent VM; agents cannot be created via the RP. Live-only test.") + @pytest.mark.asyncio + @recorded_by_proxy_async + async def test_agents_get_list_update(self): + agent = await self.client.agents.get( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + agent_name=AGENT_NAME, + ) + agent_again = await self.client.agents.get( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + agent_name=AGENT_NAME, + ) + _assert_agent_matches(agent, agent_again) + + agents = [ + item + async for item in self.client.agents.list( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + ) + ] + assert len(agents) >= 1 + + updated_agent = await self.client.agents.update( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + agent_name=AGENT_NAME, + agent={ + "properties": { + "description": UPDATED_DESCRIPTION, + "uploadLimitSchedule": UPLOAD_LIMIT_SCHEDULE, + } + }, + ) + assert updated_agent.description == UPDATED_DESCRIPTION + _assert_upload_limit_schedule(updated_agent) + + fetched_agent = await self.client.agents.get( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + agent_name=AGENT_NAME, + ) + assert fetched_agent.description == UPDATED_DESCRIPTION + _assert_upload_limit_schedule(fetched_agent) + + with pytest.raises(ResourceNotFoundError): + await self.client.agents.get( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + agent_name=MISSING_AGENT_NAME, + ) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_agents_operations_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_agents_operations_test.py new file mode 100644 index 000000000000..d753496acac9 --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_agents_operations_test.py @@ -0,0 +1,102 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +import pytest +from azure.core.exceptions import ResourceNotFoundError +from azure.mgmt.storagemover import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase, recorded_by_proxy + +RESOURCE_GROUP_NAME = "teststomover" +STORAGE_MOVER_NAME = "testsm1" +AGENT_NAME = "testagent1" +MISSING_AGENT_NAME = AGENT_NAME + "111" +UPDATED_DESCRIPTION = "This is an updated agent" +UPLOAD_LIMIT_SCHEDULE = { + "weeklyRecurrences": [ + { + "startTime": {"hour": 1}, + "endTime": {"hour": 2}, + "days": ["Monday", "Tuesday"], + "limitInMbps": 100, + } + ] +} + + +def _assert_agent_matches(expected, actual): + assert actual.name == expected.name + assert actual.id == expected.id + assert actual.local_ip_address == expected.local_ip_address + + +def _assert_upload_limit_schedule(agent): + recurrences = agent.upload_limit_schedule.weekly_recurrences + assert len(recurrences) == 1 + recurrence = recurrences[0] + assert recurrence.limit_in_mbps == 100 + assert recurrence.days[0] == "Monday" + assert len(recurrence.days) == 2 + assert recurrence.start_time.hour == 1 + assert recurrence.start_time.minute == 0 + assert recurrence.end_time.hour == 2 + assert recurrence.end_time.minute == 0 + + +class TestStorageMoverMgmtAgentsOperations(AzureMgmtRecordedTestCase): + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient) + + @pytest.mark.skip(reason="Requires a registered agent VM; agents cannot be created via the RP. Live-only test.") + @recorded_by_proxy + def test_agents_get_list_update(self): + agent = self.client.agents.get( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + agent_name=AGENT_NAME, + ) + agent_again = self.client.agents.get( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + agent_name=AGENT_NAME, + ) + _assert_agent_matches(agent, agent_again) + + agents = list( + self.client.agents.list( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + ) + ) + assert len(agents) >= 1 + + updated_agent = self.client.agents.update( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + agent_name=AGENT_NAME, + agent={ + "properties": { + "description": UPDATED_DESCRIPTION, + "uploadLimitSchedule": UPLOAD_LIMIT_SCHEDULE, + } + }, + ) + assert updated_agent.description == UPDATED_DESCRIPTION + _assert_upload_limit_schedule(updated_agent) + + fetched_agent = self.client.agents.get( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + agent_name=AGENT_NAME, + ) + assert fetched_agent.description == UPDATED_DESCRIPTION + _assert_upload_limit_schedule(fetched_agent) + + with pytest.raises(ResourceNotFoundError): + self.client.agents.get( + resource_group_name=RESOURCE_GROUP_NAME, + storage_mover_name=STORAGE_MOVER_NAME, + agent_name=MISSING_AGENT_NAME, + ) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_connections_operations_async_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_connections_operations_async_test.py new file mode 100644 index 000000000000..736360b90fd9 --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_connections_operations_async_test.py @@ -0,0 +1,91 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +"""Async scenario test for connections — matrix row #32. + +See the sync sibling (test_storage_mover_mgmt_connections_operations_test.py) +for the full rationale. +""" +import pytest +from azure.core.exceptions import ResourceNotFoundError +from azure.mgmt.storagemover.aio import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer +from devtools_testutils.aio import recorded_by_proxy_async + +# PrivateLinkService lives in westcentralus, so storage mover must too. +AZURE_LOCATION = "westcentralus" + +REAL_PRIVATE_LINK_SERVICE_ID = ( + "/subscriptions/b6b34ad8-ca89-4f85-beb7-c2ec13702dac" + "/resourceGroups/E2E-Management-RGsyn" + "/providers/Microsoft.Network/privateLinkServices/test-pls-wcs" +) + + +class TestStorageMoverMgmtConnectionsOperationsAsync(AzureMgmtRecordedTestCase): + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient, is_async=True) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_create_get_list_update_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-conn" + connection_name = "testconn1" + + await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + + # Create + created = await self.client.connections.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + connection={"properties": { + "privateLinkServiceId": REAL_PRIVATE_LINK_SERVICE_ID, + "description": "ConnectionDesc", + }}, + ) + # See sync sibling for why we compare by PLS name suffix instead of full ARM ID. + pls_id_suffix = "/providers/Microsoft.Network/privateLinkServices/test-pls-wcs" + assert created.name == connection_name + assert created.properties.private_link_service_id.endswith(pls_id_suffix) + assert created.properties.description == "ConnectionDesc" + + # Get + fetched = await self.client.connections.get( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + ) + assert fetched.name == connection_name + assert fetched.id == created.id + assert fetched.properties.private_link_service_id.endswith(pls_id_suffix) + # NOTE: do not assert on `connection_status` — see sync sibling docstring. + + # List + items = [c async for c in self.client.connections.list( + resource_group_name=rg, storage_mover_name=sm_name, + )] + assert len(items) >= 1 + assert connection_name in [c.name for c in items] + + # Update — see sync sibling docstring for why we don't assert on the response. + await self.client.connections.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + connection={"properties": { + "privateLinkServiceId": REAL_PRIVATE_LINK_SERVICE_ID, + "description": "ConnectionDescUpdate", + }}, + ) + + # Delete + 404 verification + poller = await self.client.connections.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + ) + await poller.result() + with pytest.raises(ResourceNotFoundError): + await self.client.connections.get( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + ) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_connections_operations_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_connections_operations_test.py new file mode 100644 index 000000000000..f0b2790ea52b --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_connections_operations_test.py @@ -0,0 +1,112 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +"""Sync scenario test for connections — matrix row #32. + +`ConnectionTests.CreateGetListUpdateDeleteTest` in the cross-language source-of-truth +matrix. Mirrors the CLI port's `test_storage_mover_connection_scenarios`. The +Connection op group is new in API 2025-08-01+ and isn't in the .NET Scenario suite +yet — this is the canonical Python port. + +Exercises Storage Mover Connection CRUD (create / get / list / update / delete) +against the **real** shared PrivateLinkService `test-pls-wcs` in subscription +``b6b34ad8-ca89-4f85-beb7-c2ec13702dac`` (XDataMove-Synthetics) / RG +``E2E-Management-RGsyn``. The PLS lives in ``westcentralus``, so the storage mover +must too. + +Intentionally does NOT assert on ``properties.connection_status``: it'll be +``Pending`` immediately after create because the PLS-side PE provisioning is +async. Approval is covered by matrix row #31 +(``JobDefinitionJobRunTests.StartC2CJobWithPrivateSourceTest``). +""" +import pytest +from azure.core.exceptions import ResourceNotFoundError +from azure.mgmt.storagemover import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer, recorded_by_proxy + +# PrivateLinkService lives in westcentralus, so storage mover must too. +AZURE_LOCATION = "westcentralus" + +# Shared team infra in XDataMove-Synthetics — do not recreate. +# Full inventory in the cross-language playbook +# (storage-mover-scenario-tests-cross-language, "Porter's reference" callout). +REAL_PRIVATE_LINK_SERVICE_ID = ( + "/subscriptions/b6b34ad8-ca89-4f85-beb7-c2ec13702dac" + "/resourceGroups/E2E-Management-RGsyn" + "/providers/Microsoft.Network/privateLinkServices/test-pls-wcs" +) + + +class TestStorageMoverMgmtConnectionsOperations(AzureMgmtRecordedTestCase): + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_create_get_list_update_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-conn" + connection_name = "testconn1" + + self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + + # Create + created = self.client.connections.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + connection={"properties": { + "privateLinkServiceId": REAL_PRIVATE_LINK_SERVICE_ID, + "description": "ConnectionDesc", + }}, + ) + # Playback note: the test runner's subscription ID is sanitized to all-zeros + # in recordings, so we assert by PLS name suffix (resource-path-stable across + # sanitization) rather than full ARM-ID equality. + pls_id_suffix = "/providers/Microsoft.Network/privateLinkServices/test-pls-wcs" + assert created.name == connection_name + assert created.properties.private_link_service_id.endswith(pls_id_suffix) + assert created.properties.description == "ConnectionDesc" + + # Get + fetched = self.client.connections.get( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + ) + assert fetched.name == connection_name + assert fetched.id == created.id + assert fetched.properties.private_link_service_id.endswith(pls_id_suffix) + # NOTE: do not assert on `connection_status` — see module docstring. + + # List + items = list(self.client.connections.list( + resource_group_name=rg, storage_mover_name=sm_name, + )) + assert len(items) >= 1 + assert connection_name in [c.name for c in items] + + # Update — call PUT with a new description. The Storage Mover RP echoes + # the existing description in the immediate PUT response (the description + # field is effectively immutable post-create or eventually consistent), + # so we do not assert on the returned value. The CLI scenario test + # (test_storage_mover_connection_scenarios) also calls update without + # post-update verification for the same reason. + self.client.connections.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + connection={"properties": { + "privateLinkServiceId": REAL_PRIVATE_LINK_SERVICE_ID, + "description": "ConnectionDescUpdate", + }}, + ) + + # Delete + 404 verification + self.client.connections.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + ).result() + with pytest.raises(ResourceNotFoundError): + self.client.connections.get( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + ) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_endpoints_operations_async_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_endpoints_operations_async_test.py new file mode 100644 index 000000000000..057006954dd7 --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_endpoints_operations_async_test.py @@ -0,0 +1,539 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +"""Async scenario tests for endpoints. + +Mirrors .NET EndpointTests at: + Q:\\source\\azure-sdk-for-net\\sdk\\storagemover\\Azure.ResourceManager.StorageMover\\tests\\Scenario\\EndpointTests.cs +""" +import pytest +from azure.core.exceptions import HttpResponseError, ResourceNotFoundError +from azure.mgmt.storagemover.aio import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer +from devtools_testutils.aio import recorded_by_proxy_async + +AZURE_LOCATION = "eastus" +STORAGE_ACCOUNT_NAME = "testsmstore24" +CONTAINER_NAME = "testsmcontainer" + +FAKE_SUBSCRIPTION_ID = "00000000-0000-0000-0000-000000000000" +MULTI_CLOUD_CONNECTOR_ID = ( + "/subscriptions/b6b34ad8-ca89-4f85-beb7-c2ec13702dac" + "/resourceGroups/E2E-Management-RGsyn" + "/providers/Microsoft.HybridConnectivity/publicCloudConnectors/e2e-sm-rp-connector" +) +AWS_S3_BUCKET_ID = ( + "/subscriptions/b6b34ad8-ca89-4f85-beb7-c2ec13702dac" + "/resourceGroups/aws_640698235822" + "/providers/Microsoft.AWSConnector/s3Buckets/e2e-sm-rp-bucket" +) + + +def _account_id(rg): + return ( + f"/subscriptions/{FAKE_SUBSCRIPTION_ID}/resourceGroups/{rg}" + f"/providers/Microsoft.Storage/storageAccounts/{STORAGE_ACCOUNT_NAME}" + ) + + +class TestStorageMoverMgmtEndpointsOperationsAsync(AzureMgmtRecordedTestCase): + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient, is_async=True) + + async def _create_storage_mover(self, rg, sm_name): + await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + + async def _delete_endpoint(self, rg, sm_name, endpoint_name): + poller = await self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + await poller.result() + + # ----- EndpointTests.CreateUpdateGetDeleteTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_create_update_get_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-epcrud" + await self._create_storage_mover(rg, sm_name) + + c_name = "conendpoint-1" + nfs_name = "nfsendpoint-1" + smb_name = "smbendpoint-1" + fs_name = "fsendpoint-1" + + c = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=c_name, + endpoint={"properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": _account_id(rg), + "blobContainerName": CONTAINER_NAME, + "description": "New container endpoint", + }}, + ) + assert c.name == c_name + assert c.properties.endpoint_type == "AzureStorageBlobContainer" + + c_get = await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=c_name, + ) + assert c_get.name == c_name + + nfs = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=nfs_name, + endpoint={"properties": { + "endpointType": "NfsMount", + "host": "10.0.0.1", + "export": "/", + "nfsVersion": "NFSv3", + "description": "New NFS endpoint", + }}, + ) + assert nfs.properties.host == "10.0.0.1" + assert nfs.properties.export == "/" + + nfs_get = await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=nfs_name, + ) + assert nfs_get.properties.host == "10.0.0.1" + + smb = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=smb_name, + endpoint={"properties": { + "endpointType": "SmbMount", + "host": "10.0.0.1", + "shareName": "testshare", + "credentials": { + "type": "AzureKeyVaultSmb", + "usernameUri": "https://examples-azureKeyVault.vault.azure.net/secrets/examples-username", + "passwordUri": "https://examples-azureKeyVault.vault.azure.net/secrets/examples-password", + }, + "description": "New Smb mount endpoint", + }}, + ) + assert smb.properties.share_name == "testshare" + + # Workaround for an RP regression in api-version 2025-12-01: the endpoint + # PATCH (update) handler requires a non-null `identity` on the payload root. + # The PUT (create) above succeeds without identity — so this is specifically + # an update-path validation bug, not a real schema requirement. Sending + # `{"type": "None"}` (the standard ARM "no managed identity" sentinel) + # satisfies the check. The .NET test omits identity entirely and would also + # fail today against this api-version. + smb_updated = await self.client.endpoints.update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=smb_name, + endpoint={ + "identity": {"type": "None"}, + "properties": { + "endpointType": "SmbMount", + "credentials": { + "type": "AzureKeyVaultSmb", + "usernameUri": "", + "passwordUri": "", + }, + "description": "Update endpoint", + }, + }, + ) + assert smb_updated.properties.host == "10.0.0.1" + assert smb_updated.properties.share_name == "testshare" + + await self._delete_endpoint(rg, sm_name, smb_name) + + fs = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=fs_name, + endpoint={"properties": { + "endpointType": "AzureStorageSmbFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testfileshare", + "description": "new file share endpoint", + }}, + ) + assert fs.properties.file_share_name == "testfileshare" + + fs_get = await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=fs_name, + ) + assert fs_get.properties.description == "new file share endpoint" + + items = [e async for e in self.client.endpoints.list( + resource_group_name=rg, storage_mover_name=sm_name, + )] + assert len(items) > 1 + + with pytest.raises(ResourceNotFoundError): + await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=c_name + "111", + ) + with pytest.raises(ResourceNotFoundError): + await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=smb_name, + ) + + # ----- EndpointTests.MultiCloudConnectorEndpointCreateGetDeleteTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_multi_cloud_connector_create_get_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-mcccrud" + await self._create_storage_mover(rg, sm_name) + + endpoint_name = "mcc-1" + endpoint = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureMultiCloudConnector", + "multiCloudConnectorId": MULTI_CLOUD_CONNECTOR_ID, + "awsS3BucketId": AWS_S3_BUCKET_ID, + "description": "Test multi-cloud connector endpoint", + }}, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.endpoint_type == "AzureMultiCloudConnector" + + endpoint = await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + assert endpoint.properties.description == "Test multi-cloud connector endpoint" + assert endpoint.properties.multi_cloud_connector_id is not None + assert endpoint.properties.aws_s3_bucket_id is not None + + await self._delete_endpoint(rg, sm_name, endpoint_name) + with pytest.raises(ResourceNotFoundError): + await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + + # ----- EndpointTests.S3WithHmacEndpointCreateGetDeleteTest ----- + # NOTE: .NET marks this [Ignore] ("requires live S3 resources that are not yet + # available for recording"). Running it anyway as the user asked — the request + # uses placeholder URIs/credentials, so the RP may reject them. + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_s3_with_hmac_create_get_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-s3hmac" + await self._create_storage_mover(rg, sm_name) + + endpoint_name = "s3hmac-1" + body = {"properties": { + "endpointType": "S3WithHMAC", + "sourceUri": "https://s3.example.com/bucket", + "sourceType": "MINIO", + "description": "Test S3 with HMAC endpoint", + "credentials": { + "type": "AzureKeyVaultS3WithHMAC", + "accessKeyUri": "https://examples-azureKeyVault.vault.azure.net/secrets/examples-accesskey", + "secretKeyUri": "https://examples-azureKeyVault.vault.azure.net/secrets/examples-secretkey", + }, + }} + + endpoint = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint=body, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.endpoint_type == "S3WithHMAC" + + endpoint = await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.source_uri == "https://s3.example.com/bucket" + assert endpoint.properties.source_type == "MINIO" + assert endpoint.properties.description == "Test S3 with HMAC endpoint" + assert endpoint.properties.credentials is not None + assert endpoint.properties.credentials.access_key_uri == \ + "https://examples-azureKeyVault.vault.azure.net/secrets/examples-accesskey" + assert endpoint.properties.credentials.secret_key_uri == \ + "https://examples-azureKeyVault.vault.azure.net/secrets/examples-secretkey" + + await self._delete_endpoint(rg, sm_name, endpoint_name) + with pytest.raises(ResourceNotFoundError): + await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + + # ----- valid-EndpointKind tests ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_nfs_mount_kind_source(self, resource_group): + rg = resource_group.name + sm_name = "testsm-nfssrc" + await self._create_storage_mover(rg, sm_name) + + endpoint_name = "nfs-src-1" + endpoint = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "NfsMount", + "host": "10.0.0.1", + "export": "/", + "endpointKind": "Source", + "description": "NFS source endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Source" + await self._delete_endpoint(rg, sm_name, endpoint_name) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_smb_mount_kind_source(self, resource_group): + rg = resource_group.name + sm_name = "testsm-smbsrc" + await self._create_storage_mover(rg, sm_name) + + endpoint_name = "smb-src-1" + endpoint = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "SmbMount", + "host": "10.0.0.1", + "shareName": "testshare", + "endpointKind": "Source", + "description": "SMB source endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Source" + await self._delete_endpoint(rg, sm_name, endpoint_name) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_multi_cloud_connector_kind_source(self, resource_group): + rg = resource_group.name + sm_name = "testsm-mccsrc" + await self._create_storage_mover(rg, sm_name) + + endpoint_name = "mcc-src-1" + endpoint = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureMultiCloudConnector", + "multiCloudConnectorId": MULTI_CLOUD_CONNECTOR_ID, + "awsS3BucketId": AWS_S3_BUCKET_ID, + "endpointKind": "Source", + "description": "Multi-cloud connector source endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Source" + await self._delete_endpoint(rg, sm_name, endpoint_name) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_blob_container_kind_source(self, resource_group): + rg = resource_group.name + sm_name = "testsm-blobsrc" + await self._create_storage_mover(rg, sm_name) + + endpoint_name = "blob-src-1" + endpoint = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": _account_id(rg), + "blobContainerName": CONTAINER_NAME, + "endpointKind": "Source", + "description": "Blob container source endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Source" + await self._delete_endpoint(rg, sm_name, endpoint_name) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_blob_container_kind_target(self, resource_group): + rg = resource_group.name + sm_name = "testsm-blobtgt" + await self._create_storage_mover(rg, sm_name) + + endpoint_name = "blob-tgt-1" + endpoint = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": _account_id(rg), + "blobContainerName": CONTAINER_NAME, + "endpointKind": "Target", + "description": "Blob container target endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Target" + await self._delete_endpoint(rg, sm_name, endpoint_name) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_smb_file_share_kind_target(self, resource_group): + rg = resource_group.name + sm_name = "testsm-smbfstgt" + await self._create_storage_mover(rg, sm_name) + + endpoint_name = "smbfs-tgt-1" + endpoint = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureStorageSmbFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testfileshare", + "endpointKind": "Target", + "description": "SMB file share target endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Target" + await self._delete_endpoint(rg, sm_name, endpoint_name) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_nfs_file_share_kind_target(self, resource_group): + rg = resource_group.name + sm_name = "testsm-nfsfstgt" + await self._create_storage_mover(rg, sm_name) + + endpoint_name = "nfsfs-tgt-1" + endpoint = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureStorageNfsFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testnfsfileshare", + "endpointKind": "Target", + "description": "NFS file share target endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Target" + await self._delete_endpoint(rg, sm_name, endpoint_name) + + # ----- invalid-EndpointKind tests ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_nfs_mount_kind_target_fails(self, resource_group): + rg = resource_group.name + sm_name = "testsm-nfstgtfail" + await self._create_storage_mover(rg, sm_name) + + with pytest.raises(HttpResponseError): + await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name="nfs-tgt-1", + endpoint={"properties": { + "endpointType": "NfsMount", + "host": "10.0.0.1", + "export": "/", + "endpointKind": "Target", + }}, + ) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_smb_mount_kind_target_fails(self, resource_group): + rg = resource_group.name + sm_name = "testsm-smbtgtfail" + await self._create_storage_mover(rg, sm_name) + + with pytest.raises(HttpResponseError): + await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name="smb-tgt-1", + endpoint={"properties": { + "endpointType": "SmbMount", + "host": "10.0.0.1", + "shareName": "testshare", + "endpointKind": "Target", + }}, + ) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_multi_cloud_connector_kind_target_fails(self, resource_group): + rg = resource_group.name + sm_name = "testsm-mcctgtfail" + await self._create_storage_mover(rg, sm_name) + + with pytest.raises(HttpResponseError): + await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name="mcc-tgt-1", + endpoint={"properties": { + "endpointType": "AzureMultiCloudConnector", + "multiCloudConnectorId": MULTI_CLOUD_CONNECTOR_ID, + "awsS3BucketId": AWS_S3_BUCKET_ID, + "endpointKind": "Target", + }}, + ) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_smb_file_share_kind_source_fails(self, resource_group): + rg = resource_group.name + sm_name = "testsm-smbfssrcfail" + await self._create_storage_mover(rg, sm_name) + + with pytest.raises(HttpResponseError): + await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name="smbfs-src-1", + endpoint={"properties": { + "endpointType": "AzureStorageSmbFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testfileshare", + "endpointKind": "Source", + }}, + ) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_nfs_file_share_kind_source_fails(self, resource_group): + rg = resource_group.name + sm_name = "testsm-nfsfssrcfail" + await self._create_storage_mover(rg, sm_name) + + with pytest.raises(HttpResponseError): + await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name="nfsfs-src-1", + endpoint={"properties": { + "endpointType": "AzureStorageNfsFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testnfsfileshare", + "endpointKind": "Source", + }}, + ) + + # ----- EndpointTests.NfsFileShareEndpointCreateGetDeleteTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_nfs_file_share_create_get_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-nfsfscrud" + await self._create_storage_mover(rg, sm_name) + + endpoint_name = "nfsfs-1" + endpoint = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureStorageNfsFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testnfsfileshare", + "description": "Test NFS file share endpoint", + }}, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.endpoint_type == "AzureStorageNfsFileShare" + + endpoint = await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + assert endpoint.properties.file_share_name == "testnfsfileshare" + assert endpoint.properties.description == "Test NFS file share endpoint" + + await self._delete_endpoint(rg, sm_name, endpoint_name) + with pytest.raises(ResourceNotFoundError): + await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_endpoints_operations_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_endpoints_operations_test.py new file mode 100644 index 000000000000..3f248b6de395 --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_endpoints_operations_test.py @@ -0,0 +1,587 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +"""Sync scenario tests for endpoints. + +Mirrors .NET EndpointTests at: + Q:\\source\\azure-sdk-for-net\\sdk\\storagemover\\Azure.ResourceManager.StorageMover\\tests\\Scenario\\EndpointTests.cs +""" +import pytest +from azure.core.exceptions import HttpResponseError, ResourceNotFoundError +from azure.mgmt.storagemover import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer, recorded_by_proxy + +AZURE_LOCATION = "eastus" +STORAGE_ACCOUNT_NAME = "testsmstore24" +CONTAINER_NAME = "testsmcontainer" + +FAKE_SUBSCRIPTION_ID = "00000000-0000-0000-0000-000000000000" +MULTI_CLOUD_CONNECTOR_ID = ( + "/subscriptions/b6b34ad8-ca89-4f85-beb7-c2ec13702dac" + "/resourceGroups/E2E-Management-RGsyn" + "/providers/Microsoft.HybridConnectivity/publicCloudConnectors/e2e-sm-rp-connector" +) +AWS_S3_BUCKET_ID = ( + "/subscriptions/b6b34ad8-ca89-4f85-beb7-c2ec13702dac" + "/resourceGroups/aws_640698235822" + "/providers/Microsoft.AWSConnector/s3Buckets/e2e-sm-rp-bucket" +) + + +def _account_id(rg): + """Build a storage-account resource id under this RG (the account does NOT need to exist for endpoint create).""" + return ( + f"/subscriptions/{FAKE_SUBSCRIPTION_ID}/resourceGroups/{rg}" + f"/providers/Microsoft.Storage/storageAccounts/{STORAGE_ACCOUNT_NAME}" + ) + + +class TestStorageMoverMgmtEndpointsOperations(AzureMgmtRecordedTestCase): + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient) + + def _create_storage_mover(self, rg, sm_name): + self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + + # ----- EndpointTests.CreateUpdateGetDeleteTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_create_update_get_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-epcrud" + self._create_storage_mover(rg, sm_name) + + c_name = "conendpoint-1" + nfs_name = "nfsendpoint-1" + smb_name = "smbendpoint-1" + fs_name = "fsendpoint-1" + + # --- Container endpoint --- + c = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=c_name, + endpoint={"properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": _account_id(rg), + "blobContainerName": CONTAINER_NAME, + "description": "New container endpoint", + }}, + ) + assert c.name == c_name + assert c.properties.endpoint_type == "AzureStorageBlobContainer" + + c_get = self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=c_name, + ) + assert c_get.name == c_name + assert c_get.properties.endpoint_type == "AzureStorageBlobContainer" + + # --- NFS mount endpoint --- + nfs = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=nfs_name, + endpoint={"properties": { + "endpointType": "NfsMount", + "host": "10.0.0.1", + "export": "/", + "nfsVersion": "NFSv3", + "description": "New NFS endpoint", + }}, + ) + assert nfs.name == nfs_name + assert nfs.properties.endpoint_type == "NfsMount" + assert nfs.properties.export == "/" + assert nfs.properties.host == "10.0.0.1" + + nfs_get = self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=nfs_name, + ) + assert nfs_get.properties.host == "10.0.0.1" + assert nfs_get.properties.export == "/" + + # --- SMB mount endpoint with credentials --- + smb = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=smb_name, + endpoint={"properties": { + "endpointType": "SmbMount", + "host": "10.0.0.1", + "shareName": "testshare", + "credentials": { + "type": "AzureKeyVaultSmb", + "usernameUri": "https://examples-azureKeyVault.vault.azure.net/secrets/examples-username", + "passwordUri": "https://examples-azureKeyVault.vault.azure.net/secrets/examples-password", + }, + "description": "New Smb mount endpoint", + }}, + ) + assert smb.name == smb_name + assert smb.properties.endpoint_type == "SmbMount" + assert smb.properties.host == "10.0.0.1" + assert smb.properties.share_name == "testshare" + + # --- SMB endpoint update + delete --- + # Workaround for an RP regression in api-version 2025-12-01: the endpoint + # PATCH (update) handler requires a non-null `identity` on the payload root. + # The PUT (create) above succeeds without identity — so this is specifically + # an update-path validation bug, not a real schema requirement. Sending + # `{"type": "None"}` (the standard ARM "no managed identity" sentinel) + # satisfies the check. The .NET test omits identity entirely and would also + # fail today against this api-version. + smb_updated = self.client.endpoints.update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=smb_name, + endpoint={ + "identity": {"type": "None"}, + "properties": { + "endpointType": "SmbMount", + "credentials": { + "type": "AzureKeyVaultSmb", + "usernameUri": "", + "passwordUri": "", + }, + "description": "Update endpoint", + }, + }, + ) + assert smb_updated.name == smb_name + assert smb_updated.properties.endpoint_type == "SmbMount" + assert smb_updated.properties.host == "10.0.0.1" + assert smb_updated.properties.share_name == "testshare" + + self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=smb_name, + ).result() + + # --- Azure Storage SMB FileShare endpoint --- + fs = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=fs_name, + endpoint={"properties": { + "endpointType": "AzureStorageSmbFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testfileshare", + "description": "new file share endpoint", + }}, + ) + assert fs.name == fs_name + assert fs.properties.endpoint_type == "AzureStorageSmbFileShare" + assert fs.properties.file_share_name == "testfileshare" + + fs_get = self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=fs_name, + ) + assert fs_get.properties.file_share_name == "testfileshare" + assert fs_get.properties.description == "new file share endpoint" + + # --- list + missing-endpoint check --- + items = list(self.client.endpoints.list( + resource_group_name=rg, storage_mover_name=sm_name, + )) + assert len(items) > 1 + + with pytest.raises(ResourceNotFoundError): + self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=c_name + "111", + ) + with pytest.raises(ResourceNotFoundError): + self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=smb_name, + ) + + # ----- EndpointTests.MultiCloudConnectorEndpointCreateGetDeleteTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_multi_cloud_connector_create_get_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-mcccrud" + self._create_storage_mover(rg, sm_name) + + endpoint_name = "mcc-1" + body = {"properties": { + "endpointType": "AzureMultiCloudConnector", + "multiCloudConnectorId": MULTI_CLOUD_CONNECTOR_ID, + "awsS3BucketId": AWS_S3_BUCKET_ID, + "description": "Test multi-cloud connector endpoint", + }} + + endpoint = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint=body, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.endpoint_type == "AzureMultiCloudConnector" + + endpoint = self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.description == "Test multi-cloud connector endpoint" + assert endpoint.properties.multi_cloud_connector_id is not None + assert endpoint.properties.aws_s3_bucket_id is not None + + self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ).result() + with pytest.raises(ResourceNotFoundError): + self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + + # ----- EndpointTests.S3WithHmacEndpointCreateGetDeleteTest ----- + # NOTE: .NET marks this [Ignore] ("requires live S3 resources that are not yet + # available for recording"). Running it anyway as the user asked — the request + # uses placeholder URIs/credentials, so the RP may reject them. + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_s3_with_hmac_create_get_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-s3hmac" + self._create_storage_mover(rg, sm_name) + + endpoint_name = "s3hmac-1" + body = {"properties": { + "endpointType": "S3WithHMAC", + "sourceUri": "https://s3.example.com/bucket", + "sourceType": "MINIO", + "description": "Test S3 with HMAC endpoint", + "credentials": { + "type": "AzureKeyVaultS3WithHMAC", + "accessKeyUri": "https://examples-azureKeyVault.vault.azure.net/secrets/examples-accesskey", + "secretKeyUri": "https://examples-azureKeyVault.vault.azure.net/secrets/examples-secretkey", + }, + }} + + endpoint = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint=body, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.endpoint_type == "S3WithHMAC" + + endpoint = self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.source_uri == "https://s3.example.com/bucket" + assert endpoint.properties.source_type == "MINIO" + assert endpoint.properties.description == "Test S3 with HMAC endpoint" + assert endpoint.properties.credentials is not None + assert endpoint.properties.credentials.access_key_uri == \ + "https://examples-azureKeyVault.vault.azure.net/secrets/examples-accesskey" + assert endpoint.properties.credentials.secret_key_uri == \ + "https://examples-azureKeyVault.vault.azure.net/secrets/examples-secretkey" + + self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ).result() + with pytest.raises(ResourceNotFoundError): + self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + + # ----- EndpointTests valid-EndpointKind tests ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_nfs_mount_kind_source(self, resource_group): + rg = resource_group.name + sm_name = "testsm-nfssrc" + self._create_storage_mover(rg, sm_name) + + endpoint_name = "nfs-src-1" + endpoint = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "NfsMount", + "host": "10.0.0.1", + "export": "/", + "endpointKind": "Source", + "description": "NFS source endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Source" + + self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ).result() + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_smb_mount_kind_source(self, resource_group): + rg = resource_group.name + sm_name = "testsm-smbsrc" + self._create_storage_mover(rg, sm_name) + + endpoint_name = "smb-src-1" + endpoint = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "SmbMount", + "host": "10.0.0.1", + "shareName": "testshare", + "endpointKind": "Source", + "description": "SMB source endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Source" + + self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ).result() + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_multi_cloud_connector_kind_source(self, resource_group): + rg = resource_group.name + sm_name = "testsm-mccsrc" + self._create_storage_mover(rg, sm_name) + + endpoint_name = "mcc-src-1" + endpoint = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureMultiCloudConnector", + "multiCloudConnectorId": MULTI_CLOUD_CONNECTOR_ID, + "awsS3BucketId": AWS_S3_BUCKET_ID, + "endpointKind": "Source", + "description": "Multi-cloud connector source endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Source" + + self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ).result() + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_blob_container_kind_source(self, resource_group): + rg = resource_group.name + sm_name = "testsm-blobsrc" + self._create_storage_mover(rg, sm_name) + + endpoint_name = "blob-src-1" + endpoint = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": _account_id(rg), + "blobContainerName": CONTAINER_NAME, + "endpointKind": "Source", + "description": "Blob container source endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Source" + + self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ).result() + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_blob_container_kind_target(self, resource_group): + rg = resource_group.name + sm_name = "testsm-blobtgt" + self._create_storage_mover(rg, sm_name) + + endpoint_name = "blob-tgt-1" + endpoint = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": _account_id(rg), + "blobContainerName": CONTAINER_NAME, + "endpointKind": "Target", + "description": "Blob container target endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Target" + + self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ).result() + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_smb_file_share_kind_target(self, resource_group): + rg = resource_group.name + sm_name = "testsm-smbfstgt" + self._create_storage_mover(rg, sm_name) + + endpoint_name = "smbfs-tgt-1" + endpoint = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureStorageSmbFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testfileshare", + "endpointKind": "Target", + "description": "SMB file share target endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Target" + + self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ).result() + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_nfs_file_share_kind_target(self, resource_group): + rg = resource_group.name + sm_name = "testsm-nfsfstgt" + self._create_storage_mover(rg, sm_name) + + endpoint_name = "nfsfs-tgt-1" + endpoint = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureStorageNfsFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testnfsfileshare", + "endpointKind": "Target", + "description": "NFS file share target endpoint", + }}, + ) + assert endpoint.properties.endpoint_kind == "Target" + + self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ).result() + + # ----- EndpointTests invalid-EndpointKind tests ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_nfs_mount_kind_target_fails(self, resource_group): + rg = resource_group.name + sm_name = "testsm-nfstgtfail" + self._create_storage_mover(rg, sm_name) + + with pytest.raises(HttpResponseError): + self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name="nfs-tgt-1", + endpoint={"properties": { + "endpointType": "NfsMount", + "host": "10.0.0.1", + "export": "/", + "endpointKind": "Target", + }}, + ) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_smb_mount_kind_target_fails(self, resource_group): + rg = resource_group.name + sm_name = "testsm-smbtgtfail" + self._create_storage_mover(rg, sm_name) + + with pytest.raises(HttpResponseError): + self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name="smb-tgt-1", + endpoint={"properties": { + "endpointType": "SmbMount", + "host": "10.0.0.1", + "shareName": "testshare", + "endpointKind": "Target", + }}, + ) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_multi_cloud_connector_kind_target_fails(self, resource_group): + rg = resource_group.name + sm_name = "testsm-mcctgtfail" + self._create_storage_mover(rg, sm_name) + + with pytest.raises(HttpResponseError): + self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name="mcc-tgt-1", + endpoint={"properties": { + "endpointType": "AzureMultiCloudConnector", + "multiCloudConnectorId": MULTI_CLOUD_CONNECTOR_ID, + "awsS3BucketId": AWS_S3_BUCKET_ID, + "endpointKind": "Target", + }}, + ) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_smb_file_share_kind_source_fails(self, resource_group): + rg = resource_group.name + sm_name = "testsm-smbfssrcfail" + self._create_storage_mover(rg, sm_name) + + with pytest.raises(HttpResponseError): + self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name="smbfs-src-1", + endpoint={"properties": { + "endpointType": "AzureStorageSmbFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testfileshare", + "endpointKind": "Source", + }}, + ) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_nfs_file_share_kind_source_fails(self, resource_group): + rg = resource_group.name + sm_name = "testsm-nfsfssrcfail" + self._create_storage_mover(rg, sm_name) + + with pytest.raises(HttpResponseError): + self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name="nfsfs-src-1", + endpoint={"properties": { + "endpointType": "AzureStorageNfsFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testnfsfileshare", + "endpointKind": "Source", + }}, + ) + + # ----- EndpointTests.NfsFileShareEndpointCreateGetDeleteTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_nfs_file_share_create_get_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-nfsfscrud" + self._create_storage_mover(rg, sm_name) + + endpoint_name = "nfsfs-1" + body = {"properties": { + "endpointType": "AzureStorageNfsFileShare", + "storageAccountResourceId": _account_id(rg), + "fileShareName": "testnfsfileshare", + "description": "Test NFS file share endpoint", + }} + + endpoint = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint=body, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.endpoint_type == "AzureStorageNfsFileShare" + + endpoint = self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.file_share_name == "testnfsfileshare" + assert endpoint.properties.description == "Test NFS file share endpoint" + assert endpoint.properties.storage_account_resource_id is not None + + self.client.endpoints.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ).result() + with pytest.raises(ResourceNotFoundError): + self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_definitions_operations_async_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_definitions_operations_async_test.py new file mode 100644 index 000000000000..58fbf628a5af --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_definitions_operations_async_test.py @@ -0,0 +1,712 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +"""Async scenario tests for job_definitions. + +Mirrors .NET JobDefinitionJobRunTests + JobDefinitionScheduleTests at: + Q:\\source\\azure-sdk-for-net\\sdk\\storagemover\\Azure.ResourceManager.StorageMover\\tests\\Scenario + +Also implements cross-language matrix row #31 +(`JobDefinitionJobRunTests.StartC2CJobWithPrivateSourceTest`) — see the sync +sibling file for the full rationale. + +NOTE: The cross-sub mgmt clients (azure-mgmt-network / -authorization / -storage) +are imported from their `.aio` namespaces so the test-proxy's async-transport +interception covers them. Older versions of these packages (in particular the +floors pinned by the `mindependency` CI leg) lack the `.aio` submodule — we +use `pytest.importorskip` so collection skips this whole module cleanly in +that environment. +""" +import asyncio +import uuid +from datetime import datetime, timedelta, timezone + +import pytest + +# Skip the entire module if the cross-sub `.aio` submodules aren't present +# (happens in `mindependency` CI legs that pin very old transitive versions). +pytest.importorskip("azure.mgmt.network.aio") +pytest.importorskip("azure.mgmt.storage.aio") +pytest.importorskip("azure.mgmt.authorization.v2022_04_01.aio") + +from azure.core.exceptions import HttpResponseError +from azure.mgmt.authorization.v2022_04_01.aio import AuthorizationManagementClient +from azure.mgmt.network.aio import NetworkManagementClient +from azure.mgmt.storage.aio import StorageManagementClient +from azure.mgmt.storagemover.aio import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer +from devtools_testutils.aio import recorded_by_proxy_async + +AZURE_LOCATION = "eastus" +# Matrix row #31 runs in westcentralus (shared PLS + storage account live there). +WCUS_LOCATION = "westcentralus" + +FAKE_STORAGE_ACCOUNT_ID = ( + "/subscriptions/00000000-0000-0000-0000-000000000000" + "/resourceGroups/fakeRg/providers/Microsoft.Storage/storageAccounts/fakeAccount" +) + +# Shared team infra in XDataMove-Synthetics — do not recreate. +# Full inventory in the cross-language playbook +# (storage-mover-scenario-tests-cross-language, "Porter's reference" callout). +SYNTHETICS_SUBSCRIPTION_ID = "b6b34ad8-ca89-4f85-beb7-c2ec13702dac" + +PLS_RESOURCE_GROUP = "E2E-Management-RGsyn" +PLS_NAME = "test-pls-wcs" +REAL_PRIVATE_LINK_SERVICE_ID = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/resourceGroups/" + PLS_RESOURCE_GROUP + + "/providers/Microsoft.Network/privateLinkServices/" + PLS_NAME +) + +STORAGE_ACCOUNT_RG = "CP_Mover_IN_WCUS" +STORAGE_ACCOUNT_NAME = "cpmoveraccount" +STORAGE_ACCOUNT_ID = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/resourceGroups/" + STORAGE_ACCOUNT_RG + + "/providers/Microsoft.Storage/storageAccounts/" + STORAGE_ACCOUNT_NAME +) + +MULTI_CLOUD_CONNECTOR_ID = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/resourceGroups/E2E-Management-RGsyn" + + "/providers/Microsoft.HybridConnectivity/publicCloudConnectors/e2e-sm-rp-connector" +) +PRIVATE_S3_BUCKET_ID = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/resourceGroups/aws_640698235822" + + "/providers/Microsoft.AWSConnector/s3Buckets/e2e-sm-rp-private-bucket" +) +AWS_PUBLIC_S3_BUCKET_ID = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/resourceGroups/aws_640698235822" + + "/providers/Microsoft.AWSConnector/s3Buckets/e2e-sm-rp-bucket" +) + +STORAGE_BLOB_DATA_CONTRIBUTOR_ROLE_DEF_GUID = "ba92f5b4-2d11-453d-a403-e96b0029c9fe" + + +class TestStorageMoverMgmtJobDefinitionsOperationsAsync(AzureMgmtRecordedTestCase): + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient, is_async=True) + + async def _provision_parents(self, rg, sm_name, project_name, source_endpoint, target_endpoint): + await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + await self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=source_endpoint, + endpoint={"properties": { + "endpointType": "NfsMount", + "host": "10.0.0.1", + "export": "/", + "nfsVersion": "NFSv3", + }}, + ) + await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=target_endpoint, + endpoint={"properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": FAKE_STORAGE_ACCOUNT_ID, + "blobContainerName": "testcontainer", + }}, + ) + + # ----- JobDefinitionJobRunTests.JobDefinitionJobRunTest (matrix row #10) ----- + # See the sync sibling for full rationale + design notes. + + @RandomNameResourceGroupPreparer(location=WCUS_LOCATION) + @recorded_by_proxy_async + async def test_job_definition_job_run(self, resource_group, **kwargs): + rg = resource_group.name + sm_name = "testsm-jdjr" + project_name = "testproj-jdjr" + source_endpoint = "testsrcep-mcc-pub" + target_endpoint = "testtgtep-blob-pub" + jd_name = "jobdef-jdjr-pub" + + variables = kwargs.pop("variables", {}) + container_name = variables.setdefault("container_name", "tc" + uuid.uuid4().hex[:10].lower()) + role_assignment_name = variables.setdefault("role_assignment_id", str(uuid.uuid4())) + + # Async cross-sub clients (test-proxy's async-transport interception + # covers them; sync clients here would bypass the proxy and hit live). + # Pin api_version explicitly so playback stays stable as the underlying + # mgmt packages bump default api-versions in future releases. + storage_client = self.create_client_from_credential( + StorageManagementClient, + self.get_credential(StorageManagementClient, is_async=True), + subscription_id=SYNTHETICS_SUBSCRIPTION_ID, + api_version="2025-06-01", + ) + authorization_client = self.create_client_from_credential( + AuthorizationManagementClient, + self.get_credential(AuthorizationManagementClient, is_async=True), + subscription_id=SYNTHETICS_SUBSCRIPTION_ID, + ) + + container_created = False + rbac_created = False + container_scope = ( + STORAGE_ACCOUNT_ID + "/blobServices/default/containers/" + container_name + ) + + try: + await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": WCUS_LOCATION}, + ) + await self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + + await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=source_endpoint, + endpoint={"properties": { + "endpointType": "AzureMultiCloudConnector", + "multiCloudConnectorId": MULTI_CLOUD_CONNECTOR_ID, + "awsS3BucketId": AWS_PUBLIC_S3_BUCKET_ID, + "endpointKind": "Source", + "description": "publicMccSourceForJobRun", + }}, + ) + + target = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=target_endpoint, + endpoint={ + "identity": {"type": "SystemAssigned"}, + "properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": STORAGE_ACCOUNT_ID, + "blobContainerName": container_name, + "description": "blobTargetForJobRun", + }, + }, + ) + assert target.identity is not None and target.identity.principal_id, ( + "Target blob endpoint did not get an auto-assigned MSI principalId" + ) + target_msi_principal_id = target.identity.principal_id + + await storage_client.blob_containers.create( + resource_group_name=STORAGE_ACCOUNT_RG, account_name=STORAGE_ACCOUNT_NAME, + container_name=container_name, blob_container={}, + ) + container_created = True + + role_definition_id = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/providers/Microsoft.Authorization/roleDefinitions/" + + STORAGE_BLOB_DATA_CONTRIBUTOR_ROLE_DEF_GUID + ) + await authorization_client.role_assignments.create( + scope=container_scope, role_assignment_name=role_assignment_name, + parameters={"properties": { + "roleDefinitionId": role_definition_id, + "principalId": target_msi_principal_id, + "principalType": "ServicePrincipal", + }}, + ) + rbac_created = True + + jd = await self.client.job_definitions.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + job_definition={"properties": { + "copyMode": "Additive", + "sourceName": source_endpoint, + "targetName": target_endpoint, + "jobType": "CloudToCloud", + "sourceSubpath": "/", + "targetSubpath": "/", + "description": "JobDefForJobRunTest", + }}, + ) + assert jd.name == jd_name + assert jd.properties.source_name == source_endpoint + assert jd.properties.target_name == target_endpoint + assert jd.properties.copy_mode == "Additive" + + jd_get = await self.client.job_definitions.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ) + assert jd_get.id == jd.id + + items = [j async for j in self.client.job_definitions.list( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + )] + assert len(items) >= 1 + assert jd_name in [j.name for j in items] + + start_result = await self.client.job_definitions.start_job( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ) + assert start_result.job_run_resource_id, "start_job did not return jobRunResourceId" + job_run_name = start_result.job_run_resource_id.rstrip("/").split("/")[-1] + + terminal_states = {"Succeeded", "Failed", "Cancelled", "PartialSucceeded"} + final_status = None + for _ in range(60): + run = await self.client.job_runs.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, job_run_name=job_run_name, + ) + current_status = ( + getattr(run.properties, "status", None) if run.properties is not None else None + ) + if current_status in terminal_states: + final_status = current_status + break + if self.is_live: + await asyncio.sleep(30) + assert final_status is not None, ( + "Job run did not reach a terminal state within 30 min" + ) + assert final_status == "Succeeded", ( + "Expected job-run to Succeed with public bucket + target MSI RBAC, " + "got: " + str(final_status) + ) + + jd_poller = await self.client.job_definitions.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ) + await jd_poller.result() + + finally: + if rbac_created: + try: + await authorization_client.role_assignments.delete( + scope=container_scope, role_assignment_name=role_assignment_name, + ) + except Exception: # noqa: BLE001 + pass + if container_created: + try: + await storage_client.blob_containers.delete( + resource_group_name=STORAGE_ACCOUNT_RG, account_name=STORAGE_ACCOUNT_NAME, + container_name=container_name, + ) + except Exception: # noqa: BLE001 + pass + for cli in (storage_client, authorization_client): + try: + await cli.close() + except Exception: # noqa: BLE001 + pass + + return variables + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_create_with_weekly_schedule(self, resource_group, **kwargs): + rg = resource_group.name + sm_name = "testsm-jdwk" + project_name = "testproj-jdwk" + source_endpoint = "testnfsendpoint" + target_endpoint = "testblobendpoint" + await self._provision_parents(rg, sm_name, project_name, source_endpoint, target_endpoint) + + jd_name = "jobdef-sched-wk" + variables = kwargs.pop("variables", {}) + now = datetime.now(timezone.utc) + start_date = variables.setdefault( + "schedule_start", (now + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ") + ) + end_date = variables.setdefault( + "schedule_end", (now + timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%SZ") + ) + body = {"properties": { + "copyMode": "Additive", + "sourceName": source_endpoint, + "targetName": target_endpoint, + "description": "Job definition with weekly schedule", + "dataIntegrityValidation": "SaveVerifyFileMD5", + "schedule": { + "frequency": "Weekly", + "isActive": True, + "executionTime": {"hour": 2}, + "startDate": start_date, + "endDate": end_date, + "daysOfWeek": ["Monday", "Wednesday", "Friday"], + }, + }} + + jd = await self.client.job_definitions.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, job_definition=body, + ) + assert jd.name == jd_name + assert jd.properties.description == "Job definition with weekly schedule" + assert jd.properties.schedule.frequency == "Weekly" + assert jd.properties.schedule.is_active is True + assert jd.properties.schedule.execution_time.hour == 2 + assert len(jd.properties.schedule.days_of_week) == 3 + + jd = await self.client.job_definitions.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ) + assert jd.properties.schedule.frequency == "Weekly" + + poller = await self.client.job_definitions.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ) + await poller.result() + + return variables + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_create_with_daily_schedule_and_preserve_permissions(self, resource_group, **kwargs): + rg = resource_group.name + sm_name = "testsm-jddl" + project_name = "testproj-jddl" + source_endpoint = "testnfsendpoint" + target_endpoint = "testblobendpoint" + await self._provision_parents(rg, sm_name, project_name, source_endpoint, target_endpoint) + + jd_name = "jobdef-sched-daily" + variables = kwargs.pop("variables", {}) + now = datetime.now(timezone.utc) + start_date = variables.setdefault( + "schedule_start", (now + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ") + ) + end_date = variables.setdefault( + "schedule_end", (now + timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%SZ") + ) + body = {"properties": { + "copyMode": "Mirror", + "sourceName": source_endpoint, + "targetName": target_endpoint, + "description": "Job definition with daily schedule", + "dataIntegrityValidation": "None", + "preservePermissions": True, + "schedule": { + "frequency": "Daily", + "isActive": True, + "executionTime": {"hour": 0}, + "startDate": start_date, + "endDate": end_date, + }, + }} + + jd = await self.client.job_definitions.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, job_definition=body, + ) + assert jd.name == jd_name + assert jd.properties.copy_mode == "Mirror" + assert jd.properties.schedule.frequency == "Daily" + assert jd.properties.schedule.is_active is True + + poller = await self.client.job_definitions.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ) + await poller.result() + + return variables + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_create_with_onetime_schedule(self, resource_group, **kwargs): + rg = resource_group.name + sm_name = "testsm-jdot" + project_name = "testproj-jdot" + source_endpoint = "testnfsendpoint" + target_endpoint = "testblobendpoint" + await self._provision_parents(rg, sm_name, project_name, source_endpoint, target_endpoint) + + jd_name = "jobdef-sched-once" + variables = kwargs.pop("variables", {}) + now = datetime.now(timezone.utc) + start_date = variables.setdefault( + "schedule_start", (now + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ") + ) + body = {"properties": { + "copyMode": "Additive", + "sourceName": source_endpoint, + "targetName": target_endpoint, + "description": "Job definition with one-time schedule", + "schedule": { + "frequency": "Onetime", + "isActive": True, + "executionTime": {"hour": 10}, + "startDate": start_date, + }, + }} + + jd = await self.client.job_definitions.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, job_definition=body, + ) + assert jd.name == jd_name + assert jd.properties.schedule.frequency == "Onetime" + assert jd.properties.schedule.is_active is True + + poller = await self.client.job_definitions.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ) + await poller.result() + + return variables + + # ----- JobDefinitionJobRunTests.StartC2CJobWithPrivateSourceTest (matrix row #31) ----- + # See the sync sibling file for the full step-by-step description; this is the + # async mirror. + + @RandomNameResourceGroupPreparer(location=WCUS_LOCATION) + @recorded_by_proxy_async + async def test_start_c2c_job_with_private_source(self, resource_group, **kwargs): + rg = resource_group.name + sm_name = "testsm-c2cpvt" + project_name = "testproj-c2cpvt" + connection_name = "testconn-pvt" + source_endpoint = "testsrcep-mcc-pvt" + target_endpoint = "testtgtep-blob-pvt" + job_definition_name = "testjobdef-c2cpvt" + + variables = kwargs.pop("variables", {}) + container_name = variables.setdefault("container_name", "tc" + uuid.uuid4().hex[:10].lower()) + role_assignment_name = variables.setdefault("role_assignment_id", str(uuid.uuid4())) + + # Async cross-sub clients (test-proxy's async-transport interception + # covers them; sync clients here would bypass the proxy and hit live). + # Pin api_version explicitly — see #10 above. + network_client = self.create_client_from_credential( + NetworkManagementClient, + self.get_credential(NetworkManagementClient, is_async=True), + subscription_id=SYNTHETICS_SUBSCRIPTION_ID, + api_version="2025-05-01", + ) + storage_client = self.create_client_from_credential( + StorageManagementClient, + self.get_credential(StorageManagementClient, is_async=True), + subscription_id=SYNTHETICS_SUBSCRIPTION_ID, + api_version="2025-06-01", + ) + authorization_client = self.create_client_from_credential( + AuthorizationManagementClient, + self.get_credential(AuthorizationManagementClient, is_async=True), + subscription_id=SYNTHETICS_SUBSCRIPTION_ID, + ) + + connection_created = False + container_created = False + rbac_created = False + container_scope = ( + STORAGE_ACCOUNT_ID + "/blobServices/default/containers/" + container_name + ) + + try: + await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": WCUS_LOCATION}, + ) + await self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + + connection = await self.client.connections.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + connection={"properties": { + "privateLinkServiceId": REAL_PRIVATE_LINK_SERVICE_ID, + "description": "ConnectionForPrivateBucketJobRun", + }}, + ) + connection_created = True + pe_resource_id = connection.properties.private_endpoint_resource_id + assert pe_resource_id, "Connection create did not return privateEndpointResourceId" + + pe_connection_name = None + for _ in range(10): + async for pec in network_client.private_link_services.list_private_endpoint_connections( + resource_group_name=PLS_RESOURCE_GROUP, service_name=PLS_NAME, + ): + pec_pe_id = (pec.private_endpoint.id if pec.private_endpoint else "") or "" + if pec_pe_id.lower() == pe_resource_id.lower(): + pe_connection_name = pec.name + break + if pe_connection_name: + break + if self.is_live: + await asyncio.sleep(15) + assert pe_connection_name, ( + "PE-connection for {} did not appear on PLS {} within 150s".format( + pe_resource_id, PLS_NAME, + ) + ) + + await network_client.private_link_services.update_private_endpoint_connection( + resource_group_name=PLS_RESOURCE_GROUP, service_name=PLS_NAME, + pe_connection_name=pe_connection_name, + parameters={"properties": { + "privateLinkServiceConnectionState": { + "status": "Approved", + "description": "approved by storage-mover SDK live test", + "actionsRequired": "None", + }, + }}, + ) + + approved = False + for _ in range(10): + conn_show = await self.client.connections.get( + resource_group_name=rg, storage_mover_name=sm_name, + connection_name=connection_name, + ) + if (conn_show.properties is not None + and getattr(conn_show.properties, "connection_status", None) == "Approved"): + approved = True + break + if self.is_live: + await asyncio.sleep(30) + assert approved, "Storage Mover Connection did not reach Approved within 300s" + + target = await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=target_endpoint, + endpoint={ + "identity": {"type": "SystemAssigned"}, + "properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": STORAGE_ACCOUNT_ID, + "blobContainerName": container_name, + "description": "blobTargetForJobRunWait", + }, + }, + ) + assert target.identity is not None and target.identity.principal_id, ( + "Target blob endpoint did not get an auto-assigned MSI principalId" + ) + target_msi_principal_id = target.identity.principal_id + + await storage_client.blob_containers.create( + resource_group_name=STORAGE_ACCOUNT_RG, account_name=STORAGE_ACCOUNT_NAME, + container_name=container_name, blob_container={}, + ) + container_created = True + + role_definition_id = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/providers/Microsoft.Authorization/roleDefinitions/" + + STORAGE_BLOB_DATA_CONTRIBUTOR_ROLE_DEF_GUID + ) + await authorization_client.role_assignments.create( + scope=container_scope, role_assignment_name=role_assignment_name, + parameters={"properties": { + "roleDefinitionId": role_definition_id, + "principalId": target_msi_principal_id, + "principalType": "ServicePrincipal", + }}, + ) + rbac_created = True + + await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=source_endpoint, + endpoint={"properties": { + "endpointType": "AzureMultiCloudConnector", + "multiCloudConnectorId": MULTI_CLOUD_CONNECTOR_ID, + "awsS3BucketId": PRIVATE_S3_BUCKET_ID, + "endpointKind": "Source", + "description": "privateMccSourceForJobRunWait", + }}, + ) + + await self.client.job_definitions.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=job_definition_name, + job_definition={"properties": { + "copyMode": "Additive", + "sourceName": source_endpoint, + "targetName": target_endpoint, + "jobType": "CloudToCloud", + "sourceSubpath": "/", + "targetSubpath": "/", + "connections": [connection.id], + "description": "JobDefForJobRunWaitTest", + }}, + ) + + start_result = await self.client.job_definitions.start_job( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=job_definition_name, + ) + assert start_result.job_run_resource_id, "start_job did not return jobRunResourceId" + job_run_name = start_result.job_run_resource_id.rstrip("/").split("/")[-1] + + terminal_states = {"Succeeded", "Failed", "Cancelled", "PartialSucceeded"} + final_status = None + for _ in range(60): + run = await self.client.job_runs.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=job_definition_name, job_run_name=job_run_name, + ) + current_status = ( + getattr(run.properties, "status", None) if run.properties is not None else None + ) + if current_status in terminal_states: + final_status = current_status + break + if self.is_live: + await asyncio.sleep(30) + assert final_status is not None, ( + "Job run did not reach a terminal state within 30 min" + ) + assert final_status == "Succeeded", ( + "Expected job-run to Succeed with approved connection + target MSI RBAC, " + "got: " + str(final_status) + ) + + jd_poller = await self.client.job_definitions.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=job_definition_name, + ) + await jd_poller.result() + + finally: + if rbac_created: + try: + await authorization_client.role_assignments.delete( + scope=container_scope, role_assignment_name=role_assignment_name, + ) + except Exception: # noqa: BLE001 + pass + if container_created: + try: + await storage_client.blob_containers.delete( + resource_group_name=STORAGE_ACCOUNT_RG, account_name=STORAGE_ACCOUNT_NAME, + container_name=container_name, + ) + except Exception: # noqa: BLE001 + pass + if connection_created: + try: + conn_del_poller = await self.client.connections.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, + connection_name=connection_name, + ) + await conn_del_poller.result() + except Exception: # noqa: BLE001 + pass + for cli in (network_client, storage_client, authorization_client): + try: + await cli.close() + except Exception: # noqa: BLE001 + pass + + return variables diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_definitions_operations_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_definitions_operations_test.py new file mode 100644 index 000000000000..3933ffed2382 --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_definitions_operations_test.py @@ -0,0 +1,778 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +"""Sync scenario tests for job_definitions. + +Mirrors .NET JobDefinitionJobRunTests + JobDefinitionScheduleTests at: + Q:\\source\\azure-sdk-for-net\\sdk\\storagemover\\Azure.ResourceManager.StorageMover\\tests\\Scenario + +Also implements cross-language matrix row #31 +(`JobDefinitionJobRunTests.StartC2CJobWithPrivateSourceTest`) — the full +private-bucket CloudToCloud E2E using the shared `test-pls-wcs` PLS, mirroring +RP `Storage-XDataMove-RP/test/E2ETest/C2CTest/StartJobTest.cs::StartC2CJobWithPrivateSourceAsyncSuccessPathTest`. +""" +import uuid +from datetime import datetime, timedelta, timezone + +import pytest +from azure.core.exceptions import HttpResponseError +from azure.mgmt.storagemover import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer, recorded_by_proxy + +# Detect whether the cross-sub mgmt clients are "modern enough" to flow through +# azure-core's RequestsTransport (which the test-proxy intercepts). The pre-20.0 +# generations of azure-mgmt-storage / azure-mgmt-network / azure-mgmt-authorization +# use msrestazure's transport, bypass the proxy, and hit live ARM. The presence +# of the `.aio` submodule + the `v2022_04_01` authorization API version is a +# reliable signal of "post-modernization". +# The `mindependency` CI leg pins those packages to very old floors that lack +# these — skip the two cross-sub tests cleanly in that environment. +try: + from azure.mgmt.authorization.v2022_04_01 import AuthorizationManagementClient + from azure.mgmt.network import NetworkManagementClient + from azure.mgmt.storage import StorageManagementClient + import azure.mgmt.network.aio # noqa: F401 (modernization signal) + import azure.mgmt.storage.aio # noqa: F401 (modernization signal) + _CROSS_SUB_CLIENTS_MODERN = True +except ImportError: + AuthorizationManagementClient = None # type: ignore[assignment] + NetworkManagementClient = None # type: ignore[assignment] + StorageManagementClient = None # type: ignore[assignment] + _CROSS_SUB_CLIENTS_MODERN = False + +_SKIP_IF_CROSS_SUB_CLIENTS_OLD = pytest.mark.skipif( + not _CROSS_SUB_CLIENTS_MODERN, + reason=( + "Cross-sub mgmt clients too old: pre-modernization versions of " + "azure-mgmt-{storage,network,authorization} use msrestazure transport " + "and bypass the test-proxy, hitting live ARM. Bump those packages to " + "their post-azure-core releases (storage>=20, network>=19, authorization>=2) " + "to enable this test." + ), +) + +AZURE_LOCATION = "eastus" +# Matrix row #31 runs in westcentralus because the shared PLS + storage account +# live there. Other tests in this file keep the default eastus location. +WCUS_LOCATION = "westcentralus" + +FAKE_STORAGE_ACCOUNT_ID = ( + "/subscriptions/00000000-0000-0000-0000-000000000000" + "/resourceGroups/fakeRg/providers/Microsoft.Storage/storageAccounts/fakeAccount" +) + +# Shared team infra in XDataMove-Synthetics — do not recreate. +# Full inventory in the cross-language playbook +# (storage-mover-scenario-tests-cross-language, "Porter's reference" callout). +SYNTHETICS_SUBSCRIPTION_ID = "b6b34ad8-ca89-4f85-beb7-c2ec13702dac" + +PLS_RESOURCE_GROUP = "E2E-Management-RGsyn" +PLS_NAME = "test-pls-wcs" +REAL_PRIVATE_LINK_SERVICE_ID = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/resourceGroups/" + PLS_RESOURCE_GROUP + + "/providers/Microsoft.Network/privateLinkServices/" + PLS_NAME +) + +STORAGE_ACCOUNT_RG = "CP_Mover_IN_WCUS" +STORAGE_ACCOUNT_NAME = "cpmoveraccount" +STORAGE_ACCOUNT_ID = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/resourceGroups/" + STORAGE_ACCOUNT_RG + + "/providers/Microsoft.Storage/storageAccounts/" + STORAGE_ACCOUNT_NAME +) + +MULTI_CLOUD_CONNECTOR_ID = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/resourceGroups/E2E-Management-RGsyn" + + "/providers/Microsoft.HybridConnectivity/publicCloudConnectors/e2e-sm-rp-connector" +) +PRIVATE_S3_BUCKET_ID = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/resourceGroups/aws_640698235822" + + "/providers/Microsoft.AWSConnector/s3Buckets/e2e-sm-rp-private-bucket" +) +AWS_PUBLIC_S3_BUCKET_ID = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/resourceGroups/aws_640698235822" + + "/providers/Microsoft.AWSConnector/s3Buckets/e2e-sm-rp-bucket" +) + +# Built-in role definition for "Storage Blob Data Contributor". +STORAGE_BLOB_DATA_CONTRIBUTOR_ROLE_DEF_GUID = "ba92f5b4-2d11-453d-a403-e96b0029c9fe" + + +class TestStorageMoverMgmtJobDefinitionsOperations(AzureMgmtRecordedTestCase): + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient) + + def _provision_parents(self, rg, sm_name, project_name, source_endpoint, target_endpoint): + self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=source_endpoint, + endpoint={"properties": { + "endpointType": "NfsMount", + "host": "10.0.0.1", + "export": "/", + "nfsVersion": "NFSv3", + }}, + ) + self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=target_endpoint, + endpoint={"properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": FAKE_STORAGE_ACCOUNT_ID, + "blobContainerName": "testcontainer", + }}, + ) + + # ----- JobDefinitionJobRunTests.JobDefinitionJobRunTest (matrix row #10) ----- + # Original .NET version: create + get + list + StartJob + StopJob (no wait). + # Python extension (2026-05-20, mirrors the CLI port's + # `test_storage_mover_job_run_scenarios` enhancement): the full C2C + # data-plane round-trip is exercised — source = MCC over the **public** + # AWS S3 bucket `e2e-sm-rp-bucket`, target = blob container under shared + # `cpmoveraccount` with the target endpoint's SystemAssigned MSI granted + # Storage Blob Data Contributor, job-run polled until Succeeded. + # + # StopJob is NOT exercised after the Succeeded poll: the RP returns HTTP + # 412 JobTerminated on already-terminal runs. The .NET equivalent calls + # StartJob/StopJob without waiting; the SDK port favours Succeeded-validation + # over StopJob API-surface coverage (which row #31 also skips). + # + # Unlike row #31 (private-bucket E2E), this row needs no Storage Mover + # Connection / PrivateLinkService approval — the public bucket is reachable + # via the MCC directly. + + @_SKIP_IF_CROSS_SUB_CLIENTS_OLD + @RandomNameResourceGroupPreparer(location=WCUS_LOCATION) + @recorded_by_proxy + def test_job_definition_job_run(self, resource_group, **kwargs): + rg = resource_group.name + sm_name = "testsm-jdjr" + project_name = "testproj-jdjr" + source_endpoint = "testsrcep-mcc-pub" + target_endpoint = "testtgtep-blob-pub" + jd_name = "jobdef-jdjr-pub" + + variables = kwargs.pop("variables", {}) + container_name = variables.setdefault("container_name", "tc" + uuid.uuid4().hex[:10].lower()) + role_assignment_name = variables.setdefault("role_assignment_id", str(uuid.uuid4())) + + storage_client = self.create_client_from_credential( + StorageManagementClient, self.get_credential(StorageManagementClient), + subscription_id=SYNTHETICS_SUBSCRIPTION_ID, + api_version="2025-06-01", + ) + authorization_client = self.create_client_from_credential( + AuthorizationManagementClient, self.get_credential(AuthorizationManagementClient), + subscription_id=SYNTHETICS_SUBSCRIPTION_ID, + ) + + container_created = False + rbac_created = False + container_scope = ( + STORAGE_ACCOUNT_ID + "/blobServices/default/containers/" + container_name + ) + + try: + # Provision mover + project (WCUS — required by cpmoveraccount). + self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": WCUS_LOCATION}, + ) + self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + + # Source MCC endpoint over the PUBLIC AWS S3 bucket. + self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=source_endpoint, + endpoint={"properties": { + "endpointType": "AzureMultiCloudConnector", + "multiCloudConnectorId": MULTI_CLOUD_CONNECTOR_ID, + "awsS3BucketId": AWS_PUBLIC_S3_BUCKET_ID, + "endpointKind": "Source", + "description": "publicMccSourceForJobRun", + }}, + ) + + # Target blob endpoint with explicit SystemAssigned MSI. + target = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=target_endpoint, + endpoint={ + "identity": {"type": "SystemAssigned"}, + "properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": STORAGE_ACCOUNT_ID, + "blobContainerName": container_name, + "description": "blobTargetForJobRun", + }, + }, + ) + assert target.identity is not None and target.identity.principal_id, ( + "Target blob endpoint did not get an auto-assigned MSI principalId" + ) + target_msi_principal_id = target.identity.principal_id + + # Cross-sub: create target container. + storage_client.blob_containers.create( + resource_group_name=STORAGE_ACCOUNT_RG, account_name=STORAGE_ACCOUNT_NAME, + container_name=container_name, blob_container={}, + ) + container_created = True + + # Cross-sub: grant target MSI Storage Blob Data Contributor on container scope. + role_definition_id = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/providers/Microsoft.Authorization/roleDefinitions/" + + STORAGE_BLOB_DATA_CONTRIBUTOR_ROLE_DEF_GUID + ) + authorization_client.role_assignments.create( + scope=container_scope, role_assignment_name=role_assignment_name, + parameters={"properties": { + "roleDefinitionId": role_definition_id, + "principalId": target_msi_principal_id, + "principalType": "ServicePrincipal", + }}, + ) + rbac_created = True + + # Create the job definition (C2C, no `connections` since no PLS). + jd = self.client.job_definitions.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + job_definition={"properties": { + "copyMode": "Additive", + "sourceName": source_endpoint, + "targetName": target_endpoint, + "jobType": "CloudToCloud", + "sourceSubpath": "/", + "targetSubpath": "/", + "description": "JobDefForJobRunTest", + }}, + ) + # CRUD assertions (mirrors .NET matrix #10 spec). + assert jd.name == jd_name + assert jd.properties.source_name == source_endpoint + assert jd.properties.target_name == target_endpoint + assert jd.properties.copy_mode == "Additive" + + jd_get = self.client.job_definitions.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ) + assert jd_get.id == jd.id + + items = list(self.client.job_definitions.list( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + )) + assert len(items) >= 1 + assert jd_name in [j.name for j in items] + + # Start the job. + start_result = self.client.job_definitions.start_job( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ) + assert start_result.job_run_resource_id, "start_job did not return jobRunResourceId" + job_run_name = start_result.job_run_resource_id.rstrip("/").split("/")[-1] + + # Poll job_runs.get every 30s up to 30 min until terminal. + terminal_states = {"Succeeded", "Failed", "Cancelled", "PartialSucceeded"} + final_status = None + for _ in range(60): + run = self.client.job_runs.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, job_run_name=job_run_name, + ) + current_status = ( + getattr(run.properties, "status", None) if run.properties is not None else None + ) + if current_status in terminal_states: + final_status = current_status + break + self.sleep(30) + assert final_status is not None, ( + "Job run did not reach a terminal state within 30 min" + ) + assert final_status == "Succeeded", ( + "Expected job-run to Succeed with public bucket + target MSI RBAC, " + "got: " + str(final_status) + ) + + self.client.job_definitions.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ).result() + + finally: + if rbac_created: + try: + authorization_client.role_assignments.delete( + scope=container_scope, role_assignment_name=role_assignment_name, + ) + except Exception: # noqa: BLE001 + pass + if container_created: + try: + storage_client.blob_containers.delete( + resource_group_name=STORAGE_ACCOUNT_RG, account_name=STORAGE_ACCOUNT_NAME, + container_name=container_name, + ) + except Exception: # noqa: BLE001 + pass + + return variables + + # ----- JobDefinitionScheduleTests.CreateJobDefinitionWithWeeklyScheduleTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_create_with_weekly_schedule(self, resource_group, **kwargs): + rg = resource_group.name + sm_name = "testsm-jdwk" + project_name = "testproj-jdwk" + source_endpoint = "testnfsendpoint" + target_endpoint = "testblobendpoint" + self._provision_parents(rg, sm_name, project_name, source_endpoint, target_endpoint) + + jd_name = "jobdef-sched-wk" + variables = kwargs.pop("variables", {}) + now = datetime.now(timezone.utc) + start_date = variables.setdefault( + "schedule_start", (now + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ") + ) + end_date = variables.setdefault( + "schedule_end", (now + timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%SZ") + ) + body = {"properties": { + "copyMode": "Additive", + "sourceName": source_endpoint, + "targetName": target_endpoint, + "description": "Job definition with weekly schedule", + "dataIntegrityValidation": "SaveVerifyFileMD5", + "schedule": { + "frequency": "Weekly", + "isActive": True, + "executionTime": {"hour": 2}, + "startDate": start_date, + "endDate": end_date, + "daysOfWeek": ["Monday", "Wednesday", "Friday"], + }, + }} + + jd = self.client.job_definitions.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, job_definition=body, + ) + assert jd.name == jd_name + assert jd.properties.source_name == source_endpoint + assert jd.properties.target_name == target_endpoint + assert jd.properties.copy_mode == "Additive" + assert jd.properties.description == "Job definition with weekly schedule" + + assert jd.properties.schedule is not None + assert jd.properties.schedule.frequency == "Weekly" + assert jd.properties.schedule.is_active is True + assert jd.properties.schedule.execution_time.hour == 2 + assert len(jd.properties.schedule.days_of_week) == 3 + + jd = self.client.job_definitions.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ) + assert jd.properties.schedule.frequency == "Weekly" + + self.client.job_definitions.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ).result() + + return variables + + # ----- JobDefinitionScheduleTests.CreateJobDefinitionWithDailyScheduleAndPreservePermissionsTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_create_with_daily_schedule_and_preserve_permissions(self, resource_group, **kwargs): + rg = resource_group.name + sm_name = "testsm-jddl" + project_name = "testproj-jddl" + source_endpoint = "testnfsendpoint" + target_endpoint = "testblobendpoint" + self._provision_parents(rg, sm_name, project_name, source_endpoint, target_endpoint) + + jd_name = "jobdef-sched-daily" + variables = kwargs.pop("variables", {}) + now = datetime.now(timezone.utc) + start_date = variables.setdefault( + "schedule_start", (now + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ") + ) + end_date = variables.setdefault( + "schedule_end", (now + timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%SZ") + ) + body = {"properties": { + "copyMode": "Mirror", + "sourceName": source_endpoint, + "targetName": target_endpoint, + "description": "Job definition with daily schedule", + "dataIntegrityValidation": "None", + "preservePermissions": True, + "schedule": { + "frequency": "Daily", + "isActive": True, + "executionTime": {"hour": 0}, + "startDate": start_date, + "endDate": end_date, + }, + }} + + jd = self.client.job_definitions.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, job_definition=body, + ) + assert jd.name == jd_name + assert jd.properties.copy_mode == "Mirror" + assert jd.properties.schedule is not None + assert jd.properties.schedule.frequency == "Daily" + assert jd.properties.schedule.is_active is True + + self.client.job_definitions.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ).result() + + return variables + + # ----- JobDefinitionScheduleTests.CreateJobDefinitionWithOnetimeScheduleTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_create_with_onetime_schedule(self, resource_group, **kwargs): + rg = resource_group.name + sm_name = "testsm-jdot" + project_name = "testproj-jdot" + source_endpoint = "testnfsendpoint" + target_endpoint = "testblobendpoint" + self._provision_parents(rg, sm_name, project_name, source_endpoint, target_endpoint) + + jd_name = "jobdef-sched-once" + variables = kwargs.pop("variables", {}) + now = datetime.now(timezone.utc) + start_date = variables.setdefault( + "schedule_start", (now + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ") + ) + body = {"properties": { + "copyMode": "Additive", + "sourceName": source_endpoint, + "targetName": target_endpoint, + "description": "Job definition with one-time schedule", + "schedule": { + "frequency": "Onetime", + "isActive": True, + "executionTime": {"hour": 10}, + "startDate": start_date, + }, + }} + + jd = self.client.job_definitions.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, job_definition=body, + ) + assert jd.name == jd_name + assert jd.properties.schedule is not None + assert jd.properties.schedule.frequency == "Onetime" + assert jd.properties.schedule.is_active is True + + self.client.job_definitions.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=jd_name, + ).result() + + return variables + + # ----- JobDefinitionJobRunTests.StartC2CJobWithPrivateSourceTest (matrix row #31) ----- + # Full private-bucket CloudToCloud E2E mirroring the RP test + # Storage-XDataMove-RP/test/E2ETest/C2CTest/StartJobTest.cs::StartC2CJobWithPrivateSourceAsyncSuccessPathTest + # 1) self-provision RG + mover + project in westcentralus + # 2) create Storage Mover Connection -> capture PE id + # 3) approve the auto-created PE-connection on the PLS (cross-sub) + # 4) poll the storage mover Connection until properties.connectionStatus == Approved + # 5) create target Blob endpoint, capture its system-assigned MSI principalId + # 6) create the target container under the shared cpmoveraccount (cross-sub) + # 7) grant the endpoint MSI Storage Blob Data Contributor on the container scope (cross-sub) + # 8) create source MCC endpoint over the PRIVATE AWS S3 bucket + # 9) create C2C job definition wired to the connection + # 10) start_job -> poll job_runs.get every 30s up to 30 min, assert Succeeded + # 11) cleanup (best-effort) of all cross-sub side effects + # See the "Porter's reference" callout in the cross-language playbook + # (storage-mover-scenario-tests-cross-language) for the canonical step-by-step. + + @_SKIP_IF_CROSS_SUB_CLIENTS_OLD + @RandomNameResourceGroupPreparer(location=WCUS_LOCATION) + @recorded_by_proxy + def test_start_c2c_job_with_private_source(self, resource_group, **kwargs): + rg = resource_group.name + sm_name = "testsm-c2cpvt" + project_name = "testproj-c2cpvt" + connection_name = "testconn-pvt" + source_endpoint = "testsrcep-mcc-pvt" + target_endpoint = "testtgtep-blob-pvt" + job_definition_name = "testjobdef-c2cpvt" + + # Recording-stable derived values — round-tripped via the `variables` dict + # so playback uses the same names the recording captured. + variables = kwargs.pop("variables", {}) + container_name = variables.setdefault("container_name", "tc" + uuid.uuid4().hex[:10].lower()) + role_assignment_name = variables.setdefault("role_assignment_id", str(uuid.uuid4())) + + # Cross-sub mgmt clients (override subscription_id at construction). + # Cross-sub mgmt clients. Pin api_version explicitly — see #10 above. + network_client = self.create_client_from_credential( + NetworkManagementClient, + self.get_credential(NetworkManagementClient), + subscription_id=SYNTHETICS_SUBSCRIPTION_ID, + api_version="2025-05-01", + ) + storage_client = self.create_client_from_credential( + StorageManagementClient, + self.get_credential(StorageManagementClient), + subscription_id=SYNTHETICS_SUBSCRIPTION_ID, + api_version="2025-06-01", + ) + authorization_client = self.create_client_from_credential( + AuthorizationManagementClient, + self.get_credential(AuthorizationManagementClient), + subscription_id=SYNTHETICS_SUBSCRIPTION_ID, + ) + + # Flags to drive best-effort cross-sub cleanup in finally. + connection_created = False + container_created = False + rbac_created = False + container_scope = ( + STORAGE_ACCOUNT_ID + "/blobServices/default/containers/" + container_name + ) + + try: + # 1. Self-provision storage mover + project. + self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": WCUS_LOCATION}, + ) + self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + + # 2. Create the Storage Mover Connection. The RP synchronously provisions + # a private endpoint on the PLS in Pending state and returns its id. + connection = self.client.connections.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, connection_name=connection_name, + connection={"properties": { + "privateLinkServiceId": REAL_PRIVATE_LINK_SERVICE_ID, + "description": "ConnectionForPrivateBucketJobRun", + }}, + ) + connection_created = True + pe_resource_id = connection.properties.private_endpoint_resource_id + assert pe_resource_id, "Connection create did not return privateEndpointResourceId" + + # 3. Wait for the auto-created PE-connection to appear on the PLS, then + # approve it via PLS PEC update (cross-sub call). + pe_connection_name = None + for _ in range(10): + for pec in network_client.private_link_services.list_private_endpoint_connections( + resource_group_name=PLS_RESOURCE_GROUP, service_name=PLS_NAME, + ): + pec_pe_id = (pec.private_endpoint.id if pec.private_endpoint else "") or "" + if pec_pe_id.lower() == pe_resource_id.lower(): + pe_connection_name = pec.name + break + if pe_connection_name: + break + self.sleep(15) + assert pe_connection_name, ( + "PE-connection for {} did not appear on PLS {} within 150s".format( + pe_resource_id, PLS_NAME, + ) + ) + + network_client.private_link_services.update_private_endpoint_connection( + resource_group_name=PLS_RESOURCE_GROUP, service_name=PLS_NAME, + pe_connection_name=pe_connection_name, + parameters={"properties": { + "privateLinkServiceConnectionState": { + "status": "Approved", + "description": "approved by storage-mover SDK live test", + "actionsRequired": "None", + }, + }}, + ) + + # 4. Poll Storage Mover Connection until connection_status == Approved. + # The storagemover side mirrors PLS state with up to ~5 min lag. + approved = False + for _ in range(10): + conn_show = self.client.connections.get( + resource_group_name=rg, storage_mover_name=sm_name, + connection_name=connection_name, + ) + if (conn_show.properties is not None + and getattr(conn_show.properties, "connection_status", None) == "Approved"): + approved = True + break + self.sleep(30) + assert approved, "Storage Mover Connection did not reach Approved within 300s" + + # 5. Create the target blob endpoint. Explicitly request a SystemAssigned + # MSI — unlike the CLI's `endpoint create-for-storage-container` command + # (which auto-injects identity), the raw SDK PUT does not set identity + # by default. Capture principalId so we can grant data-plane RBAC. + target = self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=target_endpoint, + endpoint={ + "identity": {"type": "SystemAssigned"}, + "properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": STORAGE_ACCOUNT_ID, + "blobContainerName": container_name, + "description": "blobTargetForJobRunWait", + }, + }, + ) + assert target.identity is not None and target.identity.principal_id, ( + "Target blob endpoint did not get an auto-assigned MSI principalId" + ) + target_msi_principal_id = target.identity.principal_id + + # 6. Cross-sub: create the target blob container under cpmoveraccount. + storage_client.blob_containers.create( + resource_group_name=STORAGE_ACCOUNT_RG, account_name=STORAGE_ACCOUNT_NAME, + container_name=container_name, blob_container={}, + ) + container_created = True + + # 7. Cross-sub: grant the endpoint MSI Storage Blob Data Contributor on + # the container scope. + role_definition_id = ( + "/subscriptions/" + SYNTHETICS_SUBSCRIPTION_ID + + "/providers/Microsoft.Authorization/roleDefinitions/" + + STORAGE_BLOB_DATA_CONTRIBUTOR_ROLE_DEF_GUID + ) + authorization_client.role_assignments.create( + scope=container_scope, role_assignment_name=role_assignment_name, + parameters={"properties": { + "roleDefinitionId": role_definition_id, + "principalId": target_msi_principal_id, + "principalType": "ServicePrincipal", + }}, + ) + rbac_created = True + + # 8. Source MCC endpoint over the PRIVATE AWS S3 bucket. + self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=source_endpoint, + endpoint={"properties": { + "endpointType": "AzureMultiCloudConnector", + "multiCloudConnectorId": MULTI_CLOUD_CONNECTOR_ID, + "awsS3BucketId": PRIVATE_S3_BUCKET_ID, + "endpointKind": "Source", + "description": "privateMccSourceForJobRunWait", + }}, + ) + + # 9. C2C job definition wired to the approved connection. + self.client.job_definitions.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=job_definition_name, + job_definition={"properties": { + "copyMode": "Additive", + "sourceName": source_endpoint, + "targetName": target_endpoint, + "jobType": "CloudToCloud", + "sourceSubpath": "/", + "targetSubpath": "/", + "connections": [connection.id], + "description": "JobDefForJobRunWaitTest", + }}, + ) + + # 10. Start the job. The RP returns the job-run resource id; extract basename. + start_result = self.client.job_definitions.start_job( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=job_definition_name, + ) + assert start_result.job_run_resource_id, "start_job did not return jobRunResourceId" + job_run_name = start_result.job_run_resource_id.rstrip("/").split("/")[-1] + + # 11. Poll job_runs.get every 30s up to 30 min until terminal. + terminal_states = {"Succeeded", "Failed", "Cancelled", "PartialSucceeded"} + final_status = None + for _ in range(60): + run = self.client.job_runs.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=job_definition_name, job_run_name=job_run_name, + ) + current_status = ( + getattr(run.properties, "status", None) if run.properties is not None else None + ) + if current_status in terminal_states: + final_status = current_status + break + self.sleep(30) + assert final_status is not None, ( + "Job run did not reach a terminal state within 30 min" + ) + assert final_status == "Succeeded", ( + "Expected job-run to Succeed with approved connection + target MSI RBAC, " + "got: " + str(final_status) + ) + + # Cleanup the job definition (other resources rolled back in finally). + self.client.job_definitions.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + job_definition_name=job_definition_name, + ).result() + + finally: + # Best-effort cleanup of cross-sub side effects in shared infra. Order + # matters: RBAC then container then connection (container can't be + # deleted while it has active RBAC pinning the principal). + if rbac_created: + try: + authorization_client.role_assignments.delete( + scope=container_scope, role_assignment_name=role_assignment_name, + ) + except Exception: # noqa: BLE001 + pass + if container_created: + try: + storage_client.blob_containers.delete( + resource_group_name=STORAGE_ACCOUNT_RG, account_name=STORAGE_ACCOUNT_NAME, + container_name=container_name, + ) + except Exception: # noqa: BLE001 + pass + if connection_created: + try: + self.client.connections.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, + connection_name=connection_name, + ).result() + except Exception: # noqa: BLE001 + pass + + return variables diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_runs_operations_async_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_runs_operations_async_test.py new file mode 100644 index 000000000000..825f57178fec --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_runs_operations_async_test.py @@ -0,0 +1,122 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +import pytest +from azure.core.exceptions import ResourceNotFoundError +from azure.mgmt.storagemover.aio import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer +from devtools_testutils.aio import recorded_by_proxy_async + +AZURE_LOCATION = "eastus" +STORAGE_MOVER_NAME = "testmoverjobrun" +PROJECT_NAME = "testprojectjobrun" +SOURCE_ENDPOINT_NAME = "testnfssrc" +TARGET_ENDPOINT_NAME = "testblobtarget" +JOB_DEFINITION_NAME = "testjobdef1" + +FAKE_STORAGE_ACCOUNT_ID = ( + "/subscriptions/00000000-0000-0000-0000-000000000000" + "/resourceGroups/fakeRg/providers/Microsoft.Storage/storageAccounts/fakeAccount" +) + + +class TestStorageMoverMgmtJobRunsOperationsAsync(AzureMgmtRecordedTestCase): + """Read-only coverage for job_runs (list + get). + + Job runs are created by the agent when StartJob is invoked on a job + definition. Without a registered agent we can't trigger a real run, so + these tests verify the read paths return sensible defaults: list yields + an empty page, and get on a non-existent run raises ResourceNotFoundError. + """ + + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient, is_async=True) + + async def _provision_parents(self, resource_group_name): + await self.client.storage_movers.create_or_update( + resource_group_name=resource_group_name, + storage_mover_name=STORAGE_MOVER_NAME, + storage_mover={ + "location": AZURE_LOCATION, + "properties": {"description": "Storage mover for job run tests"}, + }, + ) + await self.client.projects.create_or_update( + resource_group_name=resource_group_name, + storage_mover_name=STORAGE_MOVER_NAME, + project_name=PROJECT_NAME, + project={"properties": {"description": "Project for job run tests"}}, + ) + await self.client.endpoints.create_or_update( + resource_group_name=resource_group_name, + storage_mover_name=STORAGE_MOVER_NAME, + endpoint_name=SOURCE_ENDPOINT_NAME, + endpoint={ + "properties": { + "endpointType": "NfsMount", + "host": "10.0.0.1", + "export": "/nfsshare", + "nfsVersion": "NFSv3", + "description": "Source NFS endpoint", + } + }, + ) + await self.client.endpoints.create_or_update( + resource_group_name=resource_group_name, + storage_mover_name=STORAGE_MOVER_NAME, + endpoint_name=TARGET_ENDPOINT_NAME, + endpoint={ + "properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": FAKE_STORAGE_ACCOUNT_ID, + "blobContainerName": "testcontainer", + "description": "Target blob container endpoint", + } + }, + ) + await self.client.job_definitions.create_or_update( + resource_group_name=resource_group_name, + storage_mover_name=STORAGE_MOVER_NAME, + project_name=PROJECT_NAME, + job_definition_name=JOB_DEFINITION_NAME, + job_definition={ + "properties": { + "copyMode": "Additive", + "sourceName": SOURCE_ENDPOINT_NAME, + "targetName": TARGET_ENDPOINT_NAME, + "description": "Job definition for job run tests", + } + }, + ) + + # ----- JobRunTests.GetExistTest ----- + # .NET version does: get(known JobName) + foreach list + Exists(JobName) + second get. + # JobRuns are produced by an agent's StartJob run; without a registered agent we + # cannot create one, so we cover the equivalent read paths: list returns an empty + # page, and get on an unknown name raises ResourceNotFoundError. + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_get_exist(self, resource_group): + await self._provision_parents(resource_group.name) + + response = self.client.job_runs.list( + resource_group_name=resource_group.name, + storage_mover_name=STORAGE_MOVER_NAME, + project_name=PROJECT_NAME, + job_definition_name=JOB_DEFINITION_NAME, + ) + result = [r async for r in response] + assert result == [] + + with pytest.raises(ResourceNotFoundError): + await self.client.job_runs.get( + resource_group_name=resource_group.name, + storage_mover_name=STORAGE_MOVER_NAME, + project_name=PROJECT_NAME, + job_definition_name=JOB_DEFINITION_NAME, + job_run_name="nonexistentrun", + ) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_runs_operations_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_runs_operations_test.py new file mode 100644 index 000000000000..183d9df2013e --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_job_runs_operations_test.py @@ -0,0 +1,121 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +import pytest +from azure.core.exceptions import ResourceNotFoundError +from azure.mgmt.storagemover import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer, recorded_by_proxy + +AZURE_LOCATION = "eastus" +STORAGE_MOVER_NAME = "testmoverjobrun" +PROJECT_NAME = "testprojectjobrun" +SOURCE_ENDPOINT_NAME = "testnfssrc" +TARGET_ENDPOINT_NAME = "testblobtarget" +JOB_DEFINITION_NAME = "testjobdef1" + +FAKE_STORAGE_ACCOUNT_ID = ( + "/subscriptions/00000000-0000-0000-0000-000000000000" + "/resourceGroups/fakeRg/providers/Microsoft.Storage/storageAccounts/fakeAccount" +) + + +class TestStorageMoverMgmtJobRunsOperations(AzureMgmtRecordedTestCase): + """Read-only coverage for job_runs (list + get). + + Job runs are created by the agent when StartJob is invoked on a job + definition. Without a registered agent we can't trigger a real run, so + these tests verify the read paths return sensible defaults: list yields + an empty page, and get on a non-existent run raises ResourceNotFoundError. + """ + + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient) + + def _provision_parents(self, resource_group_name): + self.client.storage_movers.create_or_update( + resource_group_name=resource_group_name, + storage_mover_name=STORAGE_MOVER_NAME, + storage_mover={ + "location": AZURE_LOCATION, + "properties": {"description": "Storage mover for job run tests"}, + }, + ) + self.client.projects.create_or_update( + resource_group_name=resource_group_name, + storage_mover_name=STORAGE_MOVER_NAME, + project_name=PROJECT_NAME, + project={"properties": {"description": "Project for job run tests"}}, + ) + self.client.endpoints.create_or_update( + resource_group_name=resource_group_name, + storage_mover_name=STORAGE_MOVER_NAME, + endpoint_name=SOURCE_ENDPOINT_NAME, + endpoint={ + "properties": { + "endpointType": "NfsMount", + "host": "10.0.0.1", + "export": "/nfsshare", + "nfsVersion": "NFSv3", + "description": "Source NFS endpoint", + } + }, + ) + self.client.endpoints.create_or_update( + resource_group_name=resource_group_name, + storage_mover_name=STORAGE_MOVER_NAME, + endpoint_name=TARGET_ENDPOINT_NAME, + endpoint={ + "properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": FAKE_STORAGE_ACCOUNT_ID, + "blobContainerName": "testcontainer", + "description": "Target blob container endpoint", + } + }, + ) + self.client.job_definitions.create_or_update( + resource_group_name=resource_group_name, + storage_mover_name=STORAGE_MOVER_NAME, + project_name=PROJECT_NAME, + job_definition_name=JOB_DEFINITION_NAME, + job_definition={ + "properties": { + "copyMode": "Additive", + "sourceName": SOURCE_ENDPOINT_NAME, + "targetName": TARGET_ENDPOINT_NAME, + "description": "Job definition for job run tests", + } + }, + ) + + # ----- JobRunTests.GetExistTest ----- + # .NET version does: get(known JobName) + foreach list + Exists(JobName) + second get. + # JobRuns are produced by an agent's StartJob run; without a registered agent we + # cannot create one, so we cover the equivalent read paths: list returns an empty + # page, and get on an unknown name raises ResourceNotFoundError. + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_get_exist(self, resource_group): + self._provision_parents(resource_group.name) + + response = self.client.job_runs.list( + resource_group_name=resource_group.name, + storage_mover_name=STORAGE_MOVER_NAME, + project_name=PROJECT_NAME, + job_definition_name=JOB_DEFINITION_NAME, + ) + result = [r for r in response] + assert result == [] + + with pytest.raises(ResourceNotFoundError): + self.client.job_runs.get( + resource_group_name=resource_group.name, + storage_mover_name=STORAGE_MOVER_NAME, + project_name=PROJECT_NAME, + job_definition_name=JOB_DEFINITION_NAME, + job_run_name="nonexistentrun", + ) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_operations_async_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_operations_async_test.py deleted file mode 100644 index d3d04a478c5c..000000000000 --- a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_operations_async_test.py +++ /dev/null @@ -1,27 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- -import pytest -from azure.mgmt.storagemover.aio import StorageMoverMgmtClient - -from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer -from devtools_testutils.aio import recorded_by_proxy_async - -AZURE_LOCATION = "eastus" - - -@pytest.mark.live_test_only -class TestStorageMoverMgmtOperationsAsync(AzureMgmtRecordedTestCase): - def setup_method(self, method): - self.client = self.create_mgmt_client(StorageMoverMgmtClient, is_async=True) - - @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) - @recorded_by_proxy_async - async def test_operations_list(self, resource_group): - response = self.client.operations.list() - result = [r async for r in response] - assert len(result) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_operations_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_operations_test.py deleted file mode 100644 index fa93a34a5ccd..000000000000 --- a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_operations_test.py +++ /dev/null @@ -1,26 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- -import pytest -from azure.mgmt.storagemover import StorageMoverMgmtClient - -from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer, recorded_by_proxy - -AZURE_LOCATION = "eastus" - - -@pytest.mark.live_test_only -class TestStorageMoverMgmtOperations(AzureMgmtRecordedTestCase): - def setup_method(self, method): - self.client = self.create_mgmt_client(StorageMoverMgmtClient) - - @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) - @recorded_by_proxy - def test_operations_list(self, resource_group): - response = self.client.operations.list() - result = [r for r in response] - assert len(result) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_projects_operations_async_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_projects_operations_async_test.py new file mode 100644 index 000000000000..96a4999e5b8c --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_projects_operations_async_test.py @@ -0,0 +1,96 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +"""Async scenario tests for projects. + +Mirrors .NET ProjectCollectionTests + ProjectResourceTests at: + Q:\\source\\azure-sdk-for-net\\sdk\\storagemover\\Azure.ResourceManager.StorageMover\\tests\\Scenario +""" +import pytest +from azure.core.exceptions import ResourceNotFoundError +from azure.mgmt.storagemover.aio import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer +from devtools_testutils.aio import recorded_by_proxy_async + +AZURE_LOCATION = "eastus" + + +class TestStorageMoverMgmtProjectsOperationsAsync(AzureMgmtRecordedTestCase): + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient, is_async=True) + + async def _create_storage_mover(self, rg, sm_name): + return await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_create_get_exists(self, resource_group): + rg = resource_group.name + sm_name = "stomover-projcol" + await self._create_storage_mover(rg, sm_name) + + project_name = "project-col1" + project = await self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + assert project.name == project_name + assert project.properties.description is None + assert project.type.lower() == "microsoft.storagemover/storagemovers/projects" + + project = await self.client.projects.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + ) + assert project.name == project_name + + items = [p async for p in self.client.projects.list( + resource_group_name=rg, storage_mover_name=sm_name, + )] + assert len(items) >= 1 + names = [p.name for p in items] + assert project_name in names + + with pytest.raises(ResourceNotFoundError): + await self.client.projects.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name + "111", + ) + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_get_update_delete(self, resource_group): + rg = resource_group.name + sm_name = "stomover-projres" + await self._create_storage_mover(rg, sm_name) + + project_name = "project-res1" + created = await self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + + fetched = await self.client.projects.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + ) + assert fetched.name == created.name + assert fetched.id == created.id + + updated = await self.client.projects.update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={"properties": {"description": "This is an updated project"}}, + ) + assert updated.properties.description == "This is an updated project" + + poller = await self.client.projects.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + ) + await poller.result() + with pytest.raises(ResourceNotFoundError): + await self.client.projects.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + ) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_projects_operations_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_projects_operations_test.py new file mode 100644 index 000000000000..b9c3379be3e9 --- /dev/null +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_projects_operations_test.py @@ -0,0 +1,104 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- +"""Sync scenario tests for projects. + +Mirrors .NET ProjectCollectionTests + ProjectResourceTests at: + Q:\\source\\azure-sdk-for-net\\sdk\\storagemover\\Azure.ResourceManager.StorageMover\\tests\\Scenario +""" +import pytest +from azure.core.exceptions import ResourceNotFoundError +from azure.mgmt.storagemover import StorageMoverMgmtClient + +from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer, recorded_by_proxy + +AZURE_LOCATION = "eastus" + + +class TestStorageMoverMgmtProjectsOperations(AzureMgmtRecordedTestCase): + def setup_method(self, method): + self.client = self.create_mgmt_client(StorageMoverMgmtClient) + + def _create_storage_mover(self, rg, sm_name): + return self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + + # ----- ProjectCollectionTests.CrateGetExistTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_create_get_exists(self, resource_group): + rg = resource_group.name + sm_name = "stomover-projcol" + self._create_storage_mover(rg, sm_name) + + project_name = "project-col1" + project = self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + assert project.name == project_name + assert project.properties.description is None + assert project.type.lower() == "microsoft.storagemover/storagemovers/projects" + + project = self.client.projects.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + ) + assert project.name == project_name + assert project.properties.description is None + + items = list(self.client.projects.list( + resource_group_name=rg, storage_mover_name=sm_name, + )) + assert len(items) >= 1 + names = [p.name for p in items] + assert project_name in names + + # Existence via get + assert self.client.projects.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + ).name == project_name + with pytest.raises(ResourceNotFoundError): + self.client.projects.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name + "111", + ) + + # ----- ProjectResourceTests.GetUpdateDeleteTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_get_update_delete(self, resource_group): + rg = resource_group.name + sm_name = "stomover-projres" + self._create_storage_mover(rg, sm_name) + + project_name = "project-res1" + created = self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + + fetched = self.client.projects.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + ) + assert fetched.name == created.name + assert fetched.properties.description == created.properties.description + assert fetched.id == created.id + + updated = self.client.projects.update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={"properties": {"description": "This is an updated project"}}, + ) + assert updated.properties.description == "This is an updated project" + + self.client.projects.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + ).result() + with pytest.raises(ResourceNotFoundError): + self.client.projects.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + ) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_storage_movers_operations_async_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_storage_movers_operations_async_test.py index 8a12c81da6b1..0fbfb15c9032 100644 --- a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_storage_movers_operations_async_test.py +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_storage_movers_operations_async_test.py @@ -2,10 +2,14 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +"""Async scenario tests for storage movers (provider operations + StorageMover CRUD). + +Mirrors .NET StorageMoverCollectionTests + StorageMoverResourceTests at: + Q:\\source\\azure-sdk-for-net\\sdk\\storagemover\\Azure.ResourceManager.StorageMover\\tests\\Scenario +""" import pytest +from azure.core.exceptions import ResourceNotFoundError from azure.mgmt.storagemover.aio import StorageMoverMgmtClient from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer @@ -13,24 +17,171 @@ AZURE_LOCATION = "eastus" +FAKE_STORAGE_ACCOUNT_ID = ( + "/subscriptions/00000000-0000-0000-0000-000000000000" + "/resourceGroups/fakeRg/providers/Microsoft.Storage/storageAccounts/fakeAccount" +) + -@pytest.mark.live_test_only class TestStorageMoverMgmtStorageMoversOperationsAsync(AzureMgmtRecordedTestCase): def setup_method(self, method): self.client = self.create_mgmt_client(StorageMoverMgmtClient, is_async=True) @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) @recorded_by_proxy_async - async def test_storage_movers_list(self, resource_group): - response = self.client.storage_movers.list( - resource_group_name=resource_group.name, + async def test_create_update_get_exists(self, resource_group): + rg = resource_group.name + sm_name = "testsm-create1" + sm_name2 = "testsm-create2" + + body = { + "location": AZURE_LOCATION, + "tags": {"tag1": "value1"}, + "properties": {"description": "This is a new storage mover"}, + } + + sm1 = await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, storage_mover=body + ) + assert sm1.name == sm_name + assert sm1.tags["tag1"] == "value1" + assert sm1.properties.description == "This is a new storage mover" + + sm2 = await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name2, storage_mover=body + ) + assert sm2.name == sm_name2 + + sm1 = await self.client.storage_movers.get(resource_group_name=rg, storage_mover_name=sm_name) + assert sm1.name == sm_name + assert sm1.tags["tag1"] == "value1" + + items = [r async for r in self.client.storage_movers.list(resource_group_name=rg)] + assert len(items) == 2 + + body["properties"]["description"] = "This is an updated storage mover" + sm1 = await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, storage_mover=body + ) + assert sm1.properties.description == "This is an updated storage mover" + + with pytest.raises(ResourceNotFoundError): + await self.client.storage_movers.get(resource_group_name=rg, storage_mover_name=sm_name + "111") + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_get_storage_mover(self, resource_group): + rg = resource_group.name + sm_name = "testsm-get1" + + created = await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION, "tags": {"k": "v"}}, ) - result = [r async for r in response] - assert result == [] + + fetched1 = await self.client.storage_movers.get(resource_group_name=rg, storage_mover_name=sm_name) + fetched2 = await self.client.storage_movers.get(resource_group_name=rg, storage_mover_name=sm_name) + + assert fetched1.name == fetched2.name == sm_name + assert fetched1.location == fetched2.location + assert fetched1.type == fetched2.type + assert fetched1.id == fetched2.id == created.id + assert fetched1.tags == fetched2.tags + + @pytest.mark.skip(reason="Agents cannot be created by the RP; this test requires a registered agent VM.") + @pytest.mark.asyncio + async def test_get_storage_mover_agent(self): + pass @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) @recorded_by_proxy_async - async def test_storage_movers_list_by_subscription(self, resource_group): - response = self.client.storage_movers.list_by_subscription() - result = [r async for r in response] - assert len(result) + async def test_get_storage_mover_endpoint(self, resource_group): + rg = resource_group.name + sm_name = "testsm-getep" + endpoint_name = "testblobendpoint" + + await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + await self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": FAKE_STORAGE_ACCOUNT_ID, + "blobContainerName": "testcontainer", + }}, + ) + + endpoint = await self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.endpoint_type == "AzureStorageBlobContainer" + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_get_storage_mover_project(self, resource_group): + rg = resource_group.name + sm_name = "testsm-getproj" + project_name = "testproj1" + + await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + await self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + + project = await self.client.projects.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + ) + assert project.name == project_name + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy_async + async def test_update_add_set_remove_tag_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-updel" + + sm = await self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + assert sm.name == sm_name + assert sm.location == AZURE_LOCATION + + sm = await self.client.storage_movers.update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"properties": {"description": "This is an updated storage mover"}}, + ) + assert sm.properties.description == "This is an updated storage mover" + + # Subscription policies may inject extra tags, so only assert on the tag we set. + sm = await self.client.storage_movers.update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"tags": {"tag1": "val1"}}, + ) + assert sm.tags.get("tag1") == "val1" + + sm = await self.client.storage_movers.update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"tags": {"tag2": "val2", "tag3": "val3"}}, + ) + assert sm.tags.get("tag2") == "val2" + assert sm.tags.get("tag3") == "val3" + + sm = await self.client.storage_movers.update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"tags": {"tag3": "val3"}}, + ) + assert sm.tags.get("tag3") == "val3" + + poller = await self.client.storage_movers.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, + ) + await poller.result() + with pytest.raises(ResourceNotFoundError): + await self.client.storage_movers.get(resource_group_name=rg, storage_mover_name=sm_name) diff --git a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_storage_movers_operations_test.py b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_storage_movers_operations_test.py index 5d6a26b75848..4e83d2edc898 100644 --- a/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_storage_movers_operations_test.py +++ b/sdk/storagemover/azure-mgmt-storagemover/tests/test_storage_mover_mgmt_storage_movers_operations_test.py @@ -2,34 +2,205 @@ # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +"""Sync scenario tests for storage movers (provider operations + StorageMover CRUD). + +Mirrors .NET StorageMoverCollectionTests + StorageMoverResourceTests at: + Q:\\source\\azure-sdk-for-net\\sdk\\storagemover\\Azure.ResourceManager.StorageMover\\tests\\Scenario +""" import pytest +from azure.core.exceptions import ResourceNotFoundError from azure.mgmt.storagemover import StorageMoverMgmtClient from devtools_testutils import AzureMgmtRecordedTestCase, RandomNameResourceGroupPreparer, recorded_by_proxy AZURE_LOCATION = "eastus" +FAKE_STORAGE_ACCOUNT_ID = ( + "/subscriptions/00000000-0000-0000-0000-000000000000" + "/resourceGroups/fakeRg/providers/Microsoft.Storage/storageAccounts/fakeAccount" +) + -@pytest.mark.live_test_only class TestStorageMoverMgmtStorageMoversOperations(AzureMgmtRecordedTestCase): def setup_method(self, method): self.client = self.create_mgmt_client(StorageMoverMgmtClient) + # ----- StorageMoverCollectionTests.CreateUpdateGetExistsTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_create_update_get_exists(self, resource_group): + rg = resource_group.name + sm_name = "testsm-create1" + sm_name2 = "testsm-create2" + + body = { + "location": AZURE_LOCATION, + "tags": {"tag1": "value1"}, + "properties": {"description": "This is a new storage mover"}, + } + + sm1 = self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, storage_mover=body + ) + assert sm1.name == sm_name + assert sm1.tags["tag1"] == "value1" + assert sm1.properties.description == "This is a new storage mover" + + sm2 = self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name2, storage_mover=body + ) + assert sm2.name == sm_name2 + + sm1 = self.client.storage_movers.get(resource_group_name=rg, storage_mover_name=sm_name) + assert sm1.name == sm_name + assert sm1.tags["tag1"] == "value1" + assert sm1.properties.description == "This is a new storage mover" + + items = list(self.client.storage_movers.list(resource_group_name=rg)) + assert len(items) == 2 + + body["properties"]["description"] = "This is an updated storage mover" + sm1 = self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, storage_mover=body + ) + assert sm1.properties.description == "This is an updated storage mover" + + # Existence check via get (Python SDK has no Exists helper) + assert self.client.storage_movers.get(resource_group_name=rg, storage_mover_name=sm_name).name == sm_name + with pytest.raises(ResourceNotFoundError): + self.client.storage_movers.get(resource_group_name=rg, storage_mover_name=sm_name + "111") + + # ----- StorageMoverResourceTests.GetStorageMoverTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_get_storage_mover(self, resource_group): + rg = resource_group.name + sm_name = "testsm-get1" + + created = self.client.storage_movers.create_or_update( + resource_group_name=rg, + storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION, "tags": {"k": "v"}}, + ) + + fetched1 = self.client.storage_movers.get(resource_group_name=rg, storage_mover_name=sm_name) + fetched2 = self.client.storage_movers.get(resource_group_name=rg, storage_mover_name=sm_name) + + assert fetched1.name == fetched2.name == sm_name + assert fetched1.location == fetched2.location + assert fetched1.type == fetched2.type + assert fetched1.id == fetched2.id == created.id + assert fetched1.tags == fetched2.tags + + # ----- StorageMoverResourceTests.GetStorageMoverAgentTest ----- + # SKIP: agents cannot be created via RP — they are registered by an actual agent VM. + + @pytest.mark.skip(reason="Agents cannot be created by the RP; this test requires a registered agent VM.") + def test_get_storage_mover_agent(self): + pass + + # ----- StorageMoverResourceTests.GetStorageMoverEndpointTest ----- + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) @recorded_by_proxy - def test_storage_movers_list(self, resource_group): - response = self.client.storage_movers.list( - resource_group_name=resource_group.name, + def test_get_storage_mover_endpoint(self, resource_group): + rg = resource_group.name + sm_name = "testsm-getep" + endpoint_name = "testblobendpoint" + + self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + self.client.endpoints.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + endpoint={"properties": { + "endpointType": "AzureStorageBlobContainer", + "storageAccountResourceId": FAKE_STORAGE_ACCOUNT_ID, + "blobContainerName": "testcontainer", + }}, + ) + + endpoint = self.client.endpoints.get( + resource_group_name=rg, storage_mover_name=sm_name, endpoint_name=endpoint_name, + ) + assert endpoint.name == endpoint_name + assert endpoint.properties.endpoint_type == "AzureStorageBlobContainer" + + # ----- StorageMoverResourceTests.GetStorageMoverProjectTest ----- + + @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) + @recorded_by_proxy + def test_get_storage_mover_project(self, resource_group): + rg = resource_group.name + sm_name = "testsm-getproj" + project_name = "testproj1" + + self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + self.client.projects.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, + project={}, + ) + + project = self.client.projects.get( + resource_group_name=rg, storage_mover_name=sm_name, project_name=project_name, ) - result = [r for r in response] - assert result == [] + assert project.name == project_name + + # ----- StorageMoverResourceTests.UpdateAddSetRemoveTagDeletTest ----- @RandomNameResourceGroupPreparer(location=AZURE_LOCATION) @recorded_by_proxy - def test_storage_movers_list_by_subscription(self, resource_group): - response = self.client.storage_movers.list_by_subscription() - result = [r for r in response] - assert len(result) + def test_update_add_set_remove_tag_delete(self, resource_group): + rg = resource_group.name + sm_name = "testsm-updel" + + sm = self.client.storage_movers.create_or_update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"location": AZURE_LOCATION}, + ) + assert sm.name == sm_name + assert sm.location == AZURE_LOCATION + + # Update description via PATCH + sm = self.client.storage_movers.update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"properties": {"description": "This is an updated storage mover"}}, + ) + assert sm.properties.description == "This is an updated storage mover" + + # Add a single tag (mirrors AddTagAsync) — Python SDK has no AddTag helper, so use update. + # Subscription policies may inject extra tags, so only assert on the tag we set. + sm = self.client.storage_movers.update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"tags": {"tag1": "val1"}}, + ) + assert sm.tags.get("tag1") == "val1" + + # Set tags (mirrors SetTagsAsync — PATCH with a new tag map). + sm = self.client.storage_movers.update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"tags": {"tag2": "val2", "tag3": "val3"}}, + ) + assert sm.tags.get("tag2") == "val2" + assert sm.tags.get("tag3") == "val3" + + # Remove a tag (mirrors RemoveTagAsync) — verify tag3 is still present. + sm = self.client.storage_movers.update( + resource_group_name=rg, storage_mover_name=sm_name, + storage_mover={"tags": {"tag3": "val3"}}, + ) + assert sm.tags.get("tag3") == "val3" + + # Delete and confirm 404 + self.client.storage_movers.begin_delete( + resource_group_name=rg, storage_mover_name=sm_name, + ).result() + with pytest.raises(ResourceNotFoundError): + self.client.storage_movers.get(resource_group_name=rg, storage_mover_name=sm_name)