package main import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "regexp" "sort" "strings" ) // ── Типы ───────────────────────────────────────────────────────────────────── type mcStatistics struct { TotalClients, ClientsOK, ClientsWarning, ClientsError int } type mcAlert struct { Hostname, Client, Container, ErrorType, Description string IsCritical bool } type mcContainerCheck struct { Number, PortsOK, Errors, Warnings int Ports []int Running bool } type mcClientCheck struct { Name string HasAngie bool AngiePorts []int Containers []mcContainerCheck Errors, Warnings int } var mcAlerts []mcAlert // ── Точка входа ────────────────────────────────────────────────────────────── func runMatchCheck(clientFilter string) error { mcAlerts = nil hostname, _ := os.Hostname() if hostname == "" { hostname = "unknown" } mcPrintHeader() if _, err := os.Stat(cfgPTAFBaseDir); os.IsNotExist(err) { mcPrintError(fmt.Sprintf("Директория %s не найдена", cfgPTAFBaseDir)) return fmt.Errorf("директория %s не найдена", cfgPTAFBaseDir) } var clients []string if clientFilter != "" { clientPath := filepath.Join(cfgPTAFBaseDir, clientFilter) if _, err := os.Stat(clientPath); os.IsNotExist(err) { mcPrintError(fmt.Sprintf("Клиент %s не найден в %s", clientFilter, cfgPTAFBaseDir)) return fmt.Errorf("клиент %s не найден", clientFilter) } clients = []string{clientFilter} } else { entries, err := os.ReadDir(cfgPTAFBaseDir) if err != nil { return fmt.Errorf("ошибка чтения директории: %v", err) } for _, entry := range entries { if entry.IsDir() { clients = append(clients, entry.Name()) } } sort.Strings(clients) } if len(clients) == 0 { mcPrintWarn(fmt.Sprintf("Клиенты не найдены в %s", cfgPTAFBaseDir)) return nil } fmt.Printf("%sНайдено клиентов: %d%s\n\n", cBlue, len(clients), cReset) stats := &mcStatistics{TotalClients: len(clients)} for _, client := range clients { result := mcCheckClient(client, hostname) if result.Errors == 0 && result.Warnings == 0 { stats.ClientsOK++ } else if result.Errors == 0 { stats.ClientsWarning++ } else { stats.ClientsError++ } } mcPrintSummary(stats) if len(mcAlerts) > 0 && isTelegramConfigured() { mcSendAlerts(hostname, stats, mcAlerts) } if stats.ClientsError > 0 { return fmt.Errorf("обнаружено %d клиентов с ошибками", stats.ClientsError) } return nil } // ── Проверка клиента ───────────────────────────────────────────────────────── func mcCheckClient(client, hostname string) mcClientCheck { result := mcClientCheck{Name: client} mcPrintClientHeader(client) angieConfPath := filepath.Join(cfgAngieConfDir, fmt.Sprintf("angie-ptaf-%s.conf", client)) if _, err := os.Stat(angieConfPath); os.IsNotExist(err) { mcPrintWarn("Angie конфиг не найден") fmt.Println(cYellow + " (возможно клиент отключен или находится на другом instance)" + cReset) fmt.Println() return result } result.HasAngie = true mcPrintOK("✓ Angie конфиг найден") angiePorts, err := mcExtractAngiePorts(angieConfPath) if err != nil { mcPrintWarn(fmt.Sprintf("Ошибка чтения Angie конфига: %v", err)) fmt.Println() return result } if len(angiePorts) == 0 { mcPrintWarn("Не найдено портов в Angie конфиге") fmt.Println() return result } result.AngiePorts = angiePorts fmt.Printf(" Порты в Angie: %d портов - %v\n", len(angiePorts), mcPortsStr(angiePorts)) clientPath := filepath.Join(cfgPTAFBaseDir, client) containers := mcFindContainers(clientPath) fmt.Printf(" Контейнеров на диске: %d\n\n", len(containers)) if len(containers) == 0 { mcPrintWarn("Контейнеры не найдены") fmt.Println() return result } for _, num := range containers { cc := mcCheckContainer(client, num, angiePorts, hostname) result.Containers = append(result.Containers, cc) result.Errors += cc.Errors result.Warnings += cc.Warnings } if result.Errors == 0 && result.Warnings == 0 { fmt.Printf("%s✓ Клиент %s: все проверки пройдены%s\n", cGreen, client, cReset) } else if result.Errors == 0 { fmt.Printf("%s⚠ Клиент %s: %d предупреждений%s\n", cYellow, client, result.Warnings, cReset) } else { fmt.Printf("%s✗ Клиент %s: %d ошибок, %d предупреждений%s\n", cRed, client, result.Errors, result.Warnings, cReset) } fmt.Println() return result } // ── Проверка контейнера ────────────────────────────────────────────────────── func mcCheckContainer(client string, num int, angiePorts []int, hostname string) mcContainerCheck { result := mcContainerCheck{Number: num} fullName := fmt.Sprintf("ptaf_%s_%03d", client, num) label := fmt.Sprintf("контейнер %03d", num) fmt.Printf(" ┌─ Контейнер %03d\n", num) clientPath := filepath.Join(cfgPTAFBaseDir, client) dirName := fmt.Sprintf("%s-ptaf-agent%03d", client, num) containerDir := filepath.Join(clientPath, dirName) composeFile := filepath.Join(containerDir, "docker-compose.yml") portsFile := filepath.Join(containerDir, ".ports.json") // docker-compose.yml if _, err := os.Stat(composeFile); os.IsNotExist(err) { fmt.Printf(" %s│ ✗ docker-compose.yml не найден%s\n", cRed, cReset) fmt.Println(" └─") result.Errors++ mcAlerts = append(mcAlerts, mcAlert{hostname, client, label, "Missing File", "docker-compose.yml не найден", true}) return result } composePorts, err := mcExtractComposePorts(composeFile) if err != nil { fmt.Printf(" %s│ ✗ Ошибка чтения docker-compose.yml: %v%s\n", cRed, err, cReset) fmt.Println(" └─") result.Errors++ mcAlerts = append(mcAlerts, mcAlert{hostname, client, label, "Read Error", fmt.Sprintf("Ошибка чтения: %v", err), true}) return result } if len(composePorts) == 0 { fmt.Printf(" %s│ ✗ Не найдено портов в docker-compose.yml%s\n", cRed, cReset) fmt.Println(" └─") result.Errors++ mcAlerts = append(mcAlerts, mcAlert{hostname, client, label, "No Ports", "Не найдено портов", true}) return result } result.Ports = composePorts fmt.Printf(" │ Порты: %v\n", mcPortsStr(composePorts)) // .ports.json if _, err := os.Stat(portsFile); err == nil { if !mcIsValidJSON(portsFile) { fmt.Printf(" %s│ ⚠ .ports.json некорректный JSON%s\n", cYellow, cReset) result.Warnings++ } } else { fmt.Printf(" %s│ ⚠ .ports.json не найден%s\n", cYellow, cReset) result.Warnings++ } // Совпадение с Angie containerOK := true var missing []int for _, port := range composePorts { if !mcContainsInt(angiePorts, port) { fmt.Printf(" %s│ ✗ Порт %d ОТСУТСТВУЕТ в Angie!%s\n", cRed, port, cReset) result.Errors++ containerOK = false missing = append(missing, port) } } if len(missing) > 0 { mcAlerts = append(mcAlerts, mcAlert{hostname, client, label, "Port Mismatch", fmt.Sprintf("Порты отсутствуют в Angie: %v", missing), true}) } // Проверка запуска if mcIsContainerRunning(fullName) { result.Running = true fmt.Printf(" %s│ ✓ Контейнер запущен%s\n", cGreen, cReset) portsOK := 0 var notListening []int for _, port := range composePorts { if mcIsPortListening(port) { portsOK++ } else { fmt.Printf(" %s│ ✗ Порт %d НЕ слушается%s\n", cRed, port, cReset) result.Errors++ containerOK = false notListening = append(notListening, port) } } if len(notListening) > 0 { mcAlerts = append(mcAlerts, mcAlert{hostname, client, label, "Port Not Listening", fmt.Sprintf("Порты не слушаются: %v", notListening), true}) } result.PortsOK = portsOK if portsOK == len(composePorts) { fmt.Printf(" %s│ ✓ Все %d портов активны%s\n", cGreen, portsOK, cReset) } } else { fmt.Printf(" %s│ ⚠ Контейнер НЕ запущен%s\n", cYellow, cReset) result.Warnings++ containerOK = false mcAlerts = append(mcAlerts, mcAlert{hostname, client, label, "Container Stopped", "Контейнер не запущен", false}) } if containerOK { fmt.Printf(" %s└─ ✓ OK%s\n", cGreen, cReset) } else { fmt.Println(" └─") } fmt.Println() return result } // ── Парсеры ────────────────────────────────────────────────────────────────── func mcExtractAngiePorts(filePath string) ([]int, error) { data, err := os.ReadFile(filePath) if err != nil { return nil, err } re := regexp.MustCompile(`server\s+127\.0\.0\.1:(\d+)`) matches := re.FindAllStringSubmatch(string(data), -1) pm := make(map[int]bool) for _, m := range matches { if len(m) > 1 { var p int fmt.Sscanf(m[1], "%d", &p) pm[p] = true } } ports := make([]int, 0, len(pm)) for p := range pm { ports = append(ports, p) } sort.Ints(ports) return ports, nil } func mcExtractComposePorts(filePath string) ([]int, error) { data, err := os.ReadFile(filePath) if err != nil { return nil, err } re := regexp.MustCompile(`127\.0\.0\.1:(\d+):\d+/tcp`) matches := re.FindAllStringSubmatch(string(data), -1) pm := make(map[int]bool) for _, m := range matches { if len(m) > 1 { var p int fmt.Sscanf(m[1], "%d", &p) pm[p] = true } } ports := make([]int, 0, len(pm)) for p := range pm { ports = append(ports, p) } sort.Ints(ports) return ports, nil } func mcFindContainers(clientPath string) []int { entries, err := os.ReadDir(clientPath) if err != nil { return nil } re := regexp.MustCompile(`-ptaf-agent(\d{3})$`) var containers []int for _, entry := range entries { if !entry.IsDir() { continue } m := re.FindStringSubmatch(entry.Name()) if len(m) > 1 { var num int fmt.Sscanf(m[1], "%d", &num) containers = append(containers, num) } } sort.Ints(containers) return containers } func mcIsValidJSON(filePath string) bool { data, err := os.ReadFile(filePath) if err != nil { return false } var js map[string]interface{} return json.Unmarshal(data, &js) == nil } func mcIsContainerRunning(name string) bool { cmd := exec.Command("docker", "ps", "--format", "{{.Names}}") output, err := cmd.Output() if err != nil { return false } for _, line := range strings.Split(string(output), "\n") { if strings.TrimSpace(line) == name { return true } } return false } func mcIsPortListening(port int) bool { cmd := exec.Command("ss", "-tlnp") output, err := cmd.Output() if err != nil { cmd = exec.Command("netstat", "-tlnp") output, err = cmd.Output() if err != nil { return false } } return strings.Contains(string(output), fmt.Sprintf("127.0.0.1:%d", port)) } func mcContainsInt(slice []int, val int) bool { for _, item := range slice { if item == val { return true } } return false } func mcPortsStr(ports []int) string { strs := make([]string, len(ports)) for i, p := range ports { strs[i] = fmt.Sprintf("%d", p) } return strings.Join(strs, " ") } // ── Вывод ──────────────────────────────────────────────────────────────────── func mcPrintHeader() { fmt.Printf("%s╔════════════════════════════════════════╗\n", cBlue) fmt.Println("║ Проверка портов PTAF (Angie ↔ Docker) ║") fmt.Printf("╚════════════════════════════════════════╝%s\n\n", cReset) } func mcPrintClientHeader(client string) { fmt.Printf("%s==========================================\n", cCyan) fmt.Printf("Клиент: %s\n", client) fmt.Printf("==========================================%s\n\n", cReset) } func mcPrintSummary(stats *mcStatistics) { fmt.Printf("%s╔════════════════════════════════════════╗\n", cBlue) fmt.Println("║ Итоговая статистика ║") fmt.Printf("╚════════════════════════════════════════╝%s\n\n", cReset) fmt.Printf("Всего клиентов проверено: %d\n", stats.TotalClients) fmt.Printf("%s✓ Без проблем: %d%s\n", cGreen, stats.ClientsOK, cReset) if stats.ClientsWarning > 0 { fmt.Printf("%s⚠ С предупреждениями: %d%s\n", cYellow, stats.ClientsWarning, cReset) } if stats.ClientsError > 0 { fmt.Printf("%s✗ С ошибками: %d%s\n", cRed, stats.ClientsError, cReset) } fmt.Println() } func mcPrintOK(msg string) { fmt.Printf("%s%s%s\n", cGreen, msg, cReset) } func mcPrintWarn(msg string) { fmt.Printf("%s⚠ %s%s\n", cYellow, msg, cReset) } func mcPrintError(msg string) { fmt.Printf("%s✗ %s%s\n", cRed, msg, cReset) } // ── Telegram ───────────────────────────────────────────────────────────────── func mcSendAlerts(hostname string, stats *mcStatistics, alertList []mcAlert) { var sb strings.Builder sb.WriteString("🚨 PTAF Docker Ports Check Alert\n\n") sb.WriteString(fmt.Sprintf("🖥 Хост: %s\n", hostname)) sb.WriteString(fmt.Sprintf("📊 Проверено клиентов: %d\n", stats.TotalClients)) sb.WriteString(fmt.Sprintf("✅ Без проблем: %d\n", stats.ClientsOK)) if stats.ClientsWarning > 0 { sb.WriteString(fmt.Sprintf("⚠️ С предупреждениями: %d\n", stats.ClientsWarning)) } if stats.ClientsError > 0 { sb.WriteString(fmt.Sprintf("❌ С ошибками: %d\n", stats.ClientsError)) } sb.WriteString("\n📋 Детали проблем:\n\n") clientAlerts := make(map[string][]mcAlert) for _, a := range alertList { clientAlerts[a.Client] = append(clientAlerts[a.Client], a) } var sortedClients []string for c := range clientAlerts { sortedClients = append(sortedClients, c) } sort.Strings(sortedClients) for _, client := range sortedClients { cas := clientAlerts[client] crit, warn := 0, 0 for _, a := range cas { if a.IsCritical { crit++ } else { warn++ } } if crit > 0 { sb.WriteString(fmt.Sprintf("❌ %s (%d ошибок", client, crit)) if warn > 0 { sb.WriteString(fmt.Sprintf(", %d предупреждений", warn)) } sb.WriteString(")\n") } else { sb.WriteString(fmt.Sprintf("⚠️ %s (%d предупреждений)\n", client, warn)) } for _, a := range cas { icon := "⚠️" if a.IsCritical { icon = "❌" } sb.WriteString(fmt.Sprintf("%s %s: %s - %s\n", icon, a.Container, a.ErrorType, a.Description)) } sb.WriteString("\n") } sb.WriteString("━━━━━━━━━━━━━━━━━━━━━\n") sb.WriteString("🔍 Проверьте должны ли быть данные ресурсы на этом хосте.\n") sb.WriteString(" Если нет, то удалите все старые контейнеры и конфиги для Angie.\n") if err := telegramSendHTML(sb.String()); err != nil { fmt.Printf("%s[Telegram] ✗ Ошибка отправки: %v%s\n", cRed, err, cReset) } else { fmt.Printf("%s[Telegram] ✓ Алерт отправлен (%d проблем)%s\n", cGreen, len(alertList), cReset) } }