mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 03:38:23 +00:00
166 lines
4.9 KiB
Python
166 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
import concurrent.futures
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
|
|
from ruamel.yaml import YAML
|
|
from ruamel.yaml.comments import CommentedMap
|
|
import sys
|
|
from .role_resolver import resolve_role_path
|
|
|
|
|
|
def _fatal(msg: str) -> None:
|
|
raise SystemExit(f"[FATAL] {msg}")
|
|
|
|
|
|
def _ensure_ruamel_map(node: CommentedMap, key: str) -> CommentedMap:
|
|
if key not in node or not isinstance(node.get(key), CommentedMap):
|
|
node[key] = CommentedMap()
|
|
return node[key]
|
|
|
|
|
|
def _generate_credentials_snippet_for_app(
|
|
app_id: str,
|
|
roles_dir: Path,
|
|
host_vars_file: Path,
|
|
vault_password_file: Path,
|
|
project_root: Path,
|
|
env: Optional[Dict[str, str]],
|
|
) -> Optional[CommentedMap]:
|
|
role_path = resolve_role_path(app_id, roles_dir, project_root, env=env)
|
|
if role_path is None:
|
|
return None
|
|
|
|
cmd = [
|
|
# Use current interpreter so utils + deps match the runtime
|
|
sys.executable,
|
|
"-m",
|
|
"cli.create.credentials",
|
|
"--role-path",
|
|
str(role_path),
|
|
"--inventory-file",
|
|
str(host_vars_file),
|
|
"--vault-password-file",
|
|
str(vault_password_file),
|
|
"--snippet",
|
|
"--allow-empty-plain",
|
|
]
|
|
|
|
result = subprocess.run(cmd, text=True, capture_output=True, env=env)
|
|
if result.returncode != 0:
|
|
_fatal(
|
|
f"Command failed ({result.returncode}): {' '.join(cmd)}\n"
|
|
f"STDOUT:\n{result.stdout or ''}\nSTDERR:\n{result.stderr or ''}"
|
|
)
|
|
|
|
snippet_text = (result.stdout or "").strip()
|
|
if not snippet_text:
|
|
return None
|
|
|
|
yaml_rt = YAML(typ="rt")
|
|
try:
|
|
data = yaml_rt.load(snippet_text)
|
|
except Exception as exc:
|
|
raise SystemExit(f"Failed to parse credentials snippet for {app_id}: {exc}")
|
|
|
|
if data is None:
|
|
return None
|
|
if not isinstance(data, CommentedMap):
|
|
cm = CommentedMap()
|
|
for k, v in dict(data).items():
|
|
cm[k] = v
|
|
return cm
|
|
|
|
return data
|
|
|
|
|
|
def generate_credentials_for_roles(
|
|
application_ids: List[str],
|
|
roles_dir: Path,
|
|
host_vars_file: Path,
|
|
vault_password_file: Path,
|
|
project_root: Path,
|
|
env: Optional[Dict[str, str]],
|
|
workers: int = 4,
|
|
) -> None:
|
|
if not application_ids:
|
|
return
|
|
|
|
max_workers = max(1, workers)
|
|
snippets: List[CommentedMap] = []
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
future_to_app: Dict[concurrent.futures.Future, str] = {}
|
|
for app_id in application_ids:
|
|
future = executor.submit(
|
|
_generate_credentials_snippet_for_app,
|
|
app_id,
|
|
roles_dir,
|
|
host_vars_file,
|
|
vault_password_file,
|
|
project_root,
|
|
env,
|
|
)
|
|
future_to_app[future] = app_id
|
|
|
|
for future in concurrent.futures.as_completed(future_to_app):
|
|
app_id = future_to_app[future]
|
|
try:
|
|
snippet = future.result()
|
|
except Exception as exc:
|
|
_fatal(f"Worker for {app_id} failed with exception: {exc}")
|
|
if snippet is not None:
|
|
snippets.append(snippet)
|
|
|
|
if not snippets:
|
|
return
|
|
|
|
yaml_rt = YAML(typ="rt")
|
|
yaml_rt.preserve_quotes = True
|
|
|
|
if host_vars_file.exists():
|
|
with host_vars_file.open("r", encoding="utf-8") as f:
|
|
doc = yaml_rt.load(f)
|
|
if doc is None:
|
|
doc = CommentedMap()
|
|
else:
|
|
doc = CommentedMap()
|
|
|
|
if not isinstance(doc, CommentedMap):
|
|
tmp = CommentedMap()
|
|
for k, v in dict(doc).items():
|
|
tmp[k] = v
|
|
doc = tmp
|
|
|
|
for snippet in snippets:
|
|
apps_snip = snippet.get("applications", {}) or {}
|
|
if isinstance(apps_snip, dict):
|
|
apps_doc = _ensure_ruamel_map(doc, "applications")
|
|
for app_id, app_block_snip in apps_snip.items():
|
|
if not isinstance(app_block_snip, dict):
|
|
continue
|
|
app_doc = _ensure_ruamel_map(apps_doc, app_id)
|
|
creds_doc = _ensure_ruamel_map(app_doc, "credentials")
|
|
|
|
creds_snip = app_block_snip.get("credentials", {}) or {}
|
|
if not isinstance(creds_snip, dict):
|
|
continue
|
|
|
|
for key, val in creds_snip.items():
|
|
if key not in creds_doc or (
|
|
creds_doc.get(key) == "" and val not in ("", None)
|
|
):
|
|
creds_doc[key] = val
|
|
|
|
if (
|
|
"ansible_become_password" in snippet
|
|
and "ansible_become_password" not in doc
|
|
):
|
|
doc["ansible_become_password"] = snippet["ansible_become_password"]
|
|
|
|
host_vars_file.parent.mkdir(parents=True, exist_ok=True)
|
|
with host_vars_file.open("w", encoding="utf-8") as f:
|
|
yaml_rt.dump(doc, f)
|