ваше сообщение коммита

This commit is contained in:
2025-10-03 18:48:11 +03:00
parent ad7b8e9716
commit 67cf473455
42 changed files with 5515 additions and 1180 deletions

View File

@@ -0,0 +1,105 @@
const { execSshCommand } = require('./sshUtils');
const log = require('./logger');
/**
* Очистка VDS сервера
*/
const cleanupVdsServer = async (options) => {
log.info('Очистка VDS сервера...');
// Остановка и удаление существующих Docker контейнеров
log.info('Остановка существующих Docker контейнеров...');
await execSshCommand('sudo docker stop $(sudo docker ps -aq) 2>/dev/null || true', options);
await execSshCommand('sudo docker rm $(sudo docker ps -aq) 2>/dev/null || true', options);
// Удаление Docker образов и очистка системы
log.info('Очистка Docker образов и системы...');
await execSshCommand('sudo docker system prune -af || true', options);
await execSshCommand('sudo docker volume prune -f || true', options);
await execSshCommand('sudo docker network prune -f || true', options);
// 🆕 Умная проверка и удаление системного nginx для избежания конфликтов портов
log.info('🔍 Проверка наличия системного nginx...');
const nginxCheck = await execSshCommand('systemctl list-units --type=service --state=active,inactive | grep nginx || echo "nginx not found"', options);
if (nginxCheck.stdout.includes('nginx.service')) {
log.info('⚠️ Обнаружен системный nginx - удаляем для освобождения портов 80/443...');
// Полная остановка и удаление системного nginx
await execSshCommand('sudo systemctl stop nginx || true', options);
await execSshCommand('sudo systemctl disable nginx || true', options);
await execSshCommand('sudo systemctl mask nginx || true', options);
await execSshCommand('sudo pkill -f nginx || true', options);
await execSshCommand('sudo apt-get purge -y nginx nginx-common nginx-full || true', options);
await execSshCommand('sudo apt-get autoremove -y || true', options);
log.success('✅ Системный nginx полностью удален, порты 80/443 освобождены для Docker nginx');
} else {
log.info(' Системный nginx не обнаружен - порты 80/443 свободны для Docker nginx');
}
// Остановка других конфликтующих сервисов
log.info('Остановка других конфликтующих сервисов...');
await execSshCommand('sudo systemctl stop apache2 2>/dev/null || true', options);
await execSshCommand('sudo systemctl disable apache2 2>/dev/null || true', options);
await execSshCommand('sudo systemctl mask apache2 2>/dev/null || true', options);
// Очистка старых пакетов
log.info('Очистка старых пакетов...');
await execSshCommand('sudo apt-get autoremove -y || true', options);
await execSshCommand('sudo apt-get autoclean || true', options);
// Очистка временных файлов
log.info('Очистка временных файлов...');
await execSshCommand('sudo rm -rf /tmp/* /var/tmp/* 2>/dev/null || true', options);
log.success('VDS сервер очищен');
};
/**
* Настройка SSH ключей для root
*/
const setupRootSshKeys = async (publicKey, options) => {
log.info('Настройка SSH ключей...');
// Создание директории .ssh для root
await execSshCommand('sudo mkdir -p /root/.ssh', options);
await execSshCommand('sudo chmod 700 /root/.ssh', options);
// Добавление публичного ключа в authorized_keys
await execSshCommand(`echo "${publicKey}" | sudo tee -a /root/.ssh/authorized_keys`, options);
await execSshCommand('sudo chmod 600 /root/.ssh/authorized_keys', options);
await execSshCommand('sudo chown root:root /root/.ssh/authorized_keys', options);
log.success('SSH ключи созданы и публичный ключ добавлен в authorized_keys');
};
/**
* Отключение парольной аутентификации
*/
const disablePasswordAuth = async (options) => {
log.info('Отключение парольной аутентификации...');
await execSshCommand('sudo sed -i "s/#PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config', options);
await execSshCommand('sudo sed -i "s/PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config', options);
await execSshCommand('sudo systemctl restart ssh', options);
log.success('Парольная аутентификация отключена, доступ только через SSH ключи');
};
/**
* Настройка firewall
*/
const setupFirewall = async (options) => {
log.info('Настройка firewall...');
await execSshCommand('sudo ufw --force enable', options);
await execSshCommand('sudo ufw allow ssh', options);
await execSshCommand('sudo ufw allow 80', options);
await execSshCommand('sudo ufw allow 443', options);
log.success('Firewall настроен');
};
module.exports = {
cleanupVdsServer,
setupRootSshKeys,
disablePasswordAuth,
setupFirewall
};

