package main import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "regexp" "sort" "strings" "sync" ) // ── Типы ───────────────────────────────────────────────────────────────────── 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 := mcBuildContainerName(client, hostname, 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 } // mcBuildContainerName строит имя Docker-контейнера по новому формату: // {ClientTitle}_{AZN}_{prefix}{nodeN}_a{agentN} // // Пример: // // hostname: PTAF-TB-docker-AZ3-HW-node01, client: PSB_CFA, agent: 1 // → PSB_CFA_AZ3_hn01_a001 // // hostname: PTAF-TB-docker-AZ1-node04, client: ITAR-TASS, agent: 1 // → ITAR-TASS_AZ1_n04_a001 func mcBuildContainerName(client, hostname string, agentNum int) string { // Извлекаем AZN: ищем AZ + цифры в hostname azRe := regexp.MustCompile(`AZ(\d+)`) azMatch := azRe.FindStringSubmatch(hostname) azPart := "AZ0" if len(azMatch) > 1 { azPart = "AZ" + azMatch[1] } // Определяем префикс ноды: HW → hn, иначе n nodePrefix := "n" if strings.Contains(hostname, "-HW-") { nodePrefix = "hn" } // Извлекаем номер ноды: цифры после "node" nodeRe := regexp.MustCompile(`node(\d+)`) nodeMatch := nodeRe.FindStringSubmatch(hostname) nodePart := "00" if len(nodeMatch) > 1 { // Берём последние 2 цифры с ведущим нулём num := nodeMatch[1] if len(num) >= 2 { nodePart = num[len(num)-2:] } else { nodePart = fmt.Sprintf("%02s", num) } } return fmt.Sprintf("%s_%s_%s%s_a%03d", client, azPart, nodePrefix, nodePart, agentNum) } // ── Кэш системных вызовов ──────────────────────────────────────────────────── // docker ps и ss вызываются один раз на всё выполнение и кэшируются. // Это критично при большом количестве контейнеров и портов (51+ портов на контейнер). var ( mcRunningContainers map[string]bool // имя контейнера → запущен mcListeningPorts map[int]bool // порт → слушается mcCacheOnce sync.Once ) func mcInitCache() { mcCacheOnce.Do(func() { mcRunningContainers = make(map[string]bool) mcListeningPorts = make(map[int]bool) // Один вызов docker ps — все запущенные контейнеры if out, err := exec.Command("docker", "ps", "--format", "{{.Names}}").Output(); err == nil { for _, line := range strings.Split(string(out), "\n") { name := strings.TrimSpace(line) if name != "" { mcRunningContainers[name] = true } } } // Один вызов ss — все слушающие порты out, err := exec.Command("ss", "-tlnp").Output() if err != nil { out, err = exec.Command("netstat", "-tlnp").Output() } if err == nil { re := regexp.MustCompile(`127\.0\.0\.1:(\d+)`) for _, m := range re.FindAllStringSubmatch(string(out), -1) { if len(m) > 1 { var p int fmt.Sscanf(m[1], "%d", &p) mcListeningPorts[p] = true } } } }) } func mcIsContainerRunning(name string) bool { mcInitCache() return mcRunningContainers[name] } func mcIsPortListening(port int) bool { mcInitCache() return mcListeningPorts[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) } if err := emailSendHTML("Auspex: Несовпадения Angie ↔ Docker", sb.String()); err != nil { fmt.Printf("%s[Email] ✗ Ошибка отправки: %v%s\n", cRed, err, cReset) } else { fmt.Printf("%s[Email] ✓ Алерт отправлен (%d проблем)%s\n", cGreen, len(alertList), cReset) } if err := mattermostSend(sb.String()); err != nil { fmt.Printf("%s[Mattermost] ✗ Ошибка отправки: %v%s\n", cRed, err, cReset) } else { fmt.Printf("%s[Mattermost] ✓ Алерт отправлен (%d проблем)%s\n", cGreen, len(alertList), cReset) } }