389 lines
13 KiB
JavaScript
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,
|
|
};
|