ваше сообщение коммита
This commit is contained in:
105
webssh-agent/utils/cleanupUtils.js
Normal file
105
webssh-agent/utils/cleanupUtils.js
Normal 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
|
||||
};
|
||||
275
webssh-agent/utils/dockerUtils.js
Normal file
275
webssh-agent/utils/dockerUtils.js
Normal 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
|
||||
};
|
||||
61
webssh-agent/utils/localUtils.js
Normal file
61
webssh-agent/utils/localUtils.js
Normal 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
|
||||
};
|
||||
10
webssh-agent/utils/logger.js
Normal file
10
webssh-agent/utils/logger.js
Normal 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;
|
||||
127
webssh-agent/utils/sshUtils.js
Normal file
127
webssh-agent/utils/sshUtils.js
Normal 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
|
||||
};
|
||||
140
webssh-agent/utils/systemUtils.js
Normal file
140
webssh-agent/utils/systemUtils.js
Normal 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
|
||||
};
|
||||
47
webssh-agent/utils/userUtils.js
Normal file
47
webssh-agent/utils/userUtils.js
Normal 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
|
||||
};
|
||||
Reference in New Issue
Block a user