Substitui o método de início de container por uma nova função que repara redes órfãs antes de iniciar o container no BackupService

This commit is contained in:
Alexander Sabino 2026-05-07 11:07:59 +01:00
parent 52bec257ee
commit f1931271b7
2 changed files with 55 additions and 15 deletions

View File

@ -397,7 +397,7 @@ class BackupService {
} }
pushLog('Iniciando container temporariamente para snapshot.', 'preparando'); pushLog('Iniciando container temporariamente para snapshot.', 'preparando');
await this.dockerService.startContainer(containerId); await this.dockerService.repairAndStartContainer(containerId);
tempStarted = true; tempStarted = true;
const sourcePaths = backupScope === 'container' const sourcePaths = backupScope === 'container'

View File

@ -1,4 +1,4 @@
const Docker = require('dockerode'); const Docker = require('dockerode');
const { PassThrough, Readable } = require('stream'); const { PassThrough, Readable } = require('stream');
const fs = require('fs'); const fs = require('fs');
const fsp = require('fs/promises'); const fsp = require('fs/promises');
@ -7,7 +7,7 @@ const zlib = require('zlib');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const { pipeline } = require('stream/promises'); const { pipeline } = require('stream/promises');
// Cria um arquivo tar POSIX em memória contendo um único arquivo. // Cria um arquivo tar POSIX em memória contendo um único arquivo.
// Usado para injetar o .snar no container sem depender do tar do sistema. // Usado para injetar o .snar no container sem depender do tar do sistema.
function buildSingleFileTar(filename, contentBuffer) { function buildSingleFileTar(filename, contentBuffer) {
const header = Buffer.alloc(512, 0); const header = Buffer.alloc(512, 0);
@ -20,19 +20,19 @@ function buildSingleFileTar(filename, contentBuffer) {
header[156] = 0x30; // type flag: regular file header[156] = 0x30; // type flag: regular file
Buffer.from('ustar\0', 'ascii').copy(header, 257); // magic Buffer.from('ustar\0', 'ascii').copy(header, 257); // magic
Buffer.from('00', 'ascii').copy(header, 263); // version Buffer.from('00', 'ascii').copy(header, 263); // version
// Checksum: calcular com campo de checksum como espaços // Checksum: calcular com campo de checksum como espaços
for (let i = 148; i < 156; i++) header[i] = 0x20; for (let i = 148; i < 156; i++) header[i] = 0x20;
let sum = 0; let sum = 0;
for (let i = 0; i < 512; i++) sum += header[i]; for (let i = 0; i < 512; i++) sum += header[i];
Buffer.from(`${sum.toString(8).padStart(6, '0')}\0 `, 'ascii').copy(header, 148); Buffer.from(`${sum.toString(8).padStart(6, '0')}\0 `, 'ascii').copy(header, 148);
// Dados com padding para múltiplo de 512 // Dados com padding para múltiplo de 512
const paddedLength = Math.ceil(contentBuffer.length / 512) * 512 || 512; const paddedLength = Math.ceil(contentBuffer.length / 512) * 512 || 512;
const dataBlock = Buffer.alloc(paddedLength, 0); const dataBlock = Buffer.alloc(paddedLength, 0);
contentBuffer.copy(dataBlock); contentBuffer.copy(dataBlock);
return Buffer.concat([header, dataBlock, Buffer.alloc(1024, 0)]); return Buffer.concat([header, dataBlock, Buffer.alloc(1024, 0)]);
} }
// Extrai o conteúdo do primeiro arquivo de um tar não comprimido em memória. // Extrai o conteúdo do primeiro arquivo de um tar não comprimido em memória.
function extractFirstFileFromTar(tarBuffer) { function extractFirstFileFromTar(tarBuffer) {
if (!tarBuffer || tarBuffer.length < 512) return null; if (!tarBuffer || tarBuffer.length < 512) return null;
const sizeField = tarBuffer.slice(124, 136).toString('ascii').replace(/[\0 ]/g, '').trim(); const sizeField = tarBuffer.slice(124, 136).toString('ascii').replace(/[\0 ]/g, '').trim();
@ -94,6 +94,46 @@ class DockerService {
await this.docker.getContainer(containerId).start(); await this.docker.getContainer(containerId).start();
} }
// Tenta iniciar o container. Se falhar por redes órfãs (network not found),
// desconecta as redes inexistentes e tenta iniciar novamente uma vez.
async repairAndStartContainer(containerId) {
try {
await this.startContainer(containerId);
return;
} catch (firstError) {
const msg = String(firstError.message || '');
const isNetworkError = /network.*not found|failed to set up container networking/i.test(msg);
if (!isNetworkError) {
throw firstError;
}
}
// Identifica as redes do container e remove as que não existem mais.
const inspect = await this.docker.getContainer(containerId).inspect();
const networkNames = Object.keys(inspect.NetworkSettings?.Networks || {});
let repaired = false;
for (const networkName of networkNames) {
try {
await this.docker.getNetwork(networkName).inspect();
} catch {
// Rede não existe — desconecta o container forçadamente.
try {
await this.docker.getNetwork(networkName).disconnect({ Container: containerId, Force: true });
repaired = true;
} catch {
// Ignora: a rede já foi removida do endpoint.
}
}
}
if (!repaired) {
// Tenta uma segunda vez mesmo sem reparação detectável.
}
await this.startContainer(containerId);
}
async ensureImage(imageName = this.helperImage) { async ensureImage(imageName = this.helperImage) {
try { try {
await this.docker.getImage(imageName).inspect(); await this.docker.getImage(imageName).inspect();
@ -126,7 +166,7 @@ class DockerService {
const container = await this.docker.createContainer({ const container = await this.docker.createContainer({
Image: this.helperImage, Image: this.helperImage,
Cmd: ['sh', '-lc', `mkdir -p ${shellQuote(`/hostfs${normalized}`)}`], Cmd: ['sh', '-c', `mkdir -p ${shellQuote(`/hostfs${normalized}`)}`],
Tty: false, Tty: false,
HostConfig: { HostConfig: {
Binds: ['/:/hostfs'], Binds: ['/:/hostfs'],
@ -187,7 +227,7 @@ class DockerService {
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
Tty: false, Tty: false,
Cmd: ['sh', '-lc', cmd], Cmd: ['sh', '-c', cmd],
}); });
const stream = await exec.start({ hijack: true, stdin: false }); const stream = await exec.start({ hijack: true, stdin: false });
@ -230,7 +270,7 @@ class DockerService {
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
Tty: false, Tty: false,
Cmd: ['sh', '-lc', cmd], Cmd: ['sh', '-c', cmd],
}); });
const stream = await exec.start({ hijack: true, stdin: false }); const stream = await exec.start({ hijack: true, stdin: false });
@ -321,7 +361,7 @@ class DockerService {
AttachStdout: false, AttachStdout: false,
AttachStderr: true, AttachStderr: true,
Tty: false, Tty: false,
Cmd: ['sh', '-lc', 'tar --listed-incremental=/dev/null -xzf - -C /'], Cmd: ['sh', '-c', 'tar --listed-incremental=/dev/null -xzf - -C /'],
}); });
const attachStream = await exec.start({ hijack: true, stdin: true }); const attachStream = await exec.start({ hijack: true, stdin: true });
@ -389,7 +429,7 @@ class DockerService {
async runHostCommand({ cmd, onOutput }) { async runHostCommand({ cmd, onOutput }) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const child = spawn('sh', ['-lc', cmd], { stdio: ['ignore', 'pipe', 'pipe'] }); const child = spawn('sh', ['-c', cmd], { stdio: ['ignore', 'pipe', 'pipe'] });
let output = ''; let output = '';
const streamOutput = (stream, streamName) => { const streamOutput = (stream, streamName) => {
@ -449,7 +489,7 @@ class DockerService {
} }
// Injeta um arquivo .snar local no container no caminho absoluto informado. // Injeta um arquivo .snar local no container no caminho absoluto informado.
// Usa tar POSIX em memória, sem depender do tar do sistema. // Usa tar POSIX em memória, sem depender do tar do sistema.
async putSnarToContainer(containerId, localSnarPath, containerSnarPath) { async putSnarToContainer(containerId, localSnarPath, containerSnarPath) {
const content = await fsp.readFile(localSnarPath); const content = await fsp.readFile(localSnarPath);
const filename = path.posix.basename(containerSnarPath); const filename = path.posix.basename(containerSnarPath);
@ -460,7 +500,7 @@ class DockerService {
} }
// Extrai o arquivo .snar do container e salva no caminho local informado. // Extrai o arquivo .snar do container e salva no caminho local informado.
// Retorna true se conseguiu, false se o arquivo não existe no container. // Retorna true se conseguiu, false se o arquivo não existe no container.
async getSnarFromContainer(containerId, containerSnarPath, localSnarPath) { async getSnarFromContainer(containerId, containerSnarPath, localSnarPath) {
const container = this.docker.getContainer(containerId); const container = this.docker.getContainer(containerId);
let archiveStream; let archiveStream;
@ -487,7 +527,7 @@ class DockerService {
const container = await this.docker.createContainer({ const container = await this.docker.createContainer({
Image: this.helperImage, Image: this.helperImage,
Cmd: ['sh', '-lc', cmd], Cmd: ['sh', '-c', cmd],
Tty: false, Tty: false,
HostConfig: { HostConfig: {
Binds: binds, Binds: binds,
@ -562,4 +602,4 @@ class DockerService {
} }
} }
module.exports = DockerService; module.exports = DockerService;