chore: publish pxmon v0.2.0

This commit is contained in:
2026-06-16 21:52:10 +04:00
commit 6b703db02b
69 changed files with 25886 additions and 0 deletions
+518
View File
@@ -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
}