View File

@@ -0,0 +1,275 @@
const { exec } = require('child_process');
const { execSshCommand, execScpCommand } = require('./sshUtils');
const log = require('./logger');
/**
* Экспорт Docker образов и данных с локальной машины
*/
const exportDockerImages = async (sendWebSocketLog) => {
log.info('Экспорт Docker образов и данных с хоста...');
sendWebSocketLog('info', '📦 Начинаем экспорт Docker образов...', 'export_images', 60);
const images = [
{ name: 'postgres:16-alpine', file: 'dapp-postgres.tar' },
{ name: 'digital_legal_entitydle-ollama:latest', file: 'dapp-ollama.tar' },
{ name: 'digital_legal_entitydle-vector-search:latest', file: 'dapp-vector-search.tar' },
{ name: 'digital_legal_entitydle-backend:latest', file: 'dapp-backend.tar' },
{ name: 'digital_legal_entitydle-frontend:latest', file: 'dapp-frontend.tar' },
{ name: 'digital_legal_entitydle-frontend-nginx:latest', file: 'dapp-frontend-nginx.tar' },
{ name: 'digital_legal_entitydle-webssh-agent:latest', file: 'dapp-webssh-agent.tar' }
];
// Экспортируем все образы
for (let i = 0; i < images.length; i++) {
const image = images[i];
const progress = 60 + Math.floor((i / images.length) * 10); // 60-70%
sendWebSocketLog('info', `📦 Экспорт образа: ${image.name}`, 'export_images', progress);
await new Promise((resolve) => {
exec(`docker save ${image.name} -o /tmp/${image.file}`, (error, stdout, stderr) => {
if (error) {
log.error(`Ошибка экспорта ${image.name}: ${error.message}`);
sendWebSocketLog('error', `❌ Ошибка экспорта ${image.name}`, 'export_images', progress);
} else {
sendWebSocketLog('success', `✅ Экспорт ${image.name} завершен`, 'export_images', progress);
}
resolve();
});
});
}
// Экспортируем данные из volumes
log.info('Экспорт данных из Docker volumes...');
sendWebSocketLog('info', '📦 Экспорт данных из Docker volumes...', 'export_data', 70);
// PostgreSQL данные - проверяем наличие данных перед экспортом
sendWebSocketLog('info', '📦 Экспорт данных PostgreSQL...', 'export_data', 72);
await new Promise((resolve) => {
// Сначала проверим, есть ли данные в volume
exec('docker run --rm -v digital_legal_entitydle_postgres_data:/data alpine ls -la /data/base', (checkError, checkStdout, checkStderr) => {
if (checkError) {
log.error(`Ошибка проверки данных PostgreSQL: ${checkError.message}`);
sendWebSocketLog('error', `❌ Ошибка проверки данных PostgreSQL`, 'export_data', 72);
resolve();
return;
}
// Если данные есть, экспортируем их
exec('docker run --rm -v digital_legal_entitydle_postgres_data:/data -v /tmp:/backup alpine tar czf /backup/postgres_data.tar.gz -C /data .', (error, stdout, stderr) => {
if (error) {
log.error(`Ошибка экспорта данных PostgreSQL: ${error.message}`);
sendWebSocketLog('error', `❌ Ошибка экспорта данных PostgreSQL`, 'export_data', 72);
} else {
log.info(`Данные PostgreSQL экспортированы: ${checkStdout.trim()}`);
sendWebSocketLog('success', `✅ Экспорт данных PostgreSQL завершен (содержит базы данных)`, 'export_data', 72);
}
resolve();
});
});
});
// Ollama данные
sendWebSocketLog('info', '📦 Экспорт данных Ollama...', 'export_data', 75);
await new Promise((resolve) => {
exec('docker run --rm -v digital_legal_entitydle_ollama_data:/data -v /tmp:/backup alpine tar czf /backup/ollama_data.tar.gz -C /data .', (error, stdout, stderr) => {
if (error) {
log.error(`Ошибка экспорта данных Ollama: ${error.message}`);
sendWebSocketLog('error', `❌ Ошибка экспорта данных Ollama`, 'export_data', 75);
} else {
sendWebSocketLog('success', `✅ Экспорт данных Ollama завершен`, 'export_data', 75);
}
resolve();
});
});
// Vector Search данные
sendWebSocketLog('info', '📦 Экспорт данных Vector Search...', 'export_data', 78);
await new Promise((resolve) => {
exec('docker run --rm -v digital_legal_entitydle_vector_search_data:/data -v /tmp:/backup alpine tar czf /backup/vector_search_data.tar.gz -C /data .', (error, stdout, stderr) => {
if (error) {
log.error(`Ошибка экспорта данных Vector Search: ${error.message}`);
sendWebSocketLog('error', `❌ Ошибка экспорта данных Vector Search`, 'export_data', 78);
} else {
sendWebSocketLog('success', `✅ Экспорт данных Vector Search завершен`, 'export_data', 78);
}
resolve();
});
});
// Проверяем размеры экспортированных данных
log.info('Проверка размеров экспортированных данных...');
sendWebSocketLog('info', '📊 Проверка размеров экспортированных данных...', 'export_data', 78);
await new Promise((resolve) => {
exec('ls -lh /tmp/postgres_data.tar.gz /tmp/ollama_data.tar.gz /tmp/vector_search_data.tar.gz 2>/dev/null || echo "Некоторые файлы не найдены"', (error, stdout, stderr) => {
if (stdout && stdout.trim()) {
log.info(`Размеры экспортированных данных:\n${stdout.trim()}`);
sendWebSocketLog('info', `📊 Размеры данных:\n${stdout.trim()}`, 'export_data', 78);
}
resolve();
});
});
// Создаем архив с ВСЕМИ образами и данными приложения
log.info('Создание архива Docker образов и данных на хосте...');
sendWebSocketLog('info', '📦 Создание архива всех данных...', 'export_data', 80);
const tarFiles = images.map(img => img.file).join(' ');
const dataFiles = 'postgres_data.tar.gz ollama_data.tar.gz vector_search_data.tar.gz';
await new Promise((resolve) => {
exec(`chmod 644 /tmp/dapp-*.tar /tmp/*_data.tar.gz && cd /tmp && tar -czf docker-images-and-data.tar.gz ${tarFiles} ${dataFiles}`, (error, stdout, stderr) => {
if (error) {
log.error('Ошибка создания архива: ' + error.message);
sendWebSocketLog('error', '❌ Ошибка создания архива', 'export_data', 80);
} else {
// Проверяем размер финального архива
exec('ls -lh /tmp/docker-images-and-data.tar.gz', (sizeError, sizeStdout, sizeStderr) => {
if (sizeStdout && sizeStdout.trim()) {
log.info(`Финальный архив создан: ${sizeStdout.trim()}`);
sendWebSocketLog('success', `✅ Архив создан успешно (${sizeStdout.trim()})`, 'export_data', 80);
} else {
sendWebSocketLog('success', '✅ Архив создан успешно', 'export_data', 80);
}
resolve();
});
}
});
});
log.success('Docker образы и данные успешно экспортированы');
sendWebSocketLog('success', '✅ Экспорт данных завершен', 'export_data', 80);
};
/**
* Передача Docker образов и данных на VDS
*/
const transferDockerImages = async (options, sendWebSocketLog) => {
const { dockerUser } = options;
log.info('Передача Docker образов и данных на VDS...');
sendWebSocketLog('info', '📤 Передача архива на VDS сервер...', 'transfer', 82);
// Передаем архив образов и данных на VDS через SCP
await execScpCommand(
'/tmp/docker-images-and-data.tar.gz',
`/home/${dockerUser}/dapp/docker-images-and-data.tar.gz`,
options
);
sendWebSocketLog('success', '✅ Архив успешно передан на VDS', 'transfer', 85);
log.success('Docker образы и данные успешно переданы на VDS');
};
/**
* Импорт Docker образов и данных на VDS
*/
const importDockerImages = async (options, sendWebSocketLog) => {
const { dockerUser } = options;
// Создаем скрипт импорта на VDS
sendWebSocketLog('info', '📥 Начинаем импорт данных на VDS...', 'import', 85);
const importScript = `#!/bin/bash
set -e
echo "🚀 Импорт Docker образов и данных на VDS..."
# Проверяем наличие архива
if [ ! -f "./docker-images-and-data.tar.gz" ]; then
echo "❌ Файл docker-images-and-data.tar.gz не найден!"
exit 1
fi
# Создаем директорию для распаковки
mkdir -p ./temp-import
# Распаковываем архив
echo "📦 Распаковка архива..."
tar -xzf ./docker-images-and-data.tar.gz -C ./temp-import
# Импортируем ВСЕ образы приложения
echo "📦 Импорт образа postgres..."
docker load -i ./temp-import/dapp-postgres.tar
echo "📦 Импорт образа ollama..."
docker load -i ./temp-import/dapp-ollama.tar
echo "📦 Импорт образа vector-search..."
docker load -i ./temp-import/dapp-vector-search.tar
echo "📦 Импорт образа backend..."
docker load -i ./temp-import/dapp-backend.tar
echo "📦 Импорт образа frontend..."
docker load -i ./temp-import/dapp-frontend.tar
echo "📦 Импорт образа frontend-nginx..."
docker load -i ./temp-import/dapp-frontend-nginx.tar
echo "📦 Импорт образа webssh-agent..."
docker load -i ./temp-import/dapp-webssh-agent.tar
# 🆕 Импортируем данные в volumes с правильными именами для соответствия docker-compose
echo "📦 Импорт данных PostgreSQL..."
# Удаляем старый volume если существует
docker volume rm dapp_postgres_data 2>/dev/null || true
docker volume create dapp_postgres_data
docker run --rm -v dapp_postgres_data:/data -v ./temp-import:/backup alpine tar xzf /backup/postgres_data.tar.gz -C /data
echo "📦 Импорт данных Ollama..."
# Удаляем старый volume если существует
docker volume rm dapp_ollama_data 2>/dev/null || true
docker volume create dapp_ollama_data
docker run --rm -v dapp_ollama_data:/data -v ./temp-import:/backup alpine tar xzf /backup/ollama_data.tar.gz -C /data
echo "📦 Импорт данных Vector Search..."
# Удаляем старый volume если существует
docker volume rm dapp_vector_search_data 2>/dev/null || true
docker volume create dapp_vector_search_data
docker run --rm -v dapp_vector_search_data:/data -v ./temp-import:/backup alpine tar xzf /backup/vector_search_data.tar.gz -C /data
# Очищаем временные файлы
rm -rf ./temp-import
echo "✅ Образы и данные успешно импортированы!"
echo "📋 Доступные образы:"
docker images | grep -E "digital_legal_entitydle|postgres"
echo "📋 Доступные volumes:"
docker volume ls | grep dapp_`;
await execSshCommand(`echo '${importScript}' | sudo tee /home/${dockerUser}/dapp/import-images-and-data.sh`, options);
await execSshCommand(`sudo chmod +x /home/${dockerUser}/dapp/import-images-and-data.sh`, options);
// Импортируем образы и данные
log.info('Импорт Docker образов и данных...');
sendWebSocketLog('info', '📥 Импорт Docker образов на VDS...', 'import', 87);
await execSshCommand(`cd /home/${dockerUser}/dapp && ./import-images-and-data.sh`, options);
sendWebSocketLog('success', '✅ Импорт Docker образов завершен', 'import', 90);
sendWebSocketLog('info', '📥 Импорт данных в volumes...', 'import', 92);
// Логи от самого скрипта импорта будут видны
sendWebSocketLog('success', '✅ Импорт данных завершен', 'import', 95);
log.success('Docker образы и данные успешно импортированы на VDS');
};
/**
* Очистка временных файлов на локальной машине
*/
const cleanupLocalFiles = async () => {
log.info('Очистка временных файлов на хосте...');
await new Promise((resolve) => {
exec('rm -f /tmp/dapp-*.tar /tmp/*_data.tar.gz /tmp/docker-images-and-data.tar.gz', (error, stdout, stderr) => {
if (error) log.error('Ошибка очистки файлов: ' + error.message);
resolve();
});
});
log.success('Временные файлы очищены (SSH ключи сохранены на хосте)');
};
module.exports = {
exportDockerImages,
transferDockerImages,
importDockerImages,
cleanupLocalFiles
};

