Files
cloud-ip-validator/docs/API.md
T
ayurishchevandClaude Sonnet 5.5 aff8fe38b5 Scan floating IPs in the background, page by page, so thousands of addresses work
The "Scan Floating IP" button failed with a client timeout: the project now
holds ~6.4k floating IPs and the scan listed them all in one unpaginated,
timeout-less Neutron request on the HTTP request context.

openstack: ListFreeFloatingIPs reads marker-based pages (fields= keeps them
small) with per-page retry/backoff on transport errors, 5xx and 429, and every
request now has a timeout (also ends hangs inside the orchestrator tick).

orchestrator: the scan is a single-flight background job on the process
context with progress (clearing/listing/enqueuing/done/error), dry_run, full
discovery before anything is enqueued, then SubmitIPs in chunks of 500 in
ascending IP order; a failed read leaves the queue untouched. The auto-cycle
gets a "scanning" phase that polls the job, so the control loop and
autoCycleMu are never held across OpenStack/DB work; it recovers after a
restart and waits for (instead of adopting) a scan started by someone else.

db: migration 0009 (indexes), paged ListIPsPage/ListRegistryPage, GROUP BY
counters, EXISTS completion check, set-based ClearAllIPs.

API: POST /admin/ips/scan -> 202 (dry_run, wait), GET /admin/ips/scan, paging
and filters on /admin/ips and /admin/registry (bare arrays without limit),
results_by_overall in /admin/status.

dashboard: scan progress panel and dry-run button, paginated /ips and
/registry with server-side filters, Overview on counters and capped lists
with progress/ETA, "select all N by filter", hx-params fix for per-row
buttons, real counts in confirmations.

Also: docs (API, USAGE, DASHBOARD, README), plan and review under
docs/changes/, bin/ rebuilt with new SHA256SUMS.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 19:31:11 +03:00

59 KiB
Raw Blame History

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.

Содержание

Аутентификация

Токен передаётся заголовком 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},
  "results_by_overall": {"pass": 8, "partial": 3, "fail": 0, "cancelled": 0},
  "total_validators": 4
}

results_by_overall — сколько адресов с каким итогом (всегда все четыре ключа). Счётчики считаются запросами GROUP BY на стороне БД, а не загрузкой всей очереди, поэтому метод быстрый и при тысячах адресов.

GET /api/v1/admin/ips

Список IP из очереди со всеми полями (см. USAGE.md — расшифровка полей и статусов).

Без параметров — как раньше: весь список одним массивом (при тысячах адресов это мегабайты — для больших очередей используйте постраничный режим). С limit — постраничный режим: ответ — конверт

{"items": [ ... ], "total": 6440, "limit": 50, "offset": 0}
Параметр Значение
limit размер страницы, 1…1000 (иначе 400); включает постраничный режим
offset смещение, >= 0 (без limit — 400)
state одно или несколько состояний через запятую (queued, assigning_fip, awaiting_self_check, checking, aggregating, done, failed, occupied)
q подстрока адреса
result итог: pass, partial, fail, cancelled
order sequence (по умолчанию, порядок очереди) или aggregated_at_desc (последние завершённые)

total — число записей после фильтров. Параметры фильтров без limit возвращают отфильтрованный массив.

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 и ставит их в очередь — тот же add/requeue/reorder, что и POST /api/v1/admin/ips. Не принимает тело запроса и сразу отвечает 202 со статусом задания; ход сканирования смотрите через GET /api/v1/admin/ips/scan.

Почему в фоне: в проекте может быть тысячи Floating IP (на стенде — около 6,4 тыс.), Neutron отдаёт такой список минуты. Control-api читает его страницами (по openstack.list_page_size, по умолчанию 200, по marker), повторяет страницу при обрыве соединения/5xx/429, сначала обнаруживает все адреса и только потом ставит их в очередь кусками по 500 в порядке возрастания IP. Если чтение не удалось (после повторов), в очередь не попадает ничего — очередь остаётся как была, а статус задания — error.

Параметр Значение
dry_run=true только найти и посчитать свободные адреса; очередь не меняется (безопасная проверка, итог — в статусе)
wait=true дождаться окончания и ответить 200 прежним телом {scanned_free, added[], requeued[], reordered[], skipped_in_progress[]} (для curl и скриптов; при ошибке 502)

Одновременно идёт одно сканирование: повторный запрос во время работы присоединяется к текущему и тоже отвечает 202 с его статусом.

GET /api/v1/admin/ips/scan

Статус и прогресс сканирования (admin-токен).

{
  "state": "listing",
  "running": true,
  "dry_run": false,
  "pages": 12,
  "discovered": 2400,
  "free": 2399,
  "added": 0,
  "requeued": 0,
  "reordered": 0,
  "skipped_in_progress": 0,
  "started_at": "2026-10-01T15:47:40.759Z",
  "finished_at": null,
  "error": ""
}

state: idle (в этом процессе сканирования ещё не было), clearing (очистка очереди — только в автоцикле), listing (чтение страниц), enqueuing (постановка в очередь), done, error (причина в error), cancelled. discovered — сколько Floating IP прочитано (свободных и занятых), free — из них свободных, added/requeued/reordered/skipped_in_progress — итог постановки в очередь (как в POST /admin/ips). Статус хранится в памяти процесса: после перезапуска control-api он снова idle.

Помимо ручного вызова, сканирование можно включить по расписанию — 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

Список адресов реестра с краткой сводкой по каждому. Без параметров — все адреса одним массивом; с limit (1…1000) — постраничный конверт {"items": [...], "total": N, "limit": L, "offset": O}, параметры offset, q (подстрока адреса) и last_result (pass/partial/fail/cancelled). Страница и фильтры применяются в SQL до расчёта сводки, поэтому реестр из тысяч адресов отдаётся за доли секунды.

[
  {
    "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.