module

Krikri::PythonLookupRunner

Constants

UNAVAILABLE_KIND = "unavailable"

The wrapper's response kind when the controller python3 cannot import the ansible package at all - the plugin file itself (which imports LookupBase at its top level) could never load either, so the whole mechanism is unavailable and the caller keeps the old "unknown lookup" behavior.

WRAPPER = "import importlib.util\nimport json\nimport os\nimport sys\n\n\n_unavailable = None\ntry:\n from ansible.plugins.lookup import LookupBase\nexcept Exception as exc:\n _unavailable = \"%s: %s\" % (type(exc).__name__, exc)\n\n\ndef _load_plugin(path):\n modname = \"krikri_lookup_%d\" % hash(path)\n spec = importlib.util.spec_from_file_location(modname, path)\n if spec is None or spec.loader is None:\n raise ImportError(\"cannot load lookup plugin: %s\" % path)\n module = importlib.util.module_from_spec(spec)\n spec.loader.exec_module(module)\n return module\n\n\ndef _register_config(module, name):\n doc = getattr(module, \"DOCUMENTATION\", None)\n if not doc:\n return\n try:\n import yaml\n from ansible import constants as C\n from ansible.parsing.yaml.loader import AnsibleLoader\n parsed = yaml.load(doc, Loader=AnsibleLoader)\n if isinstance(parsed, dict) and isinstance(parsed.get(\"options\"), dict):\n C.config.initialize_plugin_configuration_definitions(\n \"lookup\", name, parsed[\"options\"])\n except Exception:\n pass\n\n\ndef _instantiate(module, plugin_class, name):\n _register_config(module, name)\n try:\n from ansible.parsing.dataloader import DataLoader\n instance = plugin_class(loader=DataLoader())\n except Exception:\n instance = plugin_class()\n instance._load_name = name\n return instance\n\n\ndef _serialize(value):\n if isinstance(value, (set, frozenset)):\n return list(value)\n if isinstance(value, bytes):\n return value.decode(\"utf-8\", \"replace\")\n return str(value)\n\n\ndef main():\n payload = json.loads(sys.stdin.read())\n if _unavailable is not None:\n sys.stdout.write(json.dumps(\n {\"ok\": False, \"kind\": \"unavailable\",\n \"error\": \"ansible is not importable by the controller python3: %s\"\n % _unavailable}))\n return\n try:\n module = _load_plugin(payload.get(\"path\"))\n except ImportError as exc:\n sys.stdout.write(json.dumps(\n {\"ok\": False, \"kind\": \"load\", \"error\": str(exc)}))\n return\n plugin_class = getattr(module, \"LookupModule\", None)\n if plugin_class is None or not isinstance(plugin_class, type) or \\\n not issubclass(plugin_class, LookupBase):\n sys.stdout.write(json.dumps(\n {\"ok\": False, \"kind\": \"not_found\",\n \"error\": \"no LookupModule subclass of LookupBase in %s\"\n % payload.get(\"path\")}))\n return\n instance = _instantiate(module, plugin_class,\n os.path.basename(payload.get(\"path\")).rsplit(\".\", 1)[0])\n result = instance.run(payload.get(\"terms\") or [],\n variables=payload.get(\"variables\") or {},\n **(payload.get(\"kwargs\") or {}))\n sys.stdout.write(json.dumps({\"ok\": True, \"result\": result},\n default=_serialize))\n\n\ntry:\n main()\n# BaseException, not Exception: a plugin's `sys.exit()` raises\n# SystemExit, which would otherwise kill the wrapper before any\n# JSON response - reported as a kind \"error\" plugin failure here\n# rather than silently degrading the whole mechanism.\nexcept BaseException as exc:\n sys.stdout.write(json.dumps(\n {\"ok\": False, \"kind\": \"error\",\n \"error\": \"%s: %s\" % (type(exc).__name__, exc)}))"

