# Документация проекта: Развертывание резервной конфигурации 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`. 2. **Серверный обработчик (`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`: ```bash # 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`: ```bash 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`: ```bash sudo nano /usr/local/bin/deploy_haproxy.sh ``` Вставьте следующий код: ```bash #!/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 ``` Назначьте права на запуск: ```bash sudo chmod +x /usr/local/bin/deploy_haproxy.sh ``` ### 2.4. Настройка периодического запуска в cron Откройте планировщик суперпользователя: ```bash sudo crontab -e ``` Добавьте задачу запуска раз в минуту: ```cron * * * * * /usr/local/bin/deploy_haproxy.sh >> /var/log/deploy_haproxy.log 2>&1 ``` --- ## 3. Настройка рабочей станции Windows ### 3.1. Генерация SSH-ключа В командной строке (`cmd`) или PowerShell выполните: ```cmd 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`: ```powershell 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` на сервере).* Проверьте беспарольный вход: ```cmd ssh -i "D:\sys\C-Sharp\deployfile\key\test_key.key" depfile@10.228.48.110 "whoami && pwd" ``` *Вывод должен вернуть:* ```text depfile /var/lib/depfile ``` --- ## 4. Клиентское приложение C# (.NET Framework 4.8) ### 4.1. Исходный код (`Program.cs`) ```csharp 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. Создайте ярлык на рабочем столе: * **Расположение объекта:** ```cmd "D:\sys\C-Sharp\deployfile\bin\Release\deployfile.exe" ``` * **Имя ярлыка:** `Откатить конфигурацию HAProxy`. --- ## 5. Проверка работы комплекса 1. **Запуск с клиента:** * Дважды кликните по созданному ярлыку на рабочем столе. * Консоль отобразит процесс подключения и результат: ```text Используется ключ: D:\sys\C-Sharp\deployfile\key\test_key.key Подключение к depfile@10.228.48.110:22... Файл status/change.me и хеш status/sha256.hash успешно созданы на сервере. ``` 2. **Проверка на сервере (в пределах 1 минуты):** * Каталог `/var/lib/depfile/status` удален. * Файл конфигурации `/etc/haproxy/haproxy.cfg` перезаписан из `/etc/haproxy/backup/haproxy.cfg`. * Статус HAProxy: `systemctl status haproxy` в состоянии `active (running)`. * Проверка фиксации развертывания: ```bash cat /var/lib/depfile/deployment/success.me ``` *Вывод:* ```text deploment success {2026-10-13 12:45:00} ``` * Журнал работы скрипта доступен в `/var/log/deploy_haproxy.log`.