@eliware/vyops 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,8 @@
7
7
  - SSH-key authentication via `ssh2`.
8
8
  - Native VyOS curly-brace configuration validation.
9
9
  - Candidate load, `compare`, `commit-confirm`, confirmation, and save.
10
- - Post-commit hook installation from `scripts/commit/post-hooks.d/`.
10
+ - Complete `scripts/` tree synchronization, including post-commit hooks,
11
+ boot scripts, binaries, and nested support files.
11
12
  - Pushback of confirmed `/config/config.boot` changes to the current Git repository.
12
13
 
13
14
  ## Requirements
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Release Notes
2
2
 
3
+ ## 1.0.5
4
+
5
+ - Create nested temporary directories before uploading synchronized scripts.
6
+ - Fix deployment of nested files such as systemd units and boot scripts.
7
+
8
+ Verification:
9
+
10
+ - `npm test`
11
+ - `npm run lint`
12
+ - `npm run audit`
13
+ - `npm run validate:package`
14
+
15
+ ## 1.0.4
16
+
17
+ - Synchronize the complete repository `scripts/` tree to `/config/scripts`.
18
+ - Preserve script file modes and transactionally back up and roll back all
19
+ synchronized paths.
20
+ - Add recursive synchronization coverage for nested script directories.
21
+
22
+ Verification:
23
+
24
+ - `npm test`
25
+ - `npm run lint`
26
+ - `npm run audit`
27
+ - `npm run validate:package`
28
+
3
29
  ## 1.0.2
4
30
 
5
31
  - Hardened SSH host verification with the user's `known_hosts` file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eliware/vyops",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Deploy native VyOS configuration files over SSH.",
5
5
  "main": "vyops.mjs",