The controller-side wrapper: imports the plugin file, finds its LookupModule class (required to subclass the real ansible.plugins.lookup.LookupBase, exactly like Ansible's own loader check), instantiates it with the loader/name Ansible's own loader would give it, and calls run(terms, variables=..., **kwargs), printing the JSON-encoded result. Always responds with a single-line JSON object ({"ok": true, ...} / {"ok": false, kind, error}), so a plugin that prints its own noise cannot corrupt the protocol - only the LAST line of stdout is parsed. An ansible package the controller python3 cannot import reports kind "unavailable" (the whole mechanism degrades; the plugin file could not have imported its own base class either).

Two pieces of Ansible's loader are reproduced here because real plugins depend on them: the plugin's DOCUMENTATION options get registered with ansible's config manager and its _load_name set (PluginLoader._load_config_defs + the loader's own name), without which self.set_options()/self.get_option() raises KeyError - a very common plugin idiom; and the constructor receives a real DataLoader, so self._loader.path_dwim_relative(...) (what find_file_in_search_path is built on) works instead of dying on None. _templar stays None, the one piece this wrapper cannot fake. Both are best-effort: any failure leaves the plugin instantiated exactly as before.

Class methods

warning_sink

Where the spawn-failure warning goes - STDERR in production, replaceable so a spec can capture the text without touching the process's STDERR (same pattern as PythonFilterRunner's).

Source
warning_sink=(warning_sink : IO)

Where the spawn-failure warning goes - STDERR in production, replaceable so a spec can capture the text without touching the process's STDERR (same pattern as PythonFilterRunner's).

Source

Instance methods

call_lookup(name : String, source : String, terms : Array(JSON::Any), variables : Hash(String, JSON::Any), kwargs : Hash(String, JSON::Any)) : JSON::Any

Invokes the LookupModule from the plugin source for name with the resolved terms/kwargs and the current task variables (Ansible passes its own vars dict, which conventionally carries an omit key - see expression_evaluator's call site), returning the structured result list. Raises LookupError when the invocation fails (callers degrade to "undefined" only when the mechanism itself is unavailable - a dispatched failure is a real lookup error).

Source
configured_roots

Ansible's configured lookup plugin directories, searched LAST (its loader puts the role/playbook _extra_dirs ahead of the configured paths). See PythonFilterRunner#configured_roots for the shared ANSIBLE_<TYPE>_PLUGINS / ANSIBLE_HOME rules.

Source
find_source(name : String, role_path : String | Nil, playbook_dir : String | Nil) : String | Nil

A lookup_plugins/<name>.py source for name, searched in the role's own lookup_plugins/ first, then the playbook-adjacent one, then the lookup_plugins/ of every role LOADED this run (Ansible's add_all_plugin_dirs at Role.load - a meta dependency's lookup plugin stays reachable from the depending role's own tasks, which is how gzm55.local_id_plugin's id served gzm55.local_ssh_known_hosts, issue #519; same nearest-first convention as PythonFilterRunner#find_sources), and finally Ansible's CONFIGURED lookup directories (see configured_roots). Unlike filters - whose names come from each FilterModule's filters() dict - Ansible's plugin loader derives a lookup plugin's name from its FILE NAME, so no Python introspection (and no mtime cache) is needed here: lookup_plugins/ manala_environment_files.py IS the manala_environment_files lookup, whatever class it declares.

Source
hashi_vault_load_failure(name : String | Nil, python : String | Nil = python_executable) : String | Nil

community.hashi_vault's lookup is not implemented, but ansible's loader imports hvac before the lookup runs, so on a controller python3 without it the task fails at load time with this message (the host whose Python is named is the controller itself). With hvac present ansible would go on to reach a Vault server, which krikri cannot do either, so only the missing-library case is reproducible.

Source
python_executable

The CONTROLLER's own python3 - the interpreter Ansible needs on the controller anyway (same detection pattern as PythonFilterRunner#python_executable).

Source

Nested types