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)
}
}