Netpulse_SasS/server/internal/grpcapi/ncm_dispatch.go
byrsapty 3816e2cfcf Етап 7: збір конфігів запрацював наскрізно
Профілі були даними без виконавця. Тепер ланцюг замкнено: черга в БД →
диспетчер у AgentService → зонд по SSH/Telnet → вивантаження стрімом →
звірка sha256 і дедуплікація.

Агент (internal/ncmx):
- усе побудовано навколо пошуку промпту: у консолі немає ані коду
  завершення, ані довжини відповіді
- промпт шукається лише в хвості 512 байтів, інакше "banner motd #"
  обривав би збір на середині конфігу
- дедлайн на паузу між байтами, а не на всю операцію: збір із шасі
  триває хвилини, а тиша означає завислий пристрій або хибний regex
- ключі SSH обладнання не звіряються свідомо: залізо міняє їх з кожною
  прошивкою, і known_hosts на сотні пристроїв означав би не збирати
  конфіги зовсім

Сервер: черга ncm.jobs із FOR UPDATE SKIP LOCKED (два екземпляри не
надішлють одне завдання двічі), диспетчер, прибирання завислих.

Знайдено живим прогоном:
- сервер ігнорував поле encoding: агент стискав gzip і рахував sha256
  від оригіналу, сервер рахував від стиснених байтів і відхиляв кожен
  бекап. Поле в контракті з Етапу 2, реалізації не було — інтеграційний
  тест користувався "none" і повз дірку проходив
- промпт обрізався не по рядку: шаблон [>#]\s*$ ловить лише символ, і
  в конфізі лишалось ім'я пристрою окремим рядком
- у конфіг потрапляли escape-послідовності bash і подвоєні \r\r\n від
  PTY: 295 байтів / 12 рядків замість 286 / 10. Після виправлення —
  285 / 10, різниця лише у фінальному переводі рядка

Живий прогін проти стенду: success, конфіг 285 байтів, повторний збір —
unchanged без другої версії.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:02:50 +03:00

86 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package grpcapi
import (
"context"
"time"
npv1 "github.com/netpulse/netpulse/gen/go/netpulse/v1"
)
// DispatchConfigJobs роздає завдання збору конфігу живим сесіям.
//
// Опитування таблиці, а не сповіщення: чергу наповнює інший процес
// (REST), і LISTEN/NOTIFY тут дав би доставку «здебільшого» — воно не
// переживає перезапуск слухача. Ціна опитування — один дешевий запит за
// індексом раз на кілька секунд, і лише тоді, коли хоч один зонд
// на зв'язку.
func (s *Service) DispatchConfigJobs(ctx context.Context, every time.Duration) {
if every <= 0 {
every = 5 * time.Second
}
t := time.NewTicker(every)
defer t.Stop()
// Зависле завдання повертається у відмову за десять хвилин: зонд міг
// зникнути разом із ним, і без цього хост лишився б без бекапів
// назавжди.
reap := time.NewTicker(2 * time.Minute)
defer reap.Stop()
s.log.Info("диспетчер збору конфігів запущено", "інтервал", every)
for {
select {
case <-ctx.Done():
return
case <-reap.C:
if n, err := s.store.ReapStuckJobs(ctx, 10*time.Minute); err != nil {
s.log.Warn("прибирання завислих завдань", "err", err)
} else if n > 0 {
s.log.Warn("завдання збору зависли й позначені як невдалі", "рядків", n)
}
case <-t.C:
online := s.onlineAgentIDs()
if len(online) == 0 {
continue
}
jobs, err := s.store.ClaimConfigJobs(ctx, online, 16, s.ring)
if err != nil {
s.log.Error("вибірка завдань збору", "err", err)
continue
}
for _, j := range jobs {
ok := s.PushToAgent(j.AgentID, &npv1.ControlDown{
Payload: &npv1.ControlDown_ConfigJob{ConfigJob: j.Job},
})
if !ok {
// Сесія обірвалась між вибіркою й відправкою.
// Повертаємо в чергу, а не втрачаємо: наступний тік
// віддасть завдання, щойно зонд повернеться.
_ = s.store.FinishConfigJob(ctx, j.JobID, "failed",
"зонд відключився до надсилання завдання", "")
continue
}
s.log.Info("завдання збору надіслано",
"job_id", j.JobID, "agent", j.AgentID,
"device", j.Job.GetDevice().GetName())
}
}
}
}
// onlineAgentIDs — хто зараз на зв'язку.
func (s *Service) onlineAgentIDs() []string {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]string, 0, len(s.sessions))
for id := range s.sessions {
out = append(out, id)
}
return out
}