diff --git a/src/migrate/HISTORY.rst b/src/migrate/HISTORY.rst deleted file mode 100644 index d730d379ded..00000000000 --- a/src/migrate/HISTORY.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. :changelog: - -Release History -=============== - -1.0.0 -+++++++++++++++ -* Initial release. - - diff --git a/src/migrate/README.md b/src/migrate/README.md deleted file mode 100644 index 526038f491e..00000000000 --- a/src/migrate/README.md +++ /dev/null @@ -1,303 +0,0 @@ -# Azure CLI Migration Module - -This module provides server discovery and replication capabilities for Azure resources and workloads through Azure CLI commands, with special focus on Azure Local (Azure Stack HCI) migrations. - -# Azure CLI Migrate Extension # -The Azure CLI extension for managing [Azure Migrate](https://aka.ms/azure-migrate) resources. - -## Install ## -You can install the extension by running: -``` sh -az extension add --name migrate -``` - -## Usage ## -``` sh -az migrate --help -``` - -## Uninstall ## -You can see if the extension is installed by running `az --version` or `az extension list`. You can remove the extension by running: -``` sh -az extension remove --name migrate -``` - - -## Features - -- **Server discovery**: Discover servers from various sources -- **Replication management**: Initialize and create new replications for supported workloads - -## Prerequisites - -- Azure CLI 2.0+ -- Valid Azure subscription -- Appropriate permissions for migration operations -- For Azure Local: Azure Stack HCI environment with proper networking - -## Command Overview - -The Azure CLI migrate module provides the following commands: - -### Server Discovery -```bash -# Get discovered servers -az migrate get-discovered-server --resource-group myRG --project-name myProject -# Create server replication -az migrate server create-replication --resource-group myRG --project-name myProject --target-vm-name myVM --target-resource-group targetRG --target-network targetNet - -# Show replication status -az migrate server show-replication-status --resource-group myRG --project-name myProject --vm-name myVM - -# Update replication properties -az migrate server update-replication --resource-group myRG --project-name myProject --vm-name myVM - -# Check cross-platform environment -az migrate server check-environment -``` -### Azure Local (Stack HCI) Migration Commands -```bash -# Initialize Azure Local replication infrastructure -az migrate local init --resource-group myRG --project-name myProject - -# Create disk mapping for fine-grained control -az migrate local create-disk-mapping --disk-id "disk001" --is-os-disk --size-gb 64 --format-type VHDX - -# Create NIC mapping for network configuration -az migrate local create-nic-mapping --nic-id "nic001" \ - --target-virtual-switch-id "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.AzureStackHCI/logicalnetworks/network001" - -# Create basic replication -az migrate local create-replication --resource-group myRG --project-name myProject \ - --server-index 0 --target-vm-name migratedVM \ - --target-storage-path-id "/subscriptions/xxx/providers/Microsoft.AzureStackHCI/storageContainers/container001" \ - --target-virtual-switch-id "/subscriptions/xxx/providers/Microsoft.AzureStackHCI/logicalnetworks/network001" \ - --target-resource-group-id "/subscriptions/xxx/resourceGroups/targetRG" - -# Create replication with custom disk and NIC mappings -az migrate local create-replication-with-mappings --resource-group myRG --project-name myProject \ - --discovered-machine-id "/subscriptions/xxx/machines/machine001" \ - --target-vm-name migratedVM \ - --target-storage-path-id "/subscriptions/xxx/providers/Microsoft.AzureStackHCI/storageContainers/container001" \ - --target-resource-group-id "/subscriptions/xxx/resourceGroups/targetRG" \ - --disk-mappings '[{"DiskID": "disk001", "IsOSDisk": true, "Size": 64, "Format": "VHDX"}]' \ - --nic-mappings '[{"NicID": "nic001", "TargetVirtualSwitchId": "/subscriptions/xxx/logicalnetworks/network001"}]' - -# Get replication job details -az migrate local get-job --resource-group myRG --project-name myProject --job-id "job-12345" - -# Get Azure Local specific job -az migrate local get-azure-local-job --resource-group myRG --project-name myProject --job-id "job-12345" - -# Start migration (planned failover) -az migrate local start-migration --target-object-id "/subscriptions/xxx/replicationProtectedItems/item001" \ - --turn-off-source-server - -# Remove replication after successful migration -az migrate local remove-replication --target-object-id "/subscriptions/xxx/replicationProtectedItems/item001" -``` - -### Authentication Management -```bash -# Check Azure authentication status -az migrate auth check - -# Login to Azure (interactive) -az migrate auth login - -# Login with device code -az migrate auth login --device-code - -# Login with service principal -az migrate auth login --app-id "app-id" --secret "secret" --tenant-id "tenant-id" - -# Set Azure context -az migrate auth set-context --subscription-id "00000000-0000-0000-0000-000000000000" - -# Show current context -az migrate auth show-context - -# Logout -az migrate auth logout -``` - -### PowerShell Module Management -```bash -# Check PowerShell module availability -az migrate powershell check-module --module-name Az.Migrate - -# Update PowerShell modules -az migrate powershell update-modules --modules Az.Migrate -``` - -## Architecture - -The migration module consists of several key components: - -1. **Cross-Platform PowerShell Integration**: Executes PowerShell cmdlets across Windows, Linux, and macOS -2. **Azure Local Migration**: Specialized support for Azure Stack HCI migration scenarios -3. **Authentication Management**: Azure authentication and context management -4. **Server Discovery and Replication**: Discovery and replication of source machines - -## Common Workflows - -### Setting up Azure Local Migration - -```bash -# 1. Check prerequisites -az migrate check-prerequisites - -# 2. Set up environment with PowerShell -az migrate setup-env --install-powershell - -# 3. Authenticate to Azure -az migrate auth login - -# 4. Set subscription context -az migrate auth set-context --subscription-id "your-subscription-id" - -# 5. Verify setup -az migrate verify-setup --resource-group "migration-rg" --project-name "azure-local-migration" - -# 6. Initialize Azure Local replication infrastructure -az migrate local init \ - --resource-group "migration-rg" \ - --project-name "azure-local-migration" - -# 7. List discovered servers -az migrate server list-discovered \ - --resource-group "migration-rg" \ - --project-name "azure-local-migration" \ - --source-machine-type VMware - -# 8. Create replication for a specific server -az migrate local create-replication \ - --resource-group "migration-rg" \ - --project-name "azure-local-migration" \ - --server-index 0 \ - --target-vm-name "WebServer-Migrated" \ - --target-storage-path-id "/subscriptions/xxx/providers/Microsoft.AzureStackHCI/storageContainers/migration-storage" \ - --target-virtual-switch-id "/subscriptions/xxx/providers/Microsoft.AzureStackHCI/logicalnetworks/migration-network" \ - --target-resource-group-id "/subscriptions/xxx/resourceGroups/azure-local-vms" - -# 9. Monitor replication progress -az migrate local get-job --resource-group "migration-rg" --project-name "azure-local-migration" --job-id "job-id" - -# 10. Start migration when ready -az migrate local start-migration --target-object-id "replication-id" --turn-off-source-server - -# 11. Monitor migration job -az migrate local get-azure-local-job --resource-group "migration-rg" --project-name "azure-local-migration" --job-id "job-id" -``` - -### Setting up Server Discovery and Replication - -```bash -# 1. Check prerequisites and setup -az migrate check-prerequisites -az migrate setup-env --install-powershell - -# 2. Authenticate and set context -az migrate auth login -az migrate auth set-context --subscription-id "your-subscription-id" - -# 3. Verify setup -az migrate verify-setup --resource-group "migration-rg" --project-name "server-migration-2025" - -# 4. List discovered servers -az migrate server list-discovered --resource-group "migration-rg" --project-name "server-migration-2025" --source-machine-type VMware - -# 5. Find specific servers -az migrate server find-by-name --resource-group "migration-rg" --project-name "server-migration-2025" --display-name "WebServer" - -# 6. Create server replication -az migrate server create-replication --resource-group "migration-rg" --project-name "server-migration-2025" --target-vm-name "WebServer-Azure" --target-resource-group "target-rg" --target-network "target-vnet" - -# 7. Monitor replication status -az migrate server show-replication-status --resource-group "migration-rg" --project-name "server-migration-2025" --vm-name "WebServer-Azure" -``` - -## PowerShell Integration - -This module provides Azure CLI equivalents to PowerShell Az.Migrate cmdlets: - -| PowerShell Cmdlet | Azure CLI Command | -|-------------------|-------------------| -| `Initialize-AzMigrateLocalReplicationInfrastructure` | `az migrate local init` | -| `New-AzMigrateLocalServerReplication` | `az migrate local create-replication` | -| `New-AzMigrateLocalDiskMappingObject` | `az migrate local create-disk-mapping` | -| `New-AzMigrateLocalNicMappingObject` | `az migrate local create-nic-mapping` | -| `Start-AzMigrateLocalServerMigration` | `az migrate local start-migration` | -| `Remove-AzMigrateLocalServerReplication` | `az migrate local remove-replication` | -| `Get-AzMigrateLocalJob` | `az migrate local get-azure-local-job` | -| `Get-AzMigrateDiscoveredServer` | `az migrate server list-discovered` | -| `New-AzMigrateServerReplication` | `az migrate server create-replication` | -| `Get-AzMigrateServerReplication` | `az migrate server show-replication-status` | - -## Error Handling - -The module includes comprehensive error handling for: - -- Invalid project configurations -- Permission and authentication issues -- Resource not found scenarios -- Azure service connectivity problems -- PowerShell execution errors -- Cross-platform compatibility issues - -## Troubleshooting - -### Common Issues - -**PowerShell Not Found** -- On Windows: Install PowerShell Core or ensure Windows PowerShell is available -- On Linux/macOS: Install PowerShell Core from https://github.com/PowerShell/PowerShell -- Use `az migrate setup-env --install-powershell` for automatic installation guidance - -**Authentication Issues** -- Use `az migrate auth check` to verify authentication status -- Re-authenticate using `az migrate auth login` -- Verify subscription context with `az migrate auth show-context` - -**Server Discovery Issues** -- Confirm the appliance is properly configured -- Verify network connectivity from appliance to Azure -- Check that discovery is running on the appliance -- Use `az migrate server list-discovered` to check for discovered servers - -**Permission Errors** -- Ensure Azure Migrate Contributor role is assigned -- Verify subscription-level permissions for creating resources -- Check resource group permissions - -**Azure Local Specific Issues** -- Verify Azure Stack HCI cluster is properly registered with Azure -- Ensure proper networking between source and Azure Local target -- Check that both source and target appliances are properly configured -- Verify storage containers and logical networks are properly set up in Azure Local -- Use `az migrate local init` to initialize infrastructure - -**Script Execution Errors** -- Check PowerShell execution policy -- Verify PowerShell module availability using `az migrate powershell check-module` -- Review error messages for specific guidance -- Use `az migrate check-prerequisites` to verify system requirements - -## Contributing - -When extending the migration module: - -1. Follow Azure CLI command naming conventions -2. Implement proper error handling and validation -3. Add comprehensive help documentation -4. Include usage examples in help text -5. Update this README with new command examples -6. Ensure cross-platform PowerShell compatibility -7. Add appropriate parameter validation -8. Include integration tests for new commands - -For more information on Azure Migrate, visit: https://docs.microsoft.com/azure/migrate/ - -## License - -This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/src/migrate/azext_migrate/__init__.py b/src/migrate/azext_migrate/__init__.py deleted file mode 100644 index 943d3ddc823..00000000000 --- a/src/migrate/azext_migrate/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -from azure.cli.core import AzCommandsLoader -from azure.cli.core.profiles import ResourceType - - -class MigrateCommandsLoader(AzCommandsLoader): - - def __init__(self, cli_ctx=None): - from azure.cli.core.commands import CliCommandType - - migrate_custom = CliCommandType( - operations_tmpl='azext_migrate.custom#{}', - ) - - super().__init__( - cli_ctx=cli_ctx, - custom_command_type=migrate_custom, - resource_type=ResourceType.MGMT_MIGRATE - ) - - def load_command_table(self, args): - from azext_migrate.commands \ - import load_command_table - load_command_table(self, args) - return self.command_table - - def load_arguments(self, command): - from azext_migrate._params import load_arguments - load_arguments(self, command) - - -COMMAND_LOADER_CLS = MigrateCommandsLoader diff --git a/src/migrate/azext_migrate/_get_discovered_server_helpers.py b/src/migrate/azext_migrate/_get_discovered_server_helpers.py deleted file mode 100644 index d001e19af4d..00000000000 --- a/src/migrate/azext_migrate/_get_discovered_server_helpers.py +++ /dev/null @@ -1,139 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -from knack.util import CLIError -import json - - -def validate_get_discovered_server_params(project_name, - resource_group_name, - source_machine_type): - """Validate required parameters for get_discovered_server.""" - if not project_name: - raise CLIError("project_name is required.") - if not resource_group_name: - raise CLIError("resource_group_name is required.") - if source_machine_type and source_machine_type not in ["VMware", "HyperV"]: - raise CLIError("source_machine_type is not 'VMware' or 'HyperV'.") - - -def build_base_uri(subscription_id, resource_group_name, project_name, - appliance_name, name, source_machine_type): - """Build the base URI for the API request.""" - if appliance_name and name: - # GetInSite: Get specific machine in specific site - if source_machine_type == "HyperV": - return (f"/subscriptions/{subscription_id}" - f"/resourceGroups/{resource_group_name}/" - f"providers/Microsoft.OffAzure/HyperVSites" - f"/{appliance_name}/machines/{name}") - # VMware or default - return (f"/subscriptions/{subscription_id}" - f"/resourceGroups/{resource_group_name}/" - f"providers/Microsoft.OffAzure/VMwareSites" - f"/{appliance_name}/machines/{name}") - - if appliance_name: - # ListInSite: List machines in specific site - if source_machine_type == "HyperV": - return (f"/subscriptions/{subscription_id}" - f"/resourceGroups/{resource_group_name}/" - f"providers/Microsoft.OffAzure/HyperVSites" - f"/{appliance_name}/machines") - # VMware or default - return (f"/subscriptions/{subscription_id}" - f"/resourceGroups/{resource_group_name}/" - f"providers/Microsoft.OffAzure" - f"/VMwareSites/{appliance_name}/machines") - - if name: - # Get: Get specific machine from project - return (f"/subscriptions/{subscription_id}" - f"/resourceGroups/{resource_group_name}/" - f"providers/Microsoft.Migrate/migrateprojects" - f"/{project_name}/machines/{name}") - - # List: List all machines in project - return (f"/subscriptions/{subscription_id}" - f"/resourceGroups/{resource_group_name}/" - f"providers/Microsoft.Migrate/migrateprojects" - f"/{project_name}/machines") - - -def fetch_all_servers(cmd, request_uri, send_get_request): - """Fetch all servers including paginated results.""" - response = send_get_request(cmd, request_uri) - data = response.json() - values = data.get('value', []) - - while data.get('nextLink'): - response = send_get_request(cmd, data.get('nextLink')) - data = response.json() - values += data.get('value', []) - - return values - - -def filter_servers_by_display_name(servers, display_name): - """Filter servers by display name.""" - filtered = [] - for server in servers: - properties = server.get('properties', {}) - if properties.get('displayName', '') == display_name: - filtered.append(server) - return filtered - - -def extract_server_info(server, index): - """Extract server information from discovery data.""" - properties = server.get('properties', {}) - discovery_data = properties.get('discoveryData', []) - - # Default values - machine_name = "N/A" - ip_addresses_str = 'N/A' - os_name = "N/A" - boot_type = "N/A" - os_disk_id = "N/A" - - if discovery_data: - latest_discovery = discovery_data[0] - machine_name = latest_discovery.get('machineName', 'N/A') - ip_addresses = latest_discovery.get('ipAddresses', []) - ip_addresses_str = ', '.join(ip_addresses) if ip_addresses else 'N/A' - os_name = latest_discovery.get('osName', 'N/A') - - extended_info = latest_discovery.get('extendedInfo', {}) - boot_type = extended_info.get('bootType', 'N/A') - - disk_details_json = extended_info.get('diskDetails', '[]') - disk_details = json.loads(disk_details_json) - if disk_details: - os_disk_id = disk_details[0].get("InstanceId", "N/A") - - return { - 'index': index, - 'machine_name': machine_name, - 'ip_addresses': ip_addresses_str, - 'operating_system': os_name, - 'boot_type': boot_type, - 'os_disk_id': os_disk_id - } - - -def print_server_info(server_info): - """Print formatted server information.""" - index_str = f"[{server_info['index']}]" - print(f"{index_str} Machine Name: " - f"{server_info['machine_name']}") - print(f"{' ' * len(index_str)} IP Addresses: " - f"{server_info['ip_addresses']}") - print(f"{' ' * len(index_str)} Operating System: " - f"{server_info['operating_system']}") - print(f"{' ' * len(index_str)} Boot Type: " - f"{server_info['boot_type']}") - print(f"{' ' * len(index_str)} OS Disk ID: " - f"{server_info['os_disk_id']}") - print() diff --git a/src/migrate/azext_migrate/_help.py b/src/migrate/azext_migrate/_help.py deleted file mode 100644 index 100dc4d7535..00000000000 --- a/src/migrate/azext_migrate/_help.py +++ /dev/null @@ -1,305 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- -# coding=utf-8 -from knack.help_files import helps # pylint: disable=unused-import - - -helps['migrate'] = """ - type: group - short-summary: Manage Azure Migrate resources and operations. - long-summary: | - Commands to manage Azure Migrate projects, - discover servers, and perform migrations - to Azure and Azure Local/Stack HCI environments. -""" - -helps['migrate local'] = """ - type: group - short-summary: Manage Azure Local/Stack HCI migration operations. - long-summary: | - Commands to manage server discovery - and replication for migrations to Azure Local - and Azure Stack HCI environments. - These commands support VMware and Hyper-V source - environments. -""" - -helps['migrate local get-discovered-server'] = """ - type: command - short-summary: Retrieve discovered servers from an Azure Migrate project. - long-summary: | - Get information about servers discovered by Azure Migrate appliances. - You can list all discovered servers in a project, - filter by display name or machine type, - or get a specific server by name. - This command supports both VMware and Hyper-V environments. - parameters: - - name: --project-name - short-summary: Name of the Azure Migrate project. - long-summary: > - The Azure Migrate project that contains - the discovered servers. - - name: --display-name - short-summary: Display name of the source machine to filter by. - long-summary: > - Filter discovered servers by their display name - (partial match supported). - - name: --source-machine-type - short-summary: Type of the source machine. - long-summary: > - Filter by source machine type. Valid values are - 'VMware' or 'HyperV'. - - name: --subscription-id - short-summary: Azure subscription ID. - long-summary: > - The subscription containing the Azure Migrate project. - Uses the default subscription if not specified. - - name: --name - short-summary: Internal name of the specific source machine. - long-summary: > - The internal machine name assigned by Azure Migrate - (different from display name). - - name: --appliance-name - short-summary: Name of the appliance (site) containing the machines. - long-summary: > - Filter servers discovered by - a specific Azure Migrate appliance. - examples: - - name: List all discovered servers in a project - text: | - az migrate local get-discovered-server \\ - --project-name myMigrateProject \\ - --resource-group-name myRG - - name: Get a specific discovered server by name - text: | - az migrate local get-discovered-server \\ - --project-name myMigrateProject \\ - --resource-group-name myRG \\ - --name machine-12345 - - name: Filter discovered servers by display name - text: | - az migrate local get-discovered-server \\ - --project-name myMigrateProject \\ - --resource-group-name myRG \\ - --display-name "web-server" - - name: List VMware servers discovered by a specific appliance - text: | - az migrate local get-discovered-server \\ - --project-name myMigrateProject \\ - --resource-group-name myRG \\ - --appliance-name myVMwareAppliance \\ - --source-machine-type VMware - - name: Get a specific server from a specific appliance - text: | - az migrate local get-discovered-server \\ - --project-name myMigrateProject \\ - --resource-group-name myRG \\ - --appliance-name myAppliance \\ - --name machine-12345 \\ - --source-machine-type HyperV -""" - -helps['migrate local replication'] = """ - type: group - short-summary: Manage replication for Azure Local/Stack HCI migrations. - long-summary: | - Commands to initialize replication infrastructure - and create new server replications - for migrations to Azure Local and Azure Stack HCI environments. -""" - -helps['migrate local replication init'] = """ - type: command - short-summary: Initialize Azure Migrate local replication infrastructure. - long-summary: | - Initialize the replication infrastructure required for - migrating servers to Azure Local or Azure Stack HCI. - This command sets up the necessary fabrics, policies, and mappings - between source and target appliances. - This is a prerequisite before creating any server replications. - - Note: This command uses a preview API version and - may experience breaking changes in future releases. - parameters: - - name: --project-name - short-summary: Name of the Azure Migrate project. - long-summary: > - The Azure Migrate project to be used - for server migration. - - name: --source-appliance-name - short-summary: Source appliance name. - long-summary: > - Name of the Azure Migrate appliance that - discovered the source servers. - - name: --target-appliance-name - short-summary: Target appliance name. - long-summary: > - Name of the Azure Local appliance that - will host the migrated servers. - - name: --subscription-id - short-summary: Azure subscription ID. - long-summary: > - The subscription containing the Azure Migrate project. - Uses the current subscription if not specified. - - name: --pass-thru - short-summary: Return true when the command succeeds. - long-summary: > - When enabled, returns a boolean value - indicating successful completion. - examples: - - name: Initialize replication infrastructure - text: | - az migrate local replication init \\ - --resource-group-name myRG \\ - --project-name myMigrateProject \\ - --source-appliance-name myVMwareAppliance \\ - --target-appliance-name myAzStackHCIAppliance - - name: Initialize and return success status - text: | - az migrate local replication init \\ - --resource-group-name myRG \\ - --project-name myMigrateProject \\ - --source-appliance-name mySourceAppliance \\ - --target-appliance-name myTargetAppliance \\ - --pass-thru -""" - -helps['migrate local replication new'] = """ - type: command - short-summary: Create a new replication for an Azure Local server. - long-summary: | - Create a new replication to migrate a discovered server to Azure Local. - You can specify the source machine either - by its ARM resource ID or by selecting it from - a numbered list of discovered servers. - - The command supports two modes: - - Default User Mode: Specify os-disk-id and target-virtual-switch-id - - Power User Mode: Specify disk-to-include and nic-to-include - - Note: This command uses a preview API version - and may experience breaking changes in - future releases. - parameters: - - name: --machine-id - short-summary: ARM resource ID of the discovered server to migrate. - long-summary: > - Full ARM resource ID of the discovered machine. - Required if --machine-index is not provided. - - name: --machine-index - short-summary: Index of the discovered server from the list - long-summary: > - Select a server by its position - in the discovered servers list. - Required if --machine-id is not provided. - - name: --project-name - short-summary: Name of the Azure Migrate project. - long-summary: > - Required when using --machine-index - to identify which project to query. - - name: --target-storage-path-id - short-summary: Storage path ARM ID where VMs will be stored. - long-summary: > - Full ARM resource ID of the storage path - on the target Azure Local cluster. - - name: --target-vm-cpu-core - short-summary: Number of CPU cores for the target VM. - long-summary: > - Specify the number of CPU cores - to allocate to the migrated VM. - - name: --target-vm-ram - short-summary: Target RAM size in MB. - long-summary: > - Specify the amount of RAM to - allocate to the target VM in megabytes. - - name: --disk-to-include - short-summary: Disks to include for replication (power user mode). - long-summary: > - Space-separated list of disk IDs - to replicate from the source server. - Use this for power user mode. - - name: --nic-to-include - short-summary: NICs to include for replication (power user mode). - long-summary: > - Space-separated list of NIC IDs - to replicate from the source server. - Use this for power user mode. - - name: --vm-name - short-summary: Name of the VM to be created. - long-summary: > - The name for the virtual machine - that will be created on the target environment. - - name: --os-disk-id - short-summary: Operating system disk ID. - long-summary: > - ID of the operating system disk for - the source server. Required for default user mode. - - name: --source-appliance-name - short-summary: Source appliance name. - long-summary: > - Name of the Azure Migrate appliance - that discovered the source server. - - name: --target-appliance-name - short-summary: Target appliance name. - long-summary: > - Name of the Azure Local appliance - that will host the migrated server. - - name: --subscription-id - short-summary: Azure subscription ID. - long-summary: > - The subscription to use. - Uses the current subscription if not specified. - examples: - - name: Create replication using machine ARM ID (default user mode) - text: | - az migrate local replication new \\ - --machine-id "XXXX" \\ - --target-storage-path-id "YYYY" \\ - --target-resource-group-id "ZZZZ" \\ - --target-vm-name migratedVM01 \\ - --source-appliance-name myVMwareAppliance \\ - --target-appliance-name myAzStackHCIAppliance \\ - --target-virtual-switch-id "XYXY" \\ - --os-disk-id "disk-0" - - name: Create replication using machine index (power user mode) - text: | - az migrate local replication new \\ - --machine-index 1 \\ - --project-name myMigrateProject \\ - --resource-group-name myRG \\ - --target-storage-path-id "XZXZ" \\ - --target-resource-group-id "YZYZ" \\ - --target-vm-name migratedVM01 \\ - --source-appliance-name mySourceAppliance \\ - --target-appliance-name myTargetAppliance \\ - --disk-to-include "disk-0" "disk-1" \\ - --nic-to-include "nic-0" - - name: Create replication with custom CPU and RAM settings - text: | - az migrate local replication new \\ - --machine-id "XXXX" \\ - --target-storage-path-id "YYYY" \\ - --target-resource-group-id "ZZZZ" \\ - --target-vm-name migratedVM01 \\ - --source-appliance-name mySourceAppliance \\ - --target-appliance-name myTargetAppliance \\ - --target-virtual-switch-id "XYXY" \\ - --os-disk-id "disk-0" \\ - --target-vm-cpu-core 4 \\ - --target-vm-ram 8192 \\ - --is-dynamic-memory-enabled false - - name: Create replication with test virtual switch - text: | - az migrate local replication new \\ - --machine-id "XXXX" \\ - --target-storage-path-id "YYYY" \\ - --target-resource-group-id "ZZZZ" \\ - --target-vm-name migratedVM01 \\ - --source-appliance-name mySourceAppliance \\ - --target-appliance-name myTargetAppliance \\ - --target-virtual-switch-id "XYXY" \\ - --target-test-virtual-switch-id "XYXY" \\ - --os-disk-id "disk-0" -""" diff --git a/src/migrate/azext_migrate/_helpers.py b/src/migrate/azext_migrate/_helpers.py deleted file mode 100644 index d06c75fc600..00000000000 --- a/src/migrate/azext_migrate/_helpers.py +++ /dev/null @@ -1,286 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- -import hashlib -from enum import Enum -from knack.util import CLIError -from knack.log import get_logger -from azure.cli.core.util import send_raw_request - -logger = get_logger(__name__) - - -class APIVersion(Enum): - Microsoft_Authorization = "2022-04-01" - Microsoft_ResourceGraph = "2021-03-01" - Microsoft_DataReplication = "2024-09-01" - Microsoft_Resources = "2021-04-01" - Microsoft_OffAzure = "2023-06-06" - Microsoft_Storage = "2023-05-01" - Microsoft_Migrate = "2020-05-01" - Microsoft_HybridCompute = "2024-07-10" - - -class ProvisioningState(Enum): - Succeeded = "Succeeded" - Creating = "Creating" - Updating = "Updating" - Deleting = "Deleting" - Deleted = "Deleted" - Failed = "Failed" - Canceled = "Canceled" - - -class StorageAccountProvisioningState(Enum): - Succeeded = "Succeeded" - Creating = "Creating" - ResolvingDNS = "ResolvingDNS" - - -class AzLocalInstanceTypes(Enum): - HyperVToAzLocal = "HyperVToAzStackHCI" - VMwareToAzLocal = "VMwareToAzStackHCI" - - -class FabricInstanceTypes(Enum): - HyperVInstance = "HyperVMigrate" - VMwareInstance = "VMwareMigrate" - AzLocalInstance = "AzStackHCI" - - -class SiteTypes(Enum): - HyperVSites = "HyperVSites" - VMwareSites = "VMwareSites" - - -class VMNicSelection(Enum): - SelectedByDefault = "SelectedByDefault" - SelectedByUser = "SelectedByUser" - NotSelected = "NotSelected" - - -# pylint: disable=too-few-public-methods -class IdFormats: - """Container for ARM resource ID format templates.""" - MachineArmIdTemplate = ( - "/subscriptions/{subscriptionId}/resourceGroups/" - "{resourceGroupName}/providers/Microsoft.OffAzure/{siteType}/" - "{siteName}/machines/{machineName}" - ) - StoragePathArmIdTemplate = ( - "/subscriptions/{subscriptionId}/resourceGroups/" - "{resourceGroupName}/providers/Microsoft.AzureStackHCI/" - "storagecontainers/{storagePathName}" - ) - ResourceGroupArmIdTemplate = ( - "/subscriptions/{subscriptionId}/resourceGroups/" - "{resourceGroupName}" - ) - LogicalNetworkArmIdTemplate = ( - "/subscriptions/{subscriptionId}/resourceGroups/" - "{resourceGroupName}/providers/Microsoft.AzureStackHCI/" - "logicalnetworks/{logicalNetworkName}" - ) - - -# pylint: disable=too-few-public-methods -class RoleDefinitionIds: - """Container for Azure role definition IDs.""" - ContributorId = "b24988ac-6180-42a0-ab88-20f7382dd24c" - StorageBlobDataContributorId = "ba92f5b4-2d11-453d-a403-e96b0029c9fe" - - -class ReplicationPolicyDetails(Enum): - RecoveryPointHistoryInMinutes = 4320 # 72 hours - CrashConsistentFrequencyInMinutes = 60 # 1 hour - AppConsistentFrequencyInMinutes = 240 # 4 hours - - -def send_get_request(cmd, request_uri): - """ - Make a GET API call and handle errors properly. - """ - response = send_raw_request( - cmd.cli_ctx, - method='GET', - url=request_uri, - ) - - if response.status_code >= 400: - error_message = f"Status: {response.status_code}" - try: - error_body = response.json() - if 'error' in error_body: - error_details = error_body['error'] - error_code = error_details.get('code', 'Unknown') - error_msg = error_details.get('message', 'No message provided') - raise CLIError(f"{error_code}: {error_msg}") - except (ValueError, KeyError): - error_message += f", Response: {response.text}" - raise CLIError(error_message) - return response - - -def generate_hash_for_artifact(artifact): - """Generate a hash for the given artifact string.""" - hash_object = hashlib.sha256(artifact.encode()) - hex_dig = hash_object.hexdigest() - # Convert to numeric hash similar to PowerShell GetHashCode - numeric_hash = int(hex_dig[:8], 16) - return str(numeric_hash) - - -def get_resource_by_id(cmd, resource_id, api_version): - """Get an Azure resource by its ARM ID.""" - uri = f"{resource_id}?api-version={api_version}" - request_uri = cmd.cli_ctx.cloud.endpoints.resource_manager + uri - - response = send_raw_request( - cmd.cli_ctx, - method='GET', - url=request_uri, - ) - - # Return None for 404 Not Found - if response.status_code == 404: - return None - - # Raise error for other non-success status codes - if response.status_code >= 400: - error_message = ( - f"Failed to get resource. Status: {response.status_code}") - try: - error_body = response.json() - if 'error' in error_body: - error_details = error_body['error'] - error_code = error_details.get('code', 'Unknown') - error_msg = ( - error_details.get('message', 'No message provided')) - - # For specific error codes, provide more helpful messages - if error_code == "ResourceGroupNotFound": - rg_parts = resource_id.split('/') - rg_name = ( - rg_parts[4] if len(rg_parts) > 4 else 'unknown') - raise CLIError( - f"Resource group '{rg_name}' does not exist. " - "Please create it first or check the subscription." - ) - if error_code == "ResourceNotFound": - raise CLIError(f"Resource not found: {error_msg}") - - raise CLIError(f"{error_code}: {error_msg}") - except (ValueError, KeyError) as e: - if not isinstance(e, CLIError): - error_message += f", Response: {response.text}" - raise CLIError(error_message) - raise - - return response.json() - - -def create_or_update_resource(cmd, resource_id, api_version, properties): - """Create or update an Azure resource. - - Args: - cmd: Command context - resource_id: Resource ID - api_version: API version - properties: Resource properties - no_wait: If True, does not wait for operation to complete - (reserved for future use) - """ - import json as json_module - - uri = f"{resource_id}?api-version={api_version}" - request_uri = cmd.cli_ctx.cloud.endpoints.resource_manager + uri - # Convert properties to JSON string for the body - body = json_module.dumps(properties) - - # Headers need to be passed as a list of strings in "key=value" format - headers = ['Content-Type=application/json'] - - response = send_raw_request( - cmd.cli_ctx, - method='PUT', - url=request_uri, - body=body, - headers=headers - ) - - if response.status_code >= 400: - error_message = ( - f"Failed to create/update resource. " - f"Status: {response.status_code}") - try: - error_body = response.json() - if 'error' in error_body: - error_details = error_body['error'] - error_code = error_details.get('code', 'Unknown') - error_msg = error_details.get('message', 'No message provided') - raise CLIError(f"{error_code}: {error_msg}") - except (ValueError, KeyError): - error_message += f", Response: {response.text}" - raise CLIError(error_message) - - # Handle empty response for async operations (202 status code) - if (response.status_code == 202 or not response.text or - response.text.strip() == ''): - return None - - try: - return response.json() - except (ValueError, json_module.JSONDecodeError): - # If we can't parse JSON, return None - return None - - -def delete_resource(cmd, resource_id, api_version): - """Delete an Azure resource.""" - uri = f"{resource_id}?api-version={api_version}" - request_uri = cmd.cli_ctx.cloud.endpoints.resource_manager + uri - - response = send_raw_request( - cmd.cli_ctx, - method='DELETE', - url=request_uri, - ) - - return response.status_code < 400 - - -def validate_arm_id_format(arm_id, template): - """ - Validate if an ARM ID matches the expected template format. - - Args: - arm_id (str): The ARM ID to validate - template (str): The template format to match against - - Returns: - bool: True if the ARM ID matches the template format - """ - import re - - if not arm_id or not arm_id.startswith('/'): - return False - - # Convert template to regex pattern - # Replace {variableName} with a pattern that matches valid Azure - # resource names - pattern = template - pattern = pattern.replace( - '{subscriptionId}', - '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') - pattern = pattern.replace('{resourceGroupName}', '[a-zA-Z0-9._-]+') - pattern = pattern.replace('{siteType}', '(HyperVSites|VMwareSites)') - pattern = pattern.replace('{siteName}', '[a-zA-Z0-9._-]+') - pattern = pattern.replace('{machineName}', '[a-zA-Z0-9._-]+') - pattern = pattern.replace('{storagePathName}', '[a-zA-Z0-9._-]+') - pattern = pattern.replace('{logicalNetworkName}', '[a-zA-Z0-9._-]+') - - # Make the pattern case-insensitive and match the whole string - pattern = f'^{pattern}$' - - return bool(re.match(pattern, arm_id, re.IGNORECASE)) diff --git a/src/migrate/azext_migrate/_initialize_replication_infrastructure_helpers.py b/src/migrate/azext_migrate/_initialize_replication_infrastructure_helpers.py deleted file mode 100644 index 68055b3265f..00000000000 --- a/src/migrate/azext_migrate/_initialize_replication_infrastructure_helpers.py +++ /dev/null @@ -1,1556 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -import time -from knack.util import CLIError -from knack.log import get_logger -from azext_migrate._helpers import ( - send_get_request, - get_resource_by_id, - delete_resource, - create_or_update_resource, - generate_hash_for_artifact, - APIVersion, - ProvisioningState, - AzLocalInstanceTypes, - FabricInstanceTypes, - ReplicationPolicyDetails, - RoleDefinitionIds, - StorageAccountProvisioningState -) -import json - - -def validate_required_parameters(resource_group_name, - project_name, - source_appliance_name, - target_appliance_name): - # Validate required parameters - if not resource_group_name: - raise CLIError("resource_group_name is required.") - if not project_name: - raise CLIError("project_name is required.") - if not source_appliance_name: - raise CLIError("source_appliance_name is required.") - if not target_appliance_name: - raise CLIError("target_appliance_name is required.") - - -def get_and_validate_resource_group(cmd, subscription_id, - resource_group_name): - """Get and validate that the resource group exists.""" - rg_uri = (f"/subscriptions/{subscription_id}/" - f"resourceGroups/{resource_group_name}") - resource_group = get_resource_by_id( - cmd, rg_uri, APIVersion.Microsoft_Resources.value) - if not resource_group: - raise CLIError( - f"Resource group '{resource_group_name}' does not exist " - f"in the subscription.") - print(f"Selected Resource Group: '{resource_group_name}'") - return rg_uri - - -def get_migrate_project(cmd, project_uri, project_name): - """Get and validate migrate project.""" - migrate_project = get_resource_by_id( - cmd, project_uri, APIVersion.Microsoft_Migrate.value) - if not migrate_project: - raise CLIError(f"Migrate project '{project_name}' not found.") - - if (migrate_project.get('properties', {}).get('provisioningState') != - ProvisioningState.Succeeded.value): - raise CLIError( - f"Migrate project '{project_name}' is not in a valid state.") - - return migrate_project - - -def get_data_replication_solution(cmd, project_uri): - """Get Data Replication Service Solution.""" - amh_solution_name = ( - "Servers-Migration-ServerMigration_DataReplication") - amh_solution_uri = f"{project_uri}/solutions/{amh_solution_name}" - amh_solution = get_resource_by_id( - cmd, amh_solution_uri, APIVersion.Microsoft_Migrate.value) - if not amh_solution: - raise CLIError( - f"No Data Replication Service Solution " - f"'{amh_solution_name}' found.") - return amh_solution - - -def get_discovery_solution(cmd, project_uri): - """Get Discovery Solution.""" - discovery_solution_name = "Servers-Discovery-ServerDiscovery" - discovery_solution_uri = ( - f"{project_uri}/solutions/{discovery_solution_name}") - discovery_solution = get_resource_by_id( - cmd, discovery_solution_uri, APIVersion.Microsoft_Migrate.value) - if not discovery_solution: - raise CLIError( - f"Server Discovery Solution '{discovery_solution_name}' " - f"not found.") - return discovery_solution - - -def get_and_setup_replication_vault(cmd, amh_solution, rg_uri): - """Get and setup replication vault with managed identity.""" - # Validate Replication Vault - vault_id = (amh_solution.get('properties', {}) - .get('details', {}) - .get('extendedDetails', {}) - .get('vaultId')) - if not vault_id: - raise CLIError( - "No Replication Vault found. Please verify your " - "Azure Migrate project setup.") - - replication_vault_name = vault_id.split("/")[8] - vault_uri = ( - f"{rg_uri}/providers/Microsoft.DataReplication/" - f"replicationVaults/{replication_vault_name}") - replication_vault = get_resource_by_id( - cmd, vault_uri, APIVersion.Microsoft_DataReplication.value) - if not replication_vault: - raise CLIError( - f"No Replication Vault '{replication_vault_name}' found.") - - # Check if vault has managed identity, if not, enable it - vault_identity = ( - replication_vault.get('identity') or - replication_vault.get('properties', {}).get('identity') - ) - if not vault_identity or not vault_identity.get('principalId'): - print( - f"Replication vault '{replication_vault_name}' does not " - f"have a managed identity. " - "Enabling system-assigned identity..." - ) - - # Update vault to enable system-assigned managed identity - vault_update_body = { - "identity": { - "type": "SystemAssigned" - } - } - - replication_vault = create_or_update_resource( - cmd, vault_uri, APIVersion.Microsoft_DataReplication.value, - vault_update_body - ) - - # Wait for identity to be created - time.sleep(30) - - # Refresh vault to get the identity - replication_vault = get_resource_by_id( - cmd, vault_uri, APIVersion.Microsoft_DataReplication.value) - vault_identity = ( - replication_vault.get('identity') or - replication_vault.get('properties', {}).get('identity') - ) - - if not vault_identity or not vault_identity.get('principalId'): - raise CLIError( - f"Failed to enable managed identity for replication " - f"vault '{replication_vault_name}'") - - print( - f"✓ Enabled system-assigned managed identity. " - f"Principal ID: {vault_identity.get('principalId')}" - ) - else: - print( - f"✓ Replication vault has managed identity. " - f"Principal ID: {vault_identity.get('principalId')}") - - return replication_vault, replication_vault_name - - -def _store_appliance_site_mapping(app_map, appliance_name, site_id): - """Store appliance name to site ID mapping in both lowercase and - original case.""" - app_map[appliance_name.lower()] = site_id - app_map[appliance_name] = site_id - - -def _process_v3_dict_map(app_map, app_map_v3): - """Process V3 appliance map in dict format.""" - for appliance_name_key, site_info in app_map_v3.items(): - if isinstance(site_info, dict) and 'SiteId' in site_info: - _store_appliance_site_mapping( - app_map, appliance_name_key, site_info['SiteId']) - elif isinstance(site_info, str): - _store_appliance_site_mapping( - app_map, appliance_name_key, site_info) - - -def _process_v3_list_item(app_map, item): - """Process a single item from V3 appliance list.""" - if not isinstance(item, dict): - return - - # Check if it has ApplianceName/SiteId structure - if 'ApplianceName' in item and 'SiteId' in item: - _store_appliance_site_mapping( - app_map, item['ApplianceName'], item['SiteId']) - return - - # Or it might be a single key-value pair - for key, value in item.items(): - if isinstance(value, dict) and 'SiteId' in value: - _store_appliance_site_mapping( - app_map, key, value['SiteId']) - elif isinstance(value, str): - _store_appliance_site_mapping(app_map, key, value) - - -def _process_v3_appliance_map(app_map, app_map_v3): - """Process V3 appliance map data structure.""" - if isinstance(app_map_v3, dict): - _process_v3_dict_map(app_map, app_map_v3) - elif isinstance(app_map_v3, list): - for item in app_map_v3: - _process_v3_list_item(app_map, item) - - -def parse_appliance_mappings(discovery_solution): - """Parse appliance name to site ID mappings from discovery solution.""" - app_map = {} - extended_details = (discovery_solution.get('properties', {}) - .get('details', {}) - .get('extendedDetails', {})) - - # Process applianceNameToSiteIdMapV2 - if 'applianceNameToSiteIdMapV2' in extended_details: - try: - app_map_v2 = json.loads( - extended_details['applianceNameToSiteIdMapV2']) - if isinstance(app_map_v2, list): - for item in app_map_v2: - if (isinstance(item, dict) and - 'ApplianceName' in item and - 'SiteId' in item): - # Store both lowercase and original case - app_map[item['ApplianceName'].lower()] = ( - item['SiteId']) - app_map[item['ApplianceName']] = item['SiteId'] - except (json.JSONDecodeError, KeyError, TypeError) as e: - get_logger(__name__).warning( - "Failed to parse applianceNameToSiteIdMapV2: %s", str(e)) - - # Process applianceNameToSiteIdMapV3 - if 'applianceNameToSiteIdMapV3' in extended_details: - try: - app_map_v3 = json.loads( - extended_details['applianceNameToSiteIdMapV3']) - _process_v3_appliance_map(app_map, app_map_v3) - except (json.JSONDecodeError, KeyError, TypeError) as e: - get_logger(__name__).warning( - "Failed to parse applianceNameToSiteIdMapV3: %s", str(e)) - - if not app_map: - raise CLIError( - "Server Discovery Solution missing Appliance Details. " - "Invalid Solution.") - - return app_map - - -def validate_and_get_site_ids(app_map, source_appliance_name, - target_appliance_name): - """Validate appliance names and get their site IDs.""" - # Validate SourceApplianceName & TargetApplianceName - try both - # original and lowercase - source_site_id = (app_map.get(source_appliance_name) or - app_map.get(source_appliance_name.lower())) - target_site_id = (app_map.get(target_appliance_name) or - app_map.get(target_appliance_name.lower())) - - if not source_site_id: - # Provide helpful error message with available appliances - # (filter out duplicates) - available_appliances = list(set(k for k in app_map - if k not in app_map or - not k.islower())) - if not available_appliances: - # If all keys are lowercase, show them - available_appliances = list(set(app_map.keys())) - raise CLIError( - f"Source appliance '{source_appliance_name}' not in " - f"discovery solution. " - f"Available appliances: {','.join(available_appliances)}" - ) - if not target_site_id: - # Provide helpful error message with available appliances - # (filter out duplicates) - available_appliances = list(set(k for k in app_map - if k not in app_map or - not k.islower())) - if not available_appliances: - # If all keys are lowercase, show them - available_appliances = list(set(app_map.keys())) - raise CLIError( - f"Target appliance '{target_appliance_name}' not in " - f"discovery solution. " - f"Available appliances: {','.join(available_appliances)}" - ) - - return source_site_id, target_site_id - - -def determine_instance_types(source_site_id, target_site_id, - source_appliance_name, - target_appliance_name): - """Determine instance types based on site IDs.""" - hyperv_site_pattern = "/Microsoft.OffAzure/HyperVSites/" - vmware_site_pattern = "/Microsoft.OffAzure/VMwareSites/" - - if (hyperv_site_pattern in source_site_id and - hyperv_site_pattern in target_site_id): - instance_type = AzLocalInstanceTypes.HyperVToAzLocal.value - fabric_instance_type = FabricInstanceTypes.HyperVInstance.value - elif (vmware_site_pattern in source_site_id and - hyperv_site_pattern in target_site_id): - instance_type = AzLocalInstanceTypes.VMwareToAzLocal.value - fabric_instance_type = FabricInstanceTypes.VMwareInstance.value - else: - src_type = ( - 'VMware' if vmware_site_pattern in source_site_id - else 'HyperV' if hyperv_site_pattern in source_site_id - else 'Unknown' - ) - tgt_type = ( - 'VMware' if vmware_site_pattern in target_site_id - else 'HyperV' if hyperv_site_pattern in target_site_id - else 'Unknown' - ) - raise CLIError( - f"Error matching source '{source_appliance_name}' and target " - f"'{target_appliance_name}' appliances. Source is {src_type}, " - f"Target is {tgt_type}" - ) - - return instance_type, fabric_instance_type - - -def find_fabric(all_fabrics, appliance_name, fabric_instance_type, - amh_solution, is_source=True): - """Find and validate a fabric for the given appliance.""" - logger = get_logger(__name__) - fabric = None - fabric_candidates = [] - - for candidate in all_fabrics: - props = candidate.get('properties', {}) - custom_props = props.get('customProperties', {}) - fabric_name = candidate.get('name', '') - - # Check if this fabric matches our criteria - is_succeeded = (props.get('provisioningState') == - ProvisioningState.Succeeded.value) - - # Check solution ID match - handle case differences and trailing - # slashes - fabric_solution_id = (custom_props.get('migrationSolutionId', '') - .rstrip('/')) - expected_solution_id = amh_solution.get('id', '').rstrip('/') - is_correct_solution = (fabric_solution_id.lower() == - expected_solution_id.lower()) - - is_correct_instance = (custom_props.get('instanceType') == - fabric_instance_type) - - # Check if fabric name contains appliance name or vice versa - name_matches = ( - fabric_name.lower().startswith(appliance_name.lower()) or - appliance_name.lower() in fabric_name.lower() or - fabric_name.lower() in appliance_name.lower() or - f"{appliance_name.lower()}-" in fabric_name.lower() - ) - - # Collect potential candidates even if they don't fully match - if custom_props.get('instanceType') == fabric_instance_type: - fabric_candidates.append({ - 'name': fabric_name, - 'state': props.get('provisioningState'), - 'solution_match': is_correct_solution, - 'name_match': name_matches - }) - - if is_succeeded and is_correct_instance and name_matches: - # If solution doesn't match, log warning but still consider it - if not is_correct_solution: - logger.warning( - "Fabric '%s' matches name and type but has " - "different solution ID", fabric_name) - fabric = candidate - break - - if not fabric: - appliance_type_label = "source" if is_source else "target" - error_msg = ( - f"Couldn't find connected {appliance_type_label} appliance " - f"'{appliance_name}'.\n") - - if fabric_candidates: - error_msg += ( - f"Found {len(fabric_candidates)} fabric(s) with " - f"matching type '{fabric_instance_type}': \n") - for candidate in fabric_candidates: - error_msg += ( - f" - {candidate['name']} " - f"(state: {candidate['state']}, " - f"solution_match: {candidate['solution_match']}, " - f"name_match: {candidate['name_match']})\n") - error_msg += "\nPlease verify:\n" - error_msg += "1. The appliance name matches exactly\n" - error_msg += "2. The fabric is in 'Succeeded' state\n" - error_msg += ( - "3. The fabric belongs to the correct migration solution") - else: - error_msg += ( - f"No fabrics found with instance type " - f"'{fabric_instance_type}'.\n") - error_msg += "\nThis usually means:\n" - error_msg += ( - f"1. The {appliance_type_label} appliance " - f"'{appliance_name}' is not properly configured\n") - if (fabric_instance_type == - FabricInstanceTypes.VMwareInstance.value): - appliance_type = 'VMware' - elif (fabric_instance_type == - FabricInstanceTypes.HyperVInstance.value): - appliance_type = 'HyperV' - else: - appliance_type = 'Azure Local' - error_msg += ( - f"2. The appliance type doesn't match " - f"(expecting {appliance_type})\n") - error_msg += ( - "3. The fabric creation is still in progress - " - "wait a few minutes and retry") - - if all_fabrics: - error_msg += "\n\nAvailable fabrics in resource group:\n" - for fab in all_fabrics: - props = fab.get('properties', {}) - custom_props = props.get('customProperties', {}) - error_msg += ( - f" - {fab.get('name')} " - f"(type: {custom_props.get('instanceType')})\n") - - raise CLIError(error_msg) - - return fabric - - -def get_fabric_agent(cmd, replication_fabrics_uri, fabric, appliance_name, - fabric_instance_type): - """Get and validate fabric agent (DRA) for the given fabric.""" - fabric_name = fabric.get('name') - dras_uri = ( - f"{replication_fabrics_uri}/{fabric_name}" - f"/fabricAgents?api-version=" - f"{APIVersion.Microsoft_DataReplication.value}" - ) - dras_response = send_get_request(cmd, dras_uri) - dras = dras_response.json().get('value', []) - - dra = None - for candidate in dras: - props = candidate.get('properties', {}) - custom_props = props.get('customProperties', {}) - if (props.get('machineName') == appliance_name and - custom_props.get('instanceType') == fabric_instance_type and - bool(props.get('isResponsive'))): - dra = candidate - break - - if not dra: - raise CLIError( - f"The appliance '{appliance_name}' is in a disconnected state." - ) - - return dra - - -def setup_replication_policy(cmd, - rg_uri, - replication_vault_name, - instance_type): - """Setup or validate replication policy.""" - policy_name = f"{replication_vault_name}{instance_type}policy" - policy_uri = ( - f"{rg_uri}/providers/Microsoft.DataReplication/replicationVaults" - f"/{replication_vault_name}/replicationPolicies/{policy_name}" - ) - - # Try to get existing policy, handle not found gracefully - try: - policy = get_resource_by_id( - cmd, policy_uri, APIVersion.Microsoft_DataReplication.value - ) - except CLIError as e: - error_str = str(e) - if ("ResourceNotFound" in error_str or "404" in error_str or - "Not Found" in error_str): - # Policy doesn't exist, this is expected for new setups - print(f"Policy '{policy_name}' does not exist, will create it.") - policy = None - else: - # Some other error occurred, re-raise it - raise - - # Handle existing policy states - if policy: - provisioning_state = ( - policy - .get('properties', {}) - .get('provisioningState') - ) - - # Wait for creating/updating to complete - if provisioning_state in [ProvisioningState.Creating.value, - ProvisioningState.Updating.value]: - print( - f"Policy '{policy_name}' found in Provisioning State " - f"'{provisioning_state}'." - ) - for i in range(20): - time.sleep(30) - policy = get_resource_by_id( - cmd, policy_uri, - APIVersion.Microsoft_DataReplication.value - ) - if policy: - provisioning_state = ( - policy.get('properties', {}).get('provisioningState') - ) - if provisioning_state not in [ - ProvisioningState.Creating.value, - ProvisioningState.Updating.value]: - break - - # Remove policy if in bad state - if provisioning_state in [ProvisioningState.Canceled.value, - ProvisioningState.Failed.value]: - print( - f"Policy '{policy_name}' found in unusable state " - f"'{provisioning_state}'. Removing..." - ) - delete_resource( - cmd, policy_uri, APIVersion.Microsoft_DataReplication.value - ) - time.sleep(30) - policy = None - - # Create policy if needed - if not policy or ( - policy and - policy.get('properties', {}).get('provisioningState') == - ProvisioningState.Deleted.value): - print(f"Creating Policy '{policy_name}'...") - - recoveryPoint = ( - ReplicationPolicyDetails.RecoveryPointHistoryInMinutes - ) - crashConsistentFreq = ( - ReplicationPolicyDetails.CrashConsistentFrequencyInMinutes - ) - appConsistentFreq = ( - ReplicationPolicyDetails.AppConsistentFrequencyInMinutes - ) - - policy_body = { - "properties": { - "customProperties": { - "instanceType": instance_type, - "recoveryPointHistoryInMinutes": recoveryPoint, - "crashConsistentFrequencyInMinutes": crashConsistentFreq, - "appConsistentFrequencyInMinutes": appConsistentFreq - } - } - } - - create_or_update_resource( - cmd, - policy_uri, - APIVersion.Microsoft_DataReplication.value, - policy_body, - ) - - # Wait for policy creation - for i in range(20): - time.sleep(30) - try: - policy = get_resource_by_id( - cmd, policy_uri, - APIVersion.Microsoft_DataReplication.value - ) - except Exception as poll_error: - # During creation, it might still return 404 initially - if ("ResourceNotFound" in str(poll_error) or - "404" in str(poll_error)): - print(f"Policy creation in progress... ({i + 1}/20)") - continue - raise - - if policy: - provisioning_state = ( - policy.get('properties', {}).get('provisioningState') - ) - print(f"Policy state: {provisioning_state}") - if provisioning_state in [ - ProvisioningState.Succeeded.value, - ProvisioningState.Failed.value, - ProvisioningState.Canceled.value, - ProvisioningState.Deleted.value]: - break - - if not policy or ( - policy.get('properties', {}).get('provisioningState') != - ProvisioningState.Succeeded.value): - raise CLIError(f"Policy '{policy_name}' is not in Succeeded state.") - - return policy - - -def setup_cache_storage_account(cmd, rg_uri, amh_solution, - cache_storage_account_id, - source_site_id, source_appliance_name, - migrate_project, project_name): - """Setup or validate cache storage account.""" - logger = get_logger(__name__) - - amh_stored_storage_account_id = ( - amh_solution.get('properties', {}) - .get('details', {}) - .get('extendedDetails', {}) - .get('replicationStorageAccountId') - ) - cache_storage_account = None - - if amh_stored_storage_account_id: - # Check existing storage account - storage_account_name = amh_stored_storage_account_id.split("/")[8] - storage_uri = ( - f"{rg_uri}/providers/Microsoft.Storage/storageAccounts" - f"/{storage_account_name}" - ) - storage_account = get_resource_by_id( - cmd, storage_uri, APIVersion.Microsoft_Storage.value - ) - - if storage_account and ( - storage_account - .get('properties', {}) - .get('provisioningState') == - StorageAccountProvisioningState.Succeeded.value - ): - cache_storage_account = storage_account - if (cache_storage_account_id and - cache_storage_account['id'] != - cache_storage_account_id): - warning_msg = ( - f"A Cache Storage Account '{storage_account_name}' is " - f"already linked. " - ) - warning_msg += "Ignoring provided -cache_storage_account_id." - logger.warning(warning_msg) - - # Use user-provided storage account if no existing one - if not cache_storage_account and cache_storage_account_id: - storage_account_name = cache_storage_account_id.split("/")[8].lower() - storage_uri = ( - f"{rg_uri}/providers/Microsoft.Storage/storageAccounts/" - f"{storage_account_name}" - ) - user_storage_account = get_resource_by_id( - cmd, storage_uri, APIVersion.Microsoft_Storage.value - ) - - if user_storage_account and ( - user_storage_account - .get('properties', {}) - .get('provisioningState') == - StorageAccountProvisioningState.Succeeded.value - ): - cache_storage_account = user_storage_account - else: - error_msg = ( - f"Cache Storage Account with Id " - f"'{cache_storage_account_id}' not found " - ) - error_msg += "or not in valid state." - raise CLIError(error_msg) - - # Create new storage account if needed - if not cache_storage_account: - artifact = f"{source_site_id}/{source_appliance_name}" - suffix_hash = generate_hash_for_artifact(artifact) - if len(suffix_hash) > 14: - suffix_hash = suffix_hash[:14] - storage_account_name = f"migratersa{suffix_hash}" - - print(f"Creating Cache Storage Account '{storage_account_name}'...") - - storage_body = { - "location": migrate_project.get('location'), - "tags": {"Migrate Project": project_name}, - "sku": {"name": "Standard_LRS"}, - "kind": "StorageV2", - "properties": { - "allowBlobPublicAccess": False, - "allowCrossTenantReplication": True, - "minimumTlsVersion": "TLS1_2", - "networkAcls": { - "defaultAction": "Allow" - }, - "encryption": { - "services": { - "blob": {"enabled": True}, - "file": {"enabled": True} - }, - "keySource": "Microsoft.Storage" - }, - "accessTier": "Hot" - } - } - - storage_uri = ( - f"{rg_uri}/providers/Microsoft.Storage/storageAccounts" - f"/{storage_account_name}" - ) - cache_storage_account = create_or_update_resource( - cmd, - storage_uri, - APIVersion.Microsoft_Storage.value, - storage_body - ) - - for _ in range(20): - time.sleep(30) - cache_storage_account = get_resource_by_id( - cmd, - storage_uri, - APIVersion.Microsoft_Storage.value - ) - if cache_storage_account and ( - cache_storage_account - .get('properties', {}) - .get('provisioningState') == - StorageAccountProvisioningState.Succeeded.value - ): - break - - if not cache_storage_account or ( - cache_storage_account - .get('properties', {}) - .get('provisioningState') != - StorageAccountProvisioningState.Succeeded.value - ): - raise CLIError("Failed to setup Cache Storage Account.") - - return cache_storage_account - - -def verify_storage_account_network_settings(cmd, - rg_uri, - cache_storage_account): - """Verify and update storage account network settings if needed.""" - storage_account_id = cache_storage_account['id'] - - # Verify storage account network settings - print("Verifying storage account network configuration...") - network_acls = ( - cache_storage_account.get('properties', {}).get('networkAcls', {}) - ) - default_action = network_acls.get('defaultAction', 'Allow') - - if default_action != 'Allow': - print( - f"WARNING: Storage account network defaultAction is " - f"'{default_action}'. " - "This may cause permission issues." - ) - print( - "Updating storage account to allow public network access..." - ) - - # Update storage account to allow public access - storage_account_name = storage_account_id.split("/")[-1] - storage_uri = ( - f"{rg_uri}/providers/Microsoft.Storage/storageAccounts/" - f"{storage_account_name}" - ) - - update_body = { - "properties": { - "networkAcls": { - "defaultAction": "Allow" - } - } - } - - create_or_update_resource( - cmd, storage_uri, APIVersion.Microsoft_Storage.value, - update_body - ) - - # Wait for network update to propagate - time.sleep(30) - - -def get_all_fabrics(cmd, rg_uri, resource_group_name, - source_appliance_name, - target_appliance_name, project_name): - """Get all replication fabrics in the resource group.""" - replication_fabrics_uri = ( - f"{rg_uri}/providers/Microsoft.DataReplication/replicationFabrics" - ) - fabrics_uri = ( - f"{replication_fabrics_uri}?api-version=" - f"{APIVersion.Microsoft_DataReplication.value}" - ) - fabrics_response = send_get_request(cmd, fabrics_uri) - all_fabrics = fabrics_response.json().get('value', []) - - # If no fabrics exist at all, provide helpful message - if not all_fabrics: - raise CLIError( - f"No replication fabrics found in resource group " - f"'{resource_group_name}'. " - f"Please ensure that: \n" - f"1. The source appliance '{source_appliance_name}' is deployed " - f"and connected\n" - f"2. The target appliance '{target_appliance_name}' is deployed " - f"and connected\n" - f"3. Both appliances are registered with the Azure Migrate " - f"project '{project_name}'" - ) - - return all_fabrics, replication_fabrics_uri - - -def _get_role_name(role_def_id): - """Get role name from role definition ID.""" - return ("Contributor" if role_def_id == RoleDefinitionIds.ContributorId - else "Storage Blob Data Contributor") - - -def _assign_role_to_principal(auth_client, storage_account_id, - subscription_id, - principal_id, role_def_id, - principal_type_name): - """Assign a role to a principal if not already assigned.""" - from uuid import uuid4 - from azure.mgmt.authorization.models import ( - RoleAssignmentCreateParameters, PrincipalType - ) - - role_name = _get_role_name(role_def_id) - - # Check if assignment exists - assignments = auth_client.role_assignments.list_for_scope( - scope=storage_account_id, - filter=f"principalId eq '{principal_id}'" - ) - - roles = [a.role_definition_id.endswith(role_def_id) for a in assignments] - has_role = any(roles) - - if not has_role: - role_assignment_params = RoleAssignmentCreateParameters( - role_definition_id=( - f"/subscriptions/{subscription_id}/providers" - f"/Microsoft.Authorization/roleDefinitions/{role_def_id}" - ), - principal_id=principal_id, - principal_type=PrincipalType.SERVICE_PRINCIPAL - ) - auth_client.role_assignments.create( - scope=storage_account_id, - role_assignment_name=str(uuid4()), - parameters=role_assignment_params - ) - print( - f" ✓ Created {role_name} role for {principal_type_name} " - f"{principal_id[:8]}..." - ) - return f"{principal_id[:8]} - {role_name}", False - print( - f" ✓ {role_name} role already exists for {principal_type_name} " - f"{principal_id[:8]}" - ) - return f"{principal_id[:8]} - {role_name} (existing)", True - - -def _verify_role_assignments(auth_client, storage_account_id, - expected_principal_ids): - """Verify that role assignments were created successfully.""" - print("Verifying role assignments...") - all_assignments = list( - auth_client.role_assignments.list_for_scope( - scope=storage_account_id - ) - ) - verified_principals = set() - - for assignment in all_assignments: - principal_id = assignment.principal_id - if principal_id in expected_principal_ids: - verified_principals.add(principal_id) - role_id = assignment.role_definition_id.split('/')[-1] - role_display = _get_role_name(role_id) - print( - f" ✓ Verified {role_display} for principal " - f"{principal_id[:8]}" - ) - - missing_principals = set(expected_principal_ids) - verified_principals - if missing_principals: - print( - f"WARNING: {len(missing_principals)} principal(s) missing role " - f"assignments: " - ) - for principal in missing_principals: - print(f" - {principal}") - - -def grant_storage_permissions(cmd, storage_account_id, source_dra, - target_dra, replication_vault, subscription_id): - """Grant role assignments for DRAs and vault identity to storage acct.""" - from azure.mgmt.authorization import AuthorizationManagementClient - - # Get role assignment client - from azure.cli.core.commands.client_factory import ( - get_mgmt_service_client - ) - auth_client = get_mgmt_service_client( - cmd.cli_ctx, AuthorizationManagementClient - ) - - source_dra_object_id = ( - source_dra.get('properties', {}) - .get('resourceAccessIdentity', {}).get('objectId') - ) - target_dra_object_id = ( - target_dra.get('properties', {}) - .get('resourceAccessIdentity', {}).get('objectId') - ) - - # Get vault identity from either root level or properties level - vault_identity = ( - replication_vault.get('identity') or - replication_vault.get('properties', {}).get('identity') - ) - vault_identity_id = ( - vault_identity.get('principalId') if vault_identity else None - ) - - print("Granting permissions to the storage account...") - print(f" Source DRA Principal ID: {source_dra_object_id}") - print(f" Target DRA Principal ID: {target_dra_object_id}") - print(f" Vault Identity Principal ID: {vault_identity_id}") - - successful_assignments = [] - failed_assignments = [] - - # Create role assignments for source and target DRAs - for object_id in [source_dra_object_id, target_dra_object_id]: - if object_id: - for role_def_id in [ - RoleDefinitionIds.ContributorId, - RoleDefinitionIds.StorageBlobDataContributorId - ]: - try: - assignment_msg, _ = _assign_role_to_principal( - auth_client, storage_account_id, subscription_id, - object_id, role_def_id, "DRA" - ) - successful_assignments.append(assignment_msg) - except CLIError as e: - role_name = _get_role_name(role_def_id) - error_msg = f"{object_id[:8]} - {role_name}: {str(e)}" - failed_assignments.append(error_msg) - - # Grant vault identity permissions if exists - if vault_identity_id: - for role_def_id in [RoleDefinitionIds.ContributorId, - RoleDefinitionIds.StorageBlobDataContributorId]: - try: - assignment_msg, _ = _assign_role_to_principal( - auth_client, storage_account_id, subscription_id, - vault_identity_id, role_def_id, "vault" - ) - successful_assignments.append(assignment_msg) - except CLIError as e: - role_name = _get_role_name(role_def_id) - error_msg = f"{vault_identity_id[:8]} - {role_name}: {str(e)}" - failed_assignments.append(error_msg) - - # Report role assignment status - print("\nRole Assignment Summary:") - print(f" Successful: {len(successful_assignments)}") - if failed_assignments: - print(f" Failed: {len(failed_assignments)}") - for failure in failed_assignments: - print(f" - {failure}") - - # If there are failures, raise an error - if failed_assignments: - raise CLIError( - f"Failed to create {len(failed_assignments)} role " - f"assignment(s). " - "The storage account may not have proper permissions." - ) - - # Add a wait after role assignments to ensure propagation - time.sleep(120) - - # Verify role assignments were successful - expected_principal_ids = [ - source_dra_object_id, target_dra_object_id, vault_identity_id - ] - _verify_role_assignments( - auth_client, storage_account_id, expected_principal_ids - ) - - -def update_amh_solution_storage(cmd, - project_uri, - amh_solution, - storage_account_id): - """Update AMH solution with storage account ID if needed.""" - amh_solution_uri = ( - f"{project_uri}/solutions/" - f"Servers-Migration-ServerMigration_DataReplication" - ) - - if (amh_solution - .get('properties', {}) - .get('details', {}) - .get('extendedDetails', {}) - .get('replicationStorageAccountId')) != storage_account_id: - extended_details = (amh_solution - .get('properties', {}) - .get('details', {}) - .get('extendedDetails', {})) - extended_details['replicationStorageAccountId'] = ( - storage_account_id - ) - - solution_body = { - "properties": { - "details": { - "extendedDetails": extended_details - } - } - } - - create_or_update_resource( - cmd, amh_solution_uri, APIVersion.Microsoft_Migrate.value, - solution_body - ) - - # Wait for the AMH solution update to fully propagate - time.sleep(60) - - return amh_solution_uri - - -def get_or_check_existing_extension(cmd, extension_uri, - replication_extension_name, - storage_account_id): - """Get existing extension and check if it's in a good state.""" - # Try to get existing extension, handle not found gracefully - try: - replication_extension = get_resource_by_id( - cmd, extension_uri, APIVersion.Microsoft_DataReplication.value - ) - except CLIError as e: - error_str = str(e) - if ("ResourceNotFound" in error_str or "404" in error_str or - "Not Found" in error_str): - # Extension doesn't exist, this is expected for new setups - print( - f"Extension '{replication_extension_name}' does not exist, " - f"will create it." - ) - return None, False - # Some other error occurred, re-raise it - raise - - # Check if extension exists and is in good state - if replication_extension: - existing_state = ( - replication_extension.get('properties', {}) - .get('provisioningState') - ) - existing_storage_id = (replication_extension - .get('properties', {}) - .get('customProperties', {}) - .get('storageAccountId')) - - print( - f"Found existing extension '{replication_extension_name}' in " - f"state: {existing_state}" - ) - - # If it's succeeded with the correct storage account, we're done - if (existing_state == ProvisioningState.Succeeded.value and - existing_storage_id == storage_account_id): - print( - "Replication Extension already exists with correct " - "configuration." - ) - print("Successfully initialized replication infrastructure") - return None, True # Signal that we're done - - # If it's in a bad state or has wrong storage account, delete it - if (existing_state in [ProvisioningState.Failed.value, - ProvisioningState.Canceled.value] or - existing_storage_id != storage_account_id): - print(f"Removing existing extension (state: {existing_state})") - delete_resource( - cmd, extension_uri, APIVersion.Microsoft_DataReplication.value - ) - time.sleep(120) - return None, False - - return replication_extension, False - - -def verify_extension_prerequisites(cmd, rg_uri, replication_vault_name, - instance_type, storage_account_id, - amh_solution_uri, source_fabric_id, - target_fabric_id): - """Verify all prerequisites before creating extension.""" - print("\nVerifying prerequisites before creating extension...") - - # 1. Verify policy is succeeded - policy_name = f"{replication_vault_name}{instance_type}policy" - policy_uri = ( - f"{rg_uri}/providers/Microsoft.DataReplication/replicationVaults" - f"/{replication_vault_name}/replicationPolicies/{policy_name}" - ) - policy_check = get_resource_by_id( - cmd, policy_uri, APIVersion.Microsoft_DataReplication.value) - if (policy_check.get('properties', {}).get('provisioningState') != - ProvisioningState.Succeeded.value): - raise CLIError( - "Policy is not in Succeeded state: {}".format( - policy_check.get('properties', {}).get('provisioningState'))) - - # 2. Verify storage account is succeeded - storage_account_name = storage_account_id.split("/")[-1] - storage_uri = ( - f"{rg_uri}/providers/Microsoft.Storage/storageAccounts/" - f"{storage_account_name}") - storage_check = get_resource_by_id( - cmd, storage_uri, APIVersion.Microsoft_Storage.value) - if (storage_check - .get('properties', {}) - .get('provisioningState') != - StorageAccountProvisioningState.Succeeded.value): - raise CLIError( - "Storage account is not in Succeeded state: {}".format( - storage_check.get('properties', {}).get( - 'provisioningState'))) - - # 3. Verify AMH solution has storage account - solution_check = get_resource_by_id( - cmd, amh_solution_uri, APIVersion.Microsoft_Migrate.value) - if (solution_check - .get('properties', {}) - .get('details', {}) - .get('extendedDetails', {}) - .get('replicationStorageAccountId') != storage_account_id): - raise CLIError( - "AMH solution doesn't have the correct storage account ID") - - # 4. Verify fabrics are responsive - source_fabric_check = get_resource_by_id( - cmd, source_fabric_id, APIVersion.Microsoft_DataReplication.value) - if (source_fabric_check.get('properties', {}).get('provisioningState') != - ProvisioningState.Succeeded.value): - raise CLIError("Source fabric is not in Succeeded state") - - target_fabric_check = get_resource_by_id( - cmd, target_fabric_id, APIVersion.Microsoft_DataReplication.value) - if (target_fabric_check.get('properties', {}).get('provisioningState') != - ProvisioningState.Succeeded.value): - raise CLIError("Target fabric is not in Succeeded state") - - print("All prerequisites verified successfully!") - time.sleep(30) - - -def list_existing_extensions(cmd, rg_uri, replication_vault_name): - """List existing extensions for informational purposes.""" - existing_extensions_uri = ( - f"{rg_uri}/providers/Microsoft.DataReplication" - f"/replicationVaults/{replication_vault_name}" - f"/replicationExtensions" - f"?api-version={APIVersion.Microsoft_DataReplication.value}" - ) - try: - existing_extensions_response = send_get_request( - cmd, existing_extensions_uri) - existing_extensions = ( - existing_extensions_response.json().get('value', [])) - if existing_extensions: - print(f"Found {len(existing_extensions)} existing " - f"extension(s): ") - for ext in existing_extensions: - ext_name = ext.get('name') - ext_state = ( - ext.get('properties', {}).get('provisioningState')) - ext_type = (ext.get('properties', {}) - .get('customProperties', {}) - .get('instanceType')) - print(f" - {ext_name}: state={ext_state}, " - f"type={ext_type}") - else: - print("No existing extensions found") - except CLIError as list_error: - # If listing fails, it might mean no extensions exist at all - print(f"Could not list extensions (this is normal for new " - f"projects): {str(list_error)}") - - -def build_extension_body(instance_type, source_fabric_id, - target_fabric_id, storage_account_id): - """Build the extension body based on instance type.""" - print("\n=== Creating extension for replication infrastructure ===") - print(f"Instance Type: {instance_type}") - print(f"Source Fabric ID: {source_fabric_id}") - print(f"Target Fabric ID: {target_fabric_id}") - print(f"Storage Account ID: {storage_account_id}") - - # Build the extension body with properties in the exact order from - # the working API call - if instance_type == AzLocalInstanceTypes.VMwareToAzLocal.value: - # Match exact property order from working call for VMware - extension_body = { - "properties": { - "customProperties": { - "azStackHciFabricArmId": target_fabric_id, - "storageAccountId": storage_account_id, - "storageAccountSasSecretName": None, - "instanceType": instance_type, - "vmwareFabricArmId": source_fabric_id - } - } - } - elif instance_type == AzLocalInstanceTypes.HyperVToAzLocal.value: - # For HyperV, use similar order but with hyperVFabricArmId - extension_body = { - "properties": { - "customProperties": { - "azStackHciFabricArmId": target_fabric_id, - "storageAccountId": storage_account_id, - "storageAccountSasSecretName": None, - "instanceType": instance_type, - "hyperVFabricArmId": source_fabric_id - } - } - } - else: - raise CLIError(f"Unsupported instance type: {instance_type}") - - # Debug: Print the exact body being sent - body_str = json.dumps(extension_body, indent=2) - print(f"Extension body being sent: \n{body_str}") - - return extension_body - - -def _wait_for_extension_creation(cmd, extension_uri): - """Wait for extension creation to complete.""" - for i in range(20): - time.sleep(30) - try: - api_version = APIVersion.Microsoft_DataReplication.value - replication_extension = get_resource_by_id( - cmd, extension_uri, api_version) - if replication_extension: - ext_state = replication_extension.get( - 'properties', {}).get('provisioningState') - print(f"Extension state: {ext_state}") - if ext_state in [ProvisioningState.Succeeded.value, - ProvisioningState.Failed.value, - ProvisioningState.Canceled.value]: - break - except CLIError: - print(f"Waiting for extension... ({i + 1}/20)") - - -def _handle_extension_creation_error(cmd, extension_uri, create_error): - """Handle errors during extension creation.""" - error_str = str(create_error) - print(f"Error during extension creation: {error_str}") - - # Check if extension was created despite the error - time.sleep(30) - try: - api_version = APIVersion.Microsoft_DataReplication.value - replication_extension = get_resource_by_id( - cmd, extension_uri, api_version) - if replication_extension: - print( - f"Extension exists despite error, " - f"state: {replication_extension.get('properties', {}).get('provisioningState')}" - ) - except CLIError: - replication_extension = None - - if not replication_extension: - raise CLIError( - f"Failed to create replication extension: " - f"{str(create_error)}") from create_error - - -def create_replication_extension(cmd, extension_uri, extension_body): - """Create the replication extension and wait for it to complete.""" - try: - result = create_or_update_resource( - cmd, extension_uri, - APIVersion.Microsoft_DataReplication.value, - extension_body) - if result: - print("Extension creation initiated successfully") - # Wait for the extension to be created - print("Waiting for extension creation to complete...") - _wait_for_extension_creation(cmd, extension_uri) - except CLIError as create_error: - _handle_extension_creation_error(cmd, extension_uri, create_error) - - -def setup_replication_extension(cmd, rg_uri, replication_vault_name, - source_fabric, target_fabric, - instance_type, storage_account_id, - amh_solution_uri, pass_thru): - """Setup replication extension - main orchestration function.""" - # Setup Replication Extension - source_fabric_id = source_fabric['id'] - target_fabric_id = target_fabric['id'] - source_fabric_short_name = source_fabric_id.split('/')[-1] - target_fabric_short_name = target_fabric_id.split('/')[-1] - replication_extension_name = ( - f"{source_fabric_short_name}-{target_fabric_short_name}-" - f"MigReplicationExtn") - - extension_uri = ( - f"{rg_uri}/providers/Microsoft.DataReplication/" - f"replicationVaults/{replication_vault_name}/" - f"replicationExtensions/{replication_extension_name}" - ) - - # Get or check existing extension - replication_extension, is_complete = get_or_check_existing_extension( - cmd, extension_uri, replication_extension_name, - storage_account_id - ) - - if is_complete: - return True if pass_thru else None - - # Verify prerequisites - verify_extension_prerequisites( - cmd, rg_uri, replication_vault_name, instance_type, - storage_account_id, amh_solution_uri, source_fabric_id, - target_fabric_id - ) - - # Create extension if needed - if not replication_extension: - print( - f"Creating Replication Extension " - f"'{replication_extension_name}'...") - - # List existing extensions for context - list_existing_extensions(cmd, rg_uri, replication_vault_name) - - # Build extension body - extension_body = build_extension_body( - instance_type, source_fabric_id, target_fabric_id, - storage_account_id - ) - - # Create the extension - create_replication_extension(cmd, extension_uri, extension_body) - - print("Successfully initialized replication infrastructure") - return True if pass_thru else None - - -def setup_project_and_solutions(cmd, - subscription_id, - resource_group_name, - project_name): - """Setup and retrieve project and solutions.""" - rg_uri = get_and_validate_resource_group( - cmd, subscription_id, resource_group_name) - project_uri = (f"{rg_uri}/providers/Microsoft.Migrate/migrateprojects/" - f"{project_name}") - migrate_project = get_migrate_project(cmd, project_uri, project_name) - amh_solution = get_data_replication_solution(cmd, project_uri) - discovery_solution = get_discovery_solution(cmd, project_uri) - - return ( - rg_uri, - project_uri, - migrate_project, - amh_solution, - discovery_solution - ) - - -def setup_appliances_and_types(discovery_solution, - source_appliance_name, - target_appliance_name): - """Parse appliance mappings and determine instance types.""" - app_map = parse_appliance_mappings(discovery_solution) - source_site_id, target_site_id = validate_and_get_site_ids( - app_map, source_appliance_name, target_appliance_name - ) - result = determine_instance_types( - source_site_id, target_site_id, source_appliance_name, - target_appliance_name - ) - instance_type, fabric_instance_type = result - return ( - source_site_id, - instance_type, - fabric_instance_type - ) - - -def setup_fabrics_and_dras(cmd, rg_uri, resource_group_name, - source_appliance_name, target_appliance_name, - project_name, fabric_instance_type, - amh_solution): - """Get all fabrics and set up DRAs.""" - all_fabrics, replication_fabrics_uri = get_all_fabrics( - cmd, rg_uri, resource_group_name, source_appliance_name, - target_appliance_name, project_name - ) - - source_fabric = find_fabric( - all_fabrics, source_appliance_name, fabric_instance_type, - amh_solution, is_source=True) - target_fabric_instance_type = FabricInstanceTypes.AzLocalInstance.value - target_fabric = find_fabric( - all_fabrics, target_appliance_name, target_fabric_instance_type, - amh_solution, is_source=False) - - source_dra = get_fabric_agent( - cmd, replication_fabrics_uri, source_fabric, - source_appliance_name, fabric_instance_type) - target_dra = get_fabric_agent( - cmd, replication_fabrics_uri, target_fabric, - target_appliance_name, target_fabric_instance_type) - - return source_fabric, target_fabric, source_dra, target_dra - - -def setup_storage_and_permissions(cmd, rg_uri, amh_solution, - cache_storage_account_id, source_site_id, - source_appliance_name, migrate_project, - project_name, source_dra, target_dra, - replication_vault, subscription_id): - """Setup storage account and grant permissions.""" - cache_storage_account = setup_cache_storage_account( - cmd, rg_uri, amh_solution, cache_storage_account_id, - source_site_id, source_appliance_name, migrate_project, project_name - ) - - storage_account_id = cache_storage_account['id'] - verify_storage_account_network_settings( - cmd, rg_uri, cache_storage_account) - grant_storage_permissions( - cmd, storage_account_id, source_dra, target_dra, - replication_vault, subscription_id) - - return storage_account_id - - -def initialize_infrastructure_components(cmd, rg_uri, project_uri, - amh_solution, - replication_vault_name, - instance_type, migrate_project, - project_name, - cache_storage_account_id, - source_site_id, - source_appliance_name, source_dra, - target_dra, replication_vault, - subscription_id): - """Initialize policy, storage, and AMH solution.""" - setup_replication_policy( - cmd, rg_uri, replication_vault_name, instance_type) - - storage_account_id = setup_storage_and_permissions( - cmd, rg_uri, amh_solution, cache_storage_account_id, - source_site_id, source_appliance_name, migrate_project, project_name, - source_dra, target_dra, replication_vault, subscription_id - ) - - amh_solution_uri = update_amh_solution_storage( - cmd, project_uri, amh_solution, storage_account_id) - - return storage_account_id, amh_solution_uri - - -def execute_replication_infrastructure_setup(cmd, subscription_id, - resource_group_name, - project_name, - source_appliance_name, - target_appliance_name, - cache_storage_account_id, - pass_thru): - """Execute the complete replication infrastructure setup workflow.""" - # Setup project and solutions - (rg_uri, project_uri, migrate_project, amh_solution, - discovery_solution) = setup_project_and_solutions( - cmd, subscription_id, resource_group_name, project_name - ) - - # Get and setup replication vault - (replication_vault, - replication_vault_name) = get_and_setup_replication_vault( - cmd, amh_solution, rg_uri) - - # Setup appliances and determine types - (source_site_id, instance_type, - fabric_instance_type) = setup_appliances_and_types( - discovery_solution, source_appliance_name, target_appliance_name - ) - - # Setup fabrics and DRAs - (source_fabric, target_fabric, source_dra, - target_dra) = setup_fabrics_and_dras( - cmd, rg_uri, resource_group_name, source_appliance_name, - target_appliance_name, project_name, fabric_instance_type, - amh_solution - ) - - # Initialize policy, storage, and AMH solution - (storage_account_id, - amh_solution_uri) = initialize_infrastructure_components( - cmd, rg_uri, project_uri, amh_solution, replication_vault_name, - instance_type, migrate_project, project_name, - cache_storage_account_id, source_site_id, source_appliance_name, - source_dra, target_dra, replication_vault, subscription_id - ) - - # Setup Replication Extension - return setup_replication_extension( - cmd, rg_uri, replication_vault_name, source_fabric, - target_fabric, instance_type, storage_account_id, - amh_solution_uri, pass_thru - ) diff --git a/src/migrate/azext_migrate/_new_local_server_replication_helpers.py b/src/migrate/azext_migrate/_new_local_server_replication_helpers.py deleted file mode 100644 index eb9703e173d..00000000000 --- a/src/migrate/azext_migrate/_new_local_server_replication_helpers.py +++ /dev/null @@ -1,1499 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -# pylint: disable=line-too-long -# pylint: disable=possibly-used-before-assignment -from azure.cli.core.commands.client_factory import get_subscription_id -from azext_migrate._helpers import ( - send_get_request, - get_resource_by_id, - create_or_update_resource, - APIVersion, - ProvisioningState, - AzLocalInstanceTypes, - FabricInstanceTypes, - SiteTypes, - VMNicSelection, - validate_arm_id_format, - IdFormats -) -import re -import json -from knack.util import CLIError -from knack.log import get_logger - -logger = get_logger(__name__) - - -def _process_v2_dict(extended_details, app_map): - try: - app_map_v2 = json.loads( - extended_details['applianceNameToSiteIdMapV2']) - if isinstance(app_map_v2, list): - for item in app_map_v2: - if (isinstance(item, dict) and - 'ApplianceName' in item and - 'SiteId' in item): - # Store both lowercase and original case - app_map[item['ApplianceName'].lower()] = item['SiteId'] - app_map[item['ApplianceName']] = item['SiteId'] - except (json.JSONDecodeError, KeyError, TypeError): - pass - return app_map - - -def _process_v3_dict_map(app_map_v3, app_map): - for appliance_name_key, site_info in app_map_v3.items(): - if isinstance(site_info, dict) and 'SiteId' in site_info: - app_map[appliance_name_key.lower()] = site_info['SiteId'] - app_map[appliance_name_key] = site_info['SiteId'] - elif isinstance(site_info, str): - app_map[appliance_name_key.lower()] = site_info - app_map[appliance_name_key] = site_info - return app_map - - -def _process_v3_dict_list(app_map_v3, app_map): - # V3 might also be in list format - for item in app_map_v3: - if isinstance(item, dict): - # Check if it has ApplianceName/SiteId structure - if 'ApplianceName' in item and 'SiteId' in item: - app_map[item['ApplianceName'].lower()] = item['SiteId'] - app_map[item['ApplianceName']] = item['SiteId'] - else: - # Or it might be a single key-value pair - for key, value in item.items(): - if isinstance(value, dict) and 'SiteId' in value: - app_map[key.lower()] = value['SiteId'] - app_map[key] = value['SiteId'] - elif isinstance(value, str): - app_map[key.lower()] = value - app_map[key] = value - return app_map - - -def _process_v3_dict(extended_details, app_map): - try: - app_map_v3 = json.loads(extended_details['applianceNameToSiteIdMapV3']) - if isinstance(app_map_v3, dict): - app_map = _process_v3_dict_map(app_map_v3, app_map) - elif isinstance(app_map_v3, list): - app_map = _process_v3_dict_list(app_map_v3, app_map) - except (json.JSONDecodeError, KeyError, TypeError): - pass - return app_map - - -def validate_server_parameters( - cmd, - machine_id, - machine_index, - project_name, - resource_group_name, - source_appliance_name, - subscription_id): - # Validate that either machine_id or machine_index is provided - if not machine_id and not machine_index: - raise CLIError( - "Either machine_id or machine_index must be provided.") - if machine_id and machine_index: - raise CLIError( - "Only one of machine_id or machine_index should be " - "provided, not both.") - - if not subscription_id: - subscription_id = get_subscription_id(cmd.cli_ctx) - - if machine_index: - if not project_name: - raise CLIError( - "project_name is required when using machine_index.") - if not resource_group_name: - raise CLIError( - "resource_group_name is required when using " - "machine_index.") - - if not isinstance(machine_index, int) or machine_index < 1: - raise CLIError( - "machine_index must be a positive integer " - "(1-based index).") - - rg_uri = ( - f"/subscriptions/{subscription_id}/" - f"resourceGroups/{resource_group_name}") - discovery_solution_name = "Servers-Discovery-ServerDiscovery" - discovery_solution_uri = ( - f"{rg_uri}/providers/Microsoft.Migrate/migrateprojects" - f"/{project_name}/solutions/{discovery_solution_name}" - ) - discovery_solution = get_resource_by_id( - cmd, discovery_solution_uri, APIVersion.Microsoft_Migrate.value) - - if not discovery_solution: - raise CLIError( - f"Server Discovery Solution '{discovery_solution_name}' " - f"not in project '{project_name}'.") - - # Get appliance mapping to determine site type - app_map = {} - extended_details = ( - discovery_solution.get('properties', {}) - .get('details', {}) - .get('extendedDetails', {})) - - # Process applianceNameToSiteIdMapV2 and V3 - if 'applianceNameToSiteIdMapV2' in extended_details: - app_map = _process_v2_dict(extended_details, app_map) - - if 'applianceNameToSiteIdMapV3' in extended_details: - app_map = _process_v3_dict(extended_details, app_map) - - # Get source site ID - try both original and lowercase - source_site_id = ( - app_map.get(source_appliance_name) or - app_map.get(source_appliance_name.lower())) - if not source_site_id: - raise CLIError( - f"Source appliance '{source_appliance_name}' " - f"not in discovery solution.") - - # Determine site type from source site ID - hyperv_site_pattern = "/Microsoft.OffAzure/HyperVSites/" - vmware_site_pattern = "/Microsoft.OffAzure/VMwareSites/" - - if hyperv_site_pattern in source_site_id: - site_name = source_site_id.split('/')[-1] - machines_uri = ( - f"{rg_uri}/providers/Microsoft.OffAzure/" - f"HyperVSites/{site_name}/machines") - elif vmware_site_pattern in source_site_id: - site_name = source_site_id.split('/')[-1] - machines_uri = ( - f"{rg_uri}/providers/Microsoft.OffAzure/" - f"VMwareSites/{site_name}/machines") - else: - raise CLIError( - f"Unable to determine site type for source appliance " - f"'{source_appliance_name}'.") - - # Get all machines from the site - request_uri = ( - f"{cmd.cli_ctx.cloud.endpoints.resource_manager}" - f"{machines_uri}?api-version={APIVersion.Microsoft_OffAzure.value}" - ) - - response = send_get_request(cmd, request_uri) - machines_data = response.json() - machines = machines_data.get('value', []) - - # Fetch all pages if there are more - while machines_data.get('nextLink'): - response = send_get_request(cmd, machines_data.get('nextLink')) - machines_data = response.json() - machines.extend(machines_data.get('value', [])) - - # Check if the index is valid - if machine_index > len(machines): - raise CLIError( - f"Invalid machine_index {machine_index}. " - f"Only {len(machines)} machines found in site '{site_name}'.") - - # Get the machine at the specified index (convert 1-based to 0-based) - selected_machine = machines[machine_index - 1] - machine_id = selected_machine.get('id') - return rg_uri - - -def validate_required_parameters(machine_id, - target_storage_path_id, - target_resource_group_id, - target_vm_name, - source_appliance_name, - target_appliance_name, - disk_to_include, - nic_to_include, - target_virtual_switch_id, - os_disk_id, - is_dynamic_memory_enabled): - # Validate required parameters - if not machine_id: - raise CLIError("machine_id could not be determined.") - if not target_storage_path_id: - raise CLIError("target_storage_path_id is required.") - if not target_resource_group_id: - raise CLIError("target_resource_group_id is required.") - if not target_vm_name: - raise CLIError("target_vm_name is required.") - if not source_appliance_name: - raise CLIError("source_appliance_name is required.") - if not target_appliance_name: - raise CLIError("target_appliance_name is required.") - - # Validate parameter set requirements - is_power_user_mode = (disk_to_include is not None or - nic_to_include is not None) - is_default_user_mode = (target_virtual_switch_id is not None or - os_disk_id is not None) - - if is_power_user_mode and is_default_user_mode: - raise CLIError( - "Cannot mix default user mode parameters " - "(target_virtual_switch_id, os_disk_id) with power user mode " - "parameters (disk_to_include, nic_to_include).") - - if is_power_user_mode: - # Power user mode validation - if not disk_to_include: - raise CLIError( - "disk_to_include is required when using power user mode.") - if not nic_to_include: - raise CLIError( - "nic_to_include is required when using power user mode.") - else: - # Default user mode validation - if not target_virtual_switch_id: - raise CLIError( - "target_virtual_switch_id is required when using " - "default user mode.") - if not os_disk_id: - raise CLIError( - "os_disk_id is required when using default user mode.") - - is_dynamic_ram_enabled = None - if is_dynamic_memory_enabled: - if is_dynamic_memory_enabled not in ['true', 'false']: - raise CLIError( - "is_dynamic_memory_enabled must be either " - "'true' or 'false'.") - is_dynamic_ram_enabled = is_dynamic_memory_enabled == 'true' - return is_dynamic_ram_enabled, is_power_user_mode - - -def validate_ARM_id_formats(machine_id, - target_storage_path_id, - target_resource_group_id, - target_virtual_switch_id, - target_test_virtual_switch_id): - # Validate ARM ID formats - if not validate_arm_id_format( - machine_id, - IdFormats.MachineArmIdTemplate): - raise CLIError( - f"Invalid -machine_id '{machine_id}'. " - f"A valid machine ARM ID should follow the format " - f"'{IdFormats.MachineArmIdTemplate}'.") - - if not validate_arm_id_format( - target_storage_path_id, - IdFormats.StoragePathArmIdTemplate): - raise CLIError( - f"Invalid -target_storage_path_id " - f"'{target_storage_path_id}'. " - f"A valid storage path ARM ID should follow the format " - f"'{IdFormats.StoragePathArmIdTemplate}'.") - - if not validate_arm_id_format( - target_resource_group_id, - IdFormats.ResourceGroupArmIdTemplate): - raise CLIError( - f"Invalid -target_resource_group_id " - f"'{target_resource_group_id}'. " - f"A valid resource group ARM ID should follow the format " - f"'{IdFormats.ResourceGroupArmIdTemplate}'.") - - if (target_virtual_switch_id and - not validate_arm_id_format( - target_virtual_switch_id, - IdFormats.LogicalNetworkArmIdTemplate)): - raise CLIError( - f"Invalid -target_virtual_switch_id " - f"'{target_virtual_switch_id}'. " - f"A valid logical network ARM ID should follow the format " - f"'{IdFormats.LogicalNetworkArmIdTemplate}'.") - - if (target_test_virtual_switch_id and - not validate_arm_id_format( - target_test_virtual_switch_id, - IdFormats.LogicalNetworkArmIdTemplate)): - raise CLIError( - f"Invalid -target_test_virtual_switch_id " - f"'{target_test_virtual_switch_id}'. " - f"A valid logical network ARM ID should follow the format " - f"'{IdFormats.LogicalNetworkArmIdTemplate}'.") - - machine_id_parts = machine_id.split("/") - if len(machine_id_parts) < 11: - raise CLIError(f"Invalid machine ARM ID format: '{machine_id}'") - - resource_group_name = machine_id_parts[4] - site_type = machine_id_parts[7] - site_name = machine_id_parts[8] - machine_name = machine_id_parts[10] - - run_as_account_id = None - instance_type = None - return site_type, site_name, machine_name, run_as_account_id, instance_type, resource_group_name - - -def process_site_type_hyperV(cmd, - rg_uri, - site_name, - machine_name, - subscription_id, - resource_group_name, - site_type): - # Get HyperV machine - machine_uri = ( - f"{rg_uri}/providers/Microsoft.OffAzure/HyperVSites" - f"/{site_name}/machines/{machine_name}") - machine = get_resource_by_id( - cmd, machine_uri, APIVersion.Microsoft_OffAzure.value) - if not machine: - raise CLIError( - f"Machine '{machine_name}' not in " - f"resource group '{resource_group_name}' and " - f"site '{site_name}'.") - - # Get HyperV site - site_uri = ( - f"{rg_uri}/providers/Microsoft.OffAzure/HyperVSites/{site_name}") - site_object = get_resource_by_id( - cmd, site_uri, APIVersion.Microsoft_OffAzure.value) - if not site_object: - raise CLIError( - f"Machine site '{site_name}' with Type '{site_type}' " - f"not found.") - - # Get RunAsAccount - properties = machine.get('properties', {}) - if properties.get('hostId'): - # Machine is on a single HyperV host - host_id_parts = properties['hostId'].split("/") - if len(host_id_parts) < 11: - raise CLIError( - f"Invalid Hyper-V Host ARM ID '{properties['hostId']}'") - - host_resource_group = host_id_parts[4] - host_site_name = host_id_parts[8] - host_name = host_id_parts[10] - - host_uri = ( - f"/subscriptions/{subscription_id}/resourceGroups" - f"/{host_resource_group}/providers/" - f"Microsoft.OffAzure/HyperVSites" - f"/{host_site_name}/hosts/{host_name}" - ) - hyperv_host = get_resource_by_id( - cmd, host_uri, APIVersion.Microsoft_OffAzure.value) - if not hyperv_host: - raise CLIError( - f"Hyper-V host '{host_name}' not in " - f"resource group '{host_resource_group}' and " - f"site '{host_site_name}'.") - - run_as_account_id = ( - hyperv_host.get('properties', {}).get('runAsAccountId')) - - elif properties.get('clusterId'): - # Machine is on a HyperV cluster - cluster_id_parts = properties['clusterId'].split("/") - if len(cluster_id_parts) < 11: - raise CLIError( - f"Invalid Hyper-V Cluster ARM ID " - f"'{properties['clusterId']}'") - - cluster_resource_group = cluster_id_parts[4] - cluster_site_name = cluster_id_parts[8] - cluster_name = cluster_id_parts[10] - - cluster_uri = ( - f"/subscriptions/{subscription_id}/resourceGroups" - f"/{cluster_resource_group}/providers/Microsoft.OffAzure" - f"/HyperVSites/{cluster_site_name}/clusters/{cluster_name}" - ) - hyperv_cluster = get_resource_by_id( - cmd, cluster_uri, APIVersion.Microsoft_OffAzure.value) - if not hyperv_cluster: - raise CLIError( - f"Hyper-V cluster '{cluster_name}' not in " - f"resource group '{cluster_resource_group}' and " - f"site '{cluster_site_name}'.") - - run_as_account_id = hyperv_cluster.get('properties', {}).get('runAsAccountId') - - return run_as_account_id, machine, site_object, AzLocalInstanceTypes.HyperVToAzLocal.value - - -def process_site_type_vmware(cmd, - rg_uri, - site_name, - machine_name, - subscription_id, - resource_group_name, - site_type): - # Get VMware machine - machine_uri = ( - f"{rg_uri}/providers/Microsoft.OffAzure/VMwareSites" - f"/{site_name}/machines/{machine_name}") - machine = get_resource_by_id( - cmd, machine_uri, APIVersion.Microsoft_OffAzure.value) - if not machine: - raise CLIError( - f"Machine '{machine_name}' not in " - f"resource group '{resource_group_name}' and " - f"site '{site_name}'.") - - # Get VMware site - site_uri = ( - f"{rg_uri}/providers/Microsoft.OffAzure/VMwareSites/{site_name}") - site_object = get_resource_by_id( - cmd, site_uri, APIVersion.Microsoft_OffAzure.value) - if not site_object: - raise CLIError( - f"Machine site '{site_name}' with Type '{site_type}' " - f"not found.") - - # Get RunAsAccount - properties = machine.get('properties', {}) - if properties.get('vCenterId'): - vcenter_id_parts = properties['vCenterId'].split("/") - if len(vcenter_id_parts) < 11: - raise CLIError( - f"Invalid VMware vCenter ARM ID " - f"'{properties['vCenterId']}'") - - vcenter_resource_group = vcenter_id_parts[4] - vcenter_site_name = vcenter_id_parts[8] - vcenter_name = vcenter_id_parts[10] - - vcenter_uri = ( - f"/subscriptions/{subscription_id}/resourceGroups" - f"/{vcenter_resource_group}/providers/Microsoft.OffAzure" - f"/VMwareSites/{vcenter_site_name}/vCenters/{vcenter_name}" - ) - vmware_vcenter = get_resource_by_id( - cmd, - vcenter_uri, - APIVersion.Microsoft_OffAzure.value) - if not vmware_vcenter: - raise CLIError( - f"VMware vCenter '{vcenter_name}' not in " - f"resource group '{vcenter_resource_group}' and " - f"site '{vcenter_site_name}'.") - - run_as_account_id = vmware_vcenter.get('properties', {}).get('runAsAccountId') - - return run_as_account_id, machine, site_object, AzLocalInstanceTypes.VMwareToAzLocal.value - - -def process_amh_solution(cmd, - machine, - site_object, - project_name, - resource_group_name, - machine_name, - rg_uri): - # Validate the VM for replication - machine_props = machine.get('properties', {}) - if machine_props.get('isDeleted'): - raise CLIError( - f"Cannot migrate machine '{machine_name}' as it is marked as " - "deleted." - ) - - # Get project name from site - discovery_solution_id = ( - site_object.get('properties', {}).get('discoverySolutionId', '') - ) - if not discovery_solution_id: - raise CLIError( - "Unable to determine project from site. Invalid site " - "configuration." - ) - - if not project_name: - project_name = discovery_solution_id.split("/")[8] - - # Get the migrate project resource - migrate_project_uri = ( - f"{rg_uri}/providers/Microsoft.Migrate/migrateprojects/" - f"{project_name}" - ) - migrate_project = get_resource_by_id( - cmd, migrate_project_uri, APIVersion.Microsoft_Migrate.value - ) - if not migrate_project: - raise CLIError(f"Migrate project '{project_name}' not found.") - - # Get Data Replication Service (AMH solution) - amh_solution_name = "Servers-Migration-ServerMigration_DataReplication" - amh_solution_uri = ( - f"{rg_uri}/providers/Microsoft.Migrate/migrateprojects/" - f"{project_name}/solutions/{amh_solution_name}" - ) - amh_solution = get_resource_by_id( - cmd, - amh_solution_uri, - APIVersion.Microsoft_Migrate.value - ) - if not amh_solution: - raise CLIError( - f"No Data Replication Service Solution " - f"'{amh_solution_name}' found in resource group " - f"'{resource_group_name}' and project '{project_name}'. " - "Please verify your appliance setup." - ) - return amh_solution, migrate_project, machine_props - - -def process_replication_vault(cmd, - amh_solution, - resource_group_name): - # Validate replication vault - vault_id = ( - amh_solution.get('properties', {}) - .get('details', {}) - .get('extendedDetails', {}) - .get('vaultId') - ) - if not vault_id: - raise CLIError( - "No Replication Vault found. Please verify your Azure Migrate " - "project setup." - ) - - replication_vault_name = vault_id.split("/")[8] - replication_vault = get_resource_by_id( - cmd, vault_id, APIVersion.Microsoft_DataReplication.value - ) - if not replication_vault: - raise CLIError( - f"No Replication Vault '{replication_vault_name}' " - f"found in Resource Group '{resource_group_name}'. " - "Please verify your Azure Migrate project setup." - ) - - prov_state = replication_vault.get('properties', {}) - prov_state = prov_state.get('provisioningState') - if prov_state != ProvisioningState.Succeeded.value: - raise CLIError( - f"The Replication Vault '{replication_vault_name}' is not in a " - f"valid state. " - f"The provisioning state is '{prov_state}'. " - "Please verify your Azure Migrate project setup." - ) - return replication_vault_name - - -def process_replication_policy(cmd, - replication_vault_name, - instance_type, - rg_uri): - # Validate Policy - policy_name = f"{replication_vault_name}{instance_type}policy" - policy_uri = ( - f"{rg_uri}/providers/Microsoft.DataReplication" - f"/replicationVaults/{replication_vault_name}" - f"/replicationPolicies/{policy_name}" - ) - policy = get_resource_by_id( - cmd, policy_uri, APIVersion.Microsoft_DataReplication.value - ) - - if not policy: - raise CLIError( - f"The replication policy '{policy_name}' not found. " - "The replication infrastructure is not initialized. " - "Run the 'az migrate local replication init " - "initialize' command." - ) - prov_state = policy.get('properties', {}).get('provisioningState') - if prov_state != ProvisioningState.Succeeded.value: - raise CLIError( - f"The replication policy '{policy_name}' is not in a valid " - f"state. " - f"The provisioning state is '{prov_state}'. " - "Re-run the 'az migrate local replication init " - "initialize' command." - ) - return policy_name - - -def _validate_appliance_map_v3(app_map, app_map_v3): - # V3 might also be in list format - for item in app_map_v3: - if isinstance(item, dict): - # Check if it has ApplianceName/SiteId structure - if 'ApplianceName' in item and 'SiteId' in item: - app_map[item['ApplianceName'].lower()] = item['SiteId'] - app_map[item['ApplianceName']] = item['SiteId'] - else: - # Or it might be a single key-value pair - for key, value in item.items(): - if isinstance(value, dict) and 'SiteId' in value: - app_map[key.lower()] = value['SiteId'] - app_map[key] = value['SiteId'] - elif isinstance(value, str): - app_map[key.lower()] = value - app_map[key] = value - return app_map - - -def process_appliance_map(cmd, rg_uri, project_name): - # Access Discovery Solution to get appliance mapping - discovery_solution_name = "Servers-Discovery-ServerDiscovery" - discovery_solution_uri = ( - f"{rg_uri}/providers/Microsoft.Migrate/migrateprojects/" - f"{project_name}/solutions/{discovery_solution_name}" - ) - discovery_solution = get_resource_by_id( - cmd, discovery_solution_uri, APIVersion.Microsoft_Migrate.value - ) - - if not discovery_solution: - raise CLIError( - f"Server Discovery Solution '{discovery_solution_name}' not " - "found." - ) - - # Get Appliances Mapping - app_map = {} - extended_details = ( - discovery_solution.get('properties', {}) - .get('details', {}) - .get('extendedDetails', {}) - ) - - # Process applianceNameToSiteIdMapV2 - if 'applianceNameToSiteIdMapV2' in extended_details: - try: - app_map_v2 = json.loads( - extended_details['applianceNameToSiteIdMapV2'] - ) - if isinstance(app_map_v2, list): - for item in app_map_v2: - is_dict = isinstance(item, dict) - has_keys = ('ApplianceName' in item and - 'SiteId' in item) - if is_dict and has_keys: - app_map[item['ApplianceName'].lower()] = ( - item['SiteId'] - ) - app_map[item['ApplianceName']] = item['SiteId'] - except (json.JSONDecodeError, KeyError, TypeError) as e: - logger.warning( - "Failed to parse applianceNameToSiteIdMapV2: %s", str(e) - ) - - # Process applianceNameToSiteIdMapV3 - if 'applianceNameToSiteIdMapV3' in extended_details: - try: - app_map_v3 = json.loads( - extended_details['applianceNameToSiteIdMapV3'] - ) - if isinstance(app_map_v3, dict): - for appliance_name_key, site_info in app_map_v3.items(): - is_dict_w_site = (isinstance(site_info, dict) and - 'SiteId' in site_info) - if is_dict_w_site: - app_map[appliance_name_key.lower()] = ( - site_info['SiteId'] - ) - app_map[appliance_name_key] = site_info['SiteId'] - elif isinstance(site_info, str): - app_map[appliance_name_key.lower()] = site_info - app_map[appliance_name_key] = site_info - elif isinstance(app_map_v3, list): - app_map = _validate_appliance_map_v3( - app_map, app_map_v3 - ) - - except (json.JSONDecodeError, KeyError, TypeError) as e: - logger.warning( - "Failed to parse applianceNameToSiteIdMapV3: %s", str(e) - ) - return app_map - - -def _validate_site_ids(app_map, - source_appliance_name, - target_appliance_name): - source_site_id = ( - app_map.get(source_appliance_name) or - app_map.get(source_appliance_name.lower()) - ) - target_site_id = ( - app_map.get(target_appliance_name) or - app_map.get(target_appliance_name.lower()) - ) - - if not source_site_id: - available_appliances = list( - set(k for k in app_map if not k.islower()) - ) - if not available_appliances: - available_appliances = list(set(app_map.keys())) - raise CLIError( - f"Source appliance '{source_appliance_name}' not in " - "discovery solution. " - f"Available appliances: {','.join(available_appliances)}" - ) - - if not target_site_id: - available_appliances = list( - set(k for k in app_map if not k.islower()) - ) - if not available_appliances: - available_appliances = list(set(app_map.keys())) - raise CLIError( - f"Target appliance '{target_appliance_name}' not in " - "discovery solution. " - f"Available appliances: {','.join(available_appliances)}" - ) - return source_site_id, target_site_id - - -def _process_source_fabrics(all_fabrics, - source_appliance_name, - amh_solution, - fabric_instance_type): - source_fabric = None - source_fabric_candidates = [] - - for fabric in all_fabrics: - props = fabric.get('properties', {}) - custom_props = props.get('customProperties', {}) - fabric_name = fabric.get('name', '') - prov_state = props.get('provisioningState') - is_succeeded = prov_state == ProvisioningState.Succeeded.value - - fabric_solution_id = ( - custom_props.get('migrationSolutionId', '').rstrip('/') - ) - expected_solution_id = amh_solution.get('id', '').rstrip('/') - is_correct_solution = ( - fabric_solution_id.lower() == expected_solution_id.lower() - ) - is_correct_instance = ( - custom_props.get('instanceType') == fabric_instance_type - ) - - name_matches = ( - fabric_name.lower().startswith( - source_appliance_name.lower() - ) or - source_appliance_name.lower() in fabric_name.lower() or - fabric_name.lower() in source_appliance_name.lower() or - f"{source_appliance_name.lower()}-" in fabric_name.lower() - ) - - # Collect potential candidates even if they don't fully match - if custom_props.get('instanceType') == fabric_instance_type: - source_fabric_candidates.append({ - 'name': fabric_name, - 'state': props.get('provisioningState'), - 'solution_match': is_correct_solution, - 'name_match': name_matches - }) - - if is_succeeded and is_correct_instance and name_matches: - # If solution doesn't match, log warning but still consider it - if not is_correct_solution: - logger.warning( - "Fabric '%s' matches name and type but has different " - "solution ID", - fabric_name - ) - source_fabric = fabric - break - return source_fabric, source_fabric_candidates - - -def _handle_no_source_fabric_error(source_appliance_name, - source_fabric_candidates, - fabric_instance_type, - all_fabrics): - error_msg = ( - f"Couldn't find connected source appliance " - f"'{source_appliance_name}'.\n" - ) - if source_fabric_candidates: - error_msg += ( - f"Found {len(source_fabric_candidates)} fabric(s) with " - f"matching type '{fabric_instance_type}': \n" - ) - for candidate in source_fabric_candidates: - error_msg += ( - f" - {candidate['name']} (state: " - f"{candidate['state']}, " - ) - error_msg += ( - f"solution_match: {candidate['solution_match']}, " - ) - error_msg += f"name_match: {candidate['name_match']})\n" - error_msg += "\nPlease verify:\n" - error_msg += "1. The appliance name matches exactly\n" - error_msg += "2. The fabric is in 'Succeeded' state\n" - error_msg += ( - "3. The fabric belongs to the correct migration solution" - ) - else: - error_msg += ( - f"No fabrics found with instance type " - f"'{fabric_instance_type}'.\n" - ) - error_msg += "\nThis usually means:\n" - error_msg += ( - f"1. The source appliance '{source_appliance_name}' is not " - "properly configured\n" - ) - if fabric_instance_type == FabricInstanceTypes.VMwareInstance.value: - appliance_type = 'VMware' - else: - appliance_type = 'HyperV' - error_msg += ( - f"2. The appliance type doesn't match (expecting " - f"{appliance_type})\n" - ) - error_msg += ( - "3. The fabric creation is still in progress - wait a few " - "minutes and retry" - ) - - # List all available fabrics for debugging - if all_fabrics: - error_msg += "\n\nAvailable fabrics in resource group:\n" - for fabric in all_fabrics: - props = fabric.get('properties', {}) - custom_props = props.get('customProperties', {}) - error_msg += ( - f" - {fabric.get('name')} " - f"(type: {custom_props.get('instanceType')})\n" - ) - - raise CLIError(error_msg) - - -def process_source_fabric(cmd, - rg_uri, - app_map, - source_appliance_name, - target_appliance_name, - amh_solution, - resource_group_name, - project_name): - # Validate and get site IDs - source_site_id, target_site_id = _validate_site_ids( - app_map, - source_appliance_name, - target_appliance_name) - - # Determine instance types based on site IDs - hyperv_site_pattern = "/Microsoft.OffAzure/HyperVSites/" - vmware_site_pattern = "/Microsoft.OffAzure/VMwareSites/" - - if (hyperv_site_pattern in source_site_id and - hyperv_site_pattern in target_site_id): - instance_type = AzLocalInstanceTypes.HyperVToAzLocal.value - fabric_instance_type = FabricInstanceTypes.HyperVInstance.value - elif (vmware_site_pattern in source_site_id and - hyperv_site_pattern in target_site_id): - instance_type = AzLocalInstanceTypes.VMwareToAzLocal.value - fabric_instance_type = FabricInstanceTypes.VMwareInstance.value - else: - src_type = ( - 'VMware' if vmware_site_pattern in source_site_id - else 'HyperV' if hyperv_site_pattern in source_site_id - else 'Unknown' - ) - tgt_type = ( - 'VMware' if vmware_site_pattern in target_site_id - else 'HyperV' if hyperv_site_pattern in target_site_id - else 'Unknown' - ) - raise CLIError( - f"Error matching source '{source_appliance_name}' and target " - f"'{target_appliance_name}' appliances. Source is {src_type}, " - f"Target is {tgt_type}" - ) - - # Get healthy fabrics in the resource group - fabrics_uri = ( - f"{rg_uri}/providers/Microsoft.DataReplication/" - f"replicationFabrics" - f"?api-version={APIVersion.Microsoft_DataReplication.value}" - ) - fabrics_response = send_get_request(cmd, fabrics_uri) - all_fabrics = fabrics_response.json().get('value', []) - - if not all_fabrics: - raise CLIError( - f"No replication fabrics found in resource group " - f"'{resource_group_name}'. Please ensure that: \n" - f"1. The source appliance '{source_appliance_name}' is " - f"deployed and connected\n" - f"2. The target appliance '{target_appliance_name}' is " - f"deployed and connected\n" - f"3. Both appliances are registered with the Azure Migrate " - f"project '{project_name}'" - ) - - source_fabric, source_fabric_candidates = _process_source_fabrics( - all_fabrics, - source_appliance_name, - amh_solution, - fabric_instance_type) - - if not source_fabric: - _handle_no_source_fabric_error( - source_appliance_name, - source_fabric_candidates, - fabric_instance_type, - all_fabrics) - return source_fabric, fabric_instance_type, instance_type, all_fabrics - - -def _process_target_fabrics(all_fabrics, - target_appliance_name, - amh_solution): - # Filter for target fabric - make matching more flexible and diagnostic - target_fabric_instance_type = FabricInstanceTypes.AzLocalInstance.value - target_fabric = None - target_fabric_candidates = [] - - for fabric in all_fabrics: - props = fabric.get('properties', {}) - custom_props = props.get('customProperties', {}) - fabric_name = fabric.get('name', '') - is_succeeded = (props.get('provisioningState') == - ProvisioningState.Succeeded.value) - - fabric_solution_id = (custom_props.get('migrationSolutionId', '') - .rstrip('/')) - expected_solution_id = amh_solution.get('id', '').rstrip('/') - is_correct_solution = (fabric_solution_id.lower() == - expected_solution_id.lower()) - is_correct_instance = (custom_props.get('instanceType') == - target_fabric_instance_type) - - name_matches = ( - fabric_name.lower().startswith(target_appliance_name.lower()) or - target_appliance_name.lower() in fabric_name.lower() or - fabric_name.lower() in target_appliance_name.lower() or - f"{target_appliance_name.lower()}-" in fabric_name.lower() - ) - - # Collect potential candidates - if (custom_props.get('instanceType') == - target_fabric_instance_type): - target_fabric_candidates.append({ - 'name': fabric_name, - 'state': props.get('provisioningState'), - 'solution_match': is_correct_solution, - 'name_match': name_matches - }) - - if is_succeeded and is_correct_instance and name_matches: - if not is_correct_solution: - logger.warning( - "Fabric '%s' matches name and type but has different " - "solution ID", fabric_name) - target_fabric = fabric - break - return target_fabric, target_fabric_candidates, \ - target_fabric_instance_type - - -def _handle_no_target_fabric_error(target_appliance_name, - target_fabric_candidates, - target_fabric_instance_type): - # Provide more detailed error message - error_msg = (f"Couldn't find connected target appliance " - f"'{target_appliance_name}'.\n") - - if target_fabric_candidates: - error_msg += (f"Found {len(target_fabric_candidates)} fabric(s) " - f"with matching type " - f"'{target_fabric_instance_type}': \n") - for candidate in target_fabric_candidates: - error_msg += (f" - {candidate['name']} " - f"(state: {candidate['state']}, ") - error_msg += (f"solution_match: " - f"{candidate['solution_match']}, " - f"name_match: " - f"{candidate['name_match']})\n") - else: - error_msg += (f"No fabrics found with instance type " - f"'{target_fabric_instance_type}'.\n") - error_msg += "\nThis usually means:\n" - error_msg += (f"1. The target appliance '{target_appliance_name}' " - f"is not properly configured for Azure Local\n") - error_msg += ("2. The fabric creation is still in progress - wait " - "a few minutes and retry\n") - error_msg += ("3. The target appliance is not connected to the " - "Azure Local cluster") - - raise CLIError(error_msg) - - -def process_target_fabric(cmd, - rg_uri, - source_fabric, - fabric_instance_type, - all_fabrics, - source_appliance_name, - target_appliance_name, - amh_solution): - # Get source fabric agent (DRA) - source_fabric_name = source_fabric.get('name') - dras_uri = ( - f"{rg_uri}/providers/Microsoft.DataReplication" - f"/replicationFabrics/{source_fabric_name}/fabricAgents" - f"?api-version={APIVersion.Microsoft_DataReplication.value}" - ) - source_dras_response = send_get_request(cmd, dras_uri) - source_dras = source_dras_response.json().get('value', []) - - source_dra = None - for dra in source_dras: - props = dra.get('properties', {}) - custom_props = props.get('customProperties', {}) - if (props.get('machineName') == source_appliance_name and - custom_props.get('instanceType') == fabric_instance_type and - bool(props.get('isResponsive'))): - source_dra = dra - break - - if not source_dra: - raise CLIError( - f"The source appliance '{source_appliance_name}' is in a " - f"disconnected state.") - - target_fabric, target_fabric_candidates, \ - target_fabric_instance_type = _process_target_fabrics( - all_fabrics, - target_appliance_name, - amh_solution) - - if not target_fabric: - _handle_no_target_fabric_error( - target_appliance_name, - target_fabric_candidates, - target_fabric_instance_type - ) - - # Get target fabric agent (DRA) - target_fabric_name = target_fabric.get('name') - target_dras_uri = ( - f"{rg_uri}/providers/Microsoft.DataReplication" - f"/replicationFabrics/{target_fabric_name}/fabricAgents" - f"?api-version={APIVersion.Microsoft_DataReplication.value}" - ) - target_dras_response = send_get_request(cmd, target_dras_uri) - target_dras = target_dras_response.json().get('value', []) - - target_dra = None - for dra in target_dras: - props = dra.get('properties', {}) - custom_props = props.get('customProperties', {}) - if (props.get('machineName') == target_appliance_name and - custom_props.get('instanceType') == - target_fabric_instance_type and - bool(props.get('isResponsive'))): - target_dra = dra - break - - if not target_dra: - raise CLIError( - f"The target appliance '{target_appliance_name}' is in a " - f"disconnected state.") - - return target_fabric, source_dra, target_dra - - -def validate_replication_extension(cmd, - rg_uri, - source_fabric, - target_fabric, - replication_vault_name): - source_fabric_id = source_fabric['id'] - target_fabric_id = target_fabric['id'] - source_fabric_short_name = source_fabric_id.split('/')[-1] - target_fabric_short_name = target_fabric_id.split('/')[-1] - replication_extension_name = ( - f"{source_fabric_short_name}-{target_fabric_short_name}-" - f"MigReplicationExtn") - extension_uri = ( - f"{rg_uri}/providers/Microsoft.DataReplication" - f"/replicationVaults/{replication_vault_name}" - f"/replicationExtensions/{replication_extension_name}" - ) - replication_extension = get_resource_by_id( - cmd, extension_uri, APIVersion.Microsoft_DataReplication.value) - - if not replication_extension: - raise CLIError( - f"The replication extension '{replication_extension_name}' " - f"not found. Run 'az migrate local replication init' first.") - - extension_state = (replication_extension.get('properties', {}) - .get('provisioningState')) - - if extension_state != ProvisioningState.Succeeded.value: - raise CLIError( - f"The replication extension '{replication_extension_name}' " - f"is not ready. State: '{extension_state}'") - return replication_extension_name - - -def get_ARC_resource_bridge_info(target_fabric, migrate_project): - target_fabric_custom_props = ( - target_fabric.get('properties', {}).get('customProperties', {})) - target_cluster_id = ( - target_fabric_custom_props.get('cluster', {}) - .get('resourceName', '')) - - if not target_cluster_id: - target_cluster_id = (target_fabric_custom_props - .get('azStackHciClusterName', '')) - - if not target_cluster_id: - target_cluster_id = (target_fabric_custom_props - .get('clusterName', '')) - - # Extract custom location from target fabric - custom_location_id = (target_fabric_custom_props - .get('customLocationRegion', '')) - - if not custom_location_id: - custom_location_id = (target_fabric_custom_props - .get('customLocationId', '')) - - if not custom_location_id: - if target_cluster_id: - cluster_parts = target_cluster_id.split('/') - if len(cluster_parts) >= 5: - custom_location_region = ( - migrate_project.get('location', 'eastus')) - custom_location_id = ( - f"/subscriptions/{cluster_parts[2]}/" - f"resourceGroups/{cluster_parts[4]}/providers/" - f"Microsoft.ExtendedLocation/customLocations/" - f"{cluster_parts[-1]}-customLocation" - ) - else: - custom_location_region = ( - migrate_project.get('location', 'eastus')) - else: - custom_location_region = ( - migrate_project.get('location', 'eastus')) - else: - custom_location_region = migrate_project.get('location', 'eastus') - return custom_location_id, custom_location_region, target_cluster_id - - -def validate_target_VM_name(target_vm_name): - if len(target_vm_name) == 0 or len(target_vm_name) > 64: - raise CLIError( - "The target virtual machine name must be between 1 and 64 " - "characters long.") - - vm_name_pattern = r"^[^_\W][a-zA-Z0-9\-]{0,63}(? 240: - raise CLIError("Target VM CPU cores must be between 1 and 240.") - - if hyperv_generation == '1': - if target_vm_ram < 512 or target_vm_ram > 1048576: # 1TB - raise CLIError( - "Target VM RAM must be between 512 MB and 1048576 MB " - "(1 TB) for Generation 1 VMs.") - else: - if target_vm_ram < 32 or target_vm_ram > 12582912: # 12TB - raise CLIError( - "Target VM RAM must be between 32 MB and 12582912 MB " - "(12 TB) for Generation 2 VMs.") - - return (hyperv_generation, source_cpu_cores, is_source_dynamic_memory, - source_memory_mb, protected_item_uri) - - -def _build_custom_properties(instance_type, custom_location_id, - custom_location_region, - machine_id, disks, nics, target_vm_name, - target_resource_group_id, - target_storage_path_id, hyperv_generation, - target_vm_cpu_core, - source_cpu_cores, is_dynamic_ram_enabled, - is_source_dynamic_memory, - source_memory_mb, target_vm_ram, source_dra, - target_dra, - run_as_account_id, target_cluster_id): - """Build custom properties for protected item creation.""" - return { - "instanceType": instance_type, - "targetArcClusterCustomLocationId": custom_location_id or "", - "customLocationRegion": custom_location_region, - "fabricDiscoveryMachineId": machine_id, - "disksToInclude": [ - { - "diskId": disk["diskId"], - "diskSizeGB": disk["diskSizeGb"], - "diskFileFormat": disk["diskFileFormat"], - "isOsDisk": disk["isOSDisk"], - "isDynamic": disk["isDynamic"], - "diskPhysicalSectorSize": 512 - } - for disk in disks - ], - "targetVmName": target_vm_name, - "targetResourceGroupId": target_resource_group_id, - "storageContainerId": target_storage_path_id, - "hyperVGeneration": hyperv_generation, - "targetCpuCores": target_vm_cpu_core, - "sourceCpuCores": source_cpu_cores, - "isDynamicRam": (is_dynamic_ram_enabled - if is_dynamic_ram_enabled is not None - else is_source_dynamic_memory), - "sourceMemoryInMegaBytes": float(source_memory_mb), - "targetMemoryInMegaBytes": int(target_vm_ram), - "nicsToInclude": [ - { - "nicId": nic["nicId"], - "selectionTypeForFailover": nic["selectionTypeForFailover"], - "targetNetworkId": nic["targetNetworkId"], - "testNetworkId": nic.get("testNetworkId", "") - } - for nic in nics - ], - "dynamicMemoryConfig": { - "maximumMemoryInMegaBytes": 1048576, # Max for Gen 1 - "minimumMemoryInMegaBytes": 512, # Min for Gen 1 - "targetMemoryBufferPercentage": 20 - }, - "sourceFabricAgentName": source_dra.get('name'), - "targetFabricAgentName": target_dra.get('name'), - "runAsAccountId": run_as_account_id, - "targetHCIClusterId": target_cluster_id - } - - -# pylint: disable=too-many-locals -def create_protected_item(cmd, - subscription_id, - resource_group_name, - replication_vault_name, - machine_name, - machine_props, - target_vm_cpu_core, - target_vm_ram, - custom_location_id, - custom_location_region, - site_type, - instance_type, - disks, - nics, - target_vm_name, - target_resource_group_id, - target_storage_path_id, - is_dynamic_ram_enabled, - source_dra, - target_dra, - policy_name, - replication_extension_name, - machine_id, - run_as_account_id, - target_cluster_id): - - config_result = _handle_configuration_validation( - cmd, - subscription_id, - resource_group_name, - replication_vault_name, - machine_name, - machine_props, - target_vm_cpu_core, - target_vm_ram, - site_type - ) - (hyperv_generation, source_cpu_cores, is_source_dynamic_memory, - source_memory_mb, protected_item_uri) = config_result - - # Construct protected item properties with only the essential properties - custom_properties = _build_custom_properties( - instance_type, custom_location_id, custom_location_region, - machine_id, disks, nics, target_vm_name, target_resource_group_id, - target_storage_path_id, hyperv_generation, target_vm_cpu_core, - source_cpu_cores, is_dynamic_ram_enabled, is_source_dynamic_memory, - source_memory_mb, target_vm_ram, source_dra, target_dra, - run_as_account_id, target_cluster_id - ) - - protected_item_body = { - "properties": { - "policyName": policy_name, - "replicationExtensionName": replication_extension_name, - "customProperties": custom_properties - } - } - - create_or_update_resource( - cmd, - protected_item_uri, - APIVersion.Microsoft_DataReplication.value, - protected_item_body) - - print(f"Successfully initiated replication for machine " - f"'{machine_name}'.") diff --git a/src/migrate/azext_migrate/_params.py b/src/migrate/azext_migrate/_params.py deleted file mode 100644 index e713ee24500..00000000000 --- a/src/migrate/azext_migrate/_params.py +++ /dev/null @@ -1,185 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -from knack.arguments import CLIArgumentType -from azure.cli.core.commands.parameters import ( - get_enum_type, - get_three_state_flag, -) - - -def load_arguments(self, _): - project_name_type = CLIArgumentType( - options_list=['--project-name'], - help='Name of the Azure Migrate project.', - id_part='name' - ) - - subscription_id_type = CLIArgumentType( - options_list=['--subscription-id'], - help='Azure subscription ID. Uses the default subscription if not ' - 'specified.' - ) - - with self.argument_context('migrate') as c: - c.argument('subscription_id', subscription_id_type) - - with self.argument_context('migrate local get-discovered-server') as c: - c.argument('project_name', project_name_type, required=True) - c.argument( - 'resource_group_name', - options_list=['--resource-group-name', '--resource-group', '-g'], - help='Name of the resource group containing the Azure Migrate ' - 'project.', - required=True) - c.argument( - 'display_name', - help='Display name of the source machine to filter by.') - c.argument('source_machine_type', - arg_type=get_enum_type(['VMware', 'HyperV']), - help='Type of the source machine.') - c.argument('subscription_id', subscription_id_type) - c.argument( - 'name', - help='Internal name of the specific source machine to retrieve.') - c.argument( - 'appliance_name', - help='Name of the appliance (site) containing the machines.') - - with self.argument_context('migrate local replication init') as c: - c.argument( - 'resource_group_name', - options_list=['--resource-group-name', '--resource-group', '-g'], - help='Specifies the Resource Group of the Azure Migrate ' - 'Project.', - required=True) - c.argument( - 'project_name', - project_name_type, - required=True, - help='Specifies the name of the Azure Migrate project to be ' - 'used for server migration.') - c.argument( - 'source_appliance_name', - options_list=['--source-appliance-name'], - help='Specifies the source appliance name for the AzLocal ' - 'scenario.', - required=True) - c.argument( - 'target_appliance_name', - options_list=['--target-appliance-name'], - help='Specifies the target appliance name for the AzLocal ' - 'scenario.', - required=True) - c.argument( - 'cache_storage_account_id', - options_list=['--cache-storage-account-id', - '--cache-storage-id'], - help='Specifies the Storage Account ARM Id to be used for ' - 'private endpoint scenario.') - c.argument('subscription_id', subscription_id_type) - c.argument( - 'pass_thru', - options_list=['--pass-thru'], - arg_type=get_three_state_flag(), - help='Returns true when the command succeeds.') - - with self.argument_context('migrate local replication new') as c: - c.argument( - 'machine_id', - options_list=['--machine-id'], - help='Specifies the machine ARM ID of the discovered server to ' - 'be migrated. Required if --machine-index is not provided.', - required=False) - c.argument( - 'machine_index', - options_list=['--machine-index'], - type=int, - help='Specifies the index (1-based) of the discovered server ' - 'from the list. Required if --machine-id is not provided.') - c.argument( - 'project_name', - project_name_type, - required=False, - help='Name of the Azure Migrate project. Required when using ' - '--machine-index.') - c.argument( - 'resource_group_name', - options_list=['--resource-group-name', '--resource-group', '-g'], - help='Name of the resource group containing the Azure Migrate ' - 'project. Required when using --machine-index.') - c.argument( - 'target_storage_path_id', - options_list=['--target-storage-path-id'], - help='Specifies the storage path ARM ID where the VMs will be ' - 'stored.', - required=True) - c.argument( - 'target_vm_cpu_core', - options_list=['--target-vm-cpu-core'], - type=int, - help='Specifies the number of CPU cores.') - c.argument( - 'target_virtual_switch_id', - options_list=['--target-virtual-switch-id', '--network-id'], - help='Specifies the logical network ARM ID that the VMs will ' - 'use.') - c.argument( - 'target_test_virtual_switch_id', - options_list=['--target-test-virtual-switch-id', - '--test-network-id'], - help='Specifies the test logical network ARM ID that the VMs ' - 'will use.') - c.argument( - 'is_dynamic_memory_enabled', - options_list=['--is-dynamic-memory-enabled', '--dynamic-memory'], - arg_type=get_enum_type(['true', 'false']), - help='Specifies if RAM is dynamic or not.') - c.argument( - 'target_vm_ram', - options_list=['--target-vm-ram'], - type=int, - help='Specifies the target RAM size in MB.') - c.argument( - 'disk_to_include', - options_list=['--disk-to-include'], - nargs='+', - help='Specifies the disks on the source server to be included ' - 'for replication. Space-separated list of disk IDs.') - c.argument( - 'nic_to_include', - options_list=['--nic-to-include'], - nargs='+', - help='Specifies the NICs on the source server to be included ' - 'for replication. Space-separated list of NIC IDs.') - c.argument( - 'target_resource_group_id', - options_list=['--target-resource-group-id', '--target-rg-id'], - help='Specifies the target resource group ARM ID where the ' - 'migrated VM resources will reside.', - required=True) - c.argument( - 'target_vm_name', - options_list=['--target-vm-name'], - help='Specifies the name of the VM to be created.', - required=True) - c.argument( - 'os_disk_id', - options_list=['--os-disk-id'], - help='Specifies the operating system disk for the source server ' - 'to be migrated.') - c.argument( - 'source_appliance_name', - options_list=['--source-appliance-name'], - help='Specifies the source appliance name for the AzLocal ' - 'scenario.', - required=True) - c.argument( - 'target_appliance_name', - options_list=['--target-appliance-name'], - help='Specifies the target appliance name for the AzLocal ' - 'scenario.', - required=True) - c.argument('subscription_id', subscription_id_type) diff --git a/src/migrate/azext_migrate/azext_metadata.json b/src/migrate/azext_migrate/azext_metadata.json deleted file mode 100644 index 06012c7c942..00000000000 --- a/src/migrate/azext_migrate/azext_metadata.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "azext.minCliCoreVersion": "2.75.0" -} \ No newline at end of file diff --git a/src/migrate/azext_migrate/commands.py b/src/migrate/azext_migrate/commands.py deleted file mode 100644 index 7c94169edaf..00000000000 --- a/src/migrate/azext_migrate/commands.py +++ /dev/null @@ -1,14 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - - -def load_command_table(self, _): - # Azure Local Migration Commands - with self.command_group('migrate local') as g: - g.custom_command('get-discovered-server', 'get_discovered_server') - - with self.command_group('migrate local replication') as g: - g.custom_command('init', 'initialize_replication_infrastructure') - g.custom_command('new', 'new_local_server_replication') diff --git a/src/migrate/azext_migrate/custom.py b/src/migrate/azext_migrate/custom.py deleted file mode 100644 index cd363dcffd6..00000000000 --- a/src/migrate/azext_migrate/custom.py +++ /dev/null @@ -1,452 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -from knack.util import CLIError -from knack.log import get_logger -from azext_migrate._helpers import ( - send_get_request, -) - -logger = get_logger(__name__) - - -def get_discovered_server(cmd, - project_name, - resource_group_name, - display_name=None, - source_machine_type=None, - subscription_id=None, - name=None, - appliance_name=None): - """ - Retrieve discovered servers from the Azure Migrate project. - - Args: - cmd: The CLI command context - project_name (str): Specifies the migrate project name (required) - resource_group_name (str): Specifies the resource group name - (required) - display_name (str, optional): Specifies the source machine - display name - source_machine_type (str, optional): Specifies the source machine - type (VMware, HyperV) - subscription_id (str, optional): Specifies the subscription id - name (str, optional): Specifies the source machine name - (internal name) - appliance_name (str, optional): Specifies the appliance name - (maps to site) - - Returns: - dict: The discovered server data from the API response - - Raises: - CLIError: If required parameters are missing or the API request - fails - """ - from azext_migrate._helpers import APIVersion - from azext_migrate._get_discovered_server_helpers import ( - validate_get_discovered_server_params, - build_base_uri, - fetch_all_servers, - filter_servers_by_display_name, - extract_server_info, - print_server_info) - - # Validate required parameters - validate_get_discovered_server_params( - project_name, resource_group_name, source_machine_type) - - # Use current subscription if not provided - if not subscription_id: - from azure.cli.core.commands.client_factory import \ - get_subscription_id - subscription_id = get_subscription_id(cmd.cli_ctx) - - # Build the base URI - base_uri = build_base_uri( - subscription_id, resource_group_name, project_name, - appliance_name, name, source_machine_type) - - # Use the correct API version - api_version = (APIVersion.Microsoft_OffAzure.value if appliance_name - else APIVersion.Microsoft_Migrate.value) - - # Prepare query parameters - query_params = [f"api-version={api_version}"] - if not appliance_name and display_name: - query_params.append(f"$filter=displayName eq '{display_name}'") - - # Construct the full URI - request_uri = ( - f"{cmd.cli_ctx.cloud.endpoints.resource_manager}{base_uri}?" - f"{'&'.join(query_params)}" - ) - - try: - # Fetch all servers - values = fetch_all_servers(cmd, request_uri, send_get_request) - - # Apply client-side filtering for display_name when using site - # endpoints - if appliance_name and display_name: - values = filter_servers_by_display_name(values, display_name) - - # Format and display the discovered servers information - for index, server in enumerate(values, 1): - server_info = extract_server_info(server, index) - print_server_info(server_info) - - except Exception as e: - logger.error("Error retrieving discovered servers: %s", str(e)) - raise CLIError( - f"Failed to retrieve discovered servers: {str(e)}") - - -def initialize_replication_infrastructure(cmd, - resource_group_name, - project_name, - source_appliance_name, - target_appliance_name, - cache_storage_account_id=None, - subscription_id=None, - pass_thru=False): - """ - Initialize Azure Migrate local replication infrastructure. - - This function is based on a preview API version and may experience - breaking changes in future releases. - - Args: - cmd: The CLI command context - resource_group_name (str): Specifies the Resource Group of the - Azure Migrate Project (required) - project_name (str): Specifies the name of the Azure Migrate - project to be used for server migration (required) - source_appliance_name (str): Specifies the source appliance name - for the AzLocal scenario (required) - target_appliance_name (str): Specifies the target appliance name - for the AzLocal scenario (required) - cache_storage_account_id (str, optional): Specifies the Storage - Account ARM Id to be used for private endpoint scenario - subscription_id (str, optional): Azure Subscription ID. Uses - current subscription if not provided - pass_thru (bool, optional): Returns True when the command - succeeds - - Returns: - bool: True if the operation succeeds (when pass_thru is True), - otherwise None - - Raises: - CLIError: If required parameters are missing or the API request - fails - """ - from azure.cli.core.commands.client_factory import \ - get_subscription_id - from azext_migrate._initialize_replication_infrastructure_helpers import ( - validate_required_parameters, - execute_replication_infrastructure_setup) - - # Validate required parameters - validate_required_parameters(resource_group_name, - project_name, - source_appliance_name, - target_appliance_name) - - try: - # Use current subscription if not provided - if not subscription_id: - subscription_id = get_subscription_id(cmd.cli_ctx) - print(f"Selected Subscription Id: '{subscription_id}'") - - # Execute the complete setup workflow - return execute_replication_infrastructure_setup( - cmd, subscription_id, resource_group_name, project_name, - source_appliance_name, target_appliance_name, - cache_storage_account_id, pass_thru - ) - - except Exception as e: - logger.error( - "Error initializing replication infrastructure: %s", str(e)) - raise CLIError( - f"Failed to initialize replication infrastructure: {str(e)}") - - -# pylint: disable=too-many-locals -def new_local_server_replication(cmd, - target_storage_path_id, - target_resource_group_id, - target_vm_name, - source_appliance_name, - target_appliance_name, - machine_id=None, - machine_index=None, - project_name=None, - resource_group_name=None, - target_vm_cpu_core=None, - target_virtual_switch_id=None, - target_test_virtual_switch_id=None, - is_dynamic_memory_enabled=None, - target_vm_ram=None, - disk_to_include=None, - nic_to_include=None, - os_disk_id=None, - subscription_id=None): - """ - Create a new replication for an Azure Local server. - - This cmdlet is based on a preview API version and may experience - breaking changes in future releases. - - Args: - cmd: The CLI command context - target_storage_path_id (str): Specifies the storage path ARM ID - where the VMs will be stored (required) - target_resource_group_id (str): Specifies the target resource - group ARM ID where the migrated VM resources will reside - (required) - target_vm_name (str): Specifies the name of the VM to be created - (required) - source_appliance_name (str): Specifies the source appliance name - for the AzLocal scenario (required) - target_appliance_name (str): Specifies the target appliance name - for the AzLocal scenario (required) - machine_id (str, optional): Specifies the machine ARM ID of the - discovered server to be migrated (required if machine_index - not provided) - machine_index (int, optional): Specifies the index of the - discovered server from the list (1-based, required if - machine_id not provided) - project_name (str, optional): Specifies the migrate project name - (required when using machine_index) - resource_group_name (str, optional): Specifies the resource group - name (required when using machine_index) - target_vm_cpu_core (int, optional): Specifies the number of CPU - cores - target_virtual_switch_id (str, optional): Specifies the logical - network ARM ID that the VMs will use (required for default - user mode) - target_test_virtual_switch_id (str, optional): Specifies the test - logical network ARM ID that the VMs will use - is_dynamic_memory_enabled (str, optional): Specifies if RAM is - dynamic or not. Valid values: 'true', 'false' - target_vm_ram (int, optional): Specifies the target RAM size in - MB - disk_to_include (list, optional): Specifies the disks on the - source server to be included for replication (power user - mode) - nic_to_include (list, optional): Specifies the NICs on the source - server to be included for replication (power user mode) - os_disk_id (str, optional): Specifies the operating system disk - for the source server to be migrated (required for default - user mode) - subscription_id (str, optional): Azure Subscription ID. Uses - current subscription if not provided - - Returns: - dict: The job model from the API response - - Raises: - CLIError: If required parameters are missing or validation fails - """ - from azext_migrate._helpers import SiteTypes - from azext_migrate._new_local_server_replication_helpers import ( - validate_server_parameters, - validate_required_parameters, - validate_ARM_id_formats, - process_site_type_hyperV, - process_site_type_vmware, - process_amh_solution, - process_replication_vault, - process_replication_policy, - process_appliance_map, - process_source_fabric, - process_target_fabric, - validate_replication_extension, - get_ARC_resource_bridge_info, - validate_target_VM_name, - construct_disk_and_nic_mapping, - create_protected_item) - - rg_uri = validate_server_parameters( - cmd, - machine_id, - machine_index, - project_name, - resource_group_name, - source_appliance_name, - subscription_id) - - is_dynamic_ram_enabled, is_power_user_mode = \ - validate_required_parameters( - machine_id, - target_storage_path_id, - target_resource_group_id, - target_vm_name, - source_appliance_name, - target_appliance_name, - disk_to_include, - nic_to_include, - target_virtual_switch_id, - os_disk_id, - is_dynamic_memory_enabled) - - try: - site_type, site_name, machine_name, run_as_account_id, \ - instance_type, resource_group_name = validate_ARM_id_formats( - machine_id, - target_storage_path_id, - target_resource_group_id, - target_virtual_switch_id, - target_test_virtual_switch_id) - - if site_type == SiteTypes.HyperVSites.value: - run_as_account_id, machine, site_object, instance_type = \ - process_site_type_hyperV( - cmd, - rg_uri, - site_name, - machine_name, - subscription_id, - resource_group_name, - site_type) - - elif site_type == SiteTypes.VMwareSites.value: - run_as_account_id, machine, site_object, instance_type = \ - process_site_type_vmware( - cmd, - rg_uri, - site_name, - machine_name, - subscription_id, - resource_group_name, - site_type) - - else: - raise CLIError( - f"Site type of '{site_type}' in -machine_id is not " - f"supported. Only '{SiteTypes.HyperVSites.value}' and " - f"'{SiteTypes.VMwareSites.value}' are supported.") - - if not run_as_account_id: - raise CLIError( - f"Unable to determine RunAsAccount for " - f"site '{site_name}' from machine '{machine_name}'. " - "Please verify your appliance setup and provided " - "-machine_id.") - - amh_solution, migrate_project, machine_props = process_amh_solution( - cmd, - machine, - site_object, - project_name, - resource_group_name, - machine_name, - rg_uri - ) - - replication_vault_name = process_replication_vault( - cmd, - amh_solution, - resource_group_name) - - policy_name = process_replication_policy( - cmd, - replication_vault_name, - instance_type, - rg_uri - ) - app_map = process_appliance_map(cmd, rg_uri, project_name) - - if not app_map: - raise CLIError( - "Server Discovery Solution missing Appliance Details. " - "Invalid Solution.") - - source_fabric, fabric_instance_type, instance_type, \ - all_fabrics = process_source_fabric( - cmd, - rg_uri, - app_map, - source_appliance_name, - target_appliance_name, - amh_solution, - resource_group_name, - project_name - ) - - target_fabric, source_dra, target_dra = process_target_fabric( - cmd, - rg_uri, - source_fabric, - fabric_instance_type, - all_fabrics, - source_appliance_name, - target_appliance_name, - amh_solution) - - # 2. Validate Replication Extension - replication_extension_name = validate_replication_extension( - cmd, - rg_uri, - source_fabric, - target_fabric, - replication_vault_name - ) - - # 3. Get ARC Resource Bridge info - custom_location_id, custom_location_region, \ - target_cluster_id = get_ARC_resource_bridge_info( - target_fabric, - migrate_project - ) - - # 4. Validate target VM name - validate_target_VM_name(target_vm_name) - - # 5. Construct disk and NIC mappings - disks, nics = construct_disk_and_nic_mapping( - is_power_user_mode, - disk_to_include, - nic_to_include, - machine_props, - site_type, - os_disk_id, - target_virtual_switch_id, - target_test_virtual_switch_id) - - # 6. Create the protected item - create_protected_item( - cmd, - subscription_id, - resource_group_name, - replication_vault_name, - machine_name, - machine_props, - target_vm_cpu_core, - target_vm_ram, - custom_location_id, - custom_location_region, - site_type, - instance_type, - disks, - nics, - target_vm_name, - target_resource_group_id, - target_storage_path_id, - is_dynamic_ram_enabled, - source_dra, - target_dra, - policy_name, - replication_extension_name, - machine_id, - run_as_account_id, - target_cluster_id - ) - - except Exception as e: - logger.error("Error creating replication: %s", str(e)) - raise diff --git a/src/migrate/azext_migrate/tests/__init__.py b/src/migrate/azext_migrate/tests/__init__.py deleted file mode 100644 index 34913fb394d..00000000000 --- a/src/migrate/azext_migrate/tests/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- diff --git a/src/migrate/azext_migrate/tests/latest/__init__.py b/src/migrate/azext_migrate/tests/latest/__init__.py deleted file mode 100644 index 34913fb394d..00000000000 --- a/src/migrate/azext_migrate/tests/latest/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- diff --git a/src/migrate/azext_migrate/tests/latest/test_migrate_commands.py b/src/migrate/azext_migrate/tests/latest/test_migrate_commands.py deleted file mode 100644 index d05a286fea1..00000000000 --- a/src/migrate/azext_migrate/tests/latest/test_migrate_commands.py +++ /dev/null @@ -1,1130 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -import unittest -from unittest import mock -from azure.cli.testsdk import ScenarioTest, record_only -from azure.cli.core.util import CLIError -from knack.util import CLIError as KnackCLIError -import pytest - - -class MigrateGetDiscoveredServerTests(ScenarioTest): - """Unit tests for the 'az migrate local get-discovered-server' command""" - - def setUp(self): - super(MigrateGetDiscoveredServerTests, self).setUp() - self.mock_subscription_id = "00000000-0000-0000-0000-000000000000" - self.mock_rg_name = "test-rg" - self.mock_project_name = "test-project" - self.mock_appliance_name = "test-appliance" - - def _create_mock_response(self, data): - """Helper to create a mock response object""" - mock_response = mock.Mock() - mock_response.json.return_value = data - return mock_response - - def _create_sample_server_data(self, index=1, - machine_name="test-machine", - display_name="TestServer"): - """Helper to create sample discovered server data""" - return { - 'id': (f'/subscriptions/sub-id/resourceGroups/rg/providers/' - f'Microsoft.Migrate/migrateprojects/project/machines/' - f'machine-{index}'), - 'name': f'machine-{index}', - 'properties': { - 'displayName': display_name, - 'discoveryData': [ - { - 'machineName': machine_name, - 'ipAddresses': ['192.168.1.10'], - 'osName': 'Windows Server 2019', - 'extendedInfo': { - 'bootType': 'UEFI', - 'diskDetails': '[{"InstanceId": "disk-0"}]' - } - } - ] - } - } - - def _create_mock_cmd(self, command_name='migrate local get-discovered-server'): - """Helper to create a properly configured mock cmd object""" - mock_cmd = mock.Mock() - mock_cmd.cli_ctx.cloud.endpoints.resource_manager = ( - "https://management.azure.com") - mock_cmd.cli_ctx.cloud.endpoints.active_directory_resource_id = ( - "https://management.core.windows.net/") - mock_cmd.cli_ctx.data = {'command': command_name} - return mock_cmd - - @mock.patch( - 'azext_migrate._get_discovered_server_helpers.fetch_all_servers') - @mock.patch( - 'azure.cli.core.commands.client_factory.get_subscription_id') - def test_get_discovered_server_list_all(self, mock_get_sub_id, - mock_fetch_servers): - """Test listing all discovered servers in a project""" - from azext_migrate.custom import ( - get_discovered_server) - - # Setup mocks - mock_get_sub_id.return_value = self.mock_subscription_id - # Mock the fetch_all_servers to return server data directly - mock_fetch_servers.return_value = [ - self._create_sample_server_data(1, "machine-1", "Server1"), - self._create_sample_server_data(2, "machine-2", "Server2") - ] - - # Create a minimal mock cmd object - mock_cmd = self._create_mock_cmd() - - # Execute the command - result = get_discovered_server( - cmd=mock_cmd, - project_name=self.mock_project_name, - resource_group_name=self.mock_rg_name - ) - - # Verify the fetch_all_servers was called correctly - mock_fetch_servers.assert_called_once() - call_args = mock_fetch_servers.call_args - # Check that the request_uri contains expected components - request_uri = call_args[0][1] # Second argument is request_uri - self.assertIn(self.mock_project_name, request_uri) - self.assertIn(self.mock_rg_name, request_uri) - self.assertIn('/machines?', request_uri) - - @mock.patch( - 'azext_migrate._get_discovered_server_helpers.fetch_all_servers') - @mock.patch( - 'azure.cli.core.commands.client_factory.get_subscription_id') - def test_get_discovered_server_with_display_name_filter( - self, mock_get_sub_id, mock_fetch_servers): - """Test filtering discovered servers by display name""" - from azext_migrate.custom import ( - get_discovered_server) - - mock_get_sub_id.return_value = self.mock_subscription_id - target_display_name = "WebServer" - # Mock fetch_all_servers to return server data directly - mock_fetch_servers.return_value = [self._create_sample_server_data( - 1, "machine-1", target_display_name)] - - mock_cmd = self._create_mock_cmd() - - result = get_discovered_server( - cmd=mock_cmd, - project_name=self.mock_project_name, - resource_group_name=self.mock_rg_name, - display_name=target_display_name - ) - - # Verify the filter was applied in the URL - call_args = mock_fetch_servers.call_args - self.assertIn("$filter", call_args[0][1]) - self.assertIn(target_display_name, call_args[0][1]) - - @mock.patch( - 'azext_migrate._get_discovered_server_helpers.fetch_all_servers') - @mock.patch( - 'azure.cli.core.commands.client_factory.get_subscription_id') - def test_get_discovered_server_with_appliance_vmware( - self, mock_get_sub_id, mock_fetch_servers): - """Test getting servers from a specific VMware appliance""" - from azext_migrate.custom import get_discovered_server - - mock_get_sub_id.return_value = self.mock_subscription_id - # Mock fetch_all_servers to return server data directly - mock_fetch_servers.return_value = [self._create_sample_server_data(1)] - - mock_cmd = self._create_mock_cmd() - - get_discovered_server( - cmd=mock_cmd, - project_name=self.mock_project_name, - resource_group_name=self.mock_rg_name, - appliance_name=self.mock_appliance_name, - source_machine_type="VMware" - ) - - # Verify VMwareSites endpoint was used - call_args = mock_fetch_servers.call_args - self.assertIn("VMwareSites", call_args[0][1]) - self.assertIn(self.mock_appliance_name, call_args[0][1]) - - @mock.patch( - 'azext_migrate._get_discovered_server_helpers.fetch_all_servers') - @mock.patch( - 'azure.cli.core.commands.client_factory.get_subscription_id') - def test_get_discovered_server_with_appliance_hyperv( - self, mock_get_sub_id, mock_fetch_servers): - """Test getting servers from a specific HyperV appliance""" - from azext_migrate.custom import get_discovered_server - - mock_get_sub_id.return_value = self.mock_subscription_id - # Mock fetch_all_servers to return server data directly - mock_fetch_servers.return_value = [self._create_sample_server_data(1)] - - mock_cmd = self._create_mock_cmd() - - result = get_discovered_server( - cmd=mock_cmd, - project_name=self.mock_project_name, - resource_group_name=self.mock_rg_name, - appliance_name=self.mock_appliance_name, - source_machine_type="HyperV" - ) - - # Verify HyperVSites endpoint was used - call_args = mock_fetch_servers.call_args - self.assertIn("HyperVSites", call_args[0][1]) - self.assertIn(self.mock_appliance_name, call_args[0][1]) - - @mock.patch( - 'azext_migrate._get_discovered_server_helpers.fetch_all_servers') - @mock.patch( - 'azure.cli.core.commands.client_factory.get_subscription_id') - def test_get_discovered_server_specific_machine( - self, mock_get_sub_id, mock_fetch_servers): - """Test getting a specific machine by name""" - from azext_migrate.custom import get_discovered_server - - mock_get_sub_id.return_value = self.mock_subscription_id - specific_name = "machine-12345" - # Mock fetch_all_servers to return server data directly - mock_fetch_servers.return_value = [self._create_sample_server_data(1, specific_name, "SpecificServer")] - - mock_cmd = self._create_mock_cmd() - - result = get_discovered_server( - cmd=mock_cmd, - project_name=self.mock_project_name, - resource_group_name=self.mock_rg_name, - name=specific_name - ) - - # Verify the specific machine endpoint was used - call_args = mock_fetch_servers.call_args - self.assertIn(f"/machines/{specific_name}?", call_args[0][1]) - - @mock.patch( - 'azext_migrate._get_discovered_server_helpers.fetch_all_servers') - @mock.patch( - 'azure.cli.core.commands.client_factory.get_subscription_id') - def test_get_discovered_server_with_pagination(self, mock_get_sub_id, - mock_fetch_servers): - """Test handling paginated results""" - from azext_migrate.custom import get_discovered_server - - mock_get_sub_id.return_value = self.mock_subscription_id - - # Mock fetch_all_servers to return combined server data from both pages - mock_fetch_servers.return_value = [ - self._create_sample_server_data(1), - self._create_sample_server_data(2) - ] - - mock_cmd = self._create_mock_cmd() - - get_discovered_server( - cmd=mock_cmd, - project_name=self.mock_project_name, - resource_group_name=self.mock_rg_name - ) - - # Verify fetch_all_servers was called once - # (the pagination logic is handled inside fetch_all_servers) - mock_fetch_servers.assert_called_once() - - def test_get_discovered_server_missing_project_name(self): - """Test error handling when project_name is missing""" - from azext_migrate.custom import get_discovered_server - - mock_cmd = self._create_mock_cmd() - - with self.assertRaises((CLIError, KnackCLIError)) as context: - get_discovered_server( - cmd=mock_cmd, - project_name=None, - resource_group_name=self.mock_rg_name - ) - - self.assertIn("project_name", str(context.exception)) - - def test_get_discovered_server_missing_resource_group(self): - """Test error handling when resource_group_name is missing""" - from azext_migrate.custom import get_discovered_server - - mock_cmd = self._create_mock_cmd() - - with self.assertRaises((CLIError, KnackCLIError)) as context: - get_discovered_server( - cmd=mock_cmd, - project_name=self.mock_project_name, - resource_group_name=None - ) - - self.assertIn("resource_group_name", str(context.exception)) - - def test_get_discovered_server_invalid_machine_type(self): - """Test error handling for invalid source_machine_type""" - from azext_migrate.custom import get_discovered_server - - mock_cmd = self._create_mock_cmd() - - with self.assertRaises((CLIError, KnackCLIError)) as context: - get_discovered_server( - cmd=mock_cmd, - project_name=self.mock_project_name, - resource_group_name=self.mock_rg_name, - source_machine_type="InvalidType" - ) - - self.assertIn("VMware", str(context.exception)) - self.assertIn("HyperV", str(context.exception)) - - -class MigrateReplicationInitTests(ScenarioTest): - """Unit tests for the 'az migrate local replication init' command""" - - def setUp(self): - super(MigrateReplicationInitTests, self).setUp() - self.mock_subscription_id = "00000000-0000-0000-0000-000000000000" - self.mock_rg_name = "test-rg" - self.mock_project_name = "test-project" - self.mock_source_appliance = "vmware-appliance" - self.mock_target_appliance = "azlocal-appliance" - - def _create_mock_cmd(self, command_name='migrate local replication init'): - """Helper to create a mock cmd object""" - mock_cmd = mock.Mock() - mock_cmd.cli_ctx.cloud.endpoints.resource_manager = ( - "https://management.azure.com") - mock_cmd.cli_ctx.cloud.endpoints.active_directory_resource_id = ( - "https://management.core.windows.net/") - mock_cmd.cli_ctx.data = {'command': command_name} - return mock_cmd - - def _create_mock_resource_group(self): - """Helper to create mock resource group response""" - return { - 'id': (f'/subscriptions/{self.mock_subscription_id}/' - f'resourceGroups/{self.mock_rg_name}'), - 'name': self.mock_rg_name, - 'location': 'eastus' - } - - def _create_mock_migrate_project(self): - """Helper to create mock migrate project response""" - return { - 'id': (f'/subscriptions/{self.mock_subscription_id}/' - f'resourceGroups/{self.mock_rg_name}/providers/' - f'Microsoft.Migrate/migrateprojects/' - f'{self.mock_project_name}'), - 'name': self.mock_project_name, - 'location': 'eastus', - 'properties': { - 'provisioningState': 'Succeeded' - } - } - - def _create_mock_solution(self, solution_name, vault_id=None, - storage_account_id=None): - """Helper to create mock solution response""" - extended_details = { - 'applianceNameToSiteIdMapV2': ( - '[{"ApplianceName": "vmware-appliance", ' - '"SiteId": "/subscriptions/sub/resourceGroups/rg/providers/' - 'Microsoft.OffAzure/VMwareSites/vmware-site"}]'), - 'applianceNameToSiteIdMapV3': ( - '{"azlocal-appliance": {"SiteId": ' - '"/subscriptions/sub/resourceGroups/rg/providers/' - 'Microsoft.OffAzure/HyperVSites/azlocal-site"}}') - } - - if vault_id: - extended_details['vaultId'] = vault_id - if storage_account_id: - extended_details['replicationStorageAccountId'] = ( - storage_account_id) - - return { - 'id': (f'/subscriptions/{self.mock_subscription_id}/' - f'resourceGroups/{self.mock_rg_name}/providers/' - f'Microsoft.Migrate/migrateprojects/' - f'{self.mock_project_name}/solutions/{solution_name}'), - 'name': solution_name, - 'properties': { - 'details': { - 'extendedDetails': extended_details - } - } - } - - def _create_mock_vault(self, with_identity=True): - """Helper to create mock replication vault response""" - vault = { - 'id': (f'/subscriptions/{self.mock_subscription_id}/' - f'resourceGroups/{self.mock_rg_name}/providers/' - f'Microsoft.DataReplication/replicationVaults/' - f'test-vault'), - 'name': 'test-vault', - 'properties': { - 'provisioningState': 'Succeeded' - } - } - - if with_identity: - vault['identity'] = { - 'type': 'SystemAssigned', - 'principalId': '11111111-1111-1111-1111-111111111111' - } - - return vault - - def _create_mock_fabric(self, fabric_name, instance_type, - appliance_name): - """Helper to create mock fabric response""" - return { - 'id': (f'/subscriptions/{self.mock_subscription_id}/' - f'resourceGroups/{self.mock_rg_name}/providers/' - f'Microsoft.DataReplication/replicationFabrics/' - f'{fabric_name}'), - 'name': fabric_name, - 'properties': { - 'provisioningState': 'Succeeded', - 'customProperties': { - 'instanceType': instance_type, - 'migrationSolutionId': ( - f'/subscriptions/{self.mock_subscription_id}/' - f'resourceGroups/{self.mock_rg_name}/providers/' - f'Microsoft.Migrate/migrateprojects/' - f'{self.mock_project_name}/solutions/' - f'Servers-Migration-ServerMigration_DataReplication') - } - } - } - - def _create_mock_dra(self, appliance_name, instance_type): - """Helper to create mock DRA (fabric agent) response""" - return { - 'id': (f'/subscriptions/{self.mock_subscription_id}/' - f'resourceGroups/{self.mock_rg_name}/providers/' - f'Microsoft.DataReplication/replicationFabrics/' - f'fabric/fabricAgents/dra'), - 'name': 'dra', - 'properties': { - 'machineName': appliance_name, - 'isResponsive': True, - 'customProperties': { - 'instanceType': instance_type - }, - 'resourceAccessIdentity': { - 'objectId': '22222222-2222-2222-2222-222222222222' - } - } - } - - @mock.patch( - 'azure.cli.core.commands.client_factory.get_mgmt_service_client') - @mock.patch( - 'azext_migrate._helpers.' - 'create_or_update_resource') - @mock.patch( - 'azext_migrate._get_discovered_server_helpers.fetch_all_servers') - @mock.patch( - 'azext_migrate._helpers.get_resource_by_id') - @mock.patch( - 'azure.cli.core.commands.client_factory.get_subscription_id') - @mock.patch('time.sleep') - def test_initialize_replication_infrastructure_success( - self, mock_sleep, mock_get_sub_id, - mock_get_resource, mock_fetch_servers, - mock_create_or_update, mock_get_client): - """Test successful initialization of replication infrastructure""" - from azext_migrate.custom import initialize_replication_infrastructure - - # Setup mocks - mock_get_sub_id.return_value = self.mock_subscription_id - - vault_id = (f'/subscriptions/{self.mock_subscription_id}/' - f'resourceGroups/{self.mock_rg_name}/providers/' - f'Microsoft.DataReplication/replicationVaults/' - f'test-vault') - - # Mock get_resource_by_id calls in sequence - mock_get_resource.side_effect = [ - self._create_mock_resource_group(), # Resource group - self._create_mock_migrate_project(), # Migrate project - self._create_mock_solution( - 'Servers-Migration-ServerMigration_DataReplication', - vault_id=vault_id), # AMH solution - self._create_mock_vault(with_identity=True), # Vault - self._create_mock_solution( - 'Servers-Discovery-ServerDiscovery'), # Discovery solution - None, # Policy (doesn't exist initially - will be created) - {'properties': {'provisioningState': 'Succeeded'}}, # Policy - {'id': vault_id, - 'properties': {'provisioningState': 'Succeeded'}}, # Storage - None, # Extension doesn't exist - ] - - # Mock send_get_request for listing fabrics and DRAs - mock_fetch_servers.side_effect = [ - # Fabrics list - self._create_mock_response({ - 'value': [ - self._create_mock_fabric( - 'vmware-appliance-fabric', - 'HyperVToAzStackHCI', - 'vmware-appliance'), - self._create_mock_fabric( - 'azlocal-appliance-fabric', - 'AzStackHCIInstance', - 'azlocal-appliance') - ] - }), - # Source DRAs - self._create_mock_response({ - 'value': [self._create_mock_dra( - 'vmware-appliance', 'HyperVToAzStackHCI')] - }), - # Target DRAs - self._create_mock_response({ - 'value': [self._create_mock_dra( - 'azlocal-appliance', 'AzStackHCIInstance')] - }) - ] - - # Mock authorization client - mock_auth_client = mock.Mock() - mock_auth_client.role_assignments.list_for_scope.return_value = [] - mock_auth_client.role_assignments.create.return_value = None - mock_get_client.return_value = mock_auth_client - - mock_cmd = self._create_mock_cmd() - - # Note: This test will fail at storage account creation, - # but validates the main logic path - with self.assertRaises(Exception): - initialize_replication_infrastructure( - cmd=mock_cmd, - resource_group_name=self.mock_rg_name, - project_name=self.mock_project_name, - source_appliance_name=self.mock_source_appliance, - target_appliance_name=self.mock_target_appliance - ) - - def _create_mock_response(self, data): - """Helper to create a mock response object""" - mock_response = mock.Mock() - mock_response.json.return_value = data - return mock_response - - def test_initialize_replication_missing_resource_group(self): - """Test error when resource_group_name is missing""" - from azext_migrate.custom import ( - initialize_replication_infrastructure) - - mock_cmd = self._create_mock_cmd() - - with self.assertRaises((CLIError, KnackCLIError)) as context: - initialize_replication_infrastructure( - cmd=mock_cmd, - resource_group_name=None, - project_name=self.mock_project_name, - source_appliance_name=self.mock_source_appliance, - target_appliance_name=self.mock_target_appliance - ) - - self.assertIn("resource_group_name", str(context.exception)) - - def test_initialize_replication_missing_project_name(self): - """Test error when project_name is missing""" - from azext_migrate.custom import ( - initialize_replication_infrastructure) - - mock_cmd = self._create_mock_cmd() - - with self.assertRaises((CLIError, KnackCLIError)) as context: - initialize_replication_infrastructure( - cmd=mock_cmd, - resource_group_name=self.mock_rg_name, - project_name=None, - source_appliance_name=self.mock_source_appliance, - target_appliance_name=self.mock_target_appliance - ) - - self.assertIn("project_name", str(context.exception)) - - def test_initialize_replication_missing_source_appliance(self): - """Test error when source_appliance_name is missing""" - from azext_migrate.custom import ( - initialize_replication_infrastructure) - - mock_cmd = self._create_mock_cmd() - - with self.assertRaises((CLIError, KnackCLIError)) as context: - initialize_replication_infrastructure( - cmd=mock_cmd, - resource_group_name=self.mock_rg_name, - project_name=self.mock_project_name, - source_appliance_name=None, - target_appliance_name=self.mock_target_appliance - ) - - self.assertIn("source_appliance_name", str(context.exception)) - - def test_initialize_replication_missing_target_appliance(self): - """Test error when target_appliance_name is missing""" - from azext_migrate.custom import ( - initialize_replication_infrastructure) - - mock_cmd = self._create_mock_cmd() - - with self.assertRaises((CLIError, KnackCLIError)) as context: - initialize_replication_infrastructure( - cmd=mock_cmd, - resource_group_name=self.mock_rg_name, - project_name=self.mock_project_name, - source_appliance_name=self.mock_source_appliance, - target_appliance_name=None - ) - - self.assertIn("target_appliance_name", str(context.exception)) - - -class MigrateReplicationNewTests(ScenarioTest): - """Unit tests for the 'az migrate local replication new' command""" - - def setUp(self): - super(MigrateReplicationNewTests, self).setUp() - self.mock_subscription_id = "00000000-0000-0000-0000-000000000000" - self.mock_rg_name = "test-rg" - self.mock_project_name = "test-project" - self.mock_machine_id = ( - f"/subscriptions/{self.mock_subscription_id}" - f"/resourceGroups/{self.mock_rg_name}/providers" - f"/Microsoft.Migrate/migrateprojects/" - f"{self.mock_project_name}/machines/machine-12345") - - def _create_mock_cmd(self, command_name='migrate local replication new'): - """Helper to create a mock cmd object""" - mock_cmd = mock.Mock() - mock_cmd.cli_ctx.cloud.endpoints.resource_manager = ( - "https://management.azure.com") - mock_cmd.cli_ctx.cloud.endpoints.active_directory_resource_id = ( - "https://management.core.windows.net/") - mock_cmd.cli_ctx.data = {'command': command_name} - return mock_cmd - - def test_new_replication_missing_machine_identifier(self): - """Test error when neither machine_id nor machine_index is provided - """ - from azext_migrate.custom import ( - new_local_server_replication) - - mock_cmd = self._create_mock_cmd() - - # Note: The actual implementation may have this validation - # This test documents the expected behavior - try: - new_local_server_replication( - cmd=mock_cmd, - machine_id=None, - machine_index=None, - target_storage_path_id=("/subscriptions/sub/resourceGroups" - "/rg/providers/" - "Microsoft.AzureStackHCI" - "/storageContainers/storage"), - target_resource_group_id=("/subscriptions/sub/resourceGroups/" - "target-rg"), - target_vm_name="test-vm", - source_appliance_name="source-appliance", - target_appliance_name="target-appliance" - ) - except (CLIError, KnackCLIError, Exception) as e: - # Expected to fail - # Either machine_id or machine_index should be provided - pass - - def test_new_replication_machine_index_without_project(self): - """Test error when machine_index is provided without project_name""" - from azext_migrate.custom import ( - new_local_server_replication) - - mock_cmd = self._create_mock_cmd() - - try: - new_local_server_replication( - cmd=mock_cmd, - machine_id=None, - machine_index=1, - project_name=None, # Missing - resource_group_name=None, # Missing - target_storage_path_id=("/subscriptions/sub/resourceGroups" - "/rg/providers/" - "Microsoft.AzureStackHCI" - "/storageContainers/storage"), - target_resource_group_id=("/subscriptions/sub/resourceGroups/" - "target-rg"), - target_vm_name="test-vm", - source_appliance_name="source-appliance", - target_appliance_name="target-appliance" - ) - except (CLIError, KnackCLIError, Exception) as e: - # Expected to fail - pass - - @mock.patch( - 'azext_migrate._helpers.send_get_request') - @mock.patch( - 'azext_migrate._helpers.get_resource_by_id') - @mock.patch( - 'azure.cli.core.commands.client_factory.get_subscription_id') - def test_new_replication_with_machine_index(self, - mock_get_sub_id, - mock_get_resource, - mock_send_get): - """Test creating replication using machine_index""" - from azext_migrate.custom import ( - new_local_server_replication) - - # Setup mocks - mock_get_sub_id.return_value = self.mock_subscription_id - - # Mock discovery solution - mock_get_resource.return_value = { - 'id': (f'/subscriptions/{self.mock_subscription_id}/' - f'resourceGroups/{self.mock_rg_name}/providers/' - f'Microsoft.Migrate/migrateprojects/' - f'{self.mock_project_name}/solutions/' - f'Servers-Discovery-ServerDiscovery'), - 'properties': { - 'details': { - 'extendedDetails': { - 'applianceNameToSiteIdMapV2': ( - '[{"ApplianceName": "source-appliance", ' - '"SiteId": "/subscriptions/sub/resourceGroups/rg' - '/providers/Microsoft.OffAzure/VMwareSites/' - 'vmware-site"}]') - } - } - } - } - - # Mock machines list response - mock_response = mock.Mock() - mock_response.json.return_value = { - 'value': [ - { - 'id': self.mock_machine_id, - 'name': 'machine-12345', - 'properties': {'displayName': 'TestMachine'} - } - ] - } - mock_send_get.return_value = mock_response - - mock_cmd = self._create_mock_cmd() - - # This will fail at a later stage, but tests the machine_index logic - exception_caught = None - try: - new_local_server_replication( - cmd=mock_cmd, - machine_id=None, - machine_index=1, - project_name=self.mock_project_name, - resource_group_name=self.mock_rg_name, - target_storage_path_id=("/subscriptions/sub/resourceGroups/" - "rg/providers/" - "Microsoft.AzureStackHCI/" - "storageContainers/storage"), - target_resource_group_id=("/subscriptions/sub/resourceGroups/" - "target-rg"), - target_vm_name="test-vm", - source_appliance_name="source-appliance", - target_appliance_name="target-appliance", - os_disk_id="disk-0", - target_virtual_switch_id=("/subscriptions/sub/resourceGroups/" - "rg/providers/" - "Microsoft.AzureStackHCI/" - "logicalNetworks/network") - ) - except Exception as e: - # Expected to fail at resource creation, - # but validates parameter handling - exception_caught = e - - # The test should pass if either: - # 1. The mocks were called as expected (normal case) - # 2. The function failed early due to missing mocks for later stages - if mock_get_resource.called and mock_send_get.called: - # Best case - the validation logic was executed - self.assertTrue(True) - else: - # If mocks weren't called, ensure we got some expected exception - # indicating the function at least tried to execute - self.assertIsNotNone(exception_caught, - "Function should have either called mocks or raised an exception") - - def test_new_replication_required_parameters_default_mode(self): - """Test that required parameters for default user mode are - validated""" - from azext_migrate.custom import ( - new_local_server_replication) - - mock_cmd = self._create_mock_cmd() - - # Default mode requires: os_disk_id and target_virtual_switch_id - # This test documents the expected required parameters - required_params = { - 'cmd': mock_cmd, - 'machine_id': self.mock_machine_id, - 'target_storage_path_id': ("/subscriptions/sub/resourceGroups/" - "rg/providers/" - "Microsoft.AzureStackHCI/" - "storageContainers/storage"), - 'target_resource_group_id': ("/subscriptions/sub/resourceGroups/" - "target-rg"), - 'target_vm_name': "test-vm", - 'source_appliance_name': "source-appliance", - 'target_appliance_name': "target-appliance", - 'os_disk_id': "disk-0", - 'target_virtual_switch_id': ("/subscriptions/sub/resourceGroups/" - "rg/providers/" - "Microsoft.AzureStackHCI/" - "logicalNetworks/network") - } - - try: - new_local_server_replication(**required_params) - except Exception as e: - # Expected to fail at later stages - pass - - def test_new_replication_required_parameters_power_user_mode(self): - """Test that required parameters for power user mode are - validated""" - from azext_migrate.custom import ( - new_local_server_replication) - - mock_cmd = self._create_mock_cmd() - - # Power user mode requires: disk_to_include and nic_to_include - required_params = { - 'cmd': mock_cmd, - 'machine_id': self.mock_machine_id, - 'target_storage_path_id': ("/subscriptions/sub/resourceGroups/" - "rg/providers/" - "Microsoft.AzureStackHCI/" - "storageContainers/storage"), - 'target_resource_group_id': ("/subscriptions/sub/resourceGroups/" - "target-rg"), - 'target_vm_name': "test-vm", - 'source_appliance_name': "source-appliance", - 'target_appliance_name': "target-appliance", - 'disk_to_include': ["disk-0", "disk-1"], - 'nic_to_include': ["nic-0"] - } - - try: - new_local_server_replication(**required_params) - except Exception as e: - # Expected to fail at later stages - pass - - -class MigrateScenarioTests(ScenarioTest): - @pytest.mark.skip(reason="Requires actual Azure resources and live authentication") - @record_only() - def test_migrate_local_get_discovered_server_all_parameters(self): - self.kwargs.update({ - 'project': 'test-migrate-project', - 'rg': 'test-resource-group', - 'display_name': 'test-server', - 'machine_type': 'VMware', - 'subscription': '00000000-0000-0000-0000-000000000000', - 'machine_name': 'machine-001', - 'appliance': 'test-appliance' - }) - - # Test with project-name and resource-group-name parameters - self.cmd('az migrate local get-discovered-server ' - '--project-name {project} ' - '--resource-group-name {rg}') - - # Test with display-name filter - self.cmd('az migrate local get-discovered-server ' - '--project-name {project} ' - '--resource-group-name {rg} ' - '--display-name {display_name}') - - # Test with source-machine-type - self.cmd('az migrate local get-discovered-server ' - '--project-name {project} ' - '--resource-group-name {rg} ' - '--source-machine-type {machine_type}') - - # Test with subscription-id - self.cmd('az migrate local get-discovered-server ' - '--project-name {project} ' - '--resource-group-name {rg} ' - '--subscription-id {subscription}') - - # Test with name parameter - self.cmd('az migrate local get-discovered-server ' - '--project-name {project} ' - '--resource-group-name {rg} ' - '--name {machine_name}') - - # Test with appliance-name - self.cmd('az migrate local get-discovered-server ' - '--project-name {project} ' - '--resource-group-name {rg} ' - '--appliance-name {appliance}') - - # Test with all parameters combined - self.cmd('az migrate local get-discovered-server ' - '--project-name {project} ' - '--resource-group-name {rg} ' - '--display-name {display_name} ' - '--source-machine-type {machine_type} ' - '--subscription-id {subscription} ' - '--appliance-name {appliance}') - - @pytest.mark.skip(reason="Requires actual Azure resources and live authentication") - @record_only() - def test_migrate_local_replication_init_all_parameters(self): - self.kwargs.update({ - 'rg': 'test-resource-group', - 'project': 'test-migrate-project', - 'source_appliance': 'vmware-appliance', - 'target_appliance': 'azlocal-appliance', - 'storage_account': ( - '/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/test-rg/providers/Microsoft.Storage' - '/storageAccounts/cachestorage'), - 'subscription': '00000000-0000-0000-0000-000000000000' - }) - - # Test with required parameters - self.cmd('az migrate local replication init ' - '--resource-group-name {rg} ' - '--project-name {project} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance}') - - # Test with cache-storage-account-id - self.cmd('az migrate local replication init ' - '--resource-group-name {rg} ' - '--project-name {project} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--cache-storage-account-id {storage_account}') - - # Test with subscription-id - self.cmd('az migrate local replication init ' - '--resource-group-name {rg} ' - '--project-name {project} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--subscription-id {subscription}') - - # Test with pass-thru - self.cmd('az migrate local replication init ' - '--resource-group-name {rg} ' - '--project-name {project} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--pass-thru') - - # Test with all parameters - self.cmd('az migrate local replication init ' - '--resource-group-name {rg} ' - '--project-name {project} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--cache-storage-account-id {storage_account} ' - '--subscription-id {subscription} ' - '--pass-thru') - - @pytest.mark.skip(reason="Requires actual Azure resources and live authentication") - @record_only() - def test_migrate_local_replication_new_with_machine_id(self): - self.kwargs.update({ - 'machine_id': ( - '/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/test-rg/providers/Microsoft.Migrate' - '/migrateprojects/test-project/machines/machine-001'), - 'storage_path': ( - '/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/test-rg/providers/Microsoft.AzureStackHCI' - '/storageContainers/storage01'), - 'target_rg': ( - '/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/target-rg'), - 'vm_name': 'migrated-vm-01', - 'source_appliance': 'vmware-appliance', - 'target_appliance': 'azlocal-appliance', - 'virtual_switch': ( - '/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/test-rg/providers/Microsoft.AzureStackHCI' - '/logicalNetworks/network01'), - 'test_switch': ( - '/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/test-rg/providers/Microsoft.AzureStackHCI' - '/logicalNetworks/test-network'), - 'os_disk': 'disk-0', - 'subscription': '00000000-0000-0000-0000-000000000000' - }) - - # Test with machine-id (default user mode) - self.cmd('az migrate local replication new ' - '--machine-id {machine_id} ' - '--target-storage-path-id {storage_path} ' - '--target-resource-group-id {target_rg} ' - '--target-vm-name {vm_name} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--target-virtual-switch-id {virtual_switch} ' - '--os-disk-id {os_disk}') - - # Test with target-vm-cpu-core - self.cmd('az migrate local replication new ' - '--machine-id {machine_id} ' - '--target-storage-path-id {storage_path} ' - '--target-resource-group-id {target_rg} ' - '--target-vm-name {vm_name} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--target-virtual-switch-id {virtual_switch} ' - '--os-disk-id {os_disk} ' - '--target-vm-cpu-core 4') - - # Test with target-vm-ram - self.cmd('az migrate local replication new ' - '--machine-id {machine_id} ' - '--target-storage-path-id {storage_path} ' - '--target-resource-group-id {target_rg} ' - '--target-vm-name {vm_name} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--target-virtual-switch-id {virtual_switch} ' - '--os-disk-id {os_disk} ' - '--target-vm-ram 8192') - - # Test with is-dynamic-memory-enabled - self.cmd('az migrate local replication new ' - '--machine-id {machine_id} ' - '--target-storage-path-id {storage_path} ' - '--target-resource-group-id {target_rg} ' - '--target-vm-name {vm_name} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--target-virtual-switch-id {virtual_switch} ' - '--os-disk-id {os_disk} ' - '--is-dynamic-memory-enabled false') - - # Test with target-test-virtual-switch-id - self.cmd('az migrate local replication new ' - '--machine-id {machine_id} ' - '--target-storage-path-id {storage_path} ' - '--target-resource-group-id {target_rg} ' - '--target-vm-name {vm_name} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--target-virtual-switch-id {virtual_switch} ' - '--target-test-virtual-switch-id {test_switch} ' - '--os-disk-id {os_disk}') - - # Test with subscription-id - self.cmd('az migrate local replication new ' - '--machine-id {machine_id} ' - '--target-storage-path-id {storage_path} ' - '--target-resource-group-id {target_rg} ' - '--target-vm-name {vm_name} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--target-virtual-switch-id {virtual_switch} ' - '--os-disk-id {os_disk} ' - '--subscription-id {subscription}') - - @pytest.mark.skip(reason="Requires actual Azure resources and live authentication") - @record_only() - def test_migrate_local_replication_new_with_machine_index(self): - """Test replication new command with machine-index""" - self.kwargs.update({ - 'machine_index': 1, - 'project': 'test-migrate-project', - 'rg': 'test-resource-group', - 'storage_path': ( - '/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/test-rg/providers/Microsoft.AzureStackHCI' - '/storageContainers/storage01'), - 'target_rg': ( - '/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/target-rg'), - 'vm_name': 'migrated-vm-02', - 'source_appliance': 'vmware-appliance', - 'target_appliance': 'azlocal-appliance', - 'virtual_switch': ( - '/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/test-rg/providers/Microsoft.AzureStackHCI' - '/logicalNetworks/network01'), - 'os_disk': 'disk-0' - }) - - # Test with machine-index and required parameters - self.cmd('az migrate local replication new ' - '--machine-index {machine_index} ' - '--project-name {project} ' - '--resource-group-name {rg} ' - '--target-storage-path-id {storage_path} ' - '--target-resource-group-id {target_rg} ' - '--target-vm-name {vm_name} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--target-virtual-switch-id {virtual_switch} ' - '--os-disk-id {os_disk}') - - @pytest.mark.skip(reason="Requires actual Azure resources and live authentication") - @record_only() - def test_migrate_local_replication_new_power_user_mode(self): - """Test replication new command with power user mode""" - self.kwargs.update({ - 'machine_id': ( - '/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/test-rg/providers/Microsoft.Migrate' - '/migrateprojects/test-project/machines/machine-003'), - 'storage_path': ( - '/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/test-rg/providers/Microsoft.AzureStackHCI' - '/storageContainers/storage01'), - 'target_rg': ('/subscriptions/00000000-0000-0000-0000-000000000000' - '/resourceGroups/target-rg'), - 'vm_name': 'migrated-vm-03', - 'source_appliance': 'vmware-appliance', - 'target_appliance': 'azlocal-appliance' - }) - - # Test with disk-to-include and nic-to-include (power user mode) - self.cmd('az migrate local replication new ' - '--machine-id {machine_id} ' - '--target-storage-path-id {storage_path} ' - '--target-resource-group-id {target_rg} ' - '--target-vm-name {vm_name} ' - '--source-appliance-name {source_appliance} ' - '--target-appliance-name {target_appliance} ' - '--disk-to-include disk-0 disk-1 ' - '--nic-to-include nic-0') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/migrate/linter_exclusions.yml b/src/migrate/linter_exclusions.yml deleted file mode 100644 index b1dbdc0142f..00000000000 --- a/src/migrate/linter_exclusions.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -# exclusions for the migrate module - -migrate: - rule_exclusions: - - missing_group_help - -migrate local: - rule_exclusions: - - missing_group_help - -migrate local replication: - rule_exclusions: - - missing_group_help - -migrate local get-discovered-server: - rule_exclusions: - - missing_command_test_coverage - - missing_parameter_test_coverage - - missing_command_example - parameters: - resource_group_name: - rule_exclusions: - - parameter_should_not_end_in_resource_group - -migrate local replication init: - rule_exclusions: - - missing_command_test_coverage - - missing_parameter_test_coverage - - missing_command_example - parameters: - resource_group_name: - rule_exclusions: - - parameter_should_not_end_in_resource_group - -migrate local replication new: - rule_exclusions: - - missing_command_test_coverage - - missing_parameter_test_coverage - - missing_command_example - parameters: - resource_group_name: - rule_exclusions: - - parameter_should_not_end_in_resource_group diff --git a/src/migrate/setup.cfg b/src/migrate/setup.cfg deleted file mode 100644 index 3c6e79cf31d..00000000000 --- a/src/migrate/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/src/migrate/setup.py b/src/migrate/setup.py deleted file mode 100644 index d434d30b0d6..00000000000 --- a/src/migrate/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python - -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -from codecs import open -from setuptools import setup, find_packages - -VERSION = "1.0.0" - -CLASSIFIERS = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'License :: OSI Approved :: MIT License', -] - -DEPENDENCIES = [] - -setup( - name='migrate', - version=VERSION, - description='Support for Azure Migrate preview', - long_description='Support for Azure Migrate preview', - license='MIT', - author='Jeffrey Li', - author_email='jefl@microsoft.com', - url='https://github.com/Azure/azure-cli-extensions/tree/main/src/migrate', - classifiers=CLASSIFIERS, - packages=find_packages(exclude=["tests"]), - install_requires=DEPENDENCIES, - package_data={'azext_migrate': ['azext_metadata.json']} -)