Skip to content

Commit 8cb5db7

Browse files
Merge pull request #640 from openshift-cherrypick-robot/cherry-pick-639-to-18.0-fr6
[18.0-fr6] Resume-safe evacuation: check migration status before re-evacuating on pod restart
2 parents 2a21385 + e718c7c commit 8cb5db7

3 files changed

Lines changed: 339 additions & 11 deletions

File tree

templates/instanceha/bin/instanceha.py

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class NovaConnectionError(Exception):
6464
MAX_PROCESSING_TIME_PADDING_SECONDS = 30
6565
MIGRATION_QUERY_MINUTES = 5
6666
MIGRATION_QUERY_LIMIT = 1000
67+
RESUME_MIGRATION_QUERY_MINUTES = 60
6768
USERNAME_MAX_LENGTH = 64
6869
FENCING_RETRY_DELAY_SECONDS = 1
6970
KDUMP_REENABLE_DELAY_SECONDS = 60
@@ -262,6 +263,7 @@ class EvacuationStatus:
262263
"""Status of an ongoing server evacuation."""
263264
completed: bool
264265
error: bool
266+
created_at: Optional[str] = None
265267

266268

267269
@dataclass
@@ -1635,13 +1637,16 @@ def _should_skip_server(server, skip_names) -> bool:
16351637
return any(fnmatch.fnmatch(server.name, pattern) for pattern in skip_names)
16361638

16371639

1638-
def _get_evacuable_servers(connection, host, service) -> List:
1640+
def _get_evacuable_servers(connection, host, service, resume=False) -> List:
16391641
"""Get list of evacuable servers from a host."""
16401642
images = service.get_evacuable_images(connection)
16411643
flavors = service.get_evacuable_flavors(connection)
16421644

16431645
servers = connection.servers.list(search_opts={'host': host, 'all_tenants': 1})
1644-
servers = [s for s in servers if s.status in {'ACTIVE', 'ERROR', 'SHUTOFF'}]
1646+
allowed_statuses = {'ACTIVE', 'ERROR', 'SHUTOFF'}
1647+
if resume:
1648+
allowed_statuses.add('REBUILD')
1649+
servers = [s for s in servers if s.status in allowed_statuses]
16451650

