@@ -86,8 +86,8 @@ def _vs_choices() -> CascadingDropdown:
8686 DropdownChoice (
8787 title = _ ("Cleanup strategy" ),
8888 choices = [
89- ("and" , _ ("defensive (both conditions must match)" )),
90- ("or" , _ ("offensive (one condition needs to match)" )),
89+ ("and" , _ ("Both conditions must match (defensive )" )),
90+ ("or" , _ ("One condition needs to match (offensive )" )),
9191 ],
9292 ),
9393 ),
@@ -145,7 +145,7 @@ def _vs_choices() -> CascadingDropdown:
145145 "strategy" ,
146146 FixedValue (
147147 "and" ,
148- _ ("defensive (both conditions must match)" ),
148+ _ ("Both conditions must match (defensive )" ),
149149 title = _ ("Cleanup strategy" ),
150150 ),
151151 ),
@@ -330,23 +330,35 @@ def _compute_timestamps_from_delta_cache_file_name(file_path: Path) -> tuple[int
330330 return None
331331
332332
333+ @dataclass (frozen = True , kw_only = True )
334+ class _AbandonedFilesOfHost :
335+ host_name : str
336+ folders_and_files : Mapping [Path , Sequence [_File ]]
337+ files : Sequence [_File ]
338+
339+ def compute_youngest_timestamp (self ) -> int | None :
340+ timestamps : set [int ] = set ()
341+ if self .files :
342+ timestamps .update (f .timestamp for f in self .files )
343+ if self .folders_and_files :
344+ timestamps .update (f .timestamp for fs in self .folders_and_files .values () for f in fs )
345+ return max (timestamps ) if timestamps else None
346+
347+
333348@dataclass (frozen = True , kw_only = True )
334349class _ClassifiedFilePaths :
335350 by_host : Mapping [HostName , _FilePathsOfHost ]
336- abandoned_host_files : Sequence [_File ]
337- abandoned_host_folders_and_files : Mapping [ Path , Sequence [_File ] ]
351+ abandoned_host_files : Sequence [_AbandonedFilesOfHost ]
352+ abandoned_files : Sequence [_File ]
338353
339354
340355def _compute_classified_file_paths (
341356 inventory_paths : InventoryPaths , host_names : Sequence [HostName ]
342357) -> _ClassifiedFilePaths :
343358 if not host_names :
344- return _ClassifiedFilePaths (
345- by_host = {},
346- abandoned_host_files = [],
347- abandoned_host_folders_and_files = {},
348- )
359+ return _ClassifiedFilePaths (by_host = {}, abandoned_files = [], abandoned_host_files = [])
349360
361+ # Construct all files of known hosts
350362 file_paths_by_host = {
351363 h : _FilePathsOfHost (
352364 inventory_tree = inventory_paths .inventory_tree (h ),
@@ -360,49 +372,79 @@ def _compute_classified_file_paths(
360372 for h in host_names
361373 }
362374
363- abandoned_host_files = []
364- for file_path in set (inventory_paths .inventory_dir .glob ("[!.]*" )).difference (
365- fp
366- for fps in file_paths_by_host .values ()
367- for fp in [
368- fps .inventory_tree .path ,
369- fps .inventory_tree .legacy ,
370- fps .inventory_tree_gz .path ,
371- fps .inventory_tree_gz .legacy ,
372- ]
373- ):
374- if (timestamp := _compute_timestamp_from_file_path (file_path )) is not None :
375- abandoned_host_files .append (_File (path = file_path , timestamp = timestamp ))
376-
377- for file_path in set (inventory_paths .status_data_dir .glob ("*" )).difference (
378- fp
379- for fps in file_paths_by_host .values ()
380- for fp in [fps .status_data_tree .path , fps .status_data_tree .legacy ]
381- ):
382- if (timestamp := _compute_timestamp_from_file_path (file_path )) is not None :
383- abandoned_host_files .append (_File (path = file_path , timestamp = timestamp ))
384-
385- abandoned_host_folders_and_files : dict [Path , list [_File ]] = {}
375+ # Compute all unknown archive files
376+ abandoned_folders_and_files_by_host : dict [str , dict [Path , list [_File ]]] = {}
386377 for file_path in set (inventory_paths .archive_dir .glob ("*/*" )).difference (
387378 fp for fps in file_paths_by_host .values () for fp in fps .archive_file_paths
388379 ):
389380 if (timestamp := _compute_timestamp_from_archive_file_name (file_path )) is not None :
390- abandoned_host_folders_and_files .setdefault (file_path .parent , []). append (
391- _File ( path = file_path , timestamp = timestamp )
392- )
381+ abandoned_folders_and_files_by_host .setdefault (file_path .parent . name , {}). setdefault (
382+ file_path . parent , []
383+ ). append ( _File ( path = file_path , timestamp = timestamp ))
393384
385+ # Compute all unknown delta cache files
394386 for file_path in set (inventory_paths .delta_cache_dir .glob ("*/*" )).difference (
395387 fp for fps in file_paths_by_host .values () for fp in fps .delta_cache_file_paths
396388 ):
397389 if (timestamps := _compute_timestamps_from_delta_cache_file_name (file_path )) is not None :
398- abandoned_host_folders_and_files .setdefault (file_path .parent , []).append (
399- _File (path = file_path , timestamp = timestamps [- 1 ])
400- )
390+ abandoned_folders_and_files_by_host .setdefault (file_path .parent .name , {}).setdefault (
391+ file_path .parent , []
392+ ).append (_File (path = file_path , timestamp = timestamps [- 1 ]))
393+
394+ # Construct inventory or status data tree files of unknown hosts
395+ # (with archive or delta cache files)
396+ abandoned_tree_files_by_host : dict [str , list [_File ]] = {}
397+ for raw_host_name in abandoned_folders_and_files_by_host :
398+ host_name = HostName (raw_host_name )
399+ inventory_tree = inventory_paths .inventory_tree (host_name )
400+ inventory_tree_gz = inventory_paths .inventory_tree_gz (host_name )
401+ status_data_tree = inventory_paths .status_data_tree (host_name )
402+ for file_path in [
403+ inventory_tree .path ,
404+ inventory_tree .legacy ,
405+ inventory_tree_gz .path ,
406+ inventory_tree_gz .legacy ,
407+ status_data_tree .path ,
408+ status_data_tree .legacy ,
409+ ]:
410+ if (timestamp := _compute_timestamp_from_file_path (file_path )) is not None :
411+ abandoned_tree_files_by_host .setdefault (raw_host_name , []).append (
412+ _File (path = file_path , timestamp = timestamp )
413+ )
401414
402415 return _ClassifiedFilePaths (
403416 by_host = file_paths_by_host ,
404- abandoned_host_files = abandoned_host_files ,
405- abandoned_host_folders_and_files = abandoned_host_folders_and_files ,
417+ abandoned_host_files = [
418+ _AbandonedFilesOfHost (
419+ host_name = host_name ,
420+ files = abandoned_tree_files_by_host .get (host_name , []),
421+ folders_and_files = folders_and_files ,
422+ )
423+ for host_name , folders_and_files in abandoned_folders_and_files_by_host .items ()
424+ ],
425+ # Construct remaining inventory or status data tree files of unknown hosts
426+ # (without archive or delta cache files)
427+ abandoned_files = [
428+ _File (path = file_path , timestamp = timestamp )
429+ for file_path in (
430+ set (inventory_paths .inventory_dir .glob ("[!.]*" ))
431+ .union (inventory_paths .status_data_dir .glob ("*" ))
432+ .difference (
433+ fp
434+ for fps in file_paths_by_host .values ()
435+ for fp in [
436+ fps .inventory_tree .path ,
437+ fps .inventory_tree .legacy ,
438+ fps .inventory_tree_gz .path ,
439+ fps .inventory_tree_gz .legacy ,
440+ fps .status_data_tree .path ,
441+ fps .status_data_tree .legacy ,
442+ ]
443+ )
444+ .difference (f .path for fs in abandoned_tree_files_by_host .values () for f in fs )
445+ )
446+ if (timestamp := _compute_timestamp_from_file_path (file_path )) is not None
447+ ],
406448 )
407449
408450
@@ -473,17 +515,33 @@ def _compute_classified_history_files(
473515def _cleanup_bundle (bundle : _File | _ArchiveBundle ) -> None :
474516 match bundle :
475517 case _File ():
476- logger .warning ("Remove inventory history entry %r" , bundle .path )
518+ logger .warning ("Remove single delta cache file %r" , bundle .path )
477519 bundle .path .unlink (missing_ok = True )
478520 case _ArchiveBundle ():
479- logger .warning ("Remove inventory history entry %r" , bundle .previous )
521+ logger .warning ("Remove archive file %r" , bundle .previous )
480522 # We never remove the current path because it may belong to the previous bundle
481523 bundle .previous .unlink (missing_ok = True )
482524 if bundle .delta_cache is not None :
483- logger .warning ("Remove inventory history entry %r" , bundle .delta_cache .path )
525+ logger .warning ("Remove delta cache file of bundle %r" , bundle .delta_cache .path )
484526 bundle .delta_cache .path .unlink (missing_ok = True )
485527
486528
529+ def _cleanup_abandoned_files_of_host (abandoned_files_of_host : _AbandonedFilesOfHost ) -> None :
530+ for folder , files in abandoned_files_of_host .folders_and_files .items ():
531+ for file in files :
532+ logger .warning ("Remove abandoned host file %r" , file .path )
533+ file .path .unlink (missing_ok = True )
534+ try :
535+ folder .rmdir ()
536+ except OSError :
537+ # Folder not empty
538+ pass
539+
540+ for file in abandoned_files_of_host .files :
541+ logger .warning ("Remove abandoned host file %r" , file .path )
542+ file .path .unlink (missing_ok = True )
543+
544+
487545class InventoryHousekeeping :
488546 def __init__ (self , omd_root : Path ) -> None :
489547 super ().__init__ ()
@@ -495,6 +553,7 @@ def _run(self, config: Config, *, host_names: Sequence[HostName], now: int) -> N
495553 abandoned_params = _ParamsFileAge (config .inventory_housekeeping ["abandoned_file_age" ])
496554
497555 classified_file_paths = _compute_classified_file_paths (self .inv_paths , host_names )
556+
498557 for host_name , file_paths in classified_file_paths .by_host .items ():
499558 if (params := _compute_host_params (hosts_params , default_params , host_name )) is None :
500559 continue
@@ -513,25 +572,20 @@ def _run(self, config: Config, *, host_names: Sequence[HostName], now: int) -> N
513572
514573 for archive_file in classified_history_files .single_archive_files :
515574 if params .file_is_too_old (now , archive_file .timestamp ):
516- logger .warning ("Remove too old archive tree %r" , archive_file .path )
575+ logger .warning ("Remove too old archive file %r" , archive_file .path )
517576 archive_file .path .unlink (missing_ok = True )
518577
519- for file in classified_file_paths .abandoned_host_files :
578+ for abandoned_files_of_host in classified_file_paths .abandoned_host_files :
579+ if (
580+ timestamp := abandoned_files_of_host .compute_youngest_timestamp ()
581+ ) is not None and abandoned_params .file_is_too_old (now , timestamp ):
582+ _cleanup_abandoned_files_of_host (abandoned_files_of_host )
583+
584+ for file in classified_file_paths .abandoned_files :
520585 if abandoned_params .file_is_too_old (now , file .timestamp ):
521586 logger .warning ("Remove abandoned file %r" , file .path )
522587 file .path .unlink (missing_ok = True )
523588
524- for folder , files in classified_file_paths .abandoned_host_folders_and_files .items ():
525- for file in files :
526- if abandoned_params .file_is_too_old (now , file .timestamp ):
527- logger .warning ("Remove abandoned file %r" , file .path )
528- file .path .unlink (missing_ok = True )
529- try :
530- folder .rmdir ()
531- except OSError :
532- # Folder not empty
533- pass
534-
535589 def __call__ (self , config : Config ) -> None :
536590 self ._run (
537591 config ,
0 commit comments