View File

@@ -0,0 +1,61 @@
const { exec } = require('child_process');
const log = require('./logger');
/**
* Выполнение команд локально (на хосте)
*/
const execLocalCommand = async (command) => {
return new Promise((resolve) => {
exec(command, (error, stdout, stderr) => {
resolve({
code: error ? error.code : 0,
stdout: stdout || '',
stderr: stderr || ''
});
});
});
};
/**
* Создание SSH ключей локально на хосте
*/
const createSshKeys = async (email) => {
log.info('Создание SSH ключей на хосте...');
return new Promise((resolve) => {
// Сначала исправляем права доступа к SSH конфигу
exec('chmod 600 /root/.ssh/config 2>/dev/null || true', (configError) => {
if (configError) {
log.warn('Не удалось исправить права доступа к SSH конфигу: ' + configError.message);
}
// Создаем SSH ключи
exec(`ssh-keygen -t rsa -b 4096 -C "${email}" -f ~/.ssh/id_rsa -N ""`, (error, stdout, stderr) => {
if (error) {
log.error('Ошибка создания SSH ключей: ' + error.message);
} else {
log.success('SSH ключи успешно созданы на хосте');
// Устанавливаем правильные права доступа к созданным ключам
exec('chmod 600 /root/.ssh/id_rsa && chmod 644 /root/.ssh/id_rsa.pub', (permError) => {
if (permError) {
log.warn('Не удалось установить права доступа к SSH ключам: ' + permError.message);
} else {
log.success('Права доступа к SSH ключам установлены');
}
resolve();
});
}
if (error) {
resolve();
}
});
});
});
};
module.exports = {
execLocalCommand,
createSshKeys
};

