305 lines
12 KiB
TypeScript
305 lines
12 KiB
TypeScript
import { Request, Response } from 'express';
|
||
import { PrismaClient } from '@prisma/client';
|
||
import {
|
||
createLXContainer,
|
||
controlContainer,
|
||
getContainerStats,
|
||
changeRootPassword as proxmoxChangeRootPassword,
|
||
deleteContainer,
|
||
resizeContainer,
|
||
createSnapshot,
|
||
listSnapshots,
|
||
rollbackSnapshot,
|
||
deleteSnapshot
|
||
} from './proxmoxApi';
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
// Создание сервера (контейнера)
|
||
export async function createServer(req: Request, res: Response) {
|
||
try {
|
||
const { osId, tariffId } = req.body;
|
||
const userId = req.user?.id;
|
||
if (!userId) return res.status(401).json({ error: 'Нет авторизации' });
|
||
|
||
const os = await prisma.operatingSystem.findUnique({ where: { id: osId } });
|
||
const tariff = await prisma.tariff.findUnique({ where: { id: tariffId } });
|
||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||
if (!os || !tariff || !user) return res.status(400).json({ error: 'Некорректные параметры' });
|
||
|
||
// Проверка баланса пользователя
|
||
if (user.balance < tariff.price) {
|
||
return res.status(400).json({ error: 'Недостаточно средств на балансе' });
|
||
}
|
||
|
||
// Списываем средства
|
||
await prisma.user.update({ where: { id: userId }, data: { balance: { decrement: tariff.price } } });
|
||
|
||
// Генерация hostname из email
|
||
let hostname = user.email.split('@')[0];
|
||
hostname = hostname.replace(/[^a-zA-Z0-9-]/g, '');
|
||
if (hostname.length < 3) hostname = `user${userId}`;
|
||
if (hostname.length > 32) hostname = hostname.slice(0, 32);
|
||
if (/^[0-9-]/.test(hostname)) hostname = `u${hostname}`;
|
||
|
||
// Создаём контейнер в Proxmox
|
||
const result = await createLXContainer({
|
||
os: { template: os.template || '', type: os.type },
|
||
tariff: { name: tariff.name, price: tariff.price, description: tariff.description || undefined },
|
||
user: { id: user.id, username: user.username },
|
||
hostname
|
||
});
|
||
if (result.status !== 'success') {
|
||
// Возвращаем деньги обратно, если не удалось создать
|
||
await prisma.user.update({ where: { id: userId }, data: { balance: { increment: tariff.price } } });
|
||
// Логируем полный текст ошибки в файл
|
||
const fs = require('fs');
|
||
const logMsg = `[${new Date().toISOString()}] Ошибка Proxmox: ${JSON.stringify(result, null, 2)}\n`;
|
||
fs.appendFile('proxmox-errors.log', logMsg, (err: NodeJS.ErrnoException | null) => {
|
||
if (err) console.error('Ошибка записи лога:', err);
|
||
});
|
||
console.error('Ошибка Proxmox:', result.message);
|
||
return res.status(500).json({
|
||
error: 'Ошибка создания сервера в Proxmox',
|
||
details: result.message,
|
||
fullError: result
|
||
});
|
||
}
|
||
|
||
// Сохраняем сервер в БД с реальным статусом
|
||
const server = await prisma.server.create({
|
||
data: {
|
||
userId,
|
||
tariffId,
|
||
osId,
|
||
status: result.containerStatus || 'creating',
|
||
proxmoxId: Number(result.vmid),
|
||
ipAddress: result.ipAddress,
|
||
rootPassword: result.rootPassword,
|
||
}
|
||
});
|
||
res.json(server);
|
||
} catch (error: any) {
|
||
console.error('Ошибка покупки сервера:', error);
|
||
res.status(500).json({ error: error?.message || 'Ошибка покупки сервера' });
|
||
}
|
||
}
|
||
|
||
// Получить статус сервера
|
||
export async function getServerStatus(req: Request, res: Response) {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const server = await prisma.server.findUnique({ where: { id } });
|
||
if (!server) return res.status(404).json({ error: 'Сервер не найден' });
|
||
if (!server.proxmoxId) return res.status(400).json({ error: 'Нет VMID Proxmox' });
|
||
const stats = await getContainerStats(server.proxmoxId);
|
||
if (stats.status === 'error') {
|
||
// Если контейнер не найден в Proxmox, возвращаем статус deleted и пустую статистику
|
||
return res.json({
|
||
...server,
|
||
status: 'deleted',
|
||
stats: {
|
||
data: {
|
||
cpu: 0,
|
||
memory: { usage: 0 }
|
||
}
|
||
},
|
||
error: 'Контейнер не найден в Proxmox',
|
||
details: stats.message
|
||
});
|
||
}
|
||
res.json({ ...server, stats });
|
||
} catch (error: any) {
|
||
res.status(500).json({ error: error?.message || 'Ошибка получения статуса' });
|
||
}
|
||
}
|
||
|
||
// Запустить сервер
|
||
export async function startServer(req: Request, res: Response) {
|
||
await handleControl(req, res, 'start');
|
||
}
|
||
// Остановить сервер
|
||
export async function stopServer(req: Request, res: Response) {
|
||
await handleControl(req, res, 'stop');
|
||
}
|
||
// Перезагрузить сервер
|
||
export async function restartServer(req: Request, res: Response) {
|
||
await handleControl(req, res, 'restart');
|
||
}
|
||
|
||
async function handleControl(req: Request, res: Response, action: 'start' | 'stop' | 'restart') {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const server = await prisma.server.findUnique({ where: { id } });
|
||
if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
|
||
// Получаем текущий статус VM
|
||
const stats = await getContainerStats(server.proxmoxId);
|
||
const currentStatus = stats.status === 'success' && stats.data ? stats.data.status : server.status;
|
||
// Ограничения на действия
|
||
if (action === 'start' && currentStatus === 'running') {
|
||
return res.status(400).json({ error: 'Сервер уже запущен' });
|
||
}
|
||
if (action === 'stop' && currentStatus === 'stopped') {
|
||
return res.status(400).json({ error: 'Сервер уже остановлен' });
|
||
}
|
||
// Выполняем действие
|
||
const result = await controlContainer(server.proxmoxId, action);
|
||
// Polling статуса VM после управления
|
||
let newStatus = server.status;
|
||
if (result.status === 'success') {
|
||
let status = '';
|
||
let attempts = 0;
|
||
const maxAttempts = 10;
|
||
while (attempts < maxAttempts) {
|
||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
const stats = await getContainerStats(server.proxmoxId);
|
||
if (stats.status === 'success' && stats.data) {
|
||
status = stats.data.status;
|
||
if ((action === 'start' && status === 'running') ||
|
||
(action === 'stop' && status === 'stopped') ||
|
||
(action === 'restart' && status === 'running')) {
|
||
break;
|
||
}
|
||
}
|
||
attempts++;
|
||
}
|
||
switch (status) {
|
||
case 'running':
|
||
newStatus = 'running';
|
||
break;
|
||
case 'stopped':
|
||
newStatus = 'stopped';
|
||
break;
|
||
case 'suspended':
|
||
newStatus = 'suspended';
|
||
break;
|
||
default:
|
||
newStatus = status || server.status;
|
||
}
|
||
await prisma.server.update({ where: { id }, data: { status: newStatus } });
|
||
}
|
||
res.json({ ...result, status: newStatus });
|
||
} catch (error: any) {
|
||
res.status(500).json({ error: error?.message || 'Ошибка управления сервером' });
|
||
}
|
||
}
|
||
|
||
// Удалить сервер
|
||
export async function deleteServer(req: Request, res: Response) {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const server = await prisma.server.findUnique({ where: { id } });
|
||
if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
|
||
// Удаляем контейнер в Proxmox
|
||
const proxmoxResult = await deleteContainer(server.proxmoxId);
|
||
if (proxmoxResult.status !== 'success') {
|
||
return res.status(500).json({ error: 'Ошибка удаления контейнера в Proxmox', details: proxmoxResult });
|
||
}
|
||
await prisma.server.delete({ where: { id } });
|
||
res.json({ status: 'deleted' });
|
||
} catch (error: any) {
|
||
res.status(500).json({ error: error?.message || 'Ошибка удаления сервера' });
|
||
}
|
||
}
|
||
|
||
// Сменить root-пароль
|
||
export async function changeRootPassword(req: Request, res: Response) {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const server = await prisma.server.findUnique({ where: { id } });
|
||
if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
|
||
const result = await proxmoxChangeRootPassword(server.proxmoxId);
|
||
if (result?.status === 'success' && result.password) {
|
||
await prisma.server.update({ where: { id }, data: { rootPassword: result.password } });
|
||
}
|
||
res.json(result);
|
||
} catch (error: any) {
|
||
res.status(500).json({ error: error?.message || 'Ошибка смены пароля' });
|
||
}
|
||
}
|
||
|
||
// Изменить конфигурацию сервера
|
||
export async function resizeServer(req: Request, res: Response) {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const { cores, memory, disk } = req.body;
|
||
const server = await prisma.server.findUnique({ where: { id } });
|
||
if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
|
||
|
||
const config: any = {};
|
||
if (cores) config.cores = Number(cores);
|
||
if (memory) config.memory = Number(memory);
|
||
if (disk) config.rootfs = `local:${Number(disk)}`;
|
||
|
||
const result = await resizeContainer(server.proxmoxId, config);
|
||
res.json(result);
|
||
} catch (error: any) {
|
||
res.status(500).json({ error: error?.message || 'Ошибка изменения конфигурации' });
|
||
}
|
||
}
|
||
|
||
// Создать снэпшот
|
||
export async function createServerSnapshot(req: Request, res: Response) {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const { snapname, description } = req.body;
|
||
if (!snapname) return res.status(400).json({ error: 'Не указано имя снэпшота' });
|
||
|
||
const server = await prisma.server.findUnique({ where: { id } });
|
||
if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
|
||
|
||
const result = await createSnapshot(server.proxmoxId, snapname, description);
|
||
res.json(result);
|
||
} catch (error: any) {
|
||
res.status(500).json({ error: error?.message || 'Ошибка создания снэпшота' });
|
||
}
|
||
}
|
||
|
||
// Получить список снэпшотов
|
||
export async function getServerSnapshots(req: Request, res: Response) {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const server = await prisma.server.findUnique({ where: { id } });
|
||
if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
|
||
|
||
const result = await listSnapshots(server.proxmoxId);
|
||
res.json(result);
|
||
} catch (error: any) {
|
||
res.status(500).json({ error: error?.message || 'Ошибка получения снэпшотов' });
|
||
}
|
||
}
|
||
|
||
// Восстановить из снэпшота
|
||
export async function rollbackServerSnapshot(req: Request, res: Response) {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const { snapname } = req.body;
|
||
if (!snapname) return res.status(400).json({ error: 'Не указано имя снэпшота' });
|
||
|
||
const server = await prisma.server.findUnique({ where: { id } });
|
||
if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
|
||
|
||
const result = await rollbackSnapshot(server.proxmoxId, snapname);
|
||
res.json(result);
|
||
} catch (error: any) {
|
||
res.status(500).json({ error: error?.message || 'Ошибка восстановления снэпшота' });
|
||
}
|
||
}
|
||
|
||
// Удалить снэпшот
|
||
export async function deleteServerSnapshot(req: Request, res: Response) {
|
||
try {
|
||
const id = Number(req.params.id);
|
||
const { snapname } = req.body;
|
||
if (!snapname) return res.status(400).json({ error: 'Не указано имя снэпшота' });
|
||
|
||
const server = await prisma.server.findUnique({ where: { id } });
|
||
if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
|
||
|
||
const result = await deleteSnapshot(server.proxmoxId, snapname);
|
||
res.json(result);
|
||
} catch (error: any) {
|
||
res.status(500).json({ error: error?.message || 'Ошибка удаления снэпшота' });
|
||
}
|
||
}
|