# API Control API Control API — единственная точка входа в систему для `validator-agent`, `prober` и оператора (администратора). Все данные передаются в формате JSON, базовый префикс прикладных методов — `/api/v1`. > **Важно.** На данный момент API не защищён аутентификацией/авторизацией > — эндпоинты доступны любому, кто может достучаться до порта control-api > по сети. Это касается и методов из раздела > [«Управление очередью и конфигурацией»](#управление-очередью-и-конфигурацией) > ниже — они меняют, что и как проверяется, без подтверждения личности > вызывающего. Для эксплуатации за пределами доверенного сегмента сети > обязательно ограничьте доступ на уровне сети/файрвола (см. > [SETUP.md](SETUP.md#сетевые-доступы)). Добавление bearer-токена — известное > направление доработки, в текущей версии не реализовано. Базовый 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) ## Общие соглашения - Тело запроса и ответа — JSON (`Content-Type: application/json`). - Успешные ответы возвращают `200 OK`, либо `204 No Content` (когда данных нет — например, у валидатора сейчас нет назначения). - Ошибки возвращают `4xx`/`5xx` и тело вида: ```json {"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` Регистрация/переактивация валидатора. Вызывается один раз при старте агента (и безопасно при каждом рестарте — идемпотентна). Запрос: ```json { "validator_id": "validator_01", "hostname": "vm-validator-01", "agent_version": "1.0.0" } ``` Ответ: ```json {"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`. Запрос (тело необязательно, поля информационные): ```json {"local_state": "idle"} ``` Ответ: `{"ok": true}`. `404`, если `validator_id` не зарегистрирован. ### `GET /api/v1/agents/{id}/assignment` Есть ли у валидатора сейчас работа. Опрашивается в каждом цикле. - `204 No Content` — заданий нет. - `200 OK` с телом: ```json { "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. Запрос: ```json { "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` и т.п. Запрос: ```json { "event_type": "config_received", "ip_id": 42, "payload": "{\"checks\":2}" } ``` Ответ: `{"ok": true}`. ### `POST /api/v1/agents/{id}/results` Отчёт о результатах исходящих (egress) проверок. Можно отправлять по одной проверке сразу после выполнения (рекомендуется — так прогресс не теряется при падении агента) либо пачкой. Запрос: ```json { "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 выполнены". Запрос: ```json {"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#управление-площадками-проберами) в USAGE.md про состояния площадки). Запрос: ```json {"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). Ответ: ```json [ {"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`](#типы-проверок-пробера-apiv1adminconfiginbound-checks) и меняется без рестарта control-api. ### `POST /api/v1/probers/{site_id}/results` Отчёт о результатах входящих (inbound) проверок с данной площадки. Запрос: ```json { "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 в каком состоянии. ```json { "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](USAGE.md#значения-полей-ip) — расшифровка полей и статусов). ### `GET /api/v1/admin/ips/{ip}` Детали по одному адресу: сам объект IP, все проверки текущей попытки и вся история событий по нему. ```json { "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](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`/уже отменён) — отменять нечего. ### `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, используйте с осторожностью. ```bash 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](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` — без паузы (поведение по умолчанию, как до появления этого параметра). | Метод | Путь | Тело | Успех | Ошибки | |---|---|---|---|---| | 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»](#методы-для-prober) выше). Один набор общий для всех площадок; список [«площадок»](#площадки-apiv1adminconfigsites) отдельно решает, *сколько* точек его применяют, а не что именно они проверяют. Пустой список портов и `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#управление-площадками-проберами) в USAGE.md про тот же принцип для `sites`/`targets`/`check_types`). Как и остальные разделы этой группы, YAML-поле `orchestrator.inbound_checks` в `control-api.yaml` — только одноразовый bootstrap для пустой БД; дальше источник истины — сама база, менять значение нужно через `PUT` выше (или страницу `/settings` в дашборде). ### Пример: конфигурация целиком через 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" ``` ## Модель состояний и связь методов с ней ``` 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`, см. [«Настройки оркестратора»](#настройки-оркестратора-apiv1adminconfigorchestrator) выше), `GET /api/v1/agents/{id}/assignment` продолжает отдавать `204` до истечения паузы, и только потом начинает отдавать assignment — состояние в БД при этом уже `awaiting_self_check`. Площадки (`siteN_complete`) — опциональны: сколько их учитывается, целиком определяется текущим списком `sites` (без ограничения по числу записей, управляется через `/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` с уже завершённым адресом в списке; - **любое состояние → адрес физически исчезает из очереди**, вместе со всей историей — `DELETE /api/v1/admin/ips/{ip}`, `POST /api/v1/admin/ips/delete`, `POST /api/v1/admin/ips/clear` (см. [выше](#delete-apiv1adminipsip-post-apiv1adminipsdelete-post-apiv1adminipsclear)). Не путать с cancel — cancel сохраняет запись как историю (`failed`/ `cancelled`), delete стирает её целиком без возможности восстановления. ## Сквозной пример работы (curl) Ниже — минимальный ручной прогон одного IP через API, как если бы вы писали собственного клиента вместо `validator-agent`/`prober`. Полезно для отладки и для понимания протокола. ```bash 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](LOCAL_E2E.md).