Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
24a2db9
Create extension
Oct 22, 2025
f5b4378
Update src/migrate/azext_migrate/__init__.py
saifaldin14 Oct 22, 2025
67bb359
Fix import issues
Oct 22, 2025
70c525b
Merge branch 'main' of https://github.com/saifaldin14/azure-cli-exten…
Oct 22, 2025
c3021fb
Update src/migrate/setup.py
saifaldin14 Oct 22, 2025
f6e2dd8
Small
Oct 22, 2025
0174a38
Merge branch 'main' of https://github.com/saifaldin14/azure-cli-exten…
Oct 22, 2025
ae26651
Small lint
Oct 22, 2025
09ff801
Small
Oct 22, 2025
749bd43
disable lint for this check
Oct 22, 2025
8002b06
Add json
Oct 22, 2025
be276db
Fix licesnse issue
Oct 22, 2025
8da3466
fix small
Oct 22, 2025
d9fa098
Small
Oct 22, 2025
68f0d46
Get rid of unused variables
Oct 22, 2025
f916208
Add service name and code owner
Oct 23, 2025
9ad08a3
Merge branch 'main' of https://github.com/saifaldin14/azure-cli-exten…
Oct 23, 2025
8ae69d2
New version
Oct 23, 2025
532cbb3
Style
Oct 23, 2025
f216aa3
Small
Oct 23, 2025
77d8eb0
Update
Oct 23, 2025
f7558d6
Follow standard
Oct 23, 2025
ea8d636
Add suggestions
Oct 23, 2025
7117986
Small
Oct 23, 2025
143028f
Not preview
Oct 23, 2025
242fb99
Add flag to become experimental
Oct 23, 2025
4dae020
Merge branch 'Azure:main' into main
saifaldin14 Oct 23, 2025
6a1f184
Update history
Oct 23, 2025
4178016
Merge branch 'main' of https://github.com/saifaldin14/azure-cli-exten…
Oct 23, 2025
8874d5e
Fix
Oct 23, 2025
38b0de2
small
Oct 23, 2025
0f7acb7
Create get job and remove replication commands
Oct 24, 2025
6cd9f31
Revert "Create get job and remove replication commands"
Oct 28, 2025
ce78b73
Merge branch 'Azure:main' into main
saifaldin14 Oct 28, 2025
047528f
Update version
Oct 28, 2025
60c647f
Sync with other branch
Jan 15, 2026
9d6fdaf
Merge branch 'main' of https://github.com/saifaldin14/azure-cli-exten…
Jan 15, 2026
2310bc0
Merge branch 'Azure:main' into main
saifaldin14 Mar 5, 2026
1fa76db
Fix error in init command
Mar 11, 2026
3b50625
Merge branch 'Azure:main' into main
saifaldin14 Mar 11, 2026
f1897de
Remove duplicate files
Mar 11, 2026
00dc1b6
Merge branch 'main' of https://github.com/saifaldin14/azure-cli-exten…
Mar 11, 2026
eb04141
Fix local issues
Mar 11, 2026
f17cbe1
Select correct fabric
Mar 11, 2026
ab10e2b
Process project name
Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/migrate/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Release History
===============
3.0.0b4
+++++++++++++++
* Fix edge case bugs in az migrate local replication init & new commands.

