Skip to content

Read the Ansible fact cache in both key schemas - #2894

Draft
ideaship wants to merge 1 commit into
mainfrom
fact-cache-both-schemas
Draft

ideaship wants to merge 1 commit into
mainfrom
fact-cache-both-schemas

Conversation

@ideaship

Copy link
Copy Markdown
Contributor

ansible-core 2.19 changed how cache plugins store entries. Before 2.19 the redis cache plugin stores a host's facts as plain JSON under ansible_facts<host>. From 2.19 on they land under ansible_factss1_<host> as {"__payload__": "<JSON>"}, and values set with set_fact: cacheable keep data tags ({"value": ..., "tags": [...], "__ansible_type": ...}).

kolla-ansible (2.20) already writes the new key, osism-ansible (2.18) and ceph-ansible (2.16) write the old one, and both coexist on a running cluster. Every reader in python-osism used only the old key:

  • osism get facts / osism dump facts and osism get states (commands/get.py)
  • GET /v1/inventory/hosts/{host}/facts and .../facts/{fact} (api.py)
  • the RabbitMQ node address lookup (utils/rabbitmq.py), used by the RabbitMQ commands in commands/migrate.py and commands/status.py
  • osism reset facts --limit (commands/reset.py), which deleted only the old key
  • the facts freshness check run by osism apply (check_ansible_facts), which derived the host name s1_<host> from new keys and then skipped those entries as malformed

Change

New module osism/utils/fact_cache.py, and all of the readers above go through it:

  • decode() parses an entry in either schema, unwraps __payload__ and strips data tags itself rather than with ansible-core's private decoder, so it does not depend on which ansible-core the reading image ships.
  • get_host_facts() reads both keys and returns the newer entry by ansible_date_time.epoch.
  • scan_host_keys() groups both schemas' keys by host; the freshness check judges each host by its newest entry.
  • delete_host_facts() deletes both keys and removes <host> / s1_<host> from the ansible_cache_keys sorted set the redis cache plugin maintains. osism reset facts without --limit also deletes that set and reports distinct hosts.

The API facts endpoints catch ValueError (which decode() also raises for a non-mapping entry). The freshness check logs "unusable ansible_date_time.epoch" for a well-formed entry with a non-numeric epoch instead of calling it malformed; the host is still skipped.

Testing

  • tests/unit/utils/test_fact_cache.py (fakeredis): both schemas alone and together, either one newer, entries without a timestamp, tagged values, corrupt entries, the sorted-set cleanup. New freshness-check and API tests with new-format entries; existing tests that pinned the exact redis calls now expect both keys. Unit suite: 3539 passed, 3 xfailed.
  • OSISM 11.0.3 as released (osism-ansible 2.18, kolla-ansible 2.20; both formats present, written ~20 s apart), read-only with this branch mounted into a container of the osism image: the newer entry is returned for every host, osism dump facts shows it where the current code shows the older one, and the freshness check names each host once.
  • An OSISM 11.0.3 deploy with a locally built osism-ansible image on ansible-core 2.20.10 and the osism image, both carrying this change (Ceph via cephadm, so no runtime writes the old key): redis held 0 old-format and 8 new-format keys; osism dump facts returned current facts and GET /v1/inventory/hosts/<host>/facts/<fact> answered 200.

🤖 Generated with Claude Code

ansible-core 2.19 changed how cache plugins store entries. Before 2.19
the redis cache plugin stores a host's facts as plain JSON under
ansible_facts<host>. From 2.19 on, ansible-core puts a schema prefix
inside the plugin's key prefix and wraps the value, so the same host's
facts land under ansible_factss1_<host> as {"__payload__": "<JSON>"}.
Values set with set_fact cacheable keep their data tags in the payload,
as {"value": ..., "tags": [...], "__ansible_type": ...}.

The runtimes OSISM ships straddle that line: kolla-ansible (2.20)
writes the new key, osism-ansible (2.18) and ceph-ansible (2.16) write
the old one, and both coexist on a running cluster. Every reader in
python-osism used only the old key:

- osism get facts / osism dump facts and osism get states
  (commands/get.py)
- GET /v1/inventory/hosts/{host}/facts and .../facts/{fact} (api.py)
- the RabbitMQ node address lookup (utils/rabbitmq.py), used by the
  RabbitMQ commands in commands/migrate.py and commands/status.py
- osism reset facts --limit (commands/reset.py), which deleted only
  the old key
- the facts freshness check run by osism apply (check_ansible_facts),
  which derived the host name "s1_<host>" from new keys and then
  skipped their entries as malformed

Once osism-ansible moves to ansible-core 2.19 or later, the facts it
gathers land under the new key as well, and only ceph-ansible keeps the
old one current. Tried on an OSISM 11.0.3 cluster with an image derived
from osism-ansible 0.20261111.0 that only replaces ansible-core with
2.19.15: after `osism apply facts`, redis held fresh facts under
ansible_factss1_<host> while ansible_facts<host> was a day old, and
osism dump facts showed the old data.

Add osism/utils/fact_cache.py and route all of these readers through
it:

- decode() parses an entry in either schema, unwraps __payload__ and
  strips data tags. It does this itself rather than with ansible-core's
  private _cache_persistence decoder, so it returns plain JSON data and
  does not depend on ansible-core internals or on which ansible-core
  the reading image ships.
- get_host_facts() reads both keys and returns the newer entry by
  ansible_date_time.epoch, since which key is current depends on which
  runtime gathered last; on a tie, or without timestamps, the
  schema-prefixed entry wins.
- scan_host_keys() groups the keys of both schemas by host; the
  freshness check judges each host by its newest entry.
- delete_host_facts() deletes both keys and also removes <host> and
  s1_<host> from the ansible_cache_keys sorted set the redis cache
  plugin maintains. osism reset facts without --limit now deletes that
  set too and reports the number of distinct hosts.

The API facts endpoints catch ValueError instead of JSONDecodeError,
since decode() also rejects entries that are not a mapping. The
freshness check now logs "unusable ansible_date_time.epoch" for a
well-formed entry with a non-numeric epoch instead of calling it
malformed; the host is still skipped.

Tests: tests/unit/utils/test_fact_cache.py covers the module against
fakeredis, and new tests cover the freshness check and the API with
new-format entries. Existing tests that pinned the exact redis calls
now expect both keys.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

1 participant