37 KiB
API Control API
Control API — единственная точка входа в систему для validator-agent,
prober и оператора (администратора). Все данные передаются в формате
JSON, базовый префикс прикладных методов — /api/v1.
Важно. На данный момент API не защищён аутентификацией/авторизацией — эндпоинты доступны любому, кто может достучаться до порта control-api по сети. Это касается и методов из раздела «Управление очередью и конфигурацией» ниже — они меняют, что и как проверяется, без подтверждения личности вызывающего. Для эксплуатации за пределами доверенного сегмента сети обязательно ограничьте доступ на уровне сети/файрвола (см. SETUP.md). Добавление bearer-токена — известное направление доработки, в текущей версии не реализовано.
Базовый URL в примерах — http://control-api.internal:8080, замените на
адрес вашего стенда (см. server.listen_addr в конфиге control-api).
Для работы из браузера вместо
curlестьadmin-dashboard— веб-панель, дающая графический доступ ко всему административному API ниже, см. DASHBOARD.md.
Содержание
- Общие соглашения
- Методы для validator-agent
- Методы для prober
- Служебные и административные методы
- Управление очередью и конфигурацией
- Модель состояний и связь методов с ней
- Сквозной пример работы (curl)
Общие соглашения
- Тело запроса и ответа — 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.
Запрос:
{"site_id": "site-1", "hostname": "probe-host-1"}
Ответ: {"ok": true, "poll_interval_seconds": 5}.
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": "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": "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 не будет считать
данные с этой площадки завершёнными.
Ответ: {"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
Безвозвратное удаление, в отличие от cancel выше: строка ip_queue
и вся её история (checks, events) стираются физически, без возможности
восстановления. Работает из любого состояния, включая активно
проверяемое — если Floating IP привязан, он отвязывается тем же
best-effort способом, что и при cancel/обычном завершении, владеющий
валидатор освобождается.
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| 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, используйте с осторожностью.
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, 2, 3}) — это ограничение схемы БД
(ip_queue.site{1,2,3}_complete), а не искусственное. Пустой список слотов
— штатный сценарий, отключающий inbound-проверки целиком (см.
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/v1/admin/config/orchestrator
Единственный на сегодня параметр — fip_settle_seconds: пауза между
привязкой Floating IP к валидатору и моментом, когда self-check по этому
адресу становится доступен агенту (GET /api/v1/agents/{id}/assignment до истечения паузы отдаёт 204, как если
бы валидатору просто нечего было делать — никаких изменений в протоколе
агента). Нужна, чтобы дать data plane OpenStack время реально начать
пропускать трафик через только что привязанный адрес, прежде чем
запускать по нему проверки. 0 — без паузы (поведение по умолчанию, как
до появления этого параметра).
| Метод | Путь | Тело | Успех | Ошибки |
|---|---|---|---|---|
| GET | /api/v1/admin/config/orchestrator |
— | {"fip_settle_seconds":N} |
|
| PUT | /api/v1/admin/config/orchestrator |
{"fip_settle_seconds":N} |
200 |
400, если N < 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 в дашборде).
Типы проверок пробера: /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), а не запрос/ответ.
Вход в awaiting_self_check не означает мгновенную видимость агенту: если
настроена пауза (fip_settle_seconds, см.
«Настройки оркестратора»
выше), GET /api/v1/agents/{id}/assignment продолжает отдавать 204 до
истечения паузы, и только потом начинает отдавать assignment — состояние
в БД при этом уже awaiting_self_check.
Площадки (siteN_complete) — опциональны: сколько их учитывается,
целиком определяется текущим списком sites (0–3 записи, управляется
через /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→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), delete стирает её целиком без возможности восстановления.
Сквозной пример работы (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.