Files
UpdateHaproxyConfig/readme.md
T
2026-10-13 11:05:58 +00:00

23 KiB
Raw Blame History

Документация проекта: Развертывание резервной конфигурации HAProxy по требованию

1. Архитектурное описание

Комплекс предназначен для контролируемого применения резервной конфигурации балансировщика HAProxy по клику оператора с рабочей станции Windows.

[ Рабочая станция Windows ]
          |
     (Ярлык на рабочем столе)
          |
    deployfile.exe (.NET 4.8)
          |
          |  (1. SSH connection: ed25519)
          |  (2. Генерация status/change.me + status/sha256.hash)
          v
[ Linux-сервер (HAProxy) ]
    /var/lib/depfile/ (Домашний каталог depfile)
          ├── .ssh/authorized_keys
          ├── status/
          │     ├── change.me
          │     └── sha256.hash
          └── deployment/
                └── success.me
          |
     (cron: раз в минуту)
          v
    deploy_haproxy.sh (Bash)
          |-- 1. Валидация SHA-256 суммы файла change.me
          |-- 2. Валидация синтаксиса backup/haproxy.cfg (haproxy -c)
          |-- 3. Подмена /etc/haproxy/haproxy.cfg
          |-- 4. Перезапуск службы haproxy (systemctl restart)
          |-- 5. Удаление каталога status/
          `-- 6. Запись метки в deployment/success.me

Принцип работы

  1. Windows-клиент (deployfile.exe):
  • Разработан на .NET Framework 4.8 без внешних библиотек.
  • Вызывает встроенный клиент Windows OpenSSH (ssh.exe) с автоопределением путей (включая обход WOW64-редиректа Sysnative для 32-битных процессов) и автоматической установкой OpenSSH через PowerShell при его отсутствии.
  • Генерирует случайный текстовый блок, подключается к серверу по SSH под пользователем depfile с использованием ED25519-ключа и записывает данные в /var/lib/depfile/status/change.me.
  • На стороне сервера генерирует файл контрольной суммы /var/lib/depfile/status/sha256.hash.
  1. Серверный обработчик (deploy_haproxy.sh):
  • Выполняется по cron от имени root.
  • Проверяет наличие файла /var/lib/depfile/status/change.me. Если файл отсутствует — завершает работу без нагрузки на систему.
  • Валидирует целостность change.me через sha256sum -c.
  • Выполняет проверку синтаксиса резервного файла (haproxy -c -f /etc/haproxy/backup/haproxy.cfg). При обнаружении ошибок применение отменяется.
  • Копирует резервную конфигурацию в боевой файл /etc/haproxy/haproxy.cfg.
  • Перезапускает службу haproxy.
  • Удаляет временную директорию /var/lib/depfile/status.
  • Фиксирует статус и метку времени в файле /var/lib/depfile/deployment/success.me.

2. Настройка Linux-сервера

2.1. Создание сервисного пользователя и директорий

Подключитесь к серверу с правами root или через sudo:

# 1. Создаем системного пользователя с домашним каталогом /var/lib/depfile
sudo useradd -m -d /var/lib/depfile -s /bin/bash -c "File deploment service" depfile

# 2. Выставляем права доступа на домашнюю директорию
sudo chown -R depfile:depfile /var/lib/depfile
sudo chmod 750 /var/lib/depfile

# 3. Создаем каталог резервных конфигураций HAProxy и копируем текущий рабочий конфиг
sudo mkdir -p /etc/haproxy/backup
sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/backup/haproxy.cfg
sudo chown -R root:root /etc/haproxy/backup
sudo chmod 750 /etc/haproxy/backup

2.2. Подготовка каталога SSH-ключей

Так как домашний каталог пользователя расположен в /var/lib/depfile, файл доверенных ключей находится по пути /var/lib/depfile/.ssh/authorized_keys:

sudo -u depfile mkdir -p /var/lib/depfile/.ssh
sudo -u depfile touch /var/lib/depfile/.ssh/authorized_keys
sudo chmod 700 /var/lib/depfile/.ssh
sudo chmod 600 /var/lib/depfile/.ssh/authorized_keys

2.3. Создание скрипта развертывания

Создайте файл /usr/local/bin/deploy_haproxy.sh:

sudo nano /usr/local/bin/deploy_haproxy.sh

Вставьте следующий код:

#!/usr/bin/env bash
set -Eeuo pipefail

# ================= КОНФИГУРАЦИЯ =================
BASE_DIR="/var/lib/depfile"
STATUS_DIR="${BASE_DIR}/status"
CHANGE_FILE="${STATUS_DIR}/change.me"
HASH_FILE="${STATUS_DIR}/sha256.hash"

DEPLOY_DIR="${BASE_DIR}/deployment"
SUCCESS_FILE="${DEPLOY_DIR}/success.me"

BACKUP_CFG="/etc/haproxy/backup/haproxy.cfg"
TARGET_CFG="/etc/haproxy/haproxy.cfg"
# ===============================================

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

err() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ОШИБКА] $*" >&2
}

# 1. Проверяем наличие триггер-файла change.me
if [ ! -f "${CHANGE_FILE}" ]; then
    exit 0
fi

log "Обнаружен файл ${CHANGE_FILE}. Запуск процедуры развертывания..."

# 2. Проверяем наличие файла с хешем
if [ ! -f "${HASH_FILE}" ]; then
    err "Файл хеша ${HASH_FILE} не найден. Отмена операции."
    exit 1
fi

# 3. Проверяем целостность файла change.me по sha256.hash
log "Проверка контрольной суммы SHA-256..."
if ! (cd "${STATUS_DIR}" && sha256sum -c --status "sha256.hash"); then
    err "Несовпадение контрольной суммы файла ${CHANGE_FILE}!"
    exit 2
fi
log "Контрольная сумма успешно подтверждена."

# 4. Проверяем наличие файла резервной копии конфигурации
if [ ! -f "${BACKUP_CFG}" ]; then
    err "Файл резервной копии не найден: ${BACKUP_CFG}"
    exit 3
fi

# 5. Валидация синтаксиса резервного конфига перед его применением
log "Валидация синтаксиса конфигурации из бэкапа..."
if ! haproxy -c -f "${BACKUP_CFG}" > /dev/null 2>&1; then
    err "Резервная конфигурация содержит синтаксические ошибки. Отмена применения."
    haproxy -c -f "${BACKUP_CFG}" || true
    exit 4
fi
log "Синтаксис резервного конфига корректен."

# 6. Замена рабочей конфигурации
log "Копирование конфигурации в ${TARGET_CFG}..."
cp -f "${BACKUP_CFG}" "${TARGET_CFG}"

# Повторный контрольный тест целевого файла
if ! haproxy -c -f "${TARGET_CFG}" > /dev/null 2>&1; then
    err "Критическая ошибка: целевой файл после копирования не прошел валидацию!"
    exit 5
fi

# 7. Перезапуск службы haproxy
log "Перезапуск службы haproxy..."
if ! systemctl restart haproxy; then
    err "Не удалось перезапустить сервис haproxy!"
    exit 6
fi
log "Сервис haproxy успешно перезапущен."

# 8. Очистка каталога status
log "Удаление директории ${STATUS_DIR}..."
rm -rf "${STATUS_DIR}"

# 9. Формирование каталога deployment и файла success.me
mkdir -p "${DEPLOY_DIR}"
chown depfile:depfile "${DEPLOY_DIR}"
CURRENT_DATETIME=$(date '+%Y-%m-%d %H:%M:%S')

# Перезаписываем файл актуальной датой и временем
echo "deploment success {${CURRENT_DATETIME}}" > "${SUCCESS_FILE}"
chown depfile:depfile "${SUCCESS_FILE}"
log "Зафиксирован статус в ${SUCCESS_FILE}."

log "Процесс успешно завершен."
exit 0

Назначьте права на запуск:

sudo chmod +x /usr/local/bin/deploy_haproxy.sh

2.4. Настройка периодического запуска в cron

Откройте планировщик суперпользователя:

sudo crontab -e

Добавьте задачу запуска раз в минуту:

* * * * * /usr/local/bin/deploy_haproxy.sh >> /var/log/deploy_haproxy.log 2>&1


3. Настройка рабочей станции Windows

3.1. Генерация SSH-ключа

В командной строке (cmd) или PowerShell выполните:

ssh-keygen -t ed25519 -f "D:\sys\C-Sharp\deployfile\key\test_key.key" -N ""

Создаются:

  • test_key.key — закрытый ключ (хранится строго локально).
  • test_key.key.pub — открытый ключ для передачи на сервер.

3.2. Добавление открытого ключа на сервер

Передайте содержимое открытого ключа в authorized_keys пользователя depfile:

type "D:\sys\C-Sharp\deployfile\key\test_key.key.pub" | ssh depfile@10.228.48.110 "cat >> /var/lib/depfile/.ssh/authorized_keys"

(При первом подключении введите пароль пользователя depfile на сервере).

Проверьте беспарольный вход:

ssh -i "D:\sys\C-Sharp\deployfile\key\test_key.key" depfile@10.228.48.110 "whoami && pwd"

Вывод должен вернуть:

depfile
/var/lib/depfile


4. Клиентское приложение C# (.NET Framework 4.8)

4.1. Исходный код (Program.cs)

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace SshStatusUpdater
{
    internal class Program
    {
        // ================= НАСТРОЙКИ ПОДКЛЮЧЕНИЯ =================
        private const string RemoteUser = "depfile";             // Логин пользователя на сервере
        private const string RemoteHost = "10.228.48.110";        // IP-адрес или доменное имя сервера
        private const int RemotePort = 22;                        // Порт SSH-сервера

        // Путь к приватному ключу по умолчанию (если не задан аргументом)
        private const string DefaultKeyPath = @"D:\sys\C-Sharp\deployfile\key\test_key.key";
        // ========================================================

        static int Main(string[] args)
        {
            // 1. Определение пути к ключу (аргумент командной строки либо константа по умолчанию)
            string rawKeyPath = (args.Length > 0 && !string.IsNullOrWhiteSpace(args[0]))
                ? args[0].Trim('"')
                : DefaultKeyPath;

            string keyPath = Environment.ExpandEnvironmentVariables(rawKeyPath);

            if (!File.Exists(keyPath))
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"Ошибка: Файл ключа не найден по указанному пути: {keyPath}");
                Console.ResetColor();
                return 2;
            }

            Console.WriteLine($"Используется ключ: {keyPath}");

            // 2. Проверка доступности OpenSSH клиента (с возможностью автоустановки)
            string sshExecutable = EnsureOpenSshAvailable();
            if (string.IsNullOrEmpty(sshExecutable))
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("\n[ОШИБКА] Клиент OpenSSH не найден в системе и не удалось выполнить его установку.");
                Console.WriteLine("Для работы приложения необходимо установить компонент OpenSSH вручную.");
                Console.WriteLine("Откройте PowerShell от имени Администратора и выполните:");
                Console.WriteLine("  Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0");
                Console.ResetColor();
                return 4;
            }

            // 3. Формирование случайных данных и удаленной команды
            string randomContent = GenerateRandomPayload(linesCount: 5, charsPerLine: 32);

            // Так как домашний каталог /var/lib/depfile, команда выполняется относительно него
            string remoteCommand = "mkdir -p status && cat > status/change.me && (cd status && sha256sum change.me > sha256.hash)";

            string sshArgs = $"-i \"{keyPath}\" -p {RemotePort} -o BatchMode=yes -o StrictHostKeyChecking=accept-new {RemoteUser}@{RemoteHost} \"{remoteCommand}\"";

            ProcessStartInfo psi = new ProcessStartInfo
            {
                FileName = sshExecutable,
                Arguments = sshArgs,
                UseShellExecute = false,
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = true
            };

            Console.WriteLine($"Подключение к {RemoteUser}@{RemoteHost}:{RemotePort}...");

            try
            {
                using (Process proc = new Process { StartInfo = psi })
                {
                    proc.Start();

                    // Передаем сгенерированный текст в STDIN удаленного процесса ssh
                    using (StreamWriter sw = new StreamWriter(proc.StandardInput.BaseStream, new UTF8Encoding(false)))
                    {
                        sw.Write(randomContent);
                    }

                    string stdOut = proc.StandardOutput.ReadToEnd();
                    string stdErr = proc.StandardError.ReadToEnd();

                    proc.WaitForExit();

                    if (proc.ExitCode == 0)
                    {
                        Console.ForegroundColor = ConsoleColor.Green;
                        Console.WriteLine("Файл status/change.me и хеш status/sha256.hash успешно созданы на сервере.");
                        Console.ResetColor();
                        return 0;
                    }
                    else
                    {
                        Console.ForegroundColor = ConsoleColor.Red;
                        Console.WriteLine($"Ошибка выполнения SSH (Код: {proc.ExitCode}):");
                        if (!string.IsNullOrWhiteSpace(stdErr))
                            Console.WriteLine(stdErr.Trim());
                        if (!string.IsNullOrWhiteSpace(stdOut))
                            Console.WriteLine(stdOut.Trim());
                        Console.ResetColor();
                        return proc.ExitCode;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"Критическая ошибка выполнения SSH: {ex.Message}");
                Console.ResetColor();
                return 3;
            }
        }

        private static string EnsureOpenSshAvailable()
        {
            string path = FindSshPath();
            if (!string.IsNullOrEmpty(path))
                return path;

            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("Клиент OpenSSH не обнаружен. Попытка автоматической установки компонента Windows...");
            Console.ResetColor();

            bool installed = TryInstallOpenSsh();
            if (installed)
            {
                path = FindSshPath();
                if (!string.IsNullOrEmpty(path))
                {
                    Console.ForegroundColor = ConsoleColor.Green;
                    Console.WriteLine("Клиент OpenSSH успешно установлен.");
                    Console.ResetColor();
                    return path;
                }
            }

            return null;
        }

        private static string FindSshPath()
        {
            string windir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);

            string[] candidates = new[]
            {
                Path.Combine(windir, "Sysnative", "OpenSSH", "ssh.exe"),
                Path.Combine(windir, "System32", "OpenSSH", "ssh.exe"),
                @"C:\Windows\System32\OpenSSH\ssh.exe"
            };

            foreach (var candidate in candidates)
            {
                if (File.Exists(candidate))
                    return candidate;
            }

            try
            {
                ProcessStartInfo testPsi = new ProcessStartInfo
                {
                    FileName = "ssh",
                    Arguments = "-V",
                    UseShellExecute = false,
                    RedirectStandardError = true,
                    RedirectStandardOutput = true,
                    CreateNoWindow = true
                };

                using (var testProc = Process.Start(testPsi))
                {
                    testProc?.WaitForExit(1000);
                    return "ssh";
                }
            }
            catch
            {
            }

            return null;
        }

        private static string TryInstallOpenSsh()
        {
            try
            {
                ProcessStartInfo installPsi = new ProcessStartInfo
                {
                    FileName = "powershell.exe",
                    Arguments = "-NoProfile -ExecutionPolicy Bypass -Command \"Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0\"",
                    UseShellExecute = true,
                    Verb = "runas",
                    WindowStyle = ProcessWindowStyle.Normal
                };

                using (Process installProc = Process.Start(installPsi))
                {
                    if (installProc == null)
                        return false;

                    installProc.WaitForExit();
                    return installProc.ExitCode == 0;
                }
            }
            catch (Win32Exception)
            {
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine("Запрос прав Администратора был отклонён пользователем.");
                Console.ResetColor();
                return false;
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine($"Не удалось запустить автоматическую установку: {ex.Message}");
                Console.ResetColor();
                return false;
            }
        }

        private static string GenerateRandomPayload(int linesCount, int charsPerLine)
        {
            const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
            StringBuilder sb = new StringBuilder();
            Random rng = new Random();

            for (int i = 0; i < linesCount; i++)
            {
                char[] buffer = new char[charsPerLine];
                for (int j = 0; j < charsPerLine; j++)
                {
                    buffer[j] = chars[rng.Next(chars.Length)];
                }
                sb.AppendLine(new string(buffer));
            }

            return sb.ToString();
        }
    }
}

4.2. Сборка и создание ярлыка на рабочем столе

  1. В Visual Studio в свойствах проекта (Свойства -> Сборка / Build) рекомендуется выставить целевую платформу x64 (или снять флаг Prefer 32-bit).
  2. Скомпилируйте проект в конфигурации Release (результирующий бинарник: D:\sys\C-Sharp\deployfile\bin\Release\deployfile.exe).
  3. Создайте ярлык на рабочем столе:
  • Расположение объекта:
"D:\sys\C-Sharp\deployfile\bin\Release\deployfile.exe"

  • Имя ярлыка: Откатить конфигурацию HAProxy.

5. Проверка работы комплекса

  1. Запуск с клиента:
  • Дважды кликните по созданному ярлыку на рабочем столе.
  • Консоль отобразит процесс подключения и результат:
Используется ключ: D:\sys\C-Sharp\deployfile\key\test_key.key
Подключение к depfile@10.228.48.110:22...
Файл status/change.me и хеш status/sha256.hash успешно созданы на сервере.

  1. Проверка на сервере (в пределах 1 минуты):
  • Каталог /var/lib/depfile/status удален.
  • Файл конфигурации /etc/haproxy/haproxy.cfg перезаписан из /etc/haproxy/backup/haproxy.cfg.
  • Статус HAProxy: systemctl status haproxy в состоянии active (running).
  • Проверка фиксации развертывания:
cat /var/lib/depfile/deployment/success.me

Вывод:

deploment success {2026-10-13 12:45:00}

  • Журнал работы скрипта доступен в /var/log/deploy_haproxy.log.