Files
computer-playbook/plugins/lookup/users.py
T
kevinveenbirkenbachandClaude Opus 5.5 91f29d332e perf(spot): carry the rendered users next to the applications
Ansible forks a fresh worker per task, so every lookup('users') in a forked worker re-rendered the whole user catalogue (298 users, 0.3 to 0.5 s locally and about 1 s on the runner, 1 s locally once a users password is vaulted): _MERGED_USERS_CACHE lives in the process and had no carrier fact.

applications_carrier now also parks lookup('users', carrier=True) in _INFINITO_USERS_RENDERED at the same park points, and clear drops both facts. get_merged_users serves the fact to templated reads when its key matches: the applications carrier key (roles dir, applications, users, DOMAIN_PRIMARY, SYSTEM_EMAIL_DOMAIN, group_names, token store digest) plus inventory_hostname, so a carrier a run_once park broadcasts never answers another host with its become password. A park whose users carrier still matches leaves it in place instead of returning 119 KB again, so parks cost what they cost before.

Real ansible-playbook harness on 2.20.6 and 2.21.5 with the real roles tree: users renders per users-reading task after a park 1 -> 0, task 0.56 -> 0.08 s (vaulted 1.21 -> 0.17 s); a rotated token, a rotated inventory password, a cleared carrier and another host's carrier all re-render; 32 of 32 probe digests equal HEAD's. Removing the carrier check fails three unit tests. The stalwart swarm roundtrip pass 1 and its DR drill ran green with it on all nodes, with no 401 from JMAP, msmtp or LDAP.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 15:53:34 +01:00

75 lines
2.3 KiB
Python

from __future__ import annotations
from typing import Any
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from utils.cache import _reset_cache_for_tests as _reset_runtime_lookup_cache
from utils.cache.carrier import merged_users_cache_key
# nocheck: lookup-cache-import (this lookup IS the users SPOT provider)
from utils.cache.users import get_merged_users
def _reset_cache_for_tests() -> None:
_reset_runtime_lookup_cache()
class LookupModule(LookupBase):
def run(
self,
terms: list[Any] | None,
variables: dict[str, Any] | None = None,
**kwargs: Any,
) -> list[Any]:
"""
Terms:
0) full merged mapping
1) canonical_user_key
2) canonical_user_key, default value
Keyword arguments:
roles_dir: roles tree to build from (defaults to the repository's)
carrier: with no terms, return ``{"key": [...], "users": {...}}``
for the constructor to park in `_INFINITO_USERS_RENDERED`
"""
terms = terms or []
if len(terms) > 2:
raise AnsibleError(
"users: expected 0, 1, or 2 terms: "
"lookup('users'[, canonical_user_key[, default]])"
)
carrier = bool(kwargs.get("carrier", False))
if carrier and terms:
raise AnsibleError("users: carrier=True takes no terms")
variables = variables or getattr(self._templar, "available_variables", {}) or {}
roles_dir = kwargs.get("roles_dir")
users = get_merged_users(
variables=variables,
roles_dir=roles_dir,
templar=getattr(self, "_templar", None),
)
if carrier:
key = merged_users_cache_key(variables, roles_dir=roles_dir)
return [{"key": [key[0], list(key[1])], "users": users}]
if len(terms) == 0:
return [users]
user_key = str(terms[0]).strip()
default_provided = len(terms) == 2
default_value = terms[1] if default_provided else None
if user_key in users:
return [users[user_key]]
if default_provided:
return [default_value]
raise AnsibleError(
f"users: user '{user_key}' not found. Known user keys: {sorted(users)}"
)