Compare commits
33
Commits
aeddc1f168
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5217607b1
|
||
|
|
856bf72560
|
||
|
|
70372ee028
|
||
|
|
16f0712a31
|
||
|
|
3bb85b85f6
|
||
|
|
7e64d76d02
|
||
|
|
6bb0d61183
|
||
|
|
304263e7b1
|
||
|
|
25886d1be0
|
||
|
|
b9be642c5c
|
||
|
|
9c75a52529
|
||
|
|
a909442d8f
|
||
|
|
6ff11061d9
|
||
|
|
a9d9eb8d83
|
||
|
|
daa4379dbe
|
||
|
|
b5b43a484e
|
||
|
|
bdc319df64
|
||
|
|
321f6e7b6b
|
||
|
|
889c8a565e
|
||
|
|
af14732652
|
||
|
|
3fd82b71b2
|
||
|
|
d4824bac6f
|
||
|
|
cbdf2e7b2f
|
||
|
|
a9d0498c12
|
||
|
|
37774f4542
|
||
|
|
4484c9d9c2
|
||
|
|
cf75db3482
|
||
|
|
07a82ca4b0
|
||
|
|
b79b1269c4
|
||
|
|
ef20e39315
|
||
|
|
626bac5303
|
||
|
|
50c975d49b
|
||
|
|
dd4a3b8456
|
@@ -15,7 +15,7 @@ Ansible-проект для автоматизации личного серве
|
|||||||
- `templates/` — общие шаблоны (например `env.template`).
|
- `templates/` — общие шаблоны (например `env.template`).
|
||||||
- `scripts/` — вспомогательные Python-скрипты (SMTP-утилиты для Yandex Cloud Postbox).
|
- `scripts/` — вспомогательные Python-скрипты (SMTP-утилиты для Yandex Cloud Postbox).
|
||||||
- `.gitea/workflows/lint.yml` — CI: yamllint + ansible-lint.
|
- `.gitea/workflows/lint.yml` — CI: yamllint + ansible-lint.
|
||||||
- `lefthook.yml` — pre-commit хуки (ruff, mypy, yamllint, ansible-lint, gitleaks, проверка vault).
|
- `lefthook.yml` — pre-commit хуки (ruff, pyrefly, yamllint, ansible-lint, gitleaks, проверка vault).
|
||||||
- `tasks.py` — задачи через invoke (`inv <task>`).
|
- `tasks.py` — задачи через invoke (`inv <task>`).
|
||||||
- `pyproject.toml` — зависимости Python, управляются через `uv`.
|
- `pyproject.toml` — зависимости Python, управляются через `uv`.
|
||||||
|
|
||||||
@@ -71,6 +71,7 @@ uv run ansible-galaxy install --role-file requirements.yml
|
|||||||
- `playbook-netdata.yml` — мониторинг.
|
- `playbook-netdata.yml` — мониторинг.
|
||||||
- `playbook-dozzle.yml` — просмотр Docker-логов.
|
- `playbook-dozzle.yml` — просмотр Docker-логов.
|
||||||
- `playbook-goaccess.yml` — аналитика веб-логов Caddy в реальном времени.
|
- `playbook-goaccess.yml` — аналитика веб-логов Caddy в реальном времени.
|
||||||
|
- `playbook-goatcounter.yml` — веб-аналитика посещений (GoatCounter, SQLite), собственный логин. Один инстанс на несколько сайтов, сайт выбирается по `Host`; список доменов — `goatcounter_sites` в `group_vars/all/main.yml`.
|
||||||
- `playbook-gramps.yml` — генеалогия.
|
- `playbook-gramps.yml` — генеалогия.
|
||||||
- `playbook-calibre.yml` — управление электронными книгами.
|
- `playbook-calibre.yml` — управление электронными книгами.
|
||||||
- `playbook-transcriber.yml` — транскрибация (образ собирается локально ролью `app_image`).
|
- `playbook-transcriber.yml` — транскрибация (образ собирается локально ролью `app_image`).
|
||||||
@@ -99,6 +100,8 @@ uv run ansible-galaxy install --role-file requirements.yml
|
|||||||
|
|
||||||
- Суффиксы шаблонов: `.template.yml`, `.template.sh`, `.template.cfg`, `.template.conf`, `.template.toml`, `.template` (для файлов без естественного расширения) — рендерятся Ansible модулем `template`. Расширение оригинального формата сохраняется после `.template.` ради подсветки синтаксиса в редакторе.
|
- Суффиксы шаблонов: `.template.yml`, `.template.sh`, `.template.cfg`, `.template.conf`, `.template.toml`, `.template` (для файлов без естественного расширения) — рендерятся Ansible модулем `template`. Расширение оригинального формата сохраняется после `.template.` ради подсветки синтаксиса в редакторе.
|
||||||
- Большинство приложений определяют переменные inline в плейбуке. Отдельные файлы переменных только у homepage и transcriber (`vars/homepage.yml`, `vars/transcriber.yml`).
|
- Большинство приложений определяют переменные inline в плейбуке. Отдельные файлы переменных только у homepage и transcriber (`vars/homepage.yml`, `vars/transcriber.yml`).
|
||||||
|
- `vars_files` в precedence выше `group_vars`: одноимённая переменная в `vars/<app>.yml` молча перебьёт значение из `group_vars`, в том числе из vault.
|
||||||
|
- Переменную, которую читает больше одного плейбука, кладём в `group_vars/all/main.yml`, а не в `vars/<app>.yml` — так её видят все. Пример: `goatcounter_sites` (плейбук goatcounter заводит по нему сайты, caddyproxy рендерит из него имена в Caddyfile).
|
||||||
- Общие переменные из `group_vars/all/` (`main.yml` + vault-файл `secrets.yml`): `application_dir`, `bin_prefix`, `primary_user` и др. Загружаются автоматически для группы `all`, поэтому плейбуки их не перечисляют. Хост-специфичные значения переопределяются в инвентаре — теперь они имеют приоритет над `group_vars`.
|
- Общие переменные из `group_vars/all/` (`main.yml` + vault-файл `secrets.yml`): `application_dir`, `bin_prefix`, `primary_user` и др. Загружаются автоматически для группы `all`, поэтому плейбуки их не перечисляют. Хост-специфичные значения переопределяются в инвентаре — теперь они имеют приоритет над `group_vars`.
|
||||||
- Каждое приложение: `app_name`, `app_user`, `app_owner_uid`, `app_owner_gid`, `base_dir`, `data_dir`.
|
- Каждое приложение: `app_name`, `app_user`, `app_owner_uid`, `app_owner_gid`, `base_dir`, `data_dir`.
|
||||||
- UID/GID сервисов: новое соглашение — диапазон `11xx`, причём `app_owner_uid == app_owner_gid` (одно число на сервис). Новому приложению берём следующий свободный номер по возрастанию. Старые сервисы ещё сидят на легаси-нумерации `10xx` (часто с разными uid/gid) — их не трогаем, но новые заводим только в `11xx`.
|
- UID/GID сервисов: новое соглашение — диапазон `11xx`, причём `app_owner_uid == app_owner_gid` (одно число на сервис). Новому приложению берём следующий свободный номер по возрастанию. Старые сервисы ещё сидят на легаси-нумерации `10xx` (часто с разными uid/gid) — их не трогаем, но новые заводим только в `11xx`.
|
||||||
@@ -106,10 +109,13 @@ uv run ansible-galaxy install --role-file requirements.yml
|
|||||||
## Линтинг и CI
|
## Линтинг и CI
|
||||||
|
|
||||||
- CI (`.gitea/workflows/lint.yml`): два параллельных job — yamllint и ansible-lint.
|
- CI (`.gitea/workflows/lint.yml`): два параллельных job — yamllint и ansible-lint.
|
||||||
- Конфиги: `.yamllint.yml` (макс. длина строки 120), `.ansible-lint.yml` (профиль production, offline).
|
- Конфиги: `.yamllint.yml` (макс. длина строки 120), `.ansible-lint.yml` (профиль production, offline),
|
||||||
|
`[tool.ruff.lint]` и `[tool.pyrefly]` в `pyproject.toml`.
|
||||||
|
- Набор правил ruff расширен относительно дефолтного (ANN, PTH, ERA, PT, C90, RET, N, Q, TID, G, LOG,
|
||||||
|
FURB, PLC) и синхронизирован с остальными репозиториями — канон настройки лежит в `rp-local-env`.
|
||||||
- Pre-commit хуки через lefthook:
|
- Pre-commit хуки через lefthook:
|
||||||
- `ruff format` + `ruff check` — форматирование и линтинг Python.
|
- `ruff format` + `ruff check --fix` + `ruff check` — форматирование и линтинг Python.
|
||||||
- `mypy` — проверка типов Python.
|
- `pyrefly` — проверка типов Python (заменил mypy).
|
||||||
- `yamllint` — линтинг YAML.
|
- `yamllint` — линтинг YAML.
|
||||||
- `ansible-lint` — линтинг Ansible (профиль production).
|
- `ansible-lint` — линтинг Ansible (профиль production).
|
||||||
- `gitleaks` — поиск секретов в staged-файлах.
|
- `gitleaks` — поиск секретов в staged-файлах.
|
||||||
@@ -146,3 +152,7 @@ ansible-playbook -i production.yml --diff playbook-gitea.yml
|
|||||||
- Шаблоны скриптов бэкапов в `files/<app>/` (backup.template.sh, gobackup.template.yml и др.).
|
- Шаблоны скриптов бэкапов в `files/<app>/` (backup.template.sh, gobackup.template.yml и др.).
|
||||||
- `files/backups/backup-all.py` — оркестратор, запускает все бэкапы через restic.
|
- `files/backups/backup-all.py` — оркестратор, запускает все бэкапы через restic.
|
||||||
- Cron-расписание настраивается в `playbook-backups.yml`.
|
- Cron-расписание настраивается в `playbook-backups.yml`.
|
||||||
|
- Уведомление включает список всех найденных приложений со значком статуса (✅ забекаплено,
|
||||||
|
❌ упал скрипт дампа, ⏭ бекапить нечего) и занятым местом, а в конце — свободное место
|
||||||
|
на дисках. Размеры считает `dust` (ставится ролью eget); если его нет, прогон продолжается
|
||||||
|
без размеров.
|
||||||
|
|||||||
@@ -17,24 +17,20 @@ _(sec-ревью)_ — три независимых security-ревью на м
|
|||||||
|
|
||||||
## Высокий
|
## Высокий
|
||||||
|
|
||||||
- [Закрыть admin API Caddy (localhost вместо всей сети)](caddy-admin-api-localhost.md) — `admin :2019` слушает всю docker-сеть; любой контейнер может переписать маршрутизацию и снять forward_auth _(sec-ревью)_
|
|
||||||
- [Убрать группу `docker` у сервисных пользователей](service-users-docker-group.md) — `docker` = root на хосте; ~все app-аккаунты фактически root, изоляция «юзер на сервис» обнуляется _(sec-ревью)_
|
|
||||||
- [Изолировать/запаролить сессионный redis Authelia](authelia-redis-isolation.md) — стор сессий SSO в общей сети без пароля; запись в него = hijack SSO всех сервисов _(sec-ревью)_
|
- [Изолировать/запаролить сессионный redis Authelia](authelia-redis-isolation.md) — стор сессий SSO в общей сети без пароля; запись в него = hijack SSO всех сервисов _(sec-ревью)_
|
||||||
- [Алерты на проблемные контейнеры](container-alerts.md) — wakapi крутился в restart-loop несколько дней незамеченным; healthcheck в compose + алерты Netdata
|
- [Алерты на проблемные контейнеры](container-alerts.md) — wakapi крутился в restart-loop несколько дней незамеченным; healthcheck в compose + алерты Netdata
|
||||||
- [Никаких секретов в выводе плейбуков](no-secrets-in-playbook-output.md) — `inv pl` всегда с `--diff`, и каждая задача, рендерящая файл с секретом, печатает его в терминал; `no_log` стоит ровно в одном месте из четырнадцати
|
- [Никаких секретов в выводе плейбуков](no-secrets-in-playbook-output.md) — `inv pl` всегда с `--diff`, и каждая задача, рендерящая файл с секретом, печатает его в терминал; `no_log` стоит ровно в одном месте из четырнадцати
|
||||||
|
|
||||||
## Средний
|
## Средний
|
||||||
|
|
||||||
|
- [Дампы бэкапов без группы `docker`](backup-dump-without-docker-group.md) — у miniflux/gitea/outline группа осталась: их backup.sh дампит через `docker compose exec` из-под самого приложения
|
||||||
- [Захардить мониторинг-стек (netdata / dozzle / goaccess)](harden-monitoring-stack.md) — netdata/dozzle: лишние привилегии + docker.sock + доверие Remote-User в общей сети; socket-proxy + monitoring_network _(sec-ревью)_
|
- [Захардить мониторинг-стек (netdata / dozzle / goaccess)](harden-monitoring-stack.md) — netdata/dozzle: лишние привилегии + docker.sock + доверие Remote-User в общей сети; socket-proxy + monitoring_network _(sec-ревью)_
|
||||||
- [Ограничить эндпоинт `/metrics` Miniflux](miniflux-metrics-restrict.md) — `METRICS_ALLOWED_NETWORKS=0.0.0.0/0` + прокси без ограничения пути отдаёт метрики публично _(sec-ревью)_
|
|
||||||
- [Закрыть calibre-web forward-auth и проверить дефолтные креды](calibre-forward-auth.md) — домен без forward_auth, защита на встроенной авторизации с дефолтом `admin/admin123` _(sec-ревью)_
|
- [Закрыть calibre-web forward-auth и проверить дефолтные креды](calibre-forward-auth.md) — домен без forward_auth, защита на встроенной авторизации с дефолтом `admin/admin123` _(sec-ревью)_
|
||||||
- [Понизить log level Authelia с debug до info](authelia-log-level.md) — `debug` в проде утекает детали аутентификации и OIDC через `docker logs`/dozzle _(sec-ревью)_
|
|
||||||
- [Включить автообновления безопасности](security-autoupdates.md) — патчи openssh/libssl только вручную; окна в недели между CVE и фиксом _(sec-ревью)_
|
- [Включить автообновления безопасности](security-autoupdates.md) — патчи openssh/libssl только вручную; окна в недели между CVE и фиксом _(sec-ревью)_
|
||||||
- [Запретить SSH-вход под root](ssh-disable-root-login.md) — `PermitRootLogin yes` пускает root по ключу; наименьшие привилегии + defense-in-depth (пароль уже off) _(sec-ревью)_
|
- [Запретить SSH-вход под root](ssh-disable-root-login.md) — `PermitRootLogin yes` пускает root по ключу; наименьшие привилегии + defense-in-depth (пароль уже off) _(sec-ревью)_
|
||||||
- [Gitea runner on-demand в Yandex Cloud](gitea-runner-on-demand.md) — раннер активен только во время сборки; webhook→Cloud Function стартует ВМ, probe/decide гасят по idle; экономия ~95%
|
- [Gitea runner on-demand в Yandex Cloud](gitea-runner-on-demand.md) — раннер активен только во время сборки; webhook→Cloud Function стартует ВМ, probe/decide гасят по idle; экономия ~95%
|
||||||
- [Синхронизация общих Ansible-ролей между репозиториями](shared-roles-sync.md) — `owner`/`eget`/`secrets` дублируются между репозиториями и дрейфуют; канон в ansible-shared + rsync-таски invoke
|
- [Синхронизация общих Ansible-ролей между репозиториями](shared-roles-sync.md) — `owner`/`eget`/`secrets` дублируются между репозиториями и дрейфуют; канон в ansible-shared + rsync-таски invoke
|
||||||
- [Уведомления о размере приложений и заполнении диска](disk-usage-alerts.md) — алерты на заполнение диска и аномальный рост data-томов; сервер ресурсно ограничен _(tududi)_
|
- [Уведомления о размере приложений и заполнении диска](disk-usage-alerts.md) — алерты на заполнение диска и аномальный рост data-томов; сервер ресурсно ограничен _(tududi)_
|
||||||
- [Handlers рестарта для оставшихся приложений](handlers-remaining-apps.md) — apprise/dashboard/transcriber/miniflux монтируют конфиг в контейнер, но рестарта нет вовсе; правка конфига не подхватывается
|
|
||||||
- [Composable-роль `backup`](ansible-backup-role.md) — бэкап — самый чистый шов для extraction (одинаков у всех, различается только список targets) _(ансибл-ревью)_
|
- [Composable-роль `backup`](ansible-backup-role.md) — бэкап — самый чистый шов для extraction (одинаков у всех, различается только список targets) _(ансибл-ревью)_
|
||||||
- [Вывести из оборота секреты с широкими правами](narrow-secret-scopes.md) — `yc_oauth_token` (весь аккаунт Yandex Cloud) уже выведен; осталось проставить радиус остальным переменным vault и сузить широкие
|
- [Вывести из оборота секреты с широкими правами](narrow-secret-scopes.md) — `yc_oauth_token` (весь аккаунт Yandex Cloud) уже выведен; осталось проставить радиус остальным переменным vault и сузить широкие
|
||||||
- [outline: секреты из файлов](secrets-file-outline.md) — умеет generic `<NAME>_FILE` для всех пяти секретов; единственное приложение, где выносится вообще всё, включая пароль БД
|
- [outline: секреты из файлов](secrets-file-outline.md) — умеет generic `<NAME>_FILE` для всех пяти секретов; единственное приложение, где выносится вообще всё, включая пароль БД
|
||||||
@@ -44,10 +40,8 @@ _(sec-ревью)_ — три независимых security-ревью на м
|
|||||||
|
|
||||||
## Низкий
|
## Низкий
|
||||||
|
|
||||||
- [Мелкий хардненинг Caddy](caddy-hardening-nits.md) — HSTS-заголовок, убрать лишний `NET_ADMIN`, убрать leftover-листенер `:29999` _(sec-ревью)_
|
|
||||||
- [Мелкий хардненинг контейнеров](container-hardening-nits.md) — `no-new-privileges`, non-root `user:`, homepage off `:latest`, соглашение о `127.0.0.1:` портах _(sec-ревью)_
|
- [Мелкий хардненинг контейнеров](container-hardening-nits.md) — `no-new-privileges`, non-root `user:`, homepage off `:latest`, соглашение о `127.0.0.1:` портах _(sec-ревью)_
|
||||||
- [Унифицировать ACL техпанелей Authelia](authelia-acl-unify.md) — rssbridge (SSRF) и dashboard за `one_factor` без subject; свести к `group:admins` + `two_factor` _(sec-ревью)_
|
- [Унифицировать ACL техпанелей Authelia](authelia-acl-unify.md) — rssbridge (SSRF) и dashboard за `one_factor` без subject; свести к `group:admins` + `two_factor` _(sec-ревью)_
|
||||||
- [Причесать роль `owner` под конвенции](ansible-owner-role-cleanup.md) — `assert` вместо `fail`+`when`, `loop` вместо `with_*`, добавить `meta`/README _(ансибл-ревью)_
|
|
||||||
- [Инвентарь: `host_vars`, именованные группы, точечный `become`](ansible-inventory-hostvars.md) — хост-специфику в `host_vars/server.yml`, хост в именованную группу, глобальный root → точечный become _(ансибл-ревью)_
|
- [Инвентарь: `host_vars`, именованные группы, точечный `become`](ansible-inventory-hostvars.md) — хост-специфику в `host_vars/server.yml`, хост в именованную группу, глобальный root → точечный become _(ансибл-ревью)_
|
||||||
- [Фоновая зачистка стиля и конфигурации Ansible](ansible-style-nits.md) — sudoers.d, профиль ansible-lint, `ansible.cfg`, кавычки, `cache_valid_time` _(ансибл-ревью)_
|
- [Фоновая зачистка стиля и конфигурации Ansible](ansible-style-nits.md) — sudoers.d, профиль ansible-lint, `ansible.cfg`, кавычки, `cache_valid_time` _(ансибл-ревью)_
|
||||||
- [Logrotate для логов бэкапов](backup-logs-logrotate.md) — логи `backup-all.py`/restic копятся без ротации _(tududi)_
|
- [Logrotate для логов бэкапов](backup-logs-logrotate.md) — логи `backup-all.py`/restic копятся без ротации _(tududi)_
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# Причесать роль `owner` под конвенции
|
|
||||||
|
|
||||||
**Приоритет:** низкий
|
|
||||||
|
|
||||||
Роль `owner` разошлась по стилю с `eget`/`secrets`:
|
|
||||||
|
|
||||||
- **`roles/owner/tasks/main.yml:2-10`** — валидация аргументов через `fail` +
|
|
||||||
`when`, причём две задачи с **идентичным именем**. `eget` для того же делает
|
|
||||||
`assert` (`roles/eget/tasks/main.yml:15`). Привести к одному стилю — `assert`
|
|
||||||
либо декларативный `meta/argument_specs.yml`.
|
|
||||||
- **`roles/owner/tasks/main.yml:32,53`** — устаревшие `with_items`/`with_dict`;
|
|
||||||
конвенция — `loop` (`loop: "{{ owner_ssh_keys }}"`,
|
|
||||||
`loop: "{{ owner_env_dict | dict2items }}"`).
|
|
||||||
- У `owner` нет `meta/main.yml` и README, тогда как у `eget` и `secrets` есть.
|
|
||||||
- Имена задач с точкой на конце (`"Prepare env variables."`) — ansible-lint в
|
|
||||||
строгом профиле это ловит.
|
|
||||||
|
|
||||||
Контекст: [docs/drafts/ansible-review.md](../drafts/ansible-review.md) §5.
|
|
||||||
|
|
||||||
Связано: roles/owner, roles/eget (образец), roles/secrets.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Понизить log level Authelia с debug до info
|
|
||||||
|
|
||||||
**Приоритет:** средний
|
|
||||||
|
|
||||||
`files/authelia/configuration.template.yml` — `log.level: "debug"`. Debug-логи
|
|
||||||
Authelia содержат детали запросов аутентификации, имена пользователей, заголовки и
|
|
||||||
внутренние подробности OIDC-потоков — они не нужны в обычной эксплуатации и
|
|
||||||
расширяют поверхность утечки через `docker logs`/dozzle (который сам открыт
|
|
||||||
админам). Поставить `info` (или `warn`), debug включать точечно при отладке.
|
|
||||||
|
|
||||||
Из sec-ревью (fable, 2026-07-13).
|
|
||||||
|
|
||||||
Связано: files/authelia/configuration.template.yml.
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Дампы бэкапов без группы `docker` у сервисных пользователей
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Группу `docker` (= root на хосте) убрали у всех сервисных пользователей, кроме
|
||||||
|
трёх: `miniflux`, `gitea`, `outline`. Их `backup.sh` делает `docker compose exec`
|
||||||
|
(pg_dump, `gitea dump`), а `files/backups/backup-all.py` запускает такие скрипты
|
||||||
|
через `su --login <app_user>`, то есть от имени самого приложения — без членства
|
||||||
|
в группе дамп упадёт.
|
||||||
|
|
||||||
|
Дампить снаружи докера нельзя: postgres порт не публикует, `gitea dump` живёт
|
||||||
|
только внутри контейнера.
|
||||||
|
|
||||||
|
Варианты, как развязать:
|
||||||
|
|
||||||
|
- **A. sudo-обёртка на дамп.** Плейбук кладёт `/usr/local/sbin/backup-<app>-dump.sh`
|
||||||
|
(root:root, 0755 — сам app_user его не перепишет) с единственным
|
||||||
|
`docker compose exec …`, в sudoers — `<app_user> ALL=(root) NOPASSWD:` ровно на
|
||||||
|
этот файл, а `backup.sh` дёргает его через `sudo`. Пользователь получает право
|
||||||
|
на одну фиксированную команду вместо права на весь докер. Механизм в проекте
|
||||||
|
уже обкатан: так `playbook-backups.yml` разрешает `primary_user` запускать
|
||||||
|
`backup-all.py`.
|
||||||
|
- **B. root-скрипты в оркестраторе.** Новый контракт в `backup-all.py`: если у
|
||||||
|
приложения есть `backup-root.sh` с владельцем root — запускать его без `su`.
|
||||||
|
Механизм один на всех, но от root пойдёт скрипт целиком, включая
|
||||||
|
`keep-files.py`, и дампы станут root-овыми файлами в директориях приложения.
|
||||||
|
- **C. оставить как есть** — три аккаунта как осознанное исключение,
|
||||||
|
задокументированное в плейбуках.
|
||||||
|
|
||||||
|
Предпочтение на момент записи — A.
|
||||||
|
|
||||||
|
Связано: playbook-miniflux.yml, playbook-gitea.yml, playbook-outline.yml,
|
||||||
|
files/*/backup.template.sh, files/backups/backup-all.py.
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Закрыть admin API Caddy (localhost вместо всей сети)
|
|
||||||
|
|
||||||
**Приоритет:** высокий
|
|
||||||
|
|
||||||
`files/caddyproxy/Caddyfile.template:8` — `admin :2019`: admin API слушает на всех
|
|
||||||
интерфейсах контейнера, а в `web_proxy_network` сидят все приложения. Любой
|
|
||||||
скомпрометированный контейнер (rssbridge, tududi, calibre, ...) может POST-ом в
|
|
||||||
`http://caddyproxy:2019/config/` переписать маршрутизацию, снять `forward_auth`,
|
|
||||||
перехватить трафик или сертификаты. Порт реально нужен только netdata для метрик
|
|
||||||
(`files/netdata/go.d/prometheus.conf`). Фикс: вернуть `admin localhost:2019`, а
|
|
||||||
метрики отдать отдельным site-блоком `:2019 { metrics }` — netdata продолжит
|
|
||||||
скрейпить, admin API станет недоступен из сети.
|
|
||||||
|
|
||||||
Из sec-ревью (fable, 2026-07-13).
|
|
||||||
|
|
||||||
Связано: files/caddyproxy/Caddyfile.template, files/netdata/go.d/prometheus.conf.
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Мелкий хардненинг Caddy
|
|
||||||
|
|
||||||
**Приоритет:** низкий
|
|
||||||
|
|
||||||
- **HSTS**: заголовок `Strict-Transport-Security` не выставляется нигде в
|
|
||||||
`files/caddyproxy/Caddyfile.template`. Одна строка `header
|
|
||||||
Strict-Transport-Security "max-age=31536000"` в общем сниппете (рядом с
|
|
||||||
`access_log`) закрывает downgrade/SSL-strip для всех поддоменов разом.
|
|
||||||
- **Лишний `NET_ADMIN`** (`files/caddyproxy/docker-compose.template.yml`) —
|
|
||||||
reverse-proxy эта capability не нужна (требуется лишь для тюнинга UDP-буферов
|
|
||||||
HTTP/3, работает и без неё с warning). Убрать у edge-контейнера, торчащего в
|
|
||||||
интернет.
|
|
||||||
- **Leftover-листенер `:29999`** в `Caddyfile.template` (site `status.vakhrushev.me,
|
|
||||||
:29999`) — наследие прямого доступа к netdata; убрать, оставив только доменный
|
|
||||||
site-блок.
|
|
||||||
|
|
||||||
Из sec-ревью (fable, 2026-07-13).
|
|
||||||
|
|
||||||
Связано: files/caddyproxy/Caddyfile.template, files/caddyproxy/docker-compose.template.yml.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Handlers рестарта для оставшихся приложений
|
|
||||||
|
|
||||||
**Приоритет:** средний
|
|
||||||
|
|
||||||
Паттерн `handlers` + `notify` внедрён в caddyproxy, netdata, authelia, bifrost,
|
|
||||||
remembos (см. задачу `ansible-handlers-restart`, закрыта). Остались плейбуки, где
|
|
||||||
конфиг bind-моунтится в контейнер, но рестарта нет **вообще** — правка конфига без
|
|
||||||
изменения docker-compose.yml не подхватывается до следующего пересоздания
|
|
||||||
контейнера:
|
|
||||||
|
|
||||||
- `playbook-apprise.yml` — `server.cfg` → `config_dir:/config`.
|
|
||||||
- `playbook-dashboard.yml` — `config/*.yaml` → `config_dir:/app/config`.
|
|
||||||
- `playbook-transcriber.yml` — `config.toml` bind-моунтится **отдельным файлом**
|
|
||||||
(`{{ config_file }}:/config/config.toml:ro`). Худший случай: `template`
|
|
||||||
перезаписывает файл с новым inode, и контейнер продолжает видеть старый до
|
|
||||||
рестарта.
|
|
||||||
- `playbook-miniflux.yml` — файлы секретов роли `secrets` → `secrets_dir:/secrets:ro`,
|
|
||||||
читаются через `*_FILE` при старте.
|
|
||||||
|
|
||||||
Заодно проверить `playbook-goaccess.yml`: конфиг там не монтируется, но
|
|
||||||
`Dockerfile`/`entrypoint.sh` меняются без изменения compose-файла — надо
|
|
||||||
убедиться, что `build: always` действительно пересоздаёт контейнер.
|
|
||||||
|
|
||||||
Отдельный кандидат: caddy сейчас перезапускается целиком, хотя умеет
|
|
||||||
`caddy reload` — применение Caddyfile без разрыва соединений на реверс-прокси,
|
|
||||||
через который идёт весь трафик. В `playbook-caddyproxy.yml` для этого уже лежит
|
|
||||||
закомментированный блок с `docker_compose_v2_exec`.
|
|
||||||
|
|
||||||
Связано: playbook-apprise.yml, playbook-dashboard.yml, playbook-transcriber.yml,
|
|
||||||
playbook-miniflux.yml, playbook-goaccess.yml, playbook-caddyproxy.yml.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Ограничить эндпоинт `/metrics` Miniflux
|
|
||||||
|
|
||||||
**Приоритет:** средний
|
|
||||||
|
|
||||||
В `files/miniflux/docker-compose.template.yml` заданы `METRICS_COLLECTOR=1` и
|
|
||||||
`METRICS_ALLOWED_NETWORKS=0.0.0.0/0`, а Caddy проксирует весь
|
|
||||||
`miniflux.vakhrushev.me` без ограничения пути. За прокси Miniflux видит приватный
|
|
||||||
IP контейнера Caddy (попадает в `0.0.0.0/0`), поэтому
|
|
||||||
`https://miniflux.vakhrushev.me/metrics` отдаётся публично без аутентификации —
|
|
||||||
утечка эксплуатационной статистики. Сузить `METRICS_ALLOWED_NETWORKS` до подсети
|
|
||||||
`monitoring_network` (netdata скрейпит по внутреннему имени) или заблокировать
|
|
||||||
путь `/metrics` в Caddy.
|
|
||||||
|
|
||||||
Из sec-ревью (fable, 2026-07-13).
|
|
||||||
|
|
||||||
Связано: files/miniflux/docker-compose.template.yml, files/caddyproxy/Caddyfile.template.
|
|
||||||
@@ -25,7 +25,7 @@ miniflux деплоится молча, а остальные — нет.
|
|||||||
расшифрованным
|
расшифрованным
|
||||||
- **transcriber** — `config.secrets.toml`, тоже vault-файл копией
|
- **transcriber** — `config.secrets.toml`, тоже vault-файл копией
|
||||||
- **bifrost** — `config.template.json`: ключ DeepSeek, ключ шифрования
|
- **bifrost** — `config.template.json`: ключ DeepSeek, ключ шифрования
|
||||||
- **apprise** — `server.template.cfg`: токен бота Telegram, SMTP
|
- **apprise** — `backups.template.cfg`: SMTP, пароль matrix-бота
|
||||||
- **remembos** — `config.template.toml`: токены memos и Telegram
|
- **remembos** — `config.template.toml`: токены memos и Telegram
|
||||||
- **outline, tududi, wakapi, wanderer, gramps, gitea** — секреты в
|
- **outline, tududi, wakapi, wanderer, gramps, gitea** — секреты в
|
||||||
`environment:` docker-compose, см. серию задач `secrets-file-*`
|
`environment:` docker-compose, см. серию задач `secrets-file-*`
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# Убрать группу `docker` у сервисных пользователей
|
|
||||||
|
|
||||||
**Приоритет:** высокий
|
|
||||||
|
|
||||||
Все приложения заводят системного пользователя с `owner_extra_groups: ["docker"]`
|
|
||||||
(во всех `playbook-*.yml`), но docker.sock реально монтируют только netdata и
|
|
||||||
dozzle. Членство в группе `docker` эквивалентно root на хосте (`docker run -v
|
|
||||||
/:/host`), поэтому выделенные «непривилегированные» аккаунты (authelia, outline,
|
|
||||||
gitea и т.д.) фактически все имеют root — изоляция «пользователь на сервис», ради
|
|
||||||
которой их и заводили, обнуляется. Контейнерами рулит Ansible от root (become),
|
|
||||||
сервисам группа не нужна. Убрать `docker` из `owner_extra_groups` у всех, кроме
|
|
||||||
тех, кому он действительно нужен для самостоятельного управления своим compose (и
|
|
||||||
тем — лучше через socket-proxy). За `primary_user` группу оставить для ручных
|
|
||||||
операций (`inv ssh`).
|
|
||||||
|
|
||||||
Из sec-ревью (fable, 2026-07-13). Усиливает риск из серии `secrets-file-*`
|
|
||||||
(секреты в `environment:` контейнеров): членство в группе `docker` — как раз тот
|
|
||||||
доступ, которым такие переменные и вычитываются.
|
|
||||||
|
|
||||||
Связано: все playbook-*.yml (owner_extra_groups), roles/owner, playbook-docker.yml.
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
mailtos://{{ postbox_user }}:{{ postbox_pass }}@{{ postbox_host }}:{{ postbox_port }}/?from=notifications@vakhrushev.me&to={{ notifications_email }}
|
||||||
|
# Пароль бота экранируем дважды: jinja-фильтр urlencode оставляет '/' как есть
|
||||||
|
# (safe='/'), а в пароле он развалил бы путь URL. После urlencode литеральных
|
||||||
|
# процентов в строке нет, поэтому замена '/' на %2F безопасна.
|
||||||
|
matrix://apprise:{{ notifications_matrix_bot_password | urlencode | regex_replace('/', '%2F') }}@tuwunel:6167/{{ notifications_matrix_room }}?msgtype=text
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
apprise_app:
|
apprise_app:
|
||||||
image: caronc/apprise:v1.5.1
|
image: caronc/apprise:v1.5.2
|
||||||
container_name: apprise_app
|
container_name: apprise_app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
tgram://{{ notifications_tg_bot_token }}/{{ notifications_tg_chat_id }}
|
|
||||||
mailtos://{{ postbox_user }}:{{ postbox_pass }}@{{ postbox_host }}:{{ postbox_port }}/?from=notifications@vakhrushev.me&to={{ notifications_email }}
|
|
||||||
@@ -121,7 +121,7 @@ server:
|
|||||||
##
|
##
|
||||||
log:
|
log:
|
||||||
## Level of verbosity for logs: info, debug, trace.
|
## Level of verbosity for logs: info, debug, trace.
|
||||||
level: "debug"
|
level: "info"
|
||||||
|
|
||||||
## Format the logs are written as: json, text.
|
## Format the logs are written as: json, text.
|
||||||
format: "json"
|
format: "json"
|
||||||
|
|||||||
+366
-136
@@ -11,13 +11,18 @@ restic-операции разнесены на фазы с разной час
|
|||||||
- verify -- check --read-data-subset, помесячно (полное покрытие за год).
|
- verify -- check --read-data-subset, помесячно (полное покрытие за год).
|
||||||
Один прогон выполняет фазы строго последовательно, поэтому restic-локи между фазами
|
Один прогон выполняет фазы строго последовательно, поэтому restic-локи между фазами
|
||||||
не конфликтуют. Наложение соседних прогонов предотвращается flock в cron-задаче.
|
не конфликтуют. Наложение соседних прогонов предотвращается flock в cron-задаче.
|
||||||
|
|
||||||
|
Размеры приложений считает dust (ставится ролью eget в bin_prefix); если его нет
|
||||||
|
или он упал, прогон продолжается, а размеры в уведомлении просто не показываются.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import itertools
|
import itertools
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pwd
|
import pwd
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -25,8 +30,9 @@ import tomllib
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from croniter import croniter
|
from croniter import croniter
|
||||||
@@ -41,6 +47,10 @@ BACKUP_TARGETS_FILE = "backup-targets"
|
|||||||
# Used when backup-targets file not exists
|
# Used when backup-targets file not exists
|
||||||
BACKUP_DEFAULT_DIR = "backups"
|
BACKUP_DEFAULT_DIR = "backups"
|
||||||
|
|
||||||
|
# Утилита подсчёта размеров директорий (github.com/bootandy/dust).
|
||||||
|
# Ставится ролью eget в bin_prefix, который есть в PATH cron-задачи.
|
||||||
|
DUST_BIN = "dust"
|
||||||
|
|
||||||
# Retention policy applied by the `forget` phase on every run.
|
# Retention policy applied by the `forget` phase on every run.
|
||||||
KEEP_DAILY = "90"
|
KEEP_DAILY = "90"
|
||||||
KEEP_MONTHLY = "36"
|
KEEP_MONTHLY = "36"
|
||||||
@@ -72,6 +82,7 @@ logger = logging.getLogger(__name__)
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Config:
|
class Config:
|
||||||
host_name: str
|
host_name: str
|
||||||
|
roots: list[Path]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -87,9 +98,9 @@ class MaintenanceOptions:
|
|||||||
class Schedule:
|
class Schedule:
|
||||||
"""Расписание обслуживающих фаз: фаза -> cron-выражение."""
|
"""Расписание обслуживающих фаз: фаза -> cron-выражение."""
|
||||||
|
|
||||||
cron: Dict[str, str] = field(default_factory=dict)
|
cron: dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
def due_phases(self, now: datetime) -> List[str]:
|
def due_phases(self, now: datetime) -> list[str]:
|
||||||
"""Фазы, которые нужно выполнить в этот прогон, в порядке PHASE_ORDER."""
|
"""Фазы, которые нужно выполнить в этот прогон, в порядке PHASE_ORDER."""
|
||||||
phases = list(ALWAYS_PHASES)
|
phases = list(ALWAYS_PHASES)
|
||||||
for phase in SCHEDULED_PHASES:
|
for phase in SCHEDULED_PHASES:
|
||||||
@@ -114,14 +125,55 @@ class Schedule:
|
|||||||
class Application:
|
class Application:
|
||||||
path: Path
|
path: Path
|
||||||
owner: str
|
owner: str
|
||||||
backup_script: Optional[Path]
|
backup_script: Path | None
|
||||||
backup_targets: List[Path]
|
backup_targets: list[Path]
|
||||||
|
|
||||||
|
|
||||||
|
class AppStatus(Enum):
|
||||||
|
"""Что случилось с приложением в этот прогон."""
|
||||||
|
|
||||||
|
DONE = "done"
|
||||||
|
FAILED = "failed"
|
||||||
|
SKIPPED = "skipped"
|
||||||
|
|
||||||
|
|
||||||
|
APP_STATUS_ICONS = {
|
||||||
|
AppStatus.DONE: "✅",
|
||||||
|
AppStatus.FAILED: "❌",
|
||||||
|
AppStatus.SKIPPED: "⏭",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AppRunResult:
|
||||||
|
"""Строка приложения в уведомлении: статус бекапа и занятое место."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
status: AppStatus
|
||||||
|
size: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DiskUsage:
|
||||||
|
"""Занятое и свободное место на файловой системе."""
|
||||||
|
|
||||||
|
path: Path
|
||||||
|
total: int
|
||||||
|
free: int
|
||||||
|
|
||||||
|
@property
|
||||||
|
def used(self) -> int:
|
||||||
|
return self.total - self.free
|
||||||
|
|
||||||
|
@property
|
||||||
|
def used_percent(self) -> float:
|
||||||
|
return 100.0 * self.used / self.total if self.total else 0.0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BackupResult:
|
class BackupResult:
|
||||||
success: bool
|
success: bool
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -129,7 +181,80 @@ class StorageRunResult:
|
|||||||
name: str
|
name: str
|
||||||
success: bool
|
success: bool
|
||||||
duration: float
|
duration: float
|
||||||
phases: List[str]
|
phases: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
def format_size(size: int) -> str:
|
||||||
|
"""Байты в человекочитаемый вид: 4.1 GiB, 512 MiB, 12 KiB."""
|
||||||
|
value = float(size)
|
||||||
|
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
|
||||||
|
if value < 1024 or unit == "TiB":
|
||||||
|
precision = 0 if unit == "B" or value >= 100 else 1
|
||||||
|
return f"{value:.{precision}f} {unit}"
|
||||||
|
value /= 1024
|
||||||
|
return f"{value:.1f} TiB"
|
||||||
|
|
||||||
|
|
||||||
|
def measure_app_sizes(paths: list[Path]) -> dict[str, int]:
|
||||||
|
"""Размеры директорий приложений одним вызовом dust: путь -> байты.
|
||||||
|
|
||||||
|
dust с `-o b` печатает размеры строками вида "1052672B", а при нескольких
|
||||||
|
аргументах заворачивает их в корень "(total)" — разбираем оба случая.
|
||||||
|
"""
|
||||||
|
if not paths:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
cmd = [DUST_BIN, "--output-json", "--output-format", "b", "--depth", "0"]
|
||||||
|
cmd += ["--no-progress", *(str(path) for path in paths)]
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
||||||
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||||
|
logger.warning("Failed to run %s: %s", DUST_BIN, exc)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.warning(
|
||||||
|
"%s exited with code %s: %s", DUST_BIN, result.returncode, result.stderr
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree = json.loads(result.stdout)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
logger.warning("Could not parse %s output: %s", DUST_BIN, exc)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
sizes: dict[str, int] = {}
|
||||||
|
for node in [tree, *tree.get("children", [])]:
|
||||||
|
raw_size = str(node.get("size", "")).rstrip("B")
|
||||||
|
if not raw_size.isdigit():
|
||||||
|
continue
|
||||||
|
sizes[str(node.get("name", ""))] = int(raw_size)
|
||||||
|
return sizes
|
||||||
|
|
||||||
|
|
||||||
|
def collect_disk_usage(paths: list[Path]) -> list[DiskUsage]:
|
||||||
|
"""Занятое/свободное место по файловым системам, на которых лежат paths.
|
||||||
|
|
||||||
|
Пути с одной и той же файловой системы схлопываются: смысла показывать
|
||||||
|
/mnt/applications дважды нет.
|
||||||
|
"""
|
||||||
|
usages: list[DiskUsage] = []
|
||||||
|
seen_devices: set[int] = set()
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
try:
|
||||||
|
device = path.stat().st_dev
|
||||||
|
if device in seen_devices:
|
||||||
|
continue
|
||||||
|
total, _used, free = shutil.disk_usage(path)
|
||||||
|
except OSError as exc:
|
||||||
|
logger.warning("Could not read disk usage for %s: %s", path, exc)
|
||||||
|
continue
|
||||||
|
seen_devices.add(device)
|
||||||
|
usages.append(DiskUsage(path=path, total=total, free=free))
|
||||||
|
|
||||||
|
return usages
|
||||||
|
|
||||||
|
|
||||||
def format_duration(seconds: float) -> str:
|
def format_duration(seconds: float) -> str:
|
||||||
@@ -149,8 +274,8 @@ class Storage(ABC):
|
|||||||
|
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
backup_dirs: List[str],
|
backup_dirs: list[str],
|
||||||
phases: List[str],
|
phases: list[str],
|
||||||
maintenance: MaintenanceOptions,
|
maintenance: MaintenanceOptions,
|
||||||
) -> BackupResult:
|
) -> BackupResult:
|
||||||
"""Run the requested phases against this storage."""
|
"""Run the requested phases against this storage."""
|
||||||
@@ -160,7 +285,7 @@ class Storage(ABC):
|
|||||||
class ResticStorage(Storage):
|
class ResticStorage(Storage):
|
||||||
TYPE_NAME = "restic"
|
TYPE_NAME = "restic"
|
||||||
|
|
||||||
def __init__(self, name: str, params: Dict[str, Any]):
|
def __init__(self, name: str, params: dict[str, Any]) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.restic_repository = str(params.get("restic_repository", ""))
|
self.restic_repository = str(params.get("restic_repository", ""))
|
||||||
self.restic_password = str(params.get("restic_password", ""))
|
self.restic_password = str(params.get("restic_password", ""))
|
||||||
@@ -170,7 +295,7 @@ class ResticStorage(Storage):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"'env' must be a table for storage backend ResticStorage: '{self.name}'"
|
f"'env' must be a table for storage backend ResticStorage: '{self.name}'"
|
||||||
)
|
)
|
||||||
self.env: Dict[str, str] = {str(k): str(v) for k, v in env_raw.items()}
|
self.env: dict[str, str] = {str(k): str(v) for k, v in env_raw.items()}
|
||||||
|
|
||||||
if not self.restic_repository or not self.restic_password:
|
if not self.restic_repository or not self.restic_password:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -179,8 +304,8 @@ class ResticStorage(Storage):
|
|||||||
|
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
backup_dirs: List[str],
|
backup_dirs: list[str],
|
||||||
phases: List[str],
|
phases: list[str],
|
||||||
maintenance: MaintenanceOptions,
|
maintenance: MaintenanceOptions,
|
||||||
) -> BackupResult:
|
) -> BackupResult:
|
||||||
try:
|
try:
|
||||||
@@ -191,12 +316,12 @@ class ResticStorage(Storage):
|
|||||||
|
|
||||||
def __build_steps(
|
def __build_steps(
|
||||||
self,
|
self,
|
||||||
backup_dirs: List[str],
|
backup_dirs: list[str],
|
||||||
phases: List[str],
|
phases: list[str],
|
||||||
maintenance: MaintenanceOptions,
|
maintenance: MaintenanceOptions,
|
||||||
) -> List[tuple[str, List[str]]]:
|
) -> list[tuple[str, list[str]]]:
|
||||||
"""Собрать restic-команды для запрошенных фаз в порядке PHASE_ORDER."""
|
"""Собрать restic-команды для запрошенных фаз в порядке PHASE_ORDER."""
|
||||||
steps: List[tuple[str, List[str]]] = []
|
steps: list[tuple[str, list[str]]] = []
|
||||||
|
|
||||||
for phase in PHASE_ORDER:
|
for phase in PHASE_ORDER:
|
||||||
if phase not in phases:
|
if phase not in phases:
|
||||||
@@ -261,8 +386,8 @@ class ResticStorage(Storage):
|
|||||||
|
|
||||||
def __run_internal(
|
def __run_internal(
|
||||||
self,
|
self,
|
||||||
backup_dirs: List[str],
|
backup_dirs: list[str],
|
||||||
phases: List[str],
|
phases: list[str],
|
||||||
maintenance: MaintenanceOptions,
|
maintenance: MaintenanceOptions,
|
||||||
) -> BackupResult:
|
) -> BackupResult:
|
||||||
logger.info("Starting restic run for storage '%s'", self.name)
|
logger.info("Starting restic run for storage '%s'", self.name)
|
||||||
@@ -283,9 +408,7 @@ class ResticStorage(Storage):
|
|||||||
|
|
||||||
return BackupResult(success=True)
|
return BackupResult(success=True)
|
||||||
|
|
||||||
def __run_step(
|
def __run_step(self, step: str, cmd: list[str], env: dict[str, str]) -> str | None:
|
||||||
self, step: str, cmd: List[str], env: Dict[str, str]
|
|
||||||
) -> Optional[str]:
|
|
||||||
"""Run a single restic command. Return None on success or error text."""
|
"""Run a single restic command. Return None on success or error text."""
|
||||||
result = subprocess.run(cmd, env=env, capture_output=True, text=True)
|
result = subprocess.run(cmd, env=env, capture_output=True, text=True)
|
||||||
|
|
||||||
@@ -306,7 +429,7 @@ class Notifier(ABC):
|
|||||||
class AppriseNotifier(Notifier):
|
class AppriseNotifier(Notifier):
|
||||||
TYPE_NAME = "apprise"
|
TYPE_NAME = "apprise"
|
||||||
|
|
||||||
def __init__(self, name: str, params: Dict[str, Any]):
|
def __init__(self, name: str, params: dict[str, Any]) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.api_url = str(params.get("api_url", "")).rstrip("/")
|
self.api_url = str(params.get("api_url", "")).rstrip("/")
|
||||||
self.tag = str(params.get("tag", ""))
|
self.tag = str(params.get("tag", ""))
|
||||||
@@ -329,18 +452,20 @@ class AppriseNotifier(Notifier):
|
|||||||
logger.info("Apprise notification sent successfully")
|
logger.info("Apprise notification sent successfully")
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Failed to send Apprise notification: {response.status_code} - {response.text}"
|
"Failed to send Apprise notification: %s - %s",
|
||||||
|
response.status_code,
|
||||||
|
response.text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ApplicationFinder:
|
class ApplicationFinder:
|
||||||
def __init__(self, roots: List[Path]):
|
def __init__(self, roots: list[Path]) -> None:
|
||||||
self.roots = roots
|
self.roots = roots
|
||||||
self.warnings: List[str] = []
|
self.warnings: list[str] = []
|
||||||
|
|
||||||
def find_applications(self) -> List[Application]:
|
def find_applications(self) -> list[Application]:
|
||||||
"""Discover all applications with their backup scripts and targets."""
|
"""Discover all applications with their backup scripts and targets."""
|
||||||
applications: List[Application] = []
|
applications: list[Application] = []
|
||||||
source_dirs = itertools.chain(*(root.iterdir() for root in self.roots))
|
source_dirs = itertools.chain(*(root.iterdir() for root in self.roots))
|
||||||
|
|
||||||
for app_dir in source_dirs:
|
for app_dir in source_dirs:
|
||||||
@@ -361,28 +486,27 @@ class ApplicationFinder:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
except (KeyError, OSError) as e:
|
except (KeyError, OSError) as e:
|
||||||
logger.warning(f"Could not get owner for {app_dir}: {e}")
|
logger.warning("Could not get owner for %s: %s", app_dir, e)
|
||||||
|
|
||||||
applications.sort(key=lambda app: app.path.name)
|
applications.sort(key=lambda app: app.path.name)
|
||||||
return applications
|
return applications
|
||||||
|
|
||||||
def _find_backup_script(self, app_dir: Path) -> Optional[Path]:
|
def _find_backup_script(self, app_dir: Path) -> Path | None:
|
||||||
"""Find executable backup script in application directory."""
|
"""Find executable backup script in application directory."""
|
||||||
for name in ("backup.sh", "backup"):
|
for name in ("backup.sh", "backup"):
|
||||||
script_path = app_dir / name
|
script_path = app_dir / name
|
||||||
if script_path.exists():
|
if script_path.exists():
|
||||||
if os.access(script_path, os.X_OK):
|
if os.access(script_path, os.X_OK):
|
||||||
return script_path
|
return script_path
|
||||||
else:
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Backup script {script_path} exists but is not executable"
|
"Backup script %s exists but is not executable", script_path
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _find_backup_targets(self, app_dir: Path) -> List[Path]:
|
def _find_backup_targets(self, app_dir: Path) -> list[Path]:
|
||||||
"""Resolve backup target directories for an application."""
|
"""Resolve backup target directories for an application."""
|
||||||
targets_file = app_dir / BACKUP_TARGETS_FILE
|
targets_file = app_dir / BACKUP_TARGETS_FILE
|
||||||
resolved_targets: List[Path] = []
|
resolved_targets: list[Path] = []
|
||||||
|
|
||||||
if targets_file.exists():
|
if targets_file.exists():
|
||||||
for target_line in self._parse_targets_file(targets_file):
|
for target_line in self._parse_targets_file(targets_file):
|
||||||
@@ -410,9 +534,9 @@ class ApplicationFinder:
|
|||||||
|
|
||||||
return resolved_targets
|
return resolved_targets
|
||||||
|
|
||||||
def _parse_targets_file(self, targets_file: Path) -> List[str]:
|
def _parse_targets_file(self, targets_file: Path) -> list[str]:
|
||||||
"""Parse backup-targets file, skipping comments and empty lines."""
|
"""Parse backup-targets file, skipping comments and empty lines."""
|
||||||
targets: List[str] = []
|
targets: list[str] = []
|
||||||
try:
|
try:
|
||||||
for raw_line in targets_file.read_text(encoding="utf-8").splitlines():
|
for raw_line in targets_file.read_text(encoding="utf-8").splitlines():
|
||||||
line = raw_line.strip()
|
line = raw_line.strip()
|
||||||
@@ -430,75 +554,156 @@ class BackupManager:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: Config,
|
config: Config,
|
||||||
storages: List[Storage],
|
storages: list[Storage],
|
||||||
notifiers: List[Notifier],
|
notifiers: list[Notifier],
|
||||||
schedule: Schedule,
|
schedule: Schedule,
|
||||||
maintenance: MaintenanceOptions,
|
maintenance: MaintenanceOptions,
|
||||||
forced_phases: Optional[List[str]] = None,
|
forced_phases: list[str] | None = None,
|
||||||
):
|
) -> None:
|
||||||
self.errors: List[str] = []
|
self.errors: list[str] = []
|
||||||
self.warnings: List[str] = []
|
self.warnings: list[str] = []
|
||||||
self.successful_backups: List[str] = []
|
self.app_results: list[AppRunResult] = []
|
||||||
|
self.disk_usages: list[DiskUsage] = []
|
||||||
self.config = config
|
self.config = config
|
||||||
self.storages = storages
|
self.storages = storages
|
||||||
self.notifiers = notifiers
|
self.notifiers = notifiers
|
||||||
self.schedule = schedule
|
self.schedule = schedule
|
||||||
self.maintenance = maintenance
|
self.maintenance = maintenance
|
||||||
self.forced_phases = forced_phases
|
self.forced_phases = forced_phases
|
||||||
self.active_phases: List[str] = []
|
self.active_phases: list[str] = []
|
||||||
self.archive_duration: float = 0.0
|
self.archive_duration: float = 0.0
|
||||||
self.storage_results: List[StorageRunResult] = []
|
self.storage_results: list[StorageRunResult] = []
|
||||||
|
|
||||||
def run_backup_process(self, applications: List[Application]) -> bool:
|
def run_backup_process(self, applications: list[Application]) -> bool:
|
||||||
"""Main backup process"""
|
"""Main backup process"""
|
||||||
logger.info("Starting backup process")
|
logger.info("Starting backup process")
|
||||||
logger.info(f"Found {len(applications)} application directories")
|
logger.info("Found %d application directories", len(applications))
|
||||||
|
|
||||||
# Какие фазы выполняем в этот прогон: либо принудительно из CLI, либо по расписанию.
|
self.active_phases = self._resolve_phases()
|
||||||
|
self._run_archive_phase(applications)
|
||||||
|
backup_dirs = self._collect_backup_dirs(applications)
|
||||||
|
overall_success = self._run_storages(backup_dirs)
|
||||||
|
self._collect_usage(applications)
|
||||||
|
|
||||||
|
self._send_notification(overall_success)
|
||||||
|
|
||||||
|
logger.info("Backup process completed")
|
||||||
|
|
||||||
|
if self.errors:
|
||||||
|
logger.error("Backup completed with %d errors", len(self.errors))
|
||||||
|
return False
|
||||||
|
if self.warnings:
|
||||||
|
logger.warning("Backup completed with %d warnings", len(self.warnings))
|
||||||
|
return True
|
||||||
|
logger.info("Backup completed successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _resolve_phases(self) -> list[str]:
|
||||||
|
"""Какие фазы выполняем в этот прогон: принудительно из CLI или по расписанию."""
|
||||||
if self.forced_phases is not None:
|
if self.forced_phases is not None:
|
||||||
self.active_phases = self.forced_phases
|
logger.info("Phases (forced): %s", ", ".join(self.forced_phases))
|
||||||
logger.info("Phases (forced): %s", ", ".join(self.active_phases))
|
return self.forced_phases
|
||||||
else:
|
|
||||||
self.active_phases = self.schedule.due_phases(datetime.now())
|
|
||||||
logger.info("Phases (scheduled): %s", ", ".join(self.active_phases))
|
|
||||||
|
|
||||||
|
phases = self.schedule.due_phases(datetime.now())
|
||||||
|
logger.info("Phases (scheduled): %s", ", ".join(phases))
|
||||||
|
return phases
|
||||||
|
|
||||||
|
def _run_archive_phase(self, applications: list[Application]) -> None:
|
||||||
|
"""Прогнать скрипты дампов приложений и собрать список того, что уедет в restic.
|
||||||
|
|
||||||
|
Фаза нужна только вместе с restic backup: без неё дампы делать некому и незачем.
|
||||||
|
"""
|
||||||
archive_start = time.monotonic()
|
archive_start = time.monotonic()
|
||||||
# Archive phase (per-app backup scripts) нужна только если будем делать restic backup.
|
|
||||||
if PHASE_BACKUP in self.active_phases:
|
if PHASE_BACKUP in self.active_phases:
|
||||||
for app in applications:
|
for app in applications:
|
||||||
app_dir = str(app.path)
|
status = self._archive_app(app)
|
||||||
username = app.owner
|
self.app_results.append(AppRunResult(name=app.path.name, status=status))
|
||||||
logger.info(f"Processing backup for app: {app_dir} (user {username})")
|
|
||||||
|
|
||||||
if app.backup_script is None:
|
|
||||||
warning_msg = (
|
|
||||||
f"No backup script found for app: {app_dir} (user {username})"
|
|
||||||
)
|
|
||||||
logger.warning(warning_msg)
|
|
||||||
self.warnings.append(warning_msg)
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._run_app_backup(str(app.backup_script), app_dir, username)
|
|
||||||
else:
|
else:
|
||||||
logger.info("Backup phase not active, skipping per-app archive scripts")
|
logger.info("Backup phase not active, skipping per-app archive scripts")
|
||||||
|
self.app_results = [
|
||||||
|
AppRunResult(name=app.path.name, status=AppStatus.SKIPPED)
|
||||||
|
for app in applications
|
||||||
|
]
|
||||||
|
|
||||||
self.archive_duration = time.monotonic() - archive_start
|
self.archive_duration = time.monotonic() - archive_start
|
||||||
logger.info(
|
logger.info(
|
||||||
"Archive phase finished in %s", format_duration(self.archive_duration)
|
"Archive phase finished in %s", format_duration(self.archive_duration)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Collect backup directories from applications
|
def _archive_app(self, app: Application) -> AppStatus:
|
||||||
backup_dirs: List[str] = []
|
"""Обработать одно приложение: сделать дамп, если он предусмотрен."""
|
||||||
|
app_dir = str(app.path)
|
||||||
|
username = app.owner
|
||||||
|
|
||||||
|
if app.backup_script is None:
|
||||||
|
if app.backup_targets:
|
||||||
|
# Приложение без дампа: restic забирает его данные как есть,
|
||||||
|
# отдельный шаг архивации ему не нужен.
|
||||||
|
logger.info(
|
||||||
|
"No backup script for app: %s (user %s), "
|
||||||
|
"data directories go to restic as is",
|
||||||
|
app_dir,
|
||||||
|
username,
|
||||||
|
)
|
||||||
|
return AppStatus.DONE
|
||||||
|
|
||||||
|
warning_msg = (
|
||||||
|
f"Nothing to back up for app: {app_dir} (user {username}): "
|
||||||
|
f"no backup script and no backup targets"
|
||||||
|
)
|
||||||
|
logger.warning(warning_msg)
|
||||||
|
self.warnings.append(warning_msg)
|
||||||
|
return AppStatus.SKIPPED
|
||||||
|
|
||||||
|
logger.info("Processing backup for app: %s (user %s)", app_dir, username)
|
||||||
|
if not self._run_app_backup(str(app.backup_script), app_dir, username):
|
||||||
|
return AppStatus.FAILED
|
||||||
|
# Дамп сделан, но в restic он попадёт только если есть цели бекапа;
|
||||||
|
# об их отсутствии уже предупредил ApplicationFinder.
|
||||||
|
return AppStatus.DONE if app.backup_targets else AppStatus.SKIPPED
|
||||||
|
|
||||||
|
def _collect_usage(self, applications: list[Application]) -> None:
|
||||||
|
"""Померить размеры приложений и свободное место на их файловых системах.
|
||||||
|
|
||||||
|
Считаем после архивации, чтобы свежие дампы попали в размер, и после
|
||||||
|
restic: цифры информационные, задерживать из-за них бекап незачем.
|
||||||
|
"""
|
||||||
|
usage_start = time.monotonic()
|
||||||
|
|
||||||
|
sizes = measure_app_sizes([app.path for app in applications])
|
||||||
|
by_name = {app.path.name: str(app.path) for app in applications}
|
||||||
|
for result in self.app_results:
|
||||||
|
result.size = sizes.get(by_name.get(result.name, ""))
|
||||||
|
|
||||||
|
# Корень системы плюс диски, на которых лежат приложения: на сервере это
|
||||||
|
# разные диски, и место кончается на них независимо.
|
||||||
|
self.disk_usages = collect_disk_usage([Path("/"), *self.config.roots])
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Usage stats collected in %s",
|
||||||
|
format_duration(time.monotonic() - usage_start),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _collect_backup_dirs(applications: list[Application]) -> list[str]:
|
||||||
|
"""Собрать цели бекапа всех приложений, сохраняя порядок и убирая дубли."""
|
||||||
|
backup_dirs: list[str] = []
|
||||||
for app in applications:
|
for app in applications:
|
||||||
for target in app.backup_targets:
|
for target in app.backup_targets:
|
||||||
target_str = str(target)
|
target_str = str(target)
|
||||||
if target_str not in backup_dirs:
|
if target_str not in backup_dirs:
|
||||||
backup_dirs.append(target_str)
|
backup_dirs.append(target_str)
|
||||||
logger.info(f"Found backup directories: {backup_dirs}")
|
logger.info("Found backup directories: %s", backup_dirs)
|
||||||
|
return backup_dirs
|
||||||
|
|
||||||
|
def _run_storages(self, backup_dirs: list[str]) -> bool:
|
||||||
|
"""Прогнать активные фазы по всем хранилищам.
|
||||||
|
|
||||||
|
Хранилища независимы: падение одного не отменяет попытку для остальных.
|
||||||
|
"""
|
||||||
overall_success = True
|
overall_success = True
|
||||||
|
|
||||||
# Each storage is processed independently: a failure in one storage
|
|
||||||
# must not prevent the others from being attempted.
|
|
||||||
for storage in self.storages:
|
for storage in self.storages:
|
||||||
storage_start = time.monotonic()
|
storage_start = time.monotonic()
|
||||||
try:
|
try:
|
||||||
@@ -531,28 +736,14 @@ class BackupManager:
|
|||||||
error_msg += f": {backup_result.error}"
|
error_msg += f": {backup_result.error}"
|
||||||
self.errors.append(error_msg)
|
self.errors.append(error_msg)
|
||||||
|
|
||||||
# Determine overall success
|
|
||||||
overall_success = overall_success and backup_result.success
|
overall_success = overall_success and backup_result.success
|
||||||
|
|
||||||
# Send notification
|
return overall_success
|
||||||
self._send_notification(overall_success)
|
|
||||||
|
|
||||||
logger.info("Backup process completed")
|
|
||||||
|
|
||||||
if self.errors:
|
|
||||||
logger.error(f"Backup completed with {len(self.errors)} errors")
|
|
||||||
return False
|
|
||||||
elif self.warnings:
|
|
||||||
logger.warning(f"Backup completed with {len(self.warnings)} warnings")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.info("Backup completed successfully")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _run_app_backup(self, script_path: str, app_dir: str, username: str) -> bool:
|
def _run_app_backup(self, script_path: str, app_dir: str, username: str) -> bool:
|
||||||
"""Run backup script as the specified user"""
|
"""Run backup script as the specified user"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"Running backup script {script_path} (user {username})")
|
logger.info("Running backup script %s (user %s)", script_path, username)
|
||||||
|
|
||||||
# Use su to run the script as the user
|
# Use su to run the script as the user
|
||||||
cmd = ["su", "--login", username, "--command", script_path]
|
cmd = ["su", "--login", username, "--command", script_path]
|
||||||
@@ -566,10 +757,8 @@ class BackupManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
logger.info(f"Backup script for {username} completed successfully")
|
logger.info("Backup script for %s completed successfully", username)
|
||||||
self.successful_backups.append(username)
|
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
error_msg = f"Backup script {script_path} failed with return code {result.returncode}"
|
error_msg = f"Backup script {script_path} failed with return code {result.returncode}"
|
||||||
if result.stderr:
|
if result.stderr:
|
||||||
error_msg += f": {result.stderr}"
|
error_msg += f": {result.stderr}"
|
||||||
@@ -588,26 +777,56 @@ class BackupManager:
|
|||||||
self.errors.append(f"App {username}: {error_msg}")
|
self.errors.append(f"App {username}: {error_msg}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _render_apps(self) -> str:
|
||||||
|
"""Список приложений: значок статуса, имя и занятое место."""
|
||||||
|
if not self.app_results:
|
||||||
|
return ""
|
||||||
|
items = ""
|
||||||
|
for result in self.app_results:
|
||||||
|
# Размер отсутствует, только если dust не отработал: тогда просто имя.
|
||||||
|
size = f" — {format_size(result.size)}" if result.size is not None else ""
|
||||||
|
items += f"<li>{APP_STATUS_ICONS[result.status]} {result.name}{size}</li>"
|
||||||
|
return f"<p>Приложения:</p><ul>{items}</ul>"
|
||||||
|
|
||||||
|
def _render_run_stats(self) -> str:
|
||||||
|
"""Фазы restic и затраченное время."""
|
||||||
|
phases_text = ", ".join(self.active_phases) if self.active_phases else "—"
|
||||||
|
block = f"<p>🔧 Фазы restic: {phases_text}</p>"
|
||||||
|
block += f"<p>⏱ Время архивации: {format_duration(self.archive_duration)}</p>"
|
||||||
|
if self.storage_results:
|
||||||
|
items = "".join(
|
||||||
|
f"<li>{'✅' if r.success else '❌'} {r.name}: {format_duration(r.duration)}</li>"
|
||||||
|
for r in self.storage_results
|
||||||
|
)
|
||||||
|
block += f"<p>⏱ Время записи в хранилища:</p><ul>{items}</ul>"
|
||||||
|
return block
|
||||||
|
|
||||||
|
def _render_disks(self) -> str:
|
||||||
|
"""Свободное место на дисках сервера."""
|
||||||
|
if not self.disk_usages:
|
||||||
|
return ""
|
||||||
|
items = "".join(
|
||||||
|
f"<li>{u.path}: свободно {format_size(u.free)} из {format_size(u.total)}"
|
||||||
|
f" (занято {u.used_percent:.0f}%)</li>"
|
||||||
|
for u in self.disk_usages
|
||||||
|
)
|
||||||
|
return f"<p>💾 Свободное место:</p><ul>{items}</ul>"
|
||||||
|
|
||||||
def _send_notification(self, success: bool) -> None:
|
def _send_notification(self, success: bool) -> None:
|
||||||
"""Send notification to Notifiers"""
|
"""Send notification to Notifiers"""
|
||||||
|
|
||||||
host = self.config.host_name
|
host = self.config.host_name
|
||||||
phases_text = ", ".join(self.active_phases) if self.active_phases else "—"
|
|
||||||
|
|
||||||
if success and not self.errors:
|
if success and not self.errors:
|
||||||
title = f"{host}: бекап успешно завершен"
|
title = f"{host}: бекап успешно завершен"
|
||||||
message = f"<p><b>{host}</b>: бекап успешно завершен!</p>"
|
message = f"<p><b>{host}</b>: бекап успешно завершен!</p>"
|
||||||
if self.successful_backups:
|
|
||||||
items = "".join(f"<li>{b}</li>" for b in self.successful_backups)
|
|
||||||
message += f"<p>Успешные бекапы:</p><ul>{items}</ul>"
|
|
||||||
else:
|
else:
|
||||||
title = f"{host}: бекап завершен с ошибками ({len(self.errors)})"
|
title = f"{host}: бекап завершен с ошибками ({len(self.errors)})"
|
||||||
message = f"<p><b>{host}</b>: бекап завершен с ошибками!</p>"
|
message = f"<p><b>{host}</b>: бекап завершен с ошибками!</p>"
|
||||||
|
|
||||||
if self.successful_backups:
|
message += self._render_apps()
|
||||||
items = "".join(f"<li>{b}</li>" for b in self.successful_backups)
|
|
||||||
message += f"<p>✅ Успешные бекапы:</p><ul>{items}</ul>"
|
|
||||||
|
|
||||||
|
if not (success and not self.errors):
|
||||||
if self.warnings:
|
if self.warnings:
|
||||||
items = "".join(f"<li>{w}</li>" for w in self.warnings)
|
items = "".join(f"<li>{w}</li>" for w in self.warnings)
|
||||||
message += f"<p>⚠️ Предупреждения:</p><ul>{items}</ul>"
|
message += f"<p>⚠️ Предупреждения:</p><ul>{items}</ul>"
|
||||||
@@ -616,23 +835,17 @@ class BackupManager:
|
|||||||
items = "".join(f"<li>{e}</li>" for e in self.errors)
|
items = "".join(f"<li>{e}</li>" for e in self.errors)
|
||||||
message += f"<p>❌ Ошибки:</p><ul>{items}</ul>"
|
message += f"<p>❌ Ошибки:</p><ul>{items}</ul>"
|
||||||
|
|
||||||
message += f"<p>🔧 Фазы restic: {phases_text}</p>"
|
message += self._render_run_stats()
|
||||||
message += f"<p>⏱ Время архивации: {format_duration(self.archive_duration)}</p>"
|
message += self._render_disks()
|
||||||
if self.storage_results:
|
|
||||||
items = "".join(
|
|
||||||
f"<li>{'✅' if r.success else '❌'} {r.name}: {format_duration(r.duration)}</li>"
|
|
||||||
for r in self.storage_results
|
|
||||||
)
|
|
||||||
message += f"<p>⏱ Время записи в хранилища:</p><ul>{items}</ul>"
|
|
||||||
|
|
||||||
for notificator in self.notifiers:
|
for notificator in self.notifiers:
|
||||||
try:
|
try:
|
||||||
notificator.send(title, message)
|
notificator.send(title, message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send notification: {str(e)}")
|
logger.error("Failed to send notification: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def parse_phases(raw: str) -> List[str]:
|
def parse_phases(raw: str) -> list[str]:
|
||||||
"""Разобрать CLI-список фаз, вернуть их в порядке PHASE_ORDER."""
|
"""Разобрать CLI-список фаз, вернуть их в порядке PHASE_ORDER."""
|
||||||
requested = {p.strip() for p in raw.split(",") if p.strip()}
|
requested = {p.strip() for p in raw.split(",") if p.strip()}
|
||||||
unknown = requested - set(PHASE_ORDER)
|
unknown = requested - set(PHASE_ORDER)
|
||||||
@@ -644,50 +857,40 @@ def parse_phases(raw: str) -> List[str]:
|
|||||||
return [p for p in PHASE_ORDER if p in requested]
|
return [p for p in PHASE_ORDER if p in requested]
|
||||||
|
|
||||||
|
|
||||||
def initialize(
|
def build_storages(raw_config: dict[str, Any]) -> list[Storage]:
|
||||||
config_path: Path,
|
"""Собрать хранилища из секции [storage] конфига."""
|
||||||
forced_phases: Optional[List[str]] = None,
|
|
||||||
) -> tuple[ApplicationFinder, BackupManager]:
|
|
||||||
try:
|
|
||||||
with config_path.open("rb") as config_file:
|
|
||||||
raw_config = tomllib.load(config_file)
|
|
||||||
except OSError as e:
|
|
||||||
logger.error(f"Failed to read config file {config_path}: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
host_name = str(raw_config.get("host_name", "unknown"))
|
|
||||||
|
|
||||||
roots_raw = raw_config.get("roots") or []
|
|
||||||
if not isinstance(roots_raw, list) or not roots_raw:
|
|
||||||
raise ValueError("roots must be a non-empty list of paths in config.toml")
|
|
||||||
roots = [Path(root) for root in roots_raw]
|
|
||||||
|
|
||||||
storage_raw = raw_config.get("storage") or {}
|
storage_raw = raw_config.get("storage") or {}
|
||||||
storages: List[Storage] = []
|
storages: list[Storage] = []
|
||||||
for name, params in storage_raw.items():
|
for name, params in storage_raw.items():
|
||||||
if not isinstance(params, dict):
|
if not isinstance(params, dict):
|
||||||
raise ValueError(f"Storage config for {name} must be a table")
|
raise ValueError(f"Storage config for {name} must be a table")
|
||||||
storage_type = params.get("type", "")
|
if params.get("type", "") == ResticStorage.TYPE_NAME:
|
||||||
if storage_type == ResticStorage.TYPE_NAME:
|
|
||||||
storages.append(ResticStorage(name, params))
|
storages.append(ResticStorage(name, params))
|
||||||
if not storages:
|
if not storages:
|
||||||
raise ValueError("At least one storage backend must be configured")
|
raise ValueError("At least one storage backend must be configured")
|
||||||
|
return storages
|
||||||
|
|
||||||
|
|
||||||
|
def build_notifiers(raw_config: dict[str, Any]) -> list[Notifier]:
|
||||||
|
"""Собрать нотификаторы из секции [notifier] конфига."""
|
||||||
notifications_raw = raw_config.get("notifier") or {}
|
notifications_raw = raw_config.get("notifier") or {}
|
||||||
notifiers: List[Notifier] = []
|
notifiers: list[Notifier] = []
|
||||||
for name, params in notifications_raw.items():
|
for name, params in notifications_raw.items():
|
||||||
if not isinstance(params, dict):
|
if not isinstance(params, dict):
|
||||||
raise ValueError(f"Notificator config for {name} must be a table")
|
raise ValueError(f"Notificator config for {name} must be a table")
|
||||||
notifier_type = params.get("type", "")
|
if params.get("type", "") == AppriseNotifier.TYPE_NAME:
|
||||||
if notifier_type == AppriseNotifier.TYPE_NAME:
|
|
||||||
notifiers.append(AppriseNotifier(name, params))
|
notifiers.append(AppriseNotifier(name, params))
|
||||||
if not notifiers:
|
if not notifiers:
|
||||||
raise ValueError("At least one notification backend must be configured")
|
raise ValueError("At least one notification backend must be configured")
|
||||||
|
return notifiers
|
||||||
|
|
||||||
|
|
||||||
|
def build_schedule(raw_config: dict[str, Any]) -> Schedule:
|
||||||
|
"""Собрать расписание обслуживающих фаз из секции [schedule] конфига."""
|
||||||
schedule_raw = raw_config.get("schedule") or {}
|
schedule_raw = raw_config.get("schedule") or {}
|
||||||
if not isinstance(schedule_raw, dict):
|
if not isinstance(schedule_raw, dict):
|
||||||
raise ValueError("'schedule' must be a table in config.toml")
|
raise ValueError("'schedule' must be a table in config.toml")
|
||||||
schedule = Schedule(
|
return Schedule(
|
||||||
cron={
|
cron={
|
||||||
phase: str(schedule_raw[phase])
|
phase: str(schedule_raw[phase])
|
||||||
for phase in SCHEDULED_PHASES
|
for phase in SCHEDULED_PHASES
|
||||||
@@ -695,11 +898,14 @@ def initialize(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_maintenance(raw_config: dict[str, Any]) -> MaintenanceOptions:
|
||||||
|
"""Собрать параметры обслуживания из секции [maintenance] конфига."""
|
||||||
maintenance_raw = raw_config.get("maintenance") or {}
|
maintenance_raw = raw_config.get("maintenance") or {}
|
||||||
if not isinstance(maintenance_raw, dict):
|
if not isinstance(maintenance_raw, dict):
|
||||||
raise ValueError("'maintenance' must be a table in config.toml")
|
raise ValueError("'maintenance' must be a table in config.toml")
|
||||||
defaults = MaintenanceOptions()
|
defaults = MaintenanceOptions()
|
||||||
maintenance = MaintenanceOptions(
|
return MaintenanceOptions(
|
||||||
verify_subset=str(maintenance_raw.get("verify_subset", defaults.verify_subset)),
|
verify_subset=str(maintenance_raw.get("verify_subset", defaults.verify_subset)),
|
||||||
prune_max_unused=str(
|
prune_max_unused=str(
|
||||||
maintenance_raw.get("prune_max_unused", defaults.prune_max_unused)
|
maintenance_raw.get("prune_max_unused", defaults.prune_max_unused)
|
||||||
@@ -709,7 +915,31 @@ def initialize(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
config = Config(host_name=host_name)
|
|
||||||
|
def initialize(
|
||||||
|
config_path: Path,
|
||||||
|
forced_phases: list[str] | None = None,
|
||||||
|
) -> tuple[ApplicationFinder, BackupManager]:
|
||||||
|
try:
|
||||||
|
with config_path.open("rb") as config_file:
|
||||||
|
raw_config = tomllib.load(config_file)
|
||||||
|
except OSError as e:
|
||||||
|
logger.error("Failed to read config file %s: %s", config_path, e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
host_name = str(raw_config.get("host_name", "unknown"))
|
||||||
|
|
||||||
|
roots_raw = raw_config.get("roots") or []
|
||||||
|
if not isinstance(roots_raw, list) or not roots_raw:
|
||||||
|
raise ValueError("roots must be a non-empty list of paths in config.toml")
|
||||||
|
roots = [Path(root) for root in roots_raw]
|
||||||
|
|
||||||
|
storages = build_storages(raw_config)
|
||||||
|
notifiers = build_notifiers(raw_config)
|
||||||
|
schedule = build_schedule(raw_config)
|
||||||
|
maintenance = build_maintenance(raw_config)
|
||||||
|
|
||||||
|
config = Config(host_name=host_name, roots=roots)
|
||||||
app_finder = ApplicationFinder(roots)
|
app_finder = ApplicationFinder(roots)
|
||||||
backup_manager = BackupManager(
|
backup_manager = BackupManager(
|
||||||
config=config,
|
config=config,
|
||||||
@@ -752,7 +982,7 @@ def main() -> None:
|
|||||||
logger.info("Backup process interrupted by user")
|
logger.info("Backup process interrupted by user")
|
||||||
sys.exit(130)
|
sys.exit(130)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected error in backup process: {str(e)}")
|
logger.error("Unexpected error in backup process: %s", e)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,4 +44,4 @@ RCLONE_CONFIG = "{{ rclone_config_file }}"
|
|||||||
[notifier.apprise]
|
[notifier.apprise]
|
||||||
type = "apprise"
|
type = "apprise"
|
||||||
api_url = "{{ apprise_external_url }}"
|
api_url = "{{ apprise_external_url }}"
|
||||||
tag = "server"
|
tag = "backups"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
bifrost_app:
|
bifrost_app:
|
||||||
image: maximhq/bifrost:v1.6.8 # проверь актуальный стабильный тег в releases
|
image: maximhq/bifrost:v1.6.11 # проверь актуальный стабильный тег в releases
|
||||||
container_name: bifrost_app
|
container_name: bifrost_app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
||||||
|
|||||||
@@ -5,21 +5,38 @@
|
|||||||
grace_period 15s
|
grace_period 15s
|
||||||
|
|
||||||
# Admin API слушает только loopback контейнера: на :2019 его видела бы вся
|
# Admin API слушает только loopback контейнера: на :2019 его видела бы вся
|
||||||
|
# web_proxy_network, и любое приложение могло бы переписать маршрутизацию
|
||||||
|
# или снять forward_auth. Метрики вынесены в отдельный site-блок ниже, и
|
||||||
|
# порт у него другой: сайт на :2019 перехватывал бы IPv6-петлю (admin висит
|
||||||
|
# только на IPv4), и `caddy reload` молча ничего не применял бы.
|
||||||
|
admin localhost:2019
|
||||||
|
|
||||||
# Enable metrics in Prometheus format
|
# Enable metrics in Prometheus format
|
||||||
# https://caddyserver.com/docs/metrics
|
# https://caddyserver.com/docs/metrics
|
||||||
metrics
|
metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
|
# Metrics
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Prometheus-метрики для netdata (files/netdata/go.d/prometheus.conf).
|
||||||
|
# Наружу порт не публикуется — доступен только внутри web_proxy_network.
|
||||||
|
http://:2020 {
|
||||||
|
metrics /metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
# Snippets
|
# Snippets
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
|
|
||||||
# Общие настройки всех сайтов: access-лог и HSTS.
|
# Общие настройки всех сайтов: access-лог и HSTS.
|
||||||
# Mode 644 lets read-only consumers (goaccess and ad-hoc host-side tail)
|
#
|
||||||
# read the file; lumberjack would otherwise default to 0600.
|
# Лог общий для всех сайтов, его разбирает GoAccess. Mode 644 нужен, чтобы
|
||||||
(access_log) {
|
# файл читали потребители на хосте (goaccess и ad-hoc tail): lumberjack иначе
|
||||||
log {
|
# ставит 0600.
|
||||||
|
(common) {
|
||||||
|
log {
|
||||||
output file /var/log/caddy/access.log {
|
output file /var/log/caddy/access.log {
|
||||||
mode 644
|
mode 644
|
||||||
roll_size 100mib
|
roll_size 100mib
|
||||||
@@ -29,6 +46,12 @@
|
|||||||
format json
|
format json
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# HSTS: браузер, однажды увидевший заголовок, целый год ходит на это имя только
|
||||||
|
# по https, минуя редирект с http, — закрывает downgrade и SSL-strip.
|
||||||
|
# Без includeSubDomains: политика по каждому имени своя, а под vakhrushev.me
|
||||||
|
# есть поддомены не с этого сервера (например photo.vakhrushev.me).
|
||||||
|
header Strict-Transport-Security "max-age=31536000"
|
||||||
|
}
|
||||||
|
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
# Applications
|
# Applications
|
||||||
@@ -37,7 +60,7 @@
|
|||||||
vakhrushev.me {
|
vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
# Matrix federation delegation: tells other servers/clients that the
|
# Matrix federation delegation: tells other servers/clients that the
|
||||||
# homeserver for vakhrushev.me lives at matrix.vakhrushev.me.
|
# homeserver for vakhrushev.me lives at matrix.vakhrushev.me.
|
||||||
# https://spec.matrix.org/latest/server-server-api/#server-discovery
|
# https://spec.matrix.org/latest/server-server-api/#server-discovery
|
||||||
@@ -65,7 +88,7 @@ vakhrushev.me {
|
|||||||
start.vakhrushev.me {
|
start.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
forward_auth authelia_app:9091 {
|
forward_auth authelia_app:9091 {
|
||||||
uri /api/authz/forward-auth
|
uri /api/authz/forward-auth
|
||||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
||||||
@@ -79,7 +102,7 @@ start.vakhrushev.me {
|
|||||||
matrix.vakhrushev.me {
|
matrix.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
reverse_proxy {
|
reverse_proxy {
|
||||||
to tuwunel_app:6167
|
to tuwunel_app:6167
|
||||||
}
|
}
|
||||||
@@ -88,14 +111,14 @@ matrix.vakhrushev.me {
|
|||||||
auth.vakhrushev.me {
|
auth.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
reverse_proxy authelia_app:9091
|
reverse_proxy authelia_app:9091
|
||||||
}
|
}
|
||||||
|
|
||||||
status.vakhrushev.me {
|
status.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
forward_auth authelia_app:9091 {
|
forward_auth authelia_app:9091 {
|
||||||
uri /api/authz/forward-auth
|
uri /api/authz/forward-auth
|
||||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
||||||
@@ -107,7 +130,7 @@ status.vakhrushev.me, :29999 {
|
|||||||
git.vakhrushev.me {
|
git.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
reverse_proxy {
|
reverse_proxy {
|
||||||
to gitea_app:3000
|
to gitea_app:3000
|
||||||
}
|
}
|
||||||
@@ -116,7 +139,7 @@ git.vakhrushev.me {
|
|||||||
outline.vakhrushev.me {
|
outline.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
reverse_proxy {
|
reverse_proxy {
|
||||||
to outline_app:3000
|
to outline_app:3000
|
||||||
}
|
}
|
||||||
@@ -125,7 +148,7 @@ outline.vakhrushev.me {
|
|||||||
gramps.vakhrushev.me {
|
gramps.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
reverse_proxy {
|
reverse_proxy {
|
||||||
to gramps_app:5000
|
to gramps_app:5000
|
||||||
}
|
}
|
||||||
@@ -134,16 +157,25 @@ gramps.vakhrushev.me {
|
|||||||
miniflux.vakhrushev.me {
|
miniflux.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
# /metrics наружу не отдаём. METRICS_ALLOWED_NETWORKS тут не помогает: за
|
# /metrics наружу не отдаём. METRICS_ALLOWED_NETWORKS тут не помогает: за
|
||||||
|
# прокси Miniflux видит приватный IP контейнера Caddy, а не адрес клиента.
|
||||||
|
# Netdata скрейпит miniflux_app:8080/metrics напрямую по докер-сети, мимо Caddy.
|
||||||
|
handle /metrics {
|
||||||
|
respond 404
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
reverse_proxy {
|
||||||
to miniflux_app:8080
|
to miniflux_app:8080
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wakapi.vakhrushev.me {
|
wakapi.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
reverse_proxy {
|
reverse_proxy {
|
||||||
to wakapi_app:3000
|
to wakapi_app:3000
|
||||||
}
|
}
|
||||||
@@ -152,7 +184,7 @@ wakapi.vakhrushev.me {
|
|||||||
wanderer.vakhrushev.me {
|
wanderer.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
reverse_proxy {
|
reverse_proxy {
|
||||||
to wanderer_web:3000
|
to wanderer_web:3000
|
||||||
}
|
}
|
||||||
@@ -161,7 +193,7 @@ wanderer.vakhrushev.me {
|
|||||||
memos.vakhrushev.me {
|
memos.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
reverse_proxy {
|
reverse_proxy {
|
||||||
to memos_app:5230
|
to memos_app:5230
|
||||||
}
|
}
|
||||||
@@ -172,7 +204,7 @@ memos.vakhrushev.me {
|
|||||||
tududi.vakhrushev.me {
|
tududi.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
reverse_proxy {
|
reverse_proxy {
|
||||||
to tududi_app:3002
|
to tududi_app:3002
|
||||||
}
|
}
|
||||||
@@ -181,7 +213,7 @@ tududi.vakhrushev.me {
|
|||||||
remembos.vakhrushev.me {
|
remembos.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
forward_auth authelia_app:9091 {
|
forward_auth authelia_app:9091 {
|
||||||
uri /api/authz/forward-auth
|
uri /api/authz/forward-auth
|
||||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
||||||
@@ -195,7 +227,7 @@ remembos.vakhrushev.me {
|
|||||||
calibre.vakhrushev.me {
|
calibre.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
reverse_proxy {
|
reverse_proxy {
|
||||||
to calibre_web_app:8083
|
to calibre_web_app:8083
|
||||||
}
|
}
|
||||||
@@ -204,7 +236,7 @@ calibre.vakhrushev.me {
|
|||||||
wanderbase.vakhrushev.me {
|
wanderbase.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
forward_auth authelia_app:9091 {
|
forward_auth authelia_app:9091 {
|
||||||
uri /api/authz/forward-auth
|
uri /api/authz/forward-auth
|
||||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
||||||
@@ -218,7 +250,7 @@ wanderbase.vakhrushev.me {
|
|||||||
rssbridge.vakhrushev.me {
|
rssbridge.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
forward_auth authelia_app:9091 {
|
forward_auth authelia_app:9091 {
|
||||||
uri /api/authz/forward-auth
|
uri /api/authz/forward-auth
|
||||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
||||||
@@ -232,7 +264,7 @@ rssbridge.vakhrushev.me {
|
|||||||
dozzle.vakhrushev.me {
|
dozzle.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
forward_auth authelia_app:9091 {
|
forward_auth authelia_app:9091 {
|
||||||
uri /api/authz/forward-auth
|
uri /api/authz/forward-auth
|
||||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name Remote-Filter
|
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name Remote-Filter
|
||||||
@@ -242,9 +274,26 @@ dozzle.vakhrushev.me {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# GoatCounter — веб-аналитика посещений. Один инстанс обслуживает несколько
|
# GoatCounter — веб-аналитика посещений. Один инстанс обслуживает несколько
|
||||||
|
# сайтов и выбирает нужный сам, по заголовку Host, поэтому здесь один блок на
|
||||||
|
# все имена, а не блок на сайт. Имена рендерятся из goatcounter_sites
|
||||||
|
# (group_vars/all/main.yml) — того же списка, по которому плейбук goatcounter
|
||||||
|
# заводит сами сайты, чтобы конфиг прокси и база не разъезжались.
|
||||||
|
#
|
||||||
|
# Без forward_auth: эндпоинты /count и /count.js обязаны быть публичными, их
|
||||||
|
# дёргает трекер с сайта. Дашборд закрыт собственным логином goatcounter.
|
||||||
|
{{ goatcounter_sites | join(', ') }} {
|
||||||
|
tls anwinged@ya.ru
|
||||||
|
import common
|
||||||
|
|
||||||
|
reverse_proxy {
|
||||||
|
to goatcounter_app:8080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goaccess.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
forward_auth authelia_app:9091 {
|
forward_auth authelia_app:9091 {
|
||||||
uri /api/authz/forward-auth
|
uri /api/authz/forward-auth
|
||||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
||||||
@@ -264,7 +313,7 @@ goaccess.vakhrushev.me {
|
|||||||
bifrost.vakhrushev.me {
|
bifrost.vakhrushev.me {
|
||||||
tls anwinged@ya.ru
|
tls anwinged@ya.ru
|
||||||
import common
|
import common
|
||||||
|
|
||||||
forward_auth authelia_app:9091 {
|
forward_auth authelia_app:9091 {
|
||||||
uri /api/authz/forward-auth
|
uri /api/authz/forward-auth
|
||||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ services:
|
|||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
- "443:443/udp"
|
- "443:443/udp"
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
volumes:
|
volumes:
|
||||||
- "{{ caddy_file_dir }}:/etc/caddy"
|
- "{{ caddy_file_dir }}:/etc/caddy"
|
||||||
- "{{ data_dir }}:/data"
|
- "{{ data_dir }}:/data"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
calibre_web_app:
|
calibre_web_app:
|
||||||
image: lscr.io/linuxserver/calibre-web:0.6.26
|
image: lscr.io/linuxserver/calibre-web:0.6.27
|
||||||
container_name: calibre_web_app
|
container_name: calibre_web_app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -85,6 +85,11 @@
|
|||||||
description: Веб-аналитика логов Caddy
|
description: Веб-аналитика логов Caddy
|
||||||
icon: goaccess.png
|
icon: goaccess.png
|
||||||
siteMonitor: http://goaccess_app:8080
|
siteMonitor: http://goaccess_app:8080
|
||||||
|
- GoatCounter:
|
||||||
|
href: https://gc-main.vakhrushev.me
|
||||||
|
description: Аналитика посещений сайтов
|
||||||
|
icon: mdi-chart-line
|
||||||
|
siteMonitor: http://goatcounter_app:8080
|
||||||
- Bifrost:
|
- Bifrost:
|
||||||
href: https://bifrost.vakhrushev.me
|
href: https://bifrost.vakhrushev.me
|
||||||
description: LLM-шлюз (DeepSeek)
|
description: LLM-шлюз (DeepSeek)
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
dashboard_app:
|
dashboard_app:
|
||||||
image: "{{ homepage_image }}"
|
# See versions: https://github.com/gethomepage/homepage/releases
|
||||||
|
image: ghcr.io/gethomepage/homepage:v2.1.2
|
||||||
container_name: dashboard_app
|
container_name: dashboard_app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
user: "{{ app_owner_uid }}:{{ app_owner_gid }}"
|
user: "{{ app_owner_uid }}:{{ app_owner_gid }}"
|
||||||
@@ -18,7 +19,7 @@ services:
|
|||||||
- "/etc/localtime:/etc/localtime:ro"
|
- "/etc/localtime:/etc/localtime:ro"
|
||||||
- "/etc/timezone:/etc/timezone:ro"
|
- "/etc/timezone:/etc/timezone:ro"
|
||||||
environment:
|
environment:
|
||||||
# homepage 1.x требует явный whitelist Host за реверс-прокси, иначе отдаёт
|
# homepage требует явный whitelist Host за реверс-прокси, иначе отдаёт
|
||||||
# «Host validation failed». Пускаем только адрес из Caddy.
|
# «Host validation failed». Пускаем только адрес из Caddy.
|
||||||
- HOMEPAGE_ALLOWED_HOSTS={{ homepage_domain }}
|
- HOMEPAGE_ALLOWED_HOSTS={{ homepage_domain }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
dozzle_app:
|
dozzle_app:
|
||||||
image: amir20/dozzle:v10.6.14
|
image: amir20/dozzle:v10.7.3
|
||||||
container_name: dozzle_app
|
container_name: dozzle_app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
gitea_app:
|
gitea_app:
|
||||||
image: gitea/gitea:1.27.1
|
image: gitea/gitea:1.27.2
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: gitea_app
|
container_name: gitea_app
|
||||||
ports:
|
ports:
|
||||||
@@ -17,6 +17,9 @@ services:
|
|||||||
- "USER_GID={{ owner_create_result.group }}"
|
- "USER_GID={{ owner_create_result.group }}"
|
||||||
- "GITEA__server__SSH_PORT=2222"
|
- "GITEA__server__SSH_PORT=2222"
|
||||||
|
|
||||||
|
# Ветка по умолчанию в новых репозиториях (у Gitea по умолчанию main)
|
||||||
|
- "GITEA__repository__DEFAULT_BRANCH=master"
|
||||||
|
|
||||||
# Mailer
|
# Mailer
|
||||||
- "GITEA__mailer__ENABLED=true"
|
- "GITEA__mailer__ENABLED=true"
|
||||||
- "GITEA__mailer__PROTOCOL=smtp+starttls"
|
- "GITEA__mailer__PROTOCOL=smtp+starttls"
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# GoatCounter — веб-аналитика (github.com/arp242/goatcounter).
|
||||||
|
# See versions: https://hub.docker.com/r/arp242/goatcounter/tags
|
||||||
|
#
|
||||||
|
# Образ ходит от собственного пользователя goatcounter (uid 1000 из alpine),
|
||||||
|
# поэтому подменяем его на системного владельца смонтированного каталога.
|
||||||
|
# Бинарник собран с osusergo/netgo и записи в /etc/passwd не требует.
|
||||||
|
#
|
||||||
|
# CMD образа — `serve -automigrate`, его не переопределяем: миграции должны
|
||||||
|
# накатываться сами при обновлении версии. Остальные флаги идут через
|
||||||
|
# GOATCOUNTER_<FLAG> (дефис в имени флага -> подчёркивание). Неизвестная
|
||||||
|
# переменная с этим префиксом валит старт, так что новые добавлять только
|
||||||
|
# сверяясь с `goatcounter help serve`.
|
||||||
|
#
|
||||||
|
# Рабочая директория в образе — /home/goatcounter, БД по умолчанию лежит в
|
||||||
|
# ./goatcounter-data/db.sqlite3, то есть внутри смонтированного data.
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
goatcounter_app:
|
||||||
|
image: arp242/goatcounter:2.7.0
|
||||||
|
container_name: goatcounter_app
|
||||||
|
restart: unless-stopped
|
||||||
|
user: "{{ app_owner_uid }}:{{ app_owner_gid }}"
|
||||||
|
networks:
|
||||||
|
- "web_proxy_network"
|
||||||
|
volumes:
|
||||||
|
- "{{ data_dir }}:/home/goatcounter/goatcounter-data"
|
||||||
|
environment:
|
||||||
|
# TLS терминирует Caddy. `http` — не поднимать собственный TLS и не
|
||||||
|
# ходить за ACME-сертификатом; https goatcounter определяет по
|
||||||
|
# X-Forwarded-Proto, IP клиента — по X-Forwarded-For (их шлёт Caddy).
|
||||||
|
- GOATCOUNTER_TLS=http
|
||||||
|
- GOATCOUNTER_LISTEN=:8080
|
||||||
|
|
||||||
|
# Почта (Yandex Cloud Postbox, те же секреты, что у outline/wakapi):
|
||||||
|
# сброс пароля и еженедельные отчёты. STARTTLS — схема smtp://, а не
|
||||||
|
# smtps://. Экранированные логин/пароль приходят из плейбука.
|
||||||
|
- GOATCOUNTER_SMTP=smtp://{{ postbox_user_enc }}:{{ postbox_pass_enc }}@{{ postbox_host }}:{{ postbox_port }}
|
||||||
|
# Без явного -email-from goatcounter зовёт user.Current() и падает,
|
||||||
|
# потому что подменённого uid нет в /etc/passwd образа.
|
||||||
|
- GOATCOUNTER_EMAIL_FROM=goatcounter@vakhrushev.me
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web_proxy_network:
|
||||||
|
external: true
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
gramps_app: &gramps_app
|
gramps_app: &gramps_app
|
||||||
image: ghcr.io/gramps-project/grampsweb:26.7.1
|
image: ghcr.io/gramps-project/grampsweb:26.8.0
|
||||||
container_name: gramps_app
|
container_name: gramps_app
|
||||||
depends_on:
|
depends_on:
|
||||||
- gramps_redis
|
- gramps_redis
|
||||||
|
|||||||
+7
-5
@@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import os
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -18,12 +19,13 @@ def main() -> None:
|
|||||||
if args.keep < 0:
|
if args.keep < 0:
|
||||||
parser.error("--keep value cannot be negative")
|
parser.error("--keep value cannot be negative")
|
||||||
|
|
||||||
if not os.path.isdir(args.directory):
|
directory = Path(args.directory)
|
||||||
|
if not directory.is_dir():
|
||||||
parser.error(f"Directory not found: {args.directory}")
|
parser.error(f"Directory not found: {args.directory}")
|
||||||
|
|
||||||
# Get list of files (exclude subdirectories)
|
# Get list of files (exclude subdirectories)
|
||||||
files = []
|
files = []
|
||||||
with os.scandir(args.directory) as entries:
|
with os.scandir(directory) as entries:
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
if entry.is_file():
|
if entry.is_file():
|
||||||
files.append(entry.name)
|
files.append(entry.name)
|
||||||
@@ -36,9 +38,9 @@ def main() -> None:
|
|||||||
|
|
||||||
# Delete files and print results
|
# Delete files and print results
|
||||||
for filename in to_delete:
|
for filename in to_delete:
|
||||||
filepath = os.path.join(args.directory, filename)
|
filepath = directory / filename
|
||||||
try:
|
try:
|
||||||
os.remove(filepath)
|
filepath.unlink()
|
||||||
print(f"Deleted: {filename}")
|
print(f"Deleted: {filename}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error deleting {filename}: {str(e)}")
|
print(f"Error deleting {filename}: {str(e)}")
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ services:
|
|||||||
- OAUTH2_PROVIDER=oidc
|
- OAUTH2_PROVIDER=oidc
|
||||||
- OAUTH2_REDIRECT_URL=https://miniflux.vakhrushev.me/oauth2/oidc/callback
|
- OAUTH2_REDIRECT_URL=https://miniflux.vakhrushev.me/oauth2/oidc/callback
|
||||||
- OAUTH2_USER_CREATION=1
|
- OAUTH2_USER_CREATION=1
|
||||||
|
# Метрики скрейпит netdata по докер-сети (miniflux_app:8080/metrics).
|
||||||
|
# Сузить METRICS_ALLOWED_NETWORKS нельзя: netdata и Caddy сидят в одной
|
||||||
|
# web_proxy_network, по IP их не различить. Наружу /metrics закрыт в Caddy.
|
||||||
- METRICS_COLLECTOR=1
|
- METRICS_COLLECTOR=1
|
||||||
- METRICS_ALLOWED_NETWORKS=0.0.0.0/0
|
- METRICS_ALLOWED_NETWORKS=0.0.0.0/0
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
netdata:
|
netdata:
|
||||||
image: netdata/netdata:v2.10.4
|
image: netdata/netdata:v2.11.0
|
||||||
container_name: netdata
|
container_name: netdata
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
cap_add:
|
cap_add:
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
update_every: 60
|
update_every: 60
|
||||||
|
|
||||||
|
# max_db_indexes задан внутри каждой джобы намеренно: на верхнем уровне
|
||||||
|
# файла разбирается только confgroup.Default (update_every,
|
||||||
|
# autodetection_retry, priority), остальные ключи yaml проглотит молча.
|
||||||
|
#
|
||||||
|
# Метрики индексов давали 205 индексов × 2 графика на две базы и никакой
|
||||||
|
# пользы на личном сервере. Лимит означает «не собирать метрики индексов для
|
||||||
|
# баз, где индексов больше N», так что 10 отключает их для обеих баз.
|
||||||
|
# Метрики таблиц (seq scans, bloat, hot updates) оставляем: стоковый лимит
|
||||||
|
# max_db_tables = 50 обе базы проходят.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
- name: outline_db
|
- name: outline_db
|
||||||
dsn: 'postgresql://netdata:{{ netdata_postgres_password }}@outline_postgres:5432/outline'
|
dsn: 'postgresql://netdata:{{ netdata_postgres_password }}@outline_postgres:5432/outline'
|
||||||
|
max_db_indexes: 10
|
||||||
|
|
||||||
- name: miniflux_db
|
- name: miniflux_db
|
||||||
dsn: 'postgresql://netdata:{{ netdata_postgres_password }}@miniflux_postgres:5432/miniflux'
|
dsn: 'postgresql://netdata:{{ netdata_postgres_password }}@miniflux_postgres:5432/miniflux'
|
||||||
|
max_db_indexes: 10
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ update_every: 15
|
|||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
- name: caddyproxy
|
- name: caddyproxy
|
||||||
url: http://caddyproxy:2019/metrics
|
url: http://caddyproxy:2020/metrics
|
||||||
selector:
|
selector:
|
||||||
allow:
|
allow:
|
||||||
- "caddy_http_*"
|
- "caddy_http_*"
|
||||||
|
|||||||
@@ -34,6 +34,13 @@
|
|||||||
#| datatype: duration (seconds), default value: 1s
|
#| datatype: duration (seconds), default value: 1s
|
||||||
update every = 10s
|
update every = 10s
|
||||||
|
|
||||||
|
# Третий tier (2 года с шагом 10 часов) на личном сервере не нужен, а
|
||||||
|
# per-metric структуры каждого tier'а лежат в памяти для всех ~12k метрик.
|
||||||
|
# Остаётся 14 дней с шагом 10s и 3 месяца с шагом 10m.
|
||||||
|
#| >>> [db].storage tiers <<<
|
||||||
|
#| datatype: number, default value: 3
|
||||||
|
storage tiers = 2
|
||||||
|
|
||||||
# enable replication = yes
|
# enable replication = yes
|
||||||
# replication period = 1d
|
# replication period = 1d
|
||||||
# replication step = 1h
|
# replication step = 1h
|
||||||
@@ -106,7 +113,13 @@
|
|||||||
# proxy = env
|
# proxy = env
|
||||||
|
|
||||||
[ml]
|
[ml]
|
||||||
# enabled = auto
|
# Anomaly detection никто не смотрит, а тренировка k-means по каждому
|
||||||
|
# измерению каждые 3 часа — основной расход CPU демона (замер: 52 MB
|
||||||
|
# памяти и заметная доля от 18% ядра). Модели в ml.db тоже перестанут расти.
|
||||||
|
#| >>> [ml].enabled <<<
|
||||||
|
#| datatype: yes, no or auto, default value: auto
|
||||||
|
enabled = no
|
||||||
|
|
||||||
# training window = 6h
|
# training window = 6h
|
||||||
# min training window = 15m
|
# min training window = 15m
|
||||||
# max training vectors = 1440
|
# max training vectors = 1440
|
||||||
@@ -221,10 +234,34 @@
|
|||||||
#| datatype: yes or no, default value: yes
|
#| datatype: yes or no, default value: yes
|
||||||
otel = no
|
otel = no
|
||||||
|
|
||||||
|
# Появился в 2.11: приёмник NetFlow/IPFIX/sFlow, стоковый netflow.yaml
|
||||||
|
# идёт с enabled: true и слушателями на UDP 2055/6343. Экспортировать
|
||||||
|
# потоки некому, а свой четырёхуровневый журнал и MMDB-базы плагин
|
||||||
|
# держит в памяти — на нашей памяти это лишний процесс.
|
||||||
|
#| >>> [plugins].netflow <<<
|
||||||
|
#| datatype: yes or no, default value: yes
|
||||||
|
netflow = no
|
||||||
|
|
||||||
#| >>> [plugins].statsd <<<
|
#| >>> [plugins].statsd <<<
|
||||||
#| datatype: yes or no, default value: yes
|
#| datatype: yes or no, default value: yes
|
||||||
statsd = no
|
statsd = no
|
||||||
|
|
||||||
|
# Периодически обходит все сокеты хоста ради карты соединений, которой мы
|
||||||
|
# не пользуемся: ~5 MiB и постоянный расход CPU.
|
||||||
|
#| >>> [plugins].network-viewer <<<
|
||||||
|
#| datatype: yes or no, default value: yes
|
||||||
|
network-viewer = no
|
||||||
|
|
||||||
|
# Даёт extfrag и zswap (33 метрики) — не смотрим.
|
||||||
|
#| >>> [plugins].debugfs <<<
|
||||||
|
#| datatype: yes or no, default value: yes
|
||||||
|
debugfs = no
|
||||||
|
|
||||||
|
# Latency-пробы диска, требуют явной настройки, у нас не настроены.
|
||||||
|
#| >>> [plugins].ioping <<<
|
||||||
|
#| datatype: yes or no, default value: yes
|
||||||
|
ioping = no
|
||||||
|
|
||||||
# idlejitter = yes
|
# idlejitter = yes
|
||||||
# netdata pulse = yes
|
# netdata pulse = yes
|
||||||
# profile = no
|
# profile = no
|
||||||
@@ -354,7 +391,13 @@
|
|||||||
# command options =
|
# command options =
|
||||||
|
|
||||||
[plugin:apps]
|
[plugin:apps]
|
||||||
# update every = 10s
|
# 86 групп процессов × 24 контекста = 4039 метрик, четверть всех. Метрики
|
||||||
|
# оставляем (нужен разрез по хостовым процессам вне контейнеров), но обход
|
||||||
|
# /proc по всем процессам делаем втрое реже — это и есть основная цена.
|
||||||
|
#| >>> [plugin:apps].update every <<<
|
||||||
|
#| datatype: duration (seconds), default value: 10s
|
||||||
|
update every = 30s
|
||||||
|
|
||||||
# command options =
|
# command options =
|
||||||
|
|
||||||
[plugin:systemd-journal]
|
[plugin:systemd-journal]
|
||||||
@@ -375,7 +418,19 @@
|
|||||||
|
|
||||||
[plugin:proc:/proc/net/dev]
|
[plugin:proc:/proc/net/dev]
|
||||||
# compressed packets for all interfaces = no
|
# compressed packets for all interfaces = no
|
||||||
# disable by default interfaces matching = lo fireqos* *-ifb fwpr* fwbr* fwln* ifb4*
|
|
||||||
|
# К стоковому списку добавлены veth*, br-* и docker0: это 50 интерфейсов
|
||||||
|
# × 9 контекстов = 462 графика, из них 43 — безымянные veth докера.
|
||||||
|
#
|
||||||
|
# Внимание, побочный эффект: проверка имени в proc_net_dev.c идёт до
|
||||||
|
# netdev_rename_this_device(), поэтому отфильтрованный veth уже не может
|
||||||
|
# быть переименован в контейнерный чарт. У нас смаплен ровно один
|
||||||
|
# (cgroup_caddyproxy.net_eth0), его мы теряем; внешний трафик виден в
|
||||||
|
# system.net по eth0. Если понадобится разрез по контейнерам — убрать
|
||||||
|
# veth* из списка и разбираться, почему маппинг не сработал для остальных.
|
||||||
|
#| >>> [plugin:proc:/proc/net/dev].disable by default interfaces matching <<<
|
||||||
|
#| datatype: simple pattern, default value: lo fireqos* *-ifb fwpr* fwbr* fwln* ifb4*
|
||||||
|
disable by default interfaces matching = lo fireqos* *-ifb fwpr* fwbr* fwln* ifb4* veth* br-* docker0
|
||||||
|
|
||||||
[plugin:proc:/proc/stat]
|
[plugin:proc:/proc/stat]
|
||||||
# cpu utilization = yes
|
# cpu utilization = yes
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
tududi_app:
|
tududi_app:
|
||||||
image: chrisvel/tududi:1.3.1
|
image: chrisvel/tududi:1.4.1
|
||||||
container_name: tududi_app
|
container_name: tududi_app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
@@ -73,6 +73,10 @@ services:
|
|||||||
# поэтому шлём deepseek/deepseek-v4-pro сразу: Bifrost резолвит провайдера по
|
# поэтому шлём deepseek/deepseek-v4-pro сразу: Bifrost резолвит провайдера по
|
||||||
# префиксу, и подменять модель governance-правилом больше не нужно.
|
# префиксу, и подменять модель governance-правилом больше не нужно.
|
||||||
# Пара LLM_* имеет приоритет над устаревшей OPENAI_*, значения те же.
|
# Пара LLM_* имеет приоритет над устаревшей OPENAI_*, значения те же.
|
||||||
|
# С 1.4.1 запросы уходят с response_format=json_schema и strict=true; если
|
||||||
|
# DeepSeek за Bifrost ответит 400 со словом response_format, tududi сам
|
||||||
|
# повторит запрос без него — на другую формулировку ошибки fallback уже не
|
||||||
|
# сработает. Лимиты токенов (LLM_MAX_TOKENS_*) оставлены дефолтными.
|
||||||
- LLM_API_KEY={{ tududi_openai_api_key }}
|
- LLM_API_KEY={{ tududi_openai_api_key }}
|
||||||
- LLM_BASE_URL=http://bifrost_app:8080/openai
|
- LLM_BASE_URL=http://bifrost_app:8080/openai
|
||||||
- LLM_MODEL=deepseek/deepseek-v4-pro
|
- LLM_MODEL=deepseek/deepseek-v4-pro
|
||||||
|
|||||||
@@ -4,12 +4,16 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
tuwunel_app:
|
tuwunel_app:
|
||||||
image: jevolk/tuwunel:v1.8.3
|
image: jevolk/tuwunel:v1.9.0
|
||||||
container_name: tuwunel_app
|
container_name: tuwunel_app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
||||||
networks:
|
networks:
|
||||||
- "web_proxy_network"
|
web_proxy_network:
|
||||||
|
# Алиас без подчёркивания: по нему в homeserver ходят соседние
|
||||||
|
# контейнеры (apprise шлёт уведомления в matrix-комнату).
|
||||||
|
aliases:
|
||||||
|
- "tuwunel"
|
||||||
volumes:
|
volumes:
|
||||||
- "{{ data_dir }}:/var/lib/tuwunel"
|
- "{{ data_dir }}:/var/lib/tuwunel"
|
||||||
environment:
|
environment:
|
||||||
@@ -21,7 +25,6 @@ services:
|
|||||||
|
|
||||||
TUWUNEL_ALLOW_REGISTRATION: "false"
|
TUWUNEL_ALLOW_REGISTRATION: "false"
|
||||||
TUWUNEL_ALLOW_FEDERATION: "true"
|
TUWUNEL_ALLOW_FEDERATION: "true"
|
||||||
TUWUNEL_ALLOW_CHECK_FOR_UPDATES: "false"
|
|
||||||
TUWUNEL_TRUSTED_SERVERS: '["matrix.org"]'
|
TUWUNEL_TRUSTED_SERVERS: '["matrix.org"]'
|
||||||
|
|
||||||
# Well-known delegation values returned to clients/servers that query tuwunel directly.
|
# Well-known delegation values returned to clients/servers that query tuwunel directly.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# See versions: https://github.com/gramps-project/gramps-web/pkgs/container/grampsweb
|
# See versions: https://github.com/muety/wakapi/releases
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
wakapi_app:
|
wakapi_app:
|
||||||
image: ghcr.io/muety/wakapi:2.17.5
|
image: ghcr.io/muety/wakapi:2.17.6
|
||||||
container_name: wakapi_app
|
container_name: wakapi_app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
user: '{{ owner_create_result.uid }}:{{ owner_create_result.group }}'
|
user: '{{ owner_create_result.uid }}:{{ owner_create_result.group }}'
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ services:
|
|||||||
|
|
||||||
wanderer_db:
|
wanderer_db:
|
||||||
container_name: wanderer_db
|
container_name: wanderer_db
|
||||||
image: "flomp/wanderer-db:{{ wanderer_version }}"
|
image: flomp/wanderer-db:v0.18.3
|
||||||
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
||||||
depends_on:
|
depends_on:
|
||||||
wanderer_search:
|
wanderer_search:
|
||||||
@@ -53,7 +53,7 @@ services:
|
|||||||
|
|
||||||
wanderer_web:
|
wanderer_web:
|
||||||
container_name: wanderer_web
|
container_name: wanderer_web
|
||||||
image: "flomp/wanderer-web:{{ wanderer_version }}"
|
image: flomp/wanderer-web:v0.18.3
|
||||||
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
user: "{{ owner_create_result.uid }}:{{ owner_create_result.group }}"
|
||||||
depends_on:
|
depends_on:
|
||||||
wanderer_search:
|
wanderer_search:
|
||||||
|
|||||||
@@ -18,3 +18,20 @@ apprise_external_url: "http://127.0.0.1:{{ apprise_external_port }}"
|
|||||||
# tools (goaccess and so on). Lives under the system log path so it is
|
# tools (goaccess and so on). Lives under the system log path so it is
|
||||||
# decoupled from any individual application's data directory.
|
# decoupled from any individual application's data directory.
|
||||||
caddy_logs_dir: "/var/log/caddy"
|
caddy_logs_dir: "/var/log/caddy"
|
||||||
|
|
||||||
|
# Сайты (арендаторы) внутри одного инстанса goatcounter. Домен здесь — не
|
||||||
|
# отслеживаемый сайт, а ключ маршрутизации: goatcounter выбирает сайт по
|
||||||
|
# заголовку Host, другого способа у него нет. Список общий, потому что его
|
||||||
|
# читают два плейбука: goatcounter заводит по нему сайты, caddyproxy рендерит
|
||||||
|
# из него имена в Caddyfile.
|
||||||
|
#
|
||||||
|
# Первый в списке — родительский: ему принадлежит единственный пользователь,
|
||||||
|
# остальные заводятся с -link на него и делят с ним логин (goatcounter ищет
|
||||||
|
# пользователя по parent). Порядок менять нельзя: он определяет, кто родитель.
|
||||||
|
#
|
||||||
|
# Косметическое поле «Your site» (link_domain) — то, на какой домен ведут
|
||||||
|
# ссылки с путей в дашборде — CLI не умеет, оно ставится разово руками в
|
||||||
|
# настройках сайта. В комментариях ниже — чему оно должно соответствовать.
|
||||||
|
goatcounter_sites:
|
||||||
|
- "gc-main.vakhrushev.me" # vakhrushev.me
|
||||||
|
- "gc-photo.vakhrushev.me" # photo.vakhrushev.me
|
||||||
|
|||||||
+245
-231
@@ -1,232 +1,246 @@
|
|||||||
$ANSIBLE_VAULT;1.1;AES256
|
$ANSIBLE_VAULT;1.1;AES256
|
||||||
34623131346139653664373036613532666565303039363565373735636363386366313233303238
|
62666365313535383334366166313961303933373163323236626565303038396662363239376438
|
||||||
6630613730376536656663636234326332353236363439370a333166386530346666303531633666
|
3738323565643363323865663630303330306530303263350a393336646463663563666362643362
|
||||||
38323538623838623237636339613138383936383232346566333532383863623738333136663364
|
61313337363936313363316635376163646633383032613265363936396536386437306139396461
|
||||||
3162386333383463330a643265386164613439643231313765623331376531306466313062363966
|
3963656162316562620a393737383636343965656433346466316333613331303435323732363239
|
||||||
65616337386232326433623763373464363135353761623430613036623761323965303333623139
|
39313834373536396237333264303930393762383333633363333265363039373061353334613530
|
||||||
65633262623261656131393333646361393733323366343833386330663461663435373561626636
|
33633161373631363061383564633465633962383835633237623938343865303363633461313830
|
||||||
31303437336431633630623835623633316538333339393866646661313762663134353364373234
|
36383865663932666562333733653935626166663066363233303962643364343739663463396364
|
||||||
37326263396362343137626537333936366135663438373034326165376537663233356466356438
|
34353034656130343739336532393334306261366331353030306161636437363464336536386638
|
||||||
35653637373864303864626433336634366132653262643731396636323963626363643666666635
|
62653362633363393838366564313165346262653432663563366362616364396633323563643239
|
||||||
37393339353331666635626531343835346632363634366437663564376339386230386533323732
|
35333232396239383233656231333264663938373364313730623364393436626361653736613939
|
||||||
63623232613137663266386161326333306530336634313666616437623931653232323431326562
|
64393833353765346633363334656631343064656333383664373039393966346332393134303030
|
||||||
34323565653761636165303132313964373031626237626232623031656430356535613831373834
|
36373436343766323038303438386436376666393239643636366330336336333761356436376234
|
||||||
65356263303364316465356138623737626339333530393661313638333931336331396336613437
|
33383266663030643931626662383636333637616539313366326539383139616334363939626161
|
||||||
65323132666132363063333435383032663330653965373334643461303466353235313130363633
|
34346263643031363236313334623162343236636665383262393666306366383064343965373639
|
||||||
31633134373732306535333433613839356362396261623364396339363630626636396632656233
|
34323062656633386661343332306664633361396233656238613436636530323839653838376563
|
||||||
62333737633332383736626363366463663166306265646365373637333465393530386665363364
|
33336664643365363930366636656138616236626639663564613132653465636462633364646166
|
||||||
37313064653434393761383066623638303162376263343130306439623166666366326639616336
|
36393966326365373530663839653737656438646662373730333666376137653337303834383032
|
||||||
65386364633865626566373636393537653134333733666531616266636638653836383732383461
|
33363636376435623064646566323065383466623138323938303630353330663132636431333830
|
||||||
65363132346563643439666431323934306330336233393363666164353839633534333132333130
|
30313434333262616138343634303438386236326330636161336462363333386561643462666439
|
||||||
34303861653662636534373565306537366235663431333532653465643866363335383762333538
|
35343837653934613338313537336232656564663337666262656361383736363939383433623938
|
||||||
62373332363265396639626137646334363933366633326462313336336265313739326131343165
|
32633965633931643836346362356334666230663734643130656566353937636331303633323934
|
||||||
39336333376137393665316435363433613661393063306638306330313333633430333034373038
|
62656634643263333566343739666634663061386137373539383736613864656230373566666162
|
||||||
62626265333433326665316634613562616631643161643930363632666262616431323536363066
|
32616363633534313933613533373265623366303766626232623966623531373930613831643034
|
||||||
64353932623332626261326564396538663866623638326432303436356263643162336166636137
|
64306236353164643138666564616464383638636263326531633866363732653664313364386661
|
||||||
39383432346365626663373932663134646566323061636235633336386236663639633663326265
|
30633536623336353532363261623938353232396336343662643965353839333761376463363631
|
||||||
34353031343465313231336162396232303436366363646131613936303333376463363230636638
|
66353862646131343961363363303432336431633761313633386235383435336563383333613830
|
||||||
30356137313035643330343536316138306633323635363066356333623365376430373231366439
|
62333864373362663538326363656531643733356232643632396631323636376164653333316137
|
||||||
63633237633866656262313233633565666433626563393730376465623033353738383662323438
|
39626336333139323733653535663132333030323938303166396336356136653231366332613039
|
||||||
39356265383161373933636133393534333934373664313962626637366466636132396563613435
|
30346237353532623065303033326534663034333938663365303264323061613964313139316538
|
||||||
32653964663433646332616161616436363636363236353262613861643939376239636139626162
|
32633537346165616136623431653766383535393230303064383433626634386331356434323561
|
||||||
31313638343765323736353961626238666536623330353537396464363935303439653139306265
|
31646137623339373662626631356337616666303131633138366433356163373766616531383138
|
||||||
30323531626133393735633833326562333230323735623465326566393832303234646536626534
|
33366361323935363163363731636631643135636332316439303335616661373039346433356138
|
||||||
30386263636537613366343431363262393433663064613631613665353139316639366334366133
|
34613061646138626637316262623632303339653633346161353836623330313432396333323464
|
||||||
39353539306662636165346466303132633231666135313734653665303165356533653739396234
|
30346232646661643839663835373439323031623462386432623332316535623764343039333039
|
||||||
35356338326631376631376136643930616164396466303733653266613766346334613338623133
|
62313031663731306539303634666461376133643138363235383531366239643331313238623666
|
||||||
39326234363161363132643630323662313536386161633033623833613639643030366533353038
|
62656631333030626337333765646339643561396537333462643166616165636261323663333963
|
||||||
31396430366236376232316462616138303832343862306361663061656263626466333036613366
|
32656336353837323231643561306134323065396131343965373534656530376237616134343034
|
||||||
36386234666363396161333830636430383039356638313830666332663931383937623433613432
|
61313430376531333261323530346430303163313864333033663536386561633939383030666430
|
||||||
35386136333830616239623161613538386238383130346435376230373838643339663066316130
|
30633864316363383631386636656331323234356438643132633637623637646536316532373639
|
||||||
63663938626662396336396562373537303635343337346637626662353530623639306237313062
|
38313535663930666335333239623935393232633432343561663938343335356565626266353630
|
||||||
34313664363838313133346533326137636239346564646661353566646163386663326232643766
|
35666134383431653163356230663038336563366433623562333333313733383536353635613835
|
||||||
34613931613063646339313764383562363930386165636134653932393833303633643266666366
|
35366539306437623336623230343666366438333334613765393930313234383365623466393934
|
||||||
61346238353636356564376165386339326662643634326431323766313831353039653565386437
|
32633931363164626464396534333431666630316365323265376161313138313232646665323532
|
||||||
62343338656466343562643930656430666230303863323238373534633136386336356266666166
|
39353563393235353632323930326537643162386430383461306136306339306535363565633662
|
||||||
36373536376263386434613533626130333261333633343439366266386336613665363530376432
|
35336434343938353538363563373363393464623633313638346236633662623837633263633166
|
||||||
36383735313566666461323363383530636333616332643637653364373834643337346537626630
|
64666630346235613664356433653436616166643762653431376666306364623237363363613333
|
||||||
61346331313237363832623535323936623163653233333562653035306336333831623338356231
|
31316533363966633362366266613863353136353662313636393234303361396531333063623863
|
||||||
35313438626632666465353535383434343630306233306238366439623335343536346234616134
|
30343534643835613462353739613730633331643837383836326236636235623965356333343961
|
||||||
39353938313536613531336134656262363830323561313730383331323165353930623066613064
|
32396135393564653537613731316133633665653061386664343233396537383332643163356639
|
||||||
63646465626233646163303964376532383338633131383065313236616537313064303265383733
|
36653134303361656438666635396339323332346133623439613031613764666665303335613738
|
||||||
66663132646130386638306335653531323531306437393364663135306466643365663235643461
|
64303537616531643237633835613937663739313466346463323037636432373235363038393461
|
||||||
61633665623532386466623635376331336662666134373366346536363134313037393939346634
|
66363162323562646264323133306538633430303538356231326436313432323931623135306262
|
||||||
64616332666630616235383136383536646233353663663634343762346236353533613832336436
|
30666132383131343262326531303931343539646130356332333964613938363033623731633463
|
||||||
33643235663366626438373932633866626366323035363835633036393266613163383933343466
|
36363635366165663064653934623235323930336538643634623233383535323930386664313530
|
||||||
36303630653731363637616337323030646164306264636237613032303738643938663032663365
|
65383264623736373261343331353339313361383539653262373338316163353063663038656465
|
||||||
65333334303030313562636166653766306265636238653161356166616138376665613338396564
|
35356133353465303137373039363535336232326233353937643465323539343263363739663531
|
||||||
36383732636331653934636666393861623965386666646630393261303365363839343838383266
|
66666566373835336433353761336338336533353463386230306665616638393864646264373563
|
||||||
31616162643662616332363737373865353366343030623839663330343861313032363161306262
|
33313339646237626264366263613036373931666366326663313964633738393164633534623839
|
||||||
34316161303432393365376431386165656464393632646534613930643966663330656532363162
|
39613132396661393761343230633135323732613836383661353665663831623766616639343331
|
||||||
63366331323634356337366433643262343531376638666536383766343963663839656438353465
|
62623862343737643930373432323563663264366234646533353835353264333333623734313435
|
||||||
33653132383336616166313330306233373630393530303161306634663830656433653739303466
|
62316666306437643835386235303139616534303932363263373863653633613464363431356132
|
||||||
63373436613663643263343262643536366138333439313538656136633932383065643135303237
|
32653661373365623364316663393262303538633039366138316630643031626565316335613436
|
||||||
34636637373863326539653836333430306537353436323832373131343435636233393762376430
|
36356137653139303334616631396634376262396133633161306233336162643632383635333333
|
||||||
34633338396138376632653863633935643462346438653066623633323431336230616564613236
|
65373633666637343666366463323765656633303732323631393064353739376430303934333232
|
||||||
39376639383837303430323939653435656437303635343335363965313734316134303166613238
|
63393831386363343233363732326234643039396637383132336162326336363033393436383931
|
||||||
62616238306363613334313362666365633335336637393332633564653035303034333866623634
|
37323763326461643837393139613165346136316663646464343338613061323936303030356466
|
||||||
30613432636539383031633266383166303639326133663065323236316561373461626637333138
|
39636138376632653063643963306334396339666665323234336135396561393166643236663733
|
||||||
61376134366335646634623062333633306666663666303464386430626639373663633938396331
|
32316364623831626134306163393239303564313466636663393166336662613739323864346637
|
||||||
35643138303561316531363165326230653461356133346531326466323966353330343438306630
|
38393064313362663935666632386339393561663735656639373633623933306635363330333736
|
||||||
36613466336638623934373232316635633032343561323535346233633261376235613263316233
|
62633966653138633231346331393533633333623835666463666330333835316631666238396334
|
||||||
34663635353732336234623935323631613831336639636338656332633832343238626361636364
|
33343232396135383834383564636235663634346336373536663539653861633862653332633962
|
||||||
65636632633534343735343737666237616331353737636332333034393630303133376136663332
|
36343136633532323565316431396630313837363138343833316363366165383337343930626361
|
||||||
31613532653566393164663738303138616233623031646665323266313863623761646465356135
|
66633535643839396463343264313336663430326137356465663166353833636134356434613839
|
||||||
62303864336538306661383931346364303961366330396639336461306537313465663239623766
|
39313439323431633966636533336530346138353935663433333661353634316632646333363434
|
||||||
36366262383662613432356333653765383432316333326535353062376539366434326638336361
|
33326165653033663965333934363633353235363931626463383565393331366566663666633733
|
||||||
30653136633362323066643732383636626530363163376230306338353066373732376133313064
|
32643863653661633137313263646630373465343434666234663236393832313761383535613363
|
||||||
63356532623331353538623561333364623037396262323864643061333131343061653330343632
|
65346335626535383335643361653531366139363637333838376562326138386361623562616165
|
||||||
63386262626431376231326131396164613463383034346362396439393133373666316637346134
|
37333730663134666466666130396662616138663962366630323330616333323539623532613733
|
||||||
64343538336332633131333530323462373864633665343439303162366335343234383362346434
|
62373531653564636662316334646338346337616433333533346539363066366338333233616337
|
||||||
36356662656530353932616339326233336637396664636464396462376636646432313665323235
|
33666130323330356364326633623165366465326331383361323630363330633833363536373930
|
||||||
66376635396436653137383039333932356665623932303433393264656632643632323165386430
|
64393464383533383335623263613136336632363132373663326263303162636331666164646234
|
||||||
34613534336632613230343532346364663338613932626333663432396461353638646237613330
|
32316133313438616563376230636537636334346637356339336533333264643461666439643238
|
||||||
63376265313536633838663031326133363563353266313464656666356538646333643361646239
|
66376461643136336665643366643363366365373636613537636462653737303131643661336364
|
||||||
63346163646164636539333566393865303534646534396366363262353430613463326637336664
|
34323261313632663064346263336463363932646261613232643030383038613137303532343766
|
||||||
35373433653336613934316333633565383863623562346663396433646633366130613339656466
|
36663366396338306534633062616436333133316438666261396136323535613436356337613339
|
||||||
62346339396438656334333934383432326661643236646537643564346538626533613565303235
|
33626565663137666265316533306231643435376465333566643366633638346631663435306534
|
||||||
64623038356162303338663037646137366136306232396535306565356132353830663566643465
|
37373836366438336365326265653433306334313630373737343630363534313131386332633134
|
||||||
30373233323930333862633133303663646661323566336238363435376138383066383136393938
|
65373862643431313031626236356565626133633663613564613238313962323934633437383930
|
||||||
37356361393334313735306238376134373930313834376566313236376665623339363039626463
|
65393765313934393438663338343239353732316130366466323638656439613234643036356265
|
||||||
37306364623863356335373234366266383331326635653730633535363464326431323030303534
|
36646631313362663833343934363930373536333531626161623038396331373065633965643735
|
||||||
39303231633664353834656162346531663933666133393462383135313764346564646162666563
|
37666162343930383133306139306131356232636530306234616464633736636538346431613066
|
||||||
30633539643432383934363339656465646166326261623465336136333236343030346238653234
|
31396630653338623733663262663335623466613430316663316435653233653837623131396462
|
||||||
32376363613864383162666138346565633239336361636435353362306163396133653432306262
|
31633531356239326538663737313437303735363565316263323131383362306235363835333830
|
||||||
39663464623636663930643162353537393834653537646536313330376136363230613063383362
|
64333438633837356633633435643830633938396637326662666137333934613531316538626633
|
||||||
38656632613834623362353366653766313735343665343661623836313339626433306338306366
|
62346431626662663538303431336234613164383536313363303932643062316538343332386165
|
||||||
31376666303734666461663462316339663130623763373034316463643537653839363762373431
|
62653937663135353638383363333634383031383362663039646133666634356661313537303566
|
||||||
64373064386564656438393030363135653030626665656337363366353130373237376331663231
|
32316361613261313830363532336238616265393661343133373231633964323535656530336136
|
||||||
35663463383530653163323565623234316431336663363136356238383064653739343661653066
|
38393163626133336235346162633165396333633339316462306433656262623736376332336330
|
||||||
32613535633631343563313262353063393733363230333236363665643238663338306639623932
|
63363733363263636336633432366633393533373566653836663165626162363735363963316532
|
||||||
35376136616464366563386138636564636138623939663535393565653638303731343561313466
|
66376238303538386537363233376431393833303263383733303261386237623136613435613530
|
||||||
64303838646538306233653437313663393730653139626166666433396434316239316662316537
|
34383738646330336636363336346264323132393962666334613134313635393165656236623234
|
||||||
36333037613761313263396631386636313662653062326630383038656463366430653236333131
|
35333535353562656463376135353562386532366235363865343038316561663933373339356336
|
||||||
33323330373833663763303930353339643363373266633734373732343632383138616235386166
|
35393830623430323365613863393932633535376436663666356562386461336239366665333436
|
||||||
65343465666264366139376466386366393230376264313935623961373966333964376362356133
|
32626534373830386134666233346333393064343933323830336462666136386538306463313531
|
||||||
65386564343332313037323336633465636439356363616261346435633536633331353935653364
|
65303263366530356138363162333833376631626631316435663631333438363833366234646131
|
||||||
64616332326239653730343530656466353664373935363062633132326436333538393961346535
|
33633236326533643139343766366131313834366663336361623563376465646338623537343934
|
||||||
37303533393036396662326263626432623135336530383939653833636634636135373431336638
|
35653665363665343264333237313663663431386530333230643832643537373464393836356365
|
||||||
39353539333436356363623665633533393538393039653230633737653231616363343636353631
|
31306465623539653361323233613136373464643334366466616561356336363161306566353033
|
||||||
39303361316565626564313237353738393533646666363362303866386462313633393531393034
|
39313735653062643361313538353464393566303435303839643061326631346434316537346561
|
||||||
33633131376230616361323530313231636334653164383833326164393965333532363736373130
|
35326536316233613035366532316132396531646561633834346530386337636532313233326433
|
||||||
35646162636538326461363163396362383366623635666162366136623363326266613632613966
|
31383364313331623433323364356566373433323034373531653736653866636539383639383163
|
||||||
37663566313338633765366333663331336132636261636536353163306333346431373733396634
|
61663362303435363539343331373931313065343662353964366566656537633664383239343430
|
||||||
61393536613634396530306239663365396563636563393038303831396135393831656430663363
|
62623865623665616638623937343361376133306564383461336136313035333731313837393762
|
||||||
30346564663765636361613265393862636664646330393962643637353536363063363834663763
|
34393936373865653761363139393530386135626538353031333736646235366165623535323338
|
||||||
62303836663963613438313161613963376262323337663166633530626264343034386563633266
|
33313964643732633032643037323064323461313930623836623438366261633033666439396535
|
||||||
38633664636362333935663666383135393530643063666162313762316230303230336665383438
|
31326131303738383237643566306561393635383831303433323963646133346534396663616338
|
||||||
30386664393239633931383230663464373235343964626534393663666236333933613965336632
|
33343139663162636134346637333330333637643063373330376134373533663864663338653264
|
||||||
65633230356565623162396161313465636532333436326430623531646563323434363734623462
|
31313530623933653934313530663139313439373662613130636135666630346261623933393833
|
||||||
31313638353237623230326361656533336661396533623566343062303164313566643538626536
|
30306166303938336439333833373434346139366162623736663662353931626639663139336462
|
||||||
32366564353863343936376362666666303838663162383139663039353864383961396437306364
|
39353336653163316136656238643031626563326238353064356430313533376536376635373262
|
||||||
65666666353335333733333538653336323763326437336338623466346435373239343533316365
|
62343534643661326435663031653634663130323035303063656163303436386131643435383733
|
||||||
35393463656533373237363537663234303231396565663230383463336665656562633162366365
|
30653061623439376262376338366166353836353535343939646134393335623630383866306539
|
||||||
33303337343464336262616332393465333963653164353063316630396432333264316136323465
|
30343862636131393863643831656338346334313930356164393135303166643265656138366266
|
||||||
39376634616632373536623563626561303933346366636239613233303039643866643038343861
|
64646636306339656662616239356666633330666237653166393663653565343533316337316237
|
||||||
63613230396632383031316336313862323166373939316362663236333564636437316338626661
|
36663930323730653236653235626437366338333263333135633634346434383530386331333166
|
||||||
63656138666137366439343364346164343435326339366530313662626664383665643931303036
|
38346563393934303830633932323564343166363936313035623034666436303831643265646433
|
||||||
38383761383964646231383431616265363932623630346162653738663531623236646138663165
|
36616362633230666132313937626136653530623865663037383239313939393739656663373166
|
||||||
30323738386664633235353562323638636466353965366638626164656535366661333338393534
|
37393233653533646664623865333865623862333263383635643238343538333137383461623565
|
||||||
31343039616562346232363831626136643131353834313161616531613633326637393864393831
|
65663036356666323137666536323435376565653234376530393166393939666663373361343530
|
||||||
34383432363230313463623536663432356333386236326335373937613062646634346439613961
|
36363466323162383930316637326333393937333232363537643065383231396236323936643364
|
||||||
33353364623363653136646262633963363265636263646466613833623330326434303634333161
|
64653061323033363135353436333036326636306639653139363064386366653562383539356131
|
||||||
65636365663133306537626232333939316531353533316231373962313233636130336333323665
|
31366432656634323730336665353132323862636339313036643234396339646165313764303734
|
||||||
35626363363432393032656434636635663263643664376134393239633538386438646232326234
|
34303737353466306537386633623261393537613735313032643033383434363233393061386538
|
||||||
33623766643863643262636338656131363861396130373438656537653061343134393436653336
|
35313834396533633735313237316433643835323465306431336331306638383165306234303366
|
||||||
36376163363133313165663436333563373335653761643236616664373231653632646539396132
|
64626239636439643538323734343966366535616437383632363631363731636463653133313266
|
||||||
63336335363631613732373834313566316336373939313639306331383063366536306138646639
|
62653036653761333166643432653563613264616362316463326538653536383763363237653238
|
||||||
31396337646465613237663733643762323339626262353862333030313661653231366461303338
|
37303564386266653239333636303034633538383232623630366232626638373664343336336462
|
||||||
31313463353934356239323464343036316538643130316463646532633234336239663162636265
|
38393038633737376562626131343131653236376432653230343833393366366566363334343263
|
||||||
66376137316235613130656266386661646336613037656563396338393134383835363232383130
|
39323062663565373132343131303530353464626133643731666239316363373437313335626232
|
||||||
39373835313533373261613139326130626530383039383936633830373931306262623431396236
|
39393931333033643265646535663935363831306163303966623164303264626161303434646538
|
||||||
64326337303236646136336466333764613032663537613935373538373939363965363734613339
|
37656232396336336635643335303165323361366562303137333831343832653763323937373061
|
||||||
32316332396433333031356365366638313332313966616231353836316162356664353665386334
|
35326263356263363636383861356339623966323564366430663632316664373536613462623838
|
||||||
35343032336664313366383435623632396538633264616637623363316461393561323966326133
|
33356462363038623633393337303161393536646665663339653234336137393139336132353235
|
||||||
39306639363264656633613462336564653066633263333132623139643132386130623138333532
|
37613138333065363731623535346138643833343663656162376139376638303231653537326131
|
||||||
63626433666566373037333035356231373238363039656562363131333634376138386662343230
|
31376334383564623331623733613365643161393165376563313235663930323037343161343866
|
||||||
32613762306235313361396262633666376663323033613339353836363534326232353236613537
|
37383336303138393064663265363939343535376433326433353165356564656132366565613839
|
||||||
38656139396265303030373539363766366439326332393130323631346233663436646362363237
|
65643365343038316139653665396334613131396238663162666365393339353539613634383935
|
||||||
35303266383763663332656235383836633733333965303066643466376465356533383439643766
|
64333535386661333363383437653864636434323832303932666364376333663633326434383064
|
||||||
65646663393632326136653332656164393834343766656333346364343332363032616636623637
|
31333464323331303939353732353763643562383538356363613739353135326630613334323037
|
||||||
34313535356133653830343138653366653664353032663537383862316262303733613264343235
|
35363335346464373064363231393435306534323431616365623765646362373738653738326232
|
||||||
34313939643639356261653334376533333330303632636266303031343031653432643763383462
|
30323563326466373766363935636535626632623138343537333038333964396262323831353038
|
||||||
30646633346239663763646437623866346461386535643338323162363039383531343330383432
|
34383665346430396361306437613339366437626266626231626239363362613065396137343962
|
||||||
62613564643966383463376133376538306539646463333338653136346665303836396538336237
|
36333463313130353165373365316563623330356638356233383333653661663462383161303136
|
||||||
32373565653061376461303938383139336437666339333762396561613661356464326266303166
|
36633038386563376335376236646537316563633739393235663930656131613030666133363430
|
||||||
65613532326438616434396461313130313761613063386633373462646462653065646162656264
|
62386263373338653161376636623061386234356632626636366232313639353138636332616630
|
||||||
36313035623662383965643336643830643033323136373066343733363036666332366537336630
|
61353435353239633630323163336133353462356263316130626638656238653264663361323135
|
||||||
31613532616135383365376635323037303361393561376166343638336663373532656165346333
|
37633038373433313863393965633661613231396536613835653361373837356232333833633236
|
||||||
34383961346431363966356339323030373565313266346436326433633934363964313866323938
|
32313439633632363465366337303739303137323839366261653836343332373230643264666535
|
||||||
32636538323838313064303330633436323062313637333764626437393737356337343661616336
|
35313865613632636437323462316639323531326432633334333432323266386435366366333838
|
||||||
37323230353931323339366136633334316261646165343430343834326339303238613534363138
|
38633830313731326638306634323565366265343130333339373032643832346166613737663430
|
||||||
65303932346435616563653961373561623761313534363732353633353330623030643033363730
|
62336434316264376361353830633562313065653433393363373734666134323263653537653830
|
||||||
61383961326632393362633932353065613636313932633966616534383065613636363734306466
|
62643838333065376234646166666332376462373536373234666163636562333865636134313766
|
||||||
66363933626561663038376334656534643566316134633338336161653532636239633536313933
|
39376237626137316366306366393038616631346161316263343237666430323966396331353065
|
||||||
66616162386538346432323761636134376638336337623539646262393535613263366536366464
|
30363437356262643034643330303534333331323464623662616534653932646130653466653332
|
||||||
66643566333162643261653136393331343431646233643130336562336366633664353734373636
|
62363030343833316133666164383037323936336232356266326261383964396364633233643737
|
||||||
30633762333366613764346436333737623266343631363330356366663632366565396564613431
|
32663635613939363265323936336263663465646666333263626139636162373462373064613362
|
||||||
31363439393463623334653666663636613366633437333237653363396561386233326334613831
|
65636266616665633033626337363136303066343232353537346635633431336266333535353131
|
||||||
33643936366566373435653161346566636639633039363939376366366336373437346664656335
|
66316266643335313033386432373962343332326238356430323435363238373334653036393839
|
||||||
65333633363336366263366433396661336262373931366639333362393939343031376166393362
|
65643337663034326232646464383934323661326432373061376662633765613861613466653937
|
||||||
32633330303861666231393137356639363164303736306631656338373938666630316261393766
|
30363065316561666235383332623364353938303734323537643064343333393864313339656161
|
||||||
64663935366265386463646163313336363066353164336631396365386465393933303566313333
|
36663264363363353430663930653965613761353630373034336166633864386234653831343636
|
||||||
62336139666337353637646566623537613038636265636537313330666631623864333662663531
|
32383934633666646235366534303130306436636665326338626566323362616664383162333837
|
||||||
64353563303165323066376133316266636433633863363039323535623862346663636464353031
|
39636539313737653933386261326262633963396364626430633631653735303934353436633532
|
||||||
34333132383661303739333861326230346234626339633336383136333135336464656435633135
|
63373866326666323565663435613530616130323035613438313634393264353266333838333131
|
||||||
32396139663737353062623533356638346563653032643465633333373761376362613434656432
|
33626236366633656365656534643538623431323635313133323630333365653132313932653863
|
||||||
31623363376461326535386164616665363065623933306435643061666630363862393138663165
|
32396139306437353839303933663432376434633462376335386264373934316339616663646130
|
||||||
63646263326665623066336361643836646437353432653963363165656364373531613362316632
|
34363130333061626433663636363032366237346136633966356139663462393836623562626532
|
||||||
63633233333764626433353433303835663962393963656536323734643966333930633162636361
|
35633930306639326639383632363561306164336535303139316162306431313236633036333964
|
||||||
62626437326437303766373331656232363763326565373535656266383365666336363466396438
|
33353063653561343764653235613330613331656338343337623431326564333761633731636531
|
||||||
34356266336132343830336431326638383933393163373130653735306534303364373933646434
|
38616539356239663861363237333733386635353532333039636631663639316434623633323634
|
||||||
37396235386162353232646639613531643332303038336330653234633437353464343131633961
|
64663139356536643864373836303335316464636163303464376264633630633738313064663630
|
||||||
31666433303238633934646531333736383831376234326530613335346530363962643935656662
|
30626363636265383064373764376538626235316138303831306262643932316332323733356535
|
||||||
35393066346136353534393934356466333036323666366263363561353861636538356363376362
|
39306564393236653530393864346261333434663232393765626265626166343864623931636133
|
||||||
31376662376566626439386165383934623135613162613339356535643762626662363263653330
|
37636532333762306339666466633939376432326230306437386337666630393530336163306335
|
||||||
33643166656634616534343133363030363930616231643535303163633436623463656364316631
|
62353037303265656534646262623836633030326636356566363362353161353764636639663538
|
||||||
62333562636533306562376665326338383539313632666435636332353363613534316162373261
|
65373831613431373665383565623361313235333036376239313039616433626539626432373363
|
||||||
31626535363334306630316463316264353633643738643939313932376461383833653232366438
|
37396232626663636165363863346533396132376531306139616664363261323339333239373639
|
||||||
31323834613565633237353331386331323063653132343639366333613633623062643932346462
|
30383136383232323837396665373235363433613864373437623834633164613266383165653966
|
||||||
64326533393438633530666266343435336165346131383330366264613138626630393939333131
|
35336337656361633435363531346134303435633262623465383938626639383332363736356630
|
||||||
32343566613730313236626165623935343535656137306263306635653966613165356263653733
|
32316637613233303531386562386636636361323163633732633732356630636165663266363532
|
||||||
62643036643430613231663332626431386432653632333538323465333562326339313538373332
|
66626433633938663262323464653335396662373634653466653964343065336139313539646536
|
||||||
34663165653833356366353065393161313230666662666638666432663132303334363965346635
|
39663134333530343537323764646566323635303361323032613633626536646236616465343365
|
||||||
32386236646366396530303635666539336538313530306462383335653738316364346637623361
|
39646336626632633839626461303633656633613939393139346639343935333966383564393263
|
||||||
32646561613539303636613535643966356537353731373034343231373437383432373638333564
|
32623639366134636563306463643230656337303666393634386432386332333137316534333361
|
||||||
65643932663566343761313064303265653963643264336230316163366638653537663464663937
|
34646631663137653139343131343236323930373036366263666162306230376664313331636238
|
||||||
36303333393833306235636565643831376135373737373562333964306537383332343162656563
|
31666465343633323931343063376530363838613934623537643338643236323237386163633131
|
||||||
31653462363734313531663738336430336630313537333039373366666539313362393134333465
|
32663263613265646662393339346463353166393630306665343534373238666235663538366637
|
||||||
35636130383936613166316536663735616335373863333564383938323633386262363534306230
|
34306664313462366462643732376365343464393833633035353863663933393566633065663531
|
||||||
35386530346539393434643362663961643430333532343032633832366137316362646234663437
|
64386231363931323363653665613732313438323630373430613634636437666565363734643730
|
||||||
61363265396264636664663665633038623736646130303430353964653136633131323266396439
|
61383265383938643336333666643434356166643831383765343736373436653430376561666338
|
||||||
63623931353330653662383261363431383866663764333037303463306637306262656630393261
|
36373033643665343431303464663838373766306466636534396634323232623434653736316531
|
||||||
63393765633137323033316465376464623634363537396135623334633234356437653430643062
|
32383365396330666333396364663561336663333965323866316131356331363633383437643230
|
||||||
36323766633162343862643538623364353839333861653862643864666234323965343336643762
|
33653337386534316537613830393337336630373436343130623535343938616133376138663236
|
||||||
34666639303061326136613433623130333234303562396235623639353136613032363139636266
|
39363133663735303037396631333062656566393461643337663161646361303739343161653731
|
||||||
66643732346339653764343965646263366639306263633837336232346465396535383530326337
|
38383636623238323261383038326433303532353039383035393666366265653334346630663839
|
||||||
64396462633330336633633766323932316637376663656632633037356133633331303166663436
|
65343336666462366432383764646137623336636662633731386534636463656364383465626531
|
||||||
38333434376534656562346638653130626462313564383366353537653531326335383138353330
|
30663631663932303161366332663964643662626631313732333764353763363764623234323030
|
||||||
35656361343039343933316435646232396363326663643133373734616336313237386365346238
|
36356330313261626134326363346133393638383834623830636165316631613432666261613032
|
||||||
31643732633630643066323935336432346363346636383239626365373232316230343239643365
|
31393231333837623266363130633331646531636136343163376166653261343464623133323635
|
||||||
36383135643830386539336431643635323062346465383930623531663834663437636433376336
|
61333636313962383961393831366262373837316531363166313964316538363634386430343739
|
||||||
32393035633534366639316339396632646538346661366564373366653766646263663137373561
|
64343363373165663934643833613064353264373539666139343734393663643735393736393065
|
||||||
37306630336566326433643939376134333030653837343638646133346262323935653565363664
|
37393262366564616561663532373137336262393439393031383439333336636535323834393138
|
||||||
30653036623638666535623964353738343461303834346535376235646566343761663438626135
|
63613734326238613762616366653533636130326631383631336433343635656230343266343365
|
||||||
64643731383535363065333331346263333464623732306566326666643230623231336231643136
|
35613033356130656632303339643236643330646339333037363830646431313032366363643336
|
||||||
66666664373334396139356332343564386332626632386332323434326533383063346532623035
|
36306332376436356639323738306638333866373532363632373861613863633133323862393036
|
||||||
62303765306132616630323264363666343162313435343737303631346635393636363337316138
|
61653637323635636163386139353038373039623937353533623336653565633730336330353936
|
||||||
30653966313563613533616362626263653261666464323664316132666462383334633937646330
|
61343431633635346339626536376464306663656262333531373438333363666237333832316238
|
||||||
38646563646130393238663537343739643566346130616461366361643961643730353465366233
|
38613336646263633932636233393734626338336433326232326164613436363232616334383230
|
||||||
63333163353532363564323032363162323235353262396338363533306131303934383562656561
|
31616333646361346638613065343065393736313735646234663333633534636331356663323463
|
||||||
39366133376134353234653830623061363832643564353936343737303932386638396232303332
|
35383261396563623363353432356431303539363666653261326363646464373865666531636131
|
||||||
64373736383538343662613033633564313532323663666333353632383431386239643631333739
|
61386634383337656539643338353066373932313134613266373366366630656236313664366164
|
||||||
38303536343233393763386330323730653861333831393464306537316363623936663631616236
|
64633163386431366232383835633235323130376462313832353736343262663737353739303934
|
||||||
62633932323831356361616130653266346439643837313339353338663330663136656133383332
|
62323436326631616134343831376561633965373561303039386266313935336164386636316139
|
||||||
32646330626161313961633035323432313461643261626130636139646333623861663165626132
|
37653238666631306161396537343462376638653263343134633234366236633966373438383639
|
||||||
30303330343632303239303938636662643037646431653662613839663865643638663966663064
|
35356532363534363133353264346661663437656162353739656265373130363931356239366339
|
||||||
32616330643030656532626635346138386431383963376262663966313164643236616164356663
|
38356538616137353737323132366130396335363262646635383763356534306465343463646632
|
||||||
37396432663463663039623334303235376462353930363939366430346664383431343062386561
|
64613431666435333330623035646463666532323131356538343034373936333762303931616463
|
||||||
31336338356537636632663661356561343838373666383862333036386137333336376638613431
|
35346231393338353065326430313032653966666362653438623333323466303431313135323733
|
||||||
32623234343065363762383035343462613766626134326165626636336335306239313363356362
|
39306466666532383933346164323931366337313937623730313162623261373666613363323233
|
||||||
63663138613632396135323762633964653831663437613763623162336339396639656434323065
|
63313161316266323561343363353038326439343661613439336132636361316563373766643164
|
||||||
346439636339643734636633613463343231
|
31646536643439643536303864386530633439646135396336383332646266613461623837656431
|
||||||
|
30356266356664666436323764316363636330633636646630633536626266343262383731633436
|
||||||
|
62313964646436366130313730623332613530386538363533633263623866663735386531353861
|
||||||
|
61363361663739333637353436336435306162643638383134653338656665326531356264343137
|
||||||
|
61303262373531646634313239393361323835376233633964613937313966643132653264663265
|
||||||
|
30386334353335343237353734303132643534316130376437386663363937366432303866346231
|
||||||
|
32616639383462363333373237343663383332343364373465646633383034383034376534396162
|
||||||
|
61643739323330353130333361396335363634373131653835363036333563613239383966343663
|
||||||
|
35306362353435663762656461666539393139616264306666393066333764663039623764323937
|
||||||
|
63643539386161353237343230333832326137666539613539346366353239623265626562386562
|
||||||
|
39623361376430633461336562613232393631623733326164643637343632633538666437626235
|
||||||
|
34393638363839653136646562336131376335303661373339326661353935623164316232653034
|
||||||
|
37306134323561616631313838336364343463303631343765393130623163346365636664333061
|
||||||
|
33356133626535383765636463326138646266363061326430393934343330356136323664326630
|
||||||
|
66356161356332383739353531366432383966366137653866336666633631636532
|
||||||
|
|||||||
+7
-2
@@ -14,13 +14,18 @@ pre-commit:
|
|||||||
run: "uv run ruff format {staged_files}"
|
run: "uv run ruff format {staged_files}"
|
||||||
stage_fixed: true
|
stage_fixed: true
|
||||||
|
|
||||||
|
- name: "fix python"
|
||||||
|
glob: "**/*.py"
|
||||||
|
run: "uv run ruff check --fix {staged_files}"
|
||||||
|
stage_fixed: true
|
||||||
|
|
||||||
- name: "check python"
|
- name: "check python"
|
||||||
glob: "**/*.py"
|
glob: "**/*.py"
|
||||||
run: "uv run ruff check {staged_files}"
|
run: "uv run ruff check {staged_files}"
|
||||||
|
|
||||||
- name: "mypy"
|
- name: "pyrefly"
|
||||||
glob: "**/*.py"
|
glob: "**/*.py"
|
||||||
run: "uv run mypy {staged_files}"
|
run: "uv run pyrefly check {staged_files}"
|
||||||
|
|
||||||
- name: "yamllint"
|
- name: "yamllint"
|
||||||
glob: "**/*.{yml,yaml}"
|
glob: "**/*.{yml,yaml}"
|
||||||
|
|||||||
@@ -70,3 +70,6 @@
|
|||||||
|
|
||||||
- name: "Configure goaccess"
|
- name: "Configure goaccess"
|
||||||
ansible.builtin.import_playbook: playbook-goaccess.yml
|
ansible.builtin.import_playbook: playbook-goaccess.yml
|
||||||
|
|
||||||
|
- name: "Configure goatcounter"
|
||||||
|
ansible.builtin.import_playbook: playbook-goatcounter.yml
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
- name: 'Configure system'
|
- name: 'Configure system'
|
||||||
ansible.builtin.import_playbook: playbook-system.yml
|
ansible.builtin.import_playbook: playbook-system.yml
|
||||||
|
|
||||||
|
- name: 'Configure firewall'
|
||||||
|
ansible.builtin.import_playbook: playbook-ufw.yml
|
||||||
|
|
||||||
- name: 'Configure docker'
|
- name: 'Configure docker'
|
||||||
ansible.builtin.import_playbook: playbook-docker.yml
|
ansible.builtin.import_playbook: playbook-docker.yml
|
||||||
|
|
||||||
|
|||||||
+30
-3
@@ -11,6 +11,15 @@
|
|||||||
config_dir: "{{ (base_dir, 'config') | path_join }}"
|
config_dir: "{{ (base_dir, 'config') | path_join }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
# Apprise молча не доставит уведомление, если id комнаты записан без
|
||||||
|
# префикса: значение без '!' и '#' плагин matrix трактует как алиас.
|
||||||
|
- name: "Check matrix room id format"
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- notifications_matrix_room is match('^[!#]')
|
||||||
|
fail_msg: "notifications_matrix_room: нужен '!room_id:server' или '#alias:server'"
|
||||||
|
quiet: true
|
||||||
|
|
||||||
- name: "Create user and environment"
|
- name: "Create user and environment"
|
||||||
ansible.builtin.import_role:
|
ansible.builtin.import_role:
|
||||||
name: owner
|
name: owner
|
||||||
@@ -18,7 +27,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
@@ -31,15 +39,23 @@
|
|||||||
- "{{ base_dir }}"
|
- "{{ base_dir }}"
|
||||||
- "{{ config_dir }}"
|
- "{{ config_dir }}"
|
||||||
|
|
||||||
|
# Бандл назывался server, пока источник уведомлений был один. Файл остался
|
||||||
|
# бы на сервере с прежними секретами, поэтому удаляем его явно.
|
||||||
|
- name: "Remove renamed apprise config"
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ config_dir }}/server.cfg"
|
||||||
|
state: "absent"
|
||||||
|
|
||||||
- name: "Copy apprise config"
|
- name: "Copy apprise config"
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: "./files/{{ app_name }}/server.template.cfg"
|
src: "./files/{{ app_name }}/backups.template.cfg"
|
||||||
dest: "{{ config_dir }}/server.cfg"
|
dest: "{{ config_dir }}/backups.cfg"
|
||||||
owner: "{{ app_user }}"
|
owner: "{{ app_user }}"
|
||||||
group: "{{ app_user }}"
|
group: "{{ app_user }}"
|
||||||
mode: "0640"
|
mode: "0640"
|
||||||
# Файл содержит секреты из vault — без no_log их печатает --diff.
|
# Файл содержит секреты из vault — без no_log их печатает --diff.
|
||||||
no_log: true
|
no_log: true
|
||||||
|
notify: "Restart application"
|
||||||
|
|
||||||
- name: "Copy docker compose file"
|
- name: "Copy docker compose file"
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
@@ -56,3 +72,14 @@
|
|||||||
remove_orphans: true
|
remove_orphans: true
|
||||||
tags:
|
tags:
|
||||||
- run-app
|
- run-app
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
# Приложение читает конфиги только при старте, а `state: present` не
|
||||||
|
# пересоздаёт контейнер, если docker-compose.yml не изменился, — поэтому
|
||||||
|
# правку конфига применяет только явный рестарт.
|
||||||
|
- name: "Restart application"
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ base_dir }}"
|
||||||
|
state: "restarted"
|
||||||
|
tags:
|
||||||
|
- run-app
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create internal application directories"
|
- name: "Create internal application directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
+10
-16
@@ -23,7 +23,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create internal application directories"
|
- name: "Create internal application directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
@@ -74,7 +73,7 @@
|
|||||||
owner: "{{ app_user }}"
|
owner: "{{ app_user }}"
|
||||||
group: "{{ app_user }}"
|
group: "{{ app_user }}"
|
||||||
mode: "0640"
|
mode: "0640"
|
||||||
notify: "Restart application"
|
notify: "Reload caddy configuration"
|
||||||
|
|
||||||
- name: "Copy docker compose file"
|
- name: "Copy docker compose file"
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
@@ -92,21 +91,16 @@
|
|||||||
tags:
|
tags:
|
||||||
- run-app
|
- run-app
|
||||||
|
|
||||||
# - name: "Reload caddy"
|
|
||||||
# community.docker.docker_compose_v2_exec:
|
|
||||||
# project_src: '{{ base_dir }}'
|
|
||||||
# service: "{{ service_name }}"
|
|
||||||
# command: caddy reload --config /etc/caddy/Caddyfile
|
|
||||||
# tags:
|
|
||||||
# - run-app
|
|
||||||
|
|
||||||
handlers:
|
handlers:
|
||||||
# Приложение читает конфиги только при старте, а `state: present` не
|
# `state: present` не пересоздаёт контейнер, если docker-compose.yml не
|
||||||
# пересоздаёт контейнер, если docker-compose.yml не изменился, — поэтому
|
# изменился, — правку Caddyfile надо применять отдельно. Через прокси идёт
|
||||||
# правку конфига применяет только явный рестарт.
|
# весь трафик, поэтому не рестарт, а `caddy reload`: конфиг применяется без
|
||||||
- name: "Restart application"
|
# разрыва соединений. Конфиг указываем явно — без --config caddy берёт файл
|
||||||
community.docker.docker_compose_v2:
|
# из своего рабочего каталога и молча перезагружает не то.
|
||||||
|
- name: "Reload caddy configuration"
|
||||||
|
community.docker.docker_compose_v2_exec:
|
||||||
project_src: "{{ base_dir }}"
|
project_src: "{{ base_dir }}"
|
||||||
state: "restarted"
|
service: "{{ service_name }}"
|
||||||
|
command: "caddy reload --config /etc/caddy/Caddyfile"
|
||||||
tags:
|
tags:
|
||||||
- run-app
|
- run-app
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
+13
-4
@@ -10,11 +10,9 @@
|
|||||||
base_dir: "{{ (application_dir, app_name) | path_join }}"
|
base_dir: "{{ (application_dir, app_name) | path_join }}"
|
||||||
config_dir: "{{ (base_dir, 'config') | path_join }}"
|
config_dir: "{{ (base_dir, 'config') | path_join }}"
|
||||||
|
|
||||||
# gethomepage.dev — проверь актуальный тег перед обновлением.
|
|
||||||
homepage_image: "ghcr.io/gethomepage/homepage:v1.13.2"
|
|
||||||
# Внутренний порт контейнера; наружу не публикуется, ходим только через Caddy.
|
# Внутренний порт контейнера; наружу не публикуется, ходим только через Caddy.
|
||||||
homepage_port: 3000
|
homepage_port: 3000
|
||||||
# Домен, на котором Caddy отдаёт дашборд. homepage 1.x требует явный whitelist
|
# Домен, на котором Caddy отдаёт дашборд. homepage требует явный whitelist
|
||||||
# Host за реверс-прокси, иначе «Host validation failed».
|
# Host за реверс-прокси, иначе «Host validation failed».
|
||||||
homepage_domain: "start.vakhrushev.me"
|
homepage_domain: "start.vakhrushev.me"
|
||||||
|
|
||||||
@@ -32,7 +30,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
@@ -53,6 +50,7 @@
|
|||||||
group: "{{ app_user }}"
|
group: "{{ app_user }}"
|
||||||
mode: "0640"
|
mode: "0640"
|
||||||
loop: "{{ config_files }}"
|
loop: "{{ config_files }}"
|
||||||
|
notify: "Restart application"
|
||||||
|
|
||||||
- name: "Copy docker compose file"
|
- name: "Copy docker compose file"
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
@@ -70,3 +68,14 @@
|
|||||||
pull: "always"
|
pull: "always"
|
||||||
tags:
|
tags:
|
||||||
- run-app
|
- run-app
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
# Приложение читает конфиги только при старте, а `state: present` не
|
||||||
|
# пересоздаёт контейнер, если docker-compose.yml не изменился, — поэтому
|
||||||
|
# правку конфига применяет только явный рестарт.
|
||||||
|
- name: "Restart application"
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ base_dir }}"
|
||||||
|
state: "restarted"
|
||||||
|
tags:
|
||||||
|
- run-app
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create internal application directories"
|
- name: "Create internal application directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@@ -19,6 +19,10 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
|
# Группа docker = root на хосте, поэтому её не выдаём никому, кроме
|
||||||
|
# приложений, чей backup.sh делает docker compose exec: оркестратор
|
||||||
|
# запускает такие скрипты через su от имени этого пользователя.
|
||||||
|
# См. docs/backlog/backup-dump-without-docker-group.md.
|
||||||
owner_extra_groups: ["docker"]
|
owner_extra_groups: ["docker"]
|
||||||
|
|
||||||
- name: "Create internal application directories"
|
- name: "Create internal application directories"
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create internal application directories"
|
- name: "Create internal application directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
@@ -83,6 +82,9 @@
|
|||||||
- { name: "Dockerfile", mode: "0640" }
|
- { name: "Dockerfile", mode: "0640" }
|
||||||
- { name: "entrypoint.sh", mode: "0750" }
|
- { name: "entrypoint.sh", mode: "0750" }
|
||||||
|
|
||||||
|
# Handler рестарта здесь не нужен: конфиг в контейнер не монтируется, а
|
||||||
|
# правку Dockerfile/entrypoint.sh применяет `build: always` — пересобранный
|
||||||
|
# образ получает новый id, и compose пересоздаёт по нему контейнер (проверено).
|
||||||
- name: "Run application with docker compose"
|
- name: "Run application with docker compose"
|
||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "{{ base_dir }}"
|
project_src: "{{ base_dir }}"
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
- name: "Configure goatcounter application"
|
||||||
|
hosts: all
|
||||||
|
|
||||||
|
# goatcounter_sites приходит из group_vars/all/main.yml (его же читает
|
||||||
|
# caddyproxy), goatcounter_admin_email/password — из vault-файла secrets.yml.
|
||||||
|
vars:
|
||||||
|
app_name: "goatcounter"
|
||||||
|
# Родительский сайт: владелец пользователя, к нему линкуются остальные.
|
||||||
|
goatcounter_parent_vhost: "{{ goatcounter_sites[0] }}"
|
||||||
|
app_user: "{{ app_name }}"
|
||||||
|
app_owner_uid: 1110
|
||||||
|
app_owner_gid: 1110
|
||||||
|
|
||||||
|
base_dir: "{{ (application_dir, app_name) | path_join }}"
|
||||||
|
data_dir: "{{ (base_dir, 'data') | path_join }}"
|
||||||
|
|
||||||
|
# Логин и пароль Postbox goatcounter принимает только внутри SMTP-URL, в
|
||||||
|
# userinfo, — значит их надо процентно экранировать. Фильтр urlencode
|
||||||
|
# оставляет `/` как есть (safe='/'), поэтому дожимаем его вручную.
|
||||||
|
postbox_user_enc: "{{ postbox_user | urlencode | replace('/', '%2F') }}"
|
||||||
|
postbox_pass_enc: "{{ postbox_pass | urlencode | replace('/', '%2F') }}"
|
||||||
|
|
||||||
|
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 }}"
|
||||||
|
|
||||||
|
- name: "Create application internal directories"
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: "directory"
|
||||||
|
owner: "{{ app_user }}"
|
||||||
|
group: "{{ app_user }}"
|
||||||
|
mode: "0750"
|
||||||
|
loop:
|
||||||
|
- "{{ base_dir }}"
|
||||||
|
- "{{ data_dir }}"
|
||||||
|
|
||||||
|
# В data лежит только SQLite-база; бэкапим её restic'ом напрямую, без
|
||||||
|
# промежуточного дампа — как у tududi. Записи редкие (пачка просмотров раз
|
||||||
|
# в 10 секунд), ночной прогон почти наверняка застаёт базу в покое, а
|
||||||
|
# retention страхует от редкого несогласованного снимка.
|
||||||
|
- name: "Create backup targets file"
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ base_dir }}/backup-targets"
|
||||||
|
content: |
|
||||||
|
{{ data_dir }}
|
||||||
|
owner: "{{ app_user }}"
|
||||||
|
group: "{{ app_user }}"
|
||||||
|
mode: "0640"
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
# SMTP-пароль стоит прямым значением в environment: — без no_log его
|
||||||
|
# печатает --diff. Файловых секретов goatcounter не умеет.
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: "Run application with docker compose"
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ base_dir }}"
|
||||||
|
state: "present"
|
||||||
|
remove_orphans: true
|
||||||
|
tags:
|
||||||
|
- run-app
|
||||||
|
|
||||||
|
# Сайты заводятся по разу, в том же прогоне, что поднимает контейнер: пока
|
||||||
|
# сайта нет в базе, goatcounter отдаёт на его домене открытый мастер
|
||||||
|
# регистрации, и это окно надо закрыть сразу.
|
||||||
|
#
|
||||||
|
# `db show site` различает три состояния по rc и stderr: rc=0 — сайт есть;
|
||||||
|
# rc=1 + "nothing found" — база готова, сайта нет; rc=1 + "doesn't exist" —
|
||||||
|
# serve ещё не накатил миграции. Последнее и есть гонка со стартом
|
||||||
|
# контейнера, поэтому ждём в until, пока ответ не станет осмысленным.
|
||||||
|
- name: "Check which goatcounter sites exist"
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ app_name }}_app"
|
||||||
|
argv:
|
||||||
|
- "goatcounter"
|
||||||
|
- "db"
|
||||||
|
- "show"
|
||||||
|
- "site"
|
||||||
|
- "-find"
|
||||||
|
- "{{ item }}"
|
||||||
|
loop: "{{ goatcounter_sites }}"
|
||||||
|
register: "goatcounter_sites_state"
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
until: "goatcounter_sites_state.rc == 0 or 'nothing found' in goatcounter_sites_state.stderr"
|
||||||
|
retries: 10
|
||||||
|
delay: 3
|
||||||
|
tags:
|
||||||
|
- run-app
|
||||||
|
|
||||||
|
# Пароль нужен только когда заводим родительский сайт: дочерние идут с
|
||||||
|
# -link и своих пользователей не получают.
|
||||||
|
- name: "Check credentials for the first goatcounter user are set"
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- "goatcounter_admin_email | default('') is match('[^@]+@[^@]+')"
|
||||||
|
- "goatcounter_admin_password | default('') | length >= 8"
|
||||||
|
fail_msg: >-
|
||||||
|
Сайт {{ goatcounter_parent_vhost }} ещё не заведён, а
|
||||||
|
goatcounter_admin_email / goatcounter_admin_password не заданы.
|
||||||
|
Пропишите их в group_vars/all/secrets.yml и повторите прогон.
|
||||||
|
when: "goatcounter_sites_state.results[0].rc != 0"
|
||||||
|
tags:
|
||||||
|
- run-app
|
||||||
|
|
||||||
|
# Вход дальше — своей парой email/пароль на https://{{ goatcounter_parent_vhost }};
|
||||||
|
# Authelia тут не участвует, эндпоинт /count обязан быть публичным.
|
||||||
|
- name: "Create the parent goatcounter site and its user"
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ app_name }}_app"
|
||||||
|
argv:
|
||||||
|
- "goatcounter"
|
||||||
|
- "db"
|
||||||
|
- "create"
|
||||||
|
- "site"
|
||||||
|
- "-vhost"
|
||||||
|
- "{{ goatcounter_parent_vhost }}"
|
||||||
|
- "-user.email"
|
||||||
|
- "{{ goatcounter_admin_email }}"
|
||||||
|
- "-password"
|
||||||
|
- "{{ goatcounter_admin_password }}"
|
||||||
|
when: "goatcounter_sites_state.results[0].rc != 0"
|
||||||
|
changed_when: true
|
||||||
|
# Пароль стоит прямым значением в argv — без no_log его печатает --diff.
|
||||||
|
no_log: true
|
||||||
|
tags:
|
||||||
|
- run-app
|
||||||
|
|
||||||
|
# -link отдаёт дочернему сайту пользователей родителя и ставит его в
|
||||||
|
# переключалку сайтов в шапке. Данные при этом раздельные: всё, что видно
|
||||||
|
# в дашборде, скоупится по site_id.
|
||||||
|
- name: "Create linked goatcounter sites"
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ app_name }}_app"
|
||||||
|
argv:
|
||||||
|
- "goatcounter"
|
||||||
|
- "db"
|
||||||
|
- "create"
|
||||||
|
- "site"
|
||||||
|
- "-vhost"
|
||||||
|
- "{{ item.item }}"
|
||||||
|
- "-link"
|
||||||
|
- "{{ goatcounter_parent_vhost }}"
|
||||||
|
loop: "{{ goatcounter_sites_state.results[1:] }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.item }}"
|
||||||
|
when: "item.rc != 0"
|
||||||
|
changed_when: true
|
||||||
|
tags:
|
||||||
|
- run-app
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
+23
-1
@@ -21,6 +21,10 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
|
# Группа docker = root на хосте, поэтому её не выдаём никому, кроме
|
||||||
|
# приложений, чей backup.sh делает docker compose exec: оркестратор
|
||||||
|
# запускает такие скрипты через su от имени этого пользователя.
|
||||||
|
# См. docs/backlog/backup-dump-without-docker-group.md.
|
||||||
owner_extra_groups: ["docker"]
|
owner_extra_groups: ["docker"]
|
||||||
|
|
||||||
- name: "Create internal directories"
|
- name: "Create internal directories"
|
||||||
@@ -37,9 +41,13 @@
|
|||||||
- "{{ postgres_data_dir }}"
|
- "{{ postgres_data_dir }}"
|
||||||
- "{{ postgres_backups_dir }}"
|
- "{{ postgres_backups_dir }}"
|
||||||
|
|
||||||
|
# notify на import_role наследуется всеми задачами роли: рестарт нужен,
|
||||||
|
# если изменился любой файл секретов — miniflux читает их через *_FILE
|
||||||
|
# только при старте.
|
||||||
- name: "Copy secrets"
|
- name: "Copy secrets"
|
||||||
ansible.builtin.import_role:
|
ansible.builtin.import_role:
|
||||||
name: secrets
|
name: secrets
|
||||||
|
notify: "Restart application"
|
||||||
vars:
|
vars:
|
||||||
secrets_dest: "{{ secrets_dir }}"
|
secrets_dest: "{{ secrets_dir }}"
|
||||||
secrets_user: "{{ app_user }}"
|
secrets_user: "{{ app_user }}"
|
||||||
@@ -72,7 +80,21 @@
|
|||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "{{ base_dir }}"
|
project_src: "{{ base_dir }}"
|
||||||
state: "present"
|
state: "present"
|
||||||
recreate: "always"
|
|
||||||
remove_orphans: true
|
remove_orphans: true
|
||||||
tags:
|
tags:
|
||||||
- run-app
|
- run-app
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
# Приложение читает секреты только при старте, а `state: present` не
|
||||||
|
# пересоздаёт контейнер, если docker-compose.yml не изменился, — поэтому
|
||||||
|
# правку применяет только явный рестарт.
|
||||||
|
- name: "Restart application"
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ base_dir }}"
|
||||||
|
# Только сам miniflux: базу дёргать незачем, пароль postgres читается
|
||||||
|
# из тех же секретов лишь при инициализации кластера.
|
||||||
|
services:
|
||||||
|
- "miniflux_app"
|
||||||
|
state: "restarted"
|
||||||
|
tags:
|
||||||
|
- run-app
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create internal application directories"
|
- name: "Create internal application directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
|
# Группа docker = root на хосте, поэтому её не выдаём никому, кроме
|
||||||
|
# приложений, чей backup.sh делает docker compose exec: оркестратор
|
||||||
|
# запускает такие скрипты через su от имени этого пользователя.
|
||||||
|
# См. docs/backlog/backup-dump-without-docker-group.md.
|
||||||
owner_extra_groups: ["docker"]
|
owner_extra_groups: ["docker"]
|
||||||
|
|
||||||
- name: "Create internal directories"
|
- name: "Create internal directories"
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create internal application directories"
|
- name: "Create internal application directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
@@ -48,6 +47,7 @@
|
|||||||
# Vault-файл: ansible расшифровывает его при копировании, и без no_log
|
# Vault-файл: ansible расшифровывает его при копировании, и без no_log
|
||||||
# --diff покажет содержимое открытым текстом.
|
# --diff покажет содержимое открытым текстом.
|
||||||
no_log: true
|
no_log: true
|
||||||
|
notify: "Restart application"
|
||||||
|
|
||||||
- name: "Copy docker compose file"
|
- name: "Copy docker compose file"
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
@@ -64,3 +64,15 @@
|
|||||||
remove_orphans: true
|
remove_orphans: true
|
||||||
tags:
|
tags:
|
||||||
- run-app
|
- run-app
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
# Конфиг монтируется отдельным файлом, а `copy` подменяет его новым inode:
|
||||||
|
# запущенный контейнер продолжает видеть старый файл по старому inode.
|
||||||
|
# Bind-моунт переустанавливается при старте контейнера, поэтому применяет
|
||||||
|
# правку только явный рестарт.
|
||||||
|
- name: "Restart application"
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ base_dir }}"
|
||||||
|
state: "restarted"
|
||||||
|
tags:
|
||||||
|
- run-app
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
---
|
---
|
||||||
|
# ufw фильтрует только цепочку INPUT, то есть хостовые сервисы (sshd,
|
||||||
|
# zabbix-агент). Порты, опубликованные докером (80, 443, 2222), идут через
|
||||||
|
# PREROUTING/FORWARD мимо ufw — закрыть их можно только правкой `ports:` в
|
||||||
|
# docker-compose соответствующего приложения.
|
||||||
- name: "Configure UFW firewall"
|
- name: "Configure UFW firewall"
|
||||||
hosts: all
|
hosts: all
|
||||||
|
|
||||||
|
vars:
|
||||||
|
# Опросчики Zabbix у Timeweb: только им нужен доступ к агенту на хосте.
|
||||||
|
# Список — из Server= в /etc/zabbix/zabbix_agentd.conf.
|
||||||
|
zabbix_poller_ips:
|
||||||
|
- "92.53.116.12"
|
||||||
|
- "92.53.116.111"
|
||||||
|
- "92.53.116.119"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: "Ensure UFW is installed"
|
- name: "Ensure UFW is installed"
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
@@ -49,6 +61,17 @@
|
|||||||
port: "443"
|
port: "443"
|
||||||
proto: udp
|
proto: udp
|
||||||
|
|
||||||
|
# Агент хостера слушает 0.0.0.0:10050 и сам проверяет адрес источника,
|
||||||
|
# но держать порт открытым для всего интернета незачем.
|
||||||
|
- name: "Allow Zabbix agent on port 10050 from Timeweb pollers"
|
||||||
|
community.general.ufw:
|
||||||
|
rule: allow
|
||||||
|
src: "{{ item }}"
|
||||||
|
port: "10050"
|
||||||
|
proto: tcp
|
||||||
|
comment: "Timeweb zabbix poller"
|
||||||
|
loop: "{{ zabbix_poller_ips }}"
|
||||||
|
|
||||||
- name: "Enable UFW"
|
- name: "Enable UFW"
|
||||||
community.general.ufw:
|
community.general.ufw:
|
||||||
state: enabled
|
state: enabled
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
backups_dir: "{{ (base_dir, 'backups') | path_join }}"
|
backups_dir: "{{ (base_dir, 'backups') | path_join }}"
|
||||||
gobackup_config: "{{ (base_dir, 'gobackup.yml') | path_join }}"
|
gobackup_config: "{{ (base_dir, 'gobackup.yml') | path_join }}"
|
||||||
|
|
||||||
wanderer_version: "v0.18.3"
|
|
||||||
wanderer_origin: "https://wanderer.vakhrushev.me"
|
wanderer_origin: "https://wanderer.vakhrushev.me"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
@@ -23,7 +22,6 @@
|
|||||||
owner_name: "{{ app_user }}"
|
owner_name: "{{ app_user }}"
|
||||||
owner_uid: "{{ app_owner_uid }}"
|
owner_uid: "{{ app_owner_uid }}"
|
||||||
owner_gid: "{{ app_owner_gid }}"
|
owner_gid: "{{ app_owner_gid }}"
|
||||||
owner_extra_groups: ["docker"]
|
|
||||||
|
|
||||||
- name: "Create application internal directories"
|
- name: "Create application internal directories"
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
+41
-1
@@ -9,10 +9,50 @@ dependencies = [
|
|||||||
"ansible-lint>=25.12.2",
|
"ansible-lint>=25.12.2",
|
||||||
"croniter>=6.0.0",
|
"croniter>=6.0.0",
|
||||||
"invoke>=2.2.1",
|
"invoke>=2.2.1",
|
||||||
"mypy>=1.19.1",
|
"pyrefly>=1.2.0",
|
||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
"ruff>=0.15.2",
|
"ruff>=0.15.2",
|
||||||
"types-croniter>=6.0.0",
|
"types-croniter>=6.0.0",
|
||||||
"types-requests>=2.32.4.20260107",
|
"types-requests>=2.32.4.20260107",
|
||||||
"yamllint>=1.37.1",
|
"yamllint>=1.37.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
extend-select = [
|
||||||
|
# Правила, выпавшие из набора по умолчанию в ruff 0.16, но нужные проекту:
|
||||||
|
# E402 — импорты не в начале файла; A001/A002 — теневание встроенных имён
|
||||||
|
# аргументами invoke-задач.
|
||||||
|
"E402",
|
||||||
|
"A001",
|
||||||
|
"A002",
|
||||||
|
# Полная типизация: скрипты уезжают на сервер и правятся редко,
|
||||||
|
# аннотации — единственная страховка.
|
||||||
|
"ANN",
|
||||||
|
# pathlib вместо os.path, отсутствие закомментированного кода, стиль
|
||||||
|
# pytest и ограничение цикломатической сложности.
|
||||||
|
"PTH",
|
||||||
|
"ERA",
|
||||||
|
"PT",
|
||||||
|
"C90",
|
||||||
|
# Современный синтаксис: dict/list вместо typing.Dict/List, `X | None`
|
||||||
|
# вместо Optional. Те же места подсвечивает basedpyright в редакторе.
|
||||||
|
"UP",
|
||||||
|
# Наборы, по которым код уже чист: включены, чтобы так и оставалось.
|
||||||
|
"RET",
|
||||||
|
"N",
|
||||||
|
"Q",
|
||||||
|
"TID",
|
||||||
|
"G",
|
||||||
|
"LOG",
|
||||||
|
"FURB",
|
||||||
|
"PLC",
|
||||||
|
]
|
||||||
|
# Any в сигнатурах используется осознанно: разбор конфигов из TOML
|
||||||
|
# и параметры сторонних API без стабов типов.
|
||||||
|
ignore = ["ANN401"]
|
||||||
|
|
||||||
|
[tool.pyrefly]
|
||||||
|
# Без секции pyrefly ругается на отсутствие конфига; здесь же задаём,
|
||||||
|
# что проверять при запуске без аргументов.
|
||||||
|
project-includes = ["**/*.py"]
|
||||||
|
project-excludes = ["**/.venv/**", "**/galaxy.roles/**", "**/__pycache__/**"]
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# owner
|
||||||
|
|
||||||
|
Заводит системного пользователя и группу под одно приложение: фиксированные
|
||||||
|
uid/gid, ssh-ключи и переменные окружения в `~/.env`.
|
||||||
|
|
||||||
|
Один сервис — один пользователь: контейнеры приложения работают от его uid/gid,
|
||||||
|
и файлы в `base_dir` принадлежат ему же. Роль вызывается первой задачей почти
|
||||||
|
каждого `playbook-<app>.yml`.
|
||||||
|
|
||||||
|
## Что делает
|
||||||
|
|
||||||
|
1. **Группа и пользователь.** Создаёт группу `owner_group` с `owner_gid` и
|
||||||
|
пользователя `owner_name` с `owner_uid` и шеллом `/bin/bash`. Номера задаёт
|
||||||
|
плейбук приложения (соглашение по нумерации — в `AGENTS.md`).
|
||||||
|
2. **SSH-ключи.** Прописывает `owner_ssh_keys` в `authorized_keys`.
|
||||||
|
3. **Переменные окружения.** Рендерит `~/.env` из `owner_env` плюс `USER_UID` и
|
||||||
|
`USER_GID` — их подставляют docker-compose шаблоны приложений. В `.bashrc`
|
||||||
|
добавляет строку, подгружающую `~/.env` целиком, и вычищает старые построчные
|
||||||
|
`export`-ы, оставшиеся от прежней схемы.
|
||||||
|
|
||||||
|
## Результат для плейбука
|
||||||
|
|
||||||
|
Роль регистрирует `owner_create_result` — результат модуля `user`. Плейбуки и
|
||||||
|
compose-шаблоны берут оттуда `owner_create_result.uid` и
|
||||||
|
`owner_create_result.group` (это gid) для `user:` в docker-compose.
|
||||||
|
|
||||||
|
## Переменные
|
||||||
|
|
||||||
|
Полный список и значения по умолчанию — в `defaults/main.yml`.
|
||||||
|
|
||||||
|
| Переменная | Обязательная | Описание |
|
||||||
|
| --------------------- | ------------ | --------------------------------------------------- |
|
||||||
|
| `owner_name` | да | Имя пользователя; по умолчанию оно же имя группы |
|
||||||
|
| `owner_uid` | да | Uid пользователя |
|
||||||
|
| `owner_group` | нет | Имя группы, по умолчанию `owner_name` |
|
||||||
|
| `owner_gid` | нет | Gid группы, по умолчанию `owner_uid` |
|
||||||
|
| `owner_extra_groups` | нет | Дополнительные группы; по умолчанию пусто |
|
||||||
|
| `owner_ssh_keys` | нет | Список публичных ключей для `authorized_keys` |
|
||||||
|
| `owner_env` | нет | Словарь переменных окружения, попадающих в `~/.env` |
|
||||||
|
|
||||||
|
## Про группу `docker`
|
||||||
|
|
||||||
|
Членство в группе `docker` равносильно root на хосте (`docker run -v /:/host`),
|
||||||
|
поэтому сервисным пользователям её не выдаём: контейнерами рулит Ansible от
|
||||||
|
root. Исключение — приложения, чей `backup.sh` дампит базу через
|
||||||
|
`docker compose exec`: оркестратор бэкапов запускает такие скрипты от имени
|
||||||
|
самого приложения. См. `docs/backlog/backup-dump-without-docker-group.md`.
|
||||||
|
|
||||||
|
## Пример использования
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- 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 }}"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
role_name: owner
|
||||||
|
author: "Anton Vakhrushev"
|
||||||
|
description: "Системный пользователь приложения: группа, uid/gid, ssh-ключи и переменные окружения"
|
||||||
|
license: "MIT"
|
||||||
|
min_ansible_version: "2.14"
|
||||||
|
platforms:
|
||||||
|
- name: Ubuntu
|
||||||
|
versions:
|
||||||
|
- all
|
||||||
|
- name: Debian
|
||||||
|
versions:
|
||||||
|
- all
|
||||||
|
galaxy_tags:
|
||||||
|
- users
|
||||||
|
- system
|
||||||
|
- environment
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
+28
-20
@@ -1,21 +1,24 @@
|
|||||||
---
|
---
|
||||||
- name: 'Check app requirements for user "{{ owner_name }}".'
|
# tasks file for owner
|
||||||
ansible.builtin.fail:
|
|
||||||
msg: You must set owner name.
|
|
||||||
when: not owner_name
|
|
||||||
|
|
||||||
- name: 'Check app requirements for user "{{ owner_name }}".'
|
- name: "Validate owner role arguments"
|
||||||
ansible.builtin.fail:
|
ansible.builtin.assert:
|
||||||
msg: You must set owner uid.
|
that:
|
||||||
when: not owner_uid
|
- owner_name is string and owner_name | length > 0
|
||||||
|
- owner_uid | int > 0
|
||||||
|
- owner_gid | int > 0
|
||||||
|
fail_msg: >-
|
||||||
|
Роль owner требует непустой owner_name и положительные owner_uid/owner_gid
|
||||||
|
(owner_name={{ owner_name }}, owner_uid={{ owner_uid }}, owner_gid={{ owner_gid }})
|
||||||
|
quiet: true
|
||||||
|
|
||||||
- name: 'Create group "{{ owner_group }}".'
|
- name: "Create group {{ owner_group }}"
|
||||||
ansible.builtin.group:
|
ansible.builtin.group:
|
||||||
name: "{{ owner_group }}"
|
name: "{{ owner_group }}"
|
||||||
gid: "{{ owner_gid }}"
|
gid: "{{ owner_gid }}"
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
- name: 'Create user "{{ owner_name }}".'
|
- name: "Create user {{ owner_name }}"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ owner_name }}"
|
name: "{{ owner_name }}"
|
||||||
group: "{{ owner_group }}"
|
group: "{{ owner_group }}"
|
||||||
@@ -24,20 +27,21 @@
|
|||||||
shell: /bin/bash
|
shell: /bin/bash
|
||||||
register: owner_create_result
|
register: owner_create_result
|
||||||
|
|
||||||
- name: 'Set up user ssh keys for user "{{ owner_name }}".'
|
- name: "Set up ssh keys for user {{ owner_name }}"
|
||||||
ansible.posix.authorized_key:
|
ansible.posix.authorized_key:
|
||||||
user: "{{ owner_name }}"
|
user: "{{ owner_name }}"
|
||||||
key: "{{ item }}"
|
key: "{{ item }}"
|
||||||
state: present
|
state: present
|
||||||
with_items: "{{ owner_ssh_keys }}"
|
loop: "{{ owner_ssh_keys }}"
|
||||||
when: owner_ssh_keys | length > 0
|
|
||||||
|
|
||||||
- name: "Prepare env variables."
|
# USER_UID/USER_GID кладём в окружение всегда: их подставляют docker-compose
|
||||||
|
# шаблоны приложений, чтобы контейнер работал от того же пользователя.
|
||||||
|
- name: "Prepare env variables"
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
# yamllint disable-line rule:line-length
|
owner_env_dict: >-
|
||||||
owner_env_dict: '{{ owner_env | combine({"USER_UID": owner_create_result.uid, "USER_GID": owner_create_result.group}) }}'
|
{{ owner_env | combine({"USER_UID": owner_create_result.uid, "USER_GID": owner_create_result.group}) }}
|
||||||
|
|
||||||
- name: 'Set up environment variables for user "{{ owner_name }}".'
|
- name: "Set up environment variables for user {{ owner_name }}"
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: env.template
|
src: env.template
|
||||||
dest: "/home/{{ owner_name }}/.env"
|
dest: "/home/{{ owner_name }}/.env"
|
||||||
@@ -45,14 +49,18 @@
|
|||||||
group: "{{ owner_group }}"
|
group: "{{ owner_group }}"
|
||||||
mode: "0640"
|
mode: "0640"
|
||||||
|
|
||||||
- name: 'Remove from bashrc absent environment variables for user "{{ owner_name }}".'
|
# Раньше переменные писались в .bashrc построчно; теперь источник истины —
|
||||||
|
# ~/.env, поэтому старые строки вычищаем.
|
||||||
|
- name: "Remove from bashrc absent environment variables for user {{ owner_name }}"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: "/home/{{ owner_name }}/.bashrc"
|
path: "/home/{{ owner_name }}/.bashrc"
|
||||||
regexp: "^export {{ item.key }}="
|
regexp: "^export {{ item.key }}="
|
||||||
state: absent
|
state: absent
|
||||||
with_dict: "{{ owner_env_dict }}"
|
loop: "{{ owner_env_dict | dict2items }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.key }}"
|
||||||
|
|
||||||
- name: 'Include in bashrc environment variables for user "{{ owner_name }}".'
|
- name: "Include in bashrc environment variables for user {{ owner_name }}"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: "/home/{{ owner_name }}/.bashrc"
|
path: "/home/{{ owner_name }}/.bashrc"
|
||||||
regexp: "^export \\$\\(grep -v"
|
regexp: "^export \\$\\(grep -v"
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import hmac
|
|||||||
import hashlib
|
import hashlib
|
||||||
import base64
|
import base64
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
# These values are required to calculate the signature. Do not change them.
|
# These values are required to calculate the signature. Do not change them.
|
||||||
@@ -55,9 +54,6 @@ def calculate_key(secret_access_key: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if sys.version_info[0] < 3:
|
|
||||||
raise Exception("Must be using Python 3")
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Convert a Secret Access Key to an SMTP password."
|
description="Convert a Secret Access Key to an SMTP password."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from invoke.context import Context
|
from invoke.context import Context
|
||||||
from invoke.exceptions import Exit
|
from invoke.exceptions import Exit
|
||||||
@@ -37,7 +38,7 @@ def _remote_host() -> str:
|
|||||||
def _authelia_docker() -> str:
|
def _authelia_docker() -> str:
|
||||||
"""Команда запуска authelia CLI на том же образе, что и задеплоенный сервис"""
|
"""Команда запуска authelia CLI на том же образе, что и задеплоенный сервис"""
|
||||||
pattern = re.compile(r"""image:\s*["']?(\S*authelia/authelia:[^"'\s]+)""")
|
pattern = re.compile(r"""image:\s*["']?(\S*authelia/authelia:[^"'\s]+)""")
|
||||||
with open(AUTHELIA_COMPOSE_FILE, encoding="utf-8") as compose:
|
with Path(AUTHELIA_COMPOSE_FILE).open(encoding="utf-8") as compose:
|
||||||
for line in compose:
|
for line in compose:
|
||||||
match = pattern.search(line)
|
match = pattern.search(line)
|
||||||
if match:
|
if match:
|
||||||
@@ -56,7 +57,7 @@ def _rest_args() -> list[str]:
|
|||||||
def _resolve_playbook(name: str) -> str:
|
def _resolve_playbook(name: str) -> str:
|
||||||
candidates = [name, f"{name}.yml", f"playbook-{name}.yml"]
|
candidates = [name, f"{name}.yml", f"playbook-{name}.yml"]
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
if os.path.isfile(candidate):
|
if Path(candidate).is_file():
|
||||||
return candidate
|
return candidate
|
||||||
raise Exit(
|
raise Exit(
|
||||||
f"Плейбук для '{name}' не найден. Проверял: {', '.join(candidates)}", code=1
|
f"Плейбук для '{name}' не найден. Проверял: {', '.join(candidates)}", code=1
|
||||||
@@ -141,16 +142,12 @@ def edit_encrypted(ctx: Context, path: str) -> None:
|
|||||||
# Путь к канону — сосед по файловой системе (../ansible-roles/roles),
|
# Путь к канону — сосед по файловой системе (../ansible-roles/roles),
|
||||||
# поэтому не зависит от текущей директории и одинаков везде.
|
# поэтому не зависит от текущей директории и одинаков везде.
|
||||||
|
|
||||||
SHARED_ROLES_DIR = os.path.normpath(
|
SHARED_ROLES_DIR = Path(__file__).resolve().parent.parent / "ansible-roles" / "roles"
|
||||||
os.path.join(
|
|
||||||
os.path.dirname(os.path.abspath(__file__)), "..", "ansible-roles", "roles"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
SYNCED_ROLES = ["eget", "app_image"]
|
SYNCED_ROLES = ["eget", "app_image"]
|
||||||
|
|
||||||
|
|
||||||
def _require_canon() -> None:
|
def _require_canon() -> None:
|
||||||
if not os.path.isdir(SHARED_ROLES_DIR):
|
if not SHARED_ROLES_DIR.is_dir():
|
||||||
raise Exit(f"Канон не найден: {SHARED_ROLES_DIR}", code=1)
|
raise Exit(f"Канон не найден: {SHARED_ROLES_DIR}", code=1)
|
||||||
|
|
||||||
|
|
||||||
@@ -171,11 +168,13 @@ def roles_status(ctx: Context) -> None:
|
|||||||
_require_canon()
|
_require_canon()
|
||||||
drift = False
|
drift = False
|
||||||
for role in SYNCED_ROLES:
|
for role in SYNCED_ROLES:
|
||||||
canon = os.path.join(SHARED_ROLES_DIR, role)
|
canon = SHARED_ROLES_DIR / role
|
||||||
if not os.path.isdir(canon):
|
if not canon.is_dir():
|
||||||
print(f"{role}: missing in shared")
|
print(f"{role}: missing in shared")
|
||||||
drift = True
|
drift = True
|
||||||
elif subprocess.run(["diff", "-rq", f"roles/{role}", canon]).returncode != 0:
|
elif (
|
||||||
|
subprocess.run(["diff", "-rq", f"roles/{role}", str(canon)]).returncode != 0
|
||||||
|
):
|
||||||
print(f"{role}: differs")
|
print(f"{role}: differs")
|
||||||
drift = True
|
drift = True
|
||||||
else:
|
else:
|
||||||
@@ -189,10 +188,10 @@ def roles_pull(ctx: Context) -> None:
|
|||||||
"""Канон → репозиторий: inv roles-pull [-- <role> ...]"""
|
"""Канон → репозиторий: inv roles-pull [-- <role> ...]"""
|
||||||
_require_canon()
|
_require_canon()
|
||||||
for role in _roles_to_sync():
|
for role in _roles_to_sync():
|
||||||
src = os.path.join(SHARED_ROLES_DIR, role)
|
src = SHARED_ROLES_DIR / role
|
||||||
if not os.path.isdir(src):
|
if not src.is_dir():
|
||||||
raise Exit(f"Роль '{role}' отсутствует в каноне: {src}", code=1)
|
raise Exit(f"Роль '{role}' отсутствует в каноне: {src}", code=1)
|
||||||
os.makedirs(f"roles/{role}", exist_ok=True)
|
Path("roles", role).mkdir(parents=True, exist_ok=True)
|
||||||
ctx.run(f"rsync -a -i --delete {src}/ roles/{role}/")
|
ctx.run(f"rsync -a -i --delete {src}/ roles/{role}/")
|
||||||
|
|
||||||
|
|
||||||
@@ -201,24 +200,24 @@ def roles_push(ctx: Context) -> None:
|
|||||||
"""Репозиторий → канон, с подтверждением при расхождении: inv roles-push [-- <role> ...]"""
|
"""Репозиторий → канон, с подтверждением при расхождении: inv roles-push [-- <role> ...]"""
|
||||||
_require_canon()
|
_require_canon()
|
||||||
for role in _roles_to_sync():
|
for role in _roles_to_sync():
|
||||||
local = f"roles/{role}"
|
local = Path("roles", role)
|
||||||
canon = os.path.join(SHARED_ROLES_DIR, role)
|
canon = SHARED_ROLES_DIR / role
|
||||||
if not os.path.isdir(local):
|
if not local.is_dir():
|
||||||
raise Exit(f"Локальной роли нет: {local}", code=1)
|
raise Exit(f"Локальной роли нет: {local}", code=1)
|
||||||
differs = (
|
differs = (
|
||||||
os.path.isdir(canon)
|
canon.is_dir()
|
||||||
and subprocess.run(["diff", "-rq", local, canon]).returncode != 0
|
and subprocess.run(["diff", "-rq", str(local), str(canon)]).returncode != 0
|
||||||
)
|
)
|
||||||
if differs:
|
if differs:
|
||||||
# Защита от затирания более свежего канона устаревшей копией.
|
# Защита от затирания более свежего канона устаревшей копией.
|
||||||
subprocess.run(["diff", "-r", canon, local])
|
subprocess.run(["diff", "-r", str(canon), str(local)])
|
||||||
answer = input(
|
answer = input(
|
||||||
f"Канон '{role}' отличается. Перезаписать его копией из этого репо? [y/N] "
|
f"Канон '{role}' отличается. Перезаписать его копией из этого репо? [y/N] "
|
||||||
)
|
)
|
||||||
if answer.strip().lower() != "y":
|
if answer.strip().lower() != "y":
|
||||||
print(f"{role}: пропущено")
|
print(f"{role}: пропущено")
|
||||||
continue
|
continue
|
||||||
os.makedirs(canon, exist_ok=True)
|
canon.mkdir(parents=True, exist_ok=True)
|
||||||
ctx.run(f"rsync -a -i --delete {local}/ {canon}/")
|
ctx.run(f"rsync -a -i --delete {local}/ {canon}/")
|
||||||
|
|
||||||
|
|
||||||
@@ -267,7 +266,7 @@ def authelia_validate_config(ctx: Context) -> None:
|
|||||||
"""Отрендерить конфиг authelia из шаблона и проверить его"""
|
"""Отрендерить конфиг authelia из шаблона и проверить его"""
|
||||||
dest = "temp/configuration.yml"
|
dest = "temp/configuration.yml"
|
||||||
# temp/ в .gitignore, на свежем клоне его нет — ansible сам директорию не создаёт.
|
# temp/ в .gitignore, на свежем клоне его нет — ansible сам директорию не создаёт.
|
||||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
Path(dest).parent.mkdir(parents=True, exist_ok=True)
|
||||||
try:
|
try:
|
||||||
ctx.run(
|
ctx.run(
|
||||||
"uv run ansible localhost"
|
"uv run ansible localhost"
|
||||||
|
|||||||
@@ -415,66 +415,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "librt"
|
|
||||||
version = "0.8.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markupsafe"
|
name = "markupsafe"
|
||||||
version = "3.0.3"
|
version = "3.0.3"
|
||||||
@@ -538,39 +478,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy"
|
|
||||||
version = "1.19.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
|
|
||||||
{ name = "mypy-extensions" },
|
|
||||||
{ name = "pathspec" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mypy-extensions"
|
name = "mypy-extensions"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -607,7 +514,7 @@ dependencies = [
|
|||||||
{ name = "ansible-lint" },
|
{ name = "ansible-lint" },
|
||||||
{ name = "croniter" },
|
{ name = "croniter" },
|
||||||
{ name = "invoke" },
|
{ name = "invoke" },
|
||||||
{ name = "mypy" },
|
{ name = "pyrefly" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
{ name = "types-croniter" },
|
{ name = "types-croniter" },
|
||||||
@@ -621,7 +528,7 @@ requires-dist = [
|
|||||||
{ name = "ansible-lint", specifier = ">=25.12.2" },
|
{ name = "ansible-lint", specifier = ">=25.12.2" },
|
||||||
{ name = "croniter", specifier = ">=6.0.0" },
|
{ name = "croniter", specifier = ">=6.0.0" },
|
||||||
{ name = "invoke", specifier = ">=2.2.1" },
|
{ name = "invoke", specifier = ">=2.2.1" },
|
||||||
{ name = "mypy", specifier = ">=1.19.1" },
|
{ name = "pyrefly", specifier = ">=1.2.0" },
|
||||||
{ name = "requests", specifier = ">=2.32.5" },
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
{ name = "ruff", specifier = ">=0.15.2" },
|
{ name = "ruff", specifier = ">=0.15.2" },
|
||||||
{ name = "types-croniter", specifier = ">=6.0.0" },
|
{ name = "types-croniter", specifier = ">=6.0.0" },
|
||||||
@@ -647,6 +554,25 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyrefly"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/89/01/a86e9f24722b095c3f88e3616132b75a21b0df53804bdc6a45314dd4d93c/pyrefly-1.2.0.tar.gz", hash = "sha256:5485f960fc2481617068c918335c39ab1507ef90b6b5bd35bf57726e60e73185", size = 6243654, upload-time = "2026-08-01T02:56:27.592Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/9d/3c0ef1d4843987b22f996ed381ec9cf5a3b1273e29804db276252e4c95eb/pyrefly-1.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7f46d983ac49ddd2b043694960a01dc6a19a5cfd8eec609d6bd9c42866f91b4e", size = 14026305, upload-time = "2026-08-01T02:56:02.611Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/06/03bbb78fbea54cdc65b626619f3597d5611aca4fdef11e72a4e8360e7e63/pyrefly-1.2.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:756f669b5555090f5c1a4fef30db1785fabe657764f7e4e6dc88994dfb8ca82d", size = 13463880, upload-time = "2026-08-01T02:56:04.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/5a/7d8bc00a38e93bbc9c3e7bd14d305f7948717e667c9bcddeab9dd42fd255/pyrefly-1.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3465812ce5ef4781fb592edbf2724547296f0a3124be115d73c7e8b2401862d", size = 13907329, upload-time = "2026-08-01T02:56:07.104Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/94/9e08b4bf799d0b8f36b55a2783c7ba5f51730cf0632a85a67b5b5ed876cd/pyrefly-1.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5de7b2ad2bba5c8055181681a84b74143eac2234a48ba5d1b7ed7e7a722b02bd", size = 15039020, upload-time = "2026-08-01T02:56:09.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/bd/bca5fd0c80f4daf8ee6903a29df9f3de1feb05ff0946b8f35ec8c5096b13/pyrefly-1.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25822ea9505f589ea8a725e4268b475132fb89e038fbf092e446510443ac142a", size = 14986199, upload-time = "2026-08-01T02:56:11.924Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/f7/f07087f3d185ad2eced0c56cef89ca5474dfb4ff25f146cd50a861c97553/pyrefly-1.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90efe75e17491ef5d636e10469e9278d7d0256b3b4c5e1f4750069bf3ae0f5d1", size = 14393715, upload-time = "2026-08-01T02:56:14.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/70/0d142c320e284b9e3ce35e9b1e58b8ce2ee1f578f2a7234bc30e5022b94f/pyrefly-1.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:368aaf7eee4f511ddc0f8e564cf14e01ab2f10b0db9105c6d5b153bf498d07bf", size = 13933008, upload-time = "2026-08-01T02:56:16.525Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/e8/e84f11b6e1f63fd453ad3654213b9a0f6f4de8cef6b58038eef2d0d5955d/pyrefly-1.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52d5da7bc65fb7675fbaa80eda879d4f8787c494f04cac21603330d3abbdbbe", size = 14431827, upload-time = "2026-08-01T02:56:18.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/06/810d31380f66c75e1c0779a408d3b16117b1b368b57894f6aa66bef21686/pyrefly-1.2.0-py3-none-win32.whl", hash = "sha256:8c90751de8506d938e8f802659c74cf35bd7a0036510ee6c634a38eebb280bfa", size = 13229447, upload-time = "2026-08-01T02:56:20.921Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/98/4dafa3c7a1caed2dc8cc708dde09ba27963c7736508f55b626fff3024113/pyrefly-1.2.0-py3-none-win_amd64.whl", hash = "sha256:8a8964c224ccc4882730130955815de21ff443c1ac3f0b90685b19bf63848170", size = 14087387, upload-time = "2026-08-01T02:56:23.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/1c/df3cb0a2e5591660ded7a1836cd2f29dc48c91adb1c0a3a700a96f6d09e1/pyrefly-1.2.0-py3-none-win_arm64.whl", hash = "sha256:3a90bb8df39dfbac74b1f3b2e9d7c526b8f80568884c3944d955023a73ebf61e", size = 13430873, upload-time = "2026-08-01T02:56:25.425Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
|
|||||||
Reference in New Issue
Block a user