16461651
skip_names = service.config.get_config_value('SKIP_SERVERS_WITH_NAME')
16471652
if skip_names:
@@ -1760,7 +1765,7 @@ def _run_concurrent(func, items, max_workers, item_id_func, log_prefix="",
17601765
return all_succeeded
17611766

17621767

1763-
def _concurrent_evacuate(connection, evacuables, service, host, service_id, target_host=None) -> bool:
1768+
def _concurrent_evacuate(connection, evacuables, service, host, service_id, target_host=None, resume=False) -> bool:
17641769
"""Evacuate concurrently, optionally with priority-ordered phases."""
17651770
orchestrated = service.config.get_config_value('ORCHESTRATED_RESTART')
17661771
phases = _build_evacuation_groups(evacuables) if orchestrated else [evacuables]
@@ -1787,7 +1792,8 @@ def _concurrent_evacuate(connection, evacuables, service, host, service_id, targ
17871792
lambda s: _server_evacuate_future(connection, s, target_host,
17881793
max_retries=max_retries,
17891794
shutdown_event=service.shutdown_event,
1790-
evacuation_timeout=evacuation_timeout),
1795+
evacuation_timeout=evacuation_timeout,
1796+
resume=resume),
17911797
phase_servers,
17921798
inner_workers,
17931799
lambda s: s.id,
@@ -1832,19 +1838,19 @@ def _traditional_evacuate(connection, evacuables, host, target_host=None) -> boo
18321838

18331839
return all_succeeded
18341840

1835-
def _host_evacuate(connection, failed_service, service, target_host=None) -> bool:
1841+
def _host_evacuate(connection, failed_service, service, target_host=None, resume=False) -> bool:
18361842
"""Evacuate all instances from a failed host."""
18371843
host = failed_service.host
18381844

1839-
evacuables = _get_evacuable_servers(connection, host, service)
1845+
evacuables = _get_evacuable_servers(connection, host, service, resume=resume)
18401846
if not evacuables:
18411847
logging.info("Nothing to evacuate")
18421848
return True
18431849

18441850
_interruptible_sleep(service.shutdown_event, service.config.get_config_value('DELAY'))
18451851

18461852
if service.config.get_config_value('ORCHESTRATED_RESTART') or service.config.get_config_value('SMART_EVACUATION'):
1847-
return _concurrent_evacuate(connection, evacuables, service, host, failed_service.id, target_host=target_host)
1853+
return _concurrent_evacuate(connection, evacuables, service, host, failed_service.id, target_host=target_host, resume=resume)
18481854
else:
18491855
return _traditional_evacuate(connection, evacuables, host, target_host=target_host)
18501856

@@ -1952,10 +1958,12 @@ def _migration_sort_key(m):
19521958
migrations = sorted(migrations, key=_migration_sort_key, reverse=True)
19531959
migration = migrations[0]
19541960
status = getattr(migration, 'status', None) or getattr(migration, '_info', {}).get('status')
1961+
created_at = getattr(migration, 'created_at', None)
19551962

19561963
return EvacuationStatus(
19571964
completed=status in MIGRATION_STATUS_COMPLETED if status else False,
1958-
error=status in MIGRATION_STATUS_ERROR if status else True
1965+
error=status in MIGRATION_STATUS_ERROR if status else True,
1966+
created_at=created_at,
19591967
)
19601968

19611969
except Exception as e:
@@ -2031,7 +2039,8 @@ def _monitor_evacuation(connection, server_id, response_uuid, start_time,
20312039
def _server_evacuate_future(connection, server, target_host=None,
20322040
max_retries=DEFAULT_EVACUATION_RETRIES,
20332041
shutdown_event=None,
2034-
evacuation_timeout=MAX_EVACUATION_TIMEOUT_SECONDS) -> bool:
2042+
evacuation_timeout=MAX_EVACUATION_TIMEOUT_SECONDS,
2043+
resume=False) -> bool:
20352044
"""Evacuate a server and monitor until completion."""
20362045
if not hasattr(server, 'id'):
20372046
logging.warning("Could not evacuate instance - missing server ID: %s",
@@ -2052,6 +2061,73 @@ def _server_evacuate_future(connection, server, target_host=None,
20522061
logging.warning("Could not lock server %s (may already be locked)", server.id)
20532062

20542063
try:
2064+
if resume:
2065+
mig_status = _server_evacuation_status(connection, server.id,
2066+
query_minutes=RESUME_MIGRATION_QUERY_MINUTES)
2067+
2068+
if mig_status.completed:
2069+
logging.info("Server %s already evacuated (migration completed), skipping",
2070+
server.id)
2071+
INSTANCE_EVACUATION_TOTAL.labels(host=source_host, result='succeeded').inc()
2072+
return True
2073+
2074+
if not mig_status.error:
2075+
# Compute elapsed time from when the migration actually started,
2076+
# not from when this pod started, so the timeout reflects real
2077+
# migration age and a crash-loop can't grant infinite extensions.
2078+
resume_start = start_time
2079+
if mig_status.created_at:
2080+
try:
2081+
created_dt = datetime.fromisoformat(
2082+
str(mig_status.created_at).replace('Z', '+00:00'))
2083+
elapsed = (datetime.now(timezone.utc) - created_dt).total_seconds()
2084+
resume_start = time.monotonic() - elapsed
2085+
except (ValueError, TypeError):
2086+
pass
2087+
2088+
logging.info("Server %s has in-progress evacuation, monitoring existing migration",
2089+
server.id)
2090+
_emit_k8s_event(source_host, 'InstanceEvacuationResumed',
2091+
f'Resuming monitoring of in-progress evacuation for {server.id}')
2092+
INSTANCE_EVACUATION_TOTAL.labels(host=source_host, result='started').inc()
2093+
2094+
result = _monitor_evacuation(
2095+
connection, server.id, server.id, resume_start,
2096+
max_retries=max_retries,
2097+
shutdown_event=shutdown_event,
2098+
evacuation_timeout=evacuation_timeout)
2099+
2100+
if result:
2101+
_emit_k8s_event(source_host, 'InstanceEvacuationSucceeded',
2102+
f'Instance {server.id} evacuated successfully (resumed)')
2103+
INSTANCE_EVACUATION_TOTAL.labels(host=source_host, result='succeeded').inc()
2104+
INSTANCE_EVACUATION_DURATION.labels(host=source_host).observe(
2105+
time.monotonic() - start_time)
2106+
if original_status in ('SHUTOFF', 'ERROR'):
2107+
current = connection.servers.get(server.id)
2108+
current_status = getattr(current, 'status', None)
2109+
if current_status != original_status:
2110+
try:
2111+
if original_status == 'SHUTOFF':
2112+
connection.servers.stop(server.id)
2113+
else:
2114+
connection.servers.reset_state(server.id, 'error')
2115+
logging.info("Restored server %s to %s state",
2116+
server.id, original_status)
2117+
except Exception as e:
2118+
logging.warning("Failed to restore %s state for %s: %s",
2119+
original_status, server.id, e)
2120+
else:
2121+
_emit_k8s_event(source_host, 'InstanceEvacuationFailed',
2122+
f'Instance {server.id} evacuation failed (resumed)',
2123+
event_type='Warning')
2124+
INSTANCE_EVACUATION_TOTAL.labels(host=source_host, result='failed').inc()
2125+
2126+
return result
2127+
2128+
logging.info("Server %s has no recent successful migration, will re-evacuate",
2129+
server.id)
2130+
20552131
_emit_k8s_event(source_host, 'InstanceEvacuationStarted',
20562132
f'Evacuating instance {server.id}')
20572133
INSTANCE_EVACUATION_TOTAL.labels(host=source_host, result='started').inc()
@@ -3003,7 +3079,8 @@ def process_service(failed_service, reserved_hosts, resume, service) -> bool:
30033079
if evac_target:
30043080
logging.info("Forcing evacuation to reserved host: %s", evac_target)
30053081
if not _execute_step("Evacuation", _host_evacuate, host_name,
3006-
conn, failed_service, service, evac_target):
3082+
conn, failed_service, service, evac_target,
3083+
resume=resume):
30073084
_emit_k8s_event(host_name, 'EvacuationFailed',
30083085
'VM evacuation failed', event_type='Warning')
30093086
EVACUATION_TOTAL.labels(host=host_name, result='failed').inc()

test/instanceha/test_orchestrated_evacuation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,8 @@ def test_target_host_passed_through(self, mock_groups, mock_future, mock_update)
333333
mock_future.assert_called_once_with(conn, server, 'reserved-01',
334334
max_retries=5,
335335
shutdown_event=service.shutdown_event,
336-
evacuation_timeout=300)
336+
evacuation_timeout=300,
337+
resume=False)
337338

338339
@patch('instanceha._update_service_disable_reason')
339340
@patch('instanceha._server_evacuate_future')

0 commit comments

Comments
 (0)