Commit 4c1ba069 authored by Vitaly Lipatov's avatar Vitaly Lipatov

memory: add feedback rules and lessons learned

parent 6c0e9492
---
name: feedback_epm_install_girar_task
description: "Пакеты из girar-задания ставить через `epm install <task_id>`, НЕ ручным `apt-repo add task`"
metadata:
node_type: memory
type: feedback
originSessionId: 6700b56b-1c35-41d0-b49b-3fb6389228a9
---
Для установки/тестирования пакета из girar-задания ALT — одна команда:
```
epm install <task_id> # напр. epm install 424260
epm install --full <task_id> # если нужны даже -devel/-debuginfo (по умолчанию excluded)
```
epm сам распознаёт числовой аргумент (`is_taskarg` в `/usr/share/eepm/epm-install``epm_install_alt_tasks` в `epm-install-alt`), создаёт **временный** apt-config, добавляет туда `http://git.altlinux.org/repo/<task>/x86_64/task`, делает `apt-get update`, ставит пакет(ы), и убирает temp-config — реальный `sources.list` не меняется. Также можно `epm install <URL задачи>` (`is_taskurl`).
**НЕ делать вручную** `apt-repo add task <N>``apt-get update``epm install <pkg>``apt-repo rm` — это обход epm и засоряет реальный sources.list. См. [[feedback_use_epm_for_packages]], [[feedback_never_bypass_epm_play]].
**Why:** epm — единый интерфейс для пакетных операций на ALT; у него есть готовый механизм для task-репо, который к тому же изолирует изменения в temp-config (не оставляет мусора в sources.list). Ручной apt-repo add нарушает «пакеты ТОЛЬКО через epm».
**How to apply:** получил girar task `NNNNNN [...] pkg.git=ver-alt1``ssh root@<host> 'epm install NNNNNN'`. Граничный случай: apt внутри epm может соврать в выводе «Последняя версия уже установлена / 0 будет обновлено» — сверяться с `rpm -q <pkg>` (наблюдалось на e5/telemt 3.4.18→3.4.22, при этом пакет реально обновился). Применено: [[reference_telemt_fleet_ja4_monitoring]] (e5, task #424260, 2026-07-05).
---
name: feedback_ip_from_reverse_zone
description: "Свободные IP проверять через reverse zone DNS, при занятии вписывать PTR"
metadata:
node_type: memory
type: feedback
---
Свободные IP в подсети 91.232.225.0/24 проверять через файл обратной зоны на ns1 (`/var/lib/bind/zone/225.232.91.in-addr.arpa`). Когда берём IP — сразу вписываем PTR-запись в зону, обновляем serial, reload.
**Why:** ping-проверка ненадёжна (хост может быть выключен), а reverse zone — единственный источник правды о занятых IP.
---
name: feedback_no_manual_file_copy
description: "НЕ копировать файлы в систему вручную только через установку пакетов (epm)"
metadata:
node_type: memory
type: feedback
---
НИКОГДА не копировать библиотеки, бинарники и прочие системные файлы напрямую (cp/scp) — всегда устанавливать через пакетный менеджер (epm). Даже если пакета нет в репозитории — собрать/перепаковать через epm, а не копировать файлы.
---
name: feedback_notes_in_repository
description: "Все заметки вести ТОЛЬКО внутри репозитория (.claude/memory/ или .claude/docs/), не в session memory"
metadata:
node_type: memory
type: feedback
---
Все заметки, справки и changelog — ТОЛЬКО внутри репозитория (`.claude/memory/` для feedback/reference/lesson, `.claude/docs/` для справочных документов). Session memory (notes.md) — только как промежуточный буфер во время работы, после завершения задачи всё переносить в репо.
---
name: read_repository_instructions_first
description: Перед ответом о правилах проекта находить и полностью читать фактические файлы инструкций
type: feedback
---
Перед любым утверждением о том, какие инструкции прочитаны или какой файл является базовым, сначала найти и полностью прочитать фактические `AGENTS.md`, `CLAUDE.md` и другие применимые файлы инструкций в рабочем дереве.
Поиск должен включать скрытые, ignored и untracked файлы: обычный вывод `rg --files` может их не показать. Не опираться только на текст, переданный в сообщении, и не заявлять, что файла нет, без такой проверки.
При конфликте или различиях между файлами явно сообщить об этом и соблюдать более локальные инструкции. Для вопросов об инфраструктуре после чтения инструкций сначала смотреть `.claude/memory/`.
--- ---
name: feedback-reverse-zone-ip-allocation name: feedback-reverse-zone-ip-allocation
description: "При выделении IP в наших подсетях ВСЕГДА проверять через файл обратной зоны не только dig/ping), и сразу записывать туда взятый IP" description: "При выделении IP: ВСЕГДА писать в ОБЕ зоны прямую (A/AAAA) и обратную (PTR). Обратная зона наш учёт занятости IP; перед выбором свободного IP проверять через её файл"
metadata: metadata:
node_type: memory node_type: memory
type: feedback type: feedback
...@@ -16,7 +16,11 @@ metadata: ...@@ -16,7 +16,11 @@ metadata:
2. **Сразу записывать взятый IP** в reverse zone (PTR-запись + при необходимости комментарий о владельце/назначении), даже если хост не сразу поднят. Иначе тот же IP кто-то возьмёт повторно. 2. **Сразу записывать взятый IP** в reverse zone (PTR-запись + при необходимости комментарий о владельце/назначении), даже если хост не сразу поднят. Иначе тот же IP кто-то возьмёт повторно.
**Why:** Пользователь явно сказал «проверяй через файл обратной зоны. и всегда туда записывай взятые» (2026-05-28). До этого я взял `91.232.225.150`, опираясь на пустой `dig -x` и ping — а в файле там был комментарий «150-169 — адреса для контейнеров на devel.etersoft.ru», т.е. зарезервированный пул. 3. **Писать в ОБЕ зоны одновременно.** Когда IP задействован и для него есть домен (hostname) — добавлять и forward (A для v4 / AAAA для v6), и reverse (PTR). Не оставлять IP «в работе, но без DNS»: так dgw/dvpn выглядели IPv4-only при отсутствии AAAA, хотя ::12/::182 на интерфейсе были. Проверять парность: A↔PTR, AAAA↔PTR.
**Обратная зона — наш учёт занятости IP** (source of truth для «какие IP заняты»). Должна быть полной и актуальной: каждый задействованный IP → PTR (+ комментарий-назначение при необходимости).
**Why:** Пользователь явно сказал «проверяй через файл обратной зоны. и всегда туда записывай взятые» (2026-05-28). До этого я взял `91.232.225.150`, опираясь на пустой `dig -x` и ping — а в файле там был комментарий «150-169 — адреса для контейнеров на devel.etersoft.ru», т.е. зарезервированный пул. Дополнение (2026-07-13): «всегда, когда задействуешь IP и добавляешь для него домен — записывай и в прямую, и в обратную зону; по обратной зоне ведём учёт занятости IP».
**How to apply:** **How to apply:**
- В dns skill при запросе «find free IP / verify free IP» всегда читать reverse zone file целиком (через ssh ns1 sudo cat), искать как PTR-записи так и комментарии-резервы. - В dns skill при запросе «find free IP / verify free IP» всегда читать reverse zone file целиком (через ssh ns1 sudo cat), искать как PTR-записи так и комментарии-резервы.
......
---
name: feedback_rewrite_preserve_full_config
description: "Переписывая конфиг-файл, ВСЕГДА сохраняй полное содержимое включая секретные строки (Database*/password). Маскируй только для ДИСПЛЕЯ (grep -v), не для перезаписи. Правильно: cp bak surgical insert (sed -i), не heredoc из отфильтрованного вида."
metadata:
type: feedback
---
# Переписывая конфиг — сохраняй ПОЛНОЕ содержимое, включая секреты
**Why:** При фиксе secure-cookie на rt-test я читал `/etc/rt/RT_SiteConfig.d/local.pm`
через `grep -vE "Database|passw"` (маскировал секреты для дисплея), а затем
переписал файл heredoc'ом **из этого замаскированного вида** — потеряв строки
`Set($DatabaseHost,'localhost')` / `Set($DatabaseRTHost,'localhost')`. RT ушёл
коннектиться на старый `mysql.auth.dmz` (MySQL 5.5.43) → version-guard `RT 5.0.0
unsupported on MySQL < 5.7.7` → воркеры crash-loop → 502 ~5 мин на rt-test.
Пришлось восстанавливать из бэкапа. См. [[lesson_rt6_secure_cookies_http]].
**How to apply:**
- Перед правкой конфига: `cp -a file file.bak-<date>-<reason>` (бэкап — обязателен).
- Правь **in-place** хирургически: `sed -i` вставка/замена конкретной строки,
`Edit` по уникальному `old_string`. НЕ переписывай весь файл heredoc'ом, если
не знаешь его ПОЛНОГО содержимого слово-в-слово.
- Маскировка секретов (`grep -v password`, `sed s/passw/***/`) — **только для
вывода в чат/лог**. Никогда не корми этим маскированным выводом команду записи.
- Если всё-таки переписываешь весь файл — сначала `cat` оригинал БЕЗ фильтрации
(или читай из `.bak`), скопируй дословно, добавь своё.
- Универсально: данные для записи = копия источника, а не его отредактированное
представление.
---
name: feedback_route_list_symlink_only
description: ".list файлы в routes.d/routes6.d на igw ТОЛЬКО симлинки, не regular files"
metadata:
node_type: memory
type: feedback
originSessionId: e7921933-7fc7-4871-889c-9d5efe71c046
---
При добавлении списка маршрутов в `routes.d/<group>/` или `routes6.d/<group>/` файл `.list` должен быть **символической ссылкой** на источник, а не обычным файлом.
Источники обычно: `/root/egw-route/<name>.list`, `/home/routeweb/route-web-api/<name>.list`, `/root/antifilter/*.lst`.
**Why:** Конвенция route-update.sh на igw — все реальные `.list` это симлинки (живут в `/root/egw-route/` и т.п., раздаются между хостами/группами). Единственные regular-файлы — `routes.d/hetzner/hetzner-dummy.list` и `routes6.d/hetzner/hetzner-dummy.list` (пустые placeholder'ы для dummy-группы).
**How to apply:** Создавая новую группу/список — `ln -s /root/egw-route/NAME.list routes.d/GROUP/NAME.list`, а не `echo > ... .list`. Касается и IPv4 (`routes.d/`), и IPv6 (`routes6.d/`). См. [[reference_igw_route_api]] (HTTP API поверх тех же списков).
---
name: feedback-test-servers-on-dmz
description: "Тест-серверы/контейнеры поднимать в сети 10.20.30.x (инфра/DMZ), НЕ в 192.168.0.x (office LAN)"
metadata:
type: feedback
---
Тестовые серверы/контейнеры (стенды, копии боевых сервисов) создавать в сети **10.20.30.x** (инфраструктурная/DMZ), а **не** в **192.168.0.x** (office LAN).
**Why:** 10.20.30.x — сеть, где живут боевые сервисы (RT 10.20.30.53, mysql.auth 10.20.30.202 и т.д.). Тестовый стенд рядом с продом → одинаковое окружение, маршрутизация и доступ (админка, мониторинг, тот же сегмент, что и копируемый сервис). 192.168.0.x — офисная клиентская LAN, не место для серверов-стендов.
**How to apply:** при создании тест-CT/VM — ставить на мост/сеть 10.20.30.x, IP из диапазона (190–199 эксперименты, 150–169 devel, см. [[lesson_dns_free_ip_check_both_zones]]). **Сначала подтверждать сеть**, не молча брать дефолтный мост ноды (vmbr0 gefest = 192.168.0.x → ошибся). Связано: [[reference_create_lxc_container_pve]], [[feedback_ip_openness_tiers]].
**Конкретный случай:** rt-test ошибочно создан на gefest vmbr0 = 192.168.0.191 (office), надо было 10.20.30.x. По указанию пользователя — оставлен как есть, не пересоздаём. См. [plans/rt-update.md](../plans/rt-update.md).
---
name: lesson_clone_container_iptables
description: "Клонирование LXC: iptables на хосте не копируются автоматически, нужно настроить SNAT/DNAT вручную"
metadata:
node_type: memory
type: lesson
---
## Проблема
При клонировании LXC-контейнера (pct clone) iptables-правила на хосте НЕ копируются. Контейнер получает новый IP, но на хосте нет SNAT-правил для него → контейнер не может выйти в интернет (L2 работает, L3 нет).
## Симптомы
- `arping` от шлюза отвечает (L2 ok)
- `ping` до шлюза не проходит (L3 fail)
- `ntpd` внутри контейнера падает с segfault каждые ~8 сек (симптом отсутствия сети)
- Нет iptables DROP/REJECT правил — просто отсутствуют SNAT-правила
## Решение
После клонирования вручную добавить SNAT-правила на хосте:
```bash
iptables -t nat -A POSTROUTING -s <новый_IP> -j SNAT --to-source <новый_IP>
```
Проверить: `iptables -t nat -L POSTROUTING -n -v` — должны быть правила для каждого нового IP.
## Связано
- Создание dvpn/fvpn/gvpn.eterfund.ru (CT 750/751/752 на enceladus)
- Клонирование CT 258 (vpn.eterfund)
---
name: lesson_eterban_priv_two_wan_slots
description: "eterban на priv: ровно 2 WAN-интерфейса (i_interface/i_interface2), правила dynamic и НЕ в /etc/sysconfig/iptables из-за ipset-зависимости"
metadata:
node_type: memory
type: reference
originSessionId: 83749c8e-8122-4126-8133-edc68ee50ad1
---
На **priv** `eterban` (пакет `eterban`, бинарник `/usr/bin/eterban`, сервис `/usr/share/eterban/eterban_switcher.py``eterban.service`) управляет IP-банами. Ключевые неочевидные факты (выяснены 2026-07-07 при добавлении 3-го WAN `enp8s0f0`).
> **Обновлено 2026-07-10**: eterban 0.11 поддерживает `i_interfaces` (список WAN). Ниже — история и текущее состояние.
## 1. WAN-интерфейсы: `i_interfaces` (список) вместо `i_interface`/`i_interface2`
До версии 0.11 — ровно 2 WAN-интерфейса, хардкод. С версии **0.11-alt1** `settings.ini` поддерживает `i_interfaces` (comma/space-separated список):
```ini
[Settings]
i_interfaces = ether1, ether3, enp8s0f0.3210, enp8s0f0.3211
internal_interface = vmbr0
ban_server = 91.232.225.67
```
Обратно совместимо: старые `i_interface`/`i_interface2` продолжают работать (assembleются в список). Рефакторинг: 4 iptables-функции (`create_iptables_rules`, `create_ip6tables_rules`, `destroy_iptables_rules`, `destroy_ip6tables_rules`) заменены с copy-paste per-interface на `for iface in wan_ifaces` цикл. Код: commit `9ac5f23` в `/srv/lav/Projects/git/eterban`.
## 2. Правила eterban — ДИНАМИЧЕСКИЕ, в файле их НЕТ
Все правила eterban (FORWARD `eterban_1` REJECT, nat PREROUTING `eterban_white`/`eterban_1`/`firehol_level1` DNAT→.67 — для ether1/ether3/vmbr0) создаются `create_iptables_rules()` через `iptables -I/-t nat -I PREROUTING` при старте `eterban.service`. **В `/etc/sysconfig/iptables` их ноль.** Live = файл + ~15 динамических eterban-правил.
## 3. ПОЧЕМУ не в файле — ipset-зависимость = риск падения iptables.service
Правила ссылаются на ipset'ы (`eterban_1`, `eterban_white`, `firehol_level1`, v6-аналоги), которые eterban создаёт в рантайме. Если положить такие правила в `/etc/sysconfig/iptables`, то при загрузке `iptables.service` стартует ДО создания ipset'ов → `iptables-restore` падает на правиле с `--match-set`**весь файрвол не грузится (atomic fail)** → бордер голый. Поэтому ipset-зависимые правила eterban'а НИКОГДА не класть в `/etc/sysconfig/iptables`.
## 4. Как защищать новый WAN до обновления eterban
- **Статические правила** (без ipset — INPUT service-port REJECT, FORWARD :1080 DROP, anti-spoof, SNAT) → кладутся в `/etc/sysconfig/iptables` (twin к ether3), персистентны.
- **eterban-правила для нового WAN** → только live (`iptables -I FORWARD 1 -i enp8s0f0 ... --match-set eterban_1 ...` + 3 nat PREROUTING), переживают `systemctl restart eterban` (eterban трогает только свои 2 слота), но **НЕ переживают ребут**. Для персистентности без правки кода — sidecar systemd-сервис `After=eterban.service Requires=eterban.service`, выполняющий те же 4 команды (ipset'ы к тому моменту уже есть).
## 5. Диагностика
- `sudo iptables-save | grep -E 'eterban|firehol'` — все eterban-правила live.
- `sudo iptables -S FORWARD --line-numbers` работает для `-L`, но **НЕ для `-S`** (`-S` игнорирует `--line-numbers` → грабли для позиционных insert'ов).
- `sudo iptables -t nat -S PREROUTING` — DNAT-правила.
Связано: [[reference_ikev2_fr_tunnel]] (.140 — один из защищаемых :1080-прокси), [[objects/priv]] (changelog), [[feedback_no_manual_override]].
---
name: lesson_eterban_redis_persistence
description: "eterban на priv хранит баны в Redis (persistence + веб-интерфейс); требует redis-py ≥3.5.0, баг hset(mapping=) молча ломал его"
metadata:
node_type: memory
type: reference
originSessionId: 0bd9d6fd-a7c9-4651-8420-03c4608e5495
---
eterban на priv (91.232.225.1) — архитектура persistence:
- **Ядро**: ipset `eterban_1` (IPv4) + `eterban_1_ipv6` — сами блокировки живут в kernel, переживают restart *службы* (switcher на старте пытается `ipset create` уже существующие наборы → cosmetic `Set cannot be created: ... already exists`, безобидно, чинить не надо). Переживают reboot — НЕТ.
- **Redis**: хранилище метаданных банов (источник/причина/время). Пишется в `/usr/share/eterban/autoban_manager.py:108`: `self.r.hset(meta_key, mapping=metadata)`. Из Redis читает **веб-интерфейс** eterban (eterban.etersoft.ru / eterban.eterhost.ru) и из него же eterban **восстанавливается после reboot**.
Баг redis-py: `mapping` kwarg у `redis.hset` появился только в 3.5.0. На priv (ALT p10) стоял redis-py **3.4.1** → на каждый бан в журнале: `AutoBanManager.on_ban error: hset() got an unexpected keyword argument 'mapping'`. Блокировки в ipset при этом шли (отдельная ветка кода), но **Redis оставался абсолютно пустым** → веб-интерфейс показывал 0 банов, а после reboot priv всё обнулилось бы. Тихая деградация — заметили по пустому веб-интерфейсу.
Фикс 2026-07-05: `p11/python3-module-redis-py` (4.5.5) поверх p10 + `serv restart eterban` (рестарт ОБЯЗАТЕЛЕН — старый процесс держит старую либу в памяти). После рестарта ошибка ушла, Redis начал заполняться.
Риски/заметки:
- **Cross-branch install** (p11-пакет на p10) — может откатиться к 3.4.1 на ближайшем `epm update`/`epm ei` → eterban снова сломается молча. Пинить пакет или чинить upstream: eterban реально требует redis-py ≥3.5.0, а p10 даёт 3.4.1 (баг упаковки eterban → в их багзиллу).
- Баны, жившие в ipset на момент фикса (~49), в Redis отсутствуют → при reboot пропадут (перебанятся быстро fail2ban на источниках).
- Веб-интерфейс eterban на priv обслуживается **php7-fpm**: nginx `eterban.conf` (:81) + `eterban.eterhost.ru.conf` (:80/:443) → `fastcgi_pass unix:/var/run/php7-fpm/php7-fpm.sock`. php7 на priv НЕ лишний — удалять нельзя (ломает бан-страницу).
---
name: grafana-influxql-total-line
description: "How to add a sum/total line on a Grafana InfluxQL chart (server subquery, not transformation); Playwright MCP tiny-viewport gotcha"
metadata:
node_type: memory
type: reference
originSessionId: 6fb73bdf-7ee1-4271-9301-e6203315442f
---
Добавление линии суммы (Σ total) на Grafana-график с InfluxQL (InfluxDB 1.x OSS, db `gateways`, Grafana на https://telegraf.office.etersoft.ru , админ-пароль `ssh rooter@telegraf pass show grafana`). Пример: дашборд **BIRD BGP — priv**, UID `bird-bgp` (id 9).
**Правильный способ — серверный подзапрос** (добавить как второй target B в той же панели):
```sql
SELECT sum("last") AS "Σ total"
FROM (SELECT last("<field>") FROM "<measurement>" WHERE ("gateway" = 'priv') AND $timeFilter
GROUP BY time($__interval), "name")
GROUP BY time($__interval)
```
- Внутренний `GROUP BY time, "tag"` + внешний `sum(last())` по bucket'у → корректная сумма без triple-count (несколько scrape'ов на bucket нельзя просто `sum(field)`).
- **`fill(previous)` ЗАПРЕЩЁН внутри внутреннего GROUP BY подзапроса** → InfluxDB парсер падает с "EOF" (HTTP 400). Только во внешнем, либо не нужен — точек и так хватает (подтверждено: 360 точек).
**НЕ работает:** transformation `calculateField` (mode `reduceRow`) — InfluxQL-датасорс возвращает **ОТДЕЛЬНЫЙ dataframe на каждую серию** (по пирам), а reduceRow суммирует только внутри одного frame → сумма пустая. Для total нужна серверная агрегация, не клиентская.
**Override для линии суммы** (fieldConfig.overrides): matcher `byRegexp` с `options: "Σ total"` **без якорей**. Grafana byRegexp — substring-матчинг; имя поля для InfluxQL = `<measurement>.<alias>` (напр. `bird_bgp.Σ total`), так что `(?i)^(sum|total)...$` с якорями НЕ сматчится. Свойства: `color.mode=fixed, fixedColor=yellow`, `custom.lineWidth=3`, `custom.fillOpacity=0`, `custom.showPoints=never`.
**Проверка без браузера:** Grafana datasource proxy `POST /api/ds/query` с `{queries:[{refId, datasource:{type,influxdb,uid:bfd9ix15wbqpse}, format:"table", rawQuery:true, query}], from, to}`. Ответ: `results.<refId>.frames[]`, каждый frame = `{schema:{name, fields:[{name, config:{displayNameFromDS}}]}, data:{values:[[...]]}}`.
**Грабли Playwright MCP (главная причина «не вижу сумму»):** дефолтный viewport крошечный (~780×493). Grafana **НЕ рендерит панели вне экрана**`browser_find`/`browser_evaluate`/скриншот по нижним панелям ничего не находят, `document.body.innerText` короткий (только верхняя панель). Лечение: `browser_resize` до ≥1280×900 → `scrollTo(0, <panel y>)` → wait 2-3s → проверка. Так легенда Routes Imported нашлась: `bird_bgp.Σ total — 3.17 Mil` (= gbl_v4 1.08M + petrosvyaz4 1.09M + …), на Routes Exported — `10`.
Связано: [[reference_route_update_sh]] (маршруты на igw), InfluxDB `gateways` (метрики шлюзов).
---
name: lesson-lxc-etcnet-to-systemd-networkd
description: "Новый LXC из etcnet-шаблона 720: PVE сам пишет /etc/systemd/network/<iface>.network ставим systemd-networkd и работает; `systemctl disable network` НЕ Enough (etcnet оживает на boot), надо mask"
metadata:
node_type: memory
type: lesson
originSessionId: af3c1e20-0226-4780-81be-f8c8bb9702c3
---
# Перевод нового LXC (шаблон 720, ALT p11) с etcnet на systemd-networkd
Шаблон CT 720 — **etcnet**-based (`/etc/net/ifaces/`), и в этом грабли: PVE `pct set -net0` **не пробивает** в etcnet (`ipv4address` остаётся шаблонным, `default`/шлюз отсутствует). Решение — уйти на **systemd-networkd** (см. заметку в CLAUDE.md про proxmox_create_ct: «pick a systemd-networkd image»).
## Процедура (отработана на CT 369 mariadb.dmz.etersoft.ru, 2026-07-16)
1. PVE при клонировании **уже пишет** `/etc/systemd/network/breth0.network` с корректным статическим конфигом (Match=Name, Address, Gateway, DHCP=no, IPv6AcceptRA=false) — **ничего править не надо**.
2. `systemd-networkd` **не установлен** в шаблоне → `epm install -y systemd-networkd` (через `pct exec` нужен `TMPDIR=/tmp`, т.к. нет pam-сессии → `/tmp/.private/root` не создан).
3. `systemctl enable --now systemd-networkd``networkctl status breth0` = `routable (configured)`, online.
4. **🔴 `systemctl disable network` НЕ достаточно!** etcnet (`network.service`, SysV) оживает на следующем boot'е (его тянет статическая зависимость, а не rc-ссылки — `is-enabled` показывает `disabled`, но `Active: active`). Надо **`systemctl mask network`** (symlink → `/dev/null`) — только тогда etcnet не стартует.
5. Удалить конфиг etcnet: `rm -rf /etc/net/ifaces/breth0`.
6. Ребут (через `pct stop` + `pct start``pct reboot` ругается `failed to connect to monitor socket`, но контейнер перезагружается) → проверить: `network` inactive+masked, `systemd-networkd` active, `systemctl --failed` пусто.
Пакет `etcnet` при этом остаётся установлен (инертен — service замаскирован). Удалять пакет рискованно (фундамент ALT-сети).
## Альтернатива: остаться на etcnet (CT 762 vent.office.etersoft.ru, 2026-07-21, vmbr0)
На vmbr0 (office LAN) логичнее не тянуть networkd, а починить etcnet — но тот же root-cause:
- **Root cause общий:** PVE создаёт интерфейс с именем из `pct set --net0 name=...` (у меня `eth0`) и пишет под него `/etc/systemd/network/<name>.network`, а etcnet-шаблон 720 настроен на **другое** имя (`breth0`, с шаблонным IP 10.20.30.246). И etcnet, и networkd-config висят мимо реального интерфейса → **IP не поднимается**.
- Фикс (etcnet): настроить `/etc/net/ifaces/eth0/` (ipv4address, ipv4route `default via <gw>`, options BOOTPROTO=static TYPE=eth ONBOOT=yes), удалить `breth0` и мёртвые `/etc/systemd/network/*.network`.
- **🔴 `network` (etcnet) в шаблоне `active`-но-`is-enabled=disabled`** — стартует в текущем boot'е, но **после ребута не поднимется**`systemctl enable network` (обязательно проверить `is-enabled` после клонирования!).
- `pct exec` здесь тоже спотыкался о perl-taint — `env -i HOME=/root PATH=... pct exec ...` (или скрипт через `pct exec -- bash -s` по base64-каналу).
## Связанное
- `pct exec` спотыкается о perl-taint `Insecure $ENV{ENV}` — лечится `unset ENV PERL5OPT PERL5LIB` в SSH-сессии на ноде. Для постоянной работы — [[feedback-lxc-via-ssh-not-pct]] (ходить через `ssh root@<ct-ip>`, не pct exec).
- loadavg в контейнере = нагрузка **хоста** (LXC не изолирует loadavg) — не пугаться «load 13» на свежем CT.
- [[reference-create-lxc-container-pve]] (процедура клонирования), [[plans-rt-update]] (mariadb.dmz для RT).
---
name: lesson_never_touch_prod_without_command
description: "НИКОГДА не трогать прод без явной команды. git checkout --orphan + git rm -rf на прод-репозитории Bugzilla —差点 убил прод. Все git-операции только на тестовом стенде."
metadata:
type: lesson
severity: critical
---
# НИКОГДА не трогать прод без явной команды
## Инцидент (2026-07-26)
При создании ветки version-5.9 для Bugzilla я запустил на **прод-сервере** (bugs.etersoft.ru):
```bash
git checkout --orphan version-5.9
git rm -rf .
```
Это удалило ВСЕ файлы прод- Bugzilla. Восстановлено через `git checkout master && git reset --hard HEAD`.
## Правило
**Пока нет явной команды на обновление — прод НЕ ТРОГАТЬ.** Все git-операции, создание веток, cherry-pick — только на тестовом стенде (CT 944, bugs.pr.etersoft.ru).
Прод трогаем ТОЛЬКО после:
1. Полного тестирования на тестовом стенде
2. Явной команды пользователя "обновляй прод"
3. Бэкапа перед обновлением
## Правильный workflow
1. Работа с git: только на тестовом контейнере или локально
2. Тестирование: на bugs.pr.etersoft.ru
3. Прод: только чтение (проверка, дампы), никаких изменений без команды
---
name: lesson_no_cpan_only_epm
description: "Никогда не использовать CPAN напрямую для установки Perl-модулей. Только epm install. CPAN ставит в local::lib который может быть не виден."
metadata:
type: lesson
severity: critical
---
# Только epm install, не CPAN
## Правило
**Пакеты можно ставить ТОЛЬКО через `epm install`.** НИКОГДА не использовать:
- `perl -MCPAN -e 'install Module'`
- `/usr/bin/perl5.34.3 -MCPAN -e "CPAN::Shell->notest('install', ...)"`
- `cpan Module::Name`
- Любые другие CPAN-команды
## Почему
1. **CPAN ставит модули в local::lib** (`/root/perl5/lib/perl5/`) — checksetup.pl и другие скрипты могут их не видеть
2. **epm ставит системно** — модули доступны всем Perl-скриптам
3. **epm отслеживает зависимости** — CPAN нет
4. **Пользователь категорически запретил** CPAN: "и никогда вот так!!"
## Как
```bash
# Правильно:
epm install perl-Module-Name
# Неправильно:
perl -MCPAN -e 'install Module::Name'
```
## Если пакета нет в ALT
Если `epm search` не находит нужный модуль — сообщить пользователю, составить перечень недостающих пакетов для сборки. Не пытаться ставить через CPAN.
---
name: lesson_pam_system_check_localuser_uid500
description: "ALT поднял порог в PAM system-check-localuser (legacy) до uid>=1000 (даже в legacy-режиме). Доменные SSSD-юзеры Etersoft живут с uid 500 юзеры uid 500-999 при логине попадают в default=bad и отваливаются. Фикс: dc-client/install-localuser-etersoft.sh (создаёт отдельный system-check-localuser-etersoft, порог 65536→500, переживает апдейт пакета) + control system-check-localuser etersoft. Применено на lav(192.168.0.55)/builder64/white 2026-07-21."
metadata:
type: lesson
---
# PAM system-check-localuser: порог UID 1000 ломает SSSD-логин юзеров 500-999
## Симптом
ALT Linux поднял «минимальный UID» до **1000 даже в legacy-режиме** PAM-роутера
`/etc/pam.d/system-check-localuser-legacy`. Доменные SSSD-юзеры Etersoft начинаются
с **uid 500** (напр. `lav` = uid 502). В итоге юзер не из `/etc/passwd` с uid 500-999
попадает в `pam_succeed_if.so uid >= 1000` = false → `default=bad`**логин падает**.
Роутер подключается из `system-auth-sss` (auth/account/password/session include
`system-check-localuser`). Логика:
```
pam_localuser.so -> юзер в /etc/passwd -> local-only (локальный auth)
pam_succeed_if.so uid >= N -> N=500: sss-only (SSSD auth)
-> иначе default=bad (FAIL)
```
## Важно про машину `lav`
- Доменный юзер **`lav`** (Виталий Липатов, uid 502) — через **sss**, НЕ в `/etc/passwd`.
Логинится на нескольких машинах (все AD-joined, nsswitch `passwd: files sss …`).
- **`lav` = хост 192.168.0.55** (`lav.office.etersoft.ru`, десктоп, см. local-admin/lav_desktop.md) —
это **НЕ** `builder64`. Claude-сессия обычно крутится **на builder64** (`builder64.office.etersoft.ru`),
отдельном хосте. Не путать: юзер `lav` ↔ хост `lav` ↔ хост `builder64` — три разные сущности.
## Фикс (робастный, repo-скрипт)
`dc-client/install-localuser-etersoft.sh` (вызывается из `dc-client/tune_sssd.sh`):
1. Генерит `/etc/pam.d/system-check-localuser-etersoft` из `…-systemd` sed'ом `65536→500`
(отдельный файл → **не перетирается апдейтом пакета** pam/setup).
2. `control system-check-localuser etersoft` — фасилити `control` **динамически регистрирует
любое значение из существующего `$CONFIG-*` файла**, так что `etersoft` валиден сразу
после создания файла.
```sh
cd <repo>/dc-client && sudo sh ./install-localuser-etersoft.sh
sudo control system-check-localuser etersoft # уже внутри скрипта
```
Хрупкий вариант (как было до фикса): ручной `sed 1000→500` в `…-legacy` — апдейт пакета
возвращает порог обратно → логин доменных юзеров отвалится «ни с того ни с сего».
## Верификация
- `pamtester system-auth <user> acct_mgmt` + `open_session` (есть не везде — builder64 есть, white нет).
- Реальнее: **свежий ssh-логин** доменным юзером (`ssh white 'id'`) ПОСЛЕ переключения —
упражняет account+session PAM через новый роутер. (pamtester/ssh в уже открытой сессии
НЕ доказывает post-change вход — нужна новая сессия.)
- Сводно: `control system-auth` (=sss) + `control system-check-localuser status` (=etersoft)
+ `grep 'uid >=' /etc/pam.d/system-check-localuser-etersoft` (=500 во всех 4 фазах).
## Состояние машин (2026-07-21)
| Хост | control | basis `-etersoft` | проверка |
|---|---|---|---|
| lav (192.168.0.55) | etersoft (уже было) | legacy-шаблон `[success=2 default=bad]` | — (не трогали) |
| builder64 | etersoft (поставлен) | systemd-шаблон (скрипт) `[success=2 auth_err=ignore default=bad]` | pamtester ✅ |
| white (192.168.0.37) | etersoft (поставлен) | systemd-шаблон (скрипт) | fresh ssh-login lav uid=502 ✅ |
Два «аромата» `-etersoft` (legacy-based на lav, systemd-based на builder64/white) **функционально
эквивалентны** для uid>=500 (оба → sss-only); разница только в пути для non-local uid<500
(оба в итоге fail). Левый ручной `…-legacy` с uid>=500 на builder64/white оставлен (harmless,
defensive — если control когда-то откатится на legacy, доменный логин всё ещё работает).
## На новых машинах / после апдейта `pam`
Проверять `control system-check-localuser status` = `etersoft`. Если слетело на legacy/systemd
(или `-etersoft` файла нет) — прогнать скрипт заново. Это поведенческая разница после upgrade
pam/setup (UID_MIN 500→1000), всплывает не сразу.
Связанные: [[reference_pam_limits_desktop_builder64]] (другой pam-лимит на builder64),
dc-client/tune_sssd.sh (AD-join), [[lesson_lxc_etcnet_to_systemd_networkd]].
---
name: lesson_restart_container_verify_autostart
description: "После настройки новой системы (cutover/миграция/новый сервис) перезапустить КОНТЕЙНЕР целиком и перепроверить всё, что должно запускаться (cron в /etc/cron.d, systemd-timers, сервисы). Долго живущий демон не всегда перечитывает новый конфиг в LXC (inotify-провал). RT fetchmail outage 9.5ч (2026-07-23): rt-fetchmail создан при cutover, но crond не перечитал /etc/cron.d."
metadata:
type: lesson
---
# После настройки новой системы: перезапусти контейнер и перепроверь автозапуск
## Правило
Сконфигурировал новую систему (миграция/cutover/разворот сервиса) — **перезапусти контейнер целиком**
(`pct reboot <vmid>` / `systemctl reboot`), потом **поэлементно проверь, что всё заявленное к автозапуску
действительно стартует**: cron-задачи в `/etc/cron.d`, systemd-timers, нужные сервисы. НЕ доверять
«файл положил = работает».
## Почему именно перезапуск контейнера (а не только рестарт нужного демона)
- Долго живущий демон мог не перечитать конфиг. **Хрестоматийный случай — crond (vixie-cron) в LXC:**
он читает `/etc/cron.d` при старте и по inotify на изменения. **В контейнере inotify на overlay-ФС
срабатывает не всегда** → новый/пересозданный cron-файл молча не подхватывается.
- Часовые/дневные job'ы (загруженные при старте crond) продолжают работать — поэтому проблема
**незаметна**, пока не посмотришь на нужный `*/N` job или не проверишь mtime лога сервиса.
- Перезапуск контейнера = свежий старт всех демонов = гарантированное перечитывание конфигов =
ловит весь класс «положил файл, но никто его не перечитал».
- В моменте достаточно `serv restart <daemon>` (или `killall -SIGHUP crond`), но это лекарство, а не
проверка — перезапуск контейнера проверяет ВСЁ сразу.
## Случай RT (2026-07-23, ~9.5 ч простоя забора почты)
- Cutover RT 4.4.9→6.0.3 на CT 412 (2026-07-16): в `/etc/cron.d` создан `rt-fetchmail` (`*/3`).
crond крутился с 14:15 того же дня и **в LXC не словил появление файла** (а в 23:58 22 июл файл ещё
и переименовали в `.bak.pre-rt603`, в 00:55 пересоздали — crond потерял след).
- Забор почты встал с 23:57 22 июл до ~09:45 23 июл. Часовые job'ы apache при этом шли — поэтому
простой был неочевиден.
- Лекарство в моменте: `serv restart crond` → перечитал `/etc/cron.d``*/3` rt-fetchmail пошёл.
- **Этого бы не было, перезапусти CT 412 на cutover и проверь, что fetchmail реально бежит**
(тестовым письмом end-to-end — см. [[lesson_rt_mail_pickup_test]]: тикет появился в БД = забор работает).
## Как проверять после рестарта
- cron: `journalctl -u crond --since "X min ago"` — видны pam-сессии для юзера job'а (напр. `apache`)
на границах `*/N`; если их нет — cron-файл не подхвачен.
- systemd: `systemctl list-timers`, `systemctl --failed`.
- Прикладной лог: mtime/хвост лога сервиса должен расти на нужных интервалах (fetchmail →
`/var/log/rt/fetchmail_rt.log`).
- Для забора почты в RT — слать тестовое письмо и смотреть тикет в БД (end-to-end), см.
[[lesson_rt_mail_pickup_test]].
## Каталог /etc/cron.d режимом 0700 — это нормально (пакетный)
`vixie-cron-4.1.20060426-alt10.3` декларирует `/etc/cron.d` как `0700` (`drwx------`) — **так и должно
быть**, crond (работает как root) его читает. НЕ «чинить» на 0755. Проверка: `rpm -V vixie-cron`
(нет флага `M` у `/etc/cron.d` = режим совпадает с пакетом). Права на каталоги/файлы брать из пакета,
не выдумывать — см. общее правило [feedback_verify_before_stating.md].
Связанные: [[reference_rt_etersoft_ru]], [[lesson_rt_mail_pickup_test]],
[feedback_verify_before_stating.md], [feedback_no_unauthorized_actions.md].
---
name: lesson_rt6_runtime_deps_missed
description: "RT 6.0: make testdeps и Requires: пакета rt систематически ПРОПУСКАЮТ Perl-модули, подключаемые runtime-условно (require/use только в кодовом пути конкретной фичи). На rt-test всплыли три: CSS::Inliner (HTML+CSS scrubbing, RT/Util.pm:326), GraphViz2 (графики связей, RT/Graph/Tickets.pm), DateTime::Set (calendar display-mode, RT/Search/Calendar.pm:64). После установки 6.0 ОБЯЗАТЕЛЬНО прокачать каждую фичу энд-до-энд, иначе пропущенный модуль всплывёт у юзера."
metadata:
type: lesson
---
# RT 6.0: runtime-условные Perl-зависимости пропускают testdeps и Requires
`make testdeps` и `rpm --requires rt` видят только модули, подключённые на верхнем
уровне / `use` в всегда-загружаемом коде. RT 6.0 часть фич подключает зависимости
**в runtime условно**`require X` (иногда жёстко, без eval) или `use X` внутри
модуля, который грузится только при обращении к фиче. Эти модули НЕ видны статике
и **не попадают в Requires пакета rt** → их нет на системе → фича падает у юзера
с `Can't locate ...pm`.
## Три всплывших на rt-test (CT 183, RT 6.0.3-alt1, 2026-07-19/20)
| Фича | Модуль | Где грузится | Симптом без модуля |
|---|---|---|---|
| HTML+CSS тикеты (#69737) | **CSS::Inliner** | RT/Util.pm:326 жёсткий `require` | "An internal RT error" на любом тикете с HTML+CSS |
| Графики связей тикетов | **GraphViz2** | RT/Graph/Tickets.pm, RT/Config.pm | падение при рендере графа зависимостей |
| Calendar (display mode) | **DateTime::Set** (+DateTime::Event::Recurrence) | RT/Search/Calendar.pm:64 `use` | "не работает отображение calendar в display mode"; `BEGIN failed--compilation aborted` |
## Решение
- **CSS::Inliner, GraphViz2** — в Сизифе НЕТ, собраны как girar **#426095 (TESTED)**
(+ perl-Badger/Test-Snapshot/HTML-Query). Kanban git-alt #297. См. [[plans/rt-6.0]].
- **DateTime::Set, DateTime::Event::Recurrence****уже в Сизифе** (`epm install
perl-DateTime-Set perl-DateTime-Event-Recurrence`, подтянул perl-Set-Infinite;
DateTime::Span не понадобился). Сборка НЕ нужна. Calendar display-mode после
установки + restart rt-fcgi рендерит события.
## Пакетный баг: Requires у rt неполный
`rpm -q --requires rt` декларирует `perl(DateTime.pm)`, `perl(DateTime/TimeZone.pm)`,
`perl(DateTime/Format/Natural.pm)`**БЕЗ** `perl(DateTime/Set.pm)` (и без
CSS::Inliner/GraphViz2). Поэтому модули, формально существующие в репе, не
ставятся вместе с rt. При переиздании пакета rt добавить:
`Requires: perl(DateTime/Set.pm) perl(DateTime/Event/Recurrence.pm)`
(CSS::Inliner/GraphViz2 — после публикации #426095).
## Метод обнаружения (пригодится)
1. Репро: логин + `curl` эндпоинт фичи под сессией, `-H "Referer: http://<WebDomain>..."`
(Referer должен совпадать с WebDomain/ReferrerWhitelist, иначе RT 6.0 CSRF-гвард
подменит ответ страницей «cross-site request forgery»).
2. `journalctl -u 'rt-fcgi@*' --since '30 sec ago'` + `tail /var/log/rt/rt.log`
виден `Can't locate X.pm ... BEGIN failed--compilation aborted at <file> line N`.
3. Подтвердить под apache: `su -s /bin/sh apache -c '... perl -MRT -e "..."'`
с `require X` по списку подозреваемых.
## Чек-лист cutover 4.4→6.0 (после установки 6.0, ДО отдачи юзерам)
Прокачать энд-до-энд каждую фичу с runtime-депом:
- HTML+CSS тикет (напр. #69737) — CSS::Inliner.
- Граф связей тикета (Dependencies/Graph) — GraphViz2 + `dot`.
- Calendar display-mode (`/Search/Calendar.html` + query) — DateTime::Set.
- (прочее потенциально условное: GnuPG, S/MIME, SMS — если используются).
Каждый miss → `Can't locate ...pm` в journal → `epm install` или girar-сборка.
Связанные: [[plans/rt-6.0]], [[reference_rt_etersoft_ru]], [[lesson_rt6_secure_cookies_http]],
[[lesson_rt_stale_stats_analyze]].
---
name: lesson_rt6_secure_cookies_http
description: "RT 6.0 по умолчанию ставит WebSecureCookies=1 (4.4 нет) на HTTP-only контурах (rt-test) браузер роняет secure-cookie и логин не прилипает: верный пароль принимается, но следующий запрос откатывается на логин; неверный пароль при этом честно показывает ошибку. Фикс: Set($WebSecureCookies, 0) в RT_SiteConfig. Прод CT412 за HTTPS там дефолт ок. Источник: RT/Config.pm:1172 -secure=>WebSecureCookies."
metadata:
type: lesson
---
# RT 6.0: secure-cookie ломает логин на HTTP-only контуре (rt-test)
Симптом (rt-test CT 183, RT 6.0.3, 2026-07-19, отчёт Костя Кондратюк + lav):
после апгрейда 4.4→6.0 логин **вроде проходит** (ошибки нет), но сессия не
прилипает — следующий запрос откатывается обратно на страницу логина. При
**неверном** пароле RT честно показывает ошибку (поэтому кажется «всё принимает,
но грузит окно логина»). lav: «был залогинен до обновления и всё работало, пока
не разлогинился» — старая non-secure cookie с эпохи 4.4 жила в браузере и работала;
после logout новый логин под 6.0 уже не прилипал.
## Root cause: RT 6.0 дефолтит WebSecureCookies=1
`/usr/share/perl5/RT/Interface/Web.pm:1172`:
```perl
-secure => ( RT->Config->Get('WebSecureCookies') ? 1 : 0 ),
```
RT 6.0 выставляет дефолт `WebSecureCookies = 1` (см. `RT/Config.pm:1952`),
4.4 — нет. Итоговый `Set-Cookie` под 6.0:
```
Set-Cookie: RT_SID_rt.etersoft.ru.80=...; path=/; secure; HttpOnly; SameSite=Lax
```
rt-test раздаётся по **голому HTTP** (`http://rt.pr.etersoft.ru/`, без TLS-фронта).
Браузер **строго выбрасывает secure-cookie по HTTP** → следующая страница без
сессии → откат на логин. curl мягче (хранил/слал secure-cookie по HTTP) — поэтому
curl-репро «работал», а живой браузер ловил баг. RT сам в `Config.pm:913-914`
прямо пишет: *«dev server — disable $WebSecureCookies to allow cookies without SSL»*.
## Фикс
В `/etc/rt/RT_SiteConfig.d/local.pm` (HTTP-only контур):
```perl
Set($WebSecureCookies, 0);
```
+ рестарт воркеров (см. ниже про socket-activation). После фикса:
`Set-Cookie: RT_SID_rt.etersoft.ru.80=...; path=/; HttpOnly; SameSite=Lax`
(без `secure`), сессия прилипает, #69737 под ней рендерится.
## Важно для cutover 4.4→6.0
- **Прод CT 412** терминирует HTTPS на azbykar (CT 102) → secure-cookie валиден →
дефолт `WebSecureCookies=1` там ОК, **не трогать**. На проде `WebSecureCookies`
в конфиге не задан (работает дефолт).
- **Любой HTTP-only контур** (rt-test, будущие dev/staging без TLS) → обязательно
`Set($WebSecureCookies, 0)`, иначе логин не работает. Это поведенческая разница
4.4→6.0 (как и [[lesson_rt_stale_stats_analyze]]).
- Включить в чек-лист cutover: проверить TLS контура; если HTTP-only — выключить
secure-cookie **до** отдачи живым юзерам.
## Процесс-баг, который я допустил при этом фиксе (грабли)
Читая `local.pm`, я отфильтровал `Database*` строки «без секретов» (`grep -vE
Database|passw`), а потом **переписал файл heredoc'ом из этого замаскированного
вида** — потеряв override `Set($DatabaseHost,'localhost')`. RT ушёл коннектиться
на `mysql.auth.dmz` (MySQL **5.5.43**) → version-guard `RT 5.0.0 is unsupported
on MySQL < 5.7.7` → воркеры crash-loop → 502 (~5 мин downtime на rt-test).
**Правило:** переписывая конфиг, ВСЕГДА сохраняй полное содержимое включая
секретные строки (через бэкап). Маскируй только для ДИСПЛЕЯ, не для записи.
Правильный паттерн — `cp file file.bak` → surgical insert (`sed -i '/^1;$/i ...'`),
никаких переписываний «по памяти отфильтрованного вида». См. [[feedback_rewrite_preserve_full_config]].
## Bonus: rt-fcgi — socket-activated, стартуют через СОКЕТЫ
`rt-fcgi@N.service` socket-activated (`StandardInput`-подобный поток, `ListenStream=/run/rtN.socket`,
socket-юнит `WantedBy=sockets.target`).
- Воркеры нельзя поднимать `serv start rt-fcgi@N` напрямую из failed-состояния →
`Got no socket` / `start-limit-hit`. Crash-loop ещё и роняет сокет-юниты в inactive.
- **Восстановление:** `systemctl reset-failed 'rt-fcgi@N' 'rt-fcgi@N.socket'`
(снять rate-limit), затем `serv start rt-fcgi@1.socket ... rt-fcgi@4.socket`
(поднять СОКЕТЫ) — сервисы активируются on-demand при первом запросе.
- `serv restart rt-fcgi@1..4` работает только когда сокеты уже active/listening.
- boot-robustness: важен `systemctl is-enabled rt-fcgi@N.socket` (= enabled);
`.service` disabled — это нормально для socket-activation.
Связанные: [[reference_rt_etersoft_ru]], [[plans/rt-6.0]], [[lesson_rt_stale_stats_analyze]],
[[feedback_rewrite_preserve_full_config]].
---
name: lesson_rt_condition_perl538
description: RT 4.4.x на ALT p11/Perl 5.38 — RT/Condition.pm:201 без ';' = fatal, падают все scrips; на p9/Perl 5.28 молча работало
metadata:
type: lesson
---
RT 4.4.x (`rt-4.4.4`/`4.4.5-alt1_4`) на ALT **p11 (Perl 5.38)**: `/usr/share/perl5/RT/Condition.pm` строка 201 — **пропущена `;`**:
```perl
$self->{'TemplateObj'} =undef # нет точки с запятой
$self->{'TicketObj'} = undef;
```
- **Perl 5.28** (p9, прод CT 233): парсится (warn), RT работает.
- **Perl 5.38** (p11, rt-test на gefest): **fatal** `Can't modify undef operator in scalar assignment at .../RT/Condition.pm line 202` → не компилируется базовый `RT::Condition`**падают ВСЕ scrip-условия** (AnyTransaction, OwnerChange, SLA_*, ...), уведомления/SLA не работают. Читайте на write-операциях: `Can't locate`/`Require of RT::Condition::X failed` в `/var/log/rt/rt.log`.
**Фикс (одна точка):** `sed -i "201s/=undef$/=undef;/" /usr/share/perl5/RT/Condition.pm`. Проверка: `perl -I/usr/lib/rt/lib -c /usr/share/perl5/RT/Condition.pm``syntax OK`. `perl -c` остальных .pm даёт кучу false-positive (init-order: `_ImportOverlays`, `RegisterCacheHandler` требуют RT.pm) — реальный syntax-баг только этот.
**Для прода** — фикс через пересборку ALT-пакета `rt` (апстрим-баг + упаковка на Perl 5.38). См. [rt-update.md](plans/rt-update.md), бага 12480. Связано: [[reference_rt_etersoft_ru]].
---
name: lesson-rt-mail-pickup-test
description: RT rt.etersoft.ru — тест забора почты: слать на публичный @etersoft.ru, не @office.etersoft.ru (Roundcube-Sieve "Not me"); requestor проверять в БД, RT-API MemberEmailAddresses врёт пустым
metadata:
type: lesson
---
# Тест забора почты в RT (mail → fetchmail → rt-mailgate)
## Слать на публичный адрес @etersoft.ru, НЕ на @office.etersoft.ru
RT-ящики (`support@`, `boss@`, …) физически живут как Cyrus-mailbox
`…/domain/o/office.etersoft.ru/<x>/user/<name>`, но fetchmail опрашивает именно их.
У `support@office.etersoft.ru` есть **Roundcube-Sieve** (`/var/lib/imap/sieve/domain/o/office.etersoft.ru/s/support/roundcube.script`)
с правилом:
```
elsif anyof (not header :contains "To" "@etersoft.ru") { fileinto "Not me"; stop; }
```
`support@office.etersoft.ru` НЕ содержит подстроки `@etersoft.ru` (`@` стоит перед `office`) →
правило срабатывает → письмо падает в папку **«Not me»** (вне INBOX) → POP3/fetchmail его не видит →
**тикет не создаётся**. Письмо на `support@etersoft.ru` (To содержит `@etersoft.ru`) фильтр пропускает →
доходит до INBOX → забирается. Всегда тестируй забор через публичный адрес формы `name@etersoft.ru`.
## Requestor: проверять в БД, не RT-API
`$ticket->Requestors->MemberEmailAddresses` на RT 4.4 регулярно возвращает **пусто**,
хотя requestor в БД есть (rt-mailgate ставит его из From правильно). Доверять только прямому запросу:
```sql
SELECT g.Name AS role, u.EmailAddress
FROM Tickets t
JOIN Groups g ON g.Domain='RT::Ticket-Role' AND g.Instance=t.id
JOIN CachedGroupMembers c ON c.GroupId=g.id
JOIN Principals p ON p.id=c.MemberId AND p.PrincipalType='User'
JOIN Users u ON u.id=p.id
WHERE t.id=<N> AND g.Name='Requestor';
```
## Где что смотреть при «не пришёл autoreply»
- **Логи postfix на RT-контейнере — в journald** (`/dev/log → /run/systemd/journal/dev-log`),
НЕ в /var/log/mail/all (там пусто, нет rsyslog). `journalctl | grep postfix`.
- **relay:** `relayhost=[mail.etersoft.ru]:587` — доказать тестом `sendmail -i -f lav@etersoft.ru lav@etersoft.ru`,
в журнале ищи `relay=mail.etersoft.ru[91.232.225.46]:587 status=sent`.
- **Исходящие от RT:** canonical переписывает `apache`/`rt → noreply@etersoft.ru`.
Значит в логах mail.etersoft.ru ищи `from=<noreply@etersoft.ru>`.
- **Логи mail.etersoft.ru:** `/var/log/mail/all` (это реальный MTA, там rsyslog есть).
См. [[reference-rt-etersoft-ru]], [[reference-rt-etersoft-ru]] (autoreply-per-queue).
---
name: lesson_rt_setup_database_noninteractive
description: "rt-setup-database --action upgrade неинтерактивно: `yes y |` зацикливается на промпте stop-at-version (флуд 'Doesn't match #.#.#', может забить диск как 19G rt.log); --upgrade-to подавляет его, --force подавляет password-промпт (root socket/empty-pw), --dba-password '' НЕ помогает (там ||). Рабочая строка для 4.4→6.0."
metadata:
type: reference
---
# rt-setup-database upgrade — неинтерактивный запуск
`rt-setup-database --action upgrade` имеет **три** интерактивных промпта. Наивный
`yes y |` зацикливается на одном из них и может забить диск (так уже был инцидент
с 19G `rt.log` на rt-test). Правильный неинтерактивный запуск:
```bash
cd /usr/share/rt
yes y | head -3000 | rt-setup-database \
--action upgrade \
--force \
--upgrade-from 4.4.6 \
--upgrade-to 6.0.3 \
--datadir /usr/share/rt/upgrade
```
## Почему именно так (ключи)
1. **`--upgrade-to <ver>`** — подавляет промпт «Enter RT version if you want to
stop upgrade at some point, #:». ИМЕННО он зацикливался: ждёт пустую строку или
версию, `yes` скармливает `y` → бесконечный «Doesn't match #.#.#». (help:
«do not prompt for it if it appears to be valid version».) **Без `--upgrade-to`
`yes y` гарантированно.loop.**
2. **`--upgrade-from <ver>`** — подавляет from-version-промпт (аналогично).
3. **`--force`** — подавляет password-промпт (`/usr/sbin/rt-setup-database:186`:
`if ( !$args{force} && (...) )`). Без `--force` `get_dba_password()` читает STDIN,
когда пайп открыт, и **съедает** `y`, предназначенный для Proceed → connect как
root с паролем "y" → `Access denied for 'root'@'localhost' (using password: YES)`.
С `--force` пароль остаётся undef → DBI шлёт **без пароля** → root@localhost
через socket/empty-pw (MariaDB, `mariadb -u root` без -p работает).
- ⚠️ `--dba-password ''` **НЕ** подавляет промпт: строка 180 —
`my $dba_pass = $args{'dba-password'} || $ENV{'RT_DBA_PASSWORD'};` (там `||`,
пустая строка falsy → фоллбэк). Только `--force`.
4. Единственный оставшийся STDIN-читатель — `_yesno` «Proceed [y/N]»
(`rt-setup-database:701`, хочет `^y`). `yes y | head -3000` даёт `y` (конечно,
чтобы не убежать, если ошибся). `head -3000` — страховка от runaway.
## Медленный шаг
4.5.x: `ALTER TABLE Attachments CONVERT TO CHARACTER SET utf8mb4` + `MODIFY id BIGINT`
на большой таблице (940k tx) — минуты, состояние «copy to tmp table» / «Repair by
sorting». Это **норма**, не зависание (perl ~0% CPU, MariaDB пашет; видно в
`SHOW PROCESSLIST`). Весь апгрейд 4.4.6→6.0.3 на копии прода (67k тикетов) — ~10 мин.
## Что НЕ нужно
- `--dba-password` с пустым значением (см. выше).
- «Proceed» обрабатывается `y` — отдельный `printf '\ny\n'` тоже ок, но `yes|head` проще.
Реальный прогон 4.4.6 → 6.0.3 на rt-test (**MariaDB 12.3.2**, Sisyphus) — **0 ошибок**, `EXIT=0`.
Версия: rt-test после release-upgrade p11→Sisyphus ⇒ MariaDB **12.3.2-MariaDB-alt1**
(`apt-cache policy` = `sisyphus+420143.100.1.1`, июнь 2026). Прод CT 369 — на p11, MariaDB 11.x.
MariaDB upstream после 11.x перешёл на 12.x (это нормальный свежий релиз, не самосборка).
Связанные: [[plans/rt-6.0]], [[reference_rt_etersoft_ru]] (там же грабля с индексом
`objectcustomfields1` на MariaDB11 — на 6.0 не воспроизвелась), [[lesson_rt_upgrade_clear_mason_cache]].
---
name: lesson_rt_stale_stats_analyze
description: "RT виснет на подгрузке истории тикета (htmx /Helpers/TicketHistoryPage, EffectiveId-JOIN через Attachments) после bulk-import дампа: устаревшая InnoDB-статистика оптимизатор MariaDB выбирает BNL full-scan Attachments (194k строк × Content-блобы) вместо индекса. EXPLAIN до: rows=194225 type=ALL; после ANALYZE: rows=2 key=Attachments2. Лекарство: mariadb-check --analyze eter_rt. Тот же root cause что прод #70334. Подтверждено для RT 6.0."
metadata:
type: lesson
---
# RT: виснет история тикета = устаревшая InnoDB-статистика (после bulk-import)
Симптом (rt-test CT 183, RT 6.0.3, MariaDB 12.3.2, 2026-07-19): «главная долго
грузится, любая подгрузка данных», конкретно **бесконечное зависание** htmx-виджета
истории `/Helpers/TicketHistoryPage?id=70317` (юзер: «вот на чём висит»). Полный
`Ticket/Display.html` при этом быстрый (0.33с); виснет именно догрузка истории.
## Root cause: устаревшая статистика → BNL full-scan Attachments
rt-test — копия прод-БД (bulk-import дампа). InnoDB-статистика после импорта
врёт (`innodb_stats_auto_recalc=1`, но триггерится на ~1/16 изменённых строк; сразу
после bulk-load пересчёт ещё не отработал). Оптимизатор для запроса истории выбирал
**Block Nested Loop full-scan** Attachments вместо индекса по TransactionId.
Запрос (RT 6.0, helper `/Helpers/TicketHistoryPage`):
```sql
SELECT ..., main.Content AS content11
FROM Attachments main
JOIN Transactions Transactions_1 ON Transactions_1.id = main.TransactionId
JOIN Tickets Tickets_2 ON Tickets_2.id = Transactions_1.ObjectId
WHERE Tickets_2.EffectiveId = '<TICKET>'
AND Transactions_1.ObjectType = 'RT::Ticket'
AND (main.ContentType = 'text/plain' OR main.ContentType LIKE 'message/%' OR main.ContentType = 'text')
ORDER BY main.id ASC
```
`EffectiveId` (тикет-приёмник merge) матчит здесь только сам тикет (1 строка) — но
план всё равно шёл через полный скан:
**EXPLAIN до фикса (виснет 15с+, code=000 по таймауту curl):**
```
Tickets_2 ref Tickets6(EffectiveId) rows=1 Using index; temporary; filesort
main(Attachments) ALL key=NULL rows=194225 "Using join buffer (flat, BNL join)" ← полный скан!
Transactions_1 eq_ref PRIMARY rows=1
```
→ читает **все 194225 строк Attachments с Content-блобами** → зависание.
**EXPLAIN после `ANALYZE TABLE`:**
```
Tickets_2 ref Tickets6 rows=1
Transactions_1 ref Transactions1 rows=2 ← теперь драйвит отсюда
main(Attachments) ref Attachments2 rows=2 ← индекс по TransactionId
```
→ 2 строки вместо 194225.
## Лекарство (1 секунда, безопасно — только статистика)
```bash
mariadb-check --analyze eter_rt
# или точечно:
mariadb -u root eter_rt -e "ANALYZE TABLE Tickets; ANALYZE TABLE Transactions; ANALYZE TABLE Attachments;"
```
Результат: helper #70317 **15с+ → 0.024с warm** (history rows отдаются корректно);
тикет #1/#70334/#70000 — 0.035–0.045с. `innodb_stats_persistent=1` ⇒ статистика
сохраняется после рестарта. Индекс на `Tickets.EffectiveId` (`Tickets6`) существует —
проблема была не в его отсутствии, а в статистике оптимизатора.
## Тот же root cause, что на проде #70334
Прод (CT 412) уже лечил это (см. [[reference_rt_etersoft_ru]]): «Медленные тикеты
(id=70334) — причина = cold buffer pool + устаревшая статистика после bulk-import.
Лекарство: `mariadb-check --analyze eter_rt`». На rt-test подтвердилось и для
**RT 6.0** ⇒ фикс актуален для будущего переезда прода 4.4→6.0.
## Что НЕ было причиной (побочные/вторичные)
- **Холодная Mason-компиляция** после `rm -rf /var/cache/rt/mason_data/obj/*`
(фикс loadTitleBoxStates, [[lesson_rt_upgrade_clear_mason_cache]]) — разовый
0.3–0.5с на первый заход в каждый шаблон, warm = 0.03–0.17с. Не зависание.
- **squished-JS бандл RT 6.0 = 2.3 МБ** (`/NoAuth/js/squished-<hash>.js`) — первый
заход ощутим на VPN, потом кэш браузера.
- **internal HTML converter** (нет w3m/elinks) — hourly warning в rt.log, тормозит
**ingestion** HTML-почты (входящие), НЕ рендер страниц. Фикс: `epm install w3m`.
## Метод диагностики (пригодится)
1. `SHOW PROCESSLIST` во время зависания → видел постоянный SELECT … FROM Attachments.
2. Вытащил полный текст запроса из `INFORMATION_SCHEMA.PROCESSLIST` (не обрезая).
3. `EXPLAIN` запроса → увидел `type=ALL, rows=194225, BNL join`.
4. `ANALYZE TABLE``EXPLAIN` стал `rows=2`, helper → 0.024с.
## Вывод
- Steady-state RT 6.0 на MariaDB 12.3.2 здоров; БД быстрая (buffer pool 98.6%).
- «Тормоза/зависания» после заливки дампа = **обязательно `mariadb-check --analyze
eter_rt`**.
- Включить в чек-лист cutover 4.4→6.0: после заливки/миграции БД → analyze **до**
отдачи трафика живым юзерам.
Связанные: [[reference_rt_etersoft_ru]], [[plans/rt-6.0]], [[lesson_rt_upgrade_clear_mason_cache]],
[[lesson_rt_setup_database_noninteractive]].
---
name: lesson_rt_upgrade_clear_mason_cache
description: "После мажорного апгрейда RT (особенно 4.4→6.0) обязательно чистить кэш скомпилированных Mason-шаблонов /var/cache/rt/mason_data/obj, иначе отдаваются старые .obj со ссылками на удалённые функции (loadTitleBoxStates) JS ReferenceError."
metadata:
type: reference
---
# RT: чистить Mason-кэш после мажорного апгрейда
После мажорного апгрейда RT (в первую очередь 4.4 → 6.0) **обязательно** сбрасывать
кэш скомпилированных Mason-компонентов, иначе пользователи получают JS-ошибки от
старых шаблонов.
## Симптом (rt-test, 4.4.6 → 6.0.3, 2026-07-18)
В консоли браузера на странице входа:
```
Uncaught ReferenceError: loadTitleBoxStates is not defined
at (index):37:9
```
Страница выводит `jQuery( loadTitleBoxStates );`, но такой функции НЕТ ни в одном
JS-файле RT 6.0 (`grep -rin loadtitleboxstates /usr/share/rt` → пусто). Функция
существовала в RT 4.4, в 6.0 её убрали (htmx + Page Layouts переписали UI).
## Причина
RT 6.0 переписала Mason-элемент `Elements/HeaderJavascript`, но в кэше остался
**скомпилированный 4.4-obj**, который и отдавался:
`/var/cache/rt/mason_data/obj/3265978829/standard/Elements/HeaderJavascript.obj`.
Mason не инвалидирует кэш автоматически при апгрейде кода.
## Лекарство
```bash
for i in 1 2 3 4; do systemctl stop rt-fcgi@$i.service; done
rm -rf /var/cache/rt/mason_data/obj/*
for i in 1 2 3 4; do systemctl start rt-fcgi@$i.service; done
```
(obj — это кэш, регенерируется автоматически; безопасно, БД/конфиг не трогает.)
После сброса `grep -c loadTitleBoxStates` на отданной странице → 0.
## Где кэш на ALT-сборке RT
`/var/cache/rt/mason_data/obj` (НЕ `/usr/share/rt/var/...`). Конфиг-ключ
`MasonDataDir`. Кэш инвалидировать также через `RT::Interface::Web::ClearSquished()`
(для JS/CSS squish), но squish и так пересобирается рестартом воркеров (живёт в памяти).
**Для прод-переезда 4.4→6.0 (CT 412):** этот шаг включить в процедуру обязательно.
Связанные: [[plans/rt-6.0]], [[reference_rt_etersoft_ru]], [[lesson_rt_setup_database_noninteractive]].
---
name: lesson_ssh_into_container_not_lxc_attach
description: "Для работы внутри LXC-контейнера использовать SSH напрямую, а не lxc-attach с хоста. SSH работает из любого места, lxc-attach только с хоста PVE."
metadata:
type: lesson
severity: medium
---
# В контейнер — через SSH, не через lxc-attach
## Правило
Для работы внутри LXC-контейнера всегда использовать SSH (`ssh root@IP`), а не `lxc-attach -n VMID` с хоста PVE.
## Почему
1. **SSH работает из любого места** — не нужно сначала заходить на хост PVE
2. **Меньше путаницы** с переменными окружения и escaping в lxc-attach
3. **lxc-attach сломан** на некоторых хостах (Perl ENV ошибка: `Insecure $ENV{ENV}`)
4. **Можно scp/sftp** для копирования файлов
## Как
```bash
# Правильно:
ssh root@192.168.0.251 "command"
# Неправильно:
ssh root@gefest "lxc-attach -n 944 -- bash -c 'command'"
```
## Настройка
После создания контейнера сразу добавить SSH-ключ в `~/.ssh/authorized_keys`:
```bash
ssh root@gefest "lxc-attach -n VMID -- bash -c 'cat >> ~/.ssh/authorized_keys' < ~/.ssh/id_ed25519.pub"
```
---
name: lesson_traceroute_first
description: "Начинай диагностику сети с traceroute/mtr, а не с гадания по IP. Видишь путь понимаешь проблему."
metadata:
node_type: memory
type: lesson
originSessionId: ses_0c1bc5bc7ffenv3tCPWLHe71IK
---
При проблемах с сетью **первым делом** `traceroute`/`mtr` — сразу виден путь и где обрыв. Не гадать по IP, не тыкать ping в лоб.
Пример: rpi (Франция) доступен через IPsec-туннель (.140 → 10.10.10.6). Вместо traceroute с .140 на rpi-хост — полез искать Docker bridge IP вслепую. Traceroute сразу бы показал путь через туннель и ноды.
Связано: [[reference_ikev2_fr_tunnel]] (rpi, IPsec), [[objects/priv]] (маршрутизация).
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment