Scan floating IPs in the background, page by page, so thousands of addresses work

The "Scan Floating IP" button failed with a client timeout: the project now
holds ~6.4k floating IPs and the scan listed them all in one unpaginated,
timeout-less Neutron request on the HTTP request context.

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-01 19:31:11 +03:00
1 parent debf2afed2
commit aff8fe38b5
61 files changed
+5833 -536

No files matched your search

+66 -22
View File
@@ -330,15 +330,37 @@ IP на данном проходе". До этого момента control-api
{
"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 из очереди со всеми полями (см.
Список 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, все проверки текущей попытки и
@@ -470,33 +492,52 @@ YAML для этой секции больше не перечитывается
### `POST /api/v1/admin/ips/scan`
Сканирует текущий проект OpenStack на предмет свободных (не привязанных ни
к одному порту) Floating IP и сразу передаёт найденный список в `POST
/api/v1/admin/ips` — тот же add/requeue/reorder-вызов, как если бы
оператор ввёл эти адреса вручную. Не принимает тело запроса.
Запускает **фоновое** сканирование проекта 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-токен).
Ответ (`200`):
```json
{
"scanned_free": 3,
"added": ["203.0.113.20"],
"requeued": [],
"reordered": ["203.0.113.10", "203.0.113.11"],
"skipped_in_progress": []
"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": ""
}
```
`scanned_free` — сколько свободных Floating IP нашлось в проекте всего
(включая уже стоящие в очереди — они попадут в `reordered`, а не
`added`). Если свободных адресов нет вообще, это не ошибка: ответ будет
`{"scanned_free": 0, "added": [], ...}`.
`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» в дашборде). Пока включён
[автоматический цикл](#автоматический-цикл-проверок), периодический скан
не выполняется.
Помимо ручного вызова, сканирование можно включить по расписанию — `orchestrator.fip_scan_interval_seconds` в `control-api.yaml`
(0, по умолчанию, — только по запросу через эту ручку или кнопку «Сканировать Floating IP» в дашборде). Пока включён
[автоматический цикл](#автоматический-цикл-проверок), периодический скан не выполняется.
## Автоматический цикл проверок
@@ -575,7 +616,10 @@ curl -s -X POST http://<control-api>:8080/api/v1/admin/auto-cycle/stop
### `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
[