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