Журнал изменений

Формат основан на Keep a Changelog, проект следует семантическому версионированию.

[2.35.0] — 2026-07-15

Добавлено — модуль cf: отказоустойчивый app-ingress через Cloudflare

Новый командный модуль 24-cloudflare (mitdev cf …): ввод app-узла в отказоустойчивый ingress через Cloudflare — всё только через API Cloudflare (без дашборда, без локальных tunnel-конфигов). Отвечает на вопрос «как пользователи попадут на живой узел приложения», в т.ч. при «сером отказе» (жив app, мёртв путь к БД), не открывая на узле ни одного входящего порта.

Безопасность: API-токен и connector-token только в кредах (0600), никогда в ps/argv (curl-конфиг на stdin; connector-token через EnvironmentFile, не в ExecStart), редакция в логах, минимальные scope. Три режима ingress (tunnel-only/lb-only/lb-over-tunnel) согласованы между командами; в lb-only health-failover не обеспечить — команды LB честно отказывают fail-loud.

Проверено офлайн (848 юнит-ассертов: API-вызовы через подменяемую обёртку cf_api, локальные проверки через стабы). Схема требует чекпойнта на живом Cloudflare перед продом — вопросы вынесены в docs/modules/24-cloudflare.md («Чекпойнт живого Cloudflare»): Host-override для origin'ов cfargotunnel.com, TUNNEL_TOKEN из окружения cloudflared, форма ответа /pools/{id}/health, socat + IPv6-bind.

[2.34.0] — 2026-07-13

Исправлено — окно двух пишущих primary не было закрыто (self-fence считался в «проверках», а не в секундах)

