@eliware/vyops 1.0.6 → 1.0.7

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Features
6
6
 
7
- - SSH-key authentication via `ssh2`.
7
+ - SSH-key authentication via `ssh2`, with optional password bootstrap via stdin.
8
8
  - Native VyOS curly-brace configuration validation.
9
9
  - Candidate load, `compare`, `commit-confirm`, confirmation, and save.
10
10
  - Complete `scripts/` tree synchronization, including post-commit hooks,
@@ -40,6 +40,14 @@ vyops Deploy or dry-run a configuration.
40
40
 
41
41
  The target must be supplied as `user@host`. The config must use native VyOS curly-brace syntax.
42
42
 
43
+ For a router that has not received its SSH key yet, provide the SSH password through stdin:
44
+
45
+ ```sh
46
+ printf '%s\n' "$VYOS_PASSWORD" | vyops --password-stdin vyos@new-router config.boot
47
+ ```
48
+
49
+ Password mode still requires the target host key to be present in `known_hosts`; it does not disable host verification. Passwords are not accepted as command-line arguments or written to logs.
50
+
43
51
  ## Usage
44
52
 
45
53
  Deploy:
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Release Notes
2
2
 
3
+ ## 1.0.7
4
+
5
+ - Add password-based SSH bootstrap through the secure `--password-stdin` mode.
6
+ - Preserve strict `known_hosts` verification when using password authentication.
7
+ - Handle VyOS return-only pager prompts and disable paging before deployment compares.
8
+ - Add packaged CLI, password-authentication, backup failure, and deployment regression tests.
9
+ - Add opt-in real-VyOS integration test scaffolding for dry-run and backup workflows.
10
+
11
+ Verification:
12
+
13
+ - `npm test` (100% coverage across statements, branches, functions, and lines)
14
+ - `npm run lint`
15
+ - `npm run audit`
16
+ - `npm run validate:package`
17
+
3
18
  ## 1.0.6
4
19
 
5
20
  - Add `--backup` to download the active `config.boot` and complete
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eliware/vyops",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Deploy native VyOS configuration files over SSH.",
5
5
  "main": "vyops.mjs",
6
6
  "scripts": {
@@ -48,4 +48,4 @@
48
48
  "LICENSE",
49
49
  "RELEASE_NOTES.md"
50
50
  ]
51
- }
51
+ }
package/src/args.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  export const usage = `Usage:
2
- vyops [--dry-run] [--force] [--debug] <user@hostname-or-ip> <config.boot>
3
- vyops --backup [--debug] <user@hostname-or-ip> <destination-directory>
2
+ vyops [--dry-run] [--force] [--debug] [--password-stdin] <user@hostname-or-ip> <config.boot>
3
+ vyops --backup [--debug] [--password-stdin] <user@hostname-or-ip> <destination-directory>
4
4
  vyops --help
5
5
  vyops --version`;
6
6
 
@@ -9,8 +9,8 @@ export function parseArgs(argv) {
9
9
  if (argv.length === 1 && argv[0] === '--version') return { version: true };
10
10
 
11
11
  const options = new Set(argv.filter(value => value.startsWith('--')));
12
- if ([...options].some(option => !['--dry-run', '--force', '--debug', '--backup'].includes(option))) throw new Error(usage);
12
+ if ([...options].some(option => !['--dry-run', '--force', '--debug', '--backup', '--password-stdin'].includes(option))) throw new Error(usage);
13
13
  const values = argv.filter(value => !value.startsWith('--'));
14
14
  if (values.length !== 2) throw new Error(usage);
15
- return { target: values[0], config: values[1], ...(options.has('--backup') ? { backup: true } : {}), ...(options.has('--dry-run') ? { dryRun: true } : {}), ...(options.has('--force') ? { force: true } : {}), ...(options.has('--debug') ? { debug: true } : {}) };
15
+ return { target: values[0], config: values[1], ...(options.has('--backup') ? { backup: true } : {}), ...(options.has('--dry-run') ? { dryRun: true } : {}), ...(options.has('--force') ? { force: true } : {}), ...(options.has('--debug') ? { debug: true } : {}), ...(options.has('--password-stdin') ? { passwordStdin: true } : {}) };
16
16
  }
package/src/backup.mjs CHANGED
@@ -7,8 +7,8 @@ function remoteScriptPath(value) {
7
7
  return name;
8
8
  }
9
9
 
