@@ -64,6 +64,7 @@ class NovaConnectionError(Exception):
6464MAX_PROCESSING_TIME_PADDING_SECONDS = 30
6565MIGRATION_QUERY_MINUTES = 5
6666MIGRATION_QUERY_LIMIT = 1000
67+ RESUME_MIGRATION_QUERY_MINUTES = 60
6768USERNAME_MAX_LENGTH = 64
6869FENCING_RETRY_DELAY_SECONDS = 1
6970KDUMP_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,
20312039def _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 ()
0 commit comments