Skip to content

Commit 0db895f

Browse files
authored
[virt] drop get_client() and force passing client arg (#4916)
##### What this PR does / why we need it: clean-up last virt utils from using get_client() in `wait_for_migration_finished` and `service_ip` function and method. also refactor `wait_for_migration_finished` - take out failed pod logs to separate function ##### Which issue(s) this PR fixes: Aligns last virt utilities with default client deprecation in python-wrapper repo https://redhat.atlassian.net/browse/CNV-68519 ##### Special notes for reviewer: ##### jira-ticket: https://redhat.atlassian.net/browse/CNV-73714 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Migration-related tests updated to use admin-aware IP resolution and the revised migration wait behavior (namespace passing simplified). * Added unit tests verifying the new migration-stuck diagnostic. * **Refactor** * Improved migration wait logic to better detect migrations stuck in Scheduling and surface richer diagnostic logs. * Service IP resolution now uses an admin-aware lookup and yields clearer errors when an IP cannot be determined. * **Chores** * Introduced a dedicated exception type for migrations stuck in Scheduling. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: vsibirsk <vsibirsk@redhat.com>
1 parent c3f814f commit 0db895f

8 files changed

Lines changed: 96 additions & 37 deletions

File tree

tests/network/migration/test_migration.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,13 +358,14 @@ def test_connectivity_after_migration_and_restart(
358358
],
359359
)
360360
def test_migration_with_masquerade(
361+
admin_client,
361362
running_vma,
362363
running_vmb,
363364
ip_family,
364365
):
365366
http_port_accessible(
366367
vm=running_vma,
367-
server_ip=running_vmb.custom_service.service_ip(ip_family=ip_family),
368+
server_ip=running_vmb.custom_service.service_ip(admin_client=admin_client, ip_family=ip_family),
368369
server_port=running_vmb.custom_service.service_port,
369370
)
370371

