Skip to content

Commit 64ac713

Browse files
committed
Add safeguard to check Ansible facts freshness before apply
Check Redis for cached Ansible facts before executing a role via the apply command. Warns if no facts exist or if facts are stale (older than 43200 seconds by default, configurable via FACTS_MAX_AGE). Skipped for gather-facts/facts roles and --show-tree. AI-assisted: Claude Code Signed-off-by: Christian Berendt <berendt@osism.tech>
1 parent 50acac4 commit 64ac713

3 files changed

Lines changed: 76 additions & 0 deletions

File tree

osism/commands/apply.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,11 @@ def take_action(self, parsed_args):
433433
dry_run = parsed_args.dry_run
434434
show_tree = parsed_args.show_tree
435435

436+
# Check if Ansible facts in Redis are available and fresh.
437+
# Skip when gathering facts or just showing the tree.
438+
if role and role not in ("gather-facts", "facts") and not show_tree:
439+
utils.check_ansible_facts()
440+
436441
rc = 0
437442

438443
if not role:

osism/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def read_secret(secret_name):
3131

3232
# 43200 seconds = 12 hours
3333
GATHER_FACTS_SCHEDULE = float(os.getenv("GATHER_FACTS_SCHEDULE", "43200.0"))
34+
FACTS_MAX_AGE = int(os.getenv("FACTS_MAX_AGE", str(int(GATHER_FACTS_SCHEDULE))))
3435
INVENTORY_RECONCILER_SCHEDULE = float(
3536
os.getenv("INVENTORY_RECONCILER_SCHEDULE", "600.0")
3637
)

osism/utils/__init__.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,76 @@ def is_task_locked():
558558
return None
559559

560560

561+
def check_ansible_facts(max_age=None):
562+
"""Check if Ansible facts exist in Redis and are not stale.
563+
564+
Scans Redis for ansible_facts* keys and checks the
565+
ansible_date_time.epoch field to determine freshness.
566+
567+
Args:
568+
max_age: Maximum age in seconds (default: settings.FACTS_MAX_AGE)
569+
"""
570+
import json
571+
572+
if max_age is None:
573+
max_age = settings.FACTS_MAX_AGE
574+
575+
try:
576+
r = _init_redis()
577+
578+
# Find all ansible_facts keys
579+
keys = []
580+
cursor = 0
581+
while True:
582+
cursor, batch = r.scan(cursor, match="ansible_facts*", count=100)
583+
keys.extend(batch)
584+
if cursor == 0:
585+
break
586+
except Exception as e:
587+
logger.warning(f"Could not check Ansible facts freshness: {e}")
588+
return
589+
590+
if not keys:
591+
logger.warning(
592+
"No Ansible facts found in Redis cache. "
593+
"Run 'osism sync facts' to gather facts."
594+
)
595+
return
596+
597+
now = time.time()
598+
stale_hosts = []
599+
600+
for key in keys:
601+
try:
602+
data = r.get(key)
603+
if not data:
604+
continue
605+
facts = json.loads(data)
606+
date_time = facts.get("ansible_date_time", {})
607+
epoch = date_time.get("epoch")
608+
if epoch is None:
609+
hostname = key.decode().replace("ansible_facts", "", 1)
610+
logger.debug(
611+
f"Host '{hostname}': facts missing ansible_date_time.epoch"
612+
)
613+
continue
614+
age = now - float(epoch)
615+
if age > max_age:
616+
hostname = key.decode().replace("ansible_facts", "", 1)
617+
stale_hosts.append((hostname, int(age)))
618+
except (json.JSONDecodeError, ValueError, TypeError):
619+
continue
620+
621+
if stale_hosts:
622+
logger.warning(
623+
f"Ansible facts in Redis are stale for {len(stale_hosts)} host(s) "
624+
f"(older than {max_age} seconds). "
625+
f"Run 'osism sync facts' to update facts."
626+
)
627+
for hostname, age in stale_hosts:
628+
logger.warning(f" Host '{hostname}': facts are {age} seconds old")
629+
630+
561631
def check_task_lock_and_exit():
562632
"""
563633
Check if tasks are locked and exit with error message if they are.

0 commit comments

Comments
 (0)