chore: publish pxmon v0.2.0
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RunbookStep struct {
|
||||
Title string `json:"title"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
type Runbook struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Steps []RunbookStep `json:"steps"`
|
||||
BuiltIn bool `json:"built_in,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
func builtinRunbooks() []Runbook {
|
||||
return []Runbook{
|
||||
{
|
||||
ID: "vm-health-check",
|
||||
Name: "VM Health Check",
|
||||
Description: "Quick validation of agent, VM inventory and VM alert policy.",
|
||||
BuiltIn: true,
|
||||
Steps: []RunbookStep{
|
||||
{Title: "Check agent status", Command: "cluster agent status"},
|
||||
{Title: "Check cluster drift", Command: "cluster drift"},
|
||||
{Title: "Check VM states", Command: "cluster alert-vm check"},
|
||||
{Title: "Inspect VM allocations", Command: "kvm top"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "traffic-billing-audit",
|
||||
Name: "Traffic Billing Audit",
|
||||
Description: "Collect interface P95 and graph evidence for billing period.",
|
||||
BuiltIn: true,
|
||||
Steps: []RunbookStep{
|
||||
{Title: "List interfaces", Command: "cluster usage --range 1h"},
|
||||
{Title: "Compute P95 for target iface", Command: "cluster p95 --iface <iface> --range 30d --graph"},
|
||||
{Title: "Export report", Command: "cluster report export --format json --out ./report.json"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) runbookPath() string {
|
||||
return filepath.Join(s.DataDir(), "runbooks", "custom.json")
|
||||
}
|
||||
|
||||
func (s *Service) loadCustomRunbooks() ([]Runbook, error) {
|
||||
path := s.runbookPath()
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []Runbook{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if len(strings.TrimSpace(string(raw))) == 0 {
|
||||
return []Runbook{}, nil
|
||||
}
|
||||
var items []Runbook
|
||||
if err := json.Unmarshal(raw, &items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]Runbook, 0, len(items))
|
||||
for _, rb := range items {
|
||||
rb.ID = strings.TrimSpace(rb.ID)
|
||||
rb.Name = strings.TrimSpace(rb.Name)
|
||||
if rb.ID == "" || rb.Name == "" {
|
||||
continue
|
||||
}
|
||||
rb.BuiltIn = false
|
||||
out = append(out, rb)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Service) saveCustomRunbooks(items []Runbook) error {
|
||||
path := s.runbookPath()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.MarshalIndent(items, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b = append(b, '\n')
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, b, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
func (s *Service) ListRunbooks() ([]Runbook, error) {
|
||||
built := builtinRunbooks()
|
||||
custom, err := s.loadCustomRunbooks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all := append(append([]Runbook{}, built...), custom...)
|
||||
sort.Slice(all, func(i, j int) bool { return strings.ToLower(all[i].ID) < strings.ToLower(all[j].ID) })
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetRunbook(selector string) (Runbook, bool) {
|
||||
selector = strings.TrimSpace(selector)
|
||||
items, err := s.ListRunbooks()
|
||||
if err != nil {
|
||||
return Runbook{}, false
|
||||
}
|
||||
for _, rb := range items {
|
||||
if strings.EqualFold(rb.ID, selector) || strings.EqualFold(rb.Name, selector) {
|
||||
return rb, true
|
||||
}
|
||||
}
|
||||
return Runbook{}, false
|
||||
}
|
||||
|
||||
func (s *Service) AddRunbook(rb Runbook) (Runbook, error) {
|
||||
rb.ID = strings.TrimSpace(rb.ID)
|
||||
rb.Name = strings.TrimSpace(rb.Name)
|
||||
if rb.ID == "" || rb.Name == "" {
|
||||
return Runbook{}, errors.New("runbook id and name are required")
|
||||
}
|
||||
if len(rb.Steps) == 0 {
|
||||
return Runbook{}, errors.New("runbook steps are required")
|
||||
}
|
||||
if b, ok := s.GetRunbook(rb.ID); ok && b.BuiltIn {
|
||||
return Runbook{}, errors.New("cannot overwrite built-in runbook")
|
||||
}
|
||||
custom, err := s.loadCustomRunbooks()
|
||||
if err != nil {
|
||||
return Runbook{}, err
|
||||
}
|
||||
for _, it := range custom {
|
||||
if strings.EqualFold(it.ID, rb.ID) {
|
||||
return Runbook{}, errors.New("runbook id already exists")
|
||||
}
|
||||
}
|
||||
now := s.now().UTC()
|
||||
rb.BuiltIn = false
|
||||
rb.CreatedAt = now
|
||||
rb.UpdatedAt = now
|
||||
custom = append(custom, rb)
|
||||
if err := s.saveCustomRunbooks(custom); err != nil {
|
||||
return Runbook{}, err
|
||||
}
|
||||
_ = s.AppendChange("runbook.add", rb.ID, rb.Name)
|
||||
return rb, nil
|
||||
}
|
||||
|
||||
func (s *Service) RemoveRunbook(selector string) (Runbook, error) {
|
||||
custom, err := s.loadCustomRunbooks()
|
||||
if err != nil {
|
||||
return Runbook{}, err
|
||||
}
|
||||
idx := -1
|
||||
for i, rb := range custom {
|
||||
if strings.EqualFold(rb.ID, selector) || strings.EqualFold(rb.Name, selector) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return Runbook{}, errors.New("custom runbook not found")
|
||||
}
|
||||
removed := custom[idx]
|
||||
custom = append(custom[:idx], custom[idx+1:]...)
|
||||
if err := s.saveCustomRunbooks(custom); err != nil {
|
||||
return Runbook{}, err
|
||||
}
|
||||
_ = s.AppendChange("runbook.remove", removed.ID, removed.Name)
|
||||
return removed, nil
|
||||
}
|
||||
Reference in New Issue
Block a user