tests/scale/test_scale_benchmark.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,6 @@ def test_mass_vm_live_migration(
455455
for batch in scale_vms:
456456
for vm in batch:
457457
wait_for_migration_finished(
458-
namespace=vm.namespace,
459458
migration=vm_migration_info[vm.name][MIGRATION_INSTANCE_STR],
460459
)
461460
verify_vm_migrated(

tests/utils.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,7 @@ def hotplug_instance_type_vm_and_verify(vm, client, instance_type):
177177

178178
def verify_hotplug(vm, client, sockets=None, memory_guest=None):
179179
vmim = get_created_migration_job(vm=vm, client=client)
180-
wait_for_migration_finished(
181-
namespace=vm.namespace, migration=vmim, timeout=TIMEOUT_30MIN if "windows" in vm.name else TIMEOUT_10MIN
182-
)
180+
wait_for_migration_finished(migration=vmim, timeout=TIMEOUT_30MIN if "windows" in vm.name else TIMEOUT_10MIN)
183181
wait_for_ssh_connectivity(vm=vm)
184182
vmi_spec_domain = vm.vmi.instance.spec.domain
185183
if sockets:

tests/virt/node/descheduler/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def deployed_vms_for_descheduler_test(
122122
def all_existing_migrations_completed(admin_client, namespace):
123123
# Descheduler may trigger multiple migrations, need to wait when all succeeded
124124
for migration in VirtualMachineInstanceMigration.get(client=admin_client, namespace=namespace):
125-
wait_for_migration_finished(namespace=namespace.name, migration=migration, timeout=TIMEOUT_5MIN)
125+
wait_for_migration_finished(migration=migration, timeout=TIMEOUT_5MIN)
126126

127127

128128
@pytest.fixture(scope="class")

tests/virt/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def migrate_and_verify_multi_vms(vm_list):
140140

141141
for vm in vm_list:
142142
migration = vms_dict[vm.name]["vm_mig"]
143-
wait_for_migration_finished(namespace=vm.namespace, migration=migration)
143+
wait_for_migration_finished(migration=migration)
144144
migration.clean_up()
145145

146146
for vm in vm_list:

utilities/exceptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ class UnsupportedCPUArchitectureError(Exception):
128128
"""Exception raised when a CPU architecture is not supported."""
129129

130130

131+
class MigrationStuckSchedulingError(Exception):
132+
"""Exception raised when a migration is stuck in Scheduling state."""
133+
134+
def __init__(self, migration_name: str) -> None:
135+
self.migration_name = migration_name
136+
137+
def __str__(self) -> str:
138+
return f"Migration {self.migration_name} is stuck in Scheduling state."
139+
140+
131141
def raise_multiple_exceptions(exceptions):
132142
"""Raising multiple exceptions
133143

utilities/unittests/test_exceptions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from utilities.exceptions import (
1010
ClusterSanityError,
1111
DataVolumeConditionMessageNotFoundError,
12+
MigrationStuckSchedulingError,
1213
MissingEnvironmentVariableError,
1314
MissingResourceException,
1415
OsDictNotFoundError,
@@ -314,3 +315,20 @@ def test_unsupported_cpu_architecture_error(self):
314315
"""Test UnsupportedCPUArchitectureError can be raised"""
315316
with pytest.raises(UnsupportedCPUArchitectureError):
316317
raise UnsupportedCPUArchitectureError("Test error")
318+
319+
320+
class TestMigrationStuckSchedulingError:
321+
"""Test cases for MigrationStuckSchedulingError exception"""
322+
323+
def test_migration_stuck_scheduling_error_init(self):
324+
"""Test MigrationStuckSchedulingError initialization"""
325+
migration_name = "test-migration"
326+
error = MigrationStuckSchedulingError(migration_name)
327+
assert error.migration_name == migration_name
328+
329+
def test_migration_stuck_scheduling_error_str(self):
330+
"""Test MigrationStuckSchedulingError string representation"""
331+
migration_name = "test-migration"
332+
error = MigrationStuckSchedulingError(migration_name)
333+
expected = "Migration test-migration is stuck in Scheduling state."
334+
assert str(error) == expected

utilities/virt.py

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
Images,
9595
)
9696
from utilities.data_collector import collect_vnc_screenshot_for_vms
97+
from utilities.exceptions import MigrationStuckSchedulingError, ResourceValueError
9798
from utilities.hco import get_hco_namespace, wait_for_hco_conditions
9899
from utilities.network import (
99100
cloud_init_network_data,
@@ -1598,7 +1599,20 @@ def to_dict(self):
15981599
if self.ip_families:
15991600
self.res["spec"]["ipFamilies"] = self.ip_families
16001601

1601-
def service_ip(self, ip_family=None):
1602+
def service_ip(self, admin_client: DynamicClient, ip_family: str | None = None) -> str:
1603+
"""
1604+
Get the IP address of the service.
1605+
1606+
Args:
1607+
admin_client (DynamicClient): admin client to be used for the service.
1608+
ip_family (str | None): IP family to be used for the service.
1609+
1610+
Returns:
1611+
str: IP address of the service.
1612+
1613+
Raises:
1614+
ResourceValueError: If the service IP cannot be retrieved.
1615+
"""
16021616
if self.service_type == Service.Type.CLUSTER_IP:
16031617
if ip_family:
16041618
cluster_ips = [
@@ -1611,11 +1625,8 @@ def service_ip(self, ip_family=None):
16111625

16121626
return self.instance.spec.clusterIP
16131627

1614-
vm_node = Node(
1615-
client=get_client(),
1616-
name=self.vmi.instance.status.nodeName,
1617-
)
16181628
if self.service_type == Service.Type.NODE_PORT:
1629+
vm_node = self.vm.vmi.get_node(privileged_client=admin_client)
16191630
if ip_family:
16201631
internal_ips = [
16211632
internal_ip
@@ -1627,6 +1638,10 @@ def service_ip(self, ip_family=None):
16271638

16281639
return self.target_ip or vm_node.internal_ip
16291640

1641+
raise ResourceValueError(
1642+
f"Could not get service IP for service {self.vm.custom_service.name} with type {self.service_type}"
1643+
)
1644+
16301645
@property
16311646
def service_port(self):
16321647
if self.service_type == Service.Type.CLUSTER_IP:
@@ -1928,7 +1943,7 @@ def migrate_vm_and_verify(
19281943
) as migration:
19291944
if not wait_for_migration_success:
19301945
return migration
1931-
wait_for_migration_finished(namespace=vm.namespace, migration=migration, timeout=timeout)
1946+
wait_for_migration_finished(migration=migration, timeout=timeout)
19321947

19331948
verify_vm_migrated(
19341949
vm=vm,
@@ -1939,7 +1954,20 @@ def migrate_vm_and_verify(
19391954
return None
19401955

19411956

1942-
def wait_for_migration_finished(namespace, migration, timeout=TIMEOUT_12MIN):
1957+
def wait_for_migration_finished(migration: VirtualMachineInstanceMigration, timeout: int = TIMEOUT_12MIN) -> None:
1958+
"""
1959+
Wait for migration to finish.
1960+
If migration is stuck in Scheduling state, abort the migration and collect data.
1961+
1962+
Args:
1963+
migration (VirtualMachineInstanceMigration): Migration object.
1964+
timeout (int): Maximum time to wait for the migration to finish.
1965+
1966+
Raises:
1967+
MigrationStuckSchedulingError: If the migration is stuck in Scheduling state.
1968+
TimeoutExpiredError: If the migration does not finish within the timeout.
1969+
"""
1970+
19431971
sleep = TIMEOUT_10SEC
19441972
samples = TimeoutSampler(wait_timeout=timeout, sleep=sleep, func=lambda: migration.instance.status.phase)
19451973
counter = 0
@@ -1948,36 +1976,43 @@ def wait_for_migration_finished(namespace, migration, timeout=TIMEOUT_12MIN):
19481976
for sample in samples:
19491977
if sample == migration.Status.SUCCEEDED:
19501978
break
1951-
elif sample == "Scheduling":
1979+
if sample == VirtualMachineInstanceMigration.Status.SCHEDULING:
19521980
counter += 1
19531981
# If migration stuck in Scheduling state for more than 4 minutes - most likely it will be failed
19541982
# Need to collect data before 5 min timeout reached and target POD is removed
19551983
if counter >= TIMEOUT_4MIN / sleep:
1956-
# Get status/events for PODs in non-running or failed state
1957-
for pod in utilities.infra.get_pod_by_name_prefix(
1958-
client=get_client(),
1959-
pod_prefix=VIRT_LAUNCHER,
1960-
namespace=namespace,
1961-
get_all=True,
1962-
):
1963-
if pod.status not in (Pod.Status.RUNNING, Pod.Status.COMPLETED, Pod.Status.SUCCEEDED):
1964-
pod_events = [
1965-
event["raw_object"]["message"]
1966-
for event in pod.events(timeout=TIMEOUT_5SEC, field_selector="type==Warning")
1967-
]
1968-
LOGGER.error(
1969-
f"POD Conditions:\n {pod.instance.status.conditions[0]}\n"
1970-
f"POD Events:\n {', '.join(pod_events)}"
1971-
)
1972-
raise TimeoutExpiredError(
1973-
f"VMIM {migration.name} stuck in Scheduling state and probably will be failed"
1974-
)
1984+
log_failed_pod_events(migration=migration)
1985+
raise MigrationStuckSchedulingError(migration_name=migration.name)
19751986
except TimeoutExpiredError:
19761987
if sample:
19771988
LOGGER.error(f"Status of VMIM {migration.name} is {sample}")
19781989
raise
19791990

19801991

1992+
def log_failed_pod_events(migration: VirtualMachineInstanceMigration) -> None:
1993+
"""
1994+
Log failed pod events for a migration.
1995+
1996+
Args:
1997+
migration (VirtualMachineInstanceMigration): Migration object.
1998+
"""
1999+
2000+
for pod in utilities.infra.get_pod_by_name_prefix(
2001+
client=migration.client, pod_prefix=VIRT_LAUNCHER, namespace=migration.namespace, get_all=True
2002+
):
2003+
# Get status/events for PODs in non-running or failed state
2004+
if pod.status not in {Pod.Status.RUNNING, Pod.Status.COMPLETED, Pod.Status.SUCCEEDED}:
2005+
pod_events = [
2006+
event["raw_object"]["message"]
2007+
for event in pod.events(timeout=TIMEOUT_5SEC, field_selector="type==Warning")
2008+
]
2009+
LOGGER.error(
2010+
f"POD Name: {pod.name}\n"
2011+
f"POD Conditions:\n {pod.instance.status.conditions[0]}\n"
2012+
f"POD Events:\n {', '.join(pod_events)}"
2013+
)
2014+
2015+
19812016
def verify_vm_migrated(
19822017
vm,
19832018
node_before,
@@ -2331,9 +2366,7 @@ def check_migration_process_after_node_drain(client, vm, admin_client):
23312366
LOGGER.info(f"The VMI was running on {source_node.name}")
23322367
wait_for_node_schedulable_status(node=source_node, status=False)
23332368
vmim = get_created_migration_job(vm=vm, client=client, timeout=TIMEOUT_5MIN)
2334-
wait_for_migration_finished(
2335-
namespace=vm.namespace, migration=vmim, timeout=TIMEOUT_30MIN if "windows" in vm.name else TIMEOUT_10MIN
2336-
)
2369+
wait_for_migration_finished(migration=vmim, timeout=TIMEOUT_30MIN if "windows" in vm.name else TIMEOUT_10MIN)
23372370

23382371
target_pod = vm.vmi.get_virt_launcher_pod(privileged_client=admin_client)
23392372
target_pod.wait_for_status(status=Pod.Status.RUNNING, timeout=TIMEOUT_3MIN)

0 commit comments

Comments
 (0)