Files
cloud-ip-validator/docs/API.md
T
ayurishchevandClaude Sonnet 5.5 068c10ea1c Analytics: compare two finished runs
New page /analytics/compare and API GET /admin/analytics/compare (+ /lists/{group}):
the administrator picks an old (A) and a new (B) run; the report shows the new
addresses (only in B), the ones that left (only in A) and the common ones whose
membership in the seven indicators (pass, partial, fail, egress https any/all,
ingress ssh any/all) differs, with a "what changed" summary per address; the
dynamics of each indicator (delta = new - left + entered - exited) and a verdict
transition matrix. Every number opens a list with CSV. Cancelled addresses are not
part of a run. The list dialog moved to a shared analytics-dialog.js and template;
/analytics got a "compare with another run" button.

Docs, plan and summary in docs/changes/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 10:26:37 +03:00

76 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, GET /agents/{id}/observed-ip; POST /probers/register, POST /probers/{site_id}/heartbeat, GET /probers/{site_id}/assignments
  • Токены разные: токен администратора не подходит для методов агентов, и наоборот.
  • Валидатор и пробер могут без токена зарегистрироваться, слать heartbeat и забирать задание (настройку); валидатор также может спросить, с какого адреса его видит control-api (observed-ip); отправка результатов без токена агентов — 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 — уже развёрнутая конфигурация проверок (тип + список целей), агенту не нужно самому сопоставлять группы целей.

GET /api/v1/agents/{id}/observed-ip

С какого адреса control-api видит соединение валидатора. Используется self-check способом control_api (self_check.methods в validator-agent.yaml) как альтернатива внешнему IP-echo сервису. Уровень доступа — открыто: отдаётся только адрес самого вызывающего.

Ответ 200:

{"ip": "203.0.113.10", "source": "remote_addr"}
  • Адрес берётся только из адреса TCP-соединения (RemoteAddr), приведённого к каноничному виду (::ffff:1.2.3.4 → 1.2.3.4). Заголовки X-Forwarded-For / X-Real-IP не учитываются: иначе валидатор мог бы подделать адрес и пройти проверку. Метод рассчитан на прямое подключение без обратного прокси.
  • 404, если validator_id не зарегистрирован.
  • Способ корректен, только если соединение выходит через внешнюю сеть (SNAT Floating IP). Если control-api достижим из облака по внутренней сети, он увидит приватный адрес валидатора. Если порт control-api опубликован через Docker, проверьте, что ручка показывает внешний адрес клиента, а не адрес шлюза Docker.

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 в этом определении не участвует. Дополнительно можно включить способ control_api (self_check.methods): агент спрашивает у control-api через GET /agents/{id}/observed-ip, с какого адреса тот его видит. Способы пробуются по приоритету, достаточно подтверждения любым из них; без настройки работает только IP-echo. Важно, что ресурс должен быть именно внешним: OpenStack применяет SNAT через Floating IP только к трафику, уходящему через внешнюю сеть, поэтому обращение к чему-либо внутри проекта (в том числе к самому control-api, если он в той же внутренней сети) покажет приватный адрес валидатора независимо от того, правильно ли привязан FIP.

Запрос:

{
  "ip_id": 42,
  "detected_egress_ip": "203.0.113.10",
  "success": true,
  "detail": "matched (control_api)"
}

Ответ: {"ok": true}. При success: false control-api сам решает — вернуть адрес в очередь или пометить IP как failed. Сбой записывается в историю адреса; повтор не отдаётся этому валидатору (он остаётся в работе и берёт остальные адреса). Итог fail ставится, когда число проваленных self-check у адреса достигло потолка self_check_max_attempts (по умолчанию 5, см. «Настройки оркестратора»); orchestrator.max_self_check_retries не используется. Сбой привязки FIP и истечение лизинга идут по max_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 /agents/{id}/results и POST /probers/{site_id}/results принимают результат только пока адрес проверяется (состояния до aggregating) и только для его текущей попытки. Результат, пришедший после начала агрегации или после вердикта, а также результат прежней попытки не сохраняется и не меняет сохранённые проверки. Ответ остаётся 200, чтобы отправитель не повторял запрос: {"ok": true, "ignored": N}, где N — число отброшенных проверок. На каждый такой запрос по адресу пишется событие result_dropped ({"source": "egress"|"inbound-site-N", "dropped": N}). Благодаря этому вердикт всегда совпадает с сохранёнными проверками и пересчитывается из них.

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}, либо {"ok": true, "ignored": N}, если часть результатов отброшена (см. «Результаты после вердикта» ниже). 404, если site_id не сконфигурирован.

GET /api/v1/probers/{site_id}/assignments

Список IP в состоянии checking, которые эта площадка ещё не зондировала в текущей попытке (валидаторов может работать несколько параллельно, поэтому список, а не один IP). Адрес выдаётся площадке, пока она не пришлёт для него результат с complete: true; после этого этой площадке он больше не выдаётся (другим площадкам выдаётся). Новая попытка (повтор после сбоя) выдаёт адрес снова. Так каждая площадка зондирует адрес один раз за попытку.

Ответ:

