admin control features and admin dashboard

This commit is contained in:
ayurishchev committed 2026-08-23 20:39:22 +03:00
1 parent c630f13c57
commit 37910e410b
69 files changed
+4959 -400

No files matched your search

+172 -7
View File
@@ -6,7 +6,10 @@ JSON, базовый префикс прикладных методов — `/ap
> **Важно.** На данный момент API не защищён аутентификацией/авторизацией
> — эндпоинты доступны любому, кто может достучаться до порта control-api
> по сети. Для эксплуатации за пределами доверенного сегмента сети
> по сети. Это касается и методов из раздела
> [«Управление очередью и конфигурацией»](#управление-очередью-и-конфигурацией)
> ниже — они меняют, что и как проверяется, без подтверждения личности
> вызывающего. Для эксплуатации за пределами доверенного сегмента сети
> обязательно ограничьте доступ на уровне сети/файрвола (см.
> [SETUP.md](SETUP.md#сетевые-доступы)). Добавление bearer-токена — известное
> направление доработки, в текущей версии не реализовано.
@@ -14,12 +17,17 @@ JSON, базовый префикс прикладных методов — `/ap
Базовый URL в примерах — `http://control-api.internal:8080`, замените на
адрес вашего стенда (см. `server.listen_addr` в конфиге control-api).
> Для работы из браузера вместо `curl` есть `admin-dashboard` — веб-панель,
> дающая графический доступ ко всему административному API ниже, см.
> [DASHBOARD.md](DASHBOARD.md).
## Содержание
- [Общие соглашения](#общие-соглашения)
- [Методы для validator-agent](#методы-для-validator-agent)
- [Методы для prober](#методы-для-prober)
- [Служебные и административные методы](#служебные-и-административные-методы)
- [Управление очередью и конфигурацией](#управление-очередью-и-конфигурацией)
- [Модель состояний и связь методов с ней](#модель-состояний-и-связь-методов-с-ней)
- [Сквозной пример работы (curl)](#сквозной-пример-работы-curl)
@@ -37,9 +45,11 @@ JSON, базовый префикс прикладных методов — `/ap
не удалось распарсить, сервер молча подставит текущее время сервера — не
полагайтесь на это в продакшене, всегда передавайте валидную метку.
- `validator_id` и `site_id` в пути запроса должны совпадать со
значениями, заданными в конфиге control-api (`validators[].validator_id`,
`sites[].site_id`) — иначе методы, требующие существующую сущность,
вернут `404`.
значениями, известными control-api — заданными в `control-api.yaml`
при первом запуске (пустая база) либо созданными позже через
`/api/v1/admin/config/*` (см.
[«Управление очередью и конфигурацией»](#управление-очередью-и-конфигурацией))
— иначе методы, требующие существующую сущность, вернут `404`.
## Методы для validator-agent
@@ -297,6 +307,152 @@ IP на данном проходе". До этого момента control-api
`assigned`, `checking`, `unreachable`) и `CurrentIPID`, если валидатор
сейчас занят.
## Управление очередью и конфигурацией
Методы этого раздела — единственный способ менять состав очереди
(`ip_addresses`), список валидаторов, площадок (`sites`) и целей
проверки (`targets`/`check_types`) **без остановки процесса**: изменения
применяются немедленно и переживают последующий рестарт control-api. Все
тела запросов/ответов — `snake_case` (в отличие от `GET
/admin/status|ips|validators` выше, которые отдают сырые поля Go-структур
в PascalCase — эти два стиля сосуществуют осознанно, см. примечание к
`GET /api/v1/admin/ips/{ip}`).
**Источник истины.** `control-api.yaml` используется только как
одноразовый bootstrap для пустой базы данных: секции `validators`,
`sites`, `targets`, `check_types` читаются из YAML один раз, при самом
первом старте на пустых таблицах. Как только в соответствующей таблице
появилась хотя бы одна строка (через bootstrap либо через методы ниже) —
YAML для этой секции больше не перечитывается ни при одном последующем
рестарте; правки нужно вносить через API. Список IP-адресов
(`ip_addresses` в YAML) — исключение, он остаётся отдельным, всегда
аддитивным путём постановки в очередь при каждом старте (см.
[SETUP.md](SETUP.md#развёртывание-control-api)); он не конфликтует с
`POST /api/v1/admin/ips` ниже.
### `POST /api/v1/admin/ips`
Единая точка для двух задач: добавить новые адреса в очередь **и**
принудительно перепроверить уже завершённые — один и тот же вызов, разница
только в текущем состоянии каждого конкретного адреса. Список
обрабатывается в one transaction, в порядке следования адресов:
- адрес неизвестен control-api → добавляется в очередь как новый
(`queued`);
- адрес сейчас `done`/`failed` → принудительно перезапускается: сбрасывается
результат, `attempt_number` увеличивается, `retry_count` обнуляется,
адрес снова становится `queued`;
- адрес сейчас `queued` (ещё не взят в работу) → только переупорядочивается
под порядок текущего списка, повторно не добавляется;
- адрес сейчас активно проверяется (`assigning_fip` / `awaiting_self_check`
/ `checking` / `aggregating`) → не трогается вообще — нельзя запустить
вторую параллельную проверку одного и того же адреса.
Порядок обработки внутри одного вызова соответствует порядку адресов в
списке; повторная отправка того же списка позже даёт тот же относительный
порядок прогона.
Запрос:
```json
{"addresses": ["203.0.113.10", "203.0.113.11"]}
```
Ответ (`200`):
```json
{
"added": ["203.0.113.11"],
"requeued": ["203.0.113.10"],
"reordered": [],
"skipped_in_progress": []
}
```
`400`, если `addresses` пуст.
### `POST /api/v1/admin/ips/{ip}/cancel`
Принудительно останавливает проверку конкретного адреса, не дожидаясь
`checking_window_seconds` — работает из любого нетерминального состояния,
включая `queued` (в этом случае это просто удаление ещё не начатой
проверки из очереди). Если Floating IP уже привязан — отвязывается
(best-effort, как и при обычном завершении проверки); владеющий валидатор
освобождается. Итог записывается как `overall_result: "cancelled"`
(состояние `failed`).
Ответ: `{"ok": true}`. `404`, если адрес неизвестен. `409`, если адрес уже
в терминальном состоянии (`done`/`failed`/уже отменён) — отменять нечего.
### Валидаторы: `/api/v1/admin/config/validators`
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| GET | `/api/v1/admin/config/validators` | — | `[{"validator_id","os_port_id","state"}]` | |
| POST | `/api/v1/admin/config/validators` | `{"validator_id","os_port_id"}` | `201` | `409`, если `validator_id` уже существует |
| PUT | `/api/v1/admin/config/validators/{id}` | `{"os_port_id"}` | `200` | `404` |
| DELETE | `/api/v1/admin/config/validators/{id}` | — | `200` | `404`; `409`, если валидатор сейчас владеет IP |
### Площадки: `/api/v1/admin/config/sites`
Слотов ровно три (`index` ∈ {1, 2, 3}) — это ограничение схемы БД
(`ip_queue.site{1,2,3}_complete`), а не искусственное. Пустой список слотов
— штатный сценарий, отключающий inbound-проверки целиком (см.
[USAGE.md](USAGE.md#управление-площадками-проберами)).
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| GET | `/api/v1/admin/config/sites` | — | `[{"index","site_id"}]` (до 3 строк) | |
| PUT | `/api/v1/admin/config/sites/{index}` | `{"site_id"}` | `200` | `400`, если `index` не 1..3; `409`, если `site_id` уже занят другим слотом |
| DELETE | `/api/v1/admin/config/sites/{index}` | — | `200` | `404` |
### Группы целей: `/api/v1/admin/config/targets`
Группа целей — именованный список URL/адресов (например `stub-targets: [
"https://hub.docker.com", ...]`), на который затем ссылаются типы
проверок.
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| GET | `/api/v1/admin/config/targets` | — | `[{"name","targets"}]` | |
| PUT | `/api/v1/admin/config/targets/{group}` | `{"targets":[...]}` | `200` | `400`, если список пуст |
| DELETE | `/api/v1/admin/config/targets/{group}` | — | `200` | `404`; `409`, если группа используется каким-то `check_type` |
### Типы проверок: `/api/v1/admin/config/check-types`
Тип проверки (`https`, `icmp`, `ssh`, ...) ссылается на одну или несколько
групп целей по имени; именно развёрнутый список отсюда validator-agent
получает в `check_config` при `GET /api/v1/agents/{id}/assignment`.
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| GET | `/api/v1/admin/config/check-types` | — | `[{"name","enabled","targets"}]` (`targets` — имена групп) | |
| PUT | `/api/v1/admin/config/check-types/{name}` | `{"enabled","targets":["group",...]}` | `200` | `400`, если названа несуществующая группа |
| DELETE | `/api/v1/admin/config/check-types/{name}` | — | `200` | `404` |
### Пример: конфигурация целиком через API, без единой строки в YAML
```bash
BASE=http://127.0.0.1:8080
curl -s -X POST "$BASE/api/v1/admin/config/validators" \
-d '{"validator_id":"validator_01","os_port_id":"port-abc123"}'
curl -s -X PUT "$BASE/api/v1/admin/config/targets/web" \
-d '{"targets":["https://hub.docker.com","https://github.com"]}'
curl -s -X PUT "$BASE/api/v1/admin/config/check-types/https" \
-d '{"enabled":true,"targets":["web"]}'
curl -s -X PUT "$BASE/api/v1/admin/config/sites/1" -d '{"site_id":"site-1"}'
# Поставить адрес в очередь и, отдельным вызовом позже, принудительно
# перепроверить его ещё раз — тот же метод, разница только в состоянии:
curl -s -X POST "$BASE/api/v1/admin/ips" -d '{"addresses":["203.0.113.10"]}'
curl -s -X POST "$BASE/api/v1/admin/ips" -d '{"addresses":["203.0.113.10"]}' # forced recheck
# Остановить проверку, не дожидаясь checking_window_seconds:
curl -s -X POST "$BASE/api/v1/admin/ips/203.0.113.10/cancel"
```
## Модель состояний и связь методов с ней
```
@@ -327,9 +483,18 @@ queued ──(control-api сам, без вызова API)──▶ assigning_fi
нет — это фоновый цикл (`Tick`), а не запрос/ответ.
Площадки (`siteN_complete`) — опциональны: сколько их учитывается,
целиком определяется списком `sites` в конфиге control-api (0–3 записи).
Пустой список — агрегация ждёт только `egress_complete`, ни одна площадка
не требуется. Подробнее — [USAGE.md](USAGE.md#управление-площадками-проберами).
целиком определяется текущим списком `sites` (0–3 записи, управляется
через `/api/v1/admin/config/sites` — см.
[выше](#управление-очередью-и-конфигурацией)). Пустой список — агрегация
ждёт только `egress_complete`, ни одна площадка не требуется. Подробнее —
[USAGE.md](USAGE.md#управление-площадками-проберами).
Два дополнительных перехода, оба инициируются оператором через
`/api/v1/admin/ips`, а не самим оркестратором:
- **любое нетерминальное состояние → `failed` (`overall_result:
"cancelled"`)** — `POST /api/v1/admin/ips/{ip}/cancel`;
- **`done`/`failed` → `queued` (новая попытка)** — `POST
/api/v1/admin/ips` с уже завершённым адресом в списке.
## Сквозной пример работы (curl)
+99
View File
@@ -0,0 +1,99 @@
# Admin Dashboard
`admin-dashboard` — 4-й компонент системы: браузерная веб-панель,
дающая полное покрытие административного API `control-api`
([docs/API.md](API.md#управление-очередью-и-конфигурацией)) без единого
`curl`. Отдельный, полностью самостоятельный процесс — не хранит
состояния, не подключается к базе данных напрямую, общается с
`control-api` только через его же HTTP admin API.
## Устройство
- Рендеринг полностью на сервере: `html/template` + [htmx](https://htmx.org)
(частичные обновления без перезагрузки страницы) +
[Alpine.js](https://alpinejs.dev) (точечная клиентская интерактивность) +
[Pico CSS](https://picocss.com) в classless-сборке (минималистичный вид
на голой семантической разметке). Никакой сборки фронтенда нет — все три
библиотеки вендорены как статические файлы
(`internal/dashboard/static/vendor/`, см. `VENDOR.md` там же) и встроены
в бинарник через `//go:embed`. Дашборд работает полностью офлайн — при
открытии страницы браузер не делает ни одного запроса за пределы самого
дашборда (можно проверить через DevTools → Network).
- Браузер никогда не видит JSON `control-api` напрямую: каждая страница и
каждый htmx-фрагмент — это HTML, отрендеренный Go-хендлером дашборда
после вызова `control-api`. CORS и reverse-proxy не нужны.
- Как и `control-api`, дашборд **не аутентифицирован** — ограничивайте
доступ на уровне сети/файрвола (см.
[SETUP.md](SETUP.md#сетевые-доступы)).
## Запуск
```bash
cp configs/admin-dashboard.example.yaml /etc/cloud-ip-validator/admin-dashboard.yaml
# отредактируйте control_api.base_url под ваш стенд
admin-dashboard -config /etc/cloud-ip-validator/admin-dashboard.yaml
```
Развёртывание как systemd-юнита — по образцу остальных компонентов, см.
[SETUP.md](SETUP.md#развёртывание-admin-dashboard). Порт по умолчанию —
`:8090` (у `control-api` — `:8080`).
## Страницы и что на них можно делать
| Страница | Назначение |
|---|---|
| `/overview` | Сводная статистика: счётчики по состояниям, «текущая проверка» (live-снимок всех IP не в терминальном состоянии) и «последние N завершённых» (по умолчанию 20, `overview.last_completed_count`) с разбивкой pass/partial/fail/cancelled. Обновляется каждые `overview.poll_interval_seconds` секунд без перезагрузки страницы. |
| `/ips` | Полная очередь. Форма сверху принимает список адресов (по одному на строке или через запятую) и отправляет их в `POST /api/v1/admin/ips` — **один и тот же вызов** добавляет новые адреса и принудительно перезапускает уже завершённые (см. ниже). У каждого адреса — кнопка «Перепроверить» (для `done`/`failed`) или «Отменить» (для активных состояний). |
| `/ips/{ip}` | Детали одного адреса: все проверки текущей попытки и вся история событий. |
| `/validators` | Список валидаторов + создание/изменение `os_port_id`/удаление. |
| `/sites` | Три фиксированных слота площадок (1/2/3) — назначить/сменить/освободить `site_id`. |
| `/targets` | Группы целей для egress-проверок — создание/редактирование/удаление. |
| `/check-types` | Типы проверок (`https`/`icmp`/`ssh`/...), включение/выключение, привязка к группам целей. |
### «Текущая» и «последняя завершённая» проверка
В `control-api` нет понятия «запуска»/«цикла проверки» как отдельной
сущности — есть только общая очередь IP-адресов
(`docs/PLAN_ADMIN_DASHBOARD.md`). Дашборд ничего не меняет в этом
устройстве и не заводит своего состояния:
- **Текущая проверка** — все адреса, которые прямо сейчас не в
состоянии `done`/`failed` (`queued`, `assigning_fip`,
`awaiting_self_check`, `checking`, `aggregating`), вычисляется заново на
каждый запрос из `GET /api/v1/admin/status` + `GET /api/v1/admin/ips`.
- **Последняя завершённая проверка** — последние N адресов, перешедших в
`done`/`failed`, отсортированные по `AggregatedAt` по убыванию (не
«последний запуск», а именно скользящее окно последних по времени
завершений).
### Добавление адресов и принудительный повтор — один и тот же вызов
Форма на `/ips` всегда бьёт в `POST /api/v1/admin/ips`. Поведение зависит
от текущего состояния каждого конкретного адреса (см.
[docs/API.md](API.md#post-apiv1adminips)): новый — встаёт в очередь;
уже `done`/`failed` — принудительно перезапускается; уже `queued` —
просто переупорядочивается; уже активно проверяется — не трогается
(дашборд честно показывает это в таблице, а не делает вид, что запрос
ничего не значил).
## Конфигурация
См. `configs/admin-dashboard.example.yaml`. Ключевые поля:
- `server.listen_addr` — где слушает сам дашборд (по умолчанию `:8090`).
- `control_api.base_url` — адрес `control-api`, обязателен.
- `control_api.timeout_seconds` — таймаут HTTP-запросов к `control-api`.
- `overview.last_completed_count` — размер окна «последних завершённых»
на странице обзора.
- `overview.poll_interval_seconds` — как часто браузер опрашивает
`/overview/fragment` для live-обновления.
## Отображение ошибок
Любая ошибка `control-api` (4xx/5xx с телом `{"error":"..."}`) или сбой
связи с ним (недоступен, таймаут) показывается баннером наверху страницы,
а не приводит к падению дашборда — таблица/страница при этом всегда
отражает актуальное состояние `control-api` (дашборд перезапрашивает
данные после любой попытки мутации, независимо от её исхода). Жёлтый
баннер — бизнес-ошибка (4xx, например «валидатор занят»), красный —
инфраструктурная проблема (5xx или `control-api` недоступен).
+17 -10
View File
@@ -32,15 +32,15 @@ control-api, фоновый оркестратор, база данных, вы
```mermaid
flowchart TB
subgraph OP["Оператор"]
CFG["control-api.yaml<br/>(validators, sites, targets,<br/>ip_addresses, check_types)"]
CFG["control-api.yaml<br/>(bootstrap пустой БД:<br/>validators, sites, targets,<br/>check_types, ip_addresses)"]
ENV["control-api.env<br/>(OS_AUTH_URL, OS_TOKEN, ...)"]
ADMIN["curl /api/v1/admin/*"]
ADMIN["curl /api/v1/admin/*<br/>(status/ips/validators,<br/>ips submit/cancel,<br/>config CRUD)"]
end
subgraph CAPI["control-api (управляющая машина, 1 экземпляр)"]
HTTP["HTTP API<br/>/api/v1/agents/*<br/>/api/v1/probers/*<br/>/api/v1/admin/*<br/>/healthz"]
ORCH["Оркестратор: Tick раз в<br/>poll_interval_seconds<br/>claim → associate FIP →<br/>ожидание self-check →<br/>checking → aggregate → release<br/>+ lease sweep + heartbeat sweep"]
DB[("SQLite<br/>validators / ip_queue<br/>checks / events")]
DB[("SQLite<br/>validators / ip_queue / sites /<br/>target_groups / check_types /<br/>checks / events")]
OSCLIENT["OpenStack-клиент<br/>(mode: mock | real)"]
end
@@ -49,9 +49,10 @@ flowchart TB
VA["validator-agent ×N<br/>(на каждой ВМ-валидаторе)"]
PR["prober ×3<br/>(на каждой внешней площадке)"]
CFG -->|"читается при старте<br/>(инициализация validators, ip_queue)"| CAPI
CFG -->|"читается только один раз,<br/>на пустых таблицах (bootstrap)"| DB
ENV -->|"переменные окружения процесса"| OSCLIENT
ADMIN --> HTTP
HTTP -->|"config/queue CRUD:<br/>источник истины после<br/>первого изменения"| DB
HTTP --> ORCH
ORCH <--> DB
ORCH --> OSCLIENT
@@ -63,14 +64,20 @@ flowchart TB
**Пояснение.** `control-api` — единственный компонент с состоянием и
единственная точка принятия решений (какой IP кому назначить, когда
считать проверку завершённой). Конфигурация читается один раз при
старте процесса (горячей перезагрузки нет — изменения требуют
`systemctl restart control-api`, см. [SETUP.md](SETUP.md)). Оркестратор
считать проверку завершённой). `control-api.yaml` используется только как
одноразовый bootstrap для четырёх секций (`validators`, `sites`,
`targets`, `check_types`) — читается лишь пока соответствующая таблица в
БД пуста; `ip_addresses` — отдельный, всегда аддитивный путь постановки в
очередь при каждом старте. После bootstrap все изменения этих сущностей,
включая состав очереди и принудительные повтор/остановку проверки, идут
через `/api/v1/admin/*` — «на лету», без `systemctl restart control-api`
(см. [API.md](API.md#управление-очередью-и-конфигурацией)). Оркестратор
работает по таймеру независимо от HTTP-запросов — назначение IP
валидаторам и агрегация результатов не привязаны к конкретному входящему
запросу, а выполняются фоновым циклом `Tick`. `validator-agent` и
`prober` — активная сторона: они сами инициируют все HTTP-запросы к
control-api (pull-модель), сам control-api к ним не обращается.
запросу, читая актуальную конфигурацию из БД на каждом проходе, а не
единожды при старте. `validator-agent` и `prober` — активная сторона: они
сами инициируют все HTTP-запросы к control-api (pull-модель), сам
control-api к ним не обращается.
---
+154
View File
@@ -0,0 +1,154 @@
# План: `cmd/admin-dashboard` — веб-панель администратора
> Статус: **реализовано**. Актуальное описание — [docs/DASHBOARD.md](DASHBOARD.md).
## Context
Единственный способ управлять `control-api` (очередь IP, валидаторы,
площадки, цели проверки) — HTTP API через `curl` (см. `docs/API.md`). API
уже покрывает весь необходимый функционал (`POST /api/v1/admin/ips` для
постановки/принудительного повтора, `POST /api/v1/admin/ips/{ip}/cancel`
для остановки, `/api/v1/admin/config/{validators,sites,targets,check-types}`
для CRUD), но curl неудобен для повседневного оперирования и не даёт
наглядной картины состояния очереди. Нужен браузерный admin dashboard —
графический доступ ко всей этой функциональности: сводная статистика
(текущая проверка / итог последней завершённой), управление конфигурацией
и принудительные операции над очередью — без YAML и без перезапуска
`control-api`.
**Согласованные решения:**
- **Без сборки фронтенда.** Server-rendered `html/template` + htmx
(частичные AJAX-обновления) + Alpine.js (точечная клиентская
интерактивность) + Pico.css classless (минимализм на голой семантической
разметке). Все три библиотеки вендорятся как статические файлы и
встраиваются через `//go:embed` — без CDN во время выполнения (принцип
проекта — полностью автономный бинарник, работает офлайн).
- **Никакого нового backend-состояния.** «Текущая проверка» — live-снимок
IP не в терминальном состоянии. «Последняя завершённая» — последние N
(по умолчанию 20, настраивается) по `AggregatedAt` desc среди
`done`/`failed`, с разбивкой по `OverallResult`. Оба вычисляются на
каждый запрос из `GET /admin/status` + `GET /admin/ips` — никакого
понятия «запуска»/«батча» в `control-api` не добавляется.
- **Без аутентификации** — как и сам API; доступ ограничивается сетью/firewall.
- **Отдельный 4-й бинарник** (`cmd/admin-dashboard`), не новые маршруты
внутри `control-api`. Ходит в `control-api` только через существующий
HTTP admin API (`internal/apiclient.Client`). Рендеринг на сервере —
браузер не видит JSON `control-api` напрямую, CORS/reverse-proxy не нужны.
- Малые допущения: без пагинации очереди (текущий масштаб — десятки
адресов); баннер ошибок различает 4xx (жёлтый) и 5xx/транспортные
(красный); порт дашборда по умолчанию `:8090`.
## 1. Структура файлов
```
cmd/admin-dashboard/main.go
internal/dashboard/
server.go, routes.go, client.go, dto.go, render.go, embed.go
handlers_overview.go, handlers_ips.go, handlers_validators.go,
handlers_sites.go, handlers_targets.go, handlers_checktypes.go
templates/ (layout, overview[+fragment], ips[+table], ip_detail,
validators[+table+row], sites[+table], targets[+table+row],
checktypes[+table+row], error_banner)
static/vendor/{htmx.min.js,alpine.min.js,pico.classless.min.css,VENDOR.md}
static/dashboard.css
configs/admin-dashboard.example.yaml
deploy/systemd/admin-dashboard.service
docs/DASHBOARD.md
```
Таблица `ips` перерисовывается целиком при любой мутации (`POST
/admin/ips` может завести новые строки и поменять `sequence`).
`validators`/`sites`/`targets`/`check-types` — точечный swap одной `<tr>`.
## 2. Маршруты дашборда
| Метод | Путь | Вызов к control-api |
|---|---|---|
| GET | `/` | редирект на `/overview` |
| GET | `/overview`, `/overview/fragment` | `GET /admin/status`, `GET /admin/ips` |
| GET | `/ips`, `/ips/{ip}` | `GET /admin/ips`, `GET /admin/ips/{ip}` |
| POST | `/ips` | `POST /admin/ips` |
| POST | `/ips/{ip}/recheck` | `POST /admin/ips` `{"addresses":[ip]}` |
| POST | `/ips/{ip}/cancel` | `POST /admin/ips/{ip}/cancel` |
| GET/POST `/validators`, PUT/DELETE `/validators/{id}` | `.../config/validators[/{id}]` |
| GET `/sites`, PUT/DELETE `/sites/{index}` | `.../config/sites[/{index}]` |
| GET/POST `/targets`, PUT/DELETE `/targets/{group}` | `.../config/targets[/{group}]` |
| GET/POST `/check-types`, PUT/DELETE `/check-types/{name}` | `.../config/check-types[/{name}]` |
| GET | `/static/*` | embed.FS |
`overview/fragment` — `hx-trigger="every Ns"` (из конфига), без фонового
тикера на сервере. `POST /targets`/`/check-types` — перевод «форма с
именем» → `PUT .../{name}` (control-api там upsert).
## 3. Ошибки control-api
`client.go`: не-2xx → `*apiErr{Status, Message}`. Хендлеры не отдают 500 —
рендерят страницу/фрагмент + out-of-band `error_banner.html`
(`hx-swap-oob="true"`, `id="error-banner"` в `layout.html`); статус ответа
дашборда = статус control-api (502 при транспортной ошибке). Баннер жёлтый
для 4xx, красный для 5xx/транспортных.
## 4. Конфигурация
`internal/config/config.go`, секция `AdminDashboard{Server, ControlAPI{
BaseURL, TimeoutSeconds}, Overview{LastCompletedCount, PollIntervalSeconds}}`
+ `LoadAdminDashboard` (defaults: `:8090`, timeout 10s, N=20, poll=5s;
`BaseURL` обязателен). `configs/admin-dashboard.example.yaml`,
`deploy/systemd/admin-dashboard.service` (без CAP_NET_RAW/EnvironmentFile).
## 5. DTO и клиент
`internal/dashboard/dto.go` — свои wire-структуры (не импортируют
приватные DTO `internal/httpapi`, тот же паттерн, что `internal/probercore`):
snake_case-структуры дословно повторяют `internal/httpapi/dto_admin.go`;
для `GET /admin/ips[/{ip}]`/`GET /admin/validators` — зеркала untagged
PascalCase `db.IPQueueItem`/`db.Validator`/`db.Check`/`db.Event`.
`client.go` — обёртка над `apiclient.Client`: `Status`, `ListIPs`, `GetIP`,
`SubmitIPs`, `CancelIP`, CRUD-методы для validators/sites/target-groups/
check-types.
`handlers_overview.go` — чистые функции: `currentlyChecking`,
`lastCompleted(items, n)`, `resultBreakdown`.
## 6. Вендоринг статики
Скачать один раз, закоммитить, задокументировать в `VENDOR.md`:
`htmx.min.js` (unpkg htmx.org, ядро без расширений), `alpine.min.js`
(unpkg alpinejs `dist/cdn.min.js`, IIFE-сборка), `pico.classless.min.css`
(unpkg @picocss/pico).
## 7. Тестирование
`httptest`-фейковый control-api + `httptest`-дашборд поверх него, проверка
рендера через `strings.Contains` (по образцу `internal/httpapi/handlers_config_test.go`).
Кейсы: overview live-агрегация и `resultBreakdown`; submit (happy +
пустой список); recheck (done→requeued, checking→skipped, явно показано);
cancel (happy + 409); CRUD-раунд-трип + конфликты (409/400) по всем 4
сущностям; control-api недоступен → баннер + 502.
**Обязательный ручной шаг:** браузерный смоук-тест против
`scripts/run-local-e2e.sh` (или отдельного mock control-api) — все
страницы/формы/действия, live-обновление во время реального прогона,
проверка через DevTools Network отсутствия внешних (CDN) запросов.
## 8. Документация
Новый `docs/DASHBOARD.md`; `README.md` («три компонента» →
«четыре»); `docs/SETUP.md` (компонент + раздел развёртывания + сетевые
доступы); `docs/API.md` (отсылка на дашборд в предупреждении об
открытости API).
## Критичные файлы
`internal/dashboard/{client.go,dto.go,routes.go,server.go,
handlers_overview.go,render.go,embed.go}`, `internal/config/config.go`
(`AdminDashboard`), `cmd/admin-dashboard/main.go`.
## Проверка
1. `go build ./... && go test ./...`
2. `scripts/run-local-e2e.sh` + `admin-dashboard` отдельно против того же control-api
3. Ручной браузерный смоук-тест (обязателен, не пропускается)
+130 -244
View File
@@ -1,53 +1,70 @@
# План доработки: API управления конфигурацией
# План доработки: динамическое управление конфигурацией и очередью через API
> Статус: **план на будущее, не реализовано**. Документ фиксирует
> согласованный дизайн доработки Control API, дающей возможность
> управлять `check_types`, `targets`, `validators` и `sites` через HTTP
> API вместо правки YAML + рестарта. Реализация — отдельная задача.
> Статус: **реализовано**. Документ фиксирует дизайн доработки Control
> API, дающей возможность управлять `validators`, `sites`,
> `check_types`/`targets` и очередью IP-адресов через HTTP API вместо
> правки YAML + рестарта, без перезапуска процесса. Актуальная
> спецификация методов — [docs/API.md](API.md#управление-очередью-и-конфигурацией);
> повседневные сценарии — [docs/USAGE.md](USAGE.md).
## Context
Сейчас `control-api` полностью read-only в части конфигурации: типы
проверок (`check_types`), список целей (`targets`), состав валидаторов
(`validators`) и внешних площадок (`sites`) читаются один раз из
`control-api.yaml` при старте процесса и живут дальше только в памяти
(`Orchestrator.Checks`, `Orchestrator.Sites`) либо (для валидаторов) в
таблице `validators`, куда при каждом рестарте они переупорядочиваются из
YAML. Единственный способ что-то поменять — отредактировать YAML и
выполнить `systemctl restart control-api`. Это задокументированное
ограничение (см. `docs/USAGE.md`).
Сейчас `control-api` в части конфигурации и очереди полностью read-only:
`validators`, `sites`, `check_types`/`targets` читаются один раз из YAML при
старте процесса (`cmd/control-api/main.go`) и живут в памяти
(`Orchestrator.Checks`, `Orchestrator.Sites`) либо переприменяются в БД при
каждом рестарте (`RegisterValidator` upsert). Список адресов на проверку
(`ip_addresses`) добавляется в очередь только при старте, и нет способа
принудительно перепроверить уже завершённый адрес или остановить проверку,
которая уже идёт. Единственный способ что-то поменять — отредактировать
YAML и выполнить `systemctl restart control-api`.
Согласованные решения (зафиксированы для будущей реализации):
- **Область доработки** — ровно четыре сущности: `check_types` (типы
проверок + их привязка к группам целей), `targets` (группы целей),
`validators` (состав ВМ-валидаторов), `sites` (состав из ≤3 внешних
площадок). Очередь `ip_addresses` и тайминги оркестратора
(`orchestrator.*`, `aggregation.*`, `inbound_checks.*`) — вне scope,
остаются YAML-only как сейчас.
- **Источник истины после первого изменения — БД.** YAML используется
только для одноразового bootstrap при пустой базе; после первого
запуска (или после первого API-изменения) YAML для этих 4 секций
больше не перечитывается и не переприменяется при рестартах.
- **Добавляется базовая аутентификация** — bearer-токен администратора,
которым закрывается весь namespace `/api/v1/admin/*` (не только новые
write-методы, но и существующие read-методы `status/ips/validators` —
единая политика для всего admin-namespace проще и логичнее половинчатой
защиты). Протокол `/api/v1/agents/*` и `/api/v1/probers/*`
(agent/prober) — вне scope, остаётся как есть.
Целевой набор фич:
## Важное архитектурное ограничение: площадки жёстко капнуты на 3
1. Администратор передаёт через API список IP-адресов на проверку.
2. Администратор передаёт через API список валидаторов.
3. Администратор передаёт через API список целей (`targets`/`check_types`).
4. Администратор может принудительно инициировать проверку адреса, даже
если она уже была выполнена ранее.
5. Администратор может принудительно остановить идущую проверку.
Схема `ip_queue` хранит завершённость площадок как три отдельные колонки
(`site1_complete`, `site2_complete`, `site3_complete`) — это не список
произвольной длины. Поэтому API для `sites` не может быть обычным
CRUD-списком: это управление максимум тремя пронумерованными слотами
(`index` ∈ {1,2,3}), где `site_id` можно назначить, переименовать или
снять со слота. Это ограничение уже описано в `docs/USAGE.md` и явно
закладывается в дизайн API ниже, а не игнорируется.
Все действия — «на лету», без перезапуска процесса. Источник истины после
первого изменения через API — БД, YAML остаётся только bootstrap для пустой
базы.
## Общий план реализации
**Согласованные решения:**
- **Sites (площадки) включены в объём доработки** — тем же CRUD-подходом,
что и validators/targets/check_types.
- **Аутентификация (admin bearer-токен) НЕ входит в этот план** — API
остаётся открытым, как сейчас. Ограничение доступа — на уровне
сети/firewall (см. `docs/SETUP.md`).
- **Фичи 1 и 4 реализуются одним механизмом**, а не двумя разными
эндпоинтами. Администратор передаёт список IP-адресов в
`POST /api/v1/admin/ips`; для каждого адреса в списке:
- если адрес не встречался раньше — добавляется в очередь как новый;
- если адрес уже в терминальном состоянии (`done`/`failed`) —
принудительно перезапускается на проверку (сброс результата, новая
попытка, `attempt_number` увеличивается, `retry_count` обнуляется);
- если адрес уже в очереди (`queued`) — переупорядочивается под порядок
текущего списка (без дублирования);
- если адрес сейчас активно проверяется (`assigning_fip` /
`awaiting_self_check` / `checking` / `aggregating`) — не трогается
вообще (не создаём вторую параллельную проверку одного и того же
адреса).
### 1. Новая схема БД — `internal/db/migrations/0002_dynamic_config.sql`
Порядок обработки в рамках одного вызова соответствует порядку адресов в
переданном списке — повторная отправка того же списка без изменений даёт
тот же порядок прогона.
## 1. Схема БД — новая миграция + обобщение `migrate()`
`internal/db/db.go: migrate()` сейчас гейтится по `PRAGMA user_version`:
`>=1 → no-op`, иначе применяет единственный embedded `migrations/0001_init.sql`
и ставит `user_version=1`. Обобщается на упорядоченный список миграций
(embed `0002_dynamic_config.sql` вторым файлом), применяются по очереди все
версии выше текущей.
Новый файл `internal/db/migrations/0002_dynamic_config.sql`:
```sql
CREATE TABLE sites (
@@ -73,229 +90,98 @@ CREATE TABLE check_types (
);
```
Список целей внутри группы и список групп внутри типа проверки хранятся
как JSON-массив в TEXT-колонке (тот же паттерн, что уже используется для
`events.payload`) — они всегда читаются/пишутся целиком, отдельная
реляционная таблица тут не нужна (не переусложняем).
`validators` — существующая таблица (`0001_init.sql`), новых колонок не
требует.
`validators` — существующая таблица, новых колонок не требует.
## 2. Типизированные ошибки — `internal/db/errors.go`
`internal/db/db.go`: функцию `migrate()` обобщить со списка из одной
миграции (`version >= 1 → return`) на упорядоченный список
`{version, sql}` и применение всех версий выше текущего
`PRAGMA user_version` — понадобится и для этой, и для будущих миграций.
Сентинелы (`errors.New` + `%w`-обёртка), чтобы `httpapi`-хендлеры маппили
их в HTTP-статусы через `errors.Is`: `ErrNotFound` (404), `ErrConflict`
(409), `ErrBusy` (409, валидатор владеет IP), `ErrInUse` (409, группа
целей используется check_type'ом), `ErrValidation` (400), `ErrInvalidState`
(409, попытка отменить уже завершённую проверку).
### 2. Bootstrap-логика — новый файл `internal/db/bootstrap.go`
## 3. Bootstrap — `internal/db/bootstrap.go`
```go
func (d *DB) BootstrapFromConfig(ctx context.Context, cfg *config.ControlAPI) error
```
Переносит и обобщает то, что сейчас разбросано по
`cmd/control-api/main.go` (`RegisterValidator` в цикле + `SeedQueue`):
Заменяет текущий цикл `RegisterValidator` + `SeedQueue` в
`cmd/control-api/main.go`:
- `ip_addresses` → `SeedQueue` — без изменений (всегда доливает новые
адреса при каждом старте; отдельный YAML-only путь, не путать с runtime
`POST /api/v1/admin/ips`).
- `validators`, `sites`, `target_groups`, `check_types` — применяются
только если соответствующая таблица пуста. Если строки уже есть — YAML
для этой секции игнорируется.
- `ip_addresses` → `SeedQueue` — **без изменений**, как сейчас (всегда
доливает новые адреса, это уже вне scope доработки).
- `validators`, `sites`, `target_groups`, `check_types` — **новая
семантика**: применяется, **только если соответствующая таблица
сейчас пуста** (`SELECT COUNT(*) ... == 0`). Если в таблице уже есть
строки — YAML для этой секции полностью игнорируется, ничего не
трогаем. Это и есть «bootstrap один раз, дальше БД главная».
**Осознанное изменение поведения**: сейчас `RegisterValidator` при каждом
рестарте переприменяет `os_port_id` из YAML поверх БД. После доработки —
только на пустой таблице (иначе API-правки не переживали бы рестарт).
`internal/db` уже не будет зависеть от `internal/orchestrator` — только
новая зависимость `internal/db → internal/config` (обратной зависимости
`config → db` нет, циклов не возникает).
## 4. Запросы к БД
`cmd/control-api/main.go`: заменить текущий цикл `RegisterValidator` +
`SeedQueue` одним вызовом `database.BootstrapFromConfig(ctx, cfg)`. Это
же делает функцию тестируемой напрямую (используется в обновлённых
`orchestrator_test.go`/`httpapi_test.go` вместо ручного построения
`Orchestrator.Checks`/`.Sites`).
- `internal/db/queries_sites.go`: `ListSites`, `UpsertSite(idx, siteID)`,
`DeleteSite(idx)`, `GetSiteIndex(ctx, siteID) (int, error)` (0, если не
найден — не ошибка).
- `internal/db/queries_targetgroups.go`: `ListTargetGroups`,
`UpsertTargetGroup(name, targets)`, `DeleteTargetGroup(name)`
(`ErrInUse`, если ссылается check_type), `GetTargetGroup(name)`.
- `internal/db/queries_checktypes.go`: `ListCheckTypes`,
`ListResolvedCheckTypes` (разворачивает группы в плоский список targets),
`UpsertCheckType(name, enabled, targetGroups)` (`ErrValidation`, если
группа не существует), `DeleteCheckType(name)`.
- `internal/db/queries_validators.go` (дополнить): `AdminCreateValidator`,
`AdminUpdateValidatorPort`, `DeleteValidator` (`ErrBusy`, если владеет
IP).
- `internal/db/queries_ipqueue.go` (дополнить): `SubmitIPs(ctx,
addresses []string) (SubmitIPsResult, error)` — единая транзакция,
реализует правило из Context выше; `CancelIP(ctx, ipID int64) error` —
условный `UPDATE ... WHERE state NOT IN ('done','failed')`,
`ErrInvalidState` при гонке/уже завершённой проверке.
**Важное следствие смены семантики валидаторов**: сейчас при каждом
рестарте `control-api` валидаторы из YAML переприменяются (в частности,
может тихо откатить `os_port_id`, изменённый через API/вручную в БД).
После доработки — только на пустой таблице. Это осознанное поведенческое
изменение, требует апдейта `docs/SETUP.md`/`docs/USAGE.md` (шаг 6 плана).
`ResultCancelled = "cancelled"` добавляется в `internal/db/models.go`.
### 3. Запросы к БД для новых сущностей
## 5. Оркестратор
Новые файлы, по аналогии с существующими `queries_*.go`:
`internal/orchestrator/orchestrator.go`: убрать статические поля
`Checks`/`Sites`, читать динамически из БД (`ListResolvedCheckTypes`,
`ListSites`, `GetSiteIndex`) в `AssignmentForValidator`,
`expectedCheckCount()`, `isReadyToAggregate()`. Новый метод `ForceCancel`
для фичи 5: отвязывает FIP (best-effort), помечает IP `cancelled`,
освобождает валидатора.
- **`internal/db/queries_sites.go`**: `ListSites`, `UpsertSite(idx, siteID)`
(проверяет допустимость `idx` 1..3 и уникальность `site_id` до записи,
чтобы вернуть чистую типизированную ошибку, а не сырую SQL), `DeleteSite(idx)`,
`GetSiteIndex(siteID) (int, error)` — заменяет текущий
`Orchestrator.SiteIndexForID`, который сканирует статический слайс.
- **`internal/db/queries_targetgroups.go`**: `ListTargetGroups`,
`UpsertTargetGroup(name, targets)`, `DeleteTargetGroup(name)` —
**перед удалением проверяет**, что ни один `check_types` не ссылается
на эту группу (иначе `ErrInUse`), `GetTargetGroup(name)`.
- **`internal/db/queries_checktypes.go`**: `ListCheckTypes`,
`ListResolvedCheckTypes` (сразу разворачивает имена групп в плоский
список URL — то, что раньше строил `orchestrator.New()` один раз при
старте), `UpsertCheckType(name, enabled, targetGroups)` (**проверяет**,
что все переданные `targetGroups` существуют — иначе `ErrValidation`),
`DeleteCheckType(name)`.
- **`internal/db/queries_validators.go`** (дополнить существующий файл):
`AdminCreateValidator(id, osPortID)` (409/`ErrConflict`, если уже
есть), `AdminUpdateValidatorPort(id, osPortID)` (404/`ErrNotFound`,
если нет), `DeleteValidator(id)` (409/`ErrBusy`, если
`current_ip_id IS NOT NULL` — валидатор сейчас владеет IP). Не путать
с существующим `RegisterValidator` — тот остаётся as-is и продолжает
использоваться только агентом при самостоятельной регистрации
(`handleAgentRegister`), полей `os_port_id` не трогает при
self-registration (это уже так в текущем коде).
**Принятый компромисс**: если конфигурация меняется API-запросом ровно в
момент агрегации уже идущей проверки, эта попытка агрегирует по текущей
(уже изменённой) конфигурации — деградирует безопасно через
`missing_counts_as_fail`, самоисправляется на следующей попытке.
Новый файл **`internal/db/errors.go`** с типизированными сентинелами
(`ErrNotFound`, `ErrConflict`, `ErrBusy`, `ErrValidation`, через `errors.New`
+ `%w`-обёртку в местах возврата) — чтобы `httpapi`-хендлеры мапили их в
404/409/400 через `errors.Is`, а не всё подряд в 500 (как сейчас местами
получается по умолчанию).
## 6. HTTP API
### 4. Оркестратор — переход на динамическое чтение конфигурации
`internal/orchestrator/orchestrator.go`:
- Убрать поля `Checks []CheckConfig` и `Sites []config.SiteConfig` из
`Orchestrator` (сейчас вычисляются один раз в `New()` и застывают на
весь жизненный цикл процесса — это и есть корень проблемы). `Inbound`
остаётся статическим полем как сейчас (вне scope).
- `AssignmentForValidator` — вместо `return item, o.Checks, nil` вызывает
`o.DB.ListResolvedCheckTypes(ctx)` и возвращает актуальный на данный
момент список.
- `SiteIndexForID` — удаляется, вызовы (`handleProberRegister`,
`handleProberAssignments`, `handleProberResults`) переходят на
`o.DB.GetSiteIndex(ctx, siteID)`.
- `expectedCheckCount()` — читает актуальные `ListResolvedCheckTypes` и
`ListSites` из БД на момент агрегации, а не статические поля.
**Принятый компромисс (осознанно, без over-engineering):** если
`check_types`/`targets`/`sites` меняются API-запросом ровно в момент,
когда чей-то IP уже находится в `checking` (self-check уже пройден,
проверки уже назначены агенту), агрегация этой конкретной попытки
посчитает *текущую* (уже изменённую) конфигурацию, а не ту, что была на
момент выдачи задания. На практике это узкое окно в несколько секунд
между админ-изменением и завершением проверки; деградирует безопасно —
через существующий механизм `missing_counts_as_fail` результат в худшем
случае будет `partial` вместо `pass` для одной попытки, самоисправляется
на следующей (после retry/requeue). Полный snapshot-per-attempt (доп.
колонки в `ip_queue` с зафиксированным ожидаемым числом проверок) —
возможное будущее усиление, не требуется для этой доработки.
### 5. HTTP API
Новый файл **`internal/httpapi/handlers_config.go`** и DTO в
`dto.go`. Все — под префиксом `/api/v1/admin/config/*`, JSON в
snake_case (в отличие от существующих `/admin/status|ips|validators`,
которые отдают сырые Go-поля в PascalCase — для новых, «настоящих»
management-эндпоинтов сразу делаем нормальный контракт, старые не
трогаем, чтобы не ломать уже задокументированное поведение).
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| GET | `/api/v1/admin/config/validators` | — | `[{validator_id, os_port_id, state}]` | |
| POST | `/api/v1/admin/config/validators` | `{validator_id, os_port_id}` | 201 | 409 если уже есть |
| PUT | `/api/v1/admin/config/validators/{id}` | `{os_port_id}` | 200 | 404 |
| DELETE | `/api/v1/admin/config/validators/{id}` | — | 200 | 404, 409 если владеет IP |
| GET | `/api/v1/admin/config/sites` | — | `[{index, site_id}]` (до 3 строк) | |
| PUT | `/api/v1/admin/config/sites/{index}` | `{site_id}` | 200 | 400 если `index` не 1..3, 409 если `site_id` занят другим слотом |
| DELETE | `/api/v1/admin/config/sites/{index}` | — | 200 | 404 |
| GET | `/api/v1/admin/config/targets` | — | `[{name, targets}]` | |
| PUT | `/api/v1/admin/config/targets/{group}` | `{targets:[...]}` | 200 | 400 пустой список |
| DELETE | `/api/v1/admin/config/targets/{group}` | — | 200 | 404, 409 если используется check_type'ом |
| GET | `/api/v1/admin/config/check-types` | — | `[{name, enabled, targets}]` | |
| PUT | `/api/v1/admin/config/check-types/{name}` | `{enabled, targets:[group,...]}` | 200 | 400 если группа не существует |
| DELETE | `/api/v1/admin/config/check-types/{name}` | — | 200 | 404 |
`routes.go`: все существующие и новые `/api/v1/admin/*`-маршруты
оборачиваются `s.requireAdmin(...)`.
### 6. Аутентификация
- `internal/config/config.go`: в `ServerConfig` добавить
`AdminTokenEnv string \`yaml:"admin_token_env"\`` — по аналогии с
`openstack.*_env` полями (в YAML — только *имя* переменной, не сам
токен).
- `internal/httpapi/server.go`: `Server.AdminToken string` +
`func (s *Server) requireAdmin(next http.HandlerFunc) http.HandlerFunc`
— сверяет `Authorization: Bearer <token>` через
`crypto/subtle.ConstantTimeCompare`. Если `s.AdminToken == ""` —
пропускает без проверки (обратная совместимость).
- `cmd/control-api/main.go`: если `cfg.Server.AdminTokenEnv` задан, но
`os.Getenv(...)` пуст — **отказ запуска** с понятной ошибкой
(fail-safe, не запускаемся с «пустым паролем»). Если
`AdminTokenEnv` вообще не задан — запускаемся как сейчас, но пишем
явный `log.Warn` про незащищённый admin API.
- `configs/control-api.example.yaml`, `deploy/systemd/control-api.service`
(добавить пример переменной в `EnvironmentFile`) — обновить.
### 7. Обновление существующих тестов и добавление новых
- `internal/orchestrator/orchestrator_test.go`,
`internal/httpapi/httpapi_test.go`: заменить ручное построение
`cfg.CheckTypes/.Targets/.Sites` + прямые поля `Orchestrator{Checks:...}`
на `db.BootstrapFromConfig(ctx, cfg)` перед `orchestrator.New(...)` —
сами тестовые сценарии (happy path, partial, lease reclaim) не меняются
по сути, меняется только способ засеять конфигурацию.
- Новые unit-тесты: `internal/db/queries_dynconfig_test.go` (CRUD +
граничные случаи: удаление занятого валидатора → `ErrBusy`, удаление
группы целей, на которую ссылается check_type → `ErrInUse`,
upsert check_type с несуществующей группой → `ErrValidation`, upsert
сайта с чужим `site_id` → `ErrConflict`, bootstrap на непустой таблице
→ YAML игнорируется).
- Новый `internal/httpapi/handlers_config_test.go` (или расширение
`httpapi_test.go`): сквозной сценарий — создать валидатора и сайт через
API вместо конфига, убедиться, что IP реально дошёл до `done`; смена
`check_types` между запусками влияет на следующий назначенный IP.
- Обновить `scripts/run-local-e2e.sh` не требуется по сути (bootstrap
из YAML при пустой БД работает как раньше), но стоит добавить один шаг
с `curl -X PUT .../config/check-types/ssh` как живую демонстрацию.
### 8. Документация (после реализации)
- `docs/API.md`: новый раздел «Методы управления конфигурацией» с
таблицей выше + примеры curl (создание валидатора, отключение ssh,
добавление цели, назначение площадки на слот) + раздел про
`Authorization: Bearer`.
- `docs/SETUP.md`: шаг про `server.admin_token_env` в
«Переменные окружения для OpenStack» (переименовать раздел или
добавить рядом «и для admin-токена»); явно описать новую
bootstrap-once семантику `validators`/`sites`/`check_types`/`targets`.
- `docs/USAGE.md`: заменить текущие разделы «Управление валидаторами» /
«Управление площадками» (сейчас там «только через YAML + restart») на
актуальные — через API; убрать утверждение «нет API-метода» там, где
оно перестало быть верным.
- `docs/DIAGRAMS.md`: в диаграмму control plane (раздел 1) добавить
новую стрелку «Оператор → HTTP API → БД (config CRUD)» вместо текущей
«CFG → читается при старте (инициализация)» как единственного пути.
`POST /api/v1/admin/ips` — постановка/принудительный перезапуск (фичи 1 и
4). `POST /api/v1/admin/ips/{ip}/cancel` — остановка (фича 5).
`/api/v1/admin/config/{validators,sites,targets,check-types}` — CRUD
(фичи 2 и 3), snake_case DTO. Подробности — `docs/API.md`.
## Критичные файлы
- `internal/db/migrations/0002_dynamic_config.sql` (новый)
- `internal/db/db.go` (обобщить `migrate()`)
- `internal/db/bootstrap.go` (новый)
- `internal/db/errors.go` (новый)
- `internal/db/migrations/0002_dynamic_config.sql`, `internal/db/db.go`,
`internal/db/bootstrap.go`, `internal/db/errors.go`, `internal/db/models.go`
- `internal/db/queries_sites.go`, `queries_targetgroups.go`,
`queries_checktypes.go` (новые), `queries_validators.go` (дополнить)
- `internal/orchestrator/orchestrator.go` (убрать статические
`Checks`/`Sites`, читать из БД)
- `internal/httpapi/handlers_config.go` (новый), `dto.go`, `routes.go`,
`server.go` (`requireAdmin`)
- `internal/config/config.go` (`AdminTokenEnv`)
- `cmd/control-api/main.go` (bootstrap-вызов, проверка токена при старте)
`queries_checktypes.go`, `queries_validators.go`, `queries_ipqueue.go`
- `internal/orchestrator/orchestrator.go`
- `internal/httpapi/handlers_config.go`, `handlers_admin.go`,
`dto_admin.go`, `routes.go`
- `cmd/control-api/main.go`
## Проверка (когда план будет реализовываться)
## Проверка
1. `go build ./... && go test ./...` — все существующие + новые unit- и
httpapi-тесты проходят.
2. `scripts/run-local-e2e.sh` — офлайн-сценарий по-прежнему проходит от
начала до конца без ручного вмешательства (bootstrap из YAML при
пустой БД работает как раньше).
3. Ручная проверка нового контракта: поднять `control-api` с пустой БД и
`admin_token_env` без токена → админ-запрос без заголовка проходит;
задать токен → запрос без `Authorization` получает 401; создать
валидатора/площадку/группу целей/тип проверки через API без
единой строчки в YAML, убедиться, что IP реально проходит полный цикл
проверки на этой конфигурации; попытаться удалить валидатора, пока он
владеет IP → 409; перезапустить `control-api` и убедиться, что
API-изменения пережили рестарт, а YAML их не затёр.
1. `go build ./... && go test ./...`
2. `scripts/run-local-e2e.sh`
3. Ручная проверка: создать validator/site/target-group/check-type только
через API; `POST /admin/ips` с уже `done`-адресом → повторный полный
цикл; `POST /admin/ips` с адресом в `checking` → не трогается;
`POST /admin/ips/{ip}/cancel` во время `checking` → FIP отвязан,
валидатор свободен, `overall_result=cancelled`; удаление занятого
валидатора → 409; рестарт control-api → все API-изменения сохранились.
+75 -15
View File
@@ -21,6 +21,7 @@
- [Развёртывание control-api](#развёртывание-control-api)
- [Развёртывание validator-agent на ВМ-валидаторах](#развёртывание-validator-agent-на-вм-валидаторах)
- [Развёртывание prober на внешних площадках](#развёртывание-prober-на-внешних-площадках)
- [Развёртывание admin-dashboard](#развёртывание-admin-dashboard)
- [Проверка после запуска](#проверка-после-запуска)
- [Сетевые доступы](#сетевые-доступы)
@@ -31,10 +32,13 @@
| `control-api` | Отдельная управляющая машина/ВМ с доступом к OpenStack API | 1 (без HA) |
| `validator-agent` | Каждая ВМ-валидатор в сервисном проекте облака | по числу валидаторов |
| `prober` | По одному на каждой из внешних тестовых площадок | 3 (по числу площадок) |
| `admin-dashboard` | Любая машина с сетевым доступом до `control-api` (опционально) | 0 или 1 |
`control-api` — единственный компонент с состоянием (SQLite). Валидаторы и
проберы не хранят локального состояния и полностью управляются через опрос
control-api (см. [API.md](API.md)).
`control-api` — единственный компонент с состоянием (SQLite). Валидаторы,
проберы и `admin-dashboard` не хранят локального состояния и полностью
управляются через опрос control-api (см. [API.md](API.md),
[DASHBOARD.md](DASHBOARD.md)). `admin-dashboard` не обязателен — вся его
функциональность доступна и через `curl` напрямую по API.
## Требования
@@ -58,7 +62,7 @@ control-api (см. [API.md](API.md)).
### Вариант A: готовые бинарники из репозитория (рекомендуется)
В директории `bin/` репозитория уже лежат три готовых бинарника —
В директории `bin/` репозитория уже лежат четыре готовых бинарника —
собирать их на целевых серверах не нужно, разворачивание сразу
начинается с копирования и запуска (раздел
[«Развёртывание control-api»](#развёртывание-control-api) и далее).
@@ -68,6 +72,7 @@ bin/
├── control-api # ~11 МБ
├── validator-agent # ~7 МБ
├── prober # ~7 МБ
├── admin-dashboard # ~9 МБ (опционален, см. DASHBOARD.md)
└── SHA256SUMS
```
@@ -97,6 +102,7 @@ sha256sum -c bin/SHA256SUMS
scp bin/control-api control-api-host:/tmp/
scp bin/validator-agent validator-host-01:/tmp/
scp bin/prober probe-site-1:/tmp/
scp bin/admin-dashboard dashboard-host:/tmp/ # опционально
```
> Если целевая платформа отличается от linux/amd64 (например, ВМ на
@@ -113,11 +119,13 @@ export CGO_ENABLED=0 GOOS=linux GOARCH=amd64 # поменяйте GOARCH дл
go build -trimpath -ldflags="-s -w" -o bin/control-api ./cmd/control-api
go build -trimpath -ldflags="-s -w" -o bin/validator-agent ./cmd/validator-agent
go build -trimpath -ldflags="-s -w" -o bin/prober ./cmd/prober
go build -trimpath -ldflags="-s -w" -o bin/admin-dashboard ./cmd/admin-dashboard
```
Каждый бинарник самодостаточен — скопируйте нужный файл на
соответствующую машину (control-api → управляющая машина, validator-agent
→ каждый валидатор, prober → каждая площадка).
→ каждый валидатор, prober → каждая площадка, admin-dashboard →
опционально, любая машина с доступом до control-api).
Убедиться, что всё собирается и юнит-тесты проходят:
@@ -130,7 +138,7 @@ go build ./... && go test ./...
обновляются автоматически**:
```bash
sha256sum bin/control-api bin/validator-agent bin/prober | sed 's#bin/##' > bin/SHA256SUMS
sha256sum bin/control-api bin/validator-agent bin/prober bin/admin-dashboard | sed 's#bin/##' > bin/SHA256SUMS
```
## Быстрая проверка без OpenStack (offline-режим)
@@ -181,6 +189,15 @@ cp configs/control-api.example.yaml /etc/cloud-ip-validator/control-api.yaml
целей для egress-проверок (по умолчанию — hub.docker.com, github.com,
packages.ubuntu.com) или включите `ssh` (по умолчанию выключен).
> `validators`, `sites`, `targets` и `check_types` читаются из этого файла
> только один раз — при самом первом старте против пустой базы данных
> (bootstrap). После этого все последующие изменения этих четырёх секций
> вносятся через `/api/v1/admin/config/*`, а не правкой YAML — см.
> [API.md](API.md#управление-очередью-и-конфигурацией) и
> [USAGE.md](USAGE.md#управление-валидаторами). `ip_addresses` — исключение,
> он остаётся YAML + аддитивным добавлением при каждом старте (плюс
> `POST /api/v1/admin/ips` для управления очередью без рестарта).
### 2. Переменные окружения для OpenStack
Учётные данные передаются **только** через переменные окружения — никогда
@@ -276,15 +293,29 @@ systemctl enable --now control-api
**Первичная инициализация базы данных происходит автоматически** — при
первом старте `control-api` создаёт файл SQLite по пути `database.path`
из конфига (миграция схемы применяется один раз, повторные запуски —
no-op). Отдельной команды "init db" не требуется.
из конфига (миграции схемы применяются один раз каждая, повторные запуски
— no-op). Отдельной команды "init db" не требуется.
При каждом старте control-api также:
1. Регистрирует в БД всех валидаторов из `validators` конфига (если их
там ещё нет).
2. Добавляет в очередь все адреса из `ip_addresses`, которых там ещё нет
(уже обработанные ранее адреса повторно не добавляются и не
сбрасываются — см. [USAGE.md](USAGE.md#добавление-новых-ip-в-очередь)).
1. **Bootstrap-once для `validators`/`sites`/`targets`/`check_types`.**
YAML применяется **только если соответствующая таблица в БД сейчас
пуста** — то есть только на самом первом старте против чистой базы.
Как только в таблице появилась хотя бы одна строка (через этот
bootstrap либо через `/api/v1/admin/config/*`, см.
[API.md](API.md#управление-очередью-и-конфигурацией)), YAML для этой
секции больше не перечитывается ни при одном последующем рестарте —
источник истины переключается на БД. Это осознанное отличие от более
ранних версий, где `validators` из YAML переприменялись при каждом
рестарте: теперь правки, сделанные через admin API (например, смена
`os_port_id` валидатора), переживают рестарт вместо того, чтобы
тихо откатываться.
2. **Всегда аддитивно** добавляет в очередь все адреса из
`ip_addresses`, которых там ещё нет (уже обработанные ранее адреса
повторно не добавляются и не сбрасываются — см.
[USAGE.md](USAGE.md#добавление-новых-ip-в-очередь)). Это отдельный,
не завязанный на bootstrap-once путь — не путайте с
`POST /api/v1/admin/ips`, который умеет то же самое (и ещё
принудительный повтор уже проверенных адресов) без перезапуска.
Проверить, что процесс поднялся:
@@ -327,6 +358,28 @@ systemctl enable --now prober
journalctl -u prober -f
```
## Развёртывание admin-dashboard
Опционально — вся его функциональность доступна и через `curl` напрямую
по API (см. [API.md](API.md)). На любой машине с сетевым доступом до
`control-api`:
```bash
cp bin/admin-dashboard /usr/local/bin/admin-dashboard
cp deploy/systemd/admin-dashboard.service /etc/systemd/system/
mkdir -p /etc/cloud-ip-validator
cp configs/admin-dashboard.example.yaml /etc/cloud-ip-validator/admin-dashboard.yaml
# отредактировать control_api.base_url под ваш стенд
systemctl daemon-reload
systemctl enable --now admin-dashboard
journalctl -u admin-dashboard -f
```
Открыть `http://<admin-dashboard>:8090/` в браузере. Подробнее о
страницах и о том, что дашборд может (и не может) — в
[DASHBOARD.md](DASHBOARD.md).
## Проверка после запуска
После того как control-api, все валидаторы и все три пробера запущены:
@@ -376,7 +429,14 @@ curl -s http://<control-api>:8080/api/v1/admin/validators | python3 -m json.tool
очередь — см. [USAGE.md](USAGE.md#частые-проблемы-и-что-с-ними-делать).
- `control-api` → OpenStack Keystone/Neutron API (`OS_AUTH_URL` и далее по
каталогу сервисов).
- `admin-dashboard` → `control-api`: тот же порт (`server.listen_addr`),
адрес задаётся в `control_api.base_url` конфига дашборда.
- Оператор (браузер) → `admin-dashboard`: порт из `server.listen_addr`
дашборда (по умолчанию 8090).
API control-api сейчас не аутентифицирован (см. предупреждение в начале
[API.md](API.md)) — ограничивайте доступ к порту control-api на уровне
сети/firewall теми хостами, где реально работают валидаторы и проберы.
[API.md](API.md)) — то же самое верно и для `admin-dashboard`, который
это API оборачивает. Ограничивайте доступ к обоим портам на уровне
сети/firewall: к control-api — теми хостами, где реально работают
валидаторы, проберы и сам дашборд; к дашборду — теми, кому разрешено
администрировать стенд.
+178 -45
View File
@@ -17,7 +17,9 @@
- [Просмотр деталей и истории по конкретному адресу](#просмотр-деталей-и-истории-по-конкретному-адресу)
- [Управление валидаторами](#управление-валидаторами)
- [Управление площадками (проберами)](#управление-площадками-проберами)
- [Управление целями проверки](#управление-целями-проверки)
- [Повторная проверка адреса](#повторная-проверка-адреса)
- [Принудительная остановка проверки](#принудительная-остановка-проверки)
- [Частые проблемы и что с ними делать](#частые-проблемы-и-что-с-ними-делать)
## Как устроена работа с системой
@@ -43,29 +45,32 @@
## Добавление новых IP в очередь
**В текущей версии добавление адресов происходит только через конфиг
control-api**, отдельного API-метода "добавить IP в очередь" нет.
Основной способ — API, без перезапуска процесса:
1. Добавьте новые адреса в список `ip_addresses` в
`/etc/cloud-ip-validator/control-api.yaml` (в конец списка, либо в
нужном порядке — очередь обрабатывается строго в порядке следования
списка, `sequence`).
2. Перезапустите control-api:
```bash
systemctl restart control-api
```
```bash
curl -s -X POST http://<control-api>:8080/api/v1/admin/ips \
-d '{"addresses": ["203.0.113.10", "203.0.113.11"]}'
```
Это безопасно для уже идущей работы: при старте control-api добавляет в
очередь только **новые** адреса (те, которых там ещё нет) — уже
обработанные ранее адреса не сбрасываются и повторно не проверяются.
Адреса, которые были удалены из `ip_addresses`, но уже есть в базе,
**не удаляются** из очереди/истории автоматически — если конкретный адрес
больше не нужно проверять и его нет в очереди/в процессе, можно просто
оставить как есть (историю он не портит).
```json
{"added": ["203.0.113.10", "203.0.113.11"], "requeued": [], "reordered": [], "skipped_in_progress": []}
```
> Совет: держите `control-api.yaml` под версионным контролем (git) —
> список адресов на проверку тогда одновременно служит и журналом того,
> что вообще когда-либо ставилось в очередь.
Адреса обрабатываются в порядке, в котором перечислены в `addresses` —
именно в этом порядке они и встанут в очередь друг за другом. Метод
идемпотентен относительно уже идущих проверок: адрес, который сейчас
активно проверяется, в ответе окажется в `skipped_in_progress` и не будет
тронут (см. [«Повторная проверка адреса»](#повторная-проверка-адреса)
ниже — тот же метод форсирует перепроверку уже завершённых адресов).
Также по-прежнему можно добавить адреса через `ip_addresses` в
`/etc/cloud-ip-validator/control-api.yaml` и перезапустить control-api —
при каждом старте control-api доливает в очередь только новые адреса из
этого списка (уже обработанные ранее не сбрасываются и повторно не
проверяются). Держать `control-api.yaml` под версионным контролем (git)
по-прежнему полезно как журнал того, что изначально ставилось в очередь
при разворачивании стенда — но для повседневного добавления адресов проще
и быстрее пользоваться API выше.
## Наблюдение за очередью
@@ -108,7 +113,7 @@ curl -s http://<control-api>:8080/api/v1/admin/ips \
| Поле | Значение |
|---|---|
| `IPAddress` | Проверяемый адрес |
| `Sequence` | Позиция в очереди (порядок из конфига) |
| `Sequence` | Позиция в очереди (порядок постановки — из конфига при первом старте либо из последнего вызова `POST /api/v1/admin/ips`) |
| `State` | Текущий этап: `queued`, `assigning_fip`, `awaiting_self_check`, `checking`, `aggregating`, `done`, `failed` |
| `OwnerValidatorID` | Какой валидатор сейчас (или последним) занимался этим адресом |
| `FIPID` | Идентификатор Floating IP в OpenStack, к которому привязан адрес (пусто, если ещё/уже не привязан) |
@@ -116,7 +121,7 @@ curl -s http://<control-api>:8080/api/v1/admin/ips \
| `RetryCount` | Сколько раз адрес уже переставлялся в очередь заново |
| `EgressComplete` | Валидатор закончил исходящие проверки |
| `Site1Complete` / `Site2Complete` / `Site3Complete` | Соответствующая площадка закончила входящие проверки |
| `OverallResult` | Итог: `pass`, `partial`, `fail`, либо пусто, пока проверка не завершена |
| `OverallResult` | Итог: `pass`, `partial`, `fail`, `cancelled` (принудительно остановлена, см. [«Принудительная остановка проверки»](#принудительная-остановка-проверки)), либо пусто, пока проверка не завершена |
| `AssignedAt` / `AggregatedAt` / `FIPReleasedAt` | Метки времени соответствующих этапов |
## Как читать итоговый результат (pass/partial/fail)
@@ -137,6 +142,10 @@ curl -s http://<control-api>:8080/api/v1/admin/ips \
трафик валидатора не пошёл через назначенный FIP — и попытки
исчерпались). Смотрите `events` по этому адресу (см. ниже), чтобы
понять, на каком шаге и почему.
- **`cancelled`** — проверку остановил оператор через `POST
/api/v1/admin/ips/{ip}/cancel` (`State` при этом — `failed`), а не
система по итогам проверок. Отличать от обычного `fail` полезно, чтобы
не путать «адрес не прошёл проверку» с «проверку прервали вручную».
Отсутствие ответа от источника (площадка не прислала результат до
истечения `checking_window_seconds`) засчитывается как провал — это
@@ -180,21 +189,48 @@ curl -s http://<control-api>:8080/api/v1/admin/validators | python3 -m json.tool
(занят), `unreachable` (пропустил heartbeat дольше
`orchestrator.heartbeat_timeout_seconds`).
**Добавление нового валидатора:**
**Добавление нового валидатора (без перезапуска control-api):**
1. Поднимите новую ВМ в сервисном проекте облака, узнайте её Neutron
`port_id`.
2. Добавьте запись в `validators` в `control-api.yaml`
(`validator_id` + `os_port_id`) и перезапустите `control-api`.
2. Зарегистрируйте валидатора через API:
```bash
curl -s -X POST http://<control-api>:8080/api/v1/admin/config/validators \
-d '{"validator_id": "validator_05", "os_port_id": "port-abc123"}'
```
3. Разверните и запустите `validator-agent` на новой ВМ с тем же
`validator_id` в его конфиге (см. [SETUP.md](SETUP.md#развёртывание-validator-agent-на-вм-валидаторах)).
Сменить `os_port_id` уже существующего валидатора (например, после
пересоздания ВМ) — `PUT /api/v1/admin/config/validators/{id}` с телом
`{"os_port_id": "новый-port-id"}`.
Полный список зарегистрированных валидаторов — `GET
/api/v1/admin/config/validators` (в отличие от `GET
/api/v1/admin/validators`, отдаёт `snake_case` и без текущего IP —
только конфигурационные поля).
**Вывод валидатора из эксплуатации:** остановите на нём
`validator-agent` (`systemctl stop validator-agent`). Он перестанет
получать новые задания после того, как закончит текущее (если оно было);
если он был убит посреди работы — control-api сам заберёт у него
незавершённый адрес обратно в очередь по истечении
`orchestrator.lease_ttl_seconds`. Удалять запись из `control-api.yaml`
не обязательно — просто выключенный агент не будет ничего забирать.
`orchestrator.lease_ttl_seconds`. Удалять регистрацию валидатора не
обязательно — просто выключенный агент не будет ничего забирать. Если всё
же нужно убрать валидатора из системы совсем:
```bash
curl -s -X DELETE http://<control-api>:8080/api/v1/admin/config/validators/validator_05
```
Возвращает `409`, если валидатор прямо сейчас владеет каким-то IP —
дождитесь освобождения (или принудительно остановите его проверку, см.
[«Принудительная остановка проверки»](#принудительная-остановка-проверки))
перед удалением.
> Правки через `validators[]` в `control-api.yaml` тоже поддерживаются,
> но только как bootstrap пустой базы данных при самом первом старте — как
> только в БД есть хотя бы один валидатор, YAML для этой секции
> игнорируется при всех последующих рестартах (см.
> [SETUP.md](SETUP.md#развёртывание-control-api)). Для стенда, который уже
> хоть раз запускался, используйте API выше.
## Управление площадками (проберами)
@@ -207,12 +243,28 @@ curl -s http://<control-api>:8080/api/v1/admin/validators | python3 -m json.tool
Явного отдельного флага "включить/выключить" нет — самого списка `sites`
достаточно.
Чтобы добавить площадку: добавьте `site_id` + `index` (1, 2 или 3 — см.
ограничение ниже) в `sites` конфига control-api и разверните на площадке
`prober` с тем же `site_id`. Чтобы отключить конкретную площадку —
уберите соответствующую запись из `sites` и перезапустите control-api;
Чтобы добавить площадку (без перезапуска control-api) — назначьте
`site_id` на один из трёх слотов (`index` 1, 2 или 3 — см. ограничение
ниже) через API:
```bash
curl -s -X PUT http://<control-api>:8080/api/v1/admin/config/sites/1 \
-d '{"site_id": "site-1"}'
```
и разверните на площадке `prober` с тем же `site_id`. Чтобы отключить
конкретную площадку — освободите слот:
```bash
curl -s -X DELETE http://<control-api>:8080/api/v1/admin/config/sites/1
```
процесс `prober` на ней можно не останавливать (он просто перестанет
получать назначения).
получать назначения — `POST /api/v1/probers/register` для отвязанного
`site_id` начнёт отвечать `400`). Текущее распределение слотов — `GET
/api/v1/admin/config/sites`.
> Правки через `sites[]` в `control-api.yaml` тоже поддерживаются, но
> только как bootstrap пустой базы данных при самом первом старте — как
> только в БД есть хотя бы одна площадка, YAML для этой секции
> игнорируется при всех последующих рестартах. Для стенда, который уже
> хоть раз запускался, используйте API выше.
> Важно: количество *возможных* слотов площадок жёстко зашито в схему БД
> (`Site1Complete`/`Site2Complete`/`Site3Complete`) — не более **трёх**,
@@ -221,23 +273,104 @@ curl -s http://<control-api>:8080/api/v1/admin/validators | python3 -m json.tool
> поддерживаемый сценарий; *больше* трёх потребует доработки схемы
> данных, одной правкой конфига не обойтись.
## Управление целями проверки
Набор egress-целей (`targets`) и типов проверок (`check_types`,
привязывающих тип — `https`/`icmp`/`ssh` — к одной или нескольким группам
целей) управляется через API так же, как валидаторы и площадки —
изменения подхватываются немедленно, следующим же назначением от
оркестратора, без перезапуска.
Посмотреть текущий набор:
```bash
curl -s http://<control-api>:8080/api/v1/admin/config/targets | python3 -m json.tool
curl -s http://<control-api>:8080/api/v1/admin/config/check-types | python3 -m json.tool
```
Создать/заменить группу целей и включить тип проверки, ссылающийся на
неё:
```bash
curl -s -X PUT http://<control-api>:8080/api/v1/admin/config/targets/web \
-d '{"targets": ["https://hub.docker.com", "https://github.com"]}'
curl -s -X PUT http://<control-api>:8080/api/v1/admin/config/check-types/ssh \
-d '{"enabled": true, "targets": ["web"]}'
```
Отключить тип проверки, не удаляя его (значения целей сохраняются):
```bash
curl -s -X PUT http://<control-api>:8080/api/v1/admin/config/check-types/ssh \
-d '{"enabled": false, "targets": ["web"]}'
```
Удалить группу целей можно только если на неё не ссылается ни один
`check_type` (иначе — `409`); удалить сам `check_type` можно в любой
момент (`DELETE /api/v1/admin/config/check-types/{name}`).
**Важное следствие принятого компромисса**: если конфигурация меняется
ровно в момент, когда чей-то IP уже находится в `checking` (self-check
уже пройден, проверки уже назначены агенту), агрегация этой конкретной
попытки посчитает уже изменённую конфигурацию, а не ту, что была на
момент выдачи задания. На практике это узкое окно в несколько секунд;
деградирует безопасно — через `aggregation.missing_counts_as_fail` худший
исход для одной попытки — `partial` вместо `pass`, самоисправляется на
следующей попытке (в том числе через принудительный повтор, см. ниже).
> Правки через `targets`/`check_types` в `control-api.yaml` тоже
> поддерживаются, но только как bootstrap пустой базы данных при самом
> первом старте — см. примечание в разделах выше.
## Повторная проверка адреса
Если адрес завершился с `failed` или `partial`, а вы хотите перепроверить
его ещё раз (например, после устранения блокировки на стороне сети):
на данный момент нет отдельного API-метода "перезапустить проверку".
Самый простой путь:
1. Убедитесь, что адрес не находится в активном состоянии (`checking`
и т.п.) — то есть уже `done`/`failed`.
2. Временно уберите и снова добавьте адрес в список `ip_addresses`
(либо просто пересоздайте запись в БД вручную, если это единичный
случай и у вас есть доступ к SQLite) и перезапустите `control-api`.
его ещё раз (например, после устранения блокировки на стороне сети) —
отправьте его тем же методом, что используется для постановки новых
адресов в очередь:
Поскольку сидирование очереди идёт по уникальности `ip_address`
(конфликт по уже существующей записи просто игнорируется), самый чистый
способ гарантированно перепроверить конкретный адрес — обратиться к
администратору БД (см. следующий раздел) либо дождаться штатной
доработки API под повторные проверки.
```bash
curl -s -X POST http://<control-api>:8080/api/v1/admin/ips \
-d '{"addresses": ["203.0.113.10"]}'
```
```json
{"added": [], "requeued": ["203.0.113.10"], "reordered": [], "skipped_in_progress": []}
```
Адрес в `requeued` означает, что он был в терминальном состоянии
(`done`/`failed`) и его перезапустили: `AttemptNumber` увеличился,
`RetryCount` обнулён, предыдущий `OverallResult` сброшен, адрес снова
`queued` и будет обработан на общих основаниях. Никакой особой обработки
для уже проверенных адресов не требуется — тот же вызов безопасно
принимает список из новых и уже проверенных адресов одновременно;
единственное, что метод не сделает — не запустит вторую параллельную
проверку адреса, который прямо сейчас уже проверяется (такой адрес
вернётся в `skipped_in_progress`, см.
[«Добавление новых IP в очередь»](#добавление-новых-ip-в-очередь)).
## Принудительная остановка проверки
Если проверка адреса зависла дольше ожидаемого либо просто больше не
актуальна, не дожидайтесь истечения `checking_window_seconds` —
остановите её сразу:
```bash
curl -s -X POST http://<control-api>:8080/api/v1/admin/ips/203.0.113.10/cancel
```
Работает из любого состояния, кроме уже терминального (`done`/`failed`
вернут `409` — отменять нечего). Если на момент отмены был привязан
Floating IP — он отвязывается (best-effort, как и при обычном завершении
проверки), владевший валидатор освобождается и снова становится `idle`.
Итог записывается как `OverallResult: "cancelled"` (в `State: "failed"`),
и виден в истории адреса наравне с обычными результатами:
```bash
curl -s http://<control-api>:8080/api/v1/admin/ips/203.0.113.10 | python3 -m json.tool
```
Чтобы позже всё же проверить этот адрес — используйте
[«Повторную проверку адреса»](#повторная-проверка-адреса) выше, она
одинаково работает и для отменённых, и для обычно завершённых адресов.
## Частые проблемы и что с ними делать