Статусы хоста — полное описание
Статус хоста хранится в таблице host_statuses (одна строка на хост, host_id — PK,
ON DELETE CASCADE от hosts), поле status под CHECK-constraint.
Допустимые значения: enrolling, active, pending, degraded, stale,
needs_rebootstrap, revoked.
UI. Dashboard-панель Issues =
pending + degraded(клик →/hosts?status=pending,degraded). Фильтр-чипы на Hosts:active / enrolling / pending / degraded / stale.needs_rebootstrapиrevoked— валидные статусы в БД, но чипов под них нет (ставятся только ручнымPATCH /hosts/{id}); фильтровать по ним — через URL-параметр?status=.
Диаграмма переходов
enroll RPC ┌──────────┐ enrollment complete
───────────► │ enrolling│ UpdateHostStatus → active
└────┬─────┘
▼
┌─────────┐◄─────────────── MarkRecoveredHosts (sweep)
│ active │◄─────────────── Hello (reconnect)
└────┬────┘
┌──────────┴──────────┐
│ heartbeat > 90s │ desired_gen > confirmed_gen
│ (нет heartbeat) │ AND прошло > 5 мин с gen-bump
▼ ▼
┌─────────┐ ┌─────────┐
│ stale │ │ pending │
└────┬────┘ └────┬────┘
│ desired>confirmed │ desired>confirmed
│ AND > 15 мин │ AND > 15 мин
▼ ▼
└────────►┌──────────┐◄┘
│ degraded │
└──────────┘
┌───────────────────┐ ┌─────────┐
│ needs_rebootstrap │ │ revoked │ ← только ручной PATCH /hosts/{id}
└───────────────────┘ └─────────┘
(выход: переэнролл) (финальный, необратимый)
Подробно по каждому статусу
enrolling
Смысл: хост создан в БД, но ещё не прошёл PKI enrollment. TLS-сертификата нет, бандл доставить нельзя.
Кто присваивает:
POST /hostsс пустымstatus— дефолтenrolling.UpsertBulkHost— при конфликте по(tenant_id, hostname)всегда сбрасывает вenrolling.- Enrollment gRPC создаёт хост через
UpsertBulkHost— сразуenrolling.
Переход:
- Успешный enrollment RPC →
active. - Ре-энролл того же hostname (
UpsertBulkHost) → обратно вenrolling.
Поведение: ResolveHosts пропускает enrolling — политики не применяются; sweep
не трогает; бандл не доставляется (нет mTLS-серта).
active
Смысл: хост работает нормально. Агент подключён или недавно был, применённые политики подтверждены.
Кто присваивает:
UpdateHostConnected— при каждом успешномHelloв gRPC-стриме (пишетagent_version,kernel_version,last_seen_ip,status='active',last_heartbeat_at=NOW()).- Конец enrollment RPC (
UpdateHostStatus → active). MarkRecoveredHostssweep — изpending/degraded.PATCH /hosts/{id}с{"status":"active"}— ручное восстановление.
Переходы:
- →
stale— sweepMarkStaleHosts, еслиlast_heartbeat_at < NOW() - 90s. - →
pending— sweepMarkPendingHosts, еслиdesired_generation > last_confirmed_generationи прошло > 5 мин с последнего gen-bump.
Поведение: входит в матчинг политик; cascade пушит бандлы немедленно, если агент подключён.
stale
Смысл: агент перестал слать heartbeat. Вероятно оффлайн.
Кто присваивает — sweep MarkStaleHosts (из active и pending):
UPDATE host_statuses hs SET status = 'stale', updated_at = NOW()
FROM hosts h
WHERE hs.host_id = h.id
AND hs.status IN ('active', 'pending')
AND hs.last_heartbeat_at < NOW() - INTERVAL '90 seconds';
Вход из pending тоже — оператор отличает «отключился» от «завис на применении».
Переходы:
- →
active— агент переподключился (UpdateHostConnectedв Hello). - →
degraded— sweepMarkDegradedHosts, еслиdesired_generation > last_confirmed_generationи прошло > 15 мин с gen-bump.
Поведение: входит в матчинг (оффлайн-хост всё равно матчит политики); cascade
инкрементит desired_generation и пишет бандл в БД — при переподключении агент
получит актуальный бандл через catch-up.
- Heartbeat-интервал агента — 30 секунд; порог stale — 90 секунд (3 пропуска).
pending
Смысл: агент живой (heartbeat есть), но не подтвердил применение нового бандла в течение 5 минут.
Кто присваивает — sweep MarkPendingHosts (только живой агент):
UPDATE host_statuses SET status = 'pending', updated_at = NOW()
WHERE status = 'active'
AND desired_generation > last_confirmed_generation
AND desired_generation_bumped_at < NOW() - INTERVAL '5 minutes'
AND last_heartbeat_at >= NOW() - INTERVAL '90 seconds';
Таймер — desired_generation_bumped_at (сбрасывается при каждом gen-bump), не
last_confirmed_at: idle-агент не переходит в pending мгновенно при новом изменении.
Guard last_heartbeat_at >= NOW()-90s держит в pending только живого агента —
оффлайн-хост с неподтверждёнными изменениями идёт в stale.
Переходы:
- →
active— sweepMarkRecoveredHosts(см. ниже). - →
stale— sweepMarkStaleHosts, если heartbeat протух. - →
degraded— sweepMarkDegradedHostsпосле 15-мин dwell (больше, чем 5-мин порогpending, — ступень остаётся наблюдаемой).
Поведение: входит в матчинг; cascade пушит бандлы.
degraded
Смысл: хост не применяет бандлы. Либо давно оффлайн, либо применил, но не подтвердил. Требует внимания оператора.
Кто присваивает — sweep MarkDegradedHosts (из stale и pending):
UPDATE host_statuses hs SET status = 'degraded', updated_at = NOW()
FROM hosts h
WHERE hs.host_id = h.id
AND hs.status IN ('stale', 'pending')
AND hs.desired_generation > hs.last_confirmed_generation
AND hs.desired_generation_bumped_at < NOW() - INTERVAL '15 minutes';
Dwell 15 мин (против 5 мин у pending) даёт ступенчатый алертинг: pending = warning,
degraded = critical. Запрос возвращает перешедшие хосты — sweeper эмитит SIEM-событие
на каждый.
Переходы:
- →
active— sweepMarkRecoveredHostsили переподключение агента (UpdateHostConnected).
Поведение: входит в матчинг; cascade пушит бандлы — при reconnect деградированный агент получит актуальный бандл.
needs_rebootstrap
Смысл: хосту нужна полная переинициализация PKI. Текущие серты отозваны или скомпрометированы; агент не переподключится без нового enrollment с новым токеном.
Кто присваивает: только PATCH /hosts/{id} с {"status":"needs_rebootstrap"} —
ручное действие оператора.
Выход: оператор выдаёт новый enrollment-токен → агент останавливается, PKI
очищается (/etc/qf/pki/), agent.conf обновляется с новым токеном → старт →
enrollment RPC → UpsertBulkHost → enrolling → active.
Поведение: автоматических переходов нет; ResolveHosts включает в матчинг (нет
явного исключения). На практике агент оффлайн, пока PKI не восстановлен — бандл
копится в desired-генерации и применится при переподключении.
revoked
Смысл: хост намеренно отозван. Агент не подключится — серт revoked в PKI, gRPC Hello отвергается. Финальное состояние.
Кто присваивает: только PATCH /hosts/{id} с {"status":"revoked"}.
Поведение:
ResolveHostsпропускаетrevoked— политики не применяются, бандл не пушится.RevocationCheckerдержит список отозванных серийников в памяти и проверяет при каждом gRPC-соединении.- Ротация серта отклоняется, если старый серт в revoked-списке.
Sweep — сводная таблица
Sweep-горутина в CP, тикер каждые 30 секунд. Порядок шагов важен:
| Шаг | Запрос | Из | В | Условие |
|---|---|---|---|---|
| 1 | MarkRecoveredHosts | pending, degraded | active | desired ≤ current AND heartbeat < 90s |
| 2 | MarkStaleHosts | active, pending | stale | heartbeat > 90s |
| 3 | MarkPendingHosts | active | pending | desired > confirmed AND gen-bump > 5 мин AND heartbeat < 90s |
| 4 | MarkDegradedHosts | stale, pending | degraded | desired > confirmed AND gen-bump > 15 мин |
Порядок 1→2→3→4 критичен: recovery идёт первым, иначе живой хост мог бы за один тик пройти degraded → recovered → снова stale. Recovery первым гарантирует, что живые хосты не переходят в более тяжёлый статус в том же тике.
Generation tracking
Определяют статус четыре поля в host_statuses:
| Поле | Кто обновляет | Когда |
|---|---|---|
desired_generation | cascade (IncrementHostDesiredGeneration) | любое изменение политик/лейблов |
current_generation | agentsrv по heartbeat (UpdateHostGeneration) | каждый heartbeat (ack BundleApplied) |
last_confirmed_generation | agentsrv (UpdateHostConfirmedGeneration) | агент явно подтвердил применение бандла |
last_confirmed_at | то же | таймстамп последнего подтверждения |
Recovery из pending/degraded гейтится на current_generation (desired ≤ current),
а не на last_confirmed_generation: current — ack применения бандла, двигается каждым
heartbeat; last_confirmed — базлайн аттестации и может отставать на генерацию.
Подтверждение приходит двумя путями:
- Watchdog reconnect (
OnConnectedв агенте) — при каждом переподключении к CP. - Watchdog auto-confirm (
check()в агенте) — если агент подключён непрерывно и бандл живёт дольшеwatchdogTimeout(60с).
Живой heartbeat в UI (host-liveness lane, B-38)
last_heartbeat_at («Last seen» в списке хостов и карточке) обновляется в интерфейсе
сам, без ручного refresh и без включённого auto-refresh, через выделенный
низкочастотный WS-канал host-liveness.
Почему отдельный канал: одиночный heartbeat (каждые ~30 c) осознанно не двигает
updated_at основного hosts-топика (P23-BUG-06 — иначе счётчик хостов на дашборде
осциллирует при рассинхроне часов между подами) и исключён из fingerprint основного
delta-стрима (иначе — поток NOTIFY/delta от сотен хостов). Поэтому liveness идёт через отдельный
throttled-эмиттер, который раз в QF_LIVENESS_INTERVAL (дефолт 15 c) шлёт один
коалесцированный кадр на тенант с узкой проекцией {id, status, last_heartbeat_at, current_generation}. Клиент мержит его в строку хоста по id (не затирая
labels/interfaces/metrics) и newest-wins по last_heartbeat_at.
| Параметр | Дефолт | Назначение |
|---|---|---|
QF_LIVENESS_INTERVAL | 15s | Период эмиттера host-liveness. Меньше — «живее», но больше кадров; ноль NOTIFY (кадр идёт in-process livepoll→hub). |
Наблюдаемость: метрики qf_liveness_delta_sent_total (кадров, ≈ тенанты/интервал) и
qf_liveness_rows_coalesced_total (хостов в кадрах) — rate первой, застрявший около
тенанты / интервал, подтверждает, что канал не перегружен.
Семантика свежести: last_heartbeat_at — это серверное время приёма beat
(time.Now() в момент приёма), а не время отправки агентом. То есть «свежесть» = «когда
CP слышал агента». Итоговая задержка UI ≈ флаш батчера heartbeat (≤10 c) + интервал
эмиттера.
Что игнорирует ResolveHosts
ResolveHosts в компиляторе политик пропускает хосты в статусе revoked, enrolling и
archived (soft-delete). Все остальные (active, stale, pending, degraded,
needs_rebootstrap) — включены в матчинг: даже оффлайн-хост должен получить актуальный
бандл при переподключении.