23 KiB
Документация проекта: Развертывание резервной конфигурации 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
Принцип работы
- 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.
- Серверный обработчик (
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. Сборка и создание ярлыка на рабочем столе
- В Visual Studio в свойствах проекта (Свойства -> Сборка / Build) рекомендуется выставить целевую платформу x64 (или снять флаг Prefer 32-bit).
- Скомпилируйте проект в конфигурации Release (результирующий бинарник:
D:\sys\C-Sharp\deployfile\bin\Release\deployfile.exe). - Создайте ярлык на рабочем столе:
- Расположение объекта:
"D:\sys\C-Sharp\deployfile\bin\Release\deployfile.exe"
- Имя ярлыка:
Откатить конфигурацию HAProxy.
5. Проверка работы комплекса
- Запуск с клиента:
- Дважды кликните по созданному ярлыку на рабочем столе.
- Консоль отобразит процесс подключения и результат:
Используется ключ: D:\sys\C-Sharp\deployfile\key\test_key.key
Подключение к depfile@10.228.48.110:22...
Файл status/change.me и хеш status/sha256.hash успешно созданы на сервере.
- Проверка на сервере (в пределах 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.