Skip to content

Commit e4966bd

Browse files
committed
HW/SW Inventory: Fix cleanup of abandoned host files
CMK-24859 Change-Id: I8b468504571413969d7263b3de79c49ce6d1c628
1 parent bf44f31 commit e4966bd

3 files changed

Lines changed: 383 additions & 129 deletions

File tree

.werks/18049.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ These parameters can be configured per host (explicit match or regex).
4242
A history entry is composed of two subsequent archive files and a delta cache
4343
file. The delta cache file may be missing because it was not computed yet.
4444

45+
The default is set to `Remove history entries older than 400 days and remove history entries right after entry number 100`
46+
for new sites. Existing sites don't have a default.
47+
4548
Similar to the `diskspace` parameters the HW/SW Inventory housekeeping cleans
4649
up abandoned host files which are older than 30 days by default. This parameter
4750
is also configurable. Abandoned host files include
@@ -54,8 +57,8 @@ is also configurable. Abandoned host files include
5457

5558
* Delta cache files
5659

57-
The default is set to `Remove history entries older than 400 days and remove history entries right after entry number 100` for new sites.
58-
Existing sites don't have a default.
60+
The deciding timestamp for the cleanup of abandoned host files is the youngest
61+
timestamp of these files per host.
5962

6063
The housekeeping job runs once a day.
6164

cmk/gui/inventory/_housekeeping.py

Lines changed: 111 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
334349
class _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

340355
def _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(
473515
def _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+
487545
class 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

Comments
 (0)