3.0.0b3
+++++++++++++++
Expand Down
4 changes: 2 additions & 2 deletions src/migrate/azext_migrate/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ def new_local_server_replication(cmd,
subscription_id = get_subscription_id(cmd.cli_ctx)
print(f"Selected Subscription Id: '{subscription_id}'")

rg_uri, machine_id, subscription_id = validate_server_parameters(
rg_uri, machine_id, subscription_id, project_name = validate_server_parameters(
cmd,
machine_id,
machine_index,
Expand Down Expand Up @@ -239,7 +239,7 @@ def new_local_server_replication(cmd,
"Please verify your appliance setup and provided "
"-machine_id.")

amh_solution, migrate_project, machine_props = process_amh_solution(
amh_solution, migrate_project, machine_props, project_name = process_amh_solution(
cmd,
machine,
site_object,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ def build_extension_body(instance_type, source_fabric_id,


def _wait_for_extension_creation(cmd, extension_uri):
"""Wait for extension creation to complete."""
"""Wait for extension creation to complete. Returns final state."""
ext_state = None
for i in range(20):
time.sleep(30)
try:
Expand All @@ -277,6 +278,7 @@ def _wait_for_extension_creation(cmd, extension_uri):
break
except CLIError:
print(f"Waiting for extension... ({i + 1}/20)")
return ext_state


def _handle_extension_creation_error(cmd, extension_uri, create_error):
Expand Down Expand Up @@ -315,7 +317,20 @@ def create_replication_extension(cmd, extension_uri, extension_body):
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)
ext_state = _wait_for_extension_creation(cmd, extension_uri)
if ext_state == ProvisioningState.Failed.value:
raise CLIError(
"Replication extension creation failed. "
"Check the extension resource in the Azure portal "
"for detailed error information.")
if ext_state == ProvisioningState.Canceled.value:
raise CLIError(
"Replication extension creation was canceled.")
if ext_state is None:
raise CLIError(
"Replication extension creation timed out after "
"10 minutes. Check the extension status in the "
"Azure portal.")
except CLIError as create_error:
_handle_extension_creation_error(cmd, extension_uri, create_error)

Expand Down
77 changes: 64 additions & 13 deletions src/migrate/azext_migrate/helpers/replication/init/_setup_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,22 @@ def find_fabric(all_fabrics, appliance_name, fabric_instance_type,
})

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 is_correct_solution:
# Perfect match - use it immediately
fabric = candidate
break
# Name/type match but wrong solution ID - keep as fallback
if not fabric:
fabric = candidate

if fabric:
fabric_props = fabric.get('properties', {}).get('customProperties', {})
fabric_sol_id = fabric_props.get('migrationSolutionId', '').rstrip('/')
expected_sol_id = amh_solution.get('id', '').rstrip('/')
if fabric_sol_id.lower() != expected_sol_id.lower():
logger.warning(
"Fabric '%s' matches name and type but has "
"different solution ID", fabric.get('name'))

if not fabric:
appliance_type_label = "source" if is_source else "target"
Expand Down Expand Up @@ -170,6 +179,7 @@ def find_fabric(all_fabrics, appliance_name, fabric_instance_type,
def get_fabric_agent(cmd, replication_fabrics_uri, fabric, appliance_name,
fabric_instance_type):
"""Get and validate fabric agent (DRA) for the given fabric."""
logger = get_logger(__name__)
fabric_name = fabric.get('name')
dras_uri = (
f"{replication_fabrics_uri}/{fabric_name}"
Expand All @@ -180,18 +190,59 @@ def get_fabric_agent(cmd, replication_fabrics_uri, fabric, appliance_name,
dras = dras_response.json().get('value', [])

dra = None
found_but_not_responsive = 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
machine_name = props.get('machineName', '')
if (machine_name.lower() == appliance_name.lower() and
custom_props.get('instanceType') == fabric_instance_type):
if bool(props.get('isResponsive')):
dra = candidate
break
found_but_not_responsive = candidate

# Accept a non-responsive DRA if it's the only match and is provisioned
if not dra and found_but_not_responsive:
nr_props = found_but_not_responsive.get('properties', {})
last_heartbeat = nr_props.get('lastHeartbeat', 'unknown')
if (nr_props.get('provisioningState') ==
ProvisioningState.Succeeded.value):
Comment thread
saifaldin14 marked this conversation as resolved.
logger.warning(
"The appliance '%s' DRA is not responsive "
"(last heartbeat: %s). Proceeding since provisioning "
"state is 'Succeeded'.",
appliance_name, last_heartbeat)
dra = found_but_not_responsive
else:
raise CLIError(
f"The appliance '{appliance_name}' is in a "
f"disconnected state (last heartbeat: {last_heartbeat}, "
f"provisioningState: "
f"{nr_props.get('provisioningState')})."
)

if not dra:
# Log available DRAs for diagnostics
if dras:
logger.warning(
"No matching fabric agent found for appliance '%s' "
"(expected instanceType '%s'). Available agents:",
appliance_name, fabric_instance_type)
for candidate in dras:
props = candidate.get('properties', {})
custom_props = props.get('customProperties', {})
logger.warning(
" - machineName: '%s', instanceType: '%s', "
"isResponsive: %s",
props.get('machineName'),
custom_props.get('instanceType'),
props.get('isResponsive'))

raise CLIError(
f"The appliance '{appliance_name}' is in a disconnected state."
f"No fabric agent found for appliance '{appliance_name}' "
f"on fabric '{fabric_name}'. Verify that the appliance is "
f"properly registered and connected."
)

return dra
Expand Down
134 changes: 96 additions & 38 deletions src/migrate/azext_migrate/helpers/replication/new/_process_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def process_amh_solution(cmd,
f"'{resource_group_name}' and project '{project_name}'. "
"Please verify your appliance setup."
)
return amh_solution, migrate_project, machine_props
return amh_solution, migrate_project, machine_props, project_name


def process_replication_vault(cmd,
Expand Down Expand Up @@ -482,15 +482,23 @@ def _process_source_fabrics(all_fabrics,
})

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
if is_correct_solution:
source_fabric = fabric
break
if not source_fabric:
source_fabric = fabric

if source_fabric:
sf_props = source_fabric.get('properties', {}).get(
'customProperties', {})
sf_sol_id = sf_props.get('migrationSolutionId', '').rstrip('/')
exp_sol_id = amh_solution.get('id', '').rstrip('/')
if sf_sol_id.lower() != exp_sol_id.lower():
logger.warning(
"Fabric '%s' matches name and type but has different "
"solution ID",
source_fabric.get('name'))

return source_fabric, source_fabric_candidates


Expand Down Expand Up @@ -679,12 +687,22 @@ def _process_target_fabrics(all_fabrics,
})

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
if is_correct_solution:
target_fabric = fabric
break
if not target_fabric:
target_fabric = fabric

if target_fabric:
tf_props = target_fabric.get('properties', {}).get(
'customProperties', {})
tf_sol_id = tf_props.get('migrationSolutionId', '').rstrip('/')
exp_sol_id = amh_solution.get('id', '').rstrip('/')
if tf_sol_id.lower() != exp_sol_id.lower():
logger.warning(
"Fabric '%s' matches name and type but has different "
"solution ID", target_fabric.get('name'))

return target_fabric, target_fabric_candidates, \
target_fabric_instance_type

Expand Down Expand Up @@ -731,28 +749,48 @@ def process_target_fabric(cmd,
amh_solution):
# Get source fabric agent (DRA)
source_fabric_name = source_fabric.get('name')
dras_uri = (
source_dras = send_get_request(
cmd,
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', [])
).json().get('value', [])

source_dra = None
source_found_not_responsive = None
for dra in source_dras:
props = dra.get('properties', {})
custom_props = props.get('customProperties', {})
Comment thread
saifaldin14 marked this conversation as resolved.
if (props.get('machineName') == source_appliance_name and
custom_props.get('instanceType') == fabric_instance_type and
bool(props.get('isResponsive'))):
source_dra = dra
break
machine_name = props.get('machineName', '')
if (machine_name.lower() == source_appliance_name.lower() and
custom_props.get('instanceType') == fabric_instance_type):
if bool(props.get('isResponsive')):
source_dra = dra
break
source_found_not_responsive = dra

if not source_dra and source_found_not_responsive:
nr_props = source_found_not_responsive.get('properties', {})
last_hb = nr_props.get('lastHeartbeat', 'unknown')
if (nr_props.get('provisioningState') ==
ProvisioningState.Succeeded.value):
logger.warning(
"The source appliance '%s' DRA is not responsive "
"(last heartbeat: %s). Proceeding since provisioning "
"state is 'Succeeded'.",
source_appliance_name, last_hb)
source_dra = source_found_not_responsive
else:
raise CLIError(
f"The source appliance '{source_appliance_name}' is in a "
f"disconnected state (last heartbeat: {last_hb}).")

if not source_dra:
raise CLIError(
f"The source appliance '{source_appliance_name}' is in a "
f"disconnected state.")
f"No fabric agent found for source appliance "
f"'{source_appliance_name}' on fabric "
f"'{source_fabric_name}'. Verify that the appliance is "
f"properly registered and connected.")

target_fabric, target_fabric_candidates, \
target_fabric_instance_type = _process_target_fabrics(
Expand All @@ -769,28 +807,48 @@ def process_target_fabric(cmd,

# Get target fabric agent (DRA)
target_fabric_name = target_fabric.get('name')
target_dras_uri = (
target_dras = send_get_request(
cmd,
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', [])
).json().get('value', [])

target_dra = None
target_found_not_responsive = None
for dra in target_dras:
props = dra.get('properties', {})
custom_props = props.get('customProperties', {})
if (props.get('machineName') == target_appliance_name and
machine_name = props.get('machineName', '')
if (machine_name.lower() == target_appliance_name.lower() and
custom_props.get('instanceType') ==
target_fabric_instance_type and
bool(props.get('isResponsive'))):
target_dra = dra
break
target_fabric_instance_type):
if bool(props.get('isResponsive')):
target_dra = dra
break
target_found_not_responsive = dra

if not target_dra and target_found_not_responsive:
nr_props = target_found_not_responsive.get('properties', {})
last_hb = nr_props.get('lastHeartbeat', 'unknown')
if (nr_props.get('provisioningState') ==
ProvisioningState.Succeeded.value):
logger.warning(
"The target appliance '%s' DRA is not responsive "
"(last heartbeat: %s). Proceeding since provisioning "
"state is 'Succeeded'.",
target_appliance_name, last_hb)
target_dra = target_found_not_responsive
else:
raise CLIError(
f"The target appliance '{target_appliance_name}' is in a "
f"disconnected state (last heartbeat: {last_hb}).")

if not target_dra:
raise CLIError(
f"The target appliance '{target_appliance_name}' is in a "
f"disconnected state.")
f"No fabric agent found for target appliance "
f"'{target_appliance_name}' on fabric "
f"'{target_fabric_name}'. Verify that the appliance is "
f"properly registered and connected.")

return target_fabric, source_dra, target_dra
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ def validate_server_parameters( # pylint: disable=too-many-locals,too-many-bran
# machine_id was provided directly
# Check if it's in Microsoft.Migrate format and needs to be resolved
if "/Microsoft.Migrate/MigrateProjects/" in machine_id or "/Microsoft.Migrate/migrateprojects/" in machine_id:
# Extract project_name from the Microsoft.Migrate machine ID
migrate_id_parts = machine_id.split("/")
if len(migrate_id_parts) >= 9 and not project_name:
project_name = migrate_id_parts[8]

# This is a Migrate Project machine ID, need to resolve to OffAzure machine ID
migrate_machine = get_resource_by_id(
cmd, machine_id, APIVersion.Microsoft_Migrate.value)
Expand Down Expand Up @@ -257,7 +262,7 @@ def validate_server_parameters( # pylint: disable=too-many-locals,too-many-bran
f"/subscriptions/{subscription_id}/"
f"resourceGroups/{resource_group_name}")

return rg_uri, machine_id, subscription_id
return rg_uri, machine_id, subscription_id, project_name


def validate_required_parameters(machine_id,
Expand Down
Loading
Loading