316 lines
11 KiB
JavaScript
316 lines
11 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 de /proc/meminfo (valeurs réelles via lxcfs)
|
|
// Formule identique à `free` : used = total - free - buffers - cached
|
|
// Cela exclut le cache disque du noyau qui est libérable à tout moment
|
|
let memTotal = 8589934592; // 8 Go par défaut
|
|
let memFree = 0, memAvailable = 0, memUsed = 0, memBuffers = 0, memCached = 0;
|
|
try {
|
|
const meminfo = fs.readFileSync('/proc/meminfo', 'utf8');
|
|
const getVal = (key) => {
|
|
const m = meminfo.match(new RegExp(`^${key}:\s+(\d+)`, 'm'));
|
|
return m ? parseInt(m[1]) * 1024 : 0;
|
|
};
|
|
memTotal = getVal('MemTotal');
|
|
memFree = getVal('MemFree');
|
|
memAvailable = getVal('MemAvailable');
|
|
memBuffers = getVal('Buffers');
|
|
// SReclaimable est la partie du slab cache récupérable (inclus dans Cached par free)
|
|
const sReclaimable = getVal('SReclaimable');
|
|
const shmem = getVal('Shmem');
|
|
memCached = getVal('Cached') + (sReclaimable > shmem ? sReclaimable - shmem : 0);
|
|
// RAM utilisée = total - libre - buffers - cache (= RAM applicative réelle)
|
|
memUsed = memTotal - memFree - memBuffers - memCached;
|
|
if (memUsed < 0) memUsed = memTotal - memAvailable; // fallback si lxcfs ne fournit pas Buffers/Cached
|
|
} catch(e) {}
|
|
|
|
// 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,
|
|
};
|