[
  {"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": "...", "...": "..."} ],
  "self_check_failed_on": ["vkiplab-v17"]
}

self_check_failed_on — валидаторы, у которых self-check на этом адресе не прошёл в текущем запуске (по алфавиту; [], если сбоев не было).

Обратите внимание: вложенные объекты 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",
    "last_cycle_id": 4,
    "egress":  {"total": 5, "ok": 5, "by_type": [
      {"type": "https", "total": 3, "ok": 3},
      {"type": "icmp",  "total": 2, "ok": 2}
    ]},
    "ingress": {"total": 4, "ok": 3, "by_type": [
      {"type": "icmp", "total": 1, "ok": 1},
      {"type": "tcp",  "total": 3, "ok": 2}
    ]}
  }
]

in_queue/current_state отражают, есть ли у адреса сейчас живая строка в ip_queue, а не только в реестре.

Уровни egress и ingress. Результат последнего цикла (last_cycle_id — наибольший цикл с записанными проверками, 0 — проверок нет), разделённый на выходные проверки валидатора (egress) и входные проверки пробера со всех площадок (ingress). В каждом уровне: total — сколько проверок записано, ok — сколько успешных, by_type — то же по типам, по алфавиту. Тип — это check_type до первого дефиса: tcp-22 и tcp-443 дают tcp, tls-443 — tls; https, icmp, ssh остаются как есть, новый тип проверки появляется в by_type сам. Без проверок на уровне: {"total": 0, "ok": 0, "by_type": []}. Счёт идёт по записанным проверкам, а last_result учитывает ещё и недостающие результаты, поэтому при неполном наборе вердикт может быть хуже, чем «ok из total».

Фильтры постраничного режима (только вместе с limit): run — только адреса, у которых есть результат в этом запуске (см. «Аналитика запусков»); subnet — только адреса внутри подсети (CIDR, например 203.0.113.0/24). Неверный run или subnet — 400.

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, "...": "..."}
  ],
  "self_check_failed_on": ["vkiplab-v17"]
}

self_check_failed_on — валидаторы, у которых self-check на этом адресе не прошёл, по всем запускам (по алфавиту; [], если сбоев не было).

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 — без ограничения (поведение по умолчанию).
  • self_check_max_attempts — потолок провалов self-check на один адрес (1…50, по умолчанию 5). Когда у адреса провалено столько self-check, он получает итог fail. Не зависит от числа валидаторов. Валидатор, проваливший self-check на адресе, этому адресу больше не выдаётся (на остальные адреса это не влияет); если все рабочие валидаторы уже провалили адрес, исключения сбрасываются и повторы продолжаются до потолка. Значение действует на следующих повторах без перезапуска. В PUT поле необязательно: если не передано, не меняется.
Метод Путь Тело Успех Ошибки
GET /api/v1/admin/config/orchestrator — {"fip_settle_seconds":N,"history_retention_cycles":M,"self_check_max_attempts":K}
PUT /api/v1/admin/config/orchestrator {"fip_settle_seconds":N,"history_retention_cycles":M,"self_check_max_attempts":K} 200 400, если N < 0, M < 0 или K вне 1…50, или если 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.

Аналитика запусков

Запуск — одна «партия» проверок. Он открывается, когда адрес попадает в пустую (или полностью обработанную) очередь; пока он открыт, в него входят все добавленные и перепроверяемые адреса. Запуск завершается, когда все его адреса получили итог (done, failed, occupied) либо удалены из очереди. Перепроверка после завершения запуска открывает новый запуск, прежний не меняется. Тип запуска: auto (скан автоцикла) или manual. Для одного адреса в запуске хранится результат его последнего цикла. Для данных, накопленных до появления запусков, запуски выделены по паузам: циклы, которые заканчиваются с промежутком меньше часа, образуют один запуск.

GET /api/v1/admin/analytics/runs

Список запусков, новые первыми, для выбора на странице «Аналитика».

[
  {"id": 2, "kind": "manual", "state": "open",      "started_at": "2026-10-03T15:30:00Z", "finalized_at": null,
   "addresses": 120, "pass": 40, "partial": 80, "fail": 0, "cancelled": 0, "total": 200, "pending": 80},
  {"id": 1, "kind": "manual", "state": "finalized", "started_at": "2026-10-02T13:46:45Z", "finalized_at": "2026-10-02T22:28:54Z",
   "addresses": 6440, "pass": 1962, "partial": 4478, "fail": 0, "cancelled": 0, "total": 6440, "pending": 0}
]

addresses — адреса с итогом, total — все адреса запуска в очереди, pending — ещё в работе.

GET /api/v1/admin/analytics/runs/{id}

Все показатели страницы по одному завершённому запуску; открытый запуск — 409, неизвестный — 404. Считаются проверки последнего цикла каждого адреса в запуске, в том числе пришедшие позже вердикта (как факты). Результат кэшируется, пока данные запуска и список подсетей не менялись.

