manus-dashboard/src/backend/src/docker.js

389 lines
13 KiB
JavaScript

const Docker = require('dockerode');
const { exec } = require('child_process');
const path = require('path');
const config = require('./config');
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
/**
* Récupérer les informations d'un conteneur Docker
*/
async function getContainerInfo(containerName) {
try {
const container = docker.getContainer(containerName);
const info = await container.inspect();
return {
id: info.Id.substring(0, 12),
name: info.Name.replace('/', ''),
state: info.State.Status,
running: info.State.Running,
startedAt: info.State.StartedAt,
image: info.Config.Image,
created: info.Created,
};
} catch (err) {
return null;
}
}
/**
* Récupérer les logs d'un conteneur
*/
async function getContainerLogs(containerName, tail = 100) {
try {
const container = docker.getContainer(containerName);
const logs = await container.logs({
stdout: true,
stderr: true,
tail: tail,
timestamps: true,
});
// Parse buffer to string
return logs.toString('utf8');
} catch (err) {
return `Erreur lors de la récupération des logs: ${err.message}`;
}
}
/**
* Lister tous les conteneurs
*/
async function listContainers() {
try {
const containers = await docker.listContainers({ all: true });
return containers.map((c) => ({
id: c.Id.substring(0, 12),
names: c.Names.map((n) => n.replace('/', '')),
state: c.State,
status: c.Status,
image: c.Image,
created: new Date(c.Created * 1000).toISOString(),
}));
} catch (err) {
return [];
}
}
/**
* Redéployer une application (docker compose build + up)
*/
function redeployApp(appConfig) {
return new Promise((resolve, reject) => {
const appDir = path.join(config.appsBasePath, appConfig.directory);
const cmd = `cd ${appDir} && docker compose build --no-cache && docker compose up -d`;
const startTime = Date.now();
const logLines = [];
logLines.push(`[${new Date().toISOString()}] Début du redéploiement de ${appConfig.name}`);
logLines.push(`[${new Date().toISOString()}] Répertoire: ${appDir}`);
logLines.push(`[${new Date().toISOString()}] Commande: ${cmd}`);
const process = exec(cmd, {
maxBuffer: 1024 * 1024 * 10, // 10MB
timeout: 300000, // 5 minutes
});
process.stdout.on('data', (data) => {
const lines = data.toString().split('\n').filter(Boolean);
lines.forEach((line) => {
logLines.push(`[${new Date().toISOString()}] ${line}`);
});
});
process.stderr.on('data', (data) => {
const lines = data.toString().split('\n').filter(Boolean);
lines.forEach((line) => {
logLines.push(`[${new Date().toISOString()}] ${line}`);
});
});
process.on('close', (code) => {
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
if (code === 0) {
logLines.push(`[${new Date().toISOString()}] Redéploiement terminé avec succès en ${duration}s`);
resolve({ success: true, logs: logLines.join('\n'), duration });
} else {
logLines.push(`[${new Date().toISOString()}] Redéploiement échoué (code: ${code}) en ${duration}s`);
resolve({ success: false, logs: logLines.join('\n'), duration, exitCode: code });
}
});
process.on('error', (err) => {
logLines.push(`[${new Date().toISOString()}] Erreur: ${err.message}`);
resolve({ success: false, logs: logLines.join('\n'), error: err.message });
});
});
}
/**
* Pull le dernier code depuis Gitea
*/
function gitPull(appConfig) {
return new Promise((resolve, reject) => {
const srcDir = path.join(config.appsBasePath, appConfig.directory, 'src');
const cmd = `cd ${srcDir} && git pull origin main 2>&1 || git pull origin master 2>&1`;
exec(cmd, { timeout: 60000 }, (error, stdout, stderr) => {
if (error) {
resolve({ success: false, output: stderr || error.message });
} else {
resolve({ success: true, output: stdout });
}
});
});
}
/**
* Démarrer un conteneur Docker
*/
async function startContainer(containerName) {
try {
const container = docker.getContainer(containerName);
await container.start();
return { success: true, message: `Conteneur ${containerName} démarré` };
} catch (err) {
return { success: false, message: err.message };
}
}
/**
* Arrêter un conteneur Docker
*/
async function stopContainer(containerName) {
try {
const container = docker.getContainer(containerName);
await container.stop({ t: 10 });
return { success: true, message: `Conteneur ${containerName} arrêté` };
} catch (err) {
return { success: false, message: err.message };
}
}
/**
* Redémarrer un conteneur Docker
*/
async function restartContainer(containerName) {
try {
const container = docker.getContainer(containerName);
await container.restart({ t: 10 });
return { success: true, message: `Conteneur ${containerName} redémarré` };
} catch (err) {
return { success: false, message: err.message };
}
}
/**
* Récupérer les métriques système du serveur (CPU, RAM, disque, réseau)
*/
function getServerMetrics() {
return new Promise((resolve) => {
const { execSync } = require("child_process");
const fs = require("fs");
try {
// Lecture directe de /proc (monté depuis l'hôte via le namespace PID du conteneur)
// Pas besoin de nsenter : /proc dans le conteneur reflète l'hôte
// CPU : mesure via cgroup v2 usage_usec (méthode précise pour conteneur LXC)
// Fallback sur /proc/stat normalisé par nproc si cgroup v2 non disponible
let cpuUsage = 0;
try {
const ncpu = parseInt(execSync('nproc').toString().trim()) || 1;
const cgroupPath = '/sys/fs/cgroup/cpu.stat';
const readUsageUsec = () => {
const stat = fs.readFileSync(cgroupPath, 'utf8');
const m = stat.match(/^usage_usec\s+(\d+)/m);
return m ? parseInt(m[1]) : null;
};
const u1 = readUsageUsec();
if (u1 !== null) {
// Méthode cgroup v2 : mesure la consommation CPU réelle du conteneur
execSync('sleep 0.5');
const u2 = readUsageUsec();
// delta en microsecondes / (500ms * ncpu) = % CPU du conteneur
const deltaUs = u2 - u1;
cpuUsage = Math.min(100, Math.round(deltaUs / (500000 * ncpu) * 100));
} else {
// Fallback : /proc/stat normalisé par nproc
const parseCpuLine = (line) => line.trim().split(/\s+/).slice(1).map(Number);
const s1 = parseCpuLine(fs.readFileSync('/proc/stat', 'utf8').split('\n').find(l => l.startsWith('cpu ')));
execSync('sleep 0.3');
const s2 = parseCpuLine(fs.readFileSync('/proc/stat', 'utf8').split('\n').find(l => l.startsWith('cpu ')));
const idle1 = s1[3], total1 = s1.reduce((a,b)=>a+b,0);
const idle2 = s2[3], total2 = s2.reduce((a,b)=>a+b,0);
const dIdle = idle2 - idle1, dTotal = total2 - total1;
const rawCpu = dTotal > 0 ? (1 - dIdle / dTotal) * 100 : 0;
// Normaliser par nproc pour éviter l'effet nœud physique sur LXC
cpuUsage = Math.round(rawCpu / ncpu);
}
} catch(e) {}
// RAM : lecture robuste multi-source
// Priorité 1 : /host/proc/meminfo (hôte LXC monté via volume docker-compose)
// Priorité 2 : /proc/meminfo si MemTotal <= 64 Go (lxcfs ou VPS direct)
// Priorité 3 : Docker API MemTotal + ratio /proc/meminfo du nœud physique
let memTotal = 0;
let memFree = 0, memAvailable = 0, memUsed = 0, memBuffers = 0, memCached = 0;
// Fonction de lecture de meminfo depuis un chemin donné
const readMeminfo = (procPath) => {
const raw = fs.readFileSync(procPath + '/meminfo', 'utf8');
const map = {};
raw.split('\n').forEach(line => {
const idx = line.indexOf(':');
if (idx > 0) {
const key = line.substring(0, idx).trim();
const rest = line.substring(idx + 1).trim();
const val = parseInt(rest.split(' ')[0]);
if (Number.isFinite(val)) map[key] = val * 1024;
}
});
return map;
};
try {
let memMap = null;
let sourceLabel = '';
// Priorité 1 : /host/proc monté depuis l'hôte LXC
if (fs.existsSync('/host/proc/meminfo')) {
try {
const hostMap = readMeminfo('/host/proc');
if (hostMap['MemTotal'] > 0 && hostMap['MemTotal'] <= 64 * 1024 * 1024 * 1024) {
memMap = hostMap;
sourceLabel = 'host/proc';
}
} catch(e) {}
}
// Priorité 2 : /proc direct si MemTotal <= 64 Go
if (!memMap) {
try {
const procMap = readMeminfo('/proc');
if (procMap['MemTotal'] > 0 && procMap['MemTotal'] <= 64 * 1024 * 1024 * 1024) {
memMap = procMap;
sourceLabel = '/proc direct';
}
} catch(e) {}
}
// Priorité 3 : Docker API MemTotal + ratio du nœud physique
if (!memMap) {
try {
const dockerInfoRaw = execSync(
'curl -s --unix-socket /var/run/docker.sock http://localhost/info',
{ timeout: 3000 }
).toString();
const dockerInfo = JSON.parse(dockerInfoRaw);
const dockerMemTotal = dockerInfo.MemTotal || 0;
if (dockerMemTotal > 0) {
// Lire le ratio d'utilisation depuis /proc du nœud physique
const nodeMap = readMeminfo('/proc');
const nodeTotal = nodeMap['MemTotal'] || 1;
const nodeAvail = nodeMap['MemAvailable'] || 0;
const usageRatio = (nodeTotal - nodeAvail) / nodeTotal;
// Appliquer ce ratio à la vraie RAM du VPS
memTotal = dockerMemTotal;
memUsed = Math.round(memTotal * usageRatio);
memFree = memTotal - memUsed;
memAvailable = memFree;
sourceLabel = 'docker-api+ratio';
}
} catch(e) {}
}
// Calculer les valeurs finales si memMap disponible
if (memMap) {
memTotal = memMap['MemTotal'] || 0;
memFree = memMap['MemFree'] || 0;
memAvailable = memMap['MemAvailable'] || 0;
memBuffers = memMap['Buffers'] || 0;
const sReclaimable = memMap['SReclaimable'] || 0;
const shmem = memMap['Shmem'] || 0;
memCached = (memMap['Cached'] || 0) + (sReclaimable > shmem ? sReclaimable - shmem : 0);
memUsed = memTotal - memFree - memBuffers - memCached;
if (memUsed < 0) memUsed = memTotal - memAvailable;
}
} catch(e) {
// Fallback ultime
try {
const m = readMeminfo('/proc');
memTotal = m['MemTotal'] || 0;
memFree = m['MemFree'] || 0;
memAvailable = m['MemAvailable'] || 0;
memUsed = memTotal - memAvailable;
} catch(e2) {}
}
// Disque : df sur la racine du conteneur (= hôte car pas de volume overlay)
let diskTotal = 0, diskUsed = 0, diskFree = 0;
try {
const diskRaw = execSync('df -B1 / | tail -1').toString().trim().split(/\s+/);
diskTotal = parseInt(diskRaw[1]);
diskUsed = parseInt(diskRaw[2]);
diskFree = parseInt(diskRaw[3]);
} catch(e) {}
// Uptime et load
const uptimeRaw = fs.readFileSync('/proc/uptime', 'utf8').trim().split(' ');
const uptimeSeconds = parseFloat(uptimeRaw[0]);
const loadRaw = fs.readFileSync('/proc/loadavg', 'utf8').trim().split(' ');
const load1 = parseFloat(loadRaw[0]);
const load5 = parseFloat(loadRaw[1]);
const load15 = parseFloat(loadRaw[2]);
// Réseau
let netRx = 0, netTx = 0;
try {
const netDev = fs.readFileSync('/proc/net/dev', 'utf8');
const netLine = netDev.split('\n').find(l => /eth0|ens|enp/.test(l));
if (netLine) {
const parts = netLine.trim().split(/\s+/);
netRx = parseInt(parts[1]) || 0;
netTx = parseInt(parts[9]) || 0;
}
} catch(e) {}
resolve({
cpu: { usage: cpuUsage },
memory: {
total: memTotal,
used: memUsed,
free: memFree,
available: memAvailable,
buffers: memBuffers,
cached: memCached,
// usagePercent = RAM applicative réelle (hors cache disque, identique à `free`)
usagePercent: Math.round((memUsed / memTotal) * 100)
},
disk: {
total: diskTotal,
used: diskUsed,
free: diskFree,
usagePercent: Math.round((diskUsed / diskTotal) * 100)
},
uptime: uptimeSeconds,
load: { load1, load5, load15 },
network: { rx: netRx, tx: netTx },
timestamp: new Date().toISOString()
});
} catch (err) {
resolve({ error: err.message });
}
});
}
module.exports = {
docker,
getContainerInfo,
getContainerLogs,
listContainers,
redeployApp,
gitPull,
startContainer,
stopContainer,
restartContainer,
getServerMetrics,
};