openvibers/openvibe.host Commits

Nightly backups: runuser by absolute path, and /usr/sbin on the backup unit's PATH — the scheduled runs of 2026-09-24 and 2026-09-25 failed for every database with 'spawn runuser ENOENT' (manual runs under sudo worked); found by the new OpenVibeBackupMissed alert

OP OpenVibers committed 25 Sept 2026 · 40a25fec7b · 3 files, +16 −2


Changes

ddiff --git a/deploy/systemd/openvibe-backup.service b/deploy/systemd/openvibe-backup.service
iindex 2f2087c..d0a11ee 100644
---- a/deploy/systemd/openvibe-backup.service
++++ b/deploy/systemd/openvibe-backup.service
@@@ -11,7 +11,8 @@ Type=oneshot
# ovhost itself, not loaded into this unit's environment.
User=root
ExecStart=/usr/local/bin/ovhost backup --all --offsite
--Environment=PATH=/usr/local/bin:/usr/bin:/bin
++# /usr/sbin for runuser (lib/executor.js also resolves it by absolute path).
++Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=OVHOST_INVENTORY=/etc/openvibe/host.json
UMask=0077
Nice=10
ddiff --git a/lib/executor.js b/lib/executor.js
iindex 7425cf1..4a98839 100644
---- a/lib/executor.js
++++ b/lib/executor.js
@@@ -52,6 +52,11 @@ function userById(uid) {
return String(uid);
}
++// runuser lives in /usr/sbin, which a systemd unit's PATH may leave out: the nightly backup unit ran with
++// PATH=/usr/local/bin:/usr/bin:/bin and every database failed with `spawn runuser ENOENT` (2026-09-24/25).
++const RUNUSER = ['/usr/sbin/runuser', '/sbin/runuser', '/usr/bin/runuser', '/bin/runuser']
++ .find((p) => { try { fs.accessSync(p, fs.constants.X_OK); return true; } catch { return false; } }) || 'runuser';
++
function createSystemExecutor({ env = process.env } = {}) {
const isRoot = () => typeof process.getuid === 'function' && process.getuid() === 0;
const me = () => { try { return os.userInfo().username; } catch { return env.USER || 'unknown'; } };
@@@ -61,7 +66,7 @@ function createSystemExecutor({ env = process.env } = {}) {
if (isRoot()) {
const pw = passwdEntry(as);
const home = pw ? pw.home : `/home/${as}`;
-- return ['runuser', ['-u', as, '--', 'env', `HOME=${home}`, `PATH=${env.PATH || '/usr/local/bin:/usr/bin:/bin'}`, cmd, ...args]];
++ return [RUNUSER, ['-u', as, '--', 'env', `HOME=${home}`, `PATH=${env.PATH || '/usr/local/bin:/usr/bin:/bin'}`, cmd, ...args]];
}
return ['sudo', ['-n', '-H', '-u', as, '--', cmd, ...args]];
}
ddiff --git a/test/executor.test.js b/test/executor.test.js
iindex 8a6442f..1421ee2 100644
---- a/test/executor.test.js
++++ b/test/executor.test.js
@@@ -108,4 +108,12 @@ runTests([
const r = await exec.run(process.execPath, ['-e', 'process.stdout.write(process.argv[1])', 'hi'], { as: await exec.userName() });
assert.deepStrictEqual([r.code, r.stdout], [0, 'hi']);
}),
++
++ test('the backup unit can find runuser: /usr/sbin is on its PATH (the 2026-09-24/25 nightly backups failed without it)', async () => {
++ const unit = require('fs').readFileSync(path.join(__dirname, '..', 'deploy', 'systemd', 'openvibe-backup.service'), 'utf8');
++ const m = /^Environment=PATH=(.+)$/m.exec(unit);
++ assert.ok(m, 'the unit sets PATH');
++ assert.ok(m[1].split(':').includes('/usr/sbin'), m[1]);
++ assert.match(require('fs').readFileSync(path.join(__dirname, '..', 'lib', 'executor.js'), 'utf8'), /\/usr\/sbin\/runuser/, 'and the executor resolves runuser by absolute path');
++ }),
]);