chore: publish pxmon v0.2.0
This commit is contained in:
@@ -0,0 +1,518 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const currentVersion = 8
|
||||
|
||||
// AuthMethod describes how SSH authentication is performed.
|
||||
type AuthMethod string
|
||||
|
||||
const (
|
||||
AuthMethodPassword AuthMethod = "password"
|
||||
AuthMethodKey AuthMethod = "key"
|
||||
)
|
||||
|
||||
// TransportMode selects how pxmon reaches the agent HTTP API on a node.
|
||||
type TransportMode string
|
||||
|
||||
const (
|
||||
// TransportDirect is the default: pxmon dials the agent's listen
|
||||
// address over plain TCP from the local machine.
|
||||
TransportDirect TransportMode = "direct"
|
||||
// TransportIPFabric tunnels the agent HTTP call through the existing SSH
|
||||
// connection. Meant for nodes on ipfabric-style networking where the node
|
||||
// has no default outbound route and we must not touch its network config.
|
||||
// The agent is expected to bind to 127.0.0.1 on the node.
|
||||
TransportIPFabric TransportMode = "ipfabric"
|
||||
)
|
||||
|
||||
func normalizeTransport(t TransportMode) TransportMode {
|
||||
switch strings.ToLower(strings.TrimSpace(string(t))) {
|
||||
case "ipfabric", "ip-fabric", "ip_fabric":
|
||||
return TransportIPFabric
|
||||
case "", "direct":
|
||||
return TransportDirect
|
||||
default:
|
||||
return TransportDirect
|
||||
}
|
||||
}
|
||||
|
||||
// Registry is the local inventory of managed clusters (nodes).
|
||||
type Registry struct {
|
||||
Version int `json:"version"`
|
||||
ActiveClusterID string `json:"active_cluster_id,omitempty"`
|
||||
Telegram Telegram `json:"telegram,omitempty"`
|
||||
Locker Locker `json:"locker,omitempty"`
|
||||
Backups BackupConfig `json:"backups,omitempty"`
|
||||
Clusters []Cluster `json:"clusters"`
|
||||
}
|
||||
|
||||
type BackupConfig struct {
|
||||
Targets []BackupTarget `json:"targets,omitempty"`
|
||||
Plans []BackupPlan `json:"plans,omitempty"`
|
||||
}
|
||||
|
||||
type BackupTarget struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // sftp|s3
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
SFTPHost string `json:"sftp_host,omitempty"`
|
||||
SFTPPort int `json:"sftp_port,omitempty"`
|
||||
SFTPUser string `json:"sftp_user,omitempty"`
|
||||
SFTPPassword string `json:"sftp_password,omitempty"`
|
||||
SFTPKeyPath string `json:"sftp_key_path,omitempty"`
|
||||
SFTPBasePath string `json:"sftp_base_path,omitempty"`
|
||||
|
||||
S3Endpoint string `json:"s3_endpoint,omitempty"`
|
||||
S3Region string `json:"s3_region,omitempty"`
|
||||
S3Bucket string `json:"s3_bucket,omitempty"`
|
||||
S3Prefix string `json:"s3_prefix,omitempty"`
|
||||
S3AccessKey string `json:"s3_access_key,omitempty"`
|
||||
S3SecretKey string `json:"s3_secret_key,omitempty"`
|
||||
S3UseSSL bool `json:"s3_use_ssl,omitempty"`
|
||||
S3PathStyle bool `json:"s3_path_style,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type BackupPlan struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Cluster string `json:"cluster,omitempty"`
|
||||
TargetID string `json:"target_id"`
|
||||
Paths []string `json:"paths"`
|
||||
Every string `json:"every,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RetainDays int `json:"retain_days,omitempty"`
|
||||
Compress bool `json:"compress"`
|
||||
LastRunAt time.Time `json:"last_run_at,omitempty"`
|
||||
LastStatus string `json:"last_status,omitempty"`
|
||||
LastArchive string `json:"last_archive,omitempty"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Telegram stores Telegram bot integration settings.
|
||||
type Telegram struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
AllowedUserIDs []int64 `json:"allowed_user_ids,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// Locker stores global UI/CLI lock settings.
|
||||
type Locker struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
PasswordHash string `json:"password_hash,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// Cluster describes one managed node that we can reach over SSH.
|
||||
type Cluster struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user"`
|
||||
Transport TransportMode `json:"transport,omitempty"`
|
||||
AuthMethod AuthMethod `json:"auth_method"`
|
||||
Password string `json:"password,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
KeyPassphrase string `json:"key_passphrase,omitempty"`
|
||||
KeyPassphraseFile string `json:"key_passphrase_file,omitempty"`
|
||||
InsecureHostKey bool `json:"insecure_host_key,omitempty"`
|
||||
Alerts AlertPolicy `json:"alerts"`
|
||||
VMAlerts VMAlertPolicy `json:"vm_alerts,omitempty"`
|
||||
AlertRouting AlertRoutingPolicy `json:"alert_routing,omitempty"`
|
||||
RepoTunnel RepoTunnelState `json:"repo_tunnel,omitempty"`
|
||||
RunbookTrigger RunbookTrigger `json:"runbook_trigger,omitempty"`
|
||||
Drift DriftControl `json:"drift,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
KVMTags map[string][]string `json:"kvm_tags,omitempty"`
|
||||
Agent AgentInstall `json:"agent,omitempty"`
|
||||
Software SoftwareInfo `json:"software,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AlertPolicy defines warning thresholds used by CLI monitor mode.
|
||||
type AlertPolicy struct {
|
||||
CPUWarnPercent float64 `json:"cpu_warn_percent"`
|
||||
RAMWarnPercent float64 `json:"ram_warn_percent"`
|
||||
SwapWarnPercent float64 `json:"swap_warn_percent"`
|
||||
DiskWarnPercent float64 `json:"disk_warn_percent"`
|
||||
NetWarnMbps float64 `json:"net_warn_mbps"`
|
||||
NetSustainEnabled bool `json:"net_sustain_enabled,omitempty"`
|
||||
NetSustainIface string `json:"net_sustain_iface,omitempty"`
|
||||
NetSustainInclude []string `json:"net_sustain_include,omitempty"`
|
||||
NetSustainExclude []string `json:"net_sustain_exclude,omitempty"`
|
||||
NetSustainMbps float64 `json:"net_sustain_mbps,omitempty"`
|
||||
NetSustainMinutes int `json:"net_sustain_minutes,omitempty"`
|
||||
NetSustainCooldownMins int `json:"net_sustain_cooldown_mins,omitempty"`
|
||||
}
|
||||
|
||||
// VMAlertPolicy controls KVM VM-state alerting per cluster.
|
||||
type VMAlertPolicy struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
WarnOnShutoff bool `json:"warn_on_shutoff,omitempty"`
|
||||
MinRunning int `json:"min_running,omitempty"`
|
||||
}
|
||||
|
||||
// AlertRoutingPolicy controls delivery behavior for alerts (e.g. Telegram).
|
||||
type AlertRoutingPolicy struct {
|
||||
CriticalImmediate bool `json:"critical_immediate,omitempty"`
|
||||
WarningBatchMins int `json:"warning_batch_mins,omitempty"`
|
||||
}
|
||||
|
||||
// RunbookTrigger controls automatic runbook execution on selected events.
|
||||
type RunbookTrigger struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
OnVMShutoff bool `json:"on_vm_shutoff,omitempty"`
|
||||
RunbookID string `json:"runbook_id,omitempty"`
|
||||
CooldownMins int `json:"cooldown_mins,omitempty"`
|
||||
LastTriggered time.Time `json:"last_triggered,omitempty"`
|
||||
}
|
||||
|
||||
// DriftBaseline stores expected values for drift comparisons.
|
||||
type DriftBaseline struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
SetAt time.Time `json:"set_at,omitempty"`
|
||||
AgentVersion string `json:"agent_version,omitempty"`
|
||||
Software string `json:"software,omitempty"`
|
||||
}
|
||||
|
||||
// DriftControl stores baseline and per-issue acknowledgement windows.
|
||||
type DriftControl struct {
|
||||
Baseline DriftBaseline `json:"baseline,omitempty"`
|
||||
AckUntil map[string]time.Time `json:"ack_until,omitempty"`
|
||||
}
|
||||
|
||||
// AgentInstall describes remote pxmon-agent installation details.
|
||||
type AgentInstall struct {
|
||||
Installed bool `json:"installed"`
|
||||
Version string `json:"version,omitempty"`
|
||||
RemoteBinary string `json:"remote_binary,omitempty"`
|
||||
RemoteConfig string `json:"remote_config,omitempty"`
|
||||
RemoteLog string `json:"remote_log,omitempty"`
|
||||
RemotePIDFile string `json:"remote_pid_file,omitempty"`
|
||||
ListenAddress string `json:"listen_address,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
RequestSecret string `json:"request_secret,omitempty"`
|
||||
TLSEnabled bool `json:"tls_enabled,omitempty"`
|
||||
TLSCertPath string `json:"tls_cert_path,omitempty"`
|
||||
TLSKeyPath string `json:"tls_key_path,omitempty"`
|
||||
TLSFingerprint string `json:"tls_fingerprint,omitempty"`
|
||||
LastBootstrapAt time.Time `json:"last_bootstrap_at,omitempty"`
|
||||
}
|
||||
|
||||
// SoftwareInfo describes discovered software/plugins on a node.
|
||||
type SoftwareInfo struct {
|
||||
DetectedAt time.Time `json:"detected_at,omitempty"`
|
||||
Bird bool `json:"bird,omitempty"`
|
||||
FRR bool `json:"frr,omitempty"`
|
||||
KVM bool `json:"kvm,omitempty"`
|
||||
LXC bool `json:"lxc,omitempty"`
|
||||
LXD bool `json:"lxd,omitempty"`
|
||||
Versions map[string]string `json:"versions,omitempty"`
|
||||
}
|
||||
|
||||
func (s SoftwareInfo) SupportedList() []string {
|
||||
out := make([]string, 0, 5)
|
||||
if s.Bird {
|
||||
out = append(out, "bird")
|
||||
}
|
||||
if s.FRR {
|
||||
out = append(out, "frr")
|
||||
}
|
||||
if s.KVM {
|
||||
out = append(out, "kvm")
|
||||
}
|
||||
if s.LXC {
|
||||
out = append(out, "lxc")
|
||||
}
|
||||
if s.LXD {
|
||||
out = append(out, "lxd")
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s SoftwareInfo) Summary() string {
|
||||
list := s.SupportedList()
|
||||
if len(list) == 0 {
|
||||
if !s.DetectedAt.IsZero() {
|
||||
return "none"
|
||||
}
|
||||
return "-"
|
||||
}
|
||||
return strings.Join(list, ",")
|
||||
}
|
||||
|
||||
func newRegistry() Registry {
|
||||
return Registry{
|
||||
Version: currentVersion,
|
||||
Backups: normalizeBackupConfig(BackupConfig{}),
|
||||
Clusters: []Cluster{},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultAlertPolicy() AlertPolicy {
|
||||
return AlertPolicy{
|
||||
CPUWarnPercent: 85,
|
||||
RAMWarnPercent: 90,
|
||||
SwapWarnPercent: 80,
|
||||
DiskWarnPercent: 90,
|
||||
NetWarnMbps: 300,
|
||||
}
|
||||
}
|
||||
|
||||
func defaultVMAlertPolicy() VMAlertPolicy {
|
||||
return VMAlertPolicy{
|
||||
Enabled: false,
|
||||
WarnOnShutoff: true,
|
||||
MinRunning: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func defaultAlertRoutingPolicy() AlertRoutingPolicy {
|
||||
return AlertRoutingPolicy{
|
||||
CriticalImmediate: true,
|
||||
WarningBatchMins: 5,
|
||||
}
|
||||
}
|
||||
|
||||
func defaultRunbookTrigger() RunbookTrigger {
|
||||
return RunbookTrigger{
|
||||
Enabled: false,
|
||||
OnVMShutoff: true,
|
||||
CooldownMins: 30,
|
||||
}
|
||||
}
|
||||
|
||||
func ensureAlertPolicy(p AlertPolicy) AlertPolicy {
|
||||
d := defaultAlertPolicy()
|
||||
if p.CPUWarnPercent <= 0 {
|
||||
p.CPUWarnPercent = d.CPUWarnPercent
|
||||
}
|
||||
if p.RAMWarnPercent <= 0 {
|
||||
p.RAMWarnPercent = d.RAMWarnPercent
|
||||
}
|
||||
if p.SwapWarnPercent <= 0 {
|
||||
p.SwapWarnPercent = d.SwapWarnPercent
|
||||
}
|
||||
if p.DiskWarnPercent <= 0 {
|
||||
p.DiskWarnPercent = d.DiskWarnPercent
|
||||
}
|
||||
if p.NetWarnMbps <= 0 {
|
||||
p.NetWarnMbps = d.NetWarnMbps
|
||||
}
|
||||
if p.NetSustainEnabled {
|
||||
if p.NetSustainMbps <= 0 {
|
||||
p.NetSustainMbps = d.NetWarnMbps
|
||||
}
|
||||
if p.NetSustainMinutes <= 0 {
|
||||
p.NetSustainMinutes = 60
|
||||
}
|
||||
if p.NetSustainCooldownMins <= 0 {
|
||||
p.NetSustainCooldownMins = 30
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func ensureVMAlertPolicy(p VMAlertPolicy) VMAlertPolicy {
|
||||
d := defaultVMAlertPolicy()
|
||||
wasZero := p == (VMAlertPolicy{})
|
||||
if p.MinRunning <= 0 {
|
||||
p.MinRunning = d.MinRunning
|
||||
}
|
||||
if !p.WarnOnShutoff {
|
||||
// Keep explicit false if user set it, but default to true for zero-value
|
||||
// policy loaded from old configs.
|
||||
if wasZero {
|
||||
p.WarnOnShutoff = d.WarnOnShutoff
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func ensureAlertRoutingPolicy(p AlertRoutingPolicy) AlertRoutingPolicy {
|
||||
d := defaultAlertRoutingPolicy()
|
||||
if p.WarningBatchMins <= 0 {
|
||||
p.WarningBatchMins = d.WarningBatchMins
|
||||
}
|
||||
// default true when unset
|
||||
if !p.CriticalImmediate && p == (AlertRoutingPolicy{}) {
|
||||
p.CriticalImmediate = d.CriticalImmediate
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func ensureRunbookTrigger(p RunbookTrigger) RunbookTrigger {
|
||||
d := defaultRunbookTrigger()
|
||||
if p.CooldownMins <= 0 {
|
||||
p.CooldownMins = d.CooldownMins
|
||||
}
|
||||
if !p.OnVMShutoff && p == (RunbookTrigger{}) {
|
||||
p.OnVMShutoff = d.OnVMShutoff
|
||||
}
|
||||
p.RunbookID = strings.TrimSpace(p.RunbookID)
|
||||
return p
|
||||
}
|
||||
|
||||
func normalizeBackupConfig(cfg BackupConfig) BackupConfig {
|
||||
if cfg.Targets == nil {
|
||||
cfg.Targets = []BackupTarget{}
|
||||
}
|
||||
if cfg.Plans == nil {
|
||||
cfg.Plans = []BackupPlan{}
|
||||
}
|
||||
for i := range cfg.Targets {
|
||||
t := &cfg.Targets[i]
|
||||
t.ID = strings.TrimSpace(t.ID)
|
||||
t.Name = strings.TrimSpace(t.Name)
|
||||
t.Type = strings.ToLower(strings.TrimSpace(t.Type))
|
||||
if t.SFTPPort <= 0 {
|
||||
t.SFTPPort = 22
|
||||
}
|
||||
if t.ID == "" {
|
||||
t.ID = newClusterID()
|
||||
}
|
||||
if t.Name == "" {
|
||||
t.Name = t.ID
|
||||
}
|
||||
if t.Type != "sftp" && t.Type != "s3" {
|
||||
t.Type = "sftp"
|
||||
}
|
||||
if !t.Enabled && t.CreatedAt.IsZero() {
|
||||
t.Enabled = true
|
||||
}
|
||||
if t.CreatedAt.IsZero() {
|
||||
t.CreatedAt = time.Now().UTC()
|
||||
}
|
||||
if t.UpdatedAt.IsZero() {
|
||||
t.UpdatedAt = t.CreatedAt
|
||||
}
|
||||
}
|
||||
for i := range cfg.Plans {
|
||||
p := &cfg.Plans[i]
|
||||
p.ID = strings.TrimSpace(p.ID)
|
||||
p.Name = strings.TrimSpace(p.Name)
|
||||
p.Cluster = strings.TrimSpace(p.Cluster)
|
||||
p.TargetID = strings.TrimSpace(p.TargetID)
|
||||
if p.ID == "" {
|
||||
p.ID = newClusterID()
|
||||
}
|
||||
if p.Name == "" {
|
||||
p.Name = p.ID
|
||||
}
|
||||
if p.Paths == nil {
|
||||
p.Paths = []string{}
|
||||
}
|
||||
if p.Every == "" {
|
||||
p.Every = "24h"
|
||||
}
|
||||
if p.RetainDays <= 0 {
|
||||
p.RetainDays = 30
|
||||
}
|
||||
if !p.Compress {
|
||||
p.Compress = true
|
||||
}
|
||||
if !p.Enabled && p.CreatedAt.IsZero() {
|
||||
p.Enabled = true
|
||||
}
|
||||
if p.CreatedAt.IsZero() {
|
||||
p.CreatedAt = time.Now().UTC()
|
||||
}
|
||||
if p.UpdatedAt.IsZero() {
|
||||
p.UpdatedAt = p.CreatedAt
|
||||
}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func normalizeDriftControl(d DriftControl) DriftControl {
|
||||
if len(d.AckUntil) == 0 {
|
||||
d.AckUntil = nil
|
||||
return d
|
||||
}
|
||||
out := make(map[string]time.Time, len(d.AckUntil))
|
||||
for k, v := range d.AckUntil {
|
||||
n := strings.ToLower(strings.TrimSpace(k))
|
||||
if n == "" || v.IsZero() {
|
||||
continue
|
||||
}
|
||||
out[n] = v.UTC()
|
||||
}
|
||||
if len(out) == 0 {
|
||||
d.AckUntil = nil
|
||||
} else {
|
||||
d.AckUntil = out
|
||||
}
|
||||
d.Baseline.AgentVersion = strings.TrimSpace(d.Baseline.AgentVersion)
|
||||
d.Baseline.Software = strings.TrimSpace(d.Baseline.Software)
|
||||
return d
|
||||
}
|
||||
|
||||
func normalizeTagList(tags []string) []string {
|
||||
if len(tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(tags))
|
||||
out := make([]string, 0, len(tags))
|
||||
for _, t := range tags {
|
||||
n := strings.ToLower(strings.TrimSpace(t))
|
||||
if n == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[n]; ok {
|
||||
continue
|
||||
}
|
||||
seen[n] = struct{}{}
|
||||
out = append(out, n)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeVMTagMap(m map[string][]string) map[string][]string {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string][]string, len(m))
|
||||
for vm, tags := range m {
|
||||
vmName := strings.TrimSpace(vm)
|
||||
if vmName == "" {
|
||||
continue
|
||||
}
|
||||
norm := normalizeTagList(tags)
|
||||
if len(norm) == 0 {
|
||||
continue
|
||||
}
|
||||
out[vmName] = norm
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeLocker(cfg Locker) Locker {
|
||||
cfg.PasswordHash = strings.TrimSpace(cfg.PasswordHash)
|
||||
if cfg.PasswordHash == "" {
|
||||
cfg.Enabled = false
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
Reference in New Issue
Block a user