Docker check playbooks added

This commit is contained in:
Magnus Root 2026-02-26 12:32:26 +03:00
parent ff1a3528c0
commit efb980d47f
3 changed files with 520 additions and 3 deletions

View file

@ -56,6 +56,7 @@ var (
cfgNginxConfigGlob = "/home/install/conf/ptaf-nginx/*/*/*/*.conf"
cfgStateFilePath = "/tmp/port_checker_state.json"
cfgServiceStateFilePath = "/tmp/auspex_service_state.json"
cfgDockerStateFilePath = "/tmp/auspex_docker_state.json"
)
// --- Параметры проверки портов ---
@ -183,6 +184,9 @@ func applyConfig(env map[string]string) {
if v, ok := env["SERVICE_STATE_FILE_PATH"]; ok && v != "" {
cfgServiceStateFilePath = v
}
if v, ok := env["DOCKER_STATE_FILE_PATH"]; ok && v != "" {
cfgDockerStateFilePath = v
}
if v, ok := env["LOG_DIR"]; ok && v != "" {
cfgLogDir = v
}

499
dockercheck.go Normal file
View file

@ -0,0 +1,499 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"strings"
"time"
)
// ── Типы ─────────────────────────────────────────────────────────────────────
type dcContainerInfo struct {
ID string
Name string
Status string // running, exited, dead, restarting, paused, created
Health string // healthy, unhealthy, starting, none
Image string
}
type dcStateFile struct {
Timestamp string `json:"timestamp"`
Containers map[string]dcContainerState `json:"containers"` // name → state
}
type dcContainerState struct {
Status string `json:"status"`
Health string `json:"health"`
}
type dcChangeKind string
const (
dcKindStatusChanged dcChangeKind = "status_changed"
dcKindHealthChanged dcChangeKind = "health_changed"
dcKindDisappeared dcChangeKind = "disappeared"
)
type dcStateChange struct {
Name string
Kind dcChangeKind
PrevState dcContainerState
CurrState dcContainerState
Image string
}
func (ch dcStateChange) isProblem() bool {
switch ch.Kind {
case dcKindDisappeared:
return true
case dcKindStatusChanged:
return ch.CurrState.Status != "running"
case dcKindHealthChanged:
return ch.CurrState.Health == "unhealthy"
}
return false
}
func (ch dcStateChange) isRecovery() bool {
switch ch.Kind {
case dcKindStatusChanged:
return ch.CurrState.Status == "running" && ch.PrevState.Status != "running"
case dcKindHealthChanged:
return ch.CurrState.Health == "healthy" && ch.PrevState.Health == "unhealthy"
}
return false
}
// ── Точка входа ───────────────────────────────────────────────────────────────
func runDockerCheck() error {
hostname, err := os.Hostname()
if err != nil {
hostname = "unknown"
log.Printf("⚠️ Не удалось получить hostname: %v", err)
}
log.Printf("🔍 DockerCheck запущен на хосте: %s", hostname)
// Проверяем доступность docker
if err := dcCheckDockerAvailable(); err != nil {
return fmt.Errorf("docker недоступен: %v", err)
}
// Получаем текущий список контейнеров
containers, err := dcGetContainers()
if err != nil {
return fmt.Errorf("ошибка получения списка контейнеров: %v", err)
}
if len(containers) == 0 {
log.Println(" Запущенных контейнеров не найдено")
}
log.Printf("📦 Найдено контейнеров: %d", len(containers))
// Выводим текущее состояние
for _, c := range containers {
emoji := dcStatusEmoji(c.Status, c.Health)
if c.Health != "" && c.Health != "none" {
log.Printf("%s %-30s | %-12s | health: %s | %s", emoji, c.Name, c.Status, c.Health, c.Image)
} else {
log.Printf("%s %-30s | %-12s | %s", emoji, c.Name, c.Status, c.Image)
}
}
// Загружаем предыдущее состояние
prevState := dcLoadState()
// Строим map текущего состояния
current := make(map[string]dcContainerInfo, len(containers))
for _, c := range containers {
current[c.Name] = c
}
// Сравниваем
changes := dcCompareStates(prevState.Containers, current)
// Статистика
runningCount := 0
problemCount := 0
for _, c := range containers {
if c.Status == "running" && c.Health != "unhealthy" {
runningCount++
} else {
problemCount++
}
}
log.Println("\n📊 Итоги проверки контейнеров:")
log.Printf(" ✅ Работают нормально: %d", runningCount)
log.Printf(" ❌ Проблемных: %d", problemCount)
log.Printf(" 🔄 Изменений статуса: %d", len(changes))
if len(changes) > 0 {
log.Println("\n🔄 Изменения с последней проверки:")
log.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
for _, ch := range changes {
arrow := dcChangeArrow(ch)
switch ch.Kind {
case dcKindDisappeared:
log.Printf(" %s %-30s | исчез (был: %s)", arrow, ch.Name, ch.PrevState.Status)
case dcKindStatusChanged:
log.Printf(" %s %-30s | %s → %s", arrow, ch.Name, ch.PrevState.Status, ch.CurrState.Status)
case dcKindHealthChanged:
log.Printf(" %s %-30s | health: %s → %s", arrow, ch.Name, ch.PrevState.Health, ch.CurrState.Health)
}
}
log.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
if isTelegramConfigured() {
dcSendAlert(hostname, changes)
} else {
log.Println("\n Telegram уведомления отключены (не настроены данные)")
}
} else {
if len(prevState.Containers) == 0 {
log.Println("\n Первый запуск — состояние сохранено, уведомления при изменениях")
} else {
log.Println("\n✅ Изменений нет — уведомления не отправляются")
}
}
// Сохраняем текущее состояние
dcSaveState(current)
log.Println("=== Проверка контейнеров завершена ===")
return nil
}
// ── Получение контейнеров ────────────────────────────────────────────────────
func dcCheckDockerAvailable() error {
err := exec.Command("docker", "info").Run()
if err != nil {
return fmt.Errorf("docker info завершился с ошибкой: %v", err)
}
return nil
}
func dcGetContainers() ([]dcContainerInfo, error) {
// Получаем все контейнеры (включая не-running) через Go-шаблон
// Поля: ID, Name, Status, Health, Image — разделитель |
out, err := exec.Command(
"docker", "ps", "-a",
"--format", "{{.ID}}|{{.Names}}|{{.Status}}|{{.Health}}|{{.Image}}",
).Output()
if err != nil {
return nil, fmt.Errorf("docker ps завершился с ошибкой: %v", err)
}
var containers []dcContainerInfo
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if line == "" {
continue
}
parts := strings.SplitN(line, "|", 5)
if len(parts) != 5 {
continue
}
c := dcContainerInfo{
ID: strings.TrimSpace(parts[0]),
Name: strings.TrimSpace(parts[1]),
Image: strings.TrimSpace(parts[4]),
Health: strings.ToLower(strings.TrimSpace(parts[3])),
}
// Status из docker ps выглядит как "Up 2 hours" или "Exited (1) 3 minutes ago"
// Нормализуем до короткого статуса
c.Status = dcNormalizeStatus(parts[2])
containers = append(containers, c)
}
return containers, nil
}
// dcNormalizeStatus приводит вывод docker ps к короткому статусу
func dcNormalizeStatus(raw string) string {
raw = strings.TrimSpace(raw)
lower := strings.ToLower(raw)
switch {
case strings.HasPrefix(lower, "up"):
return "running"
case strings.HasPrefix(lower, "exited"):
return "exited"
case strings.HasPrefix(lower, "restarting"):
return "restarting"
case strings.HasPrefix(lower, "dead"):
return "dead"
case strings.HasPrefix(lower, "paused"):
return "paused"
case strings.HasPrefix(lower, "created"):
return "created"
case strings.HasPrefix(lower, "removal"):
return "removing"
default:
return lower
}
}
// ── Сравнение состояний ──────────────────────────────────────────────────────
func dcCompareStates(prev map[string]dcContainerState, curr map[string]dcContainerInfo) []dcStateChange {
var changes []dcStateChange
// Проверяем текущие контейнеры
for name, currInfo := range curr {
prevInfo, existed := prev[name]
if !existed {
// Новый контейнер — не алертим, просто добавится в state
continue
}
currState := dcContainerState{Status: currInfo.Status, Health: currInfo.Health}
// Изменение статуса
if prevInfo.Status != currState.Status {
changes = append(changes, dcStateChange{
Name: name,
Kind: dcKindStatusChanged,
PrevState: prevInfo,
CurrState: currState,
Image: currInfo.Image,
})
}
// Изменение health (независимо от статуса)
if prevInfo.Health != currState.Health &&
currState.Health != "" && currState.Health != "none" &&
prevInfo.Health != "" && prevInfo.Health != "none" {
changes = append(changes, dcStateChange{
Name: name,
Kind: dcKindHealthChanged,
PrevState: prevInfo,
CurrState: currState,
Image: currInfo.Image,
})
}
}
// Проверяем исчезнувшие контейнеры (были в state, нет сейчас)
for name, prevInfo := range prev {
if _, exists := curr[name]; !exists {
changes = append(changes, dcStateChange{
Name: name,
Kind: dcKindDisappeared,
PrevState: prevInfo,
CurrState: dcContainerState{},
Image: "",
})
}
}
return changes
}
// ── State-файл ────────────────────────────────────────────────────────────────
func dcLoadState() dcStateFile {
data, err := os.ReadFile(cfgDockerStateFilePath)
if err != nil {
return dcStateFile{Containers: make(map[string]dcContainerState)}
}
var state dcStateFile
if err := json.Unmarshal(data, &state); err != nil {
log.Printf("⚠️ Не удалось прочитать state-файл docker: %v", err)
return dcStateFile{Containers: make(map[string]dcContainerState)}
}
return state
}
func dcSaveState(current map[string]dcContainerInfo) {
containers := make(map[string]dcContainerState, len(current))
for name, info := range current {
containers[name] = dcContainerState{
Status: info.Status,
Health: info.Health,
}
}
state := dcStateFile{
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
Containers: containers,
}
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
log.Printf("⚠️ Ошибка сериализации state-файла docker: %v", err)
return
}
if err := os.WriteFile(cfgDockerStateFilePath, data, 0644); err != nil {
log.Printf("⚠️ Ошибка записи state-файла %s: %v", cfgDockerStateFilePath, err)
}
}
// ── Отправка алерта ──────────────────────────────────────────────────────────
func dcSendAlert(hostname string, changes []dcStateChange) {
var problems, recoveries []dcStateChange
for _, ch := range changes {
if ch.isRecovery() {
recoveries = append(recoveries, ch)
} else if ch.isProblem() {
problems = append(problems, ch)
}
}
// Если нет ни проблем, ни восстановлений — молчим
if len(problems) == 0 && len(recoveries) == 0 {
return
}
// Заголовок по наиболее критичному событию
var header string
switch {
case len(problems) > 0 && len(recoveries) > 0:
header = "⚠️ <b>Изменения в состоянии контейнеров</b>"
case len(problems) > 0:
header = "🚨 <b>АЛЕРТ: Проблемы с контейнерами!</b>"
default:
header = "✅ <b>Контейнеры восстановлены</b>"
}
message := fmt.Sprintf(
"%s\n\n"+
"🖥 <b>Хост:</b> <code>%s</code>\n"+
"⏰ <b>Время:</b> %s\n\n",
header,
hostname,
time.Now().Format("2006-01-02 15:04:05"),
)
// Проблемные контейнеры
if len(problems) > 0 {
message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
message += fmt.Sprintf("❌ <b>Проблемы (%d):</b>\n\n", len(problems))
for _, ch := range problems {
emoji := dcChangeEmoji(ch)
switch ch.Kind {
case dcKindDisappeared:
message += fmt.Sprintf(
"%s <b>%s</b>\n"+
" 📌 Контейнер исчез (был: <code>%s</code>)\n\n",
emoji, ch.Name, ch.PrevState.Status,
)
case dcKindStatusChanged:
message += fmt.Sprintf(
"%s <b>%s</b>\n"+
" 📌 <code>%s</code> → <code>%s</code>\n"+
" 🐳 <code>%s</code>\n"+
" 🔧 <code>docker logs %s --tail 50</code>\n\n",
emoji, ch.Name,
ch.PrevState.Status, ch.CurrState.Status,
ch.Image, ch.Name,
)
case dcKindHealthChanged:
message += fmt.Sprintf(
"%s <b>%s</b>\n"+
" 📌 health: <code>%s</code> → <code>%s</code>\n"+
" 🐳 <code>%s</code>\n"+
" 🔧 <code>docker inspect %s</code>\n\n",
emoji, ch.Name,
ch.PrevState.Health, ch.CurrState.Health,
ch.Image, ch.Name,
)
}
}
}
// Восстановленные контейнеры
if len(recoveries) > 0 {
message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
message += fmt.Sprintf("✅ <b>Восстановились (%d):</b>\n\n", len(recoveries))
for _, ch := range recoveries {
switch ch.Kind {
case dcKindStatusChanged:
message += fmt.Sprintf(
"✅ <b>%s</b>\n"+
" 📌 <code>%s</code> → <code>%s</code>\n\n",
ch.Name, ch.PrevState.Status, ch.CurrState.Status,
)
case dcKindHealthChanged:
message += fmt.Sprintf(
"✅ <b>%s</b>\n"+
" 📌 health: <code>%s</code> → <code>%s</code>\n\n",
ch.Name, ch.PrevState.Health, ch.CurrState.Health,
)
}
}
}
message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
message += "💡 <i>docker ps -a | docker logs &lt;имя&gt; --tail 50</i>"
if err := telegramSendHTML(message); err != nil {
log.Printf("❌ Ошибка отправки в Telegram: %v", err)
} else {
log.Println("✅ Алерт успешно отправлен в Telegram")
}
}
// ── Вспомогательные функции ──────────────────────────────────────────────────
func dcStatusEmoji(status, health string) string {
if health == "unhealthy" {
return "🤒"
}
switch status {
case "running":
return "✅"
case "restarting":
return "🔄"
case "exited":
return "🔴"
case "dead":
return "💀"
case "paused":
return "⏸"
default:
return "❓"
}
}
func dcChangeEmoji(ch dcStateChange) string {
switch ch.Kind {
case dcKindDisappeared:
return "👻"
case dcKindHealthChanged:
return "🤒"
case dcKindStatusChanged:
switch ch.CurrState.Status {
case "restarting":
return "🔄"
case "exited":
return "🔴"
case "dead":
return "💀"
}
}
return "❓"
}
func dcChangeArrow(ch dcStateChange) string {
if ch.isRecovery() {
return "✅"
}
if ch.isProblem() {
return "❌"
}
return "🔄"
}