View File

@@ -0,0 +1,10 @@
const chalk = require('chalk');
const log = {
info: (message) => console.log(chalk.blue('[INFO]'), message),
success: (message) => console.log(chalk.green('[SUCCESS]'), message),
error: (message) => console.log(chalk.red('[ERROR]'), message),
warn: (message) => console.log(chalk.yellow('[WARN]'), message)
};
module.exports = log;

View File

@@ -0,0 +1,127 @@
const { exec } = require('child_process');
const log = require('./logger');
/**
* Исправление прав доступа к SSH конфигурации
*/
const fixSshPermissions = async () => {
return new Promise((resolve) => {
// Исправляем владельца и права доступа к SSH конфигу
exec('chown root:root /root/.ssh/config 2>/dev/null || true && chmod 600 /root/.ssh/config 2>/dev/null || true', (error) => {
if (error) {
log.warn('Не удалось исправить права доступа к SSH конфигу: ' + error.message);
} else {
log.info('Права доступа к SSH конфигу исправлены');
}
resolve();
});
});
};
/**
* Выполнение SSH команд с поддержкой ключей и пароля
*/
const execSshCommand = async (command, options = {}) => {
const {
sshHost,
sshPort = 22,
sshConnectUser,
sshConnectPassword,
vdsIp
} = options;
// Исправляем права доступа к SSH конфигу перед выполнением команды
await fixSshPermissions();
// Сначала пробуем подключиться с SSH ключами (без BatchMode для возможности fallback на пароль)
let sshCommand = `ssh -p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${sshConnectUser}@${sshHost || vdsIp} "${command.replace(/"/g, '\\"')}"`;
log.info(`🔍 Выполняем SSH команду: ${sshCommand}`);
return new Promise((resolve) => {
exec(sshCommand, (error, stdout, stderr) => {
log.info(`📤 SSH результат - код: ${error ? error.code : 0}, stdout: "${stdout}", stderr: "${stderr}"`);
if (error && error.code === 255 && sshConnectPassword) {
// Если подключение с ключами не удалось, пробуем с паролем
log.info('SSH ключи не сработали, пробуем с паролем...');
const passwordCommand = `sshpass -p "${sshConnectPassword}" ssh -p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${sshConnectUser}@${sshHost || vdsIp} "${command.replace(/"/g, '\\"')}"`;
log.info(`🔍 Выполняем SSH команду с паролем: ${passwordCommand}`);
exec(passwordCommand, (passwordError, passwordStdout, passwordStderr) => {
log.info(`📤 SSH с паролем результат - код: ${passwordError ? passwordError.code : 0}, stdout: "${passwordStdout}", stderr: "${passwordStderr}"`);
resolve({
code: passwordError ? passwordError.code : 0,
stdout: passwordStdout || '',
stderr: passwordStderr || ''
});
});
} else {
resolve({
code: error ? error.code : 0,
stdout: stdout || '',
stderr: stderr || ''
});
}
});
});
};
/**
* Выполнение SCP команд с поддержкой ключей и пароля
*/
const execScpCommand = async (sourcePath, targetPath, options = {}) => {
const {
sshHost,
sshPort = 22,
sshConnectUser,
sshConnectPassword,
vdsIp
} = options;
// Исправляем права доступа к SSH конфигу перед выполнением команды
await fixSshPermissions();
const scpCommand = `scp -P ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${sourcePath} ${sshConnectUser}@${sshHost || vdsIp}:${targetPath}`;
return new Promise((resolve) => {
exec(scpCommand, (error, stdout, stderr) => {
if (error && error.code === 255 && sshConnectPassword) {
// Если SCP с ключами не удался, пробуем с паролем
log.info('SCP с ключами не сработал, пробуем с паролем...');
const passwordScpCommand = `sshpass -p "${sshConnectPassword}" scp -P ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${sourcePath} ${sshConnectUser}@${sshHost || vdsIp}:${targetPath}`;
exec(passwordScpCommand, (passwordError, passwordStdout, passwordStderr) => {
if (passwordError) {
log.error('❌ Ошибка SCP: ' + passwordError.message);
} else {
log.success('✅ SCP успешно выполнен');
}
resolve({
code: passwordError ? passwordError.code : 0,
stdout: passwordStdout || '',
stderr: passwordStderr || ''
});
});
} else {
if (error) {
log.error('❌ Ошибка SCP: ' + error.message);
} else {
log.success('✅ SCP успешно выполнен');
}
resolve({
code: error ? error.code : 0,
stdout: stdout || '',
stderr: stderr || ''
});
}
});
});
};
module.exports = {
execSshCommand,
execScpCommand,
fixSshPermissions
};

