147 вбудованих профілів досі жили лише в базі — виправити команди під свою прошивку було ніяк. Тепер є сторінка з пошуком і формою. Вбудований не правиться, а перекривається власною копією з тим самим ключем: вони спільні для всіх кабінетів, і правка під одну прошивку не має міняти їх усім. resolveProfile уже віддавав перевагу тенантському, тож нічого дописувати не довелося. Профіль — це три регулярні вирази й перелік команд, і порожня форма з такими полями не підказує нічого. Тому три робочі заготовки, кнопка «За зразок» на кожному вбудованому й приклад у кожному полі. Вирази компілюються при збереженні: інакше про друкарську помилку дізнаються з бекапу, який завис, чекаючи неіснуючого запрошення. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"github.com/netpulse/netpulse/server/internal/store"
|
||
)
|
||
|
||
// Профілі збору конфігів: чим під'єднатись, що спитати, як упізнати
|
||
// запрошення й що викинути перед порівнянням версій.
|
||
|
||
var validTransports = map[string]bool{"ssh": true, "telnet": true}
|
||
|
||
func (s *Server) handleListNcmProfiles(w http.ResponseWriter, r *http.Request, p *Principal) {
|
||
if !requirePerm(w, p, "ncm:read") {
|
||
return
|
||
}
|
||
list, err := s.store.ListNcmProfiles(r.Context(), p.TenantID)
|
||
if err != nil {
|
||
s.writeStoreError(w, "профілі збору", err)
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]any{"profiles": list})
|
||
}
|
||
|
||
func (s *Server) handleSaveNcmProfile(w http.ResponseWriter, r *http.Request, p *Principal) {
|
||
if !requirePerm(w, p, "ncm:write") {
|
||
return
|
||
}
|
||
var in store.NcmProfile
|
||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||
writeError(w, http.StatusBadRequest, "bad_json", "не вдалося прочитати тіло запиту")
|
||
return
|
||
}
|
||
in.ID = r.PathValue("id")
|
||
in.Name = strings.TrimSpace(in.Name)
|
||
in.Key = strings.TrimSpace(in.Key)
|
||
if in.Key == "" {
|
||
in.Key = slugify(in.Name)
|
||
}
|
||
if in.Key == "" {
|
||
writeError(w, http.StatusBadRequest, "bad_request", "профіль без назви")
|
||
return
|
||
}
|
||
if in.Transport == "" {
|
||
in.Transport = "ssh"
|
||
}
|
||
if !validTransports[in.Transport] {
|
||
writeError(w, http.StatusBadRequest, "bad_transport",
|
||
"конфіги знімаються через ssh або telnet")
|
||
return
|
||
}
|
||
|
||
// Порожні рядки в переліку команд — сліди зайвого Enter у формі, а
|
||
// не команда. Пристрою вони віддадуться як порожній рядок і зіб'ють
|
||
// очікування запрошення.
|
||
in.Commands = nonEmpty(in.Commands)
|
||
in.ScrubPatterns = nonEmpty(in.ScrubPatterns)
|
||
in.RedactPatterns = nonEmpty(in.RedactPatterns)
|
||
|
||
id, err := s.store.SaveNcmProfile(r.Context(), p.TenantID, in)
|
||
if err != nil {
|
||
if isUniqueViolation(err) {
|
||
writeError(w, http.StatusConflict, "duplicate_key",
|
||
"профіль із ключем «"+in.Key+"» у вас уже є")
|
||
return
|
||
}
|
||
s.writeStoreError(w, "профіль збору", err)
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]any{"id": id})
|
||
}
|
||
|
||
func (s *Server) handleDeleteNcmProfile(w http.ResponseWriter, r *http.Request, p *Principal) {
|
||
if !requirePerm(w, p, "ncm:write") {
|
||
return
|
||
}
|
||
err := s.store.DeleteNcmProfile(r.Context(), p.TenantID, r.PathValue("id"))
|
||
if errors.Is(err, store.ErrBuiltin) {
|
||
writeError(w, http.StatusForbidden, "builtin",
|
||
"вбудований профіль спільний для всіх — його не видаляють, а перекривають своїм")
|
||
return
|
||
}
|
||
if err != nil {
|
||
s.writeStoreError(w, "профіль збору", err)
|
||
return
|
||
}
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
|
||
func nonEmpty(in []string) []string {
|
||
out := make([]string, 0, len(in))
|
||
for _, x := range in {
|
||
if v := strings.TrimSpace(x); v != "" {
|
||
out = append(out, v)
|
||
}
|
||
}
|
||
return out
|
||
}
|