Предыдущий фикс инварианта C2 сравнивал SELF_FENCE_THRESHOLD=3 с FAIL_THRESHOLD=6 («3 < 6 → безопасно») — но итерация цикла на пути self-fence (primary зондирует ВСЕХ пиров дважды: цикл поиска winner'а + quorum_ok) стоит дороже итерации на пути promote (реплика зондирует только PRIMARY_HOST). При сетевом blackhole (WireGuard молча роняет пакеты — каждый зонд уходит в полный таймаут) это давало ~92с до реального огораживания против ~80с до promote на majority-стороне — окно ~12с, в течение которого оба узла принимают запись, а pg_rewind затем молча выбрасывает транзакции одной из сторон.

Исправлено — редакция пароля в логах не работала (SUPERUSER-пароль PostgreSQL уходил в install.log открытым текстом)

redact_log_text (lib/core.sh) имеет единственного вызывающего — format_command, который прогонял каждый аргумент через printf '%q' ДО редакции. После %q строка PASSWORD 'S3cret' превращается в PASSWORD\ \'S3cret\' — пробел и кавычка становятся экранированными последовательностями, и правило s/(PASSWORD[[:space:]]+)'[^']*'/…/Ig никогда не совпадало. run_step писал команды вида CREATE ROLE "crm_plugin_admin" WITH LOGIN SUPERUSER PASSWORD 'xxx' в install.log открытым текстом.

[2.33.0] — 2026-07-13

Исправлено — self-fence огораживал primary, но не рвал его сессии (тихая потеря транзакций)

Watchdog огораживает вставший в меньшинство mesh primary тем, что переводит role-endpoint (:8008) в 503 — HAProxy должен на это среагировать разрывом уже открытых соединений, иначе приложение продолжает писать в огороженный узел, а при восстановлении сети pg_rewind молча отбрасывает эти транзакции. Ранее в этой же ветке on-marked-down shutdown-sessions был снят из pg-proxy.cfg, чтобы блип :8008 не ронял живой пул — но тем самым self-fence лишился исполнительного механизма.

Первопричина блипов устранена ниже (персистентный listener вместо socket-activation), поэтому shutdown-sessions возвращён — DOWN теперь означает настоящую смену роли, а не блип.

Исправлено — role-endpoint терял ~50% коннектов на socket-activation

[email protected] поднимался per-connection через systemd socket-activation и ронял примерно каждое второе TCP-соединение RST-ом до ответа — это и было первопричиной блипов :8008 выше. Заменён на персистентный socat-листенер (mitdev-pg-role.service, User=postgres). На db-узлах появилась новая зависимость — пакет socat.

Добавлено — отдельная SUPERUSER-роль для провижена плагинов

Провижен плагинов CRM теперь выполняется от отдельной роли crm_plugin_admin (SUPERUSER), а не от обычной app-роли — привилегия не размазывается на рантайм-подключения приложения. Повторный прогон не перевыпускает пароль молча (иначе рвался PLUGIN_DB_ADMIN_URL).

Исправлено — пароль SUPERUSER-роли утекал в лог открытым текстом

redact_log_text редактировал только пары вида password=значение, но не SQL-литерал PASSWORD '...' — команды вида CREATE ROLE ... SUPERUSER PASSWORD 'xxx' уходили в install.log открытым текстом. Каталог логов создавался простым mkdir -p, что при root umask 022 даёт 0755, а лог — 0644: локальный пользователь мог прочитать пароль суперпользователя PostgreSQL (заодно и пароли app-роли, роли репликации).

Исправлено — осиротевший слот репликации мог забить диск на primary

heal_slot в watchdog глотал ошибку создания слота (|| true) и безусловно шёл дальше на ALTER SYSTEM + рестарт postgres — если слот в итоге не создавался (например, на новом primary после failover ещё нет нужной pg_hba-записи), реплика получала ту же ошибку, которую heal_slot должен был вылечить, плюс лишние рестарты. Теперь результат создания слота проверяется фактом на целевом узле, а не по проглоченному коду возврата. max_slot_wal_keep_size ограничивает, сколько WAL слот без потребителя может удержать — раньше предела не было, и осиротевший слот мог довести pg_wal до отказа диска на primary. Тот же параметр добавлен и во второе место, где рендерится drop-in репликации (mitdev pg add-replica), — раньше там был только wal_keep_size без предела.

Исправлено — self-fence мог сработать позже промоушна на majority-стороне, а снимался несимметрично быстро

[2.32.3] — 2026-07-10

Исправлено — self-update ставил устаревшую сборку из кеша

Установщик /get скачивает архив с ?nocache=<timestamp> — иначе CDN и обратные прокси отдают вчерашний mitdev.tar.gz. mitdev self-update этой защиты не имел: он получал закешированный архив, сообщал его версию как «новую» и рапортовал об успешном обновлении. На узле оставался старый код, хотя version.txt на сайте показывал свежую версию.

[2.32.2] — 2026-07-10

Исправлено — «Отставание» реплики показывало тишину на primary

mitdev pg status считал отставание как now() - pg_last_xact_replay_timestamp(). Это не отставание, а время с момента, когда реплика применила последнюю транзакцию: пока primary ничего не пишет, величина растёт секунда за секундой на идеально синхронной реплике. «Отставание 233 с» означало «на primary четыре минуты не было записи». Обратная ошибка опаснее: реально отставшая реплика на нагруженном primary показывала бы близкие к нулю секунды.

[2.32.1] — 2026-07-10

Исправлено — правила UFW молча не создавались (авто-failover PostgreSQL не работал)

Проверка «активен ли файрвол» была написана так:

ufw status 2>/dev/null | head -1 | grep -q active

head -1 закрывает поток после первой строки, ufw получает SIGPIPE и завершается с кодом 141, а pipefail протаскивает его через весь конвейер. Условие становится ложным, и mitdev решает, что UFW выключен — правила молча не создаются. Это гонка: на свежем сервере вывод ufw status короткий и всё работает, на живом кластере с десятками правил — нет.

Последствие для PostgreSQL-кластера: узлы не открывали друг другу 5432 и 8008 (эндпоинт роли), поэтому watchdog видел 1/3 узлов вместо 3/3. Кворум (N/2+1) не набирался НИКОГДА, авто-failover был невозможен, а primary вдобавок сам себя огораживал (SELF-FENCE, роль отдаёт 503). Кластер выглядел здоровым и не переживал первую же аварию.

[2.32.0] — 2026-07-10

Добавлено — mitdev redis proxy: приложение переживает failover

Failover переключает сервер, а не клиента. Приложение, настроенное на прямой адрес мастера, после переключения продолжает стучаться в узел, ставший репликой, и получает -READONLY на каждую запись: кластер жив, лежит приложение.

[2.31.9] — 2026-07-10

Исправлено — remove redis --purge оставлял dpkg в состоянии «rc»

Удаление вызывало apt-get remove (не purge) и отдельно делало rm -rf /etc/redis. Для dpkg это означает, что conffiles сняты администратором: пакет остаётся в состоянии «rc», а при переустановке ТОЙ ЖЕ версии файлы из пакета не разворачиваются. Redis поднимался с неполным конфигом — в частности без директивы dir, из-за чего репликация ломалась необъяснимым образом (см. 2.31.8).

[2.31.8] — 2026-07-10

Исправлено — исчезнувшая директива dir ломала репликацию

Реплика бесконечно повторяла «Opening the temp file needed for MASTER <-> REPLICA synchronization: Read-only file system», хотя /var/lib/redis существовал и был доступен на запись. Redis туда и не обращался: в redis.conf не было директивы dir, а без неё каталогом данных считается «.» — рабочий каталог процесса. Под systemd это /, и внутри ProtectSystem=true оно смонтировано только для чтения.

Директива исчезала сама. remove redis --purge удаляет /var/lib/redis у работающего Redis; его cwd повисает на удалённом inode. Дальше Sentinel при каждой смене роли дёргает CONFIG REWRITE, а тот берёт путь из getcwd() — не получив его, Redis просто выбрасывает dir из файла.

[2.31.7] — 2026-07-10

Исправлено — ложное «РАСХОЖДЕНИЕ» (split-brain) на исправном мастере

mitdev redis status сравнивал адрес мастера, полученный от Sentinel, с redis_priv_ip. Без root net_meta_get не читает состояние приватной сети, и redis_priv_ip откатывается на публичный адрес (eth0). Мастер, слушающий wg0, «не узнавал сам себя» — и на полностью исправном кластере печаталось пугающее предупреждение о split-brain.

Теперь проверяется, принадлежит ли адрес узлу по ЛЮБОМУ интерфейсу (redis_ip_is_local): список локальных адресов виден и без привилегий. Предупреждение остаётся только когда Sentinel указывает на действительно чужой адрес — то есть когда split-brain настоящий.

[2.31.6] — 2026-07-10

Исправлено — проверка каталога данных не срабатывала, когда была нужнее всего

redis_data_dir спрашивал путь у живого Redis через config get dir. Но Redis лежит ровно в тех случаях, когда с каталогом беда: ответа нет, путь пустой, проверка каталога молча пропускается — и redis check рапортует сплошные PONG, ни словом не обмолвившись, что каталога данных не существует.

[2.31.5] — 2026-07-10

Исправлено — повреждённый при копировании токен винил файрвол

Токен кластера копируют через терминал, где он переносится на несколько строк. Вставка добавляет пробелы и переводы строк, а иногда токен склеивается сам с собой — base64 такую конкатенацию декодирует молча, и в расшифровке оказывается ПО ДВЕ строки MASTER_IP= и PORT=. grep -oE возвращал оба совпадения, redis-cli получал многострочный хост и падал с «Name or service not known», а mitdev выводил «Мастер недоступен — откройте порты» с адресом, разъехавшимся на три строки.

[2.31.4] — 2026-07-10

Исправлено — redis cluster-init ронял живой кластер в failover

Повторный mitdev redis cluster-init перезапускал redis-server на мастере безусловно — даже когда в конфиге нечего было менять. Мастер пропадал на несколько секунд, соседние Sentinel'ы объявляли +sdown, набирали кворум и промоутили реплику. Штатная команда сопровождения роняла кластер.

[2.31.3] — 2026-07-10

Исправлено — переустановка Redis не возвращала каталог данных

mitdev remove redis --purge удаляет /var/lib/redis. Каталог создаёт postinst пакета — но только при ПЕРВОЙ установке: если пакет остался в системе, повторный apt install postinst не запускает, и каталог не возвращается. Redis при этом спокойно стартует, отвечает PONG и обслуживает клиентов — ломается только репликация: временный файл RDB писать некуда.

Диагноз при этом обманчив. Юнит запускается с ProtectSystem=true, где /var/lib/redis пробрасывается на запись через ReadWriteDirectories=-/var/lib/redis (дефис = «не падать, если пути нет»). Каталога нет → проброса нет → путь остаётся в read-only части namespace, и Redis сообщает Read-only file system, хотя ФС смонтирована rw, а настоящая причина — отсутствующий каталог.

[2.31.2] — 2026-07-10

Исправлено — диагностика реплики врала в двух случаях

[2.31.1] — 2026-07-10

Исправлено — redis join объявлял неудачу на исправной реплике

[2.31.0] — 2026-07-10

Исправлено — mitdev redis join не работал на Ubuntu 22.04

redis-cli -t <таймаут> поддерживается не всеми версиями: Ubuntu 22.04 LTS — основная целевая система — везёт redis-tools 6.0.16, где эта опция отвергается (Unrecognized option or bad number of args for: '-t'), и соединение не устанавливается вовсе. Проба мастера падала до попытки связи, а пользователь получал вердикт «Мастер недоступен — узел НЕ присоединён» с советом открыть порты: настоящая причина была на своём же узле, а не в файрволе.

Исправлено — mitdev молча завершался посреди работы (pipefail + errexit)

mitdev redis cluster-remove на боевом узле не делал ничего и не печатал ни строчки, а узел навсегда оставался role:slave при мёртвом мастере (master_link_status:down, slave_read_only:1 — то есть база в read-only).

Причина: под set -Eeuo pipefail конструкция x=$(… | grep … | …) завершает весь скрипт, если grep ничего не нашёл — «нет совпадений» для grep это код возврата 1, а pipefail протаскивает его через весь конвейер. В ufw_delete_by_comment это срабатывало на любом кластере, собранном до появления меток mitdev: в правилах UFW: Sentinel уже остановлен, а до снятия replicaof из redis.conf дело не доходило.

Добавлено — удаление модуля изнутри службы

Исправлено — проверка CRLF в tests/smoke.sh не работала

Проверка «все скрипты — LF» была сломана в обе стороны сразу, и ошибки маскировали друг друга. Под Git Bash (MSYS) grep читает файл в текстовом режиме и вырезает \r, поэтому настоящий CRLF не находился никогда; а литерал $'\r' разворачивался там в пустую строку, из-за чего grep совпадал с каждым файлом подряд и тест «краснел всегда». Теперь CR-байты считаются через tr/wc: проверка ловит реальный CRLF и молчит на чистом дереве.

[2.30.0] — 2026-07-10

Исправлено — узел не поднимался после перезагрузки, файрвол и остальные кластеры

Добавлено

Исправлено — отказоустойчивость PostgreSQL (потеря данных и split-brain)

Аудит после инцидента с Redis показал, что кластер PostgreSQL не переживал отказ узла: у watchdog не было кворума, а взаимная видимость узлов была сломана. Каждый дефект по отдельности приводил к двум primary одновременно.

Исправлено

Добавлено

Изменено

[2.29.0] — 2026-07-09

Добавлено

Изменено

Исправлено

[2.28.1] — 2026-07-08

Добавлено

Изменено — GUIDE разбит на короткие страницы

[2.28.0] — 2026-07-08

Добавлено — упаковка/подача (маркетинговая читаемость)

[2.27.2] — 2026-07-08

Исправлено — защищённый откат при переустановке

Документация

[2.27.1] — 2026-07-08

Исправлено — корректная переустановка (повторный запуск не ломает службы)

Документация

[2.27.0] — 2026-07-08

Добавлено — единый HA-обзор в doctor и настройка кластеров в установщике

Документация

[2.26.0] — 2026-07-08

Добавлено — распределённый MinIO (erasure coding), команда mitdev minio

Документация

[2.25.0] — 2026-07-08

Добавлено — кластер RabbitMQ, команда mitdev rabbitmq

Документация

[2.24.0] — 2026-07-08

Добавлено — отказоустойчивый MongoDB (replica set), команда mitdev mongo

Документация

[2.23.0] — 2026-07-08

Добавлено — отказоустойчивый Redis (Sentinel), команда mitdev redis

Документация

[2.22.0] — 2026-07-08

Добавлено — IPv6 (dual-stack) в приватной сети

Документация

[2.21.0] — 2026-07-08

Добавлено — «Listen on: private only» прямо в установщике

Документация

[2.20.0] — 2026-07-08

Добавлено — mitdev net bind: службы слушают только приватную сеть

Документация

[2.19.0] — 2026-07-08

Добавлено — отказоустойчивость приватной сети (WireGuard mesh watchdog)

Документация

[2.18.0] — 2026-07-08

Добавлено — zero-touch подключение к сети по SSH

Документация

[2.17.0] — 2026-07-08

Добавлено — приватная сеть WireGuard (full-mesh), команда mitdev net

Отложено (следующая фаза, честно)

Документация

[2.16.0] — 2026-07-08

Добавлено — нативные рантаймы PHP / Python / Go (пресеты заработали без Docker)

Документация

[2.15.0] — 2026-07-08

Добавлено — пресеты приложений mitdev app <пресет> (роадмап 5/5)

Документация

[2.14.0] — 2026-07-08

Добавлено — mitdev update с сохранением конфигураций (роадмап 4/5)

Документация

[2.13.0] — 2026-07-08

Добавлено — mitdev doctor --deep: реальные функциональные пробы (роадмап 3/5)

Документация

[2.12.0] — 2026-07-08

Добавлено — полноценный backup framework (роадмап 2/5)

Документация

[2.11.0] — 2026-07-08

Добавлено — версионирование и перенос конфигурации (роадмап «Server Operating Platform», 1/5)

Документация

[2.10.2] — 2026-07-07

Исправлено — mitdev pg add-replica теперь самодостаточен

Документация

[2.10.1] — 2026-07-07

Исправлено

Документация

[2.10.0] — 2026-07-07

Добавлено — удаление реплик и их данных (PostgreSQL-кластер)

Изменено

[2.9.1] — 2026-07-07

Документация

[2.9.0] — 2026-07-07

Добавлено — удаление компонентов (у каждого модуля своя команда)

Изменено

[2.8.2] — 2026-07-07

Изменено (синхронизация документации с кодом)

[2.8.1] — 2026-07-07

Исправлено / добавлено (свежесть дистрибутива)

[2.8.0] — 2026-07-07

Добавлено

[2.7.0] — 2026-07-07

Добавлено

[2.6.0] — 2026-07-07

Добавлено

Исправлено

[2.5.1] — 2026-07-07

Исправлено

[2.5.0] — 2026-07-07

Добавлено — полное самовосстановление («настроил и забыл»)

[2.4.1] — 2026-07-07

Добавлено

[2.4.0] — 2026-07-07

Добавлено

[2.3.0] — 2026-07-07

Добавлено

[2.2.0] — 2026-07-07

Добавлено

Исправлено

[2.1.0] — 2026-07-07

Добавлено

[2.0.0] — 2026-07-07

Добавлено

Изменено (несовместимо)

[1.6.0] — 2026-07-07

Добавлено

[1.5.0] — 2026-07-07

Добавлено

[1.4.0] — 2026-07-07

Добавлено

[1.3.0] — 2026-07-07

Добавлено

[1.2.0] — 2026-07-07

Добавлено

[1.1.0] — 2026-07-07

Добавлено

[1.0.0] — 2026-07-07

Добавлено