docs(vault-sync): acl fix closed 236/236 INHERIT+WRITE, watchdog cron, root cause hermes-taiga
This commit is contained in:
1 parent
8b831a85a1
commit
afbf895e2e
2 files changed
+154
-8
No files matched your search
@@ -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`).
|
||||
|
||||
@@ -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 '<one json with path + dacl + options>'
|
||||
# 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 <repo>/objects/XX/.wrtest && echo WRITE_OK # у КАЖДОГО каталога
|
||||
# + тест наследования: mkdir objects/zzz → getacl → должен быть INHERITED:True → touch внутрь
|
||||
# + end-to-end: git clone <repo> && 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
|
||||
|
||||
Reference in new issue
Block a user