Revise start_fact_cache and finish_fact_cache to use JSON file (#15970)

* Revise start_fact_cache and finish_fact_cache to use JSON file with host list inside it

* Revise artifacts path to be relative to the job private_data_dir

* Update calls to start_fact_cache and finish_fact_cache to agree with new reference to artifacts_dir

* Prevents unnecessary updates to ansible_facts_modified, fixing timestamp-related test failures.
This commit is contained in:
Lila Yasin
2025-05-14 13:20:47 -04:00
committed by GitHub
parent 872349ac75
commit f734d8bf19
3 changed files with 124 additions and 65 deletions

View File

@@ -22,27 +22,29 @@ system_tracking_logger = logging.getLogger('awx.analytics.system_tracking')
@log_excess_runtime(logger, debug_cutoff=0.01, msg='Inventory {inventory_id} host facts prepared for {written_ct} hosts, took {delta:.3f} s', add_log_data=True)
def start_fact_cache(hosts, destination, log_data, timeout=None, inventory_id=None):
def start_fact_cache(hosts, artifacts_dir, timeout=None, inventory_id=None, log_data=None):
log_data = log_data or {}
log_data['inventory_id'] = inventory_id
log_data['written_ct'] = 0
hosts_cached = list()
try:
os.makedirs(destination, mode=0o700)
except FileExistsError:
pass
hosts_cached = []
# Create the fact_cache directory inside artifacts_dir
fact_cache_dir = os.path.join(artifacts_dir, 'fact_cache')
os.makedirs(fact_cache_dir, mode=0o700, exist_ok=True)
if timeout is None:
timeout = settings.ANSIBLE_FACT_CACHE_TIMEOUT
last_filepath_written = None
last_write_time = None
for host in hosts:
hosts_cached.append(host)
hosts_cached.append(host.name)
if not host.ansible_facts_modified or (timeout and host.ansible_facts_modified < now() - datetime.timedelta(seconds=timeout)):
continue # facts are expired - do not write them
filepath = os.sep.join(map(str, [destination, host.name]))
if not os.path.realpath(filepath).startswith(destination):
system_tracking_logger.error('facts for host {} could not be cached'.format(smart_str(host.name)))
filepath = os.path.join(fact_cache_dir, host.name)
if not os.path.realpath(filepath).startswith(fact_cache_dir):
logger.error(f'facts for host {smart_str(host.name)} could not be cached')
continue
try:
@@ -50,15 +52,21 @@ def start_fact_cache(hosts, destination, log_data, timeout=None, inventory_id=No
os.chmod(f.name, 0o600)
json.dump(host.ansible_facts, f)
log_data['written_ct'] += 1
last_filepath_written = filepath
last_write_time = os.path.getmtime(filepath)
except IOError:
system_tracking_logger.error('facts for host {} could not be cached'.format(smart_str(host.name)))
logger.error(f'facts for host {smart_str(host.name)} could not be cached')
continue
if last_filepath_written:
return os.path.getmtime(last_filepath_written), hosts_cached
return None, hosts_cached
# Write summary file directly to the artifacts_dir
if inventory_id is not None:
summary_file = os.path.join(artifacts_dir, 'host_cache_summary.json')
summary_data = {
'last_write_time': last_write_time,
'hosts_cached': hosts_cached,
'written_ct': log_data['written_ct'],
}
with open(summary_file, 'w', encoding='utf-8') as f:
json.dump(summary_data, f, indent=2)
@log_excess_runtime(
@@ -67,34 +75,54 @@ def start_fact_cache(hosts, destination, log_data, timeout=None, inventory_id=No
msg='Inventory {inventory_id} host facts: updated {updated_ct}, cleared {cleared_ct}, unchanged {unmodified_ct}, took {delta:.3f} s',
add_log_data=True,
)
def finish_fact_cache(hosts_cached, destination, facts_write_time, log_data, job_id=None, inventory_id=None):
def finish_fact_cache(artifacts_dir, job_id=None, inventory_id=None, log_data=None):
log_data = log_data or {}
log_data['inventory_id'] = inventory_id
log_data['updated_ct'] = 0
log_data['unmodified_ct'] = 0
log_data['cleared_ct'] = 0
# The summary file is directly inside the artifacts dir
summary_path = os.path.join(artifacts_dir, 'host_cache_summary.json')
if not os.path.exists(summary_path):
logger.error(f'Missing summary file at {summary_path}')
return
hosts_cached = sorted((h for h in hosts_cached if h.id is not None), key=lambda h: h.id)
try:
with open(summary_path, 'r', encoding='utf-8') as f:
summary = json.load(f)
facts_write_time = os.path.getmtime(summary_path) # After successful read
except (json.JSONDecodeError, OSError) as e:
logger.error(f'Error reading summary file at {summary_path}: {e}')
return
host_names = summary.get('hosts_cached', [])
hosts_cached = Host.objects.filter(name__in=host_names).order_by('id').iterator()
# Path where individual fact files were written
fact_cache_dir = os.path.join(artifacts_dir, 'fact_cache')
hosts_to_update = []
for host in hosts_cached:
filepath = os.sep.join(map(str, [destination, host.name]))
if not os.path.realpath(filepath).startswith(destination):
system_tracking_logger.error('facts for host {} could not be cached'.format(smart_str(host.name)))
filepath = os.path.join(fact_cache_dir, host.name)
if not os.path.realpath(filepath).startswith(fact_cache_dir):
logger.error(f'Invalid path for facts file: {filepath}')
continue
if os.path.exists(filepath):
# If the file changed since we wrote the last facts file, pre-playbook run...
modified = os.path.getmtime(filepath)
if (not facts_write_time) or modified > facts_write_time:
with codecs.open(filepath, 'r', encoding='utf-8') as f:
try:
if not facts_write_time or modified >= facts_write_time:
try:
with codecs.open(filepath, 'r', encoding='utf-8') as f:
ansible_facts = json.load(f)
except ValueError:
continue
except ValueError:
continue
if ansible_facts != host.ansible_facts:
host.ansible_facts = ansible_facts
host.ansible_facts_modified = now()
hosts_to_update.append(host)
system_tracking_logger.info(
'New fact for inventory {} host {}'.format(smart_str(host.inventory.name), smart_str(host.name)),
logger.info(
f'New fact for inventory {smart_str(host.inventory.name)} host {smart_str(host.name)}',
extra=dict(
inventory_id=host.inventory.id,
host_name=host.name,
@@ -104,6 +132,8 @@ def finish_fact_cache(hosts_cached, destination, facts_write_time, log_data, job
),
)
log_data['updated_ct'] += 1
else:
log_data['unmodified_ct'] += 1
else:
log_data['unmodified_ct'] += 1
else:
@@ -112,9 +142,11 @@ def finish_fact_cache(hosts_cached, destination, facts_write_time, log_data, job
host.ansible_facts = {}
host.ansible_facts_modified = now()
hosts_to_update.append(host)
system_tracking_logger.info('Facts cleared for inventory {} host {}'.format(smart_str(host.inventory.name), smart_str(host.name)))
logger.info(f'Facts cleared for inventory {smart_str(host.inventory.name)} host {smart_str(host.name)}')
log_data['cleared_ct'] += 1
if len(hosts_to_update) > 100:
if len(hosts_to_update) >= 100:
bulk_update_sorted_by_id(Host, hosts_to_update, fields=['ansible_facts', 'ansible_facts_modified'])
hosts_to_update = []
bulk_update_sorted_by_id(Host, hosts_to_update, fields=['ansible_facts', 'ansible_facts_modified'])