commit 52e3c9875cf1afb6330b61f99c700133f93b9157 Author: admin Date: Tue Oct 13 11:05:58 2026 +0000 Добавить readme.md diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..26b9498 --- /dev/null +++ b/readme.md @@ -0,0 +1,576 @@ +# Документация проекта: Развертывание резервной конфигурации 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`. \ No newline at end of file