View File

@@ -0,0 +1,140 @@
const { execSshCommand } = require('./sshUtils');
const log = require('./logger');
// Системные требования
const SYSTEM_REQUIREMENTS = {
minMemoryGB: 6, // Минимум 6GB RAM (Ollama требует 4GB + система)
minDiskGB: 30, // Минимум 30GB свободного места (AI модели + Docker образы)
minCpuCores: 2, // Минимум 2 CPU ядра
recommendedMemoryGB: 8, // Рекомендуется 8GB RAM (для комфортной работы)
recommendedDiskGB: 50 // Рекомендуется 50GB свободного места
};
/**
* Проверка системных требований VDS
*/
const checkSystemRequirements = async (options) => {
log.info('🔍 Проверка системных требований VDS...');
try {
// Проверка памяти
log.info('📊 Проверка памяти...');
const memoryResult = await execSshCommand('free -h | grep "Mem:" | awk \'{print $2}\'', options);
const memoryStr = memoryResult.stdout.trim().replace('G', '').replace('Gi', '');
const memoryGB = parseFloat(memoryStr);
// Проверка диска
log.info('💾 Проверка диска...');
const diskResult = await execSshCommand('df -h / | tail -1 | awk \'{print $4}\'', options);
const diskStr = diskResult.stdout.trim().replace('G', '').replace('Gi', '');
const diskGB = parseFloat(diskStr);
// Проверка CPU
log.info('⚡ Проверка CPU...');
const cpuResult = await execSshCommand('nproc', options);
const cpuCores = parseInt(cpuResult.stdout.trim());
// Проверка архитектуры
log.info('🏗️ Проверка архитектуры...');
const archResult = await execSshCommand('uname -m', options);
const architecture = archResult.stdout.trim();
// Дополнительная диагностика архитектуры
const archInfoResult = await execSshCommand('uname -a', options);
log.info(`Архитектура (uname -m): "${architecture}"`);
log.info(`Полная информация (uname -a): "${archInfoResult.stdout.trim()}"`);
// Проверка версии Ubuntu
log.info('🐧 Проверка версии Ubuntu...');
const ubuntuResult = await execSshCommand('lsb_release -d | cut -f2', options);
const ubuntuVersion = ubuntuResult.stdout.trim();
const systemInfo = {
memoryGB: memoryGB,
diskGB: diskGB,
cpuCores: cpuCores,
architecture: architecture,
ubuntuVersion: ubuntuVersion
};
log.info(`📋 Системная информация:`);
log.info(` 💾 Память: ${memoryGB}GB (минимум: ${SYSTEM_REQUIREMENTS.minMemoryGB}GB, рекомендуется: ${SYSTEM_REQUIREMENTS.recommendedMemoryGB}GB)`);
log.info(` 💿 Диск: ${diskGB}GB свободно (минимум: ${SYSTEM_REQUIREMENTS.minDiskGB}GB, рекомендуется: ${SYSTEM_REQUIREMENTS.recommendedDiskGB}GB)`);
log.info(` ⚡ CPU: ${cpuCores} ядер (минимум: ${SYSTEM_REQUIREMENTS.minCpuCores})`);
log.info(` 🏗️ Архитектура: ${architecture}`);
log.info(` 🐧 Ubuntu: ${ubuntuVersion}`);
// Валидация требований
const warnings = [];
const errors = [];
if (memoryGB < SYSTEM_REQUIREMENTS.minMemoryGB) {
errors.push(`Недостаточно памяти: ${memoryGB}GB (требуется минимум ${SYSTEM_REQUIREMENTS.minMemoryGB}GB)`);
} else if (memoryGB < SYSTEM_REQUIREMENTS.recommendedMemoryGB) {
warnings.push(`Мало памяти: ${memoryGB}GB (рекомендуется ${SYSTEM_REQUIREMENTS.recommendedMemoryGB}GB)`);
}
if (diskGB < SYSTEM_REQUIREMENTS.minDiskGB) {
errors.push(`Недостаточно места на диске: ${diskGB}GB (требуется минимум ${SYSTEM_REQUIREMENTS.minDiskGB}GB)`);
} else if (diskGB < SYSTEM_REQUIREMENTS.recommendedDiskGB) {
warnings.push(`Мало места на диске: ${diskGB}GB (рекомендуется ${SYSTEM_REQUIREMENTS.recommendedDiskGB}GB)`);
}
if (cpuCores < SYSTEM_REQUIREMENTS.minCpuCores) {
errors.push(`Недостаточно CPU ядер: ${cpuCores} (требуется минимум ${SYSTEM_REQUIREMENTS.minCpuCores})`);
}
// Проверка архитектуры (поддерживаем различные архитектуры)
const supportedArchitectures = [
'x86_64', 'amd64', 'x64', // Intel/AMD 64-bit
'aarch64', 'arm64', // ARM 64-bit
'armv7l', 'armv8l', // ARM 32/64-bit
'i386', 'i686', // Intel 32-bit
'ppc64le', 's390x' // PowerPC, IBM Z
];
const isSupportedArch = supportedArchitectures.some(arch =>
architecture.toLowerCase().includes(arch.toLowerCase())
);
if (!isSupportedArch) {
// Вместо ошибки делаем предупреждение для неизвестных архитектур
warnings.push(`Неизвестная архитектура: ${architecture} (поддерживаются: x86_64, amd64, aarch64, arm64, armv7l, armv8l, i386, i686, ppc64le, s390x)`);
log.warn(`⚠️ Архитектура ${architecture} не в списке поддерживаемых, но попробуем продолжить...`);
}
// Вывод результатов
if (warnings.length > 0) {
log.warn('⚠️ Предупреждения:');
warnings.forEach(warning => log.warn(` ${warning}`));
}
if (errors.length > 0) {
log.error('❌ Критические ошибки:');
errors.forEach(error => log.error(` ${error}`));
throw new Error(`VDS не соответствует минимальным требованиям: ${errors.join(', ')}`);
}
if (warnings.length === 0 && errors.length === 0) {
log.success('✅ Все системные требования выполнены!');
} else {
log.warn('⚠️ Система соответствует минимальным требованиям, но рекомендуется улучшить конфигурацию');
}
return {
systemInfo,
warnings,
errors,
isCompatible: errors.length === 0,
hasWarnings: warnings.length > 0
};
} catch (error) {
log.error(`Ошибка при проверке системных требований: ${error.message}`);
throw error;
}
};
module.exports = {
checkSystemRequirements,
SYSTEM_REQUIREMENTS
};

