511 lines
16 KiB
Go
511 lines
16 KiB
Go
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("🚨 <b>PTAF Docker Ports Check Alert</b>\n\n")
|
||
sb.WriteString(fmt.Sprintf("🖥 <b>Хост:</b> <code>%s</code>\n", hostname))
|
||
sb.WriteString(fmt.Sprintf("📊 <b>Проверено клиентов:</b> %d\n", stats.TotalClients))
|
||
sb.WriteString(fmt.Sprintf("✅ <b>Без проблем:</b> %d\n", stats.ClientsOK))
|
||
if stats.ClientsWarning > 0 {
|
||
sb.WriteString(fmt.Sprintf("⚠️ <b>С предупреждениями:</b> %d\n", stats.ClientsWarning))
|
||
}
|
||
if stats.ClientsError > 0 {
|
||
sb.WriteString(fmt.Sprintf("❌ <b>С ошибками:</b> %d\n", stats.ClientsError))
|
||
}
|
||
|
||
sb.WriteString("\n<b>📋 Детали проблем:</b>\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("❌ <b>%s</b> (%d ошибок", client, crit))
|
||
if warn > 0 {
|
||
sb.WriteString(fmt.Sprintf(", %d предупреждений", warn))
|
||
}
|
||
sb.WriteString(")\n")
|
||
} else {
|
||
sb.WriteString(fmt.Sprintf("⚠️ <b>%s</b> (%d предупреждений)\n", client, warn))
|
||
}
|
||
for _, a := range cas {
|
||
icon := "⚠️"
|
||
if a.IsCritical {
|
||
icon = "❌"
|
||
}
|
||
sb.WriteString(fmt.Sprintf("%s <code>%s</code>: %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)
|
||
}
|
||
}
|