From d743cb2df05d9353d6cfcdcefaa3d7e345eda7f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Oct 2025 07:37:14 +0000 Subject: [PATCH] Add Proxmox API extensions, WebSocket monitoring, and email notifications Co-authored-by: Ospab <189454929+Ospab@users.noreply.github.com> --- ospabhost/backend/package-lock.json | 234 +++++++++++++++++- ospabhost/backend/package.json | 5 +- ospabhost/backend/src/index.ts | 21 +- .../src/modules/notification/email.service.ts | 133 ++++++++++ .../src/modules/server/monitoring.service.ts | 191 ++++++++++++++ .../backend/src/modules/server/proxmoxApi.ts | 127 ++++++++++ .../src/modules/server/server.controller.ts | 92 ++++++- .../src/modules/server/server.routes.ts | 14 +- 8 files changed, 809 insertions(+), 8 deletions(-) create mode 100644 ospabhost/backend/src/modules/notification/email.service.ts create mode 100644 ospabhost/backend/src/modules/server/monitoring.service.ts diff --git a/ospabhost/backend/package-lock.json b/ospabhost/backend/package-lock.json index a79fc1b..ff582fd 100644 --- a/ospabhost/backend/package-lock.json +++ b/ospabhost/backend/package-lock.json @@ -17,7 +17,9 @@ "dotenv": "^16.4.5", "express": "^4.21.2", "jsonwebtoken": "^9.0.2", - "multer": "^2.0.2" + "multer": "^2.0.2", + "nodemailer": "^6.9.16", + "socket.io": "^4.8.1" }, "devDependencies": { "@types/bcrypt": "^6.0.0", @@ -27,6 +29,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.0.0", "@types/node": "^20.12.12", + "@types/nodemailer": "^6.4.17", "prisma": "^6.16.2", "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" @@ -158,6 +161,12 @@ "@prisma/debug": "6.16.2" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -235,7 +244,6 @@ "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -313,12 +321,21 @@ "version": "20.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -466,6 +483,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -922,6 +948,67 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1716,6 +1803,15 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/nodemailer": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", + "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2212,6 +2308,116 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2498,7 +2704,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -2548,6 +2753,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/ospabhost/backend/package.json b/ospabhost/backend/package.json index aea5dd3..4f02ecb 100644 --- a/ospabhost/backend/package.json +++ b/ospabhost/backend/package.json @@ -20,7 +20,9 @@ "dotenv": "^16.4.5", "express": "^4.21.2", "jsonwebtoken": "^9.0.2", - "multer": "^2.0.2" + "multer": "^2.0.2", + "nodemailer": "^6.9.16", + "socket.io": "^4.8.1" }, "devDependencies": { "@types/bcrypt": "^6.0.0", @@ -30,6 +32,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.0.0", "@types/node": "^20.12.12", + "@types/nodemailer": "^6.4.17", "prisma": "^6.16.2", "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" diff --git a/ospabhost/backend/src/index.ts b/ospabhost/backend/src/index.ts index 06262c8..3b8ea4e 100644 --- a/ospabhost/backend/src/index.ts +++ b/ospabhost/backend/src/index.ts @@ -1,6 +1,8 @@ import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; +import http from 'http'; +import { Server as SocketIOServer } from 'socket.io'; import authRoutes from './modules/auth/auth.routes'; import ticketRoutes from './modules/ticket/ticket.routes'; import checkRoutes from './modules/check/check.routes'; @@ -8,10 +10,21 @@ import proxmoxRoutes from '../proxmox/proxmox.routes'; import tariffRoutes from './modules/tariff'; import osRoutes from './modules/os'; import serverRoutes from './modules/server'; +import { MonitoringService } from './modules/server/monitoring.service'; dotenv.config(); const app = express(); +const server = http.createServer(app); + +// Настройка Socket.IO с CORS +const io = new SocketIOServer(server, { + cors: { + origin: ['http://localhost:3000', 'http://localhost:5173'], + methods: ['GET', 'POST'], + credentials: true + } +}); // ИСПРАВЛЕНО: более точная настройка CORS app.use(cors({ @@ -65,7 +78,13 @@ app.use('/api/server', serverRoutes); const PORT = process.env.PORT || 5000; -app.listen(PORT, () => { +// Инициализация сервиса мониторинга +const monitoringService = new MonitoringService(io); +monitoringService.startMonitoring(); + +server.listen(PORT, () => { console.log(`🚀 Сервер запущен на порту ${PORT}`); console.log(`📊 База данных: ${process.env.DATABASE_URL ? 'подключена' : 'НЕ НАСТРОЕНА'}`); + console.log(`🔌 WebSocket сервер запущен`); + console.log(`📡 Мониторинг серверов активен`); }); \ No newline at end of file diff --git a/ospabhost/backend/src/modules/notification/email.service.ts b/ospabhost/backend/src/modules/notification/email.service.ts new file mode 100644 index 0000000..6839ac9 --- /dev/null +++ b/ospabhost/backend/src/modules/notification/email.service.ts @@ -0,0 +1,133 @@ +import nodemailer from 'nodemailer'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +// Конфигурация email транспорта +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST || 'smtp.gmail.com', + port: Number(process.env.SMTP_PORT) || 587, + secure: false, // true для 465, false для других портов + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS + } +}); + +export interface EmailNotification { + to: string; + subject: string; + text?: string; + html?: string; +} + +// Отправка email уведомления +export async function sendEmail(notification: EmailNotification) { + try { + // Проверяем наличие конфигурации SMTP + if (!process.env.SMTP_USER || !process.env.SMTP_PASS) { + console.log('SMTP not configured, skipping email notification'); + return { status: 'skipped', message: 'SMTP not configured' }; + } + + const info = await transporter.sendMail({ + from: `"Ospab Host" <${process.env.SMTP_USER}>`, + ...notification + }); + + console.log('Email sent: %s', info.messageId); + return { status: 'success', messageId: info.messageId }; + } catch (error: any) { + console.error('Error sending email:', error); + return { status: 'error', message: error.message }; + } +} + +// Отправка уведомления о высокой нагрузке +export async function sendResourceAlertEmail(userId: number, serverId: number, alertType: string, value: string) { + try { + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) return { status: 'error', message: 'User not found' }; + + const subject = `Предупреждение: Высокая нагрузка на сервер #${serverId}`; + const html = ` +

Предупреждение о ресурсах сервера

+

Здравствуйте, ${user.username}!

+

Обнаружено превышение лимитов ресурсов на вашем сервере #${serverId}:

+ +

Рекомендуем проверить сервер и при необходимости увеличить его ресурсы.

+

С уважением,
Команда Ospab Host

+ `; + + return await sendEmail({ + to: user.email, + subject, + html + }); + } catch (error: any) { + console.error('Error sending resource alert email:', error); + return { status: 'error', message: error.message }; + } +} + +// Отправка уведомления о создании сервера +export async function sendServerCreatedEmail(userId: number, serverId: number, serverDetails: any) { + try { + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) return { status: 'error', message: 'User not found' }; + + const subject = `Ваш сервер #${serverId} успешно создан`; + const html = ` +

Сервер успешно создан!

+

Здравствуйте, ${user.username}!

+

Ваш новый сервер был успешно создан:

+ +

Вы можете управлять сервером через панель управления.

+

С уважением,
Команда Ospab Host

+ `; + + return await sendEmail({ + to: user.email, + subject, + html + }); + } catch (error: any) { + console.error('Error sending server created email:', error); + return { status: 'error', message: error.message }; + } +} + +// Отправка уведомления о приближении срока оплаты +export async function sendPaymentReminderEmail(userId: number, serverId: number, daysLeft: number) { + try { + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) return { status: 'error', message: 'User not found' }; + + const subject = `Напоминание: Оплата за сервер #${serverId}`; + const html = ` +

Напоминание об оплате

+

Здравствуйте, ${user.username}!

+

До окончания срока действия вашего тарифа для сервера #${serverId} осталось ${daysLeft} дней.

+

Пожалуйста, пополните баланс, чтобы избежать прерывания обслуживания.

+

Ваш текущий баланс: ${user.balance}₽

+

С уважением,
Команда Ospab Host

+ `; + + return await sendEmail({ + to: user.email, + subject, + html + }); + } catch (error: any) { + console.error('Error sending payment reminder email:', error); + return { status: 'error', message: error.message }; + } +} diff --git a/ospabhost/backend/src/modules/server/monitoring.service.ts b/ospabhost/backend/src/modules/server/monitoring.service.ts new file mode 100644 index 0000000..7570bf4 --- /dev/null +++ b/ospabhost/backend/src/modules/server/monitoring.service.ts @@ -0,0 +1,191 @@ +import { Server as SocketIOServer } from 'socket.io'; +import { PrismaClient } from '@prisma/client'; +import { getContainerStats } from './proxmoxApi'; +import { sendResourceAlertEmail } from '../notification/email.service'; + +const prisma = new PrismaClient(); + +export class MonitoringService { + private io: SocketIOServer; + private monitoringInterval: NodeJS.Timeout | null = null; + private readonly MONITORING_INTERVAL = 30000; // 30 секунд + + constructor(io: SocketIOServer) { + this.io = io; + this.setupSocketHandlers(); + } + + private setupSocketHandlers() { + this.io.on('connection', (socket) => { + console.log(`Client connected: ${socket.id}`); + + // Подписка на обновления конкретного сервера + socket.on('subscribe-server', async (serverId: number) => { + console.log(`Client ${socket.id} subscribed to server ${serverId}`); + socket.join(`server-${serverId}`); + + // Отправляем начальную статистику + try { + const server = await prisma.server.findUnique({ where: { id: serverId } }); + if (server && server.proxmoxId) { + const stats = await getContainerStats(server.proxmoxId); + socket.emit('server-stats', { serverId, stats }); + } + } catch (error) { + console.error(`Error fetching initial stats for server ${serverId}:`, error); + } + }); + + // Отписка от обновлений сервера + socket.on('unsubscribe-server', (serverId: number) => { + console.log(`Client ${socket.id} unsubscribed from server ${serverId}`); + socket.leave(`server-${serverId}`); + }); + + socket.on('disconnect', () => { + console.log(`Client disconnected: ${socket.id}`); + }); + }); + } + + // Запуск периодического мониторинга + public startMonitoring() { + if (this.monitoringInterval) { + console.log('Monitoring already running'); + return; + } + + console.log('Starting server monitoring service...'); + this.monitoringInterval = setInterval(async () => { + await this.checkAllServers(); + }, this.MONITORING_INTERVAL); + + // Первая проверка сразу + this.checkAllServers(); + } + + // Остановка мониторинга + public stopMonitoring() { + if (this.monitoringInterval) { + clearInterval(this.monitoringInterval); + this.monitoringInterval = null; + console.log('Monitoring service stopped'); + } + } + + // Проверка всех активных серверов + private async checkAllServers() { + try { + const servers = await prisma.server.findMany({ + where: { + status: { + in: ['running', 'stopped', 'creating'] + } + } + }); + + for (const server of servers) { + if (server.proxmoxId) { + try { + const stats = await getContainerStats(server.proxmoxId); + + if (stats.status === 'success' && stats.data) { + // Обновляем статус и метрики в БД + await prisma.server.update({ + where: { id: server.id }, + data: { + status: stats.data.status, + cpuUsage: stats.data.cpu || 0, + memoryUsage: stats.data.memory?.usage || 0, + diskUsage: stats.data.disk?.usage || 0, + networkIn: stats.data.network?.in || 0, + networkOut: stats.data.network?.out || 0, + lastPing: new Date() + } + }); + + // Отправляем обновления подписанным клиентам + this.io.to(`server-${server.id}`).emit('server-stats', { + serverId: server.id, + stats + }); + + // Проверяем превышение лимитов и отправляем алерты + await this.checkResourceLimits(server, stats.data); + } + } catch (error) { + console.error(`Error monitoring server ${server.id}:`, error); + } + } + } + } catch (error) { + console.error('Error in checkAllServers:', error); + } + } + + // Проверка превышения лимитов ресурсов + private async checkResourceLimits(server: any, stats: any) { + const alerts = []; + + // CPU превышает 90% + if (stats.cpu && stats.cpu > 0.9) { + alerts.push({ + type: 'cpu', + message: `CPU usage is at ${(stats.cpu * 100).toFixed(1)}%`, + level: 'warning' + }); + + // Отправляем email уведомление + await sendResourceAlertEmail( + server.userId, + server.id, + 'CPU', + `${(stats.cpu * 100).toFixed(1)}%` + ); + } + + // Memory превышает 90% + if (stats.memory?.usage && stats.memory.usage > 90) { + alerts.push({ + type: 'memory', + message: `Memory usage is at ${stats.memory.usage.toFixed(1)}%`, + level: 'warning' + }); + + // Отправляем email уведомление + await sendResourceAlertEmail( + server.userId, + server.id, + 'Memory', + `${stats.memory.usage.toFixed(1)}%` + ); + } + + // Disk превышает 90% + if (stats.disk?.usage && stats.disk.usage > 90) { + alerts.push({ + type: 'disk', + message: `Disk usage is at ${stats.disk.usage.toFixed(1)}%`, + level: 'warning' + }); + + // Отправляем email уведомление + await sendResourceAlertEmail( + server.userId, + server.id, + 'Disk', + `${stats.disk.usage.toFixed(1)}%` + ); + } + + // Отправляем алерты, если есть + if (alerts.length > 0) { + this.io.to(`server-${server.id}`).emit('server-alerts', { + serverId: server.id, + alerts + }); + + console.log(`Alerts for server ${server.id}:`, alerts); + } + } +} diff --git a/ospabhost/backend/src/modules/server/proxmoxApi.ts b/ospabhost/backend/src/modules/server/proxmoxApi.ts index aa3aacf..fed4bc6 100644 --- a/ospabhost/backend/src/modules/server/proxmoxApi.ts +++ b/ospabhost/backend/src/modules/server/proxmoxApi.ts @@ -342,6 +342,133 @@ export async function getConsoleURL(vmid: number): Promise<{ status: string; url } } +// Изменение конфигурации контейнера (CPU, RAM, Disk) +export async function resizeContainer(vmid: number, config: { cores?: number; memory?: number; rootfs?: string }) { + try { + const response = await axios.put( + `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/config`, + config, + { headers: getProxmoxHeaders() } + ); + return { + status: 'success', + data: response.data?.data + }; + } catch (error: any) { + console.error('Ошибка изменения конфигурации:', error); + return { + status: 'error', + message: error.response?.data?.errors || error.message + }; + } +} + +// Создание снэпшота +export async function createSnapshot(vmid: number, snapname: string, description?: string) { + try { + const response = await axios.post( + `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/snapshot`, + { + snapname, + description: description || `Snapshot ${snapname}` + }, + { headers: getProxmoxHeaders() } + ); + return { + status: 'success', + taskId: response.data?.data, + snapname + }; + } catch (error: any) { + console.error('Ошибка создания снэпшота:', error); + return { + status: 'error', + message: error.response?.data?.errors || error.message + }; + } +} + +// Получение списка снэпшотов +export async function listSnapshots(vmid: number) { + try { + const response = await axios.get( + `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/snapshot`, + { headers: getProxmoxHeaders() } + ); + return { + status: 'success', + data: response.data?.data || [] + }; + } catch (error: any) { + console.error('Ошибка получения списка снэпшотов:', error); + return { + status: 'error', + message: error.response?.data?.errors || error.message + }; + } +} + +// Восстановление из снэпшота +export async function rollbackSnapshot(vmid: number, snapname: string) { + try { + const response = await axios.post( + `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/snapshot/${snapname}/rollback`, + {}, + { headers: getProxmoxHeaders() } + ); + return { + status: 'success', + taskId: response.data?.data + }; + } catch (error: any) { + console.error('Ошибка восстановления снэпшота:', error); + return { + status: 'error', + message: error.response?.data?.errors || error.message + }; + } +} + +// Удаление снэпшота +export async function deleteSnapshot(vmid: number, snapname: string) { + try { + const response = await axios.delete( + `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/snapshot/${snapname}`, + { headers: getProxmoxHeaders() } + ); + return { + status: 'success', + taskId: response.data?.data + }; + } catch (error: any) { + console.error('Ошибка удаления снэпшота:', error); + return { + status: 'error', + message: error.response?.data?.errors || error.message + }; + } +} + +// Получение списка всех контейнеров +export async function listContainers() { + try { + const response = await axios.get( + `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc`, + { headers: getProxmoxHeaders() } + ); + return { + status: 'success', + data: response.data?.data || [] + }; + } catch (error: any) { + console.error('Ошибка получения списка контейнеров:', error); + return { + status: 'error', + message: error.response?.data?.errors || error.message + }; + } +} + // Проверка соединения с Proxmox export async function checkProxmoxConnection() { try { diff --git a/ospabhost/backend/src/modules/server/server.controller.ts b/ospabhost/backend/src/modules/server/server.controller.ts index e1a2832..bf2e1a1 100644 --- a/ospabhost/backend/src/modules/server/server.controller.ts +++ b/ospabhost/backend/src/modules/server/server.controller.ts @@ -5,7 +5,12 @@ import { controlContainer, getContainerStats, changeRootPassword as proxmoxChangeRootPassword, - deleteContainer + deleteContainer, + resizeContainer, + createSnapshot, + listSnapshots, + rollbackSnapshot, + deleteSnapshot } from './proxmoxApi'; const prisma = new PrismaClient(); @@ -212,3 +217,88 @@ export async function changeRootPassword(req: Request, res: Response) { 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 || 'Ошибка удаления снэпшота' }); + } +} diff --git a/ospabhost/backend/src/modules/server/server.routes.ts b/ospabhost/backend/src/modules/server/server.routes.ts index 2c17b28..2e182ec 100644 --- a/ospabhost/backend/src/modules/server/server.routes.ts +++ b/ospabhost/backend/src/modules/server/server.routes.ts @@ -7,7 +7,12 @@ import { restartServer, getServerStatus, deleteServer, - changeRootPassword + changeRootPassword, + resizeServer, + createServerSnapshot, + getServerSnapshots, + rollbackServerSnapshot, + deleteServerSnapshot } from './server.controller'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); @@ -72,4 +77,11 @@ router.post('/:id/restart', restartServer); router.delete('/:id', deleteServer); router.post('/:id/password', changeRootPassword); +// Новые маршруты для управления конфигурацией и снэпшотами +router.put('/:id/resize', resizeServer); +router.post('/:id/snapshots', createServerSnapshot); +router.get('/:id/snapshots', getServerSnapshots); +router.post('/:id/snapshots/rollback', rollbackServerSnapshot); +router.delete('/:id/snapshots', deleteServerSnapshot); + export default router; \ No newline at end of file