Перейти к основному содержимому

Статусы хоста — полное описание

Статус хоста хранится в таблице 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).
  • MarkRecoveredHosts sweep — из pending/degraded.
  • PATCH /hosts/{id} с {"status":"active"} — ручное восстановление.

Переходы:

  • stale — sweep MarkStaleHosts, если last_heartbeat_at < NOW() - 90s.
  • pending — sweep MarkPendingHosts, если 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 — sweep MarkDegradedHosts, если 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 — sweep MarkRecoveredHosts (см. ниже).
  • stale — sweep MarkStaleHosts, если heartbeat протух.
  • degraded — sweep MarkDegradedHosts после 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 — sweep MarkRecoveredHosts или переподключение агента (UpdateHostConnected).

Поведение: входит в матчинг; cascade пушит бандлы — при reconnect деградированный агент получит актуальный бандл.


needs_rebootstrap

Смысл: хосту нужна полная переинициализация PKI. Текущие серты отозваны или скомпрометированы; агент не переподключится без нового enrollment с новым токеном.

Кто присваивает: только PATCH /hosts/{id} с {"status":"needs_rebootstrap"} — ручное действие оператора.

Выход: оператор выдаёт новый enrollment-токен → агент останавливается, PKI очищается (/etc/qf/pki/), agent.conf обновляется с новым токеном → старт → enrollment RPC → UpsertBulkHostenrollingactive.

Поведение: автоматических переходов нет; ResolveHosts включает в матчинг (нет явного исключения). На практике агент оффлайн, пока PKI не восстановлен — бандл копится в desired-генерации и применится при переподключении.


revoked

Смысл: хост намеренно отозван. Агент не подключится — серт revoked в PKI, gRPC Hello отвергается. Финальное состояние.

Кто присваивает: только PATCH /hosts/{id} с {"status":"revoked"}.

Поведение:

  • ResolveHosts пропускает revoked — политики не применяются, бандл не пушится.
  • RevocationChecker держит список отозванных серийников в памяти и проверяет при каждом gRPC-соединении.
  • Ротация серта отклоняется, если старый серт в revoked-списке.

Sweep — сводная таблица

Sweep-горутина в CP, тикер каждые 30 секунд. Порядок шагов важен:

ШагЗапросИзВУсловие
1MarkRecoveredHostspending, degradedactivedesired ≤ current AND heartbeat < 90s
2MarkStaleHostsactive, pendingstaleheartbeat > 90s
3MarkPendingHostsactivependingdesired > confirmed AND gen-bump > 5 мин AND heartbeat < 90s
4MarkDegradedHostsstale, pendingdegradeddesired > confirmed AND gen-bump > 15 мин

Порядок 1→2→3→4 критичен: recovery идёт первым, иначе живой хост мог бы за один тик пройти degraded → recovered → снова stale. Recovery первым гарантирует, что живые хосты не переходят в более тяжёлый статус в том же тике.


Generation tracking

Определяют статус четыре поля в host_statuses:

ПолеКто обновляетКогда
desired_generationcascade (IncrementHostDesiredGeneration)любое изменение политик/лейблов
current_generationagentsrv по heartbeat (UpdateHostGeneration)каждый heartbeat (ack BundleApplied)
last_confirmed_generationagentsrv (UpdateHostConfirmedGeneration)агент явно подтвердил применение бандла
last_confirmed_atто жетаймстамп последнего подтверждения

Recovery из pending/degraded гейтится на current_generation (desired ≤ current), а не на last_confirmed_generation: current — ack применения бандла, двигается каждым heartbeat; last_confirmed — базлайн аттестации и может отставать на генерацию.

Подтверждение приходит двумя путями:

  1. Watchdog reconnect (OnConnected в агенте) — при каждом переподключении к CP.
  2. 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_INTERVAL15sПериод эмиттера 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) — включены в матчинг: даже оффлайн-хост должен получить актуальный бандл при переподключении.