370 lines
12 KiB
TypeScript
370 lines
12 KiB
TypeScript
import axios from 'axios';
|
||
import crypto from 'crypto';
|
||
import dotenv from 'dotenv';
|
||
dotenv.config();
|
||
|
||
const PROXMOX_API_URL = process.env.PROXMOX_API_URL;
|
||
const PROXMOX_TOKEN_ID = process.env.PROXMOX_TOKEN_ID;
|
||
const PROXMOX_TOKEN_SECRET = process.env.PROXMOX_TOKEN_SECRET;
|
||
const PROXMOX_NODE = process.env.PROXMOX_NODE || 'proxmox';
|
||
|
||
function getProxmoxHeaders(): Record<string, string> {
|
||
return {
|
||
'Authorization': `PVEAPIToken=${PROXMOX_TOKEN_ID}=${PROXMOX_TOKEN_SECRET}`,
|
||
'Content-Type': 'application/json'
|
||
};
|
||
}
|
||
|
||
// Генерация случайного пароля
|
||
export function generateSecurePassword(length: number = 16): string {
|
||
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
|
||
let password = '';
|
||
for (let i = 0; i < length; i++) {
|
||
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
||
}
|
||
return password;
|
||
}
|
||
|
||
// Получение следующего доступного VMID
|
||
export async function getNextVMID(): Promise<number> {
|
||
try {
|
||
const res = await axios.get(
|
||
`${PROXMOX_API_URL}/cluster/nextid`,
|
||
{ headers: getProxmoxHeaders() }
|
||
);
|
||
return res.data.data || Math.floor(100 + Math.random() * 899);
|
||
} catch (error) {
|
||
console.error('Ошибка получения VMID:', error);
|
||
return Math.floor(100 + Math.random() * 899);
|
||
}
|
||
}
|
||
|
||
// Создание LXC контейнера
|
||
export interface CreateContainerParams {
|
||
os: { template: string; type: string };
|
||
tariff: { name: string; price: number; description?: string };
|
||
user: { id: number; username: string };
|
||
hostname?: string;
|
||
}
|
||
|
||
export async function createLXContainer({ os, tariff, user }: CreateContainerParams) {
|
||
try {
|
||
const vmid = await getNextVMID();
|
||
const rootPassword = generateSecurePassword();
|
||
// Используем hostname из параметров, если есть
|
||
const hostname = arguments[0].hostname || `user${user.id}-${tariff.name.toLowerCase().replace(/\s/g, '-')}`;
|
||
|
||
// Определяем ресурсы по названию тарифа (парсим описание)
|
||
const description = tariff.description || '1 ядро, 1ГБ RAM, 20ГБ SSD';
|
||
const cores = parseInt(description.match(/(\d+)\s*ядр/)?.[1] || '1');
|
||
const memory = parseInt(description.match(/(\d+)ГБ\s*RAM/)?.[1] || '1') * 1024; // в MB
|
||
const diskSize = parseInt(description.match(/(\d+)ГБ\s*SSD/)?.[1] || '20');
|
||
|
||
const containerConfig = {
|
||
vmid,
|
||
hostname,
|
||
password: rootPassword,
|
||
ostemplate: os.template,
|
||
cores,
|
||
memory,
|
||
rootfs: `local:${diskSize}`,
|
||
net0: 'name=eth0,bridge=vmbr0,ip=dhcp',
|
||
unprivileged: 1,
|
||
start: 1, // Автостарт после создания
|
||
protection: 0,
|
||
console: 1,
|
||
cmode: 'console'
|
||
};
|
||
|
||
console.log('Создание LXC контейнера с параметрами:', containerConfig);
|
||
|
||
const response = await axios.post(
|
||
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc`,
|
||
containerConfig,
|
||
{ headers: getProxmoxHeaders() }
|
||
);
|
||
|
||
if (response.data?.data) {
|
||
// Polling статуса контейнера до running или timeout
|
||
let status = '';
|
||
let attempts = 0;
|
||
const maxAttempts = 10;
|
||
while (attempts < maxAttempts) {
|
||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
const info = await getContainerStatus(vmid);
|
||
status = info?.status || '';
|
||
if (status === 'running' || status === 'stopped' || status === 'created') break;
|
||
attempts++;
|
||
}
|
||
// Получаем IP адрес контейнера
|
||
const ipAddress = await getContainerIP(vmid);
|
||
return {
|
||
status: 'success',
|
||
vmid,
|
||
rootPassword,
|
||
ipAddress,
|
||
hostname,
|
||
taskId: response.data.data,
|
||
containerStatus: status
|
||
};
|
||
}
|
||
// Получить статус контейнера по VMID
|
||
async function getContainerStatus(vmid: number): Promise<{ status: string }> {
|
||
try {
|
||
const res = await axios.get(
|
||
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/status/current`,
|
||
{ headers: getProxmoxHeaders() }
|
||
);
|
||
return { status: res.data.data.status };
|
||
} catch (error) {
|
||
return { status: 'error' };
|
||
}
|
||
}
|
||
|
||
throw new Error('Не удалось создать контейнер');
|
||
} catch (error: any) {
|
||
console.error('Ошибка создания LXC контейнера:', error);
|
||
return {
|
||
status: 'error',
|
||
message: error.response?.data?.errors || error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
// Получение IP адреса контейнера
|
||
export async function getContainerIP(vmid: number): Promise<string | null> {
|
||
try {
|
||
await new Promise(resolve => setTimeout(resolve, 10000)); // Ждём запуска
|
||
|
||
const response = await axios.get(
|
||
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/interfaces`,
|
||
{ headers: getProxmoxHeaders() }
|
||
);
|
||
|
||
const interfaces = response.data?.data;
|
||
if (interfaces && interfaces.length > 0) {
|
||
for (const iface of interfaces) {
|
||
if (iface.inet && iface.inet !== '127.0.0.1') {
|
||
return iface.inet.split('/')[0]; // Убираем маску подсети
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
} catch (error) {
|
||
console.error('Ошибка получения IP:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Управление контейнером (старт/стоп/перезагрузка)
|
||
export async function controlContainer(vmid: number, action: 'start' | 'stop' | 'restart' | 'suspend' | 'resume') {
|
||
try {
|
||
const response = await axios.post(
|
||
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/status/${action}`,
|
||
{},
|
||
{ headers: getProxmoxHeaders() }
|
||
);
|
||
return {
|
||
status: 'success',
|
||
action,
|
||
taskId: response.data?.data
|
||
};
|
||
} catch (error: any) {
|
||
console.error(`Ошибка ${action} контейнера:`, error);
|
||
return {
|
||
status: 'error',
|
||
message: error.response?.data?.errors || error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
// Удаление контейнера
|
||
export async function deleteContainer(vmid: number) {
|
||
try {
|
||
const response = await axios.delete(
|
||
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}`,
|
||
{ 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 getContainerStats(vmid: number) {
|
||
try {
|
||
// Получаем текущий статус
|
||
const statusResponse = await axios.get(
|
||
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/status/current`,
|
||
{ headers: getProxmoxHeaders() }
|
||
);
|
||
|
||
const status = statusResponse.data?.data;
|
||
|
||
// Получаем статистику RRD (за последний час)
|
||
let rrdData = [];
|
||
let latest: any = {};
|
||
try {
|
||
const rrdResponse = await axios.get(
|
||
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/rrd?timeframe=hour`,
|
||
{ headers: getProxmoxHeaders() }
|
||
);
|
||
rrdData = rrdResponse.data?.data || [];
|
||
latest = rrdData[rrdData.length - 1] || {};
|
||
} catch (err: any) {
|
||
// Если ошибка 400, возвращаем пустую статистику, но не ошибку
|
||
if (err?.response?.status === 400) {
|
||
return {
|
||
status: 'success',
|
||
data: {
|
||
vmid,
|
||
status: status?.status || 'unknown',
|
||
uptime: status?.uptime || 0,
|
||
cpu: 0,
|
||
memory: {
|
||
used: status?.mem || 0,
|
||
max: status?.maxmem || 0,
|
||
usage: 0
|
||
},
|
||
disk: {
|
||
used: status?.disk || 0,
|
||
max: status?.maxdisk || 0,
|
||
usage: 0
|
||
},
|
||
network: {
|
||
in: 0,
|
||
out: 0
|
||
},
|
||
rrdData: []
|
||
}
|
||
};
|
||
} else {
|
||
throw err;
|
||
}
|
||
}
|
||
return {
|
||
status: 'success',
|
||
data: {
|
||
vmid,
|
||
status: status?.status || 'unknown',
|
||
uptime: status?.uptime || 0,
|
||
cpu: latest.cpu || 0,
|
||
memory: {
|
||
used: status?.mem || 0,
|
||
max: status?.maxmem || 0,
|
||
usage: status?.maxmem ? (status.mem / status.maxmem) * 100 : 0
|
||
},
|
||
disk: {
|
||
used: status?.disk || 0,
|
||
max: status?.maxdisk || 0,
|
||
usage: status?.maxdisk ? (status.disk / status.maxdisk) * 100 : 0
|
||
},
|
||
network: {
|
||
in: latest.netin || 0,
|
||
out: latest.netout || 0
|
||
},
|
||
rrdData: rrdData.slice(-60) // Последние 60 точек для графиков
|
||
}
|
||
};
|
||
} catch (error: any) {
|
||
console.error('Ошибка получения статистики:', error);
|
||
return {
|
||
status: 'error',
|
||
message: error.response?.data?.errors || error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
// Смена root пароля
|
||
export async function changeRootPassword(vmid: number): Promise<{ status: string; password?: string; message?: string }> {
|
||
try {
|
||
const newPassword = generateSecurePassword();
|
||
|
||
// Выполняем команду смены пароля в контейнере
|
||
const response = await axios.post(
|
||
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/status/exec`,
|
||
{
|
||
command: `echo 'root:${newPassword}' | chpasswd`
|
||
},
|
||
{ headers: getProxmoxHeaders() }
|
||
);
|
||
|
||
return {
|
||
status: 'success',
|
||
password: newPassword
|
||
};
|
||
} catch (error: any) {
|
||
console.error('Ошибка смены пароля:', error);
|
||
return {
|
||
status: 'error',
|
||
message: error.response?.data?.errors || error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
// Получение ссылки на noVNC консоль
|
||
export async function getConsoleURL(vmid: number): Promise<{ status: string; url?: string; message?: string }> {
|
||
try {
|
||
const response = await axios.post(
|
||
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/vncproxy`,
|
||
{
|
||
websocket: 1
|
||
},
|
||
{ headers: getProxmoxHeaders() }
|
||
);
|
||
|
||
const data = response.data?.data;
|
||
if (data?.ticket && data?.port) {
|
||
const consoleUrl = `${process.env.PROXMOX_WEB_URL}/?console=lxc&vmid=${vmid}&node=${PROXMOX_NODE}&resize=off&ticket=${data.ticket}&port=${data.port}`;
|
||
|
||
return {
|
||
status: 'success',
|
||
url: consoleUrl
|
||
};
|
||
}
|
||
|
||
throw new Error('Не удалось получить данные для консоли');
|
||
} catch (error: any) {
|
||
console.error('Ошибка получения консоли:', error);
|
||
return {
|
||
status: 'error',
|
||
message: error.response?.data?.errors || error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
// Проверка соединения с Proxmox
|
||
export async function checkProxmoxConnection() {
|
||
try {
|
||
const response = await axios.get(
|
||
`${PROXMOX_API_URL}/version`,
|
||
{ headers: getProxmoxHeaders() }
|
||
);
|
||
|
||
if (response.data?.data) {
|
||
return {
|
||
status: 'success',
|
||
message: 'Соединение с Proxmox установлено',
|
||
version: response.data.data.version,
|
||
node: PROXMOX_NODE
|
||
};
|
||
}
|
||
return { status: 'error', message: 'Не удалось получить версию Proxmox' };
|
||
} catch (error: any) {
|
||
return {
|
||
status: 'error',
|
||
message: 'Ошибка соединения с Proxmox',
|
||
error: error.response?.data || error.message
|
||
};
|
||
}
|
||
}
|