Схема tpl.* (шаблон → елементи → прив'язка до хоста), реконсиляція в core.checks, REST, редактор у вебі, чотири вбудовані шаблони SNMP. Елемент шаблону — одна метрика; у чеки вони групуються за (шаблон, тип, інтервал) в один snmp.get. Сотня окремих чеків замість однієї пачки — це сотня SNMP-сесій там, де досить кількох PDU. Позначка template_id у core.checks дає реконсиляції право власності: без неї відв'язування шаблону не знало б, що прибирати. Звірка планів раз на 5 секунд — те, чого бракувало весь час. Чеки міняє REST-процес, живу сесію зонда тримає AgentService; досі будь-яка зміна доїжджала до зонда лише при обриві зв'язку, тобто ніколи. Знайдено живими прогонами й виправлено: - креденшели не їхали разом із планом, і хост, приписаний зонду після його підключення, падав на кожній задачі з «немає SNMP-креденшелів»; - форма хоста відв'язувала зонд: поле починалося порожнім, підпис обіцяв «не змінювати», сервер трактував порожнє буквально; - hrProcessorLoad.1 у базовому шаблоні — здогадка, а не адреса: на net-snmp «No Such Instance». Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
195 lines
6.3 KiB
Go
195 lines
6.3 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"github.com/netpulse/netpulse/server/internal/store"
|
||
)
|
||
|
||
func (s *Server) handleListTemplates(w http.ResponseWriter, r *http.Request, p *Principal) {
|
||
if !requirePerm(w, p, "devices:read") {
|
||
return
|
||
}
|
||
list, err := s.store.ListTemplates(r.Context(), p.TenantID)
|
||
if err != nil {
|
||
s.writeStoreError(w, "шаблони", err)
|
||
return
|
||
}
|
||
if list == nil {
|
||
list = []store.Template{}
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]any{"templates": list})
|
||
}
|
||
|
||
func (s *Server) handleGetTemplate(w http.ResponseWriter, r *http.Request, p *Principal) {
|
||
if !requirePerm(w, p, "devices:read") {
|
||
return
|
||
}
|
||
t, err := s.store.GetTemplate(r.Context(), p.TenantID, r.PathValue("id"))
|
||
if err != nil {
|
||
s.writeStoreError(w, "шаблон", err)
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, t)
|
||
}
|
||
|
||
// handleSaveTemplate створює новий шаблон (POST) або замінює наявний (PUT).
|
||
//
|
||
// Елементи приходять повним списком: форма редагує шаблон як єдиний
|
||
// документ, і часткові оновлення тут лише додали б способів отримати
|
||
// стан, якого людина не просила.
|
||
func (s *Server) handleSaveTemplate(w http.ResponseWriter, r *http.Request, p *Principal) {
|
||
if !requirePerm(w, p, "devices:write") {
|
||
return
|
||
}
|
||
|
||
var in struct {
|
||
Key string `json:"key"`
|
||
Name string `json:"name"`
|
||
Description string `json:"description"`
|
||
Vendor string `json:"vendor"`
|
||
Items []struct {
|
||
Key string `json:"key"`
|
||
Name string `json:"name"`
|
||
CheckType string `json:"check_type"`
|
||
OID string `json:"oid"`
|
||
MetricKey string `json:"metric_key"`
|
||
Unit string `json:"unit"`
|
||
Scale float64 `json:"scale"`
|
||
IntervalSec int `json:"interval_sec"`
|
||
Enabled *bool `json:"enabled"`
|
||
} `json:"items"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||
writeError(w, http.StatusBadRequest, "bad_json", "не вдалося прочитати тіло запиту")
|
||
return
|
||
}
|
||
|
||
in.Key = strings.TrimSpace(in.Key)
|
||
in.Name = strings.TrimSpace(in.Name)
|
||
if in.Key == "" || in.Name == "" {
|
||
writeError(w, http.StatusBadRequest, "bad_request", "ключ і назва обов'язкові")
|
||
return
|
||
}
|
||
|
||
t := store.Template{
|
||
ID: r.PathValue("id"),
|
||
Key: in.Key,
|
||
Name: in.Name,
|
||
Description: strings.TrimSpace(in.Description),
|
||
Vendor: strings.TrimSpace(in.Vendor),
|
||
}
|
||
for _, i := range in.Items {
|
||
oid := strings.TrimSpace(i.OID)
|
||
metric := strings.TrimSpace(i.MetricKey)
|
||
if oid == "" || metric == "" {
|
||
writeError(w, http.StatusBadRequest, "bad_request",
|
||
"кожна метрика потребує OID і ключ метрики")
|
||
return
|
||
}
|
||
// Крапка на початку — те, як OID пишуть у документації вендорів
|
||
// і як його чекає gosnmp. Дописати її самим дешевше, ніж
|
||
// відмовляти людині через символ, який нічого не означає.
|
||
if !strings.HasPrefix(oid, ".") {
|
||
oid = "." + oid
|
||
}
|
||
enabled := true
|
||
if i.Enabled != nil {
|
||
enabled = *i.Enabled
|
||
}
|
||
t.Items = append(t.Items, store.TemplateItem{
|
||
Key: strings.TrimSpace(i.Key),
|
||
Name: strings.TrimSpace(i.Name),
|
||
CheckType: i.CheckType,
|
||
OID: oid,
|
||
MetricKey: metric,
|
||
Unit: strings.TrimSpace(i.Unit),
|
||
Scale: i.Scale,
|
||
IntervalSec: i.IntervalSec,
|
||
Enabled: enabled,
|
||
})
|
||
}
|
||
|
||
id, err := s.store.SaveTemplate(r.Context(), p.TenantID, t)
|
||
if err != nil {
|
||
if errors.Is(err, store.ErrBuiltin) {
|
||
writeError(w, http.StatusForbidden, "builtin",
|
||
"вбудований шаблон не редагується — зробіть копію")
|
||
return
|
||
}
|
||
s.writeStoreError(w, "збереження шаблону", err)
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]any{"id": id})
|
||
}
|
||
|
||
func (s *Server) handleDeleteTemplate(w http.ResponseWriter, r *http.Request, p *Principal) {
|
||
if !requirePerm(w, p, "devices:write") {
|
||
return
|
||
}
|
||
err := s.store.DeleteTemplate(r.Context(), p.TenantID, r.PathValue("id"))
|
||
if err != nil {
|
||
if errors.Is(err, store.ErrBuiltin) {
|
||
writeError(w, http.StatusForbidden, "builtin", "вбудований шаблон не видаляється")
|
||
return
|
||
}
|
||
s.writeStoreError(w, "видалення шаблону", err)
|
||
return
|
||
}
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Прив'язка до хоста
|
||
// ---------------------------------------------------------------------
|
||
|
||
func (s *Server) handleGetDeviceTemplates(w http.ResponseWriter, r *http.Request, p *Principal) {
|
||
if !requirePerm(w, p, "devices:read") {
|
||
return
|
||
}
|
||
deviceID := r.PathValue("id")
|
||
if !p.Scope().CanRead(deviceID) {
|
||
writeError(w, http.StatusForbidden, "forbidden", "немає доступу до цього хоста")
|
||
return
|
||
}
|
||
ids, err := s.store.DeviceTemplateIDs(r.Context(), p.TenantID, deviceID)
|
||
if err != nil {
|
||
s.writeStoreError(w, "шаблони хоста", err)
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]any{"template_ids": ids})
|
||
}
|
||
|
||
func (s *Server) handleSetDeviceTemplates(w http.ResponseWriter, r *http.Request, p *Principal) {
|
||
if !requirePerm(w, p, "devices:write") {
|
||
return
|
||
}
|
||
deviceID := r.PathValue("id")
|
||
if !p.Scope().CanWrite(deviceID) {
|
||
writeError(w, http.StatusForbidden, "forbidden", "немає доступу на запис до цього хоста")
|
||
return
|
||
}
|
||
|
||
var in struct {
|
||
TemplateIDs []string `json:"template_ids"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||
writeError(w, http.StatusBadRequest, "bad_json", "не вдалося прочитати тіло запиту")
|
||
return
|
||
}
|
||
|
||
if err := s.store.SetDeviceTemplates(r.Context(), p.TenantID, deviceID, in.TemplateIDs); err != nil {
|
||
s.writeStoreError(w, "прив'язка шаблонів", err)
|
||
return
|
||
}
|
||
|
||
ids, err := s.store.DeviceTemplateIDs(r.Context(), p.TenantID, deviceID)
|
||
if err != nil {
|
||
s.writeStoreError(w, "шаблони хоста", err)
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]any{"template_ids": ids})
|
||
}
|