View File

@@ -0,0 +1,47 @@
const { execSshCommand } = require('./sshUtils');
const log = require('./logger');
/**
* Создание пользователя с SSH ключами
*/
const createUserWithSshKeys = async (username, publicKey, options) => {
log.info(`Создание пользователя ${username}...`);
// Создание пользователя
await execSshCommand(`sudo useradd -m -s /bin/bash ${username} || true`, options);
await execSshCommand(`sudo usermod -aG sudo ${username}`, options);
// Настройка SSH ключей для пользователя
await execSshCommand(`sudo mkdir -p /home/${username}/.ssh`, options);
await execSshCommand(`echo "${publicKey}" | sudo tee /home/${username}/.ssh/authorized_keys`, options);
await execSshCommand(`sudo chown -R ${username}:${username} /home/${username}/.ssh`, options);
await execSshCommand(`sudo chmod 700 /home/${username}/.ssh`, options);
await execSshCommand(`sudo chmod 600 /home/${username}/.ssh/authorized_keys`, options);
log.success(`Пользователь ${username} создан с SSH ключами`);
};
/**
* Создание всех необходимых пользователей
*/
const createAllUsers = async (ubuntuUser, dockerUser, publicKey, options) => {
// Создание пользователя Ubuntu
await createUserWithSshKeys(ubuntuUser, publicKey, options);
// Создание пользователя Docker
await createUserWithSshKeys(dockerUser, publicKey, options);
// Добавление пользователя Docker в группу docker
await execSshCommand(`sudo usermod -aG docker ${dockerUser}`, options);
// Создание директории для приложения
await execSshCommand(`sudo mkdir -p /home/${dockerUser}/dapp`, options);
await execSshCommand(`sudo chown ${dockerUser}:${dockerUser} /home/${dockerUser}/dapp`, options);
log.success('Все пользователи созданы');
};
module.exports = {
createUserWithSshKeys,
createAllUsers
};