package main import ( "encoding/json" "fmt" "strconv" "strings" "time" ) type MaintenanceWindow struct { Enabled bool `json:"enabled"` Active bool `json:"active"` StartedAt int64 `json:"startedAt"` EndsAt int64 `json:"endsAt"` Reason string `json:"reason"` AllAlerts bool `json:"allAlerts"` ServiceIDs []int64 `json:"serviceIds"` VMIDs []int `json:"vmids"` Automatic bool `json:"automatic"` Source string `json:"source,omitempty"` } func automaticBackupMaintenance(activity ProxmoxActivity, links []ServiceLink, now time.Time) MaintenanceWindow { vmids := []int{} backupRunning := false for _, task := range activity.Tasks { if task.Type != "vzdump" || task.EndTime > 0 || (task.Status != "" && !strings.EqualFold(task.Status, "running")) { continue } backupRunning = true if vmid := backupTaskVMID(task); vmid > 0 { vmids = append(vmids, vmid) } } vmids = uniqueInt(vmids) if !backupRunning { return MaintenanceWindow{} } selected := map[int]bool{} for _, vmid := range vmids { selected[vmid] = true } serviceIDs := []int64{} for _, link := range links { if selected[link.VMID] { serviceIDs = append(serviceIDs, link.ServiceID) } } return MaintenanceWindow{Enabled: true, Active: true, StartedAt: now.Unix(), EndsAt: now.Add(2 * time.Minute).Unix(), Reason: "Автоматически: выполняется backup VM/LXC", VMIDs: vmids, ServiceIDs: uniqueInt64(serviceIDs), AllAlerts: len(vmids) == 0, Automatic: true, Source: "backup"} } func backupTaskVMID(task ProxmoxTask) int { if vmid, err := strconv.Atoi(strings.TrimSpace(task.ID)); err == nil && vmid > 0 { return vmid } parts := strings.Split(task.UPID, ":") for i, part := range parts { if part == "vzdump" && i+1 < len(parts) { vmid, _ := strconv.Atoi(parts[i+1]) return vmid } } return 0 } func (s *Store) MaintenanceWindow() MaintenanceWindow { var value string var window MaintenanceWindow if s.db.QueryRow(`SELECT value FROM settings WHERE key='maintenance_window'`).Scan(&value) == nil { _ = json.Unmarshal([]byte(value), &window) } now := time.Now().Unix() window.Active = window.Enabled && window.StartedAt <= now && window.EndsAt > now if window.Enabled && window.EndsAt <= now { window.Enabled = false data, _ := json.Marshal(window) _, _ = s.db.Exec(`INSERT INTO settings(key,value) VALUES('maintenance_window',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`, string(data)) } return window } func (s *Store) SaveMaintenanceWindow(window MaintenanceWindow) (MaintenanceWindow, error) { now := time.Now() window.Reason = strings.TrimSpace(window.Reason) if window.Reason == "" { return window, fmt.Errorf("укажите причину плановых работ") } if window.EndsAt <= now.Unix() || window.EndsAt > now.Add(30*24*time.Hour).Unix() { return window, fmt.Errorf("окончание должно быть в будущем, максимум через 30 дней") } if !window.AllAlerts && len(window.ServiceIDs) == 0 && len(window.VMIDs) == 0 { return window, fmt.Errorf("выберите сервис, VM/LXC или все алерты") } window.Enabled = true window.Active = true window.StartedAt = now.Unix() window.ServiceIDs = uniqueInt64(window.ServiceIDs) window.VMIDs = uniqueInt(window.VMIDs) data, _ := json.Marshal(window) _, err := s.db.Exec(`INSERT INTO settings(key,value) VALUES('maintenance_window',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`, string(data)) return window, err } func (s *Store) StopMaintenanceWindow() (MaintenanceWindow, error) { window := s.MaintenanceWindow() window.Enabled = false window.Active = false window.EndsAt = time.Now().Unix() data, _ := json.Marshal(window) _, err := s.db.Exec(`INSERT INTO settings(key,value) VALUES('maintenance_window',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`, string(data)) return window, err } func splitMaintenanceAlerts(alerts []Alert, window MaintenanceWindow) (active, planned []Alert) { if !window.Active { return alerts, nil } for _, alert := range alerts { if maintenanceMatchesAlert(window, alert) { planned = append(planned, alert) } else { active = append(active, alert) } } return active, planned } func maintenanceMatchesAlert(window MaintenanceWindow, alert Alert) bool { if window.AllAlerts { return true } for _, id := range window.ServiceIDs { value := strconv.FormatInt(id, 10) if strings.HasPrefix(alert.ID, "service-"+value+"-") || strings.HasPrefix(alert.ID, "service-tls-"+value+"-") { return true } } for _, vmid := range window.VMIDs { value := strconv.Itoa(vmid) if strings.HasSuffix(alert.ID, "-"+value) || strings.Contains(alert.Message, "VMID "+value) { return true } } return false } func uniqueInt64(values []int64) []int64 { seen := map[int64]bool{} result := make([]int64, 0, len(values)) for _, value := range values { if value > 0 && !seen[value] { seen[value] = true result = append(result, value) } } return result } func uniqueInt(values []int) []int { seen := map[int]bool{} result := make([]int, 0, len(values)) for _, value := range values { if value > 0 && !seen[value] { seen[value] = true result = append(result, value) } } return result }