10
- export async function backup({ target, config }) {
11
- const client = await connect(target);
10
+ export async function backup({ target, config, password }) {
11
+ const client = password === undefined ? await connect(target) : await connect(target, { password });
12
12
  try {
13
13
  await fs.promises.mkdir(config, { recursive: true });
14
14
  await fs.promises.mkdir(path(config, 'scripts'), { recursive: true });
package/src/deploy.mjs CHANGED
@@ -76,10 +76,10 @@ async function installScripts(client, config, log, runId) {
76
76
  };
77
77
  }
78
78
 
79
- export async function deploy({ target, config }) {
79
+ export async function deploy({ target, config, password }) {
80
80
  const debugLog = message => log.debug(`[vyops] ${message}`);
81
81
  debugLog(`connecting: ${target}`);
82
- const client = await connect(target);
82
+ const client = password === undefined ? await connect(target) : await connect(target, { password });
83
83
  debugLog('SSH connected');
84
84
  let finalizeHooks;
85
85
  let hooksFinalized = false;
@@ -95,6 +95,7 @@ export async function deploy({ target, config }) {
95
95
  const output = await interactive(client, [
96
96
  'configure',
97
97
  { command: `load ${remote}`, reject: /(?:load failed|commit failed|commit aborted|cannot commit|configuration (?:commit )?failed|invalid configuration|error|invalid)/i },
98
+ 'run set terminal length 0',
98
99
  "printf '%s\\n' '--- compare ---'",
99
100
  'compare',
100
101
  "printf '%s\\n' '--- end compare ---'",
package/src/main.mjs CHANGED
@@ -6,6 +6,7 @@ import { readAndValidateConfig } from './validate.mjs';
6
6
  import { pushBack, shouldSkip } from './git.mjs';
7
7
  import { closeAll } from './ssh.mjs';
8
8
  import { log, registerHandlers, registerSignals } from '@eliware/common';
9
+ import { readFile } from 'node:fs/promises';
9
10
 
10
11
  const errors = registerHandlers({ log });
11
12
  const signals = registerSignals({ log, shutdownHook: async () => closeAll() });
@@ -21,6 +22,10 @@ try {
21
22
  log.info(packageJson.version);
22
23
  process.exit(0);
23
24
  }
25
+ if (args.passwordStdin) {
26
+ args.password = (await readFile(0, 'utf8')).replace(/\r?\n$/, '');
27
+ if (!args.password) throw new Error('password-stdin received an empty password');
28
+ }
24
29
  if (args.backup) {
25
30
  await backup(args);
26
31
  log.info(`Backup successful: ${args.config}`);
package/src/ssh.mjs CHANGED
@@ -67,10 +67,12 @@ function verifyKnownHost(host, key, knownHosts) {
67
67
  return matched;
68
68
  }
69
69
 
70
- export async function connect(target) {
70
+ export async function connect(target, { password } = {}) {
71
71
  const { username, host } = parseTarget(target);
72
72
  log.debug(`[vyops] SSH connecting: ${username}@${host}`);
73
- const privateKey = await fs.promises.readFile(process.env.VYOPS_SSH_KEY || `${process.env.HOME}/.ssh/id_rsa`);
73
+ const privateKey = password === undefined
74
+ ? await fs.promises.readFile(process.env.VYOPS_SSH_KEY || `${process.env.HOME}/.ssh/id_rsa`)
75
+ : undefined;
74
76
  const knownHostsPath = `${process.env.HOME}/.ssh/known_hosts`;
75
77
  const knownHosts = await fs.promises.readFile(knownHostsPath, 'utf8');
76
78
  return new Promise((resolve, reject) => {
@@ -84,6 +86,7 @@ export async function connect(target) {
84
86
  username,
85
87
  agent: process.env.SSH_AUTH_SOCK,
86
88
  privateKey,
89
+ ...(password === undefined ? {} : { password }),
87
90
  hostVerifier: (key, callback) => callback(verifyKnownHost(host, key, knownHosts)),
88
91
  readyTimeout: CONNECT_TIMEOUT,
89
92
  authTimeout: CONNECT_TIMEOUT,
@@ -190,12 +193,16 @@ export function interactive(client, commands, log = () => {}) {
190
193
  reject(new Error(`interactive command failed: ${currentItem.command}`));
191
194
  return;
192
195
  }
193
- const pager = /(?:^|\n):\s*$/.test(cleaned) || /--More--\s*$/i.test(cleaned);
196
+ const pagerReturn = /No next tag\s*\(press RETURN\)/i.test(cleaned);
197
+ const pager = /(?:^|\n):\s*$/.test(cleaned)
198
+ || /--More--\s*$/i.test(cleaned)
199
+ || pagerReturn;
194
200
  log(`state after data: index=${index}, waiting=${waiting}, answering=${answering}, prompt=${prompt(cleaned)}, pager=${pager}, bytes=${text.length}`);
195
201
  if (pager) {
196
- log('pager prompt detected; sending space');
197
- stream.write(' ');
198
- log('write complete: space');
202
+ const key = pagerReturn ? '\n' : ' ';
203
+ log(`pager prompt detected; sending ${pagerReturn ? 'return' : 'space'}`);
204
+ stream.write(key);
205
+ log(`write complete: ${pagerReturn ? 'return' : 'space'}`);
199
206
  return;
200
207
  }
201
208
  if (/Proceed\s*\?\s*\[Y\/n\]/i.test(cleaned) && !answering) {