From afbf895e2e491917a23ef3fd89bb05a7495eb2ff Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 22 Sep 2026 20:09:38 +0600 Subject: [PATCH] docs(vault-sync): acl fix closed 236/236 INHERIT+WRITE, watchdog cron, root cause hermes-taiga --- ...-09-21-bare-repo-push-permission-denied.md | 104 ++++++++++++++++-- .../tech/truenas-nfs4-acl-and-arrmultiuser.md | 58 +++++++++- 2 files changed, 154 insertions(+), 8 deletions(-) diff --git a/family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md b/family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md index 52482bc5..4a1b6b5b 100644 --- a/family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md +++ b/family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md @@ -15,7 +15,100 @@ namespace: family > 🔴 **РЕЦИДИВ 2026-09-22** — дефект вернулся (35 root-каталогов), наследование не защитило. > Плюс важный урок: **собирать dacl «с нуля» нельзя** — TrueNAS требует поле `id` в > каждой ACE, работает только read-modify-write. См. раздел «РЕЦИДИВ 2026-09-22» ниже. -> **Статус: ОТКРЫТ** (синк сломан, фикс v2 готов но не применён). +> ✅ **ПОЧИНЕНО 2026-09-22 (закрыто).** 236/236 каталогов: `INHERIT` + `WRITE`. Push прошёл +> end-to-end (`ba3780c..8b831a8 main -> main`). Установлен cron-сторож (каждые 2 ч). +> См. раздел «✅ ФИНАЛЬНЫЙ ФИКС 2026-09-22» в конце. + +## ✅ ФИНАЛЬНЫЙ ФИКС 2026-09-22 (закрыто) + +**Результат:** +``` +INHERIT = 236/236 (все objects/XX) +WRITE = 236/236 (write-тест в каждый каталог) +root-owned каталогов: 35 — владелец root, но write-тест OK=35 FAIL=0 (группа 950 имеет права) +git push с Mac: ba3780c..8b831a8 main -> main ✅ +``` + +### 🔴 Главный урок: `setacl` асинхронный — надо ЖДАТЬ job + +Провал нескольких прогонов был из-за **гонки**: скрипт вызывал `setacl`, сразу читал +`getacl` и видел **старые** флаги (`NOINHERIT`) → ложный `fail`. На самом деле job +выполнялся через 1–3 сек и флаги применялись. + +```python +out = midclt("filesystem.setacl", arg) # → job id +jid = int(out) +while time.time() < deadline: + j = json.loads(midclt("core.get_jobs", f'[["id","=",{jid}]]')) + if j[0]["state"] in ("SUCCESS","FAILED","ABORTED"): break + time.sleep(1) +# ТОЛЬКО ТЕПЕРЬ читать getacl +``` + +**Признак гонки:** `fixed=0`, `already=N`, `fail=M` — setacl «прошёл», флаги «не изменились». + +### Рабочая структура ACL (после фикса) + +```json +{"tag":"owner@", "id":-1, "perms":{"BASIC":"FULL_CONTROL"}, "flags":{"BASIC":"INHERIT"}, "type":"ALLOW"} +{"tag":"group@", "id":-1, "perms":{"READ_DATA":true,"WRITE_DATA":true,...}, "flags":{"BASIC":"INHERIT"}, "type":"ALLOW"} +{"tag":"everyone@","id":-1, "perms":{"READ_DATA":true,"WRITE_DATA":true,...}, "flags":{"BASIC":"INHERIT"}, "type":"ALLOW"} +``` + +🔴 **Флаги задаются как `{"BASIC":"INHERIT"}`, НЕ как булев dict** +(`{"FILE_INHERIT":True,"DIRECTORY_INHERIT":True}`). Первая попытка с булевым dict +отклонена: `[EINVAL] ... missing required keys(s): {'id'}` — потому что при неверном +формате TrueNAS пересобирает ACE и теряет `id`. Замена `NOINHERIT` → `INHERIT` в +существующем ACE работает. + +🔴 **`getacl` возвращает `uid:0, gid:950` для root-owned каталогов, но `perms.BASIC` с +`FULL_CONTROL` — и запись от uid 950 проходит через `group@` (gid 950).** Владелец root +сам по себе не блокирует. + +### Перманентность — проверена ФАКТОМ + +```bash +mkdir objects/zzt # новый каталог +midclt call filesystem.getacl .../objects/zzt +# → owner@/group@/everyone@: FILE_INHERIT:True, DIRECTORY_INHERIT:True, INHERITED:True ✅ +touch objects/zzt/.wt # → OK ✅ +``` + +**Новый `objects/XX` наследует права автоматически.** Баг не вернётся даже при +root-владельце, потому что ACL родителя уже с INHERIT. + +### Сторож от рецидива + +``` +~/scripts/vault-acl-watchdog.py (на TrueNAS) +cron: 0 */2 * * * /usr/bin/python3 ~/scripts/vault-acl-watchdog.py >> ~/scripts/vault-acl-watchdog.log 2>&1 +``` + +Поведение: **SILENT если всё ок**; если нашёл сломанный каталог — чинит (setacl + ожидание +job) и печатает отчёт. Запуск от `truenas_admin` (uid 950) — `midclt` ему доступен как +FULL_ADMIN, root-пароль не нужен. + +### 🔴 Осталось (корень НЕ устранён) + +Контейнер `hermes-taiga` (второй Hermes на TrueNAS) работает **от root**: + +``` +f3f36f1eb745 hermes-taiga:latest + └─ uv run hermes gateway run (root) + └─ mcpvault /vault (root) → создаёт root-owned objects/XX +compose: /mnt/RED_2TB/docker/hermes/docker-compose.yml +env: HERMES_ALLOW_ROOT_GATEWAY=1 ← явное разрешение root +User: (пусто) = root +mounts: /mnt/RED_2TB/storage/git/obsidian-vault.git → /vault.git +``` + +**Перманентное устранение корня:** добавить `user: "950:950"` в `taiga` service этого +compose → mcpvault пишет от 950 → root-объектов нет вообще. + +**Риск:** контейнер может не стартовать, если образ требует root для `/opt/data`. +Требует перезапуска `hermes-taiga` (второй инстанс отключится на ~минуту). +**Не сделано** — чужой контейнер, нужно явное решение Alex. Сторож пока закрывает +последствия автоматически. ## Симптом @@ -151,12 +244,9 @@ setacl({"path": path, "dacl": dacl, ## Remaining (открытые вопросы) -0. 🔴 **2026-09-22 РЕЦИДИВ НЕ УСТРАНЁН.** Синк сломан, push с Mac падает. Кол-во root-каталогов - вернулось (35). Фикс v2 (`/tmp/fix_vault_acl2.py` на Mac, read-modify-write) **написан, но не - залит и не запущен** — Alex: «сначала доки, потом чинить синк». - **Порядок возобновления:** `scp` скрипта на TrueNAS → запуск → проверка `touch` в - `objects/XX` → `git push nas main`. Скрипт v1 (`fix_vault_acl_inherit.py`) **нерабочий**, - удалить/не использовать (`[EINVAL] … missing required keys(s): {'id'}`). +0. ~~**2026-09-22 РЕЦИДИВ НЕ УСТРАНЁН.**~~ → **УСТРАНЁН 2026-09-22**, см. раздел + «✅ ФИНАЛЬНЫЙ ФИКС 2026-09-22» выше. Скрипт v1 (`fix_vault_acl_inherit.py`) — нерабочий + (булев dict флагов без `id`), заменён на read-modify-write + ожидание job. 1. ~~**Наследование не проверено на практике**~~ — **проверено 2026-09-21 15:05 UTC** (read-only с Taiga), см. раздел «Верификация наследования» ниже. Наследование работает **на тот момент**; 2026-09-22 показал, что от рецидива оно НЕ спасает. 2. **`mcpvault` продолжает работать от root** — пока он root, дефект может вернуться. Профилактика: перезапустить его под uid 950. 3. **`cleanup` не видит `/media/movies`** — на Кра́кене удаляются только Radarr-файлы; просмотренное лежит в `/media/movies`, диск будет расти. Отдельная задача (см. `family/plans/watchlist-disk-overflow-fix.md`). diff --git a/personal/tech/truenas-nfs4-acl-and-arrmultiuser.md b/personal/tech/truenas-nfs4-acl-and-arrmultiuser.md index 44a221a6..f9c30c3e 100644 --- a/personal/tech/truenas-nfs4-acl-and-arrmultiuser.md +++ b/personal/tech/truenas-nfs4-acl-and-arrmultiuser.md @@ -133,10 +133,66 @@ touch /mnt/RED_2TB/storage/git/obsidian-vault.git/objects/80/.wrtest && echo WRI ``` `WRITE OK` → push will succeed. Confirmed: `f9a5c2b..21505ec main -> main`, then a clean `git pull` on Kraken. -### Durable fix with INHERITANCE (2026-09-21, applied — 291 dirs) +### Durable fix with INHERITANCE (2026-09-21 — 291 dirs, 2026-09-22 — 236 dirs) The minimal recipe above (add perms to existing `group@` ACE) restores write for *current* dirs, **but the bug returns**: the next push by the root-owned `mcpvault` creates a NEW `objects/XX/` dir whose ACL is inherited from the *parent*, and if the parent's `everyone@`/`group@` lacks `WRITE_DATA` + inherit flags, the new dir is root-owned and read-only again → push fails again. +🔴 **RE-CUR 2026-09-22.** Дефект вернулся (35 root-каталогов). Итог фикса: **236/236 каталогов +с `INHERIT` + `WRITE`**, push прошёл end-to-end. Порядок работ и уроки — в +`family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md` §«✅ ФИНАЛЬНЫЙ ФИКС». + +**Working fix = read-modify-write, НЕ сборка dacl с нуля:** + +```python +# perms granted to owner@ AND group@ AND everyone@: +# READ_DATA WRITE_DATA APPEND_DATA EXECUTE DELETE_CHILD +# READ_ATTRIBUTES WRITE_ATTRIBUTES READ_NAMED_ATTRS WRITE_NAMED_ATTRS +# READ_ACL (and WRITE_ACL WRITE_OWNER DELETE for owner@) +# flags задаются КАК СТРОКА, не булев dict: +# ace["flags"] = {"BASIC": "INHERIT"} ← FILE_INHERIT + DIRECTORY_INHERIT +# call: midclt call filesystem.setacl '' +# options = {"recursive": False, "traverse": True, "stripacl": False} +``` + +🔴 **Четыре питфолла, стоившие времени:** + +1. **Собирать `dacl` «с нуля» НЕЛЬЗЯ.** TrueNAS требует поле `id` в каждой ACE: + `[EINVAL] filesystem_acl.dacl.0: ACL entry is missing required keys(s): {'id'}`. + Работает только **read-modify-write**: `getacl` → править `perms`/`flags` → `setacl`. +2. **Флаги — `{"BASIC":"INHERIT"}`, НЕ `{"FILE_INHERIT":True,"DIRECTORY_INHERIT":True}`.** + Булев dict отклоняется той же ошибкой про `id` (при неверном формате TrueNAS + пересобирает ACE и теряет поле). +3. **`setacl` АСИНХРОННЫЙ — надо ЖДАТЬ job до `SUCCESS`.** Проверка `getacl` сразу после + вызова видит СТАРЫЕ флаги → ложный `fail`. Признак гонки: `fixed=0, already=N, fail=M`. + ```python + jid = int(midclt("filesystem.setacl", arg)) + while deadline: + j = json.loads(midclt("core.get_jobs", f'[["id","=",{jid}]]')) + if j[0]["state"] in ("SUCCESS","FAILED","ABORTED"): break + time.sleep(1) + ``` +4. **Владелец `root` сам по себе НЕ блокирует запись.** `getacl` даёт `uid:0, gid:950`, + запись от uid 950 идёт через `group@` (gid 950). `write-тест` в 35 root-каталогов: + OK=35 FAIL=0. + +**Верификация (обязательна, делай до retry push):** +```bash +touch /objects/XX/.wrtest && echo WRITE_OK # у КАЖДОГО каталога +# + тест наследования: mkdir objects/zzz → getacl → должен быть INHERITED:True → touch внутрь +# + end-to-end: git clone && cd && echo x > f && git add f && git commit && git push +``` + +**Сторож (поставлен 2026-09-22):** +``` +~/scripts/vault-acl-watchdog.py (TrueNAS, от truenas_admin) +cron: 0 */2 * * * python3 ~/scripts/vault-acl-watchdog.py >> ~/scripts/vault-acl-watchdog.log 2>&1 +``` +SILENT если всё ок; чинит + докладывает если сломано. + +⚠️ **Корень НЕ устранён:** `mcpvault` контейнера `hermes-taiga` +(`/mnt/RED_2TB/docker/hermes/docker-compose.yml`, `HERMES_ALLOW_ROOT_GATEWAY=1`, `User:` пусто) +работает от root. Перманентный фикс — добавить `user: "950:950"` в `taiga` service. + Working fix = **full dacl replacement** (not per-ACE edit) with **inheritance flags ON**, applied to every dir: ```python