control-api: every route now carries a mandatory access level (admin / agent / open) in a route table. All /api/v1/admin/* require the admin token; the write calls of validator-agent and prober (self-check, events, results, complete) require a separate static agent token; register, heartbeat and fetching the assignment stay open. Tokens come from env vars, are compared in constant time and never logged. An empty token leaves that level open with a startup warning (backward compatible). validator-agent / prober: apiclient sends the agent token only to control-api. admin-dashboard: login/password (from env) with a stateless HMAC session cookie, Origin-based CSRF check, per-IP brute-force throttle, HX-Redirect for htmx polls, logout in the sidebar; the dashboard calls control-api with the admin token. Login page layout fixed after review. Also: env plumbing in docker-compose/rxprod-compose/systemd/config examples, e2e script with token assertions, tests, docs (API, SETUP, USAGE, DASHBOARD, README), plan and review under docs/changes/, bin/ rebuilt with new SHA256SUMS. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
54 KiB
API Control API
Control API — единственная точка входа в систему для validator-agent,
prober и оператора (администратора). Все данные передаются в формате
JSON, базовый префикс прикладных методов — /api/v1.
Аутентификация. Доступ к API определяется двумя статическими bearer-токенами — см. «Аутентификация». Токен задаётся переменной окружения; если токен не задан, соответствующий уровень остаётся открытым (control-api стартует с предупреждением в логе) — так сделано для обратной совместимости. Поэтому для эксплуатации за пределами доверенного сегмента сети токены нужно задать, а доступ дополнительно ограничить на уровне сети/файрвола (см. SETUP.md).
Базовый URL в примерах — http://control-api.internal:8080, замените на
адрес вашего стенда (см. server.listen_addr в конфиге control-api).
Для работы из браузера вместо
curlестьadmin-dashboard— веб-панель, дающая графический доступ ко всему административному API ниже, см. DASHBOARD.md.
Содержание
- Аутентификация
- Общие соглашения
- Методы для validator-agent
- Методы для prober
- Служебные и административные методы
- Управление очередью и конфигурацией
- Автоматический цикл проверок
- Реестр адресов и история проверок
- Модель состояний и связь методов с ней
- Сквозной пример работы (curl)
Аутентификация
Токен передаётся заголовком Authorization: Bearer <токен>. Токены статические, без срока жизни; ротация — смена
переменной окружения и перезапуск. Сравнение выполняется в константное время.
| Уровень | Токен (переменная на control-api) | Какие методы |
|---|---|---|
| admin | CONTROL_API_ADMIN_TOKEN |
все /api/v1/admin/* (очередь, реестр, автоцикл, config/*) |
| agent | CONTROL_API_AGENT_TOKEN |
запись результатов: POST /agents/{id}/self-check, /events, /results, /complete и POST /probers/{site_id}/results |
| открыто | — | GET /healthz; POST /agents/register, POST /agents/{id}/heartbeat, GET /agents/{id}/assignment; POST /probers/register, POST /probers/{site_id}/heartbeat, GET /probers/{site_id}/assignments |
- Токены разные: токен администратора не подходит для методов агентов, и наоборот.
- Валидатор и пробер могут без токена зарегистрироваться, слать heartbeat и забирать задание (настройку); отправка результатов без токена агентов —
401. - Имена переменных меняются в секции
authконфига control-api (admin_token_env,agent_token_env); сами значения в YAML не хранятся. - Ответ при отказе:
401 {"error": "unauthorized"}с заголовкомWWW-Authenticate: Bearer. - Токен не задан (пустая переменная) — уровень открыт; в логе control-api при старте предупреждение. Токены нужно генерировать случайными:
openssl rand -hex 32. - Токены уходят открытым текстом, если TLS не терминируется перед control-api, — публикуйте API через reverse-proxy с TLS.
export ADMIN_TOKEN=... # значение CONTROL_API_ADMIN_TOKEN
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" http://<control-api>:8080/api/v1/admin/status
Во всех примерах
curlниже заголовокAuthorizationдля краткости опущен; если токен администратора задан, добавляйте его к методам/api/v1/admin/*.
Общие соглашения
- Тело запроса и ответа — JSON (
Content-Type: application/json). - Успешные ответы возвращают
200 OK, либо204 No Content(когда данных нет — например, у валидатора сейчас нет назначения). - Ошибки возвращают
4xx/5xxи тело вида:{"error": "текст ошибки"} - Временные метки (
checked_atв запросах) передаются в формате RFC3339/RFC3339Nano, например2026-08-21T09:15:00.123456789Z. Если поле не удалось распарсить, сервер молча подставит текущее время сервера — не полагайтесь на это в продакшене, всегда передавайте валидную метку. validator_idиsite_idв пути запроса должны совпадать со значениями, известными control-api — заданными вcontrol-api.yamlпри первом запуске (пустая база) либо созданными позже через/api/v1/admin/config/*(см. «Управление очередью и конфигурацией») — иначе методы, требующие существующую сущность, вернут404.
Методы для validator-agent
Эти методы вызывает бинарник validator-agent, работающий на ВМ-валидаторе.
Оператору вручную дёргать их обычно не требуется — они приведены для
понимания протокола и для отладки через curl.
POST /api/v1/agents/register
Регистрация/переактивация валидатора. Вызывается один раз при старте агента (и безопасно при каждом рестарте — идемпотентна).
Запрос:
{
"validator_id": "validator_01",
"hostname": "vm-validator-01",
"agent_version": "1.0.0"
}
Ответ:
{"ok": true, "poll_interval_seconds": 5}
poll_interval_seconds — рекомендованный интервал опроса, значение берётся
из orchestrator.poll_interval_seconds конфига control-api.
validator_idдолжен быть заранее описан в конфиге control-api (validators[].validator_id) вместе сos_port_id— сам агент порт ID не передаёт и не может его сменить через API.
POST /api/v1/agents/{id}/heartbeat
"Я жив". Обновляет last_heartbeat_at валидатора. Если валидатор не
присылает heartbeat дольше orchestrator.heartbeat_timeout_seconds, он
помечается unreachable.
Запрос (тело необязательно, поля информационные):
{"local_state": "idle"}
Ответ: {"ok": true}. 404, если validator_id не зарегистрирован.
GET /api/v1/agents/{id}/assignment
Есть ли у валидатора сейчас работа. Опрашивается в каждом цикле.
204 No Content— заданий нет.200 OKс телом:{ "ip_id": 42, "ip_address": "203.0.113.10", "phase": "awaiting_self_check", "check_config": [ {"type": "https", "targets": ["https://hub.docker.com", "https://github.com", "https://packages.ubuntu.com"]}, {"type": "icmp", "targets": ["https://hub.docker.com", "https://github.com", "https://packages.ubuntu.com"]} ] }
phase — awaiting_self_check (нужно выполнить self-check) либо
checking (self-check уже пройден, можно/нужно выполнять проверки).
check_config — уже развёрнутая конфигурация проверок (тип + список
целей), агенту не нужно самому сопоставлять группы целей.
POST /api/v1/agents/{id}/self-check
Отчёт о результате self-check — подтверждение, что исходящий трафик
валидатора действительно идёт через только что назначенный FIP. Агент
определяет это сам, обращаясь к внешнему (снаружи облака) IP-echo
сервису (self_check.ip_echo_urls в validator-agent.yaml, например
api.ipify.org) и сравнивая ответ с ip_address из задания — control-api
в этом определении не участвует. Важно, что ресурс должен быть именно
внешним: OpenStack применяет SNAT через Floating IP только к трафику,
уходящему через внешнюю сеть, поэтому обращение к чему-либо внутри
проекта (в том числе к самому control-api, если он в той же внутренней
сети) покажет приватный адрес валидатора независимо от того, правильно
ли привязан FIP.
Запрос:
{
"ip_id": 42,
"detected_egress_ip": "203.0.113.10",
"success": true,
"detail": "matched"
}
Ответ: {"ok": true}. При success: false control-api сам решает —
повторить попытку назначения FIP или пометить IP как failed (после
исчерпания orchestrator.max_self_check_retries).
POST /api/v1/agents/{id}/events
Произвольная запись в журнал аудита, привязанная (опционально) к IP.
Используется агентом для событий config_received, self_check_result
и т.п.
Запрос:
{
"event_type": "config_received",
"ip_id": 42,
"payload": "{\"checks\":2}"
}
Ответ: {"ok": true}.
POST /api/v1/agents/{id}/results
Отчёт о результатах исходящих (egress) проверок. Можно отправлять по одной проверке сразу после выполнения (рекомендуется — так прогресс не теряется при падении агента) либо пачкой.
Запрос:
{
"results": [
{
"ip_id": 42,
"check_type": "https",
"target": "https://github.com",
"success": true,
"latency_ms": 87,
"detail": "ok",
"checked_at": "2026-08-21T09:15:00.123Z"
}
]
}
Ответ: {"ok": true}. Повторная отправка того же (ip_id, check_type, target) в рамках текущей попытки — безопасна и просто перезапишет
результат (upsert по уникальному ключу).
POST /api/v1/agents/{id}/complete
Сигнал "все исходящие проверки для этого IP выполнены".
Запрос:
{"ip_id": 42}
Ответ: {"ok": true}.
Методы для prober
Эти методы вызывает бинарник prober, работающий на внешней площадке.
POST /api/v1/probers/register
Регистрация пробера. site_id должен присутствовать в конфиге control-api
(sites[].site_id), иначе — 400. Помимо привычной проверки, запоминает
hostname и переводит площадку в состояние idle (если она была
unregistered/unreachable) — то же самое, что делает validator-agent
при своей регистрации (см.
«Управление площадками» в
USAGE.md про состояния площадки).
Запрос:
{"site_id": "site-1", "hostname": "probe-host-1"}
Ответ: {"ok": true, "poll_interval_seconds": 5}.
POST /api/v1/probers/{site_id}/heartbeat
«Я жив». Обновляет last_heartbeat_at площадки. Если площадка не
присылает heartbeat дольше orchestrator.heartbeat_timeout_seconds
(тот же параметр, что и для валидаторов), она помечается unreachable.
Полный аналог POST /api/v1/agents/{id}/heartbeat для пробера.
Запрос: тело не требуется.
Ответ: {"ok": true}. 404, если site_id не сконфигурирован.
GET /api/v1/probers/{site_id}/assignments
Список всех IP, которые сейчас находятся в состоянии checking — то есть
всё, что нужно прозондировать на этом цикле опроса (валидаторов может
работать несколько параллельно, поэтому список, а не один IP).
Ответ:
[
{"ip_id": 42, "ip_address": "203.0.113.10", "ports": [22, 80, 443, 8080], "icmp": true}
]
Пустой список [], если сейчас нечего проверять. ports/icmp — текущая
конфигурация типов проверок пробера, одна и та же для каждого IP в ответе;
управляется через
/api/v1/admin/config/inbound-checks
и меняется без рестарта control-api.
POST /api/v1/probers/{site_id}/results
Отчёт о результатах входящих (inbound) проверок с данной площадки.
Запрос:
{
"results": [
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "tcp-22", "success": true, "latency_ms": 12, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "ssh", "success": true, "latency_ms": 15, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "tcp-80", "success": true, "latency_ms": 9, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "tcp-443", "success": true, "latency_ms": 10, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "tls-443", "success": true, "latency_ms": 34, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "tcp-8080","success": false,"latency_ms": 0, "checked_at": "2026-08-21T09:15:01Z", "complete": false},
{"ip_id": 42, "ip_address": "203.0.113.10", "check_type": "icmp", "success": true, "latency_ms": 5, "checked_at": "2026-08-21T09:15:01Z", "complete": true}
]
}
complete: true нужно проставить ровно на одном (обычно последнем)
результате в пачке — это сигнал "площадка N закончила зондирование этого
IP на данном проходе". До этого момента control-api не будет считать
данные с этой площадки завершёнными.
Порты 22 и 443 в ports — особый случай: если они присутствуют в
списке, prober дополнительно к базовой tcp-<port>-проверке доступности
выполняет настоящий протокольный чек — ssh (обмен SSH-банером на
порту 22) и tls-443 (полноценный TLS-хендшейк на порту 443)
соответственно. Это не отдельно настраиваемые типы проверок — они
включаются/выключаются автоматически вместе с самим портом, без
дополнительного флага. Сертификат в tls-443 не валидируется
(InsecureSkipVerify) — проверяется только сам факт TLS-хендшейка, не
доверие к серверу.
Ответ: {"ok": true}.
Служебные и административные методы
GET /healthz
Проверка живости процесса. Ответ: {"ok": true}. Используется в systemd/
внешних системах мониторинга.
GET /api/v1/admin/status
Сводка по очереди — сколько IP в каком состоянии.
{
"total_ips": 25,
"ips_by_state": {"queued": 10, "checking": 3, "done": 11, "failed": 1},
"total_validators": 4
}
GET /api/v1/admin/ips
Полный список всех IP из очереди со всеми полями (см. USAGE.md — расшифровка полей и статусов).
GET /api/v1/admin/ips/{ip}
Детали по одному адресу: сам объект IP, все проверки текущей попытки и вся история событий по нему.
{
"ip": { "ID": 42, "IPAddress": "203.0.113.10", "State": "done", "OverallResult": "pass", "...": "..." },
"checks": [ {"Source": "egress", "CheckType": "https", "Target": "https://github.com", "Success": true, "...": "..."} ],
"events": [ {"EventType": "fip_associated", "OccurredAt": "...", "...": "..."} ]
}
Обратите внимание: вложенные объекты
ip/checks/eventsсериализуются без переопределения имён полей (используются имена Go-структур, напримерIPAddress,State,Success) — в отличие от методов для agent/prober, где поля вsnake_case. Это осознанная асимметрия: административные методы — для человека/дашборда, а не для машинного протокола.
GET /api/v1/admin/validators
Список всех валидаторов с их текущим состоянием (unregistered, idle,
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); он не конфликтует с
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) → не трогается вообще — нельзя запустить вторую параллельную проверку одного и того же адреса.
Порядок обработки внутри одного вызова соответствует порядку адресов в списке; повторная отправка того же списка позже даёт тот же относительный порядок прогона.
Запрос:
{"addresses": ["203.0.113.10", "203.0.113.11"]}
Ответ (200):
{
"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/уже отменён) — отменять нечего.
DELETE /api/v1/admin/ips/{ip}, POST /api/v1/admin/ips/delete, POST /api/v1/admin/ips/clear
Удаляет строку ip_queue — адрес пропадает из очереди/GET /api/v1/admin/ips* — безвозвратно, без возможности восстановить именно
эту строку. Работает из любого состояния, включая активно проверяемое —
если Floating IP привязан, он отвязывается тем же best-effort способом,
что и при cancel/обычном завершении, владеющий валидатор освобождается.
Накопленная история адреса при этом не теряется: checks/events
остаются в реестре (ip_registry, см. раздел «Реестр
адресов» ниже) и доступны через GET /api/v1/admin/registry/{ip} даже после удаления строки из очереди — в
отличие от ip_queue, реестровая запись никогда не удаляется этими
методами.
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| DELETE | /api/v1/admin/ips/{ip} |
— | 200 {"ok":true} |
404 неизвестный адрес |
| POST | /api/v1/admin/ips/delete |
{"addresses":[...]} |
200 {"deleted":[...],"not_found":[...]} |
400 пустой список |
| POST | /api/v1/admin/ips/clear |
— | 200 {"deleted":[...]} |
— |
POST .../delete удаляет ровно перечисленный список (неизвестные адреса
идут в not_found, не ошибка — тот же терпимый стиль, что у POST /api/v1/admin/ips). POST .../clear удаляет вообще всё, что сейчас в
очереди, включая адреса в процессе проверки — самая опасная операция
этого API, используйте с осторожностью (и, опять же, ничья история при
этом физически не стирается — см. выше).
POST /api/v1/admin/ips/scan
Сканирует текущий проект OpenStack на предмет свободных (не привязанных ни
к одному порту) Floating IP и сразу передаёт найденный список в POST /api/v1/admin/ips — тот же add/requeue/reorder-вызов, как если бы
оператор ввёл эти адреса вручную. Не принимает тело запроса.
Ответ (200):
{
"scanned_free": 3,
"added": ["203.0.113.20"],
"requeued": [],
"reordered": ["203.0.113.10", "203.0.113.11"],
"skipped_in_progress": []
}
scanned_free — сколько свободных Floating IP нашлось в проекте всего
(включая уже стоящие в очереди — они попадут в reordered, а не
added). Если свободных адресов нет вообще, это не ошибка: ответ будет
{"scanned_free": 0, "added": [], ...}.
Помимо ручного вызова, сканирование можно включить по расписанию —
orchestrator.fip_scan_interval_seconds в control-api.yaml (0, по
умолчанию, — только по запросу через эту ручку или кнопку «Сканировать
Floating IP» в дашборде). Пока включён
автоматический цикл, периодический скан
не выполняется.
Автоматический цикл проверок
Опциональный повторяющийся сценарий «очистить очередь → просканировать Floating IP → дождаться завершения всех проверок → пауза → заново» (описание для оператора — USAGE.md). По умолчанию выключен. Состояние и параметры хранятся в базе; перезапуск control-api их не сбрасывает.
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
GET |
/api/v1/admin/auto-cycle |
— | 200, статус |
— |
PUT |
/api/v1/admin/auto-cycle |
{"interval_seconds": N, "max_run_seconds": M} — любое поле можно опустить |
200, статус |
400 — interval_seconds < 60 или max_run_seconds < 0 (ничего не применяется) |
POST |
/api/v1/admin/auto-cycle/start |
— | 200, статус |
— |
POST |
/api/v1/admin/auto-cycle/stop |
— | 200, статус |
— |
Каждый метод отвечает полным объектом статуса:
{
"enabled": true,
"interval_seconds": 3600,
"max_run_seconds": 0,
"phase": "waiting",
"run_started_at": null,
"next_run_at": "2026-10-01T08:00:12.345Z",
"last_run_started_at": "2026-10-01T07:00:01.100Z",
"last_run_finished_at": "2026-10-01T07:00:12.345Z",
"last_outcome": "completed",
"last_error": "",
"last_scanned_free": 12,
"runs_total": 5
}
phase—idle(выключен или ещё не стартовал),running(идут проверки),waiting(пауза доnext_run_at).last_outcome—completed,no_free_ips,timeout,errorилиstopped; пустая строка, пока не завершился ни один цикл. Дляerrorпричина — вlast_error.last_scanned_free— сколько свободных Floating IP нашёл скан последнего цикла;runs_total— сколько циклов завершилось исходомcompleted.max_run_seconds = 0— без ограничения времени ожидания проверок.- Времена — RFC 3339 (UTC),
null, пока не наступили.
POST .../start идемпотентен: у уже включённого цикла ничего не меняется
(идущий цикл не перезапускается). Включённый цикл начинает первый прогон на
ближайшем шаге оркестратора (порядка orchestrator.poll_interval_seconds).
POST .../stop выключает цикл и переводит его в idle; проверки, которые
уже идут, не прерываются. Новое значение interval_seconds начинает действовать
со следующей паузы.
События цикла (auto_cycle_started, auto_cycle_completed,
auto_cycle_timeout, auto_cycle_error, auto_cycle_stopped) попадают в
общий журнал событий; шаги цикла записывают обычные queue_cleared и
fip_scan.
curl -s -X PUT http://<control-api>:8080/api/v1/admin/auto-cycle \
-d '{"interval_seconds": 7200, "max_run_seconds": 1800}'
curl -s -X POST http://<control-api>:8080/api/v1/admin/auto-cycle/start
curl -s http://<control-api>:8080/api/v1/admin/auto-cycle
curl -s -X POST http://<control-api>:8080/api/v1/admin/auto-cycle/stop
Реестр адресов и история проверок
В отличие от ip_queue (текущая рабочая очередь, см. выше), реестр —
ip_registry — это накопительная запись обо всех адресах, когда-либо
поставленных на проверку, вне зависимости от того, стоят ли они сейчас в
очереди. Запись в реестре переживает удаление адреса из ip_queue (DELETE /api/v1/admin/ips/{ip} и т.п.) и повторное добавление того же адреса
позже — обе истории (до и после) остаются доступны и не перекрывают друг
друга (каждой постановке на проверку соответствует свой cycle_id,
уникальный в пределах адреса на всё время).
GET /api/v1/admin/registry
Список всех адресов реестра с краткой сводкой по каждому.
[
{
"ip_address": "203.0.113.10",
"first_seen_at": "2026-01-10T12:00:00Z",
"last_seen_at": "2026-02-01T09:00:00Z",
"total_cycles": 4,
"last_result": "pass",
"last_checked_at": "2026-02-01T09:05:00Z",
"in_queue": true,
"current_state": "done"
}
]
in_queue/current_state отражают, есть ли у адреса сейчас живая строка в
ip_queue, а не только в реестре.
GET /api/v1/admin/registry/{ip}
Реестровая запись по одному адресу плюс вся сохранённая история проверок
по нему, по всем циклам (не только текущему — в отличие от GET /api/v1/admin/ips/{ip}, который отдаёт проверки только текущей попытки).
Порядок — от новых циклов к старым.
{
"registry": { "ip_address": "203.0.113.10", "total_cycles": 4, "...": "..." },
"checks": [
{"CycleID": 4, "Source": "egress", "CheckType": "https", "Success": true, "...": "..."},
{"CycleID": 3, "Source": "egress", "CheckType": "https", "Success": false, "...": "..."}
]
}
404, если адрес никогда не ставился на проверку.
Глубина хранения. Сколько последних циклов на адрес хранится в
checks (и синхронно — в events), управляется полем
history_retention_cycles в GET/PUT /api/v1/admin/config/orchestrator
(0, по умолчанию, — без ограничения). Сама реестровая запись (ip_address,
first_seen_at, счётчик циклов) не удаляется никогда, независимо от этой
настройки — она лишь ограничивает глубину детальной истории проверок.
curl -s -X DELETE "$BASE/api/v1/admin/ips/203.0.113.10"
curl -s -X POST "$BASE/api/v1/admin/ips/delete" -d '{"addresses":["203.0.113.10","203.0.113.11"]}'
curl -s -X POST "$BASE/api/v1/admin/ips/clear"
Валидаторы: /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,
столько площадок, сколько нужно оператору. Пустой список слотов — штатный
сценарий, отключающий inbound-проверки целиком (см.
USAGE.md).
Помимо index/site_id, объект площадки несёт состояние подключения
пробера — hostname/state/last_heartbeat_at, тот же смысл, что у
аналогичных полей валидатора (unregistered/idle/unreachable,
обновляются через POST /api/v1/probers/register и POST /api/v1/probers/{site_id}/heartbeat, см. выше). PUT на слот всегда
сбрасывает эти три поля к значениям «ещё не подключался» — новый (или
даже тот же) site_id трактуется как новая идентичность пробера.
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| GET | /api/v1/admin/config/sites |
— | [{"index","site_id","hostname","state","last_heartbeat_at"}] |
|
| PUT | /api/v1/admin/config/sites/{index} |
{"site_id"} |
200 |
400, если index < 1; 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/v1/admin/config/orchestrator
Два параметра:
fip_settle_seconds— пауза между привязкой Floating IP к валидатору и моментом, когда self-check по этому адресу становится доступен агенту (GET /api/v1/agents/{id}/assignmentдо истечения паузы отдаёт204, как если бы валидатору просто нечего было делать — никаких изменений в протоколе агента). Нужна, чтобы дать data plane OpenStack время реально начать пропускать трафик через только что привязанный адрес, прежде чем запускать по нему проверки.0— без паузы (поведение по умолчанию, как до появления этого параметра).history_retention_cycles— сколько последних циклов проверки хранить на адрес в реестре (GET /api/v1/admin/registry/{ip}, см. «Реестр адресов»).0— без ограничения (поведение по умолчанию).
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| GET | /api/v1/admin/config/orchestrator |
— | {"fip_settle_seconds":N,"history_retention_cycles":M} |
|
| PUT | /api/v1/admin/config/orchestrator |
{"fip_settle_seconds":N,"history_retention_cycles":M} |
200 |
400, если N < 0 или M < 0, или если fip_settle_seconds + self_check_timeout_seconds >= lease_ttl_seconds (пауза не должна съедать весь лизинг адреса — иначе self-check не успеет пройти до истечения lease_ttl_seconds, и адрес будет вечно возвращаться в очередь) |
Как и остальные разделы этой группы, YAML-поле orchestrator. fip_settle_seconds в control-api.yaml — только одноразовый bootstrap
для пустой БД; дальше источник истины — сама база, менять значение нужно
через PUT выше (или страницу /settings в дашборде).
history_retention_cycles не имеет YAML-эквивалента вообще — управляется
только через PUT выше/дашборд, значение по умолчанию 0 всегда
применяется на пустой БД.
Типы проверок пробера: /api/v1/admin/config/inbound-checks
Единый глобальный набор TCP-портов и флага ICMP, которые prober
проверяет на каждой настроенной площадке для каждого адреса в состоянии
checking — то же самое ports/icmp, что отдаётся в ответе GET /api/v1/probers/{site_id}/assignments (см.
«Методы для prober» выше). Один набор общий для всех
площадок; список «площадок» отдельно
решает, сколько точек его применяют, а не что именно они проверяют.
Пустой список портов и icmp: false одновременно — допустимая
конфигурация: временно отключает inbound-проверки, не трогая список
sites.
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| GET | /api/v1/admin/config/inbound-checks |
— | {"ports":[...],"icmp":bool} |
|
| PUT | /api/v1/admin/config/inbound-checks |
{"ports":[...],"icmp":bool} |
200 |
400, если какой-то порт вне диапазона 1..65535 или порты повторяются |
Изменение вступает в силу немедленно — как для следующего ответа GET /api/v1/probers/{site_id}/assignments, так и для агрегации уже идущих
проверок (см. «Управление площадками»
в USAGE.md про тот же принцип для sites/targets/check_types). Как и
остальные разделы этой группы, YAML-поле orchestrator.inbound_checks в
control-api.yaml — только одноразовый bootstrap для пустой БД; дальше
источник истины — сама база, менять значение нужно через PUT выше (или
страницу /settings в дашборде).
Пример: конфигурация целиком через API, без единой строки в YAML
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"
Модель состояний и связь методов с ней
queued ──(control-api сам, без вызова API)──▶ assigning_fip
│
OpenStack FIP associate успешен
▼
awaiting_self_check
│
POST .../self-check {success:true}
▼
checking
│ POST .../results (agent) │ POST .../results (prober, по числу настроенных площадок)
▼ ▼
egress_complete=true siteN_complete=true (только для N, перечисленных в sites конфига)
│
egress + все НАСТРОЕННЫЕ площадки complete=true ИЛИ истекло checking_window_seconds
▼
aggregating
│
done (pass/partial/fail)
или failed
Переходы queued → assigning_fip → awaiting_self_check и финальная
агрегация выполняются control-api самостоятельно по таймеру (см.
orchestrator.poll_interval_seconds), явного HTTP-метода для их запуска
нет — это фоновый цикл (Tick), а не запрос/ответ.
Из assigning_fip есть и второй, терминальный исход: если на момент
попытки ассоциации Floating IP уже привязан к чужому порту (облако живое —
список адресов мог разойтись с реальностью с момента постановки в
очередь, либо адрес был ошибочно передан занятым), control-api переводит
адрес в состояние occupied вместо продолжения в awaiting_self_check —
цикл проверки для этой попытки не запускается вовсе. Это отдельное
терминальное состояние, а не failed: failed означает «проверка
стартовала и не прошла», occupied — «проверка не стартовала, потому что
адрес занят кем-то другим». В аудит-логе адреса (events) фиксируется
строка fip_occupied. overall_result для этого состояния остаётся
пустым. Как и done/failed, occupied сбрасывается обратно в queued
повторной постановкой через POST /api/v1/admin/ips — этим способом
оператор возвращает адрес в работу, убедившись, что конфликт в облаке
разрешился.
Вход в awaiting_self_check не означает мгновенную видимость агенту: если
настроена пауза (fip_settle_seconds, см.
«Настройки оркестратора»
выше), GET /api/v1/agents/{id}/assignment продолжает отдавать 204 до
истечения паузы, и только потом начинает отдавать assignment — состояние
в БД при этом уже awaiting_self_check.
Площадки (siteN_complete) — опциональны: сколько их учитывается,
целиком определяется текущим списком sites (без ограничения по числу
записей, управляется через /api/v1/admin/config/sites — см.
выше). Пустой список — агрегация
ждёт только egress_complete, ни одна площадка не требуется. Подробнее —
USAGE.md.
Три дополнительных перехода, все инициируются оператором через
/api/v1/admin/ips*, а не самим оркестратором:
- любое нетерминальное состояние →
failed(overall_result: "cancelled") —POST /api/v1/admin/ips/{ip}/cancel; done/failed/occupied→queued(новая попытка) —POST /api/v1/admin/ipsс уже завершённым (или занятым) адресом в списке;- любое состояние → адрес физически исчезает из очереди —
DELETE /api/v1/admin/ips/{ip},POST /api/v1/admin/ips/delete,POST /api/v1/admin/ips/clear(см. выше). Не путать с cancel — cancel сохраняет запись как историю (failed/cancelled) прямо вip_queue; delete убирает саму строкуip_queueбезвозвратно, но накопленная история проверок остаётся в реестре (GET /api/v1/admin/registry/{ip}) — см. «Реестр адресов».
Сквозной пример работы (curl)
Ниже — минимальный ручной прогон одного IP через API, как если бы вы
писали собственного клиента вместо validator-agent/prober. Полезно
для отладки и для понимания протокола.
BASE=http://127.0.0.1:8080
# 1. Регистрация валидатора (validator_01 уже должен быть в конфиге control-api)
curl -s -X POST "$BASE/api/v1/agents/register" \
-d '{"validator_id":"validator_01","hostname":"debug-host","agent_version":"manual"}'
# 2. Подождать, пока control-api (фоновым тиком) назначит IP и привяжет FIP —
# проверяем через admin/status или admin/ips, либо просто опрашиваем assignment
curl -s "$BASE/api/v1/agents/validator_01/assignment"
# => {"ip_id":1,"ip_address":"203.0.113.10","phase":"awaiting_self_check","check_config":[...]}
# 3. Self-check: спросить у ВНЕШНЕГО (вне облака) IP-echo сервиса, каким
# адресом мы наружу выглядим — это делает сам агент, control-api тут
# ни при чём (см. self_check.ip_echo_urls в validator-agent.yaml)
curl -s "https://api.ipify.org"
# => 203.0.113.10 (в реальном стенде это и есть проверка через FIP)
curl -s -X POST "$BASE/api/v1/agents/validator_01/self-check" \
-d '{"ip_id":1,"detected_egress_ip":"203.0.113.10","success":true,"detail":"matched"}'
# 4. Отправить результаты egress-проверок (по одному check_config пункту)
curl -s -X POST "$BASE/api/v1/agents/validator_01/results" \
-d '{"results":[{"ip_id":1,"check_type":"https","target":"https://github.com","success":true,"latency_ms":80,"checked_at":"2026-08-21T09:00:00Z"}]}'
# 5. Сообщить, что все egress-проверки выполнены
curl -s -X POST "$BASE/api/v1/agents/validator_01/complete" -d '{"ip_id":1}'
# 6. Со стороны пробера: узнать, что сейчас проверяется, и отправить результат
curl -s -X POST "$BASE/api/v1/probers/register" -d '{"site_id":"site-1"}'
curl -s "$BASE/api/v1/probers/site-1/assignments"
curl -s -X POST "$BASE/api/v1/probers/site-1/results" \
-d '{"results":[{"ip_id":1,"ip_address":"203.0.113.10","check_type":"icmp","success":true,"latency_ms":5,"checked_at":"2026-08-21T09:00:01Z","complete":true}]}'
# 7. Проверить итоговый результат (после того как control-api агрегирует)
curl -s "$BASE/api/v1/admin/ips/203.0.113.10" | python3 -m json.tool
Для полностью автоматизированного локального прогона (без ручных curl)
см. scripts/run-local-e2e.sh и docs/LOCAL_E2E.md.