Блок Содержимое
run id, kind, state, started_at, finalized_at, duration_seconds, rechecked (адресов с несколькими циклами в запуске)
summary addresses, pass, partial, fail, cancelled; egress_ok, ingress_ok (адреса, у которых все записанные проверки уровня успешны); egress_https_any_failed и egress_https_all_failed (хотя бы одна / все https-проверки провалены), egress_https_all_targets_failed (все цели полного набора); ingress_ssh_any_failed, ingress_ssh_all_failed; addresses_per_minute
reasons причины partial, каждый адрес один раз: «Только egress», «Ingress и egress», «Egress и неполный набор», «Ingress, egress и неполный набор», «Только неполный набор», «Только ingress»; нулевые не выдаются
quality late_failed_checks_at_pass, late_failed_addresses_at_pass, ingress_failed_checks, ingress_failed_late, incomplete_addresses, pass_with_failed_addresses, pass_by_facts
subnets по подсети: cidr, label, addresses, pass, egress_ok, ingress_ok (без списка подсетей — группы по /24; адрес вне списка — «прочие»)
targets types (семейства egress-проверок), targets (хосты, по убыванию провалов https), failed (по типу: число адресов с провалом на каждую цель)
matrix по типу: строки «подсеть × цель» для подсетей с partial (partial, percent по целям)
sites types и строки площадок: total и ok проверок по типу
errors классы ошибок проваленных ingress-проверок («SSH: таймаут», «ICMP: нет ответа», …) со счётчиками
validators по валидатору: total, ok https-проверок egress

Тип проверки — это check_type до первого дефиса: tcp-22 и tcp-443 дают tcp.

GET /api/v1/admin/analytics/runs/{id}/lists/{kind}

Таблица адресов за показателем или классом ошибки: {"kind", "class", "columns": [...], "rows": [[...]]}. kind: verdict_pass, verdict_partial, verdict_fail, egress_https_any, egress_https_all, ingress_ssh_any, ingress_ssh_all или error (с ?class=SSH: таймаут; без класса и неизвестный kind — 404). С ?format=csv — файл CSV (UTF-8 с BOM, Content-Disposition: attachment, имя вида ingress_ssh_all_run1.csv). Для verdict_* — адреса запуска с этим вердиктом (без cancelled, по числовому порядку; число строк равно summary.pass/partial/fail): адрес, подсеть, валидатор (по https-проверкам, «—», если их нет), Egress и Ingress («успешно из всех», «—» без проверок), «Проверок в цикле» (записано из ожидаемых) и у partial ещё «Причина» (как в блоке reasons). Для error строка — одна проваленная проверка: адрес, подсеть, площадка, валидатор, вердикт адреса, статус («провал, в вердикте» или «провал, после вердикта»).

GET /api/v1/admin/analytics/compare?base=A&target=B

Сравнение двух завершённых запусков: base — старый (A), target — новый (B). Адрес — это IP; адрес с итогом cancelled в запуск не входит (их число — в cancelled). Ответ:

Поле Содержимое
runs base и target: сведения о запуске и addresses
groups new (есть в B, нет в A), left (были в A, нет в B), common (в обоих), changed, same; common = changed + same
indicators по семи индикаторам (verdict_pass, verdict_partial, verdict_fail, egress_https_any, egress_https_all, ingress_ssh_any, ingress_ssh_all): base, target, delta, new, left, entered, exited; delta = new − left + entered − exited
transitions verdicts, matrix[из][в] вердиктов общих адресов, new и left — новые/выбывшие по вердикту
cancelled отменённые адреса в base и target

Общий адрес изменился, если его принадлежность хотя бы к одному из семи индикаторов в A и B разная (другой набор проваленных целей или площадок при тех же индикаторах — не изменение). 400 — нет или неверные base/target либо они совпадают, 404 — запуска нет, 409 — запуск ещё идёт.

GET /api/v1/admin/analytics/compare/lists/{group}?base=A&target=B

Таблица адресов группы new, left, common, changed, same, entered или exited. Фильтры: indicator (ключ индикатора; для entered и exited обязателен; для new/left — адрес входит в индикатор в своём запуске, для common/changed/same — хотя бы в одном из запусков), from и to вместе (вердикт в A и в B; только для общих групп). Неизвестные группа, индикатор или фильтр — 404. Столбцы new и left: адрес, подсеть, вердикт, Egress, Ingress, индикаторы. Остальные группы: адрес, подсеть, вердикт, Egress и Ingress в виде A → B и «Что изменилось» (вердикт, вход в индикаторы и выход из них, добавленные и убранные цели https и площадки ssh, смена валидатора; у группы без изменений — «без изменений»). С ?format=csv — файл CSV (UTF-8 с BOM), имя compare_<group>[_<indicator>][_<from>-<to>]_run<A>-<B>.csv.

GET /api/v1/admin/config/subnets, PUT /api/v1/admin/config/subnets

Список подсетей, по которым группируются адреса на странице «Аналитика». PUT заменяет список целиком:

{"subnets": [{"cidr": "83.166.248.0/21", "label": "москва"}, {"cidr": "10.0.0.0/8"}]}

CIDR приводится к канонической записи (10.1.2.3/24 → 10.1.2.0/24), повторы схлопываются; неверный CIDR — 400, список остаётся прежним. Адрес относится к самой узкой подходящей подсети.