Skip to content

Commit 1fa76db

Browse files
author
Saif Al-Din Ali
committed
Fix error in init command
1 parent 2310bc0 commit 1fa76db

File tree

5 files changed

+207
-22
lines changed

5 files changed

+207
-22
lines changed

src/migrate/HISTORY.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
33
Release History
44
===============
5+
3.0.0b4
6+
+++++++++++++++
7+
* Fix edge case bugs in az migrate local replication init & new commands.
58

69
3.0.0b3
710
+++++++++++++++

src/migrate/azext_migrate/helpers/replication/init/_setup_policy.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def find_fabric(all_fabrics, appliance_name, fabric_instance_type,
170170
def get_fabric_agent(cmd, replication_fabrics_uri, fabric, appliance_name,
171171
fabric_instance_type):
172172
"""Get and validate fabric agent (DRA) for the given fabric."""
173+
logger = get_logger(__name__)
173174
fabric_name = fabric.get('name')
174175
dras_uri = (
175176
f"{replication_fabrics_uri}/{fabric_name}"
@@ -180,18 +181,60 @@ def get_fabric_agent(cmd, replication_fabrics_uri, fabric, appliance_name,
180181
dras = dras_response.json().get('value', [])
181182

182183
dra = None
184+
found_but_not_responsive = None
183185
for candidate in dras:
184186
props = candidate.get('properties', {})
185187
custom_props = props.get('customProperties', {})
186-
if (props.get('machineName') == appliance_name and
187-
custom_props.get('instanceType') == fabric_instance_type and
188-
bool(props.get('isResponsive'))):
189-
dra = candidate
190-
break
188+
machine_name = props.get('machineName', '')
189+
if (machine_name.lower() == appliance_name.lower() and
190+
custom_props.get('instanceType') == fabric_instance_type):
191+
if bool(props.get('isResponsive')):
192+
dra = candidate
193+
break
194+
else:
195+
found_but_not_responsive = candidate
196+
197+
# Accept a non-responsive DRA if it's the only match and is provisioned
198+
if not dra and found_but_not_responsive:
199+
nr_props = found_but_not_responsive.get('properties', {})
200+
last_heartbeat = nr_props.get('lastHeartbeat', 'unknown')
201+
if (nr_props.get('provisioningState') ==
202+
ProvisioningState.Succeeded.value):
203+
logger.warning(
204+
"The appliance '%s' DRA is not responsive "
205+
"(last heartbeat: %s). Proceeding since provisioning "
206+
"state is 'Succeeded'.",
207+
appliance_name, last_heartbeat)
208+
dra = found_but_not_responsive
209+
else:
210+
raise CLIError(
211+
f"The appliance '{appliance_name}' is in a "
212+
f"disconnected state (last heartbeat: {last_heartbeat}, "
213+
f"provisioningState: "
214+
f"{nr_props.get('provisioningState')})."
215+
)
191216

192217
if not dra:
218+
# Log available DRAs for diagnostics
219+
if dras:
220+
logger.warning(
221+
"No matching fabric agent found for appliance '%s' "
222+
"(expected instanceType '%s'). Available agents:",
223+
appliance_name, fabric_instance_type)
224+
for candidate in dras:
225+
props = candidate.get('properties', {})
226+
custom_props = props.get('customProperties', {})
227+
logger.warning(
228+
" - machineName: '%s', instanceType: '%s', "
229+
"isResponsive: %s",
230+
props.get('machineName'),
231+
custom_props.get('instanceType'),
232+
props.get('isResponsive'))
233+
193234
raise CLIError(
194-
f"The appliance '{appliance_name}' is in a disconnected state."
235+
f"No fabric agent found for appliance '{appliance_name}' "
236+
f"on fabric '{fabric_name}'. Verify that the appliance is "
237+
f"properly registered and connected."
195238
)
196239

197240
return dra

src/migrate/azext_migrate/helpers/replication/new/_process_inputs.py

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -740,19 +740,41 @@ def process_target_fabric(cmd,
740740
source_dras = source_dras_response.json().get('value', [])
741741

742742
source_dra = None
743+
source_found_not_responsive = None
743744
for dra in source_dras:
744745
props = dra.get('properties', {})
745746
custom_props = props.get('customProperties', {})
746-
if (props.get('machineName') == source_appliance_name and
747-
custom_props.get('instanceType') == fabric_instance_type and
748-
bool(props.get('isResponsive'))):
749-
source_dra = dra
750-
break
747+
machine_name = props.get('machineName', '')
748+
if (machine_name.lower() == source_appliance_name.lower() and
749+
custom_props.get('instanceType') == fabric_instance_type):
750+
if bool(props.get('isResponsive')):
751+
source_dra = dra
752+
break
753+
else:
754+
source_found_not_responsive = dra
755+
756+
if not source_dra and source_found_not_responsive:
757+
nr_props = source_found_not_responsive.get('properties', {})
758+
last_hb = nr_props.get('lastHeartbeat', 'unknown')
759+
if (nr_props.get('provisioningState') ==
760+
ProvisioningState.Succeeded.value):
761+
logger.warning(
762+
"The source appliance '%s' DRA is not responsive "
763+
"(last heartbeat: %s). Proceeding since provisioning "
764+
"state is 'Succeeded'.",
765+
source_appliance_name, last_hb)
766+
source_dra = source_found_not_responsive
767+
else:
768+
raise CLIError(
769+
f"The source appliance '{source_appliance_name}' is in a "
770+
f"disconnected state (last heartbeat: {last_hb}).")
751771

752772
if not source_dra:
753773
raise CLIError(
754-
f"The source appliance '{source_appliance_name}' is in a "
755-
f"disconnected state.")
774+
f"No fabric agent found for source appliance "
775+
f"'{source_appliance_name}' on fabric "
776+
f"'{source_fabric_name}'. Verify that the appliance is "
777+
f"properly registered and connected.")
756778

757779
target_fabric, target_fabric_candidates, \
758780
target_fabric_instance_type = _process_target_fabrics(
@@ -778,19 +800,41 @@ def process_target_fabric(cmd,
778800
target_dras = target_dras_response.json().get('value', [])
779801

780802
target_dra = None
803+
target_found_not_responsive = None
781804
for dra in target_dras:
782805
props = dra.get('properties', {})
783806
custom_props = props.get('customProperties', {})
784-
if (props.get('machineName') == target_appliance_name and
807+
machine_name = props.get('machineName', '')
808+
if (machine_name.lower() == target_appliance_name.lower() and
785809
custom_props.get('instanceType') ==
786-
target_fabric_instance_type and
787-
bool(props.get('isResponsive'))):
788-
target_dra = dra
789-
break
810+
target_fabric_instance_type):
811+
if bool(props.get('isResponsive')):
812+
target_dra = dra
813+
break
814+
else:
815+
target_found_not_responsive = dra
816+
817+
if not target_dra and target_found_not_responsive:
818+
nr_props = target_found_not_responsive.get('properties', {})
819+
last_hb = nr_props.get('lastHeartbeat', 'unknown')
820+
if (nr_props.get('provisioningState') ==
821+
ProvisioningState.Succeeded.value):
822+
logger.warning(
823+
"The target appliance '%s' DRA is not responsive "
824+
"(last heartbeat: %s). Proceeding since provisioning "
825+
"state is 'Succeeded'.",
826+
target_appliance_name, last_hb)
827+
target_dra = target_found_not_responsive
828+
else:
829+
raise CLIError(
830+
f"The target appliance '{target_appliance_name}' is in a "
831+
f"disconnected state (last heartbeat: {last_hb}).")
790832

791833
if not target_dra:
792834
raise CLIError(
793-
f"The target appliance '{target_appliance_name}' is in a "
794-
f"disconnected state.")
835+
f"No fabric agent found for target appliance "
836+
f"'{target_appliance_name}' on fabric "
837+
f"'{target_fabric_name}'. Verify that the appliance is "
838+
f"properly registered and connected.")
795839

796840
return target_fabric, source_dra, target_dra

src/migrate/azext_migrate/tests/latest/test_migrate_commands.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3385,7 +3385,7 @@ def test_find_fabric_matching_succeeded(self):
33853385

33863386
@mock.patch('azext_migrate.helpers.replication.init._setup_policy.send_get_request')
33873387
def test_get_fabric_agent_not_responsive(self, mock_get_request):
3388-
"""Test get_fabric_agent when agent is not responsive."""
3388+
"""Test get_fabric_agent when agent is not responsive and not Succeeded."""
33893389
from azext_migrate.helpers.replication.init._setup_policy import get_fabric_agent
33903390
from knack.util import CLIError
33913391

@@ -3397,6 +3397,8 @@ def test_get_fabric_agent_not_responsive(self, mock_get_request):
33973397
'properties': {
33983398
'machineName': 'appliance1',
33993399
'isResponsive': False,
3400+
'provisioningState': 'Failed',
3401+
'lastHeartbeat': '2026-01-01T00:00:00Z',
34003402
'customProperties': {
34013403
'instanceType': 'HyperV'
34023404
}
@@ -3413,6 +3415,99 @@ def test_get_fabric_agent_not_responsive(self, mock_get_request):
34133415

34143416
self.assertIn('disconnected state', str(context.exception))
34153417

3418+
@mock.patch('azext_migrate.helpers.replication.init._setup_policy.send_get_request')
3419+
def test_get_fabric_agent_not_responsive_but_succeeded(self, mock_get_request):
3420+
"""Test get_fabric_agent proceeds when DRA is not responsive but provisioning succeeded."""
3421+
from azext_migrate.helpers.replication.init._setup_policy import get_fabric_agent
3422+
3423+
mock_cmd = mock.Mock()
3424+
mock_response = mock.Mock()
3425+
mock_response.json.return_value = {
3426+
'value': [
3427+
{
3428+
'properties': {
3429+
'machineName': 'appliance1',
3430+
'isResponsive': False,
3431+
'provisioningState': 'Succeeded',
3432+
'lastHeartbeat': '2026-03-05T21:46:47Z',
3433+
'customProperties': {
3434+
'instanceType': 'HyperV'
3435+
}
3436+
}
3437+
}
3438+
]
3439+
}
3440+
mock_get_request.return_value = mock_response
3441+
3442+
fabric = {'name': 'fabric1'}
3443+
3444+
# Should succeed with a warning, not raise
3445+
result = get_fabric_agent(
3446+
mock_cmd, '/fabrics', fabric, 'appliance1', 'HyperV')
3447+
self.assertEqual(
3448+
result['properties']['machineName'], 'appliance1')
3449+
3450+
@mock.patch('azext_migrate.helpers.replication.init._setup_policy.send_get_request')
3451+
def test_get_fabric_agent_case_insensitive_match(self, mock_get_request):
3452+
"""Test get_fabric_agent matches machineName case-insensitively."""
3453+
from azext_migrate.helpers.replication.init._setup_policy import get_fabric_agent
3454+
3455+
mock_cmd = mock.Mock()
3456+
mock_response = mock.Mock()
3457+
mock_response.json.return_value = {
3458+
'value': [
3459+
{
3460+
'properties': {
3461+
'machineName': 'Appliance1',
3462+
'isResponsive': True,
3463+
'customProperties': {
3464+
'instanceType': 'HyperV'
3465+
}
3466+
}
3467+
}
3468+
]
3469+
}
3470+
mock_get_request.return_value = mock_response
3471+
3472+
fabric = {'name': 'fabric1'}
3473+
3474+
# Should succeed despite case difference
3475+
result = get_fabric_agent(
3476+
mock_cmd, '/fabrics', fabric, 'appliance1', 'HyperV')
3477+
self.assertEqual(
3478+
result['properties']['machineName'], 'Appliance1')
3479+
3480+
@mock.patch('azext_migrate.helpers.replication.init._setup_policy.send_get_request')
3481+
def test_get_fabric_agent_no_matching_agent(self, mock_get_request):
3482+
"""Test get_fabric_agent when no agent matches the appliance name."""
3483+
from azext_migrate.helpers.replication.init._setup_policy import get_fabric_agent
3484+
from knack.util import CLIError
3485+
3486+
mock_cmd = mock.Mock()
3487+
mock_response = mock.Mock()
3488+
mock_response.json.return_value = {
3489+
'value': [
3490+
{
3491+
'properties': {
3492+
'machineName': 'other-appliance',
3493+
'isResponsive': True,
3494+
'customProperties': {
3495+
'instanceType': 'HyperV'
3496+
}
3497+
}
3498+
}
3499+
]
3500+
}
3501+
mock_get_request.return_value = mock_response
3502+
3503+
fabric = {'name': 'fabric1'}
3504+
3505+
with self.assertRaises(CLIError) as context:
3506+
get_fabric_agent(
3507+
mock_cmd, '/fabrics', fabric, 'appliance1', 'HyperV')
3508+
3509+
self.assertIn('No fabric agent found', str(context.exception))
3510+
34163511

34173512
class MigrateNewExecuteTests2(unittest.TestCase):
34183513
"""Additional test class for new/_execute_new.py functions."""

src/migrate/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from setuptools import setup, find_packages
99

10-
VERSION = "3.0.0b3"
10+
VERSION = "3.0.0b4"
1111

1212
CLASSIFIERS = [
1313
'Development Status :: 4 - Beta',

0 commit comments

Comments
 (0)