control-api is hosted outside the cloud and validators reach it directly,
so it sees the floating IP as the connection's source address. New open
route GET /api/v1/agents/{id}/observed-ip returns that address (taken only
from the TCP peer; forwarding headers are ignored so a validator cannot
forge it).
The agent gets self_check.methods, a priority-ordered list of ip_echo
(unchanged) and control_api; the default stays [ip_echo]. The self-check
passes when any method confirms the address; the next method is tried on
no answer and on a mismatch. Each method has its own timeout so a hung
first method cannot starve the fallback, and control_api uses a new TCP
connection per call (a connection opened before the floating IP was
attached would keep reporting the old address).
Also: docker agent template/env, example config, docs, plan in
docs/changes, e2e script switch E2E_SELF_CHECK_METHODS, rebuilt
bin/control-api and bin/validator-agent.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
958 lines
61 KiB
Markdown
958 lines
61 KiB
Markdown
# API Control API
|
||
|
||
Control API — единственная точка входа в систему для `validator-agent`,
|
||
`prober` и оператора (администратора). Все данные передаются в формате
|
||
JSON, базовый префикс прикладных методов — `/api/v1`.
|
||
|
||
> **Аутентификация.** Доступ к API определяется двумя статическими
|
||
> bearer-токенами — см. [«Аутентификация»](#аутентификация). Токен задаётся
|
||
> переменной окружения; **если токен не задан, соответствующий уровень остаётся
|
||
> открытым** (control-api стартует с предупреждением в логе) — так сделано для
|
||
> обратной совместимости. Поэтому для эксплуатации за пределами доверенного
|
||
> сегмента сети токены нужно задать, а доступ дополнительно ограничить на уровне
|
||
> сети/файрвола (см. [SETUP.md](SETUP.md#сетевые-доступы)).
|
||
|
||
Базовый 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)
|
||
|
||
## Аутентификация
|
||
|
||
Токен передаётся заголовком `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.
|
||
|
||
```bash
|
||
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` и тело вида:
|
||
```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` — уже развёрнутая конфигурация проверок (тип + список
|
||
целей), агенту не нужно самому сопоставлять группы целей.
|
||
|
||
### `GET /api/v1/agents/{id}/observed-ip`
|
||
|
||
С какого адреса control-api видит соединение валидатора. Используется
|
||
self-check способом `control_api` (`self_check.methods` в
|
||
`validator-agent.yaml`) как альтернатива внешнему IP-echo сервису. Уровень
|
||
доступа — открыто: отдаётся только адрес самого вызывающего.
|
||
|
||
Ответ `200`:
|
||
```json
|
||
{"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.
|
||
|
||
Запрос:
|
||
```json
|
||
{
|
||
"ip_id": 42,
|
||
"detected_egress_ip": "203.0.113.10",
|
||
"success": true,
|
||
"detail": "matched (control_api)"
|
||
}
|
||
```
|
||
|
||
Ответ: `{"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": "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 в каком состоянии.
|
||
|
||
```json
|
||
{
|
||
"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](USAGE.md#значения-полей-ip) — расшифровка полей и статусов).
|
||
|
||
**Без параметров** — как раньше: весь список одним массивом (при тысячах адресов это мегабайты — для больших очередей
|
||
используйте постраничный режим). **С `limit`** — постраничный режим: ответ — конверт
|
||
|
||
```json
|
||
{"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, все проверки текущей попытки и
|
||
вся история событий по нему.
|
||
|
||
```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`
|
||
|
||
Удаляет строку `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-токен).
|
||
|
||
```json
|
||
{
|
||
"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](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`, статус | — |
|
||
|
||
Каждый метод отвечает полным объектом статуса:
|
||
|
||
```json
|
||
{
|
||
"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`.
|
||
|
||
```bash
|
||
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 до расчёта сводки, поэтому
|
||
реестр из тысяч адресов отдаётся за доли секунды.
|
||
|
||
```json
|
||
[
|
||
{
|
||
"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}`, который отдаёт проверки только текущей попытки).
|
||
Порядок — от новых циклов к старым.
|
||
|
||
```json
|
||
{
|
||
"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`, счётчик циклов) не удаляется никогда, независимо от этой
|
||
настройки — она лишь ограничивает глубину детальной истории проверок.
|
||
|
||
```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` — без паузы (поведение по умолчанию, как
|
||
до появления этого параметра).
|
||
- `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»](#методы-для-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`), а не запрос/ответ.
|
||
|
||
Из `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`, см.
|
||
[«Настройки оркестратора»](#настройки-оркестратора-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`/`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` (см.
|
||
[выше](#delete-apiv1adminipsip-post-apiv1adminipsdelete-post-apiv1adminipsclear)).
|
||
Не путать с cancel — cancel сохраняет запись как историю (`failed`/
|
||
`cancelled`) прямо в `ip_queue`; delete убирает саму строку `ip_queue`
|
||
безвозвратно, но накопленная история проверок остаётся в реестре (`GET
|
||
/api/v1/admin/registry/{ip}`) — см.
|
||
[«Реестр адресов»](#реестр-адресов-и-история-проверок).
|
||
|
||
## Сквозной пример работы (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).
|