20
main.go
View file

@ -7,7 +7,7 @@ import (
"strings"
)
const appVersion = "1.1.0"
const appVersion = "1.2.0"
// Цвета терминала (общие для всех модулей)
const (
@ -86,6 +86,13 @@ func runByFlag(flag string, extraArgs []string) {
os.Exit(1)
}
case "-dc", "-d":
fmt.Printf("%s🐳 Auspex — Проверка контейнеров Docker%s\n\n", cCyan, cReset)
if err := runDockerCheck(); err != nil {
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
os.Exit(1)
}
case "-v", "--version":
fmt.Printf("auspex v%s\n", appVersion)
@ -109,9 +116,10 @@ func showMenu() {
fmt.Printf(" %s4%s) %s-m %s Проверка совпадений портов Angie ↔ Docker\n", cGreen, cReset, cCyan, cReset)
fmt.Printf(" %s5%s) %s-l %s Проверка активности логов (PostgreSQL)\n", cGreen, cReset, cCyan, cReset)
fmt.Printf(" %s6%s) %s-sv%s Проверка состояния служб (systemd)\n", cGreen, cReset, cCyan, cReset)
fmt.Printf(" %s7%s) %s-dc%s Проверка контейнеров Docker\n", cGreen, cReset, cCyan, cReset)
fmt.Printf(" %s0%s) Выход\n\n", cGreen, cReset)
fmt.Printf("%sДля быстрого вызова: auspex <ключ>%s\n", cYellow, cReset)
fmt.Printf("%sПример: auspex -ss | auspex -c | auspex -p | auspex -m [клиент] | auspex -l | auspex -sv%s\n\n", cYellow, cReset)
fmt.Printf("%sПример: auspex -ss | auspex -c | auspex -p | auspex -m [клиент] | auspex -l | auspex -sv | auspex -dc%s\n\n", cYellow, cReset)
reader := bufio.NewReader(os.Stdin)
@ -154,11 +162,16 @@ func showMenu() {
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
}
return
case "7", "-dc", "-d", "dc", "d":
if err := runDockerCheck(); err != nil {
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
}
return
case "0", "q", "quit", "exit":
fmt.Println("Выход.")
return
default:
fmt.Printf("%sНеизвестная команда. Введите 1-6 или 0 для выхода.%s\n", cRed, cReset)
fmt.Printf("%sНеизвестная команда. Введите 1-7 или 0 для выхода.%s\n", cRed, cReset)
}
}
}
@ -184,6 +197,7 @@ func printUsage() {
fmt.Printf(" %-12s Проверка совпадений портов Angie ↔ Docker [имя_клиента]\n", "-m")
fmt.Printf(" %-12s Проверка активности логов (PostgreSQL)\n", "-l")
fmt.Printf(" %-12s Проверка состояния служб systemd\n", "-sv")
fmt.Printf(" %-12s Проверка контейнеров Docker\n", "-dc")
fmt.Printf(" %-12s Показать версию\n", "-v")
fmt.Printf(" %-12s Показать эту справку\n\n", "-h")
fmt.Printf("Без аргументов запускается интерактивное меню.\n")