6
6
  "scripts": {
package/src/deploy.mjs CHANGED
@@ -11,42 +11,60 @@ export function extractCompare(output) {
11
11
  return match?.[1].replace(/^\[edit\]\s*$/gm, '').trim() ?? '';
12
12
  }
13
13
 
14
- async function installPostCommitHooks(client, config, log, runId) {
15
- const hooksDir = eliwarePath(config, '..', 'scripts', 'commit', 'post-hooks.d');
14
+ async function listScripts(directory, relative = '') {
15
+ const entries = await fs.promises.readdir(directory, { withFileTypes: true });
16
+ const files = [];
17
+ for (const entry of entries) {
18
+ const path = relative ? eliwarePath(relative, entry.name) : entry.name;
19
+ if (entry.isDirectory?.()) {
20
+ files.push(...await listScripts(eliwarePath(directory, entry.name), path));
21
+ } else if (entry.isFile?.()) {
22
+ files.push(path);
23
+ }
24
+ }
25
+ return files.sort();
26
+ }
27
+
28
+ async function installScripts(client, config, log, runId) {
29
+ const scriptsDir = eliwarePath(config, '..', 'scripts');
16
30
  let names;
17
31
  try {
18
- names = (await fs.promises.readdir(hooksDir, { withFileTypes: true }))
19
- .filter(entry => entry.isFile())
20
- .map(entry => entry.name)
21
- .sort();
32
+ names = await listScripts(scriptsDir);
22
33
  } catch (error) {
23
34
  if (error.code === 'ENOENT') return null;
24
35
  throw error;
25
36
  }
26
37
  if (!names.length) return null;
27
- if (names.some(name => !name || name === '.' || name === '..' || /[\\/]/.test(name))) {
28
- throw new Error('post-commit hook name is invalid');
38
+ if (names.some(name => !name || name === '.' || name === '..' || name.startsWith('/') || name.split('/').includes('..'))) {
39
+ throw new Error('script path is invalid');
29
40
  }
30
- const remoteDir = `/home/vyos/.post-hooks.${runId}`;
31
- const backupDir = `/home/vyos/.post-hooks-backup.${runId}`;
32
- const installDir = '/config/scripts/commit/post-hooks.d';
33
- const made = await exec(client, `mkdir -p ${JSON.stringify(remoteDir)} && sudo mkdir -p ${JSON.stringify(installDir)} ${JSON.stringify(backupDir)}`);
34
- if (made.code !== 0) throw new Error(`post-commit hook directory setup failed: ${made.stderr || made.stdout}`.trim());
41
+ const remoteDir = `/home/vyos/.scripts.${runId}`;
42
+ const backupDir = `/home/vyos/.scripts-backup.${runId}`;
43
+ const installDir = '/config/scripts';
44
+ const made = await exec(client, `mkdir -p ${JSON.stringify(remoteDir)} ${JSON.stringify(backupDir)} && sudo mkdir -p ${JSON.stringify(installDir)}`);
45
+ if (made.code !== 0) throw new Error(`script directory setup failed: ${made.stderr || made.stdout}`.trim());
35
46
  try {
36
47
  for (const name of names) {
37
- const local = eliwarePath(hooksDir, name);
48
+ const local = eliwarePath(scriptsDir, name);
38
49
  const remote = `${remoteDir}/${name}`;
39
50
  const installed = `${installDir}/${name}`;
40
51
  const backup = `${backupDir}/${name}`;
41
- const backedUp = await exec(client, `if sudo test -e ${JSON.stringify(installed)}; then sudo cp -p -- ${JSON.stringify(installed)} ${JSON.stringify(backup)}; fi`);
42
- if (backedUp.code !== 0) throw new Error(`post-commit hook backup failed (${name}): ${backedUp.stderr || backedUp.stdout}`.trim());
52
+ const parent = eliwarePath(name, '..');
53
+ const mode = (await fs.promises.stat(local)).mode & 0o777;
54
+ const backedUp = await exec(client, `if sudo test -e ${JSON.stringify(installed)}; then sudo mkdir -p ${JSON.stringify(`${backupDir}/${parent}`)} && sudo cp -p -- ${JSON.stringify(installed)} ${JSON.stringify(backup)}; fi`);
55
+ if (backedUp.code !== 0) throw new Error(`script backup failed (${name}): ${backedUp.stderr || backedUp.stdout}`.trim());
56
+ if (parent !== '.') {
57
+ const remoteParent = `${remoteDir}/${parent}`;
58
+ const remoteParentResult = await exec(client, `mkdir -p ${JSON.stringify(remoteParent)}`);
59
+ if (remoteParentResult.code !== 0) throw new Error(`script upload directory setup failed (${name}): ${remoteParentResult.stderr || remoteParentResult.stdout}`.trim());
60
+ }
43
61
  await upload(client, local, remote);
44
- const installedResult = await exec(client, `sudo install -m 755 ${JSON.stringify(remote)} ${JSON.stringify(installed)}`);
45
- if (installedResult.code !== 0) throw new Error(`post-commit hook install failed (${name}): ${installedResult.stderr || installedResult.stdout}`.trim());
46
- log(`installed post-commit hook: ${name}`);
62
+ const installedResult = await exec(client, `sudo mkdir -p ${JSON.stringify(`${installDir}/${parent}`)} && sudo install -m ${mode.toString(8)} ${JSON.stringify(remote)} ${JSON.stringify(installed)}`);
63
+ if (installedResult.code !== 0) throw new Error(`script install failed (${name}): ${installedResult.stderr || installedResult.stdout}`.trim());
64
+ log(`installed script: ${name}`);
47
65
  }
48
66
  } catch (error) {
49
- await exec(client, `sudo rm -f -- ${names.map(name => JSON.stringify(`${installDir}/${name}`)).join(' ')}; for f in ${JSON.stringify(backupDir)}/*; do [ -e "$f" ] && sudo mv -- "$f" ${JSON.stringify(installDir)}/; done; rm -rf -- ${JSON.stringify(remoteDir)} ${JSON.stringify(backupDir)}`).catch(cleanupError => log(`hook rollback failed: ${cleanupError.message}`));
67
+ await exec(client, `sudo rm -f -- ${names.map(name => JSON.stringify(`${installDir}/${name}`)).join(' ')}; sudo cp -a ${JSON.stringify(`${backupDir}/.`)} ${JSON.stringify(`${installDir}/`)} 2>/dev/null || true; rm -rf -- ${JSON.stringify(remoteDir)} ${JSON.stringify(backupDir)}`).catch(cleanupError => log(`script rollback failed: ${cleanupError.message}`));
50
68
  throw error;
51
69
  }
52
70
  return async committed => {
@@ -54,7 +72,7 @@ async function installPostCommitHooks(client, config, log, runId) {
54
72
  await exec(client, `sudo rm -rf -- ${JSON.stringify(backupDir)}; rm -rf -- ${JSON.stringify(remoteDir)}`).catch(error => log(`hook cleanup failed: ${error.message}`));
55
73
  return;
56
74
  }
57
- await exec(client, `sudo rm -f -- ${names.map(name => JSON.stringify(`${installDir}/${name}`)).join(' ')}; for f in ${JSON.stringify(backupDir)}/*; do [ -e "$f" ] && sudo mv -- "$f" ${JSON.stringify(installDir)}/; done; rm -rf -- ${JSON.stringify(remoteDir)} ${JSON.stringify(backupDir)}`).catch(error => log(`hook rollback failed: ${error.message}`));
75
+ await exec(client, `sudo rm -f -- ${names.map(name => JSON.stringify(`${installDir}/${name}`)).join(' ')}; sudo cp -a ${JSON.stringify(`${backupDir}/.`)} ${JSON.stringify(`${installDir}/`)} 2>/dev/null || true; rm -rf -- ${JSON.stringify(remoteDir)} ${JSON.stringify(backupDir)}`).catch(error => log(`script rollback failed: ${error.message}`));
58
76
  };
59
77
  }
60
78
 
@@ -70,7 +88,7 @@ export async function deploy({ target, config }) {
70
88
  debugLog(`uploading config: ${config}`);
71
89
  await upload(client, config, remote);
72
90
  debugLog(`upload complete: ${remote}`);
73
- finalizeHooks = await installPostCommitHooks(client, config, debugLog, runId);
91
+ finalizeHooks = await installScripts(client, config, debugLog, runId);
74
92
  debugLog('starting interactive deployment sequence');
75
93
  const output = await interactive(client, [
76
94
  'configure',