From 1ce168655d0418cfb17f8b261daa946057576f5d Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Fri, 22 May 2026 20:45:52 +0300 Subject: [PATCH] Migration: application directory as parameter --- docs/drafts/timeweb-migration-log.md | 97 ++++++++++++++++++++++++++++ playbook-remove-user-and-app.yml | 3 +- playbook-system.yml | 7 +- production.yml | 1 + tasks.py | 23 ++++++- vars/vars.yml | 5 +- 6 files changed, 129 insertions(+), 7 deletions(-) diff --git a/docs/drafts/timeweb-migration-log.md b/docs/drafts/timeweb-migration-log.md index be91619..16336da 100644 --- a/docs/drafts/timeweb-migration-log.md +++ b/docs/drafts/timeweb-migration-log.md @@ -8,6 +8,103 @@ --- +## Шаг 5 — переезд default application_dir на /srv (2026-05-22, выполнено) + +`/mnt` по FHS — место для точек монтирования внешних дисков; на +системном диске Timeweb (фаза 1) это семантически неверно. Поменяли +дефолт на `/srv/applications` (FHS: «data for services provided by +this system»), для текущего YC-сервера сделали override в инвентаре. + +Изменения: + +- `vars/vars.yml` — `application_dir: "/srv/applications"` + (комментарий обновлён). +- `production.yml` — у хоста `server` добавлен override + `application_dir: "/mnt/applications"`. +- `playbook-system.yml` — добавлен `vars/vars.yml` в `vars_files`, + захардкоженный `/mnt/applications` в задачах + `Create directory for mount` и `Mount external storages` заменён + на `{{ application_dir }}`. +- `playbook-remove-user-and-app.yml` — то же самое (`vars/vars.yml` + в `vars_files` + `{{ (application_dir, user_name) | path_join }}`). +- `tasks.py` — новый helper `_application_dir()` читает значение + сначала из inventory (override), затем из `vars/vars.yml`. `login_as_app` + больше не содержит `/mnt/applications`. + +Что остаётся хардкодом — только `/mnt/applications` в `production.yml` +как override, и это правильно. + +На Timeweb-инвентаре (когда появится) можно либо не задавать +`application_dir` вовсе (применится дефолт `/srv/applications`), либо +задать явно — для читаемости. + +Проверить прогоном `inv pl -- system` на текущем сервере (Yandex +Cloud) — ничего не должно поменяться, потому что inventory override +возвращает `/mnt/applications` и mount всё ещё включён. Diff ожидается +пустой. + +### Восстановление restic-снапшотов после смены путей + +Старые снапшоты записаны с путями `/mnt/applications/`. На +Timeweb данные должны лежать в `/srv/applications/`. У restic +нет встроенного «remap path» при restore, поэтому делается в два +шага: восстановить во временный каталог, затем `rsync` на новое +место с сохранением uid/gid (приложения уже созданы playbook'ом с +теми же uid/gid, см. шаг про подготовку target). + +Пример — восстановить gitea на Timeweb-машине: + +```bash +sudo /usr/local/sbin/restic-shell.sh + +# Распакуем нужную поддиректорию во временный каталог +restic restore latest \ + --target /tmp/restic-restore \ + --include /mnt/applications/gitea + +# Перенесём данные на новый путь, сохранив владельца/группу/ACL/xattr +sudo rsync -aAX --info=progress2 \ + /tmp/restic-restore/mnt/applications/gitea/ \ + /srv/applications/gitea/ + +sudo rm -rf /tmp/restic-restore +``` + +Несколько приложений за один проход: + +```bash +restic restore latest \ + --target /tmp/restic-restore \ + --include /mnt/applications/gitea \ + --include /mnt/applications/outline \ + --include /mnt/applications/miniflux + +for app in gitea outline miniflux; do + sudo rsync -aAX --info=progress2 \ + "/tmp/restic-restore/mnt/applications/$app/" \ + "/srv/applications/$app/" +done +sudo rm -rf /tmp/restic-restore +``` + +Альтернатива через `restic mount` (если не хочется промежуточной +копии — данные мапятся как FUSE-FS): + +```bash +sudo mkdir -p /mnt/restic-snapshots +restic mount /mnt/restic-snapshots & +sudo rsync -aAX \ + /mnt/restic-snapshots/snapshots/latest/mnt/applications/gitea/ \ + /srv/applications/gitea/ +sudo fusermount -u /mnt/restic-snapshots +``` + +После переезда новые снапшоты будут записываться уже с путями +`/srv/applications/` — никаких трюков для текущих бэкапов не +нужно. + +--- + ## Шаг 4 — условное монтирование внешнего диска (2026-05-22, выполнено) Задача `Mount external storages` в `playbook-system.yml` теперь diff --git a/playbook-remove-user-and-app.yml b/playbook-remove-user-and-app.yml index 98ffa0b..1de9944 100644 --- a/playbook-remove-user-and-app.yml +++ b/playbook-remove-user-and-app.yml @@ -4,6 +4,7 @@ vars_files: - vars/secrets.yml + - vars/vars.yml vars: user_name: "" @@ -27,7 +28,7 @@ - name: "Remove application dir" ansible.builtin.file: - path: "/mnt/applications/{{ user_name }}" + path: "{{ (application_dir, user_name) | path_join }}" state: absent - name: "Remove home dir" diff --git a/playbook-system.yml b/playbook-system.yml index c334a07..a1d899c 100644 --- a/playbook-system.yml +++ b/playbook-system.yml @@ -4,6 +4,7 @@ vars_files: - vars/secrets.yml + - vars/vars.yml vars: apt_packages: @@ -40,9 +41,9 @@ group: root mode: "0755" - - name: 'Create directory for mount' + - name: 'Create directory for applications' ansible.builtin.file: - path: '/mnt/applications' + path: '{{ application_dir }}' state: 'directory' mode: '0755' tags: @@ -50,7 +51,7 @@ - name: 'Mount external storages' ansible.posix.mount: - path: '/mnt/applications' + path: '{{ application_dir }}' src: 'UUID=3942bffd-8328-4536-8e88-07926fb17d17' fstype: ext4 state: mounted diff --git a/production.yml b/production.yml index e65b3e5..d901e84 100644 --- a/production.yml +++ b/production.yml @@ -5,4 +5,5 @@ ungrouped: ansible_host: "158.160.46.255" ansible_user: "major" ansible_become: true + application_dir: "/mnt/applications" mount_external_storage: true diff --git a/tasks.py b/tasks.py index 6ff69b3..f2c729b 100644 --- a/tasks.py +++ b/tasks.py @@ -10,6 +10,7 @@ from invoke.exceptions import Exit from invoke.tasks import task HOSTS_FILE = "production.yml" +VARS_FILE = "vars/vars.yml" AUTHELIA_DOCKER = "docker run --rm -v $PWD:/data authelia/authelia:4.39.4 authelia" @@ -28,6 +29,25 @@ def _remote_host() -> str: return _yq(".ungrouped.hosts.server.ansible_host") +def _application_dir() -> str: + """Чтение application_dir: сначала из inventory (override), затем из vars/vars.yml.""" + inv_value = _yq('.ungrouped.hosts.server.application_dir // ""') + if inv_value: + return inv_value + result = subprocess.run( + ["yq", ".application_dir", VARS_FILE], + capture_output=True, + text=True, + check=True, + ) + value = result.stdout.strip() + if not value or value == "null": + raise Exit( + f"application_dir не определён ни в inventory, ни в {VARS_FILE}", code=1 + ) + return value + + def _rest_args() -> list[str]: """Возвращает аргументы после '--' из sys.argv""" try: @@ -85,8 +105,9 @@ def login_as_app(ctx: Context, app: str) -> None: """SSH и переключиться на пользователя приложения: inv login gitea""" # sudo -i: login shell, -u: от имени пользователя # bash -i: интерактивный режим (job control), -l: login (читает профиль) + app_dir = f"{_application_dir()}/{app}" subprocess.run( - f"""ssh {_remote_user()}@{_remote_host()} -t 'sudo -iu {app} bash -c "cd /mnt/applications/{app} && exec bash -il"'""", + f"""ssh {_remote_user()}@{_remote_host()} -t 'sudo -iu {app} bash -c "cd {app_dir} && exec bash -il"'""", shell=True, ) diff --git a/vars/vars.yml b/vars/vars.yml index f162512..004a23c 100644 --- a/vars/vars.yml +++ b/vars/vars.yml @@ -7,8 +7,9 @@ primary_user_gid: 1001 # Directory for all user binaries and scripts bin_prefix: "/usr/local/bin" -# External disk for application data -application_dir: "/mnt/applications" +# Root directory for application data. Override in inventory if host +# uses a different path (e.g. external disk mounted elsewhere). +application_dir: "/srv/applications" apprise_external_port: 8000 apprise_external_url: "http://127.0.0.1:{{ apprise_external_port }}"