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
Changes
@@@ -11,7 +11,8 @@ Type=oneshot# ovhost itself, not loaded into this unit's environment.User=rootExecStart=/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:/binEnvironment=OVHOST_INVENTORY=/etc/openvibe/host.jsonUMask=0077Nice=10@@@ -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]];}@@@ -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');++ }),]);
@@ -11,7 +11,8 @@ Type=oneshot
@@ -11,7 +11,8 @@ Type=oneshot
# ovhost itself, not loaded into this unit's environment.
# ovhost itself, not loaded into this unit's environment.
User=root
User=root
ExecStart=/usr/local/bin/ovhost backup --all --offsite
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
Environment=OVHOST_INVENTORY=/etc/openvibe/host.json
UMask=0077
UMask=0077
Nice=10
Nice=10
@@ -52,6 +52,11 @@ function userById(uid) {
@@ -52,6 +52,11 @@ function userById(uid) {
return String(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 } = {}) {
function createSystemExecutor({ env = process.env } = {}) {
const isRoot = () => typeof process.getuid === 'function' && process.getuid() === 0;
const isRoot = () => typeof process.getuid === 'function' && process.getuid() === 0;
const me = () => { try { return os.userInfo().username; } catch { return env.USER || 'unknown'; } };
const me = () => { try { return os.userInfo().username; } catch { return env.USER || 'unknown'; } };
@@ -61,7 +66,7 @@ function createSystemExecutor({ env = process.env } = {}) {
@@ -61,7 +66,7 @@ function createSystemExecutor({ env = process.env } = {}) {
if (isRoot()) {
if (isRoot()) {
const pw = passwdEntry(as);
const pw = passwdEntry(as);
const home = pw ? pw.home : `/home/${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]];
return ['sudo', ['-n', '-H', '-u', as, '--', cmd, ...args]];
}
}
@@ -108,4 +108,12 @@ runTests([
@@ -108,4 +108,12 @@ runTests([
const r = await exec.run(process.execPath, ['-e', 'process.stdout.write(process.argv[1])', 'hi'], { as: await exec.userName() });
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']);
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');
+ }),
]);
]);