Files
computer-playbook/plugins/lookup/engine.py
T

240 lines
7.6 KiB
Python

"""Lookup ``engine``: resolve a central/embedded engine connection for a consumer.
{{ lookup('engine', 'redis', application_id) }} # full dict
{{ lookup('engine', 'redis', application_id, 'url') }} # one field
{{ lookup('engine', 'redis', application_id, 'session_path') }} # phpredis session.save_path
Mirrors the ``database`` lookup but for the non-RDBMS engines registered in
:data:`utils.roles.applications.services.engines.ENGINES` (redis, memcached,
rabbitmq, elasticsearch, typesense, qdrant, unbound). When the consumer declares
``services.<engine>.shared: true`` the connection points at the central
``svc-db-*`` / ``svc-dns-*`` stack with the consumer's own provisioned credentials;
otherwise it points at the embedded sidecar inside the consumer's own stack.
See ``docs/architecture/central-engines.md``.
"""
from __future__ import annotations
from typing import Any
from urllib.parse import quote
from ansible.errors import AnsibleError
from ansible.plugins.loader import lookup_loader
from ansible.plugins.lookup import LookupBase
from utils.manager.credential_key import OVERRIDE_SECTION
from utils.roles.applications.config import get
from utils.roles.applications.services.engines import (
ENGINES,
engine_service,
is_engine_enabled,
is_engine_shared,
)
from utils.roles.entity.name import entity_name
_FIELDS = (
"engine",
"enabled",
"shared",
"local",
"host",
"port",
"username",
"password",
"instance",
"prefix",
"url",
"container",
"session_path",
)
def redis_session_path(
host: str, port: int, prefix: str, username: str, password: str
) -> str:
"""Return the phpredis ``session.save_path`` of one consumer.
Args:
host: redis host the consumer reaches.
port: redis port.
prefix: consumer prefix; session keys become ``<prefix>:session:<id>``.
username: ACL user, sent only together with a password.
password: redis password; empty for an embedded sidecar.
"""
path = f"tcp://{host}:{port}?prefix={quote(prefix, safe='')}:session:"
if password:
path += (
f"&auth[user]={quote(username, safe='')}"
f"&auth[pass]={quote(password, safe='')}"
)
return path
def qdrant_consumer_key(applications: dict[str, Any], svc_id: str) -> str:
"""Return the engine-wide key a data-owning qdrant consumer presents.
Args:
applications: the resolved applications mapping.
svc_id: the qdrant service role id.
qdrant has no per-consumer principals, only two engine-wide keys, so a
consumer that owns data here holds the engine's own key. The read-only key
is deliberately never returned: it belongs to the MCP adapter, whose whole
read-only guarantee rests on being unable to present anything else.
"""
return str(
get(
applications,
svc_id,
f"{OVERRIDE_SECTION}.api_key",
strict=False,
default="",
skip_missing_app=True,
)
or ""
).strip()
class LookupModule(LookupBase):
def run(
self,
terms: list[Any],
variables: dict[str, Any] | None = None,
**kwargs: Any,
) -> list[Any]:
terms = terms or []
if len(terms) not in (2, 3):
raise AnsibleError(
"engine: requires engine_type, consumer_id [, want_path]"
)
engine = str(terms[0]).strip()
consumer_id = str(terms[1]).strip()
want = str(terms[2]).strip() if len(terms) == 3 else "all"
if engine not in ENGINES:
raise AnsibleError(
f"engine: unknown engine '{engine}'; known: {', '.join(ENGINES)}"
)
if not consumer_id:
raise AnsibleError("engine: consumer_id must not be empty")
vars_ = variables or self._templar.available_variables
applications = lookup_loader.get(
"applications", loader=self._loader, templar=getattr(self, "_templar", None)
).run([], variables=vars_)[0]
descriptor = ENGINES[engine]
svc_id = descriptor["svc"]
port = int(descriptor["port"])
entity = entity_name(consumer_id)
enabled = is_engine_enabled(applications, consumer_id, engine)
shared = is_engine_shared(applications, consumer_id, engine)
svc_cfg = engine_service(applications, consumer_id, engine)
central_name = str(
get(
applications,
svc_id,
f"services.{engine}.name",
strict=False,
default=engine,
skip_missing_app=True,
)
or engine
).strip()
username = entity
prefix = entity
password = str(
svc_cfg.get("password")
or get(
applications,
consumer_id,
f"{OVERRIDE_SECTION}.{engine}_password",
strict=False,
default="",
)
or ""
).strip()
host = central_name if shared else engine
instance = central_name if shared else entity
if engine == "redis" and shared:
username = "default"
password = str(
get(
applications,
svc_id,
f"{OVERRIDE_SECTION}.REDIS_PASSWORD",
strict=False,
default="",
skip_missing_app=True,
)
or ""
).strip()
if engine == "qdrant" and shared:
password = qdrant_consumer_key(applications, svc_id)
if engine == "rabbitmq":
url = (
f"amqp://{username}:{password}@{host}:{port}/{entity}"
if shared
else f"amqp://{host}:{port}/"
)
elif engine in ("redis", "memcached"):
scheme = "redis" if engine == "redis" else "memcached"
auth = f"{username}:{password}@" if (shared and password) else ""
url = f"{scheme}://{auth}{host}:{port}"
elif engine in ("elasticsearch", "typesense", "qdrant"):
url = f"http://{host}:{port}"
else:
url = host
consumer_name = str(
get(
applications,
consumer_id,
f"services.{engine}.name",
strict=False,
default="",
skip_missing_app=True,
)
or ""
).strip()
container = central_name if shared else (consumer_name or f"{entity}-{engine}")
resolved = {
"engine": engine,
"enabled": enabled,
"shared": shared,
"local": bool(enabled and not shared),
"host": host,
"port": port,
"username": username if shared else "",
"password": password if shared else "",
"instance": instance,
"prefix": prefix,
"url": url,
"container": container,
"session_path": (
redis_session_path(
host,
port,
prefix,
username if shared else "",
password if shared else "",
)
if engine == "redis"
else ""
),
}
if want == "all":
return [resolved]
if want not in _FIELDS:
raise AnsibleError(
f"engine: unknown want '{want}'; known: {', '.join(_FIELDS)}"
)
return [resolved.get(want, "")]