Initial commit: RIPE CIDR/FQDN collector
Collector daemon, FastAPI server (addresses, diff, collect, sources), SQLite storage with change journal, Docker Compose deployment, tests, documentation and project rules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
bcf8156085
45 files changed
+3134
No files matched your search
@@ -0,0 +1,63 @@
|
||||
# Анализ проекта: сделано и осталось (2026-09-20)
|
||||
|
||||
Состояние проекта после трёх доработок: надёжность и безопасность, тесты в контейнере, форматы вывода. Планы и итоги лежат рядом в `docs/`.
|
||||
|
||||
## Сделано
|
||||
|
||||
| Направление | Результат |
|
||||
|---|---|
|
||||
| Токен на `POST /schedule` | Заголовок `X-API-Key`, токен из `RIPE_API_TOKEN`, без токена запись отключена (503). Читающие эндпоинты открыты намеренно. |
|
||||
| TTL записей | `ttl_days` (по умолчанию 90, `0` = бессрочно), `first_seen`/`last_seen`, авто-миграция старых данных. |
|
||||
| Надёжность хранения | Атомарная запись, `flock`, битый JSON переименовывается в `*.corrupt-*`, API отвечает 503. |
|
||||
| Логирование и `/health` | `logging` вместо `print`, `/health` со статусом заданий. |
|
||||
| Агрегация CIDR, `ip_version` | Параметры `aggregate`, `ip_version` в `GET /addresses`; ответ по умолчанию побайтно прежний. |
|
||||
| Форматы вывода | `nftables`, `mikrotik`, `bird`, `frr` (`format`, `name`). Проверены `bird -p` и `nft -c`. |
|
||||
| Тесты | 9 тестов, запуск в контейнере (`Dockerfile.test`). |
|
||||
| Развёртывание | Запуск не от root в systemd и OpenRC (документация в README). |
|
||||
|
||||
## Не сделано
|
||||
|
||||
| Направление | Комментарий |
|
||||
|---|---|
|
||||
| Метрики Prometheus (`/metrics`), алерты | Не начато |
|
||||
| Управление ASN и FQDN через API | Сейчас только CLI |
|
||||
| Diff-эндпоинт (`?since=`), уведомления (webhook, Telegram) | Не начато |
|
||||
| Хранение в SQLite | Не начато |
|
||||
| Разделение сборщика и API на два процесса | Не начато |
|
||||
| Контейнер для самого приложения (Dockerfile, compose) | Не начато |
|
||||
| Форматы `text`, `ipset` | Пользователь не выбрал |
|
||||
|
||||
## Остаточные риски
|
||||
|
||||
| # | Риск | Статус |
|
||||
|---|---|---|
|
||||
| 1 | После порчи файла API отдаёт пустой список, резервной копии нет | Не закрыт |
|
||||
| 2 | Токен только на `POST`, читать список может любой | Осознанный выбор |
|
||||
| 3 | CLI `add`/`remove` меняет конфиг без общей блокировки | Не закрыт |
|
||||
| 4 | Версии в `requirements.txt` не закреплены | Не закрыт |
|
||||
| 5 | Запросы к RIPE без повторов | Не закрыт |
|
||||
| 6 | Нет репозитория git, истории изменений нет | Не закрыт |
|
||||
| 7 | MikroTik и FRR не проверены на реальном ПО | Не закрыт |
|
||||
| 8 | Тесты не покрывают `/health` и параллельную запись | По правилам проекта минимум |
|
||||
|
||||
Для FRR отдельно проверить: не выдаёт ли `no ip prefix-list <name>` ошибку, если список ещё не создан.
|
||||
|
||||
## Новые наблюдения
|
||||
|
||||
1. Реальные `data.json` и `fqdn_data.json` ещё в старом формате (нет блока `seen`). Миграция и TTL заработают после первого запуска сборщика.
|
||||
2. Невалидные адреса при `aggregate=true` и в форматах пропускаются с предупреждением в логе, клиент об этом не узнаёт.
|
||||
3. В данных нет версии схемы, что осложнит следующую миграцию (например, на SQLite).
|
||||
4. `/addresses` читает и разбирает JSON при каждом запросе. Для текущих объёмов это нормально, при росте списков понадобится кэш.
|
||||
|
||||
## Рекомендуемый порядок
|
||||
|
||||
1. **Гигиена:** `git init` с первым коммитом, закрепление версий (`pip freeze` внутри контейнера), резервная копия `data.json.bak` (риски 1, 4, 6).
|
||||
2. **Dockerfile и compose для приложения** (продолжение работы с контейнерами).
|
||||
3. **Управление ASN и FQDN через API с токеном и `/metrics`.**
|
||||
4. **SQLite и diff/уведомления**, если понадобится история изменений.
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- `plan-reliability-security.md`, `summary-reliability-security.md`
|
||||
- `plan-tests-in-container.md`, `summary-tests-in-container.md`
|
||||
- `plan-output-formats.md`, `summary-output-formats.md`
|
||||
@@ -0,0 +1,89 @@
|
||||
# Анализ проекта: сделано и осталось (2026-09-21, сверка с графом)
|
||||
|
||||
Состояние после восьми доработок: надёжность и безопасность, тесты в контейнере, форматы вывода, разделение процессов, управление ASN/FQDN через API, контейнер приложения, SQLite, `POST /collect`, `GET /addresses/diff`. Предыдущий анализ: `analysis-2026-09-20.md`. Планы и итоги лежат рядом в `docs/`.
|
||||
|
||||
Проект: 7 модулей Python (API, сборщик, демон, БД, форматы, хранилище, healthcheck), 1235 строк, 18 тестов (15 функций с параметризацией) в 5 файлах, 20 документов в `docs/`.
|
||||
|
||||
**Граф знаний** (`graphify-out/`, построен по коду и `docs/`): 315 узлов, 643 связи, 13 сообществ. Использован для сверки: узлы-«хабы», связи между модулями и документами, изолированные узлы. Все выводы из графа ниже проверены по исходникам.
|
||||
|
||||
## Что изменилось с предыдущей версии этого файла
|
||||
|
||||
- **Выполнено:** `GET /addresses/diff` (журнал изменений на триггерах SQLite, курсор, `410` за горизонтом, заголовок `X-Changes-Cursor`). Из «Не сделано» убран diff; уведомления остались.
|
||||
- **Исправлено по сверке:**
|
||||
- Безымянных образов Docker не 65, а 3 (всего образов 31). Наблюдение 3 снято.
|
||||
- Предположение, что в демоне не ловится `sqlite3.Error`, не подтвердилось: `run_job` перехватывает любое исключение сбора и пишет его в `last_error`. Пробела нет.
|
||||
- Метрики: строк кода 1235 (было 1102), тестов 18 (было 16).
|
||||
- **Новое:** `graphify-out/` (1,2 МБ) не был исключён из `.gitignore` и `.dockerignore`, то есть попадал бы в образ. Исправлено, в README добавлен пункт о графе.
|
||||
|
||||
## Сделано
|
||||
|
||||
| Направление | Результат |
|
||||
|---|---|
|
||||
| Токен на изменяющие запросы | `X-API-Key` на `POST`/`DELETE`, без токена запись отключена (503). Чтение открыто намеренно. |
|
||||
| TTL, атомарная запись, защита от порчи | TTL (`ttl_days`) на SQLite. Битые JSON и база уходят в `*.corrupt-*`, API отвечает 503. |
|
||||
| Форматы вывода, агрегация CIDR, `ip_version` | nftables, mikrotik, bird, frr. |
|
||||
| Разделение сборщика и API | Отдельный демон, singleton, heartbeat, `/health` по `status.json`. |
|
||||
| Управление ASN и FQDN через API | Добавление, удаление, `purge`, старение данных удалённых источников. |
|
||||
| Контейнер приложения | `Dockerfile` и `docker-compose.yml`: два сервиса, том, non-root, read-only, healthcheck. |
|
||||
| SQLite | Таблица `addresses`, WAL, автоматическая миграция из JSON (оригиналы сохраняются), транзакции, чтение без блокировок. |
|
||||
| `POST /collect` | Токен, ответ 202, запрос демону файлом `collect_request.json` (опрос каждые 5 с), объединение запросов, пропуск наложения, `503` без живого демона. |
|
||||
| `GET /addresses/diff` | Журнал `changes` (триггеры, схема версии 2), итоговый эффект вместо истории, срок хранения `changes_retention_days` (30 дней), курсор или время, `410` за горизонтом. Проверено тестами и на копии базы версии 1. |
|
||||
| Граф знаний | `graphify-out/`: интерактивный граф, отчёт, JSON; исключён из git и образа. |
|
||||
| Тесты | 18 проверок, запуск в контейнере. |
|
||||
|
||||
## Не сделано
|
||||
|
||||
| Направление | Статус |
|
||||
|---|---|
|
||||
| Автоматическая резервная копия базы | Есть только команда в README (`sqlite3 ".backup"`), заданий в демоне нет |
|
||||
| Метрики `/metrics`, алерты | Не начато |
|
||||
| Уведомления о изменениях (webhook, Telegram) | Не начато; основа (журнал) есть |
|
||||
| Повторные попытки запросов к RIPE | Не начато |
|
||||
| TLS перед API | Не начато (решение пользователя: порт наружу без TLS) |
|
||||
|
||||
## Остаточные риски
|
||||
|
||||
| # | Риск | Статус |
|
||||
|---|---|---|
|
||||
| 1 | Нет репозитория git, истории изменений нет. | Не закрыт |
|
||||
| 2 | Версии в `requirements.txt` не закреплены, это влияет и на образ. | Не закрыт |
|
||||
| 3 | После порчи базы API отдаёт пустой список, пока сборщик не наполнит новую базу; автовосстановления из копии нет. Курсоры diff после пересоздания базы недействительны (`410`), клиент делает полную выгрузку. | Не закрыт |
|
||||
| 4 | MikroTik и FRR не проверены на реальном ПО. | Не закрыт |
|
||||
| 5 | Токен идёт по HTTP открытым текстом. | Осознанный выбор |
|
||||
| 6 | Один общий токен, без ротации и аудита. | Не закрыт |
|
||||
| 7 | Тестов минимум, что соответствует правилам проекта. | Осознанно |
|
||||
| 8 | `POST /collect` без ограничения частоты: повторные запросы во время идущего сбора пропускаются, но защиты от нагрузки на RIPE нет. | Не закрыт |
|
||||
| 9 | Журнал diff проверен только на копиях и тестах: размер и нагрузка на реальных данных неизвестны. | Не закрыт (новое) |
|
||||
|
||||
## Наблюдения
|
||||
|
||||
| # | Наблюдение | Состояние |
|
||||
|---|---|---|
|
||||
| 1 | **Изменения не применены к реальным данным.** В каталоге проекта нет `ripe.db`, `data.json` и `fqdn_data.json` остались в старом формате; всё проверено только на копиях. Первый запуск на боевых данных выполнит миграцию до схемы версии 2: `last_seen` старых записей станет равным времени миграции, журнал diff начнётся пустым (импорт в него не пишется), клиентам стартовать с `X-Changes-Cursor`. | Без изменений |
|
||||
| 2 | **`/health` не видит сбоев источников.** Недоступный RIPE или DNS пишется в лог, задание считается успешным (`last_error` отражает только исключение всего сбора). | Без изменений |
|
||||
| 3 | ~~Мусор от сборок: 65 безымянных образов.~~ Сейчас 3 безымянных образа. | Снято |
|
||||
| 4 | **Повреждённый `config.json`** переименовывается при чтении; первый `POST /asns` после этого создаст файл без остальных источников, расписания и `ttl_days`. | Без изменений |
|
||||
| 5 | **Логи:** сообщение «data saved» пишется после каждой транзакции, даже без изменений (`cidr_collector.py:152,206`). | Без изменений |
|
||||
| 6 | **Структура README:** разделы 8 (Docker) и 9 (SQLite) дописаны в конец, разделы 1-4 описывают ручную установку. Стоит перестроить: Docker в начало, ручная установка ниже. | Без изменений |
|
||||
| 7 | **`google.com`** в данных не входит в конфигурацию, его адрес удалится через 90 дней после миграции. | Без изменений |
|
||||
| 8 | **Ручной сбор стартует не мгновенно**, а в пределах 5 секунд (интервал опроса демона). | Без изменений |
|
||||
| 9 | **Логи APScheduler о плановых запусках скрыты** (чтобы опрос запросов каждые 5 с не засорял лог); остаются сообщения самого приложения. | Без изменений |
|
||||
| 10 | **Общая точка отказа хранилища (по графу).** Главные узлы: `load_json()` (19 связей), `session()` (17), `StorageError` (16). Это осознанная развязка: одно исключение скрывает JSON и SQLite от API, демона и CLI (проверено по исходникам: `api_server.py:94,192`, `collector_daemon.py:79,102,147`, `cidr_collector.py:265`). Но любое изменение `storage.py`/`db.py` затрагивает все три процесса, покрытие тестами здесь важнее всего. | Новое |
|
||||
| 11 | **`api_server.py` растёт (320 строк, связность сообщества 0,06 по графу):** схемы, проверки, все эндпоинты в одном модуле; после diff стал больше. Разделять пока не нужно, но при следующем эндпоинте стоит вынести схемы и разбор параметров. | Новое |
|
||||
| 12 | **Граф не заменяет проверку.** 19 изолированных узлов (например, описания эндпоинтов в README) не связаны с обработчиками в коде: это ограничение семантической выгрузки, а не обязательно пробел в документации. Расход токенов на построение в `cost.json` не записан (нули). Документы `analysis-*.md` сами входят в граф, поэтому он частично отражает выводы анализа, а не независимую оценку. | Новое |
|
||||
|
||||
## Рекомендуемый порядок
|
||||
|
||||
1. **Гигиена и резервные копии:** `git init` и первый коммит (после этого можно поставить хук графа), закрепление версий, ежедневное задание демона `db_backup` (`.backup` с хранением нескольких копий) и автовосстановление из последней копии при порче.
|
||||
2. **Наблюдаемость:** учёт ошибок по каждому источнику (`last_success`, число ошибок) в `status.json` и `/health`, затем `/metrics` для Prometheus и повторные попытки запросов к RIPE.
|
||||
3. **Развёртывание на реальных данных:** запуск через compose с миграцией текущих файлов (наблюдение 1), оценка размера журнала (риск 9).
|
||||
4. **TLS-прокси** перед API, если появится внешний доступ.
|
||||
5. **Уведомления** (webhook, Telegram): демон после сбора считает diff по журналу и отправляет непустой результат.
|
||||
|
||||
Обновлять граф после доработок: `/graphify . --update`.
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- `plan-*.md` / `summary-*.md`: reliability-security, tests-in-container, output-formats, process-split, asn-fqdn-api, app-container, sqlite-storage, collect-endpoint, diff-endpoint.
|
||||
- Предыдущий анализ: `analysis-2026-09-20.md`.
|
||||
- Граф: `graphify-out/GRAPH_REPORT.md`, `graphify-out/graph.html`.
|
||||
@@ -0,0 +1,62 @@
|
||||
# План: контейнер для приложения (Dockerfile и compose)
|
||||
|
||||
## Context
|
||||
Сейчас приложение ставится вручную: venv, два systemd/OpenRC-сервиса, ручное управление пользователем и правами. Контейнеры используются только для тестов (`Dockerfile.test`). Цель: запускать API и демон-сборщик одной командой `docker compose up -d`, с данными на томе, без root и с проверками состояния. Формат данных и API не меняются.
|
||||
|
||||
Решение пользователя: порт 8000 публикуется наружу без TLS (как сейчас). Риск: токен `X-API-Key` идёт по HTTP открытым текстом, защита только файрволом; это фиксируется в README, TLS - отдельная доработка.
|
||||
|
||||
## Артефакты по правилам проекта (создаются при реализации)
|
||||
- `docs/plan-app-container.md` - копия этого плана (первым шагом)
|
||||
- `docs/summary-app-container.md` - итоги (в конце)
|
||||
- обновить `README.md` (раздел про Docker Compose, миграция существующих данных)
|
||||
|
||||
## Архитектура
|
||||
Один образ, два сервиса compose (`api`, `collector`) с общим томом `ripe_data`, смонтированным в `/data`. Сервисы по-прежнему общаются только файлами (`config.json`, `data.json`, `fqdn_data.json`, `status.json`, `*.lock`).
|
||||
|
||||
## Изменения
|
||||
|
||||
### 1. Код: настраиваемый каталог данных (минимальная правка)
|
||||
- `cidr_collector.py`: добавить `DATA_DIR = os.environ.get("RIPE_DATA_DIR", BASE_DIR)`; пути `CONFIG_FILE`, `DATA_FILE`, `FQDN_DATA_FILE`, `STATUS_FILE` строятся от `DATA_DIR`. Без переменной поведение прежнее (systemd/venv-установки не ломаются).
|
||||
- `collector_daemon.py`: `LOCK_FILE` строится от `cc.DATA_DIR` (сейчас `cc.BASE_DIR`).
|
||||
- Тесты не меняются (они подменяют атрибуты модуля).
|
||||
|
||||
### 2. `Dockerfile` (новый)
|
||||
- База `python:3.11-slim`; зависимости отдельным слоем из `requirements.txt` (`pip install --no-cache-dir`).
|
||||
- Копируются только рабочие файлы: `api_server.py`, `cidr_collector.py`, `collector_daemon.py`, `formatters.py`, `storage.py`, `healthcheck.py`.
|
||||
- Пользователь `ripe` (uid 10001), `/data` создаётся и принадлежит ему (именованный том при первом создании наследует владельца).
|
||||
- `ENV RIPE_DATA_DIR=/data PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1`, `VOLUME /data`.
|
||||
- `CMD` в exec-форме (сигналы доходят до процесса): по умолчанию `uvicorn api_server:app --host 0.0.0.0 --port 8000`; сборщик переопределяет команду в compose.
|
||||
|
||||
### 3. `healthcheck.py` (новый, маленький)
|
||||
- `python healthcheck.py api` - GET `http://127.0.0.1:8000/health`, успех при HTTP 200.
|
||||
- `python healthcheck.py collector` - читает `status.json` (`cc.STATUS_FILE`), успех, если `updated_at` моложе `cc.STATUS_STALE_AFTER` (переиспользуем константы и логику heartbeat).
|
||||
|
||||
### 4. `docker-compose.yml` (новый)
|
||||
- `api`: `build: .`, `ports: "8000:8000"`, `env_file: .env` (`RIPE_API_TOKEN`), `healthcheck` (api), `restart: unless-stopped`.
|
||||
- `collector`: тот же образ, `command: ["python", "collector_daemon.py"]`, `healthcheck` (collector), `stop_grace_period: 60s` (идущий сбор успевает завершиться по SIGTERM), токен не передаётся.
|
||||
- Оба: `volumes: ripe_data:/data`, `environment: TZ=${TZ:-UTC}` (расписание cron считается в этом поясе; в README отметить), усиление изоляции: `read_only: true`, `tmpfs: /tmp`, `cap_drop: [ALL]`, `security_opt: [no-new-privileges:true]`, ротация логов `json-file` (`max-size: 10m`, `max-file: 3`).
|
||||
- `volumes: ripe_data:`.
|
||||
- `.env.example` с `RIPE_API_TOKEN=change-me` и `TZ=UTC`; `.env` в `.gitignore` и `.dockerignore`. Если токен не задан, `POST` остаётся отключённым (503) - поведение fail closed сохраняется.
|
||||
|
||||
### 5. Документация и миграция (`README.md`)
|
||||
- Раздел «Docker Compose»: `cp .env.example .env`, генерация токена, `docker compose up -d`, `docker compose ps`, логи, обновление (`docker compose build && docker compose up -d`).
|
||||
- Миграция существующих данных в том: копирование `config.json`, `data.json`, `fqdn_data.json` во временный контейнер с `chown 10001`.
|
||||
- Предупреждения: порт 8000 без TLS - токен виден в сети, ограничить файрволом или поставить proxy; часовой пояс `TZ`; `google.com` в текущих данных не входит в конфигурацию и будет стареть по TTL.
|
||||
- Уточнить: systemd/OpenRC-установка остаётся рабочей альтернативой; `Dockerfile.test` используется только для тестов.
|
||||
- `.dockerignore`: добавить `.env`.
|
||||
|
||||
### 6. Тесты
|
||||
Новых автотестов не добавляем (минимум по правилам проекта): `healthcheck.py` и сборка проверяются в контейнерном сценарии ниже; 13 существующих тестов продолжают проходить (и в `Dockerfile.test`).
|
||||
|
||||
## Критичные файлы
|
||||
Новые: `Dockerfile`, `docker-compose.yml`, `healthcheck.py`, `.env.example`. Правки: `cidr_collector.py` (пути), `collector_daemon.py` (`LOCK_FILE`), `README.md`, `.gitignore`, `.dockerignore`.
|
||||
|
||||
## Проверка
|
||||
1. `docker build -f Dockerfile.test ...` и запуск: 13 passed (регрессия после правки путей).
|
||||
2. `docker compose build && docker compose up -d` с тестовым `.env` на копии данных (проект в каталоге scratchpad, реальные данные не трогаем); порт можно временно сменить переменной, чтобы не конфликтовать.
|
||||
3. `docker compose ps`: оба сервиса `healthy` не позднее чем через ~1 минуту.
|
||||
4. `curl /health`: `collector_alive: true`; `POST /asns` с токеном (201), без токена (401); `GET /addresses?format=nftables` отдаёт конфиг.
|
||||
5. Данные переживают `docker compose down && up -d` (том), файлы в томе принадлежат uid 10001; `docker compose exec api id` - не root; запись вне `/data` невозможна (read-only rootfs).
|
||||
6. `docker compose stop collector` завершается штатно (меньше `stop_grace_period`), остановка демона делает `/health` `degraded` через ~2 минуты.
|
||||
7. Миграция: скопировать реальные `config.json`/`data.json`/`fqdn_data.json` (копии) в том по инструкции README, убедиться, что API видит те же адреса.
|
||||
8. По окончании убрать тестовые контейнеры, тома и образы, созданные при проверке.
|
||||
@@ -0,0 +1,29 @@
|
||||
# План: управление ASN и FQDN через API (реализация - отдельной командой)
|
||||
|
||||
## Часть B. Управление ASN и FQDN через API (только план)
|
||||
|
||||
### Эндпоинты
|
||||
Изменяющие запросы требуют `X-API-Key` (как `POST /schedule`, тот же `verify_token`); чтение открыто.
|
||||
|
||||
| Метод и путь | Действие |
|
||||
| :--- | :--- |
|
||||
| `GET /asns`, `GET /fqdns` | список из `config.json` |
|
||||
| `POST /asns` `{"asn": 62041}` | добавить (идемпотентно: 201 при добавлении, 200 если уже есть) |
|
||||
| `POST /fqdns` `{"fqdn": "example.com"}` | то же |
|
||||
| `DELETE /asns/{asn}?purge=false`, `DELETE /fqdns/{fqdn}?purge=false` | убрать из конфигурации; 404, если нет |
|
||||
|
||||
### Правила
|
||||
- **Валидация:** ASN - целое `1..4294967295` (pydantic `Field`); FQDN - нормализация (нижний регистр, без завершающей точки), длина до 253, метки до 63 символов из `[a-z0-9-]`, без начального и конечного дефиса, IP-литералы отклоняются. Ошибки -> 422.
|
||||
- **Запись конфига:** общий хелпер `update_config(mutator)` в `cidr_collector.py` (блокировка `flock` + атомарная запись), используется в `POST /schedule`, новых эндпоинтах и в `CIDRCollector.add_asn/remove_asn`, `FQDNCollector.add_fqdn/remove_fqdn` - заодно закрывает риск «CLI меняет конфиг без блокировки».
|
||||
- **Данные удалённого источника:** по умолчанию сохраняются. Чтобы не висеть вечно (TTL применяется только к опрашиваемым источникам), в `run_collection` под блокировкой данных записи, которых уже нет в конфигурации, проходят `merge_entry` с пустым набором: адреса истекают по `ttl_days`, пустая запись удаляется. `purge=true` удаляет запись из `data.json`/`fqdn_data.json` сразу (под блокировкой данных, после обновления конфига, без вложенных блокировок).
|
||||
- В `run_collection` перед слиянием конфигурация перечитывается под блокировкой данных: источник, удалённый во время сбора, не воскресает.
|
||||
- Новые источники подхватываются на ближайшем запуске сбора (демон читает конфиг при каждом запуске); немедленный сбор по запросу (`POST /collect`) - за рамками этого шага.
|
||||
|
||||
### Тесты (минимум, в контейнере)
|
||||
1. Добавление/удаление ASN и FQDN: 401 без ключа, 201/200 идемпотентность, 422 на невалидные значения, 404 при удалении несуществующего, конфиг обновлён.
|
||||
2. `purge=true` удаляет данные источника, без `purge` - данные остаются; при сборе запись без источника в конфигурации истекает по TTL.
|
||||
|
||||
### Проверка
|
||||
Тесты в контейнере; вручную `curl` с токеном на копии данных (добавить, увидеть в `GET /asns`, запуск сбора демоном, удалить с `purge`, адреса исчезли из `/addresses`).
|
||||
|
||||
Зависит от `docs/plan-process-split.md` (выполняется первым).
|
||||
@@ -0,0 +1,22 @@
|
||||
# План: POST /collect (немедленный сбор)
|
||||
|
||||
## Цель
|
||||
После добавления источника (или по необходимости) запускать сбор сразу, не дожидаясь расписания. API и сборщик - разные процессы, поэтому API не собирает сам, а передаёт демону запрос через файл.
|
||||
|
||||
## Дизайн
|
||||
- `POST /collect` (токен `X-API-Key`), тело необязательно: `{"type": "asn" | "fqdn" | "all"}`, по умолчанию `all`. Ответ `202` с перечнем запрошенных типов; ход выполнения видно в `GET /health`.
|
||||
- Если демон не жив (heartbeat старше 120 с или его не было), API отвечает `503`, запрос не ставится в очередь «в пустоту».
|
||||
- Запрос передаётся файлом `collect_request.json` в `DATA_DIR` (запись атомарная, под блокировкой): повторные `POST` объединяются (типы складываются), лишних сборов не будет.
|
||||
- Демон: задание `check_collect_requests` каждые 5 секунд (`TRIGGER_POLL_INTERVAL`) забирает и удаляет файл и ставит разовое задание `<type>_manual` (не блокирует опрос).
|
||||
- Защита от наложения: на каждый тип один запуск за раз (неблокирующая блокировка в `run_job`); если такой сбор уже идёт (по расписанию или ручной), повторный запуск пропускается с записью в лог.
|
||||
- `status.json`: для каждого задания добавляются `running` и `last_finished`, чтобы клиент мог дождаться завершения через `/health`.
|
||||
|
||||
## Изменения
|
||||
1. `cidr_collector.py`: `COLLECT_REQUEST_FILE`, `TRIGGER_POLL_INTERVAL`, `request_collection(types)`, `pop_collection_requests()`.
|
||||
2. `collector_daemon.py`: блокировки запусков, поля `running`/`last_finished`, `check_collect_requests(scheduler)`, регистрация задания в `build_scheduler`.
|
||||
3. `api_server.py`: общий помощник состояния демона (используется `/health` и `/collect`), эндпоинт `POST /collect`.
|
||||
4. `README.md`: описание эндпоинта, поля `/health`; `.gitignore`/`.dockerignore`: `collect_request.json`.
|
||||
5. Тесты (2): API (401 без ключа, 503 без демона, 202 с объединением типов, 422 на неверный тип); демон (запрос превращается в разовые задания, файл удалён, повторный вызов ничего не делает).
|
||||
|
||||
## Проверка
|
||||
Тесты в контейнере; вручную на копии данных: демон и API отдельными процессами с редким расписанием, `POST /collect` запускает сбор за несколько секунд, `/health` показывает `running` и `last_finished`; при остановленном демоне 503; два быстрых запроса дают один сбор; проверка в Docker Compose (общий том).
|
||||
@@ -0,0 +1,27 @@
|
||||
# План: `GET /addresses/diff?since=` (изменения списка)
|
||||
|
||||
## Цель
|
||||
Клиент (роутер, файрвол, скрипт) запрашивает только изменения с момента прошлой синхронизации: что добавилось и что исчезло. Полный список каждый раз не скачивается.
|
||||
|
||||
## Дизайн
|
||||
- `GET /addresses/diff?since=<время | курсор>[&type=cidr|fqdn|all][&ip_version=all|4|6]`, чтение открыто (как у `/addresses`).
|
||||
- Ответ: `{"since", "now", "cursor", "added": [...], "removed": [...]}`. Форматы конфигураций и агрегация не поддерживаются (агрегированный diff не аддитивен).
|
||||
- **Журнал изменений** в SQLite: таблица `changes(id, ts, kind, value, action add|del)`. Пишется триггерами на `addresses`, поэтому охватывает все пути удаления (TTL, снятый источник, `purge`) и добавления:
|
||||
- `add`: значение появилось, а у других источников его не было;
|
||||
- `del`: удалена последняя запись значения.
|
||||
Значение, которое есть у нескольких источников, в diff не попадает, пока хотя бы один источник его держит. Первичный импорт из JSON в журнал не пишется.
|
||||
- **Итоговый эффект, а не история:** по каждому значению берётся первое действие после точки `since` (`add` - значения не было, `del` - было) и сверяется с текущим состоянием. Удалено и возвращено в тот же интервал - в diff не попадает.
|
||||
- **Точка отсчёта:** `since` - время ISO 8601 (без часового пояса считается UTC) либо целый курсор из прошлого ответа. **Рекомендуется курсор:** порядковый номер записи журнала не зависит от часов и от длительности транзакции сборщика. Время округляется до миллисекунд, границы включительные (доставка «минимум один раз», повтор безвреден).
|
||||
- **Срок хранения:** `changes_retention_days` в `config.json` (по умолчанию 30, `<= 0` - без очистки). Очистка выполняется в транзакции сбора. Границу («горизонт») хранит таблица `meta`. Если `since` старше горизонта или курсор не из этой базы (больше текущего) - `410 Gone`: клиент забирает полный `/addresses` и продолжает с нового `cursor`.
|
||||
- Ошибки: `400` при неверном `since`; `503` при недоступной базе (общий обработчик).
|
||||
- Чтение diff выполняется в одной read-транзакции (согласованный снимок).
|
||||
|
||||
## Изменения
|
||||
1. `db.py`: `SCHEMA_VERSION = 2` (миграция 1 -> 2: `changes`, `meta`, триггеры; для v0 таблицы создаются после импорта JSON), `prune_changes()`, `get_changes()`.
|
||||
2. `cidr_collector.py`: `DEFAULT_CHANGES_RETENTION_DAYS`, вызов очистки в обоих `run_collection`.
|
||||
3. `api_server.py`: эндпоинт `/addresses/diff`, разбор `since`; заголовок `X-Changes-Cursor` в ответах `/addresses` (курсор для первой синхронизации, читается до данных).
|
||||
4. `README.md`: описание эндпоинта, ключ конфигурации, раздел о хранении.
|
||||
5. Тесты (2): БД (триггеры: несколько источников, TTL, возврат в тот же интервал, очистка и горизонт); API (400, 410, курсор и время, фильтры).
|
||||
|
||||
## Проверка
|
||||
Тесты в контейнере; вручную на копии данных: миграция базы версии 1 -> 2 сохраняет данные, `purge`/TTL порождают `removed`, повторный запрос с `cursor` возвращает пустой diff.
|
||||
@@ -0,0 +1,20 @@
|
||||
# План: репозиторий git (п. 1.1 рекомендаций)
|
||||
|
||||
## Цель
|
||||
Появляется история изменений (риск 1 анализа): `git init`, ветка `main`, подключение удалённого репозитория, первый коммит текущего состояния.
|
||||
|
||||
## Решения
|
||||
- Ветка `main`, remote `origin` = `https://artstore.rxmsk.ru/ayurishchev/ripe-cidr-collector.git` (подготовлен пользователем).
|
||||
- Автор коммита: существующая глобальная настройка git (`ayurishchev`); настройки не меняются.
|
||||
- **В репозиторий входят:** код, тесты, `Dockerfile*`, `docker-compose.yml`, `requirements*.txt`, `config.json` (исходная конфигурация источников), `.env.example`, `docs/`, `README.md`, `.claude/CLAUDE.md` (правила проекта).
|
||||
- **Не входят:** `venv/`, кэши, `.env`, базы `*.db*`, служебные файлы, `graphify-out/` (уже в `.gitignore`) и **боевые данные `data.json`, `fqdn_data.json`** (старый формат, ждут миграции в SQLite; в `.dockerignore` они уже исключены, в `.gitignore` не хватало).
|
||||
- Проверка перед коммитом: список файлов в индексе, поиск секретов (токены, пароли); `.env.example` содержит только заглушку.
|
||||
- **Отправка на сервер (`git push`) выполняется только после подтверждения пользователя:** это публикация кода вовне.
|
||||
|
||||
## Изменения
|
||||
1. `.gitignore`: `data.json`, `fqdn_data.json`.
|
||||
2. `README.md`: раздел о репозитории (что входит и не входит, как обновлять граф).
|
||||
3. Артефакты: этот план и `docs/summary-git-init.md`.
|
||||
|
||||
## Проверка
|
||||
`git status` чистый после коммита; `git ls-files` не содержит данных, баз, `venv`, `.env`; тесты в контейнере проходят (код не менялся).
|
||||
@@ -0,0 +1,65 @@
|
||||
# План: форматы вывода, агрегация CIDR и ip_version
|
||||
|
||||
## Context
|
||||
`GET /addresses` отдаёт только плоский JSON-список. Потребителям (маршрутизаторы, файрволы) приходится самим конвертировать его в конфигурацию и убирать пересекающиеся префиксы (`/23` + `/24`). Цель: отдавать готовые конфигурации для nftables, MikroTik, BIRD и FRR, добавить агрегацию CIDR и фильтр по версии IP. Текущий ответ по умолчанию остаётся байт-в-байт прежним (старые потребители не ломаются).
|
||||
|
||||
Решения пользователя: форматы `nftables`, `mikrotik`, `bird`, `frr` (плюс текущий JSON); агрегация выключена по умолчанию, включается `aggregate=true`.
|
||||
|
||||
## Артефакты по правилам проекта (создаются при реализации)
|
||||
- `docs/plan-output-formats.md` - копия этого плана (первым шагом)
|
||||
- `docs/summary-output-formats.md` - итоги (в конце)
|
||||
- обновить `README.md` (параметры, примеры интеграции)
|
||||
|
||||
## API
|
||||
`GET /addresses` получает параметры (существующий `type=cidr|fqdn|all` не меняется):
|
||||
|
||||
| Параметр | Значения | По умолчанию |
|
||||
| :--- | :--- | :--- |
|
||||
| `format` | `json`, `nftables`, `mikrotik`, `bird`, `frr` | `json` |
|
||||
| `ip_version` | `all`, `4`, `6` | `all` |
|
||||
| `aggregate` | `true`/`false` | `false` |
|
||||
| `name` | имя списка/набора, `^[A-Za-z][A-Za-z0-9_]{0,31}$` (защита от инъекций в конфиг) | `ripe` |
|
||||
|
||||
Не-JSON форматы отдаются как `text/plain`. Невалидные значения -> 422 (валидация FastAPI).
|
||||
|
||||
## Изменения
|
||||
|
||||
### 1. Новый модуль `formatters.py` (чистые функции, без обращений к диску и сети)
|
||||
- `select(values, ip_version, aggregate)`:
|
||||
- Путь по умолчанию (`json`, `aggregate=false`): только фильтр по версии (по наличию `:` в строке), сортировка как сейчас (`sorted(set(...))`) - вывод не меняется.
|
||||
- Иначе: разбор через `ipaddress.ip_network(v, strict=False)` (одиночные IP из FQDN становятся `/32` и `/128`), невалидные значения пропускаются с `logging.warning`; при `aggregate=true` - `ipaddress.collapse_addresses` отдельно для v4 и v6 (после объединения источников `cidr` и `fqdn`, поэтому IP внутри префикса исчезает); сортировка по (версия, сеть).
|
||||
- Для `json` с агрегацией `/32` и `/128` печатаются как «голый» IP, как в текущем README.
|
||||
- `render(fmt, v4, v6, name)`: каждый формат возвращает идемпотентный скрипт; пустая версия пропускается.
|
||||
- **nftables** (`nft -f`): `add table inet <name>`; `add set inet <name> <name>_v4 { type ipv4_addr; flags interval; auto-merge; }`; `flush set ...`; `add element ... { ... }` (то же для `_v6`, `ipv6_addr`). `auto-merge` нужен, иначе nft отвергает пересекающиеся интервалы. Другие объекты таблицы не затрагиваются.
|
||||
- **MikroTik** (RouterOS `/import`): `/ip firewall address-list remove [find list=<name>]` затем `add list=<name> address=...`; для v6 - `/ipv6 firewall address-list`.
|
||||
- **BIRD 2** (`include`): `define <NAME>_V4 = [ a/len, ... ];` и `..._V6` - префикс-сеты для фильтров (не статические маршруты: next-hop определяет потребитель).
|
||||
- **FRR** (`vtysh -f`): `no ip prefix-list <name>_v4` затем `ip prefix-list <name>_v4 seq 5|10|... permit <prefix>`; для v6 - `ipv6 prefix-list`.
|
||||
- Все скрипты начинаются с комментарием `# generated <ts>, ip_version=..., aggregate=...` (в синтаксисе формата), заканчиваются переводом строки.
|
||||
|
||||
### 2. `api_server.py` (`get_addresses`)
|
||||
- Добавить enum-параметры `format`, `ip_version`, `aggregate: bool`, `name` (Query с regex).
|
||||
- Собрать множество как сейчас (`get_cidrs`, `get_fqdn_ips`), затем `formatters.select` и `render`.
|
||||
- Убрать `response_model=List[str]` (ответ бывает текстовым); для JSON возвращать список как раньше; для остальных - `PlainTextResponse`. Описать типы ответов через `responses=` для Swagger.
|
||||
- Ошибки хранилища по-прежнему -> 503 (существующий обработчик).
|
||||
|
||||
### 3. Документация (`README.md`)
|
||||
- Таблица параметров и примеры:
|
||||
- nftables: `curl -s "$URL/addresses?format=nftables&ip_version=4&aggregate=true" | nft -f -`
|
||||
- MikroTik: `/tool fetch url="$URL/addresses?format=mikrotik" dst-path=ripe.rsc` и `/import ripe.rsc`
|
||||
- BIRD: сохранить в файл, `include`, `birdc configure`
|
||||
- FRR: `curl ... > ripe.conf && vtysh -f ripe.conf`
|
||||
- Примечания: имя `name` определяет имена наборов; у MikroTik замена списка даёт короткое окно без записей; агрегация удаляет `/32` внутри более широкого префикса.
|
||||
|
||||
### 4. Тесты (минимум 3, `tests/test_formats.py`, запуск в контейнере)
|
||||
1. `select`: по умолчанию вывод равен прежнему (сортировка, без /32), `ip_version=4|6` фильтрует, `aggregate=true` схлопывает `/23`+`/24` и вложенный host-IP.
|
||||
2. `render`: параметризованный тест по 4 форматам - ключевые строки (для v4 и v6, пустая версия пропущена).
|
||||
3. API: дефолтный `/addresses` возвращает JSON-список, `format=mikrotik` - `text/plain`, некорректное `name` -> 422 (данные подменены через `cc.DATA_FILE`).
|
||||
|
||||
## Критичные файлы
|
||||
Новый `formatters.py`; `api_server.py` (`get_addresses`); `tests/test_formats.py`; `README.md`; `Dockerfile.test` не меняется (`COPY . .` подхватит модуль).
|
||||
|
||||
## Проверка
|
||||
1. `docker build -f Dockerfile.test -t ripe-collector-test . && docker run --rm ripe-collector-test` - все тесты (включая 3 прежних) проходят.
|
||||
2. На копии реальных данных: `curl /addresses` до и после изменений даёт идентичный ответ (сравнить diff).
|
||||
3. Вручную: `format=...` для каждого формата; `aggregate=true` уменьшает число строк; `ip_version=6` не содержит IPv4.
|
||||
4. Проверка синтаксиса генерируемых конфигов в контейнерах, где возможно: BIRD (`bird -p -c`), nftables (`nft -c -f`, если хватит прав). MikroTik и FRR сверить по документации (эмулятора нет); риск: поведение `no ip prefix-list` для несуществующего списка в FRR - проверить по документации.
|
||||
@@ -0,0 +1,55 @@
|
||||
# План: разделение сборщика и API на два процесса
|
||||
|
||||
## Context
|
||||
Сейчас планировщик (APScheduler) живёт внутри процесса API: перезапуск или падение API останавливает сбор, а тяжёлый сбор делит процесс с обработкой запросов. Плюс ASN и FQDN можно менять только через CLI и правкой `config.json`.
|
||||
|
||||
Запрос состоит из двух частей, порядок выполнения важен, так как часть B опирается на часть A:
|
||||
- **Часть A - разделение на два процесса: планируется и выполняется** после утверждения плана.
|
||||
- **Часть B - управление ASN и FQDN через API: только план.** Реализация начнётся отдельной командой после части A.
|
||||
|
||||
Артефакты по правилам проекта: `docs/plan-process-split.md` и `docs/plan-asn-fqdn-api.md` (копии соответствующих частей плана, первым шагом), затем `docs/summary-process-split.md` (после части A) и обновление `README.md`.
|
||||
|
||||
## Часть A. Разделение на два процесса (выполняется)
|
||||
|
||||
### Архитектура
|
||||
Два независимых сервиса без IPC, общение через файлы в каталоге проекта (уже атомарные и под `flock`):
|
||||
- **API** (`uvicorn api_server:app`): только читает данные и пишет `config.json`; планировщика в нём больше нет.
|
||||
- **Сборщик-демон** (`python collector_daemon.py`, новый): держит расписание, запускает сбор, пишет `status.json`.
|
||||
|
||||
Изменение расписания через `POST /schedule` доходит до демона через `config.json`: демон раз в 30 секунд сверяет секцию `schedule` и перепланирует задания без перезапуска (задержка применения до 30 с, документируется).
|
||||
|
||||
### Изменения
|
||||
1. **`collector_daemon.py` (новый)**
|
||||
- `BlockingScheduler`, задания `asn_job` и `fqdn_job` из `schedule` (значения по умолчанию как сейчас: `0 2 * * *` / `0 3 * * *`), `max_instances=1`, `coalesce=True`.
|
||||
- Задание `sync_schedule` каждые 30 с: читает `load_full_config()`, при изменении cron делает `reschedule_job`; невалидный cron логируется и игнорируется (остаётся прежнее расписание).
|
||||
- `run_job(name, collector_cls)` переезжает из `api_server.py` (`CIDRCollector`/`FQDNCollector` создаются заново на каждый запуск - свежий конфиг); статусы `last_run`, `last_error`, `next_run` пишутся в `status.json` атомарно вместе с `updated_at` (heartbeat каждые 30 с).
|
||||
- Единственный экземпляр: неблокирующий `flock` на `collector.daemon.lock`; второй экземпляр завершается с понятной ошибкой.
|
||||
- Корректное завершение по `SIGTERM`/`SIGINT` (`scheduler.shutdown`).
|
||||
2. **`storage.py`**: добавить `try_lock(path)` (неблокирующая эксклюзивная блокировка для singleton).
|
||||
3. **`cidr_collector.py`**: константа `STATUS_FILE` рядом с остальными путями (единая точка для API и тестов).
|
||||
4. **`api_server.py`**
|
||||
- Убрать `BackgroundScheduler`, `lifespan`, `job_state`, `run_*_job`, `JOBS`, `start_scheduler`.
|
||||
- `POST /schedule`: валидация cron через `CronTrigger.from_crontab` (как сейчас), запись в `config.json` под блокировкой; ответ уточняет, что применение демоном до 30 с.
|
||||
- `GET /health`: читает `status.json`; `collector_alive = now - updated_at < 120 с`; `status = ok` только если демон жив и нет `last_error`, иначе `degraded` (нет файла = демон не запускался). HTTP-код остаётся 200.
|
||||
5. **Развёртывание (`README.md`, `.gitignore`, `.dockerignore`)**
|
||||
- Второй сервис `ripe-collector` для systemd и OpenRC (та же учётная запись `ripe`, `ExecStart=.../python collector_daemon.py`, `Restart=always`); юниты API и сборщика независимы.
|
||||
- Раздел про cron остаётся как альтернатива ручного запуска (`cidr_collector.py run`), но основной путь - демон.
|
||||
- **Примечание об обновлении:** после разделения одного `ripe-api` недостаточно, без `ripe-collector` сбор не идёт; `/health` покажет `degraded`.
|
||||
- Переписать раздел «Scheduler Logic» под новую схему; добавить `status.json` в `.gitignore` и `.dockerignore`.
|
||||
|
||||
### Тесты (минимум, в контейнере)
|
||||
1. `sync_schedule`: смена cron в `config.json` перепланирует задание; невалидный cron игнорируется (планировщик без запуска, проверка триггера задания).
|
||||
2. `/health`: свежий `status.json` -> `collector_alive: true`, устаревший или отсутствующий -> `false` и `degraded`.
|
||||
|
||||
### Проверка
|
||||
1. `docker build -f Dockerfile.test -t ripe-collector-test . && docker run --rm ripe-collector-test` - все тесты (прежние 9 + новые) проходят.
|
||||
2. Вручную на копии данных в scratchpad (реальные данные не трогаем): запустить демон с расписанием `*/1 * * * *` и API в двух процессах; убедиться, что сбор идёт без API; `POST /schedule` с токеном меняет расписание демона не позднее чем через 30 с (по логу и `/health`); остановка демона -> `/health` через 2 минуты `degraded`, `collector_alive: false`; второй запуск демона завершается с ошибкой singleton; `kill -TERM` останавливает демон чисто.
|
||||
3. Перезапуск API во время работы демона не прерывает сбор.
|
||||
|
||||
## Критичные файлы
|
||||
`collector_daemon.py` (новый), `api_server.py`, `cidr_collector.py`, `storage.py`, `README.md`, `.gitignore`, `.dockerignore`, `tests/`. Переиспользуем: `load_full_config`, `save_json_atomic`, `file_lock`, `load_json`, `verify_token`, `merge_entry`.
|
||||
|
||||
## Порядок выполнения после утверждения
|
||||
1. Скопировать планы в `docs/`.
|
||||
2. Выполнить часть A (код, тесты, проверка, README, `docs/summary-process-split.md`).
|
||||
3. Часть B не реализуется, пока не будет отдельной команды.
|
||||
@@ -0,0 +1,59 @@
|
||||
# План: надёжность и безопасность ripe_cidr_collector
|
||||
|
||||
## Context
|
||||
Сейчас данные пишутся неатомарно, при повреждении JSON молча превращаются в `{}` (следующий сбор перезапишет хорошие данные), записи копятся бессрочно, `POST /schedule` открыт всем, а сервис слушает `0.0.0.0` без защиты. Цель: сделать хранение устойчивым, ограничить срок жизни адресов (TTL 90 дней), закрыть управляющий эндпоинт токеном. Формат ответа `GET /addresses` не меняется, поэтому потребители не ломаются.
|
||||
|
||||
Решения пользователя: токен только на `POST`; TTL 90 дней, настраиваемый (0 = бессрочно).
|
||||
|
||||
## Артефакты по правилам проекта (создаются при реализации)
|
||||
- `docs/plan-reliability-security.md` - этот план (копия в проект первым шагом)
|
||||
- `docs/summary-reliability-security.md` - итоги, в конце
|
||||
- обновить `README.md` (токен, TTL, /health, non-root systemd, схема данных)
|
||||
|
||||
## Изменения
|
||||
|
||||
### 1. Новый модуль `storage.py` (общий, убирает дублирование load/save из обоих файлов)
|
||||
- `load_json(path, default)`: при отсутствии файла возвращает default; при битом JSON логирует ошибку, переименовывает файл в `<name>.corrupt-<ts>` и бросает `StorageError` (сборщик прерывается, не затирая данные; API отвечает 503).
|
||||
- `save_json_atomic(path, data)`: запись во временный файл в том же каталоге, `flush` + `fsync`, `os.replace`.
|
||||
- `file_lock(path)`: контекстный менеджер на `fcntl.flock` по `<path>.lock`. Чтение-изменение-запись в сборщике идёт под блокировкой (защита от одновременного cron и APScheduler).
|
||||
|
||||
### 2. `cidr_collector.py`
|
||||
- Заменить локальные `load_*/save_*` и `save_full_config` на функции из `storage.py`.
|
||||
- Схема записи: `prefixes`/`ips` остаются списком (совместимость с API), добавляется `seen: {value: {first_seen, last_seen}}`.
|
||||
- Слияние при успешном получении данных: обновить `last_seen` у увиденных, добавить новые с `first_seen`, удалить те, у кого `last_seen` старше `ttl_days`. При ошибке RIPE/DNS (`None`/пусто) ничего не удаляется.
|
||||
- Миграция на лету: если `seen` нет, инициализировать `first_seen = last_seen = last_updated` (старые данные не теряются).
|
||||
- Частота записи: файл пишется, если изменился состав адресов, либо если `last_seen` у какой-то записи старше 24 ч (иначе при запуске каждые 15 минут файл переписывался бы постоянно). Так TTL остаётся корректным, а лишних записей нет.
|
||||
- `print` заменить на `logging` (INFO/WARNING/ERROR).
|
||||
- `ttl_days` читается из `config.json` (ключ `ttl_days`, по умолчанию 90).
|
||||
|
||||
### 3. `api_server.py`
|
||||
- `POST /schedule`: зависимость `verify_token` - заголовок `X-API-Key`, сравнение через `secrets.compare_digest`. Токен берётся из переменной окружения `RIPE_API_TOKEN` (не из `config.json`). Если переменная не задана, `POST` возвращает 503 (fail closed).
|
||||
- Запись конфига через `storage.save_json_atomic` под блокировкой вместо прямого `open(...,'w')`.
|
||||
- Валидация тела через pydantic-модель (`type: Literal["asn","fqdn"]`, `cron: str`) вместо `Dict[str,str]`.
|
||||
- `@app.on_event` заменить на `lifespan`; в `shutdown` использовать `scheduler.shutdown(wait=False)`.
|
||||
- Добавить `GET /health`: время последнего успешного сбора по asn/fqdn, число записей, статус планировщика.
|
||||
- Ошибки чтения данных (`StorageError`) -> 503 вместо тихого пустого списка.
|
||||
- `__main__`: хост по умолчанию `127.0.0.1`.
|
||||
|
||||
### 4. Развёртывание (только документация в `README.md`)
|
||||
- systemd: запуск от отдельного пользователя `ripe`, `EnvironmentFile=/etc/ripe-api.env` (`RIPE_API_TOKEN=...`, права 600), опции `NoNewPrivileges=true`, `ProtectSystem=strict`, `ReadWritePaths=/opt/ripe_collector`.
|
||||
- OpenRC: аналогично (`command_user`, `env` файл).
|
||||
- Пояснение: `0.0.0.0` оставлен, т.к. потребители удалённые; ограничивать доступ к порту 8000 файрволом.
|
||||
- Добавить `.gitignore` (`venv/`, `*.corrupt-*`, `*.lock`, `*.tmp`).
|
||||
|
||||
### 5. Тесты (минимум, `tests/test_core.py`, pytest)
|
||||
1. Слияние и TTL: старый адрес удаляется по истечении срока, новый добавляется, при ошибке источника ничего не удаляется.
|
||||
2. Битый JSON: `load_json` бросает `StorageError`, файл переименован, исходные данные не перезаписываются.
|
||||
3. Авторизация: `POST /schedule` без токена -> 401, с верным токеном -> 200, без заданного `RIPE_API_TOKEN` -> 503 (`fastapi.testclient`, сеть замокана).
|
||||
Добавить `pytest` и `httpx` в `requirements.txt` (или `requirements-dev.txt`).
|
||||
|
||||
## Критичные файлы
|
||||
`cidr_collector.py`, `api_server.py`, `config.json` (+`ttl_days`), новый `storage.py`, `README.md`, `requirements.txt`.
|
||||
|
||||
## Проверка
|
||||
1. `source venv/bin/activate && pytest -q` - 3 теста зелёные.
|
||||
2. На копии текущих `data.json`/`fqdn_data.json`: `python cidr_collector.py run` - миграция без потери адресов (сравнить количество до/после), повторный запуск не переписывает файл.
|
||||
3. Испортить копию `data.json` - сборщик завершается с ошибкой, файл переименован в `.corrupt-*`, API отвечает 503.
|
||||
4. Запустить `uvicorn api_server:app`: `curl /addresses` без токена работает; `curl -X POST /schedule` без ключа -> 401, с `X-API-Key` -> 200, `config.json` обновлён; `curl /health` возвращает статусы.
|
||||
5. Параллельный запуск двух `run --mode asn` - данные не повреждены (lock работает).
|
||||
6. Выставить `ttl_days` малым значением и проверить удаление устаревшей записи.
|
||||
@@ -0,0 +1,72 @@
|
||||
# План: хранение собранных адресов в SQLite
|
||||
|
||||
## Context
|
||||
Собранные адреса хранятся в `data.json` и `fqdn_data.json`: каждая запись целиком читается и переписывается, доступ между процессами (демон, API, CLI) защищён файловыми блокировками, а повреждение файла ведёт к потере накопленной истории. SQLite даёт транзакции, конкурентное чтение во время записи, запросы по адресам и основу для будущих diff и истории (`first_seen`/`last_seen` уже есть). API, форматы вывода и семантика TTL не меняются.
|
||||
|
||||
Границы: в SQLite переезжают только собранные адреса. `config.json` (источники, расписание, `ttl_days`) и `status.json` (heartbeat) остаются JSON: их редактируют вручную, а демон опрашивает конфиг. Резервное копирование, diff и уведомления в этот шаг не входят.
|
||||
|
||||
## Артефакты по правилам проекта (создаются при реализации)
|
||||
- `docs/plan-sqlite-storage.md` - копия этого плана (первым шагом)
|
||||
- `docs/summary-sqlite-storage.md` - итоги (в конце)
|
||||
- обновить `README.md` (схема хранения, миграция, откат)
|
||||
|
||||
## Схема (`ripe.db` в `DATA_DIR`)
|
||||
```sql
|
||||
CREATE TABLE addresses (
|
||||
kind TEXT NOT NULL CHECK (kind IN ('asn', 'fqdn')),
|
||||
source TEXT NOT NULL, -- '62041' или 'example.com'
|
||||
value TEXT NOT NULL, -- префикс или IP
|
||||
first_seen TEXT NOT NULL, -- ISO-время
|
||||
last_seen TEXT NOT NULL,
|
||||
PRIMARY KEY (kind, source, value)
|
||||
);
|
||||
CREATE INDEX addresses_value ON addresses (value);
|
||||
PRAGMA user_version = 1; -- версия схемы для будущих миграций
|
||||
```
|
||||
Режим `journal_mode=WAL`, `busy_timeout=5000`, `synchronous=NORMAL`, `temp_store=MEMORY`. Время хранится строками ISO (секундная точность для новых записей), сравнение `last_seen < cutoff` корректно лексикографически.
|
||||
|
||||
## Изменения
|
||||
|
||||
### 1. Новый модуль `db.py`
|
||||
- `connect(path=None)`: открывает базу (`cc.DB_FILE`), применяет PRAGMA, при необходимости создаёт схему и **однократно импортирует старые JSON** (см. ниже). Ошибки уровня `sqlite3.DatabaseError`, кроме `OperationalError` (например, «database is locked»), считаются порчей: файл переименовывается в `ripe.db.corrupt-<ts>` (вместе с `-wal`/`-shm`), поднимается `StorageError` (API -> 503, демон записывает `last_error`) - то же поведение, что было для битого JSON.
|
||||
- `merge_source(conn, kind, source, values, now, ttl_days)`: в одной транзакции upsert найденных значений (`ON CONFLICT DO UPDATE SET last_seen`), затем `DELETE ... WHERE kind=? AND source=? AND last_seen < cutoff` (просроченные, которых сегодня не видели; при `ttl_days = 0` не удаляется ничего). Возвращает добавленные и удалённые значения для лога. Заменяет `merge_entry` и правило «обновлять last_seen раз в сутки» (запись в SQLite дешёвая, обновляем всегда).
|
||||
- `sweep_unconfigured(conn, kind, configured, now, ttl_days)`: удаляет просроченные значения источников, которых нет в конфигурации (одним `DELETE ... source NOT IN (...)`); пустые записи как сущность больше не нужны.
|
||||
- `purge_source(conn, kind, source)`, `get_values(conn, kind=None)` (`SELECT DISTINCT value`), `count_values(conn, kind)`.
|
||||
|
||||
### 2. Миграция старых данных (внутри `connect`)
|
||||
- Условие: `user_version = 0` и таблицы нет. Под `BEGIN IMMEDIATE` (API и демон могут стартовать одновременно; второй ждёт и видит уже выполненную миграцию).
|
||||
- Импорт `data.json` (kind `asn`) и `fqdn_data.json` (kind `fqdn`): значения с блоком `seen` сохраняют `first_seen`/`last_seen`; записи старого формата без `seen` (реальные данные сейчас именно такие) получают `first_seen = last_updated`, `last_seen = сейчас` (как в прежней миграции: TTL идёт с момента перехода). Сразу пишется `user_version = 1`.
|
||||
- После успешной транзакции JSON-файлы переименовываются в `data.json.migrated-<ts>` и `fqdn_data.json.migrated-<ts>` (не удаляются - это и есть резервная копия и путь отката).
|
||||
- Импорт идемпотентен: повторный запуск (`user_version = 1`) ничего не читает.
|
||||
|
||||
### 3. `cidr_collector.py` (упрощается)
|
||||
- Константа `DB_FILE = os.path.join(DATA_DIR, "ripe.db")`; `DATA_FILE` и `FQDN_DATA_FILE` остаются только как пути для импорта старых данных.
|
||||
- `CIDRCollector`/`FQDNCollector.run_collection`: сетевая часть без изменений; затем `with db.connect() as conn` (одна транзакция): читает конфиг, для найденных источников вызывает `db.merge_source`, затем `db.sweep_unconfigured`. Источник, удалённый во время сбора, по-прежнему пропускается (проверка по конфигу внутри транзакции).
|
||||
- Удаляются `load_data`/`save_data`, `merge_entry`, `sweep_unconfigured` (JSON-версия), `purge_entry`, `LAST_SEEN_REFRESH`, блокировки `file_lock(DATA_FILE)`. Блокировка конфига (`update_config`) остаётся.
|
||||
|
||||
### 4. `api_server.py`
|
||||
- `get_cidrs()`/`get_fqdn_ips()` и счётчики `/health` читают через `db.get_values`/`db.count_values` (соединение на запрос).
|
||||
- `DELETE /asns|/fqdns ?purge=true` вызывает `db.purge_source`.
|
||||
- `StorageError` от `db.connect` обрабатывается существующим обработчиком (503).
|
||||
|
||||
### 5. Развёртывание
|
||||
- `Dockerfile`: добавить `db.py` в список копируемых файлов (иначе образ не соберёт рабочее приложение).
|
||||
- `.gitignore`/`.dockerignore`: `*.db`, `*.db-wal`, `*.db-shm`, `*.migrated-*`.
|
||||
- Docker: оба сервиса используют одну базу в томе `/data` (WAL работает между контейнерами на одном хосте с локальным томом; не рекомендуется на сетевых ФС - отметить в README). `requirements.txt` не меняется (`sqlite3` из стандартной библиотеки; нужна SQLite >= 3.24 для upsert, в `python:3.11-slim` и на текущем хосте выполняется).
|
||||
|
||||
### 6. Документация (`README.md`)
|
||||
Раздел о хранении: таблица `addresses`, что хранится в JSON, а что в БД, автоматическая миграция, **откат** (остановить сервисы, вернуть `*.migrated-*` в `data.json`/`fqdn_data.json`, запустить прежнюю версию; адреса, собранные после миграции, при откате будут потеряны), просмотр данных (`sqlite3 ripe.db "SELECT ..."`), примечание про сетевые ФС; обновить описание «Collector Logic» и переносимость данных в раздел Docker (миграция теперь копирует и `ripe.db`).
|
||||
|
||||
### 7. Тесты (минимум)
|
||||
Существующие тесты, использовавшие `data.json`, переводятся на БД (подмена `cc.DB_FILE` на временный файл, заполнение через `db.merge_source`): TTL и безопасность при сбое (`test_core`), форматы API (`test_formats`), purge и старение удалённого источника (`test_sources_api`); `test_daemon` не затрагивается. Добавляется 1 тест миграции: JSON в старом формате (без `seen`) и в новом (с `seen`) импортируются с ожидаемыми `first_seen`/`last_seen`, файлы переименованы, повторное открытие ничего не меняет. Итого 14 тестов.
|
||||
|
||||
## Критичные файлы
|
||||
Новый `db.py`; правки `cidr_collector.py`, `api_server.py`, `Dockerfile`, `README.md`, `.gitignore`, `.dockerignore`, `tests/test_core.py`, `tests/test_formats.py`, `tests/test_sources_api.py`. Переиспользуем: `StorageError` (`storage.py`), `update_config`, `load_full_config`, `formatters.build_output` (без изменений: принимает множество значений).
|
||||
|
||||
## Проверка
|
||||
1. `docker build -f Dockerfile.test -t ripe-collector-test . && docker run --rm ripe-collector-test` - 14 тестов.
|
||||
2. **Эталонное сравнение**: на копии реальных данных (`scratchpad/backup`) запустить API после миграции; ответы `/addresses?type=all|cidr|fqdn` побайтно совпадают с эталонами `before_*.txt`, снятыми старой версией; JSON-файлы переименованы, повторный запуск не импортирует заново.
|
||||
3. Сбор на копии: демон/CLI `run` создаёт и обновляет строки; повторный запуск не плодит дубликатов; уменьшенный `ttl_days` удаляет просроченное; `purge=true` удаляет адреса источника.
|
||||
4. Конкурентность: во время идущего сбора цикл запросов `curl /addresses` не даёт ошибок (WAL: чтение не блокируется записью); одновременный старт API и демона на пустой базе выполняет миграцию один раз.
|
||||
5. Порча: записать мусор в `ripe.db` - API отвечает 503, файл переименован в `*.corrupt-*`, демон фиксирует ошибку.
|
||||
6. Docker Compose на копии данных (как в прошлый раз, отдельный проект): оба контейнера работают с одной базой в томе (`healthy`, API видит адреса, собранные демоном, после `down`/`up` данные на месте); по окончании убрать тестовые контейнеры, тома и образы.
|
||||
@@ -0,0 +1,16 @@
|
||||
# План: запуск тестов в контейнере
|
||||
|
||||
## Цель
|
||||
Тесты выполняются в изолированном воспроизводимом окружении (Docker), а не в локальном `venv`.
|
||||
|
||||
## Изменения
|
||||
1. `Dockerfile.test` - образ `python:3.11-slim`, зависимости из `requirements.txt` + `requirements-dev.txt` ставятся отдельным слоем (кэш), затем копируется код; `CMD ["pytest", "-q"]`.
|
||||
2. `.dockerignore` - исключить `venv/`, данные (`data.json`, `fqdn_data.json`), `*.lock`, `__pycache__`, `.git`, чтобы тесты не зависели от боевых данных и образ был лёгким.
|
||||
3. `README.md` - раздел про запуск тестов: `docker build -f Dockerfile.test -t ripe-collector-test . && docker run --rm ripe-collector-test`.
|
||||
4. Убрать из README шаг с локальным `pytest` (пункт 5 установки).
|
||||
|
||||
## Проверка
|
||||
Сборка образа и запуск контейнера: 3 теста проходят; в образе нет `data.json`.
|
||||
|
||||
## Итоги
|
||||
`docs/summary-tests-in-container.md`.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Итоги: контейнер для приложения
|
||||
|
||||
План: `docs/plan-app-container.md`.
|
||||
|
||||
## Сделано
|
||||
- **`Dockerfile`**: `python:3.11-slim`, зависимости отдельным слоем, копируются только рабочие файлы, пользователь `ripe` (uid 10001), `VOLUME /data`, `CMD` в exec-форме (uvicorn).
|
||||
- **`docker-compose.yml`**: сервисы `api` (порт `${API_PORT:-8000}`, токен из `.env`) и `collector` (`collector_daemon.py`, `stop_grace_period: 60s`), общий том `ripe_data`, `read_only`, `tmpfs /tmp`, `cap_drop: ALL`, `no-new-privileges`, ротация логов, healthcheck обоих сервисов, `TZ` для расписания.
|
||||
- **`healthcheck.py`**: `api` (GET `/health`, 200) и `collector` (heartbeat в `status.json` моложе `STATUS_STALE_AFTER`).
|
||||
- **Код**: `RIPE_DATA_DIR` (`cidr_collector.DATA_DIR`) для путей данных и `LOCK_FILE` демона; без переменной поведение прежнее.
|
||||
- **`.env.example`**, `.env` в `.gitignore` и `.dockerignore`; README, раздел 8 (запуск, настройки, миграция данных, эксплуатация, риски).
|
||||
|
||||
## Проверка (стенд в scratchpad, реальные данные не менялись)
|
||||
- Регрессия: 13 тестов в `Dockerfile.test` проходят.
|
||||
- `docker compose up -d`: оба сервиса `healthy` через ~45 с.
|
||||
- Миграция копий данных в том по инструкции: API видит те же адреса (24 CIDR), после `down`/`up -d` данные на месте.
|
||||
- `/health`: `collector_alive: true`; POST без ключа 401, с ключом 201; `format=nftables` работает.
|
||||
- Пользователь в контейнере `uid=10001`, запись вне `/data` даёт `Read-only file system`; `TZ=Europe/Moscow` применился.
|
||||
- Демон в контейнере выполнил плановый сбор (`CIDR data saved to /data/data.json`).
|
||||
- `docker compose stop collector`: штатное завершение за 1 с, через ~2 минуты `/health` = `degraded`, `collector_alive: false`.
|
||||
- После проверки удалены тестовые контейнеры, том и образы.
|
||||
|
||||
## Замечания
|
||||
- Порт публикуется наружу без TLS (решение пользователя): токен идёт открытым текстом.
|
||||
- Файлы `config.json` и `status.json`, записанные сервисами, получают режим 600 (создаются через `mkstemp`); оба сервиса работают под одним пользователем, но сторонние читатели тома этих файлов не прочитают.
|
||||
- Зависимости в образе не закреплены (риск воспроизводимости сборки).
|
||||
- Строка `google.com` в текущих данных не входит в конфигурацию и будет стареть по TTL (актуально и при миграции в том).
|
||||
- Для миграции нужно знать имя тома `<проект>_ripe_data` (`docker volume ls`), в README это описано.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Итоги: управление ASN и FQDN через API
|
||||
|
||||
План: `docs/plan-asn-fqdn-api.md`.
|
||||
|
||||
## Сделано
|
||||
- **Эндпоинты** (`api_server.py`): `GET /asns`, `GET /fqdns` (открыты), `POST /asns`, `POST /fqdns` (201 при добавлении, 200 если уже есть), `DELETE /asns/{asn}`, `DELETE /fqdns/{fqdn}` (404 для неизвестного, `?purge=true`). Изменения требуют `X-API-Key`.
|
||||
- **Валидация**: ASN `1..4294967295`; FQDN нормализуется (нижний регистр, без точки в конце), проверяются длина и метки, IP-литералы отклоняются (422).
|
||||
- **Запись конфига** (`cidr_collector.py`): единый `update_config` (блокировка + атомарная запись) и `add_to_config_list`/`remove_from_config_list`. Через них работают API, `POST /schedule` и CLI (`add`, `remove`, `add-fqdn`, `remove-fqdn`); прежнее чтение-изменение-запись без блокировки в CLI закрыто.
|
||||
- **Данные удалённого источника**: по умолчанию остаются и истекают по `ttl_days` (`sweep_unconfigured` при сборе, пустая запись удаляется); `purge=true` удаляет сразу (`purge_entry`, под блокировкой данных, без вложенных блокировок).
|
||||
- **Гонка сбора и удаления**: конфиг перечитывается под блокировкой данных, удалённый во время сбора источник не воскресает.
|
||||
- **README**: описание эндпоинтов и поведения данных. **Тесты**: `tests/test_sources_api.py` (2 теста), всего 13, в контейнере 13 passed.
|
||||
|
||||
## Проверка (два сценария на копии данных)
|
||||
- `DELETE /asns/44907?purge=true`: адреса ASN пропали из `/addresses` (24 -> 21); без ключа 401.
|
||||
- `POST /asns` вернул источник (201), `POST /fqdns` добавил `telegram.org` (нормализован), `8.8.8.8` отклонён.
|
||||
- Сбор подхватил новые источники (`AS44907: +3`, `telegram.org: +2 IPs`).
|
||||
- CLI `add`/`remove`/`list` работают через новый общий путь записи.
|
||||
|
||||
## Замечания
|
||||
- **В реальных данных есть запись `google.com`, которой нет в конфигурации** (осталась от первоначальной настройки). Теперь она стареет: после миграции адреса получат `last_seen` = момент первого сбора и удалятся через 90 дней. Если она нужна, добавьте её: `POST /fqdns {"fqdn": "google.com"}`. Реальные файлы данных в ходе проверки не менялись.
|
||||
- Сбор для нового источника происходит на ближайшем запуске по расписанию; немедленный сбор по запросу (`POST /collect`) не реализован.
|
||||
- Изменять списки может любой владелец токена; аудита изменений (кто и когда) нет, только запись в лог.
|
||||
- При `ttl_days = 0` данные удалённого источника не истекают, нужен `purge=true`.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Итоги: POST /collect (немедленный сбор)
|
||||
|
||||
План: `docs/plan-collect-endpoint.md`. Результат анализа сохранён отдельно: `docs/analysis-2026-09-21.md`.
|
||||
|
||||
## Сделано
|
||||
- **`POST /collect`** (`api_server.py`, токен `X-API-Key`): тело необязательно (`type`: `asn`/`fqdn`/`all`, по умолчанию `all`), ответ `202`. Если демон не жив (нет свежего heartbeat), ответ `503` и запрос не ставится в очередь.
|
||||
- **Передача запроса демону** (`cidr_collector.py`): `request_collection`/`pop_collection_requests` через файл `collect_request.json` в `DATA_DIR` (атомарная запись под блокировкой); повторные запросы объединяются.
|
||||
- **Демон** (`collector_daemon.py`): задание `check_collect_requests` каждые 5 с ставит разовые задания `<type>_manual`; на каждый тип один запуск за раз (наложение планового и ручного сбора пропускается с записью в лог); в `status.json` добавлены `running` и `last_finished`; служебные запуски опроса скрыты из лога.
|
||||
- **`/health`**: общий помощник `collector_state()` (используется и `/collect`), в заданиях новые поля.
|
||||
- **README**: описание эндпоинта, `/health`, логика планировщика; `collect_request.json` в `.gitignore`/`.dockerignore`.
|
||||
- **Тесты**: 2 новых (API `/collect`; демон ставит задания и пропускает наложение). Всего 16, в контейнере 16 passed.
|
||||
|
||||
## Проверка
|
||||
- Отдельные процессы на копии данных (расписание `0 4 * * *`, чтобы плановых запусков не было): без демона `503`; `POST /collect {"type":"asn"}` запустил сбор через ~1 с (24 -> 42 CIDR), `/health` показал `running: true`, затем `last_finished`; два быстрых запроса дали один запуск; запрос без тела запустил `asn` и `fqdn`.
|
||||
- Docker Compose (API и демон в разных контейнерах, общий том): запрос принят, сбор выполнен, данные сохранены в `/data/ripe.db`, оба сервиса `healthy`; стенд убран.
|
||||
|
||||
## Замечания
|
||||
- Сбор стартует не мгновенно, а в пределах 5 секунд (интервал опроса демона).
|
||||
- Любой владелец токена может запускать сбор без ограничения частоты; повторные запросы во время идущего сбора пропускаются, но защиты от частых запросов к RIPE нет.
|
||||
- Ручной сбор не сбрасывает и не сдвигает расписание.
|
||||
- `POST /collect` не ждёт результата: итог виден в `/health` (`last_error` показывает только исключение сбора, но не сбои отдельных источников - это остаётся в списке улучшений).
|
||||
@@ -0,0 +1,24 @@
|
||||
# Итоги: `GET /addresses/diff?since=`
|
||||
|
||||
План: `docs/plan-diff-endpoint.md`.
|
||||
|
||||
## Сделано
|
||||
- **Журнал изменений** (`db.py`): таблицы `changes` и `meta`, схема версии 2. Запись ведут SQL-триггеры на `addresses`, поэтому охвачены сбор, TTL, `purge` и снятие источника. Значение попадает в журнал, только если оно появилось или исчезло в итоговом наборе (адрес, который держит другой источник, не считается удалённым). Импорт из JSON в журнал не пишется. База версии 1 обновляется автоматически при первом открытии.
|
||||
- **`get_changes()`**: в одной read-транзакции берёт первое действие по значению после точки отсчёта и сверяет с текущим состоянием, поэтому удалённое и возвращённое в тот же интервал в результат не попадает.
|
||||
- **Срок хранения** (`cidr_collector.py`): `changes_retention_days` (по умолчанию 30, `0` - без очистки), очистка внутри транзакции обоих сборов; граница («горизонт») хранится в `meta`.
|
||||
- **API** (`api_server.py`): `GET /addresses/diff?since=<курсор | время>&type=&ip_version=` с ответом `{since, now, cursor, added, removed}`; `400` при неверном `since`, `410` при выходе за горизонт или чужом курсоре. В ответы `/addresses` добавлен заголовок `X-Changes-Cursor` (курсор читается до данных) для первой синхронизации.
|
||||
- **README**: описание эндпоинта, ключа `changes_retention_days`, журнала в разделе о хранении.
|
||||
- **Тесты**: 2 новых (журнал и очистка в БД; API), всего 18, в контейнере 18 passed.
|
||||
|
||||
## Проверка
|
||||
Вручную на копии базы версии 1: миграция сохранила данные, в журнале после неё пусто; при сборе истёкший по TTL префикс попал в `removed`, новый - в `added`; время с часовым поясом (`+03:00`) разобрано.
|
||||
|
||||
## Отличия от плана
|
||||
- Курсор рекомендован вместо времени: номер записи не зависит от часов и от длительности транзакции сборщика (иначе запись, зафиксированная позже, могла бы получить метку времени раньше выданного `now` и потеряться).
|
||||
- Добавлен заголовок `X-Changes-Cursor` (в плане не было), чтобы клиент мог согласованно начать синхронизацию.
|
||||
|
||||
## Замечания
|
||||
- Diff считается отдельно по типам (`asn`, `fqdn`); одинаковая строка в обоих типах при `type=all` сообщалась бы дважды - на практике не встречается (CIDR и IP различаются записью).
|
||||
- Только JSON и без агрегации: агрегированный diff не аддитивен.
|
||||
- Журнал растёт с числом изменений; при 30 днях хранения и редких изменениях RIPE объём мал. На реальных данных не проверялось (наблюдение 1 анализа остаётся в силе).
|
||||
- Уведомления (webhook, Telegram) не сделаны: следующий шаг поверх журнала.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Итоги: репозиторий git (п. 1.1)
|
||||
|
||||
План: `docs/plan-git-init.md`.
|
||||
|
||||
## Сделано
|
||||
- `git init`, ветка `main`, remote `origin` = `https://artstore.rxmsk.ru/ayurishchev/ripe-cidr-collector.git`.
|
||||
- Первый коммит: 44 файла (код, тесты, Docker-файлы, `config.json`, `.env.example`, `docs/`, `README.md`, `.claude/CLAUDE.md`). Автор - существующая глобальная настройка git, настройки не менялись.
|
||||
- `.gitignore`: добавлены `data.json` и `fqdn_data.json` (боевые данные в старом формате; в `.dockerignore` они уже были).
|
||||
- Проверка индекса: баз, `.env`, `venv`, `graphify-out`, кэшей и данных нет; поиск секретов (ключи, токены, пароли со значениями) ничего не нашёл.
|
||||
- README: пункт о репозитории (что входит и не входит).
|
||||
|
||||
## Не сделано
|
||||
- **`git push` не выполнен:** отправка кода на сервер ждёт подтверждения пользователя.
|
||||
|
||||
## Замечания
|
||||
- `config.json` в репозитории содержит рабочий список источников (ASN и FQDN): для внутреннего репозитория допустимо, для публичного стоит заменить примером.
|
||||
- Код не менялся, тесты не запускались повторно.
|
||||
- Хук обновления графа после коммита не ставился (отдельное решение, п. 1 плана допускает после git).
|
||||
@@ -0,0 +1,23 @@
|
||||
# Итоги: форматы вывода, агрегация CIDR, ip_version
|
||||
|
||||
План: `docs/plan-output-formats.md`.
|
||||
|
||||
## Сделано
|
||||
- **`formatters.py` (новый)**: фильтр по версии IP, агрегация (`ipaddress.collapse_addresses`, отдельно v4/v6, после объединения источников), рендер `nftables`, `mikrotik`, `bird`, `frr`. Все скрипты идемпотентны (замена списка при повторном применении).
|
||||
- **`GET /addresses`**: новые параметры `format`, `ip_version`, `aggregate`, `name` (валидация имени защищает от подстановки в конфиг, ошибки -> 422). Ответ по умолчанию не изменился.
|
||||
- **README**: таблица параметров, форматы и способы применения.
|
||||
- **Тесты**: `tests/test_formats.py` (3 теста, параметризованный на 4 формата). Всего 9 тестов, все в контейнере: 9 passed.
|
||||
|
||||
## Проверка
|
||||
- На реальных данных ответы `type=all|cidr|fqdn` без параметров побайтно совпадают с прежними.
|
||||
- Агрегация: 26 -> 14 записей; `ip_version=6` отдаёт только IPv6.
|
||||
- Генерируемые конфиги: BIRD (`bird -p`, Debian 12) - OK; nftables (`nft -c`, Alpine) - OK.
|
||||
- В процессе проверки найдена и исправлена ошибка: BIRD не принимает запятую после последнего элемента `[...]` (теперь запятые только между элементами).
|
||||
|
||||
## Не проверено
|
||||
- MikroTik и FRR не проверялись на реальном ПО (эмулятора нет), синтаксис сверен по документации. В FRR нужно убедиться, что `no ip prefix-list <name>` для ещё не существующего списка не даёт ошибку при `vtysh -f`; если даёт - убрать эту строку или применять с `-m`.
|
||||
- Нет тестов на очень большие списки (десятки тысяч записей).
|
||||
|
||||
## Замечания
|
||||
- Формат `json` с `aggregate=true` печатает `/32` и `/128` как «голый» IP.
|
||||
- `nftables`: имя таблицы совпадает с `name`; свои правила пользователь добавляет в эту же таблицу (или создаёт набор в своей, изменив `name`).
|
||||
@@ -0,0 +1,24 @@
|
||||
# Итоги: разделение сборщика и API на два процесса
|
||||
|
||||
План: `docs/plan-process-split.md`. Часть B (управление ASN/FQDN через API) не реализована, план: `docs/plan-asn-fqdn-api.md`.
|
||||
|
||||
## Сделано
|
||||
- **`collector_daemon.py` (новый)**: отдельный процесс с `BlockingScheduler`. Расписание берёт из `config.json`, раз в 30 с сверяет его и перепланирует задания без перезапуска (невалидный cron игнорируется). Пишет `status.json` (состояние заданий и heartbeat). Один экземпляр гарантируется блокировкой `collector.daemon.lock`. По `SIGTERM` завершается штатно после текущего сбора.
|
||||
- **`api_server.py`**: планировщик, `lifespan` и состояние заданий удалены. `POST /schedule` только валидирует cron и пишет конфиг (в ответе `applied_within_seconds`). `GET /health` читает `status.json`: `collector_alive` (heartbeat моложе 120 с), задания, счётчики; без демона `status = degraded`.
|
||||
- **`storage.py`**: `try_lock` (неблокирующая блокировка для singleton). **`cidr_collector.py`**: `STATUS_FILE`, `SYNC_INTERVAL`, `STATUS_STALE_AFTER`.
|
||||
- **README**: два сервиса (systemd и OpenRC), примечание об обновлении, переписаны «Scheduler Logic» и описание `/health`; cron оставлен как альтернатива демону. `status.json` добавлен в `.gitignore` и `.dockerignore`.
|
||||
- **Тесты**: `tests/test_daemon.py` (2 теста: перепланирование и heartbeat в `/health`). Всего 11, в контейнере: 11 passed.
|
||||
|
||||
## Проверка (два реальных процесса на копии данных)
|
||||
- Сбор идёт демоном без участия API; `/health`: `ok`, `collector_alive: true`.
|
||||
- Второй запуск демона завершается с ошибкой singleton (exit 1).
|
||||
- `POST /schedule` (`*/1` -> `*/2`): демон перепланировал задание через 19 с, `/health` показал новый cron.
|
||||
- Перезапуск API не повлиял на демон.
|
||||
- `SIGTERM`: демон остановился штатно.
|
||||
- Через ~130 с после остановки: `status = degraded`, `collector_alive = false`.
|
||||
|
||||
## Замечания
|
||||
- **Обязательное действие при обновлении:** нужно запустить сервис `ripe-collector`, одного `ripe-api` больше недостаточно.
|
||||
- Изменение расписания применяется с задержкой до 30 с.
|
||||
- Демон пишет `status.json` каждые 30 с (маленький файл, атомарная запись).
|
||||
- Реальные `data.json`, `fqdn_data.json`, `config.json` не менялись.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Итоги: надёжность и безопасность
|
||||
|
||||
План: `docs/plan-reliability-security.md`.
|
||||
|
||||
## Что сделано
|
||||
- **`storage.py` (новый)**: атомарная запись (temp + fsync + `os.replace`), межпроцессная блокировка `flock`, безопасное чтение: битый JSON переименовывается в `*.corrupt-<ts>`, вызывается `StorageError` (раньше молча возвращался `{}` и затирал данные).
|
||||
- **`cidr_collector.py`**: TTL записей (`ttl_days`, по умолчанию 90, `0` = бессрочно) на основе `first_seen`/`last_seen`; при сбое RIPE/DNS ничего не удаляется; авто-миграция старых данных (TTL идёт с момента миграции); `logging` вместо `print`; сетевые запросы вне блокировки; пути к файлам теперь относительно каталога скрипта (cron больше не зависит от cwd).
|
||||
- **`api_server.py`**: `POST /schedule` требует `X-API-Key` (токен из `RIPE_API_TOKEN`, без токена - 503, сравнение через `compare_digest`); pydantic-модель тела; атомарная запись конфига под блокировкой; `lifespan` вместо `on_event`; `GET /health`; `StorageError` -> 503; хост по умолчанию для `__main__` - `127.0.0.1`.
|
||||
- **Документация/окружение**: README (токен, TTL, `/health`, non-root systemd/OpenRC), `.gitignore`, `requirements-dev.txt`, `ttl_days` в `config.json`.
|
||||
- **Тесты (3)**: `tests/test_core.py` - TTL и безопасность при сбое, битый JSON, авторизация POST.
|
||||
|
||||
## Проверка
|
||||
- `pytest -q`: 3 passed.
|
||||
- На копии данных: миграция без потерь (список префиксов не уменьшился), повторный запуск не меняет файлы, параллельный запуск двух сборщиков не портит JSON, битый файл -> API 503, сборщик завершается с ошибкой; POST без ключа 401, с ключом 200 и конфиг обновлён; `/health` возвращает статусы.
|
||||
|
||||
## Замечания
|
||||
- Старый `venv/` был создан на macOS (`/Users/tstark/...`) и на этом сервере не запускался. Он удалён, создан новый Linux `venv/`.
|
||||
- После первого 503 битый файл уже переименован, поэтому следующие запросы вернут пустой список, пока сборщик не создаст данные заново. История накопления в этом случае теряется; сам битый файл сохранён как `*.corrupt-*`.
|
||||
- `0.0.0.0` в systemd оставлен (потребители удалённые): доступ к порту 8000 нужно ограничить файрволом.
|
||||
- Реальные `data.json`/`fqdn_data.json` не изменялись; миграция произойдёт при первом запуске сборщика.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Итоги: хранение собранных адресов в SQLite
|
||||
|
||||
План: `docs/plan-sqlite-storage.md`.
|
||||
|
||||
## Сделано
|
||||
- **`db.py` (новый)**: SQLite (`ripe.db`, WAL), таблица `addresses(kind, source, value, first_seen, last_seen)`, `user_version`; `connect`/`session`/`transaction`, `merge_source` (upsert + TTL), `sweep_unconfigured`, `purge_source`, `get_values`, `count_values`.
|
||||
- **Миграция**: при первом открытии базы `data.json` и `fqdn_data.json` импортируются одной транзакцией (`BEGIN IMMEDIATE`, параллельный старт безопасен), `first_seen`/`last_seen` сохраняются (старый формат без них: `last_seen` = момент миграции); оригиналы переименовываются в `*.migrated-<ts>` (резервная копия и путь отката).
|
||||
- **`cidr_collector.py`**: сбор пишет в БД одной транзакцией (конфиг перечитывается внутри неё, удалённый во время сбора источник не воскресает); удалены JSON-хранилище, `merge_entry`, `purge_entry`, файловые блокировки данных. Блокировка конфига (`update_config`) осталась.
|
||||
- **`api_server.py`**: чтение адресов, счётчики `/health` и `purge` работают через БД; `sqlite3.Error` и `StorageError` -> 503.
|
||||
- **Порча базы**: `not a database`/`malformed` -> файл (с `-wal`/`-shm`) переименовывается в `ripe.db.corrupt-<ts>`, `StorageError` (503); `database is locked` не считается порчей и файл не трогает.
|
||||
- **Развёртывание**: `db.py` добавлен в `Dockerfile`; `*.db`, `*.db-wal`, `*.db-shm`, `*.migrated-*` в `.gitignore`/`.dockerignore`; README: раздел 9 (схема, миграция, откат, бэкап), обновлены логика сбора и раздел Docker.
|
||||
- **Тесты**: тесты на JSON переведены на БД, добавлены проверка порчи базы (в существующем тесте) и `tests/test_db.py` (импорт старого/нового формата, идемпотентность). Всего 14, в контейнере 14 passed.
|
||||
|
||||
## Проверка
|
||||
- **Эталон**: на копии реальных данных ответы `/addresses?type=all|cidr|fqdn` после миграции побайтно совпадают со снятыми старой JSON-версией; повторный старт ничего не импортирует.
|
||||
- Сбор: 6 ASN и 3 FQDN обновлены, повторный запуск дубликатов не создаёт (49 = 49 уникальных).
|
||||
- Конкурентность: 60 запросов `/addresses` во время сбора - все 200; 15 прогонов по 5 процессов, одновременно мигрирующих чистую копию, - без ошибок, ровно один импорт.
|
||||
- Порча: мусор в `ripe.db` -> 503, файл убран в `*.corrupt-*`.
|
||||
- Docker Compose (два контейнера, один том): миграция в томе, эталон совпал, демон записывает, API читает, `purge` виден, после `down`/`up` данные на месте; стенд убран.
|
||||
|
||||
## Замечания
|
||||
- После порчи базы следующие запросы вернут пустой список (создаётся новая пустая база), пока сборщик не наполнит её заново; повреждённый файл сохранён как `*.corrupt-*`. Это осталось прежним поведением, автоматического восстановления из копии нет.
|
||||
- Резервное копирование не автоматизировано (в README описана команда `sqlite3 ".backup"`), это можно добавить заданием демона.
|
||||
- Сообщение лога «... data saved» теперь пишется после каждой транзакции, даже если ничего не изменилось.
|
||||
- Первый запуск на реальных данных: запись `google.com` (нет в конфигурации) станет стареть по TTL, её адрес удалится через 90 дней после миграции; `last_seen` старых записей после миграции = момент миграции.
|
||||
- WAL не рекомендуется на сетевых ФС.
|
||||
- Хранение истории изменений и `?since=` в этот шаг не входили; схема (`first_seen`/`last_seen`) для них подготовлена.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Итоги: запуск тестов в контейнере
|
||||
|
||||
План: `docs/plan-tests-in-container.md`.
|
||||
|
||||
## Сделано
|
||||
- `Dockerfile.test` (`python:3.11-slim`): зависимости отдельным слоем, `CMD ["pytest", "-q"]`.
|
||||
- `.dockerignore`: `venv/`, боевые данные, кэши, lock/tmp-файлы.
|
||||
- `README.md`: шаг «Tests» заменён на запуск через `docker build` / `docker run`.
|
||||
|
||||
## Проверка
|
||||
Образ собран, `docker run --rm ripe-collector-test` - 3 passed. В образе нет `data.json` и `fqdn_data.json`, тесты не зависят от боевых данных.
|
||||
|
||||
## Замечания
|
||||
- Локальный `venv/` остаётся для разработки; для тестов он не нужен.
|
||||
- Образ `ripe-collector-test` остался в локальном Docker (`docker rmi ripe-collector-test` для удаления).
|
||||
Reference in new issue
Block a user