#!/usr/bin/env python3 """Invoke tasks — замена Taskfile.yml""" import os import subprocess import sys from invoke.context import Context from invoke.exceptions import Exit from invoke.tasks import task HOSTS_FILE = "production.yml" AUTHELIA_DOCKER = "docker run --rm -v $PWD:/data authelia/authelia:4.39.4 authelia" def _yq(query: str) -> str: result = subprocess.run( ["yq", query, HOSTS_FILE], capture_output=True, text=True, check=True ) return result.stdout.strip() def _remote_user() -> str: return _yq(".ungrouped.hosts.server.ansible_user") def _remote_host() -> str: return _yq(".ungrouped.hosts.server.ansible_host") def _rest_args() -> list[str]: """Возвращает аргументы после '--' из sys.argv""" try: return sys.argv[sys.argv.index("--") + 1 :] except ValueError: return [] def _resolve_playbook(name: str) -> str: candidates = [name, f"{name}.yml", f"playbook-{name}.yml"] for candidate in candidates: if os.path.isfile(candidate): return candidate raise Exit( f"Плейбук для '{name}' не найден. Проверял: {', '.join(candidates)}", code=1 ) @task def install_roles(ctx: Context) -> None: """Установить ansible-galaxy roles""" ctx.run("uv run ansible-galaxy role install --role-file requirements.yml --force") @task def pl(ctx: Context) -> None: """Запустить плейбуки по имени: inv pl -- gitea miniflux""" names = _rest_args() if not names: raise Exit("Укажи хотя бы один плейбук: inv pl -- [name ...]", code=1) playbooks = [_resolve_playbook(name) for name in names] ctx.run( f"uv run ansible-playbook -i production.yml --diff {' '.join(playbooks)}", pty=True, ) @task def ssh(ctx: Context) -> None: """SSH на удалённый сервер""" ctx.run(f"ssh {_remote_user()}@{_remote_host()}", pty=True) @task def btop(ctx: Context) -> None: """Запустить btop на удалённом сервере""" ctx.run(f"ssh {_remote_user()}@{_remote_host()} -t btop", pty=True) @task def encrypt(ctx: Context, args: str = "") -> None: """Зашифровать файлы через ansible-vault""" ctx.run(f"uv run ansible-vault encrypt {args}") @task def decrypt(ctx: Context, args: str = "") -> None: """Расшифровать файлы через ansible-vault""" ctx.run(f"uv run ansible-vault decrypt {args}") @task def authelia_cli(ctx: Context, args: str = "") -> None: """Запустить authelia CLI в docker""" ctx.run(f"{AUTHELIA_DOCKER} {args}") @task def authelia_validate_config(ctx: Context) -> None: """Отрендерить конфиг authelia из шаблона и проверить его""" dest = "temp/configuration.yml" try: ctx.run( "uv run ansible localhost" " --module-name template" f' --args "src=files/authelia/configuration.template.yml dest={dest}"' " --extra-vars @vars/secrets.yml" " --extra-vars @files/authelia/secrets.yml" ) ctx.run(f"{AUTHELIA_DOCKER} validate-config --config /data/{dest}") finally: ctx.run(f"rm -f {dest}", warn=True) @task def authelia_gen_random_string(ctx: Context, length: int = 10) -> None: """Сгенерировать случайную alphanumeric-строку""" ctx.run(f"{AUTHELIA_DOCKER} crypto rand --length {length} --charset alphanumeric") @task def authelia_gen_secret_and_hash(ctx: Context, length: int = 72) -> None: """Сгенерировать случайный секрет и его pbkdf2-sha512 хэш""" ctx.run( f"{AUTHELIA_DOCKER} crypto hash generate pbkdf2" f" --variant sha512 --random --random.length {length} --random.charset rfc3986" ) @task def format_py_files(ctx: Context) -> None: """Отформатировать Python-файлы через Black в docker""" uid = os.getuid() gid = os.getgid() ctx.run( f"docker run --rm -u {uid}:{gid} -v $PWD:/app -w /app" " pyfound/black:latest_release black ." )