From 5cd66d01bcb2c2a9352f82418faead2df9a4469c Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 4 Jul 2026 16:56:01 +0300 Subject: [PATCH] Tududi: add task tracke application --- AGENTS.md | 2 + docs/drafts/shared-roles-sync.md | 208 +++++++++++ files/authelia/configuration.template.yml | 35 ++ files/bifrost/config.template.json | 71 ++++ files/bifrost/docker-compose.template.yml | 33 ++ files/caddyproxy/Caddyfile.template | 25 ++ files/tududi/docker-compose.template.yml | 75 ++++ playbook-all-applications.yml | 6 + playbook-bifrost.yml | 79 +++++ playbook-tududi.yml | 71 ++++ vars/secrets.yml | 403 +++++++++++++--------- 11 files changed, 846 insertions(+), 162 deletions(-) create mode 100644 docs/drafts/shared-roles-sync.md create mode 100644 files/bifrost/config.template.json create mode 100644 files/bifrost/docker-compose.template.yml create mode 100644 files/tududi/docker-compose.template.yml create mode 100644 playbook-bifrost.yml create mode 100644 playbook-tududi.yml diff --git a/AGENTS.md b/AGENTS.md index 6760f4e..5b4c05e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,6 +75,8 @@ uv run ansible-galaxy install --role-file requirements.yml - `playbook-wanderer.yml` — пешие маршруты. - `playbook-remembos.yml` — интервальное повторение. - `playbook-tuwunel.yml` — Matrix-сервер (Tuwunel) с federation-делегацией на apex-домен. +- `playbook-tududi.yml` — планировщик задач (SQLite, OIDC через Authelia, AI-фичи через Bifrost). +- `playbook-bifrost.yml` — LLM-шлюз (OpenAI-совместимый) для AI-фич других приложений, провайдер DeepSeek. ### Агрегатные и служебные diff --git a/docs/drafts/shared-roles-sync.md b/docs/drafts/shared-roles-sync.md new file mode 100644 index 0000000..479271c --- /dev/null +++ b/docs/drafts/shared-roles-sync.md @@ -0,0 +1,208 @@ +# Общие Ansible-роли между серверными репозиториями + +## Статус + +Черновик. Подход обсуждён, реализация не начата. + +## Контекст + +Серверы управляются изолированными Ansible-репозиториями: по одному на +сервер (pet-project-server, buckland, далее — новый торрент-бокс). +Кастомные роли (`owner`, `eget`, `secrets`) концептуально общие, но +физически продублированы, и копии дрейфуют: `eget` разошёлся +косметически, `owner` в buckland превратился в мёртвый код, пока +актуальная версия развивалась в pet-project-server. + +Нужен способ держать роли общими между репозиториями, при этом: + +- правка роли применяется мгновенно — без петли commit → push → + install; +- каждый репозиторий остаётся самодостаточным: клонируется и + деплоится без внешних зависимостей, CI (yamllint + ansible-lint) + работает как сейчас; +- обновление роли на конкретном сервере — явное решение, а не + незаметный сайд-эффект. + +## Осознанный выбор для pets-серверов + +Это решение спроектировано под подход «pets, not cattle» и не +претендует на масштаб. Серверы — питомцы: у каждого своё железо, свой +набор приложений и свой темп жизни. Из этого следуют принципы, +противоположные командной инженерии: + +- дублирование допустимо, принудительная консистентность — нет; +- каждый сервер принимает обновление общей роли явно, в свой момент, + через видимый дифф в собственной истории git; +- никакой инфраструктуры публикации (registry, версии, релизы) — + потребитель ролей один человек, который сам же их и пишет. + +Если серверов станет много или появятся другие потребители — решение +нужно пересмотреть (см. «Путь миграции»). + +## Решение + +Вендоринг копий плюс локальная синхронизация через файловую систему. + +Каноническая версия ролей живёт в отдельной директории +`~/projects/private/ansible-shared/`. Каждый серверный репозиторий +держит свою закоммиченную копию нужных ролей в `roles/` и +синхронизируется с каноном командами invoke (rsync, git не участвует). + +``` +~/projects/private/ + ansible-shared/ + roles/ + owner/ + eget/ + secrets/ + pet-project-server/ + roles/owner/ # закоммиченная копия + roles/eget/ + roles/secrets/ + buckland/ + roles/eget/ # подписан только на eget + <новый сервер>/ + roles/... +``` + +Ключевое свойство: «пином версии» служит сама закоммиченная копия. +Репозиторий всегда деплоит ровно то, что лежит в его git; обновление +роли — это `roles-pull` + осознанный коммит с читаемым диффом. + +### Подписка + +Каждый репозиторий объявляет в `tasks.py` список ролей, которые он +синхронизирует: + +```python +SYNCED_ROLES = ["owner", "eget", "secrets"] +``` + +Подписка явная и пер-серверная: buckland может не подписываться на +`owner`, пока живёт в старой архитектуре (всё под `primary_user`). +Роли вне списка синхронизация не трогает — репозиторий может держать +и сугубо локальные роли. + +### Команды + +Три задачи invoke, одинаковый блок (~30 строк) в `tasks.py` каждого +репозитория: + +- `inv roles-pull [-- ]` — канон → репозиторий. Для каждой роли + из подписки (или одной указанной): + `rsync -a --delete /roles// roles//`. + Флаг `--delete` обязателен: удалённые в каноне файлы должны исчезать + из копии, иначе дрейф через «хвосты». +- `inv roles-push [-- ]` — репозиторий → канон, тот же rsync в + обратную сторону. +- `inv roles-status` — `diff -r -q` каждой подписанной роли против + канона; выводит `ok` / `differs` / `missing in shared` по ролям, + ненулевой код выхода при расхождении. + +Защита от ошибок: + +- обе команды печатают список затронутых файлов (`rsync -i`) — операция + видна, а не молчалива; +- `roles-push` при отличающемся каноне показывает дифф и просит + подтверждение (защита от затирания более свежего канона устаревшей + копией; типовой случай — правили роль в двух репозиториях + параллельно); +- `roles-pull` подтверждения не требует: затирается рабочая копия, её + состояние защищено git — `git diff` покажет, что приехало, и любое + изменение можно откатить до коммита. + +### Рабочий цикл + +Правка роли: + +1. Роль правится прямо в том репозитории, где идёт работа, — тестировать + её всё равно можно только запуском реального плейбука этого сервера. + Петля правка → запуск нулевая. +2. Когда правка устоялась: `inv roles-push` и коммит в серверном + репозитории. +3. В остальных репозиториях — `inv roles-pull`, когда их сервер готов + принять обновление. Дифф виден в `git diff`, коммитится в историю + этого репозитория. + +Новый сервер: + +1. Создать репозиторий, объявить `SYNCED_ROLES`. +2. `inv roles-pull` — роли приезжают из канона, коммитятся. + +Новая общая роль: написать в любом репозитории, добавить в его +`SYNCED_ROLES`, `inv roles-push`; остальные подписываются по мере +надобности. + +### Предохранитель в lefthook + +Дрейф неизбежен (синхронизация — дисциплина, не механизм), поэтому он +должен быть видимым. В pre-commit каждого репозитория — проверка: если +в staged-файлах есть `roles/<подписанная роль>/` и роль отличается от +канона, печатается предупреждение «роль X отличается от ansible-shared, +не забудь roles-push / roles-pull». Именно предупреждение, не +блокировка: коммит локальной правки до push в канон — нормальный +рабочий момент. Если директории `ansible-shared` нет на машине, +проверка молча пропускается (CI, чужая машина). + +### Статус ansible-shared + +`ansible-shared` — рабочая директория, а не репозиторий-сервис. Можно +сделать её git-репозиторием для бэкапа и истории канона, но этот git +не стоит в рабочей петле: коммиты туда делаются когда угодно и ничего +не блокируют. Серверные репозитории не знают о её git-статусе — они +видят только файлы. + +## Отвергнутые альтернативы + +- **Общий `roles_path` в ansible.cfg** (`roles_path = + ./roles:../ansible-shared/roles`). Нулевая синхронизация, но + репозитории теряют самодостаточность (CI и клон на другой машине + ломаются), а главное — правка роли молча применяется ко всем серверам + при следующем запуске. Для pets-подхода это анти-свойство. +- **Симлинки на общую директорию, закоммиченные в git.** Та же + семантика плюс ломкость: валидны только при конкретной раскладке + директорий на диске. +- **Git submodule / subtree.** Решают задачу для команд ценой петли + commit → push → update и известной UX-боли; принудительная + консистентность здесь не нужна, а петля — главное, от чего уходим. +- **Galaxy-роли из git-репозитория** (`requirements.yml` с git-URL, + версии тегами). Правильный инструмент при росте масштаба, но сейчас + церемония tag → push → install — это и есть слишком большая петля + обратной связи для одного человека. +- **Монорепозиторий на все серверы.** Отвергнут на уровне организации + проектов: серверы — pets, изолированные репозитории с локальной + историей ценнее общей точки правды. + +## Цена и риски + +- Синхронизация держится на дисциплине. Забытый `roles-push` — канон + отстал; правка одной роли в двух репозиториях без push — конфликт, + который разрешается руками через дифф. Митигация — предупреждение в + lefthook и `roles-status`; при текущем темпе изменений (единицы в + год) этого достаточно. +- Канон существует в одном экземпляре на одной машине. Потеря машины — + потеря только канона, не ролей: они восстановимы из любой свежей + копии (`roles-push`). Опциональный git в `ansible-shared` закрывает + и это. +- Блок задач в `tasks.py` дублируется по репозиториям и сам может + дрейфовать. Принимаем: это ~30 строк, меняются реже ролей. + +## Путь миграции + +Если ролей станет десяток, серверов — больше трёх, или появится второй +пользователь, `ansible-shared` уже имеет структуру стандартного +galaxy-источника: повесить теги, перевести репозитории на +`requirements.yml` с git-URL, удалить rsync-задачи. Вендоринг копий +делает переход безопасным — в любой момент репозитории самодостаточны. + +## План внедрения + +1. Создать `~/projects/private/ansible-shared/roles/` из актуальных + ролей pet-project-server (`owner`, `eget`, `secrets`). +2. Добавить задачи `roles-pull` / `roles-push` / `roles-status` в + `tasks.py` pet-project-server; объявить подписку на все три роли. +3. То же в buckland; подписка только на `eget` (его копия сначала + выравнивается через `roles-pull` — дифф косметический, стиль + кавычек). Мёртвую `roles/owner` удалить. +4. Предупреждение о дрейфе в lefthook обоих репозиториев. +5. Новый сервер с самого начала строится на подписке. diff --git a/files/authelia/configuration.template.yml b/files/authelia/configuration.template.yml index be9da45..197d305 100644 --- a/files/authelia/configuration.template.yml +++ b/files/authelia/configuration.template.yml @@ -732,6 +732,10 @@ access_control: subject: "group:admins" policy: "two_factor" + - domain: "bifrost.vakhrushev.me" + subject: "group:admins" + policy: "two_factor" + - domain: "wanderbase.vakhrushev.me" subject: "group:admins" policy: "two_factor" @@ -1343,6 +1347,18 @@ identity_providers: ## Authorization Policies which can be utilized by clients. The 'policy_name' is an arbitrary value that you pick ## which is utilized as the value for the 'authorization_policy' on the client. + ## Claims Policies (Authelia 4.39+): по умолчанию Authelia НЕ кладёт email/ + ## профильные claims в ID Token — только в UserInfo. tududi читает claims + ## ТОЛЬКО из ID Token (userinfo не вызывает), поэтому копируем их в id_token + ## явно (при условии, что соответствующий scope выдан клиенту). + claims_policies: + tududi_claims: + id_token: + - "email" + - "email_verified" + - "name" + - "preferred_username" + authorization_policies: outline_policy: rules: @@ -1401,6 +1417,25 @@ identity_providers: userinfo_signed_response_alg: "none" token_endpoint_auth_method: "client_secret_basic" + - client_name: "Tududi" + client_id: "{{ oidc__tududi__client_id }}" + client_secret: "{{ oidc__tududi__client_secret }}" + redirect_uris: + - "https://tududi.vakhrushev.me/api/oidc/callback/authelia" + scopes: + - "openid" + - "profile" + - "email" + response_types: + - "code" + grant_types: + - "authorization_code" + access_token_signed_response_alg: "none" + userinfo_signed_response_alg: "none" + token_endpoint_auth_method: "client_secret_basic" + authorization_policy: "one_factor" + claims_policy: "tududi_claims" + - client_name: "Wakapi" client_id: "{{ oidc__wakapi__client_id }}" client_secret: "{{ oidc__wakapi__client_secret }}" diff --git a/files/bifrost/config.template.json b/files/bifrost/config.template.json new file mode 100644 index 0000000..d6a1ec8 --- /dev/null +++ b/files/bifrost/config.template.json @@ -0,0 +1,71 @@ +{# Конфиг Bifrost — ИСТОЧНИК ПРАВДЫ. Рендерится в config_dir/config.json (app-dir + Bifrost смонтирован на /app/data), Bifrost читает его при старте. + source_of_truth=config.json: на каждом старте секции этого файла авторитетно + перезаписывают состояние в config.db (git выигрывает у правок через UI). + + Раскладка каталогов (по требованию «конфиги/данные раздельно»): + - config_dir -> /app/data : сам config.json (app-dir Bifrost по умолчанию); + - data_dir -> /app/store : config.db + logs.db (пути заданы явно ниже). + + Несекретные настройки (base URL провайдера, имя модели) заданы здесь прямо + строками — они больше нигде не переиспользуются. Секреты (ключ DeepSeek, + encryption_key) рендерятся строками из vars/secrets.yml, поэтому готовый + config.json — секретный файл (плейбук пишет его на 0600). + + Провайдер deepseek заведён как КАСТОМНЫЙ OpenAI-совместимый + (base_provider_type=openai + network_config.base_url) — нативного провайдера + deepseek у Bifrost нет. ВАЖНО: Bifrost сам дописывает к base_url путь + /v1/chat/completions, поэтому base_url задаём БЕЗ хвостового /v1. + + Роутинг — декларативно через governance.routing_rules (требует включённого + config_store). tududi ходит на drop-in эндпоинт /openai и шлёт ЗАХАРДКОЖЕННОЕ + имя модели gpt-4o-mini «голым». Правило ловит его по cel и подменяет на + реальную модель deepseek/deepseek-v4-pro. Фолбэка нет — единственный провайдер. + Чтобы сменить бэкенд-модель, правим ТОЛЬКО этот файл, конфиг tududi не трогаем. + + Эти Jinja-комментарии в готовый config.json не попадают (на выходе чистый JSON). #} +{ + "$schema": "https://www.getbifrost.ai/schema", + "encryption_key": {{ bifrost_encryption_key | to_json }}, + "client": { + "enable_logging": true + }, + "providers": { + "deepseek": { + "keys": [ + { "name": "deepseek-primary", "value": {{ bifrost_deepseek_api_key | to_json }}, "models": ["*"], "weight": 1.0 } + ], + "network_config": { "base_url": "https://api.deepseek.com", "max_retries": 2 }, + "custom_provider_config": { + "base_provider_type": "openai", + "allowed_requests": { "chat_completion": true, "chat_completion_stream": true } + } + } + }, + "config_store": { + "enabled": true, + "type": "sqlite", + "config": { "path": "/app/store/config.db" }, + "source_of_truth": "config.json" + }, + "logs_store": { + "enabled": true, + "type": "sqlite", + "config": { "path": "/app/store/logs.db" } + }, + "governance": { + "routing_rules": [ + { + "id": "tududi-gpt4omini-to-deepseek-pro", + "name": "Tududi gpt-4o-mini -> DeepSeek v4 Pro", + "enabled": true, + "cel_expression": "model == 'gpt-4o-mini'", + "targets": [ + { "provider": "deepseek", "model": "deepseek-v4-pro", "weight": 1 } + ], + "scope": "global", + "priority": 0 + } + ] + } +} diff --git a/files/bifrost/docker-compose.template.yml b/files/bifrost/docker-compose.template.yml new file mode 100644 index 0000000..be96561 --- /dev/null +++ b/files/bifrost/docker-compose.template.yml @@ -0,0 +1,33 @@ +# Bifrost — LLM-gateway (github.com/maximhq/bifrost). Единый OpenAI-совместимый +# шлюз для AI-фич других приложений (сейчас — tududi). За ним провайдер DeepSeek. +# Клиенты ходят в него по имени bifrost_app:8080 в общей web_proxy_network и про +# провайдера/модель ничего не знают — роутинг решает config.json. +# See versions: https://hub.docker.com/r/maximhq/bifrost/tags +# +# В контейнере Bifrost слушает 8080 (дефолт). Наружу host-порт НЕ пробрасываем: +# веб-UI (Dashboard + Logs) и API доступны только через Caddy +# (bifrost.vakhrushev.me, за Authelia forward_auth) и внутри web_proxy_network. +# +# App-dir Bifrost по умолчанию — /app/data; туда монтируется config_dir с +# config.json (источник правды, с секретами, 0600). SQLite-сторы config.db / +# logs.db вынесены в data_dir -> /app/store (пути заданы явно в config.json). + +services: + + bifrost_app: + image: maximhq/bifrost:v1.6.2 # проверь актуальный стабильный тег в releases + container_name: bifrost_app + restart: unless-stopped + user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}" + networks: + - "web_proxy_network" + volumes: + # App-dir: config.json (рендерит плейбук). + - "{{ config_dir }}:/app/data" + # Сторы, создаёт и держит сам Bifrost — бекапить не нужно (config.db + # восстанавливается из config.json, logs.db — просто логи запросов). + - "{{ data_dir }}:/app/store" + +networks: + web_proxy_network: + external: true diff --git a/files/caddyproxy/Caddyfile.template b/files/caddyproxy/Caddyfile.template index 536c193..fb78a61 100644 --- a/files/caddyproxy/Caddyfile.template +++ b/files/caddyproxy/Caddyfile.template @@ -152,6 +152,17 @@ memos.vakhrushev.me { } } +# tududi сам логинит пользователей через OIDC (Authelia), поэтому здесь без +# forward_auth — иначе двойная аутентификация. +tududi.vakhrushev.me { + tls anwinged@ya.ru + import access_log + + reverse_proxy { + to tududi_app:3002 + } +} + remembos.vakhrushev.me { tls anwinged@ya.ru import access_log @@ -233,3 +244,17 @@ goaccess.vakhrushev.me { reverse_proxy goaccess_app:8080 } +# Bifrost — внутренний LLM-шлюз. Веб-UI (дашборд + LLM-логи + ключи) закрыт за +# Authelia (2FA). Сам API tududi дёргает не через Caddy, а по web_proxy_network. +bifrost.vakhrushev.me { + tls anwinged@ya.ru + import access_log + + forward_auth authelia_app:9091 { + uri /api/authz/forward-auth + copy_headers Remote-User Remote-Groups Remote-Email Remote-Name + } + + reverse_proxy bifrost_app:8080 +} + diff --git a/files/tududi/docker-compose.template.yml b/files/tududi/docker-compose.template.yml new file mode 100644 index 0000000..0cc7ece --- /dev/null +++ b/files/tududi/docker-compose.template.yml @@ -0,0 +1,75 @@ +# Tududi — планировщик задач (github.com/chrisvel/tududi). +# See versions: https://hub.docker.com/r/chrisvel/tududi/tags +# +# Образ стартует entrypoint'ом от root, chown'ит /app/backend на PUID:PGID и +# сбрасывает права через su-exec — поэтому НЕ задаём `user:`, только PUID/PGID +# (совпадают с системным пользователем tududi, владельцем смонтированных каталогов). +# +# Внутренний порт — 3002 (frontend отдаётся тем же backend'ом). Caddy проксирует +# tududi.vakhrushev.me -> tududi_app:3002. Аутентификация — собственный OIDC-логин +# tududi к Authelia (НЕ forward_auth), поэтому в Caddy домен открыт. + +services: + + tududi_app: + image: chrisvel/tududi:1.1.1 + container_name: tududi_app + restart: unless-stopped + networks: + - "web_proxy_network" + volumes: + - "{{ db_dir }}:/app/backend/db" + - "{{ uploads_dir }}:/app/backend/uploads" + environment: + - PUID={{ app_owner_uid }} + - PGID={{ app_owner_gid }} + + - NODE_ENV=production + - PORT=3002 + - DB_FILE=db/production.sqlite3 + - TUDUDI_UPLOAD_PATH=/app/backend/uploads + + - BASE_URL=https://tududi.vakhrushev.me + - TUDUDI_ALLOWED_ORIGINS=https://tududi.vakhrushev.me + - TUDUDI_TRUST_PROXY=true + + - TUDUDI_SESSION_SECRET={{ tududi_session_secret }} + # Чистый OIDC: сид-админ не создаётся (TUDUDI_USER_EMAIL/PASSWORD не заданы), + # первый вход через Authelia сам заводит пользователя и выдаёт ему admin по + # OIDC_ADMIN_EMAIL_DOMAINS. Локальный парольный вход выключен. + - PASSWORD_AUTH_ENABLED=false + + # OIDC (Authelia). Redirect URI регистрируется в Authelia как + # https://tududi.vakhrushev.me/api/oidc/callback/{OIDC_PROVIDER_SLUG}. + - OIDC_ENABLED=true + - OIDC_PROVIDER_NAME=Authelia + - OIDC_PROVIDER_SLUG=authelia + - OIDC_ISSUER_URL=https://auth.vakhrushev.me + - OIDC_CLIENT_ID={{ tududi_oidc_client_id }} + - OIDC_CLIENT_SECRET={{ tududi_oidc_client_secret }} + - OIDC_SCOPE=openid profile email + - OIDC_AUTO_PROVISION=true + # Домен email, чьим пользователям при auto-provision выдаётся роль админа + # (должен совпадать с доменом email в id_token от Authelia). + - OIDC_ADMIN_EMAIL_DOMAINS=vakhrushev.me + + # Почта (Yandex Cloud Postbox, те же секреты, что у outline/authelia). + - ENABLE_EMAIL=true + - EMAIL_SMTP_HOST={{ postbox_host }} + - EMAIL_SMTP_PORT={{ postbox_port }} + - EMAIL_SMTP_SECURE=false + - EMAIL_SMTP_USERNAME={{ postbox_user }} + - EMAIL_SMTP_PASSWORD={{ postbox_pass }} + - EMAIL_FROM_ADDRESS=tududi@vakhrushev.me + - EMAIL_FROM_NAME=Tududi + + # AI-фичи через Bifrost (drop-in OpenAI-эндпоинт в общей сети — НЕ localhost, + # это сам контейнер). Модель у tududi захардкожена как gpt-4o-mini; Bifrost + # по governance-правилу роутит её на deepseek-v4-pro. openai-node сам + # подхватывает OPENAI_BASE_URL. + - OPENAI_API_KEY={{ tududi_openai_api_key }} + - OPENAI_BASE_URL=http://bifrost_app:8080/openai + +networks: + web_proxy_network: + external: true diff --git a/playbook-all-applications.yml b/playbook-all-applications.yml index 3c31cfd..2a30838 100644 --- a/playbook-all-applications.yml +++ b/playbook-all-applications.yml @@ -43,6 +43,12 @@ - name: "Configure tuwunel" ansible.builtin.import_playbook: playbook-tuwunel.yml +- name: "Configure bifrost" + ansible.builtin.import_playbook: playbook-bifrost.yml + +- name: "Configure tududi" + ansible.builtin.import_playbook: playbook-tududi.yml + # - name: "Configure homepage" diff --git a/playbook-bifrost.yml b/playbook-bifrost.yml new file mode 100644 index 0000000..c925396 --- /dev/null +++ b/playbook-bifrost.yml @@ -0,0 +1,79 @@ +--- +- name: "Configure bifrost application" + hosts: all + + vars_files: + - vars/secrets.yml + - vars/vars.yml + + vars: + app_name: "bifrost" + app_user: "{{ app_name }}" + app_owner_uid: 1107 + app_owner_gid: 1107 + + base_dir: "{{ (application_dir, app_name) | path_join }}" + config_dir: "{{ (base_dir, 'config') | path_join }}" + data_dir: "{{ (base_dir, 'data') | path_join }}" + config_file: "{{ (config_dir, 'config.json') | path_join }}" + + tasks: + - name: "Create user and environment" + ansible.builtin.import_role: + name: owner + vars: + owner_name: "{{ app_user }}" + owner_uid: "{{ app_owner_uid }}" + owner_gid: "{{ app_owner_gid }}" + owner_extra_groups: ["docker"] + + - name: "Create application internal directories" + ansible.builtin.file: + path: "{{ item }}" + state: "directory" + owner: "{{ app_user }}" + group: "{{ app_user }}" + mode: "0750" + loop: + - "{{ base_dir }}" + - "{{ config_dir }}" + - "{{ data_dir }}" + + - name: "Copy bifrost config (source of truth, secret)" + ansible.builtin.template: + src: "./files/{{ app_name }}/config.template.json" + dest: "{{ config_file }}" + owner: "{{ app_user }}" + group: "{{ app_user }}" + mode: "0600" + register: config_file_result + + - name: "Copy docker compose file" + ansible.builtin.template: + src: "./files/{{ app_name }}/docker-compose.template.yml" + dest: "{{ base_dir }}/docker-compose.yml" + owner: "{{ app_user }}" + group: "{{ app_user }}" + mode: "0640" + register: docker_compose_file_result + + - name: "Run application with docker compose" + community.docker.docker_compose_v2: + project_src: "{{ base_dir }}" + state: "present" + remove_orphans: true + tags: + - run-app + + # Bifrost читает config.json только при старте. Если изменился лишь конфиг + # (а compose — нет), `state: present` не пересоздаёт контейнер, и правки + # (ротация ключа, смена модели/правил) не подхватятся — нужен явный рестарт. + - name: "Restart docker compose services if config changed but not docker-compose.yml" + community.docker.docker_compose_v2: + project_src: "{{ base_dir }}" + state: "restarted" + when: + - config_file_result.changed + - not docker_compose_file_result.changed + tags: + - run-app diff --git a/playbook-tududi.yml b/playbook-tududi.yml new file mode 100644 index 0000000..f2cf613 --- /dev/null +++ b/playbook-tududi.yml @@ -0,0 +1,71 @@ +--- +- name: "Configure tududi application" + hosts: all + + vars_files: + - vars/secrets.yml + - vars/vars.yml + + vars: + app_name: "tududi" + app_user: "{{ app_name }}" + app_owner_uid: 1108 + app_owner_gid: 1108 + + base_dir: "{{ (application_dir, app_name) | path_join }}" + db_dir: "{{ (base_dir, 'db') | path_join }}" + uploads_dir: "{{ (base_dir, 'uploads') | path_join }}" + + tasks: + - name: "Create user and environment" + ansible.builtin.import_role: + name: owner + vars: + owner_name: "{{ app_user }}" + owner_uid: "{{ app_owner_uid }}" + owner_gid: "{{ app_owner_gid }}" + owner_extra_groups: ["docker"] + + - name: "Create application internal directories" + ansible.builtin.file: + path: "{{ item }}" + state: "directory" + owner: "{{ app_user }}" + group: "{{ app_user }}" + mode: "0750" + loop: + - "{{ base_dir }}" + - "{{ db_dir }}" + - "{{ uploads_dir }}" + + # SQLite-база и загрузки бэкапятся restic'ом напрямую (без gobackup-дампа): + # ночной прогон одного пользователя -> вероятность записи в момент чтения + # ничтожна, а retention (90 дневных / 36 месячных) страхует от редкого + # несогласованного снимка. + - name: "Create backup targets file" + ansible.builtin.lineinfile: + path: "{{ base_dir }}/backup-targets" + line: "{{ item }}" + create: true + owner: "{{ app_user }}" + group: "{{ app_user }}" + mode: "0750" + loop: + - "{{ db_dir }}" + - "{{ uploads_dir }}" + + - name: "Copy docker compose file" + ansible.builtin.template: + src: "./files/{{ app_name }}/docker-compose.template.yml" + dest: "{{ base_dir }}/docker-compose.yml" + owner: "{{ app_user }}" + group: "{{ app_user }}" + mode: "0640" + + - name: "Run application with docker compose" + community.docker.docker_compose_v2: + project_src: "{{ base_dir }}" + state: "present" + remove_orphans: true + tags: + - run-app diff --git a/vars/secrets.yml b/vars/secrets.yml index 1fdf55a..c898339 100644 --- a/vars/secrets.yml +++ b/vars/secrets.yml @@ -1,163 +1,242 @@ $ANSIBLE_VAULT;1.1;AES256 -37316466386337633433633632333065613137643963356437653030653433353464666636383238 -6131616231356633383839653530666333343666316363630a323736656238336633336363656634 -35633031623730613966343533393033386538346162333061613135353063323533336135376337 -6262346462636361370a356336616330323738383535356238383238656366303833313137613461 -39613266616632376433636263653332363661373832316538343530353963363762356534373264 -38316134643632386564626662616235316563343833646164336439623035303838316630393030 -32636566663835626362666235363432316430633139333935383031303564353633323835616530 -39353336366634356530393339633836613563666435323533323731666666656339313431613161 -63386639663635393339643965643664303130633234333130663336623738313665366630316539 -39373930316434636261646431633666653066373632396136313639633266383036643739323936 -63343836666263623966333364616230393338386439616264616437373739373366326430363038 -61333237323139336236333366666164313364366663333335646139353061323362313465613865 -32376234323936336466356239666162323330323133616166333733613566636635303965316665 -61616135333964613862303432323032313533353132303762396539323033636636643539663334 -36663866313130303531333961373337316462376564396639656265393663373230623937376337 -34656339323864633462633030333730613765653631636233356535643333356661333331303935 -33646364636434336162356535393162353533636262333735616463623432653933316230306261 -38353766363130333963613830643735663930353162343665323435613435326638363162356438 -35666666343165323561396336646264613633323066613665346535313165306435666463353030 -33626661326536646264343531653834616563316533386539656431353165633161666366386433 -66623662303636393732643330333664373935313932306232306433653038623461393030306562 -66663637636534393631633939363037303331383166343561373163666263323563313636326538 -31656132376636653336353666316639623230613563333363653965333435323231303161333231 -65353539633132373036346233626533356139373938646130376437626532383864623138613463 -65373530343563613633323439636638316330663639306438323761306238616539353262326166 -61323239666661366665343736323834633065306237643538383238616563353366343936663835 -66313066653832636534346136373962653136303435623365376235626463366633623561313231 -66343861363336303136666136363531386166316631643565383835333261356237373736386638 -32653565623034303939383134613235353237393035336662303432653734636630373235303733 -37303234333764303863323733356363353864323038393762373339303239656566363966363832 -66373965333362383031636330613637656436653365303930316233396431323338636239333930 -62353637333562343534663964636235623363313463613135373639653536636339376435313662 -31616465643730336562373463353965616263366137316430303666346532633735333661616337 -31303361633437663065326666633437383534366639396661623336643565383336623232666563 -64376136303637393330363263666631336461396163373932346138653362326163666331336434 -66356232666439633132363537626436613064663562333536303238313834303666363830353632 -33633633373738383165363135323232313735393335646238326261643765613061623264343261 -37663531396165653838346431666534636365373563366636383165376137396537353031653261 -62393437633735366433613166356337303564313538633933633465363331316332656262303439 -62383535383666383535326538356431313365663937313065363138636336666631643737343634 -61396664383539316261386235643430383436396638303737366632306538636133666239616162 -33373066623365353832666139633436343664333434393933646265323132616665623938643061 -32663763316639333036343333373632633431373038326666323036333664333332313731313865 -33363638616661363763313263623230663735383834353835613861373063666538363764333433 -65316333643937366632313335643364333736633936316132313534373862646365323037363963 -63353237373730623533633961323038616532643034643939353034383036326461383566346230 -61363561666265393536323164323462356237373764306632383034646665613130373030383336 -39353235353036323964356139363530636364346666663331643736306361653966636164613830 -65353664643733383737663766613965616334356561376538623763613331343334613532386562 -33623564636233663234313631623734333934303439383662336665623936643039623039303962 -63336433333766376130356339303239626536383364373063663435653236386139613366323938 -34653161313061333837373164653238663539326562633237616134623366643039373033353331 -31633436323463623661656636383964643362613130336537653731646239313365633662656132 -35393262613830323964346662666664346664316662623865663236643065333533313135643063 -37343461316564316666343465303539383463613663306136653465383336353232366238316332 -31313837323533313863333565643762653362633231333436396238666562363139626336666532 -38326331653736393031636264316330643532353862366235613334383266613238326431343066 -33666566613061363465376637636636626432383132636330343736653034643965396566373064 -37303830393531383131646161613335666631633535383739626130366464313930343262393533 -66373137303531623663643339643233373737303831323735623561656165383034373131386639 -32333838663264366661616262373439323433386538373261373465343163326438623962333566 -30376463646631663330366364626133386131313261396434656336636566323234633864323731 -61393864666537656338646462353764356131303836333762333932643130346439356432323231 -65373866393263313236353937363031366661666664346362313466356366663336383761623637 -66343938303839663630363562353634353938643638373335636530323430656336303963666662 -63643031396133646531316162323836616461373863643463663132383764363034663562363365 -37383234663939646539643562376532323936616264326533333637323633373330643162623537 -63626138616165343938643237313562616234303665303635653830343735326661666235643333 -36623333343365613161663663356566656533363736313231336536386339326336373730643630 -61386139313064376161303066323162316564313761303734376663653464323138303339323763 -35373930366262373936356464613731396136613634663562373737663739613562373333386265 -35323161316633373430633833663266633030613430613635356135393161653836366464326236 -63373738366661333964623962363035636131656539613139373138643839643138373564333430 -34623932396134343762393334373033663132306636633864643235373861386633366639316566 -66623439613730353237366366663161363030646663323836653638383330323164333461666334 -62333662643631373361643263646537323239643234346333333261623933626166333630356662 -61333362313230343134343331303031303634383335313339303363383930373136333336313062 -39626532646365663734313462343766333962333266363965373630363831626633623830633733 -37633666663361343639366663313237633161353062396337393138613261383935353730353932 -32383066386636346433306662326130633662363135313331383530346632356333363238383266 -61303138636664303637326137376638333266343132356237383432613238333632356163643232 -32333966383164343064643934333765386136323736303138326635663561343964393934316433 -37663763653034363039636436346630303664346237616433376462303463633930383430646430 -38343762663635373033373135326361393935386566363031366630366561663431333461663332 -62393538306163386139656431353235343265373033663530306163316464373432353661343439 -35356634323966646263613937663234613431316333656330343765316163626339376430343064 -30363335633337623139343464656431353738623230633137633130353430373435303963663333 -66646563626336393538613134636162643864316163626361663663313131343063643336396532 -36333336386130653537653637653163343031376436383763386163376336623837336365616530 -66386262363633353465663664326537343037383838333863366662306131303364626538623763 -30376132613861636539393138326131356333613837336365383862383362303233633333396238 -34663163363234363564336338656366313931353262303032343334303864386337356437633361 -39383366386334303039313439376236663533633035326662626466333539626261643466653966 -66383862663635346539396139323138613264373436653338653136366135613962373562373932 -62316134636636373264623365613636373132396433373630643531633536333130646130373963 -33356538316334336236346532356263363933633933376165666563313161343638656365373137 -61656438313063363661316537336538646233326564633934303766383738343130373737653961 -32326462626435343363326366663364376431613837646365383933396437383138333665383832 -33376563363466333637656233643137333636326535653335393931653063623937373836313863 -30363238356466363461313633306565646336633366346461333032643635366336646365643761 -37363433623037333838663731376663306134363665383835333164643131646632393639363538 -32656539643461653861376436396130316166616265336433356663343235333763636564326464 -32663530333030306130366532636439353264303137613366353266363664346433666565303135 -34343835623936643631643166636339306231323138356565373663636665303264653161613539 -65336230633035336464373431663130383131376430306136333634376435393933623238653136 -61633138653339336631313932353264366334616630363464373131376538303630663566653061 -30386665383864383838613835613337633937616437376363626263376634643061393937653430 -38626539383634616536396665396262633666376438363930383635353936623534353131666333 -31616536383662663666333265623032613936383063303636366362346337333337633739393665 -62306639376466636161333233656239313964373239336134646434613166646534656466356431 -36626162383866643633323938646130366261373366353764646234653236323166363463366435 -65636137643639353039616138383265633832373864643438366333393031326363333461376138 -65303838626435656634626236663338316632613664646463306561663532353062643637663866 -32663236633630383365663235313337373837396531326532333632343736343035643733333262 -61633532323330623338393838626333323932376630316239303335313765343039313332316336 -32303638356539333136353163396536313830333264613266303139376465613166363730343635 -37623532356135613831363137356238373863653933366532346532363130386363616133373764 -62353066376631343934356337326465613436363361646431373962656639393566323731316136 -30313730386438663261636562643061333664303636373732663464356530336333383739313566 -35616566663065313732656532346330636335383664383061323131306530623632353132326432 -34313365396430396338363063376534363332306432313531303261353736353138383733346363 -37373063666630633632613039353763626165613038616464363333383662356363373133623261 -61316130393165633434623334616239666137653831636131393732363330363039306666346363 -38353832356138383732306533646133663865663062663862663232323038383532616230386265 -38626535313931386533613936343637373064666663336436326464313834333334366264336130 -37346438383739313735636139343638373063306162313230366364316136333137653939313032 -63303035663465653062613764333434323234323332316365333165383533396430316537623164 -37356138623233656436663738393966346662646339626137663533616234653535663663383738 -37323564653664653530653533396332333664636261306339663830373730613832363565613363 -63333635646339656535366365633638393164663562633562383162373932306233313164386331 -37613036323664316237336130366138333863316563666337333464383932353363306231336338 -65393537636162316637343262313333386532616632376563333039386131336230316266396265 -34626630633038623462346365316438316238663163346136663436393736623464643961303339 -35613565393633373861626239663563336633643565383833373632353737393838666161633462 -36316634313530663032366430656131373838616161613265633936633930646137646661653738 -31356266666162643736363538646464363164633434643161643239333336326262386135633165 -65656334313632336138393364356664386237313936626333616638663666396530623464613665 -36636537396565343864353465633638613439663663633766356331333036313130383432366136 -66383333643938613831386631386537353166653561376161633431306531343033646233343731 -34363164316463633133616230316433373966343031353733343762643032356264646631323236 -33393231333330303339613634643333346134643035653330343230663035343230626465386339 -36323030633037363462343130343333313538393163393263633339323133656239366161323837 -30383231323061323538393063316666303534663664623464383634613834326237363532353165 -30356561643738373537343831663461623839303363366266356663666664623030333936656436 -39616261626464343039623161343234626566623537396264366439666438343739306361383666 -33326433383830313933383030393235306261323462323265623862363861653331643736363336 -36373561363566346232613631653464613034653438343462383432643962626665303634646230 -63656261613463623466393064623534306363663835616138626332356430633165363366343461 -66623133666461313934346365623434653433373734376632616435386636613166633935376337 -61326437333132306637663436663930396461393131643766643362316434346438316630343833 -31343638383339646436363238393634636363303861383266313638623663303732333231316366 -37653861663737656631666535383366643265373135396236393936353264613765656534313430 -39663431623737346462303035613162393939623239326232326330636562326236323830613238 -32303263333864616434316361646162623163306262646131346135613162353833366537666532 -39326430306331663736363365386264653634613530376639313336613939363135656638353632 -31656430306363396366306539626436356436306233303032623265356337303666363965353964 -62373031336438326635386632313365336236303737373832366363333634383835306231623065 -62646236643264373036303465663838366565616530306463613563613463323735646430663566 -34356263353637386163316639636334616663303935666530396333653633343861393335313966 -36383139383835343937393934316330623338646663636466396436353739303464313161376138 -61663033623031653361643735366562393431643236636531323061303033313966633838646237 -3831653036383565396363663066316432636537376430376165 +30616437646339396362313065656236323032383738623765393335333730303638393636326365 +6662656665353264383330363464633864313638613762610a643338323866623932636339616137 +61396536316165313762643866353336343263386664393930386332323234333831383762323439 +3437353835393531350a643032666361666337383562303636373035363533353734656362383165 +32306332343230653033663539353431636339343139633162336362383464363137656234303838 +36633963613036613932613734613364613835656361333564633833643662653337323837663038 +66663265316433646436656235386531303634376464636430386431343765613831333132313666 +33383637313939386666363663356162356463613637643431626636366133386638643065633463 +33663432336439666438383837313464653233663932326638653664313536373232343263656362 +66366234636366326631373466316538653734373537323663646562383730393535383536666538 +65336435313136393135306137323833323361633133356437393630316532316366633530616132 +33326462323266343139616464653132373034316336333161643032653538613666636562316537 +38613031643935383435656365343031343361323364356134626438313433393261383365373832 +62616262326362306561396662363266316239646266356661386362333534646238303438326262 +32393762383366336334333336663834633834666663656666383264356462346138393936343932 +64666131333636316334396562396139623834666432353239366238373266636233636433366539 +64613862313764353064303039393463326663333765386334386662326638376163333738636432 +61623330313937383830616635616461376334333332363235303861373061353234383533373963 +30333966316631616466306533656135313765633033366634663462393330396639333737636132 +62343136393262306661646334393036376233303533386261333130653465633238663566356438 +64363832356163366264386135363466623031316133626333356230393063636535373139333063 +32323736623434346430353532343362643435666439323136383563663066386135643363313765 +34333865323635343731303763646639343435626432316363353764373337653266383462633030 +33656261626166633366653861663833663739326361666561373039346436613261663138376566 +35646137363439363438333435366132383037643135353431623061653739363565393562343965 +65353932666361623765343465653735393337636364326263366234303461336662626336336631 +39373231646264646364326533333966333631356636333661386262333032613435356165623839 +38356639646263376632656436373233613063326433336235363434626666326131353961626566 +34373462313265643263346330643535613134366366306539316432653964333332636139396238 +36613065346236633331326230623238373533616531393231396134383462346235386265333836 +31656566633136636465383537616631353666356465623631306336386136366332303638306635 +35666262343964333930653865623461306633633936633066616462366137356262343932376230 +30306233633137376264373938383463373835396532306632616539363132356338313966363664 +64306363373063323561646439626365363938363936306533626139643637663733353766613237 +32396566633337646262383138323365373535663333343537383961316139623437626661643162 +31376138356332373066323130333361663765353839356163353439376131333262643637313265 +31333432616632396632343135633264333230633963313836346565343562383639306430633338 +39636264663633353337323035356461613966343637616238303266303039313463393866663436 +66656631356538646362656366626135393338313761366632616230383866666533643366323439 +36303538643662303134313838343939616536376531666164663133323933323831343266633835 +31613265616136366432396365653164666164373535333532663230383733613538383139396566 +37633066396161383239613362336533646337396632653933363831623032616462383164336436 +64353930326137306437636264336662343735353737313033316132663132643238616430326333 +65336238326638636637656330393937333032303966323036643134656532623136303138313930 +61313733336233633764343330633166353030316632613234613634303136396339396264636630 +37316434666631376366303430636562326465373139363665363436626131376333653835316530 +61366663653239323539396166646163646262623761303939396265333835336634666132396634 +35313235653039373937353164313339343562646330643463626533356439613932613766393337 +37653835313563633337656639316336323138333863396236636334313730343238613033383164 +35643462383635666466613666313836636435616536353939633764636466303065316362643737 +65313561343536393835643461643162356536636536356463616232356365346437333934363132 +66663233353664366665366365313537633864333134386232323139623637613532343262313838 +65343934663033636262653337373231616162366563393238383166646338346233616131336533 +35633262356265356534653533663333313532653336333564363764643939616639366563643432 +38363163663830636264346535666230363633346430393464643731303665313935366263306635 +38623032656262313939343366346233646534616139653533306633326363396262656664326132 +32313636366561366230646536323734383739316332613566346165393562396638393365346232 +33346339376362393766663664616231626135316332346266633039346239636536383237666334 +39646538323262333038643565646134336534303061636661306630643631663737353739623631 +30373630343466616332303361626365613237313136336539303464616662376133386135313362 +62323630303137396434653965386363353138613031663832373664626633656262646361373330 +30376537373034633739313365666537643535306231326235663964373565383432306334383661 +66623166396264383735336230303835303366376338633433653761353531336133363438623931 +65626237663962666239353339363162663937303936393061633239663362323038353863333061 +66616132376135326662663332363661616339363561653631363563323831323136636236326632 +31333235626437363561316533613762336563613762653531316233306337356261616563396233 +63656232643364383465383638353562313865316335353332656633616566303261653833663666 +65303834333561633436633933623234346238336161316631616362386637303665633938396336 +33346337343836376534356534373538343235303236333039316538393866323433366564356565 +65363630623737633162626265643235623461396163663833636564313333313330613064396437 +37303234316466666565613838633663653839353966303163376330323964303238393038383732 +30306336306465663766313933643132393636623066623466643130303465353632626538623165 +36323839313333376134616131653433316336663363343562336635623337396435663636343633 +37306334393439633835646635316437346464393532383535383336356435346266373534393732 +31343335626665326430383131333132303239313562356435333633623733323366313833343431 +30353466636531653464626462633637306131393037653837383833383664363762363435616436 +37373762666662663763613661343935333834316332643439376164316564386564336664363461 +32613561306630356437306536666566353533376466613137323830336361633762636433316235 +66663166623630653335633963366238343166386361326537666465626238346466326230633961 +61633565326565646636393937393034333835643663636464316362366431353166646263623330 +38666431646232373163373361353135323862376366666362313235336431333734323264613265 +34313330303262323432646634666565343237333431363766376131653138343233303332643636 +65316232613734663239646131336230343663353136393262646135663734616134366664373361 +63303932393239326330396637353361333337336165663032356533646637363438323633306430 +32666266623561343132323730353839316339653464613965376637333132333463303238393838 +64313332626633383262633362343465326334343739316462383762633334643465386334646634 +36623963386361633139623533333635663663343865323630356566316639353733393137623339 +33353363613766653163333633353661616362376130656633323264396162646262376566363437 +37616261363234316230656362653562643536366239656261616630376431316532373561653231 +61333438353031353639346532393733616561393536363732323039363034353064353138313836 +34396564383132373138396466363864323538326538336432313961373663323264343132316537 +36656435623232623139626139313032333532356561346531303535303235626466383036353832 +35333931633161386662386564396662356432366164663163353539303638653762393761353063 +33326666306561363731353435303132326335366334646336353530313264663438346536633030 +63373136313430306439613666626239316338646537666236363539656338663238373337323464 +32373565666165623062613338616331326232653966373962636639626166396639643961643539 +39643732623165653464313861656537373333323566306238356462343462626663616330333830 +37376337646635346634663461303363643537363633356339393330323565373266336461386361 +31393632386635653137623434663032336631633065323235656232326639326539313463366230 +63653235333865333266306162643938643162363766643464653665646131363463326433643662 +35346138393433646162366164316331366462346332326137343833313762306635396661346231 +32623230373433646237303635343837343063656639313236306563376164386239346262643930 +66386465396262353434633937303732653631373766663139306236616130383833383062303338 +31613434393138333437646432373531623666363763323564663730393565343930323233643334 +66373665386238623066366166616163663362376463646331373838343863643831376165333765 +32653561343363666161643762383831356639663737393132616566613230623162616439326562 +34313836656136323565343636396530663865366332326436303162386535393736656330326534 +61356538643337666432656232613962393139656336373462353636636233363534643137393031 +64343965336637663064653563346232323439653437366238353039663932346266626338343038 +61353064653730636232666661653830373661303162666539646231373230336362653136643137 +65623662303534316530313739333233623865303866623630663136663761666134666663393437 +36653331393566353032313036363533383038363937376535626435383939303666316531363266 +30353235336333376337393531326661303937353266346164663438633062613166656431663834 +33663539343433623564306638366131303032333033656466646134353036666133346236373639 +39663136666331613231646639306565306561656439366566653666396133363561643663396632 +34316461326562663161633630316531613363616264303036616336313939336465366163366262 +31646539363239346637396561306466353338346461626237333633313131316534323661356663 +30366434356137333763643937363164393833376261616131336639366231383261333262613637 +65646262336439626262353934616530343730353436313232393663363237393034316433663038 +31643261663534303639313433626339303562343036303335363539396234333465326538383062 +32613266396338646135626462383534323763323835663863646337646139306265633865613933 +61623866343636653638373032303732616530326265613366616535376435616266326366386435 +39366139613638333237646164326661323362663564313831386437343838383937326362656538 +37313032613434646238383064613266623138343166613138353532663231633235653363396162 +39623733393161343932663361343462393231666638666166663838373161656631303762346461 +36346263386631663839386261636465326337616331646631656366346338306436313066353735 +31323431636538613264613536396534363565333939376466353031663939663362646638363261 +38323139346630633031316532663538613330626365333234313361356266303261376366643534 +66643166363536313066343064346635616465303266336134393065386262613932653434393831 +31306165383039643533613439646263316338306562653332323739646335336433646535616362 +35393634623533343064373137313431616439316432646635343333636432633634623861393032 +64363135373166326533326235363030363732616263313263336566653634393765306533303834 +64363035363232663332303262393561626663653430643061326162616535353439316665663561 +63643939613037303531323231373330333461636438373561313866353865373733373461366333 +35373534306636396563623433663663353938663766656436346430666635326632356235366664 +33396237346532613436306531616365646231626138393632356265376337343062616337313665 +35616166323266393735323964613536303936323763646135383361346536343931323938616464 +32623363623262306135363732333432616131663537313765343961393464656235666539336561 +61616631373032393538306138356433303461383235653165363762383161633961386237363338 +38356235656563393163343062333365643366633962646432613530346433653838313762623938 +31313563323062323564363035386637653730393133393634383734646533323866396535366331 +65373539323964383434656138346332633261303861653034336534393266313639663737636362 +30616165326230633830633264666366323738396433663132316133326166633139613735653866 +36316330636331663338313431656131363863613438353231366166633034343531303638373431 +38303061616161353961626364666539303334373965396461346463623065646164656638363130 +65646563636563643733306165633038623639333761363239646164616531316137393330323632 +64303238626564626130343331663730643361353765633835366338303031363265363362303534 +37333963623737323830383764656165613765383036626638323238613464613431363965316236 +63366364336665653731613637346639376139643539363038396438333463396636346436366564 +31393464316533363333343763343337366365333033343331623265616336383064666438373662 +37656239363366633038626366343466353935633832633663356261323237306261313432353830 +64353734336535636331636634326332373637643238313465306565626532633665333766633933 +37343831323830393838306436306333363138633634393232363730353561626437323061656536 +34633535363365303865383961623739393461376233643163643930376163363334393266663839 +65353039666339613438396563356637343163383738613238323830643534666166663538633961 +36353064393834643035366433613866386531656536353362656435623562386536386662613935 +65663464646165376134666163386662363038393132333034316566643366663562643730326663 +32626538636538393333323732353339306138393031623336333236323335626365366235393136 +66333763303333396233336561376233633431376234303563303637303662303533313166663463 +35316464316335333865336432646663376464303235613161333661626363386631313933326261 +64653133653763396131383737353734646536343361316466303162323161323763663031356633 +66643737616565303337663866346532363861353938636234333337333737623534663437383866 +62613364653938626239333165356461333836323764323862393738393237353331306138336231 +30613366613430363938306439373766616262373838636331383932303439356333623737633634 +66343366306631336631613730646639336236383136656562316262356364356534633731333134 +35313433353539353033343862623161653330313138336339326262613531643630623766363334 +38666563326537613934633132383033363532343834613066356537396661323966623738343132 +63333263333962336665633738643461376635336632393461383234313261616137333966636231 +38393933383464353835356330373139313464643733656562356632346465633035326362373664 +64373130353033306161376339636531336332663739623063383965623335666236646133663762 +34383263366632363832623666656661653261396463383239323437333161656639313562666238 +39356361316564393963333039646161656164643264653132653963666362376531626663663362 +37656231356132616533646132323064333734646433366330333132373666636461316333656237 +30363936383930313962303332303937326239366139303563303639663938613061303339373836 +38616331303830363862313639636638393630323161373962343064366664636532663439353935 +36633035326165663237623036303839383037636532386164353865633430656135646134643738 +34613236373361326634323633333332633535353030343261393838313635343232363764626233 +39396437643338313733313434663835336532393062313563653630373931636361656536343637 +38356364333432363831623934353161623431353863626438306339393062626338633833633233 +39633639383731646431613536363837653862333965326337626230656631623336316435616639 +38363063353965383361313464623961616139643166626531646138373261356462633465363962 +61356235386363306264376334643031613638366366363438623230303738656162393131393765 +36303138316462643465346632326632653131633234643466656266306330613333656534626233 +35326262386466363532373763323331323262383635636435346539633033383966376530366666 +37333766313235333131643135386130313035636536313266323865653265373432323464373061 +66633639363935663432626132623534346339306662366530386136333366346130306639343161 +31316161393161303039633130653862326136643563336461666366376535666239306531383566 +62363238636634616433646365313461336530396263653764633330333232386134663038306166 +62633937306666633665356237616236383263343165646164343030646137373330623934383435 +39656633303565376665316639326230336339386364626436383433343366613565303761636237 +65373165383864663530343037616632363433663765623538643636386464623363366461306330 +61666137316663323931373532306366643432626530323333626532373061333436663362396638 +35636638386162343632653439313930346261613863646431623932316262656230303062336463 +62646433653231363138623232643539376636663738616163653666633132396461666334633332 +64616462303864313030383037663065653665323832383037646133663235613961383738633561 +64333265656332653662323434613534313934616434353338656439646131343133353632383863 +35663637303666633530396263356131333564323239663966333139313336613435333839343762 +31623966306138636463626534313839353439623061363431366636353362633666626465323730 +32373937343361623031643466383333636266393166383032393939366331316466336662343161 +32613763363965636465363261366432376633346234613730663431373765323736316665313131 +36323431323832616335643134653665643434336166653361343533323765323032323232343835 +63356265373139386430623564613334666264633665643938643433383237313264643638326462 +35656134353561666162643633363761326135643665643030396636636533373031643664306133 +64663162313563656435366365343965323363633264326563376663353963356261636661373561 +66653963353935386162336332636365313631383066306431633334326538306634636337653838 +61653762313034616333326231623666653363343564636433356138323736356433393133643831 +38363634363536343231653739333539336362663866643735343332623631336665333834636331 +35363638663632356131333335363438313437656133336161393231616636353536356338376534 +39613635366562656564666132386238363566623033356536333265356533333136623764663336 +61313030653163613063666566306432333338386633343565316365663238353161336364393834 +36366166393863633636323433663138666266393732386333306336633163393962383137656339 +61386663653164383436356564323561346262633163613339643834623633666266353566386333 +30626261656139376335303461623761656438643062393732333463343261633132623233633439 +30353830393931386336396130366334623862336234346332313636633832313933303465363534 +38653334376231326137633836643636383163613736623061326433636361643965316539383832 +66666361663132663932376438653734653135323464323036393933656635353433336636326666 +66643939373730346362613361326638393162393137386566623334356265653430343635306636 +64353938356633336134333938326630653133666363326530643161363734393830643637303766 +39393261353235326133623066626331316537373061363464663438386231613532623234326237 +62376463623033613165386639626262333133386438643463613930333332613066386437316134 +37646664303264386231376261393963323634666431343637316364616130343339396539666534 +65643937386239653561376632316239376236323366386134303339316566316230323936633635 +32313432323532363466343238653930363439643332626665333433643863366366316463336265 +32643433636139373333653833373439626239316434333132376664373534623939306436613665 +61306265313338353365303262663231306264343866626137613864363536623031666133633766 +36663839353132643337303737323665643163356134633132343765313231613835333938623761 +64336637343762356562653335376132626431373237623964633036376261363532643463316262 +63353530373634303262653132656633396464383134343433376463303364356639646134636436 +32353162623536363432633136393732613534656531376465336130646364643939323763346361 +64333639393563636364396236663530343738643363623365323438363236623466663132663166 +64306262623836666536623434363439393537323632653162623934346438646539643638633933 +64376136393062643039306535336133346563326466636666626565313366333331343636326532 +34653564336637346637353536656166353130333635373836643337316331343630353936396438 +39323065363565626136376235313936366630663638343263623564643165613765343838343436 +64323635326362313961616634393036643932623133393632346539633036383061396463343461 +37636432323734643632376531373336383532393531633762626633613466383165393235336433 +32303263353831336364313066353761353134623831616135666438383662323639626337626635 +62326637643132393761626364623139666330633264363233613864313965333830303335663761 +63393733343035623137383563663433626433356130336635313034636533633037653238316563 +31643033626662363030613838373963376138653232383062646330356136366462313866633139 +63316139336335316139623231326539326439663933336335323137343866623161