@calltelemetry/cli 0.5.17 → 0.6.0
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/dist/lib/certs.d.ts +16 -2
- package/dist/lib/certs.d.ts.map +1 -1
- package/dist/lib/certs.js +80 -8
- package/dist/lib/certs.js.map +1 -1
- package/dist/lib/identity.d.ts +38 -0
- package/dist/lib/identity.d.ts.map +1 -0
- package/dist/lib/identity.js +85 -0
- package/dist/lib/identity.js.map +1 -0
- package/dist/lib/prefs.d.ts +4 -0
- package/dist/lib/prefs.d.ts.map +1 -1
- package/dist/lib/prefs.js.map +1 -1
- package/dist/lib/time.d.ts +57 -0
- package/dist/lib/time.d.ts.map +1 -0
- package/dist/lib/time.js +200 -0
- package/dist/lib/time.js.map +1 -0
- package/dist/lib/users.d.ts.map +1 -1
- package/dist/lib/users.js +13 -5
- package/dist/lib/users.js.map +1 -1
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/shell/commands/config.d.ts +3 -0
- package/dist/shell/commands/config.d.ts.map +1 -1
- package/dist/shell/commands/config.js +76 -3
- package/dist/shell/commands/config.js.map +1 -1
- package/dist/shell/commands/diag.d.ts.map +1 -1
- package/dist/shell/commands/diag.js +73 -0
- package/dist/shell/commands/diag.js.map +1 -1
- package/dist/shell/commands/registry.d.ts.map +1 -1
- package/dist/shell/commands/registry.js +16 -2
- package/dist/shell/commands/registry.js.map +1 -1
- package/dist/shell/network-onboarding.d.ts +40 -10
- package/dist/shell/network-onboarding.d.ts.map +1 -1
- package/dist/shell/network-onboarding.js +648 -173
- package/dist/shell/network-onboarding.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* network-onboarding.ts — First-boot
|
|
2
|
+
* network-onboarding.ts — First-boot setup wizard for ct shell.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Six-step wizard that collects all configuration atomically before applying:
|
|
5
|
+
* 1. Network (IP/DHCP, DNS, search domain)
|
|
6
|
+
* 2. System Identity (hostname, location)
|
|
7
|
+
* 3. Admin Credentials (root password)
|
|
8
|
+
* 4. Timezone
|
|
9
|
+
* 5. NTP Time Servers
|
|
10
|
+
* 6. Preferences (auto-update, IPv6, JTAPI)
|
|
11
|
+
*
|
|
12
|
+
* After collection, shows a unified summary and offers Apply/Edit/Cancel.
|
|
13
|
+
* Apply phase runs all changes sequentially with progress output.
|
|
14
|
+
* Verify phase checks connectivity and reports warnings (not blockers).
|
|
10
15
|
*/
|
|
11
16
|
import * as readline from 'node:readline';
|
|
12
17
|
import { execSync, spawnSync } from 'node:child_process';
|
|
13
18
|
import { writeFileSync } from 'node:fs';
|
|
14
19
|
import chalk from 'chalk';
|
|
15
|
-
import {
|
|
20
|
+
import { savePrefs } from '../lib/prefs.js';
|
|
21
|
+
import { getHostname, isValidHostname, buildFqdn, applyHostname, applyAdminPassword, isStrongPassword, } from '../lib/identity.js';
|
|
22
|
+
import { getSystemTimezone, TIMEZONE_REGIONS, searchTimezones, isValidTimezone, applyTimezone, applyNtpServers, checkNtpSync, DEFAULT_NTP, } from '../lib/time.js';
|
|
23
|
+
import { generateSelfSignedCerts, getCertInfo } from '../lib/certs.js';
|
|
24
|
+
export { buildFqdn } from '../lib/identity.js';
|
|
16
25
|
export const NETWORK_SENTINEL = '/etc/ct-network-configured';
|
|
17
26
|
const DEFAULT_DNS1 = '8.8.8.8';
|
|
18
27
|
const DEFAULT_DNS2 = '8.8.4.4';
|
|
19
|
-
|
|
28
|
+
const DEFAULT_SEARCH_DOMAIN = 'local';
|
|
29
|
+
// ── Validation helpers (exported for testing) ────────────────────────────────
|
|
20
30
|
const IPV4_RE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
21
31
|
export function isValidIp(ip) {
|
|
22
32
|
const m = IPV4_RE.exec(ip.trim());
|
|
@@ -48,7 +58,14 @@ export function parsePingMs(stdout) {
|
|
|
48
58
|
// macOS: round-trip min/avg/max/stddev = 1.234/2.345/3.456/0.123 ms
|
|
49
59
|
return null;
|
|
50
60
|
}
|
|
51
|
-
|
|
61
|
+
/** Validate a DNS search domain (e.g. 'local', 'corp.example.com'). */
|
|
62
|
+
export function isValidSearchDomain(domain) {
|
|
63
|
+
if (domain.length === 0)
|
|
64
|
+
return true; // empty means no search domain
|
|
65
|
+
// RFC 1123-ish: labels separated by dots, alphanumeric + hyphens
|
|
66
|
+
return /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/.test(domain) && domain.length <= 253;
|
|
67
|
+
}
|
|
68
|
+
// ── System helpers ───────────────────────────────────────────────────────────
|
|
52
69
|
/** Return current global IPv4 address if DHCP already provided one. */
|
|
53
70
|
export function detectCurrentIp() {
|
|
54
71
|
try {
|
|
@@ -128,112 +145,669 @@ export function applyDhcp(iface) {
|
|
|
128
145
|
const up = spawnSync('sudo', ['nmcli', 'connection', 'up', 'ct-network'], { stdio: 'pipe' });
|
|
129
146
|
return up.status === 0;
|
|
130
147
|
}
|
|
131
|
-
// ── Readline helper
|
|
148
|
+
// ── Readline helper ──────────────────────────────────────────────────────────
|
|
132
149
|
function makeAsker(rl) {
|
|
133
150
|
return (question) => new Promise((resolve) => rl.question(question, resolve));
|
|
134
151
|
}
|
|
135
|
-
// ──
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
152
|
+
// ── Step progress helper ─────────────────────────────────────────────────────
|
|
153
|
+
function stepProgress(label, ok) {
|
|
154
|
+
if (ok) {
|
|
155
|
+
console.log(chalk.green(` ✓ ${label}`));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.log(chalk.red(` ✗ ${label}`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// ── Step 1: Network ──────────────────────────────────────────────────────────
|
|
162
|
+
async function stepNetwork(ask, state) {
|
|
141
163
|
console.log('');
|
|
142
|
-
console.log(chalk.bold.cyan('
|
|
164
|
+
console.log(chalk.bold.cyan(' Step 1 of 6 — Network'));
|
|
143
165
|
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
144
|
-
|
|
166
|
+
const iface = detectIface();
|
|
167
|
+
state.iface = iface;
|
|
145
168
|
const existingIp = detectCurrentIp();
|
|
169
|
+
console.log(chalk.dim(` Interface : ${iface}`));
|
|
146
170
|
if (existingIp) {
|
|
147
|
-
|
|
148
|
-
console.log(chalk.green(` ✓ Network ready (DHCP): ${existingIp}`));
|
|
171
|
+
console.log(chalk.green(` Current IP: ${existingIp} (via DHCP)`));
|
|
149
172
|
}
|
|
150
173
|
else {
|
|
151
|
-
|
|
174
|
+
console.log(chalk.yellow(' No IP address detected.'));
|
|
175
|
+
}
|
|
176
|
+
console.log('');
|
|
177
|
+
// Mode selection — order depends on whether DHCP already has an IP
|
|
178
|
+
if (existingIp) {
|
|
179
|
+
console.log(chalk.cyan(' Network mode:'));
|
|
180
|
+
console.log(chalk.dim(' 1 Keep current DHCP configuration'));
|
|
181
|
+
console.log(chalk.dim(' 2 Configure static IP'));
|
|
152
182
|
console.log('');
|
|
153
|
-
|
|
183
|
+
const mode = (await ask(chalk.bold(' Choice [1]: '))).trim() || '1';
|
|
184
|
+
if (mode === '2') {
|
|
185
|
+
state.mode = 'static';
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
state.mode = 'dhcp';
|
|
189
|
+
state.ip = existingIp;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
console.log(chalk.cyan(' Network mode:'));
|
|
194
|
+
console.log(chalk.dim(' 1 Static IP'));
|
|
195
|
+
console.log(chalk.dim(' 2 DHCP (auto)'));
|
|
196
|
+
console.log('');
|
|
197
|
+
const mode = (await ask(chalk.bold(' Choice [1]: '))).trim() || '1';
|
|
198
|
+
if (mode === '2') {
|
|
199
|
+
state.mode = 'dhcp';
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
state.mode = 'static';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Collect static IP fields
|
|
206
|
+
if (state.mode === 'static') {
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(chalk.cyan(' Static IP Configuration'));
|
|
154
209
|
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
210
|
+
let ip = '';
|
|
211
|
+
while (!isValidIp(ip)) {
|
|
212
|
+
ip = (await ask(chalk.bold(' IP Address (e.g. 192.168.1.100) : '))).trim();
|
|
213
|
+
if (!isValidIp(ip))
|
|
214
|
+
console.log(chalk.red(' ✗ Invalid IP address — enter four octets, e.g. 192.168.1.100'));
|
|
215
|
+
}
|
|
216
|
+
state.ip = ip;
|
|
217
|
+
let prefix = '';
|
|
218
|
+
while (!isValidPrefix(prefix)) {
|
|
219
|
+
prefix = (await ask(chalk.bold(' Prefix (e.g. 24 for /24) : '))).trim();
|
|
220
|
+
if (!isValidPrefix(prefix))
|
|
221
|
+
console.log(chalk.red(' ✗ Invalid prefix — enter a number between 1 and 30'));
|
|
222
|
+
}
|
|
223
|
+
state.prefix = prefix;
|
|
224
|
+
let gateway = '';
|
|
225
|
+
while (!isValidGateway(gateway)) {
|
|
226
|
+
gateway = (await ask(chalk.bold(' Gateway (e.g. 192.168.1.1) : '))).trim();
|
|
227
|
+
if (!isValidGateway(gateway))
|
|
228
|
+
console.log(chalk.red(' ✗ Invalid gateway — enter a valid IPv4 address'));
|
|
229
|
+
}
|
|
230
|
+
state.gateway = gateway;
|
|
231
|
+
}
|
|
232
|
+
// DNS (applies to both modes)
|
|
233
|
+
const dns1Raw = await ask(chalk.bold(` Primary DNS [${DEFAULT_DNS1}] : `));
|
|
234
|
+
state.dns1 = parseDnsField(dns1Raw, DEFAULT_DNS1);
|
|
235
|
+
const dns2Raw = await ask(chalk.bold(` Secondary DNS [${DEFAULT_DNS2}] : `));
|
|
236
|
+
state.dns2 = parseDnsField(dns2Raw, DEFAULT_DNS2);
|
|
237
|
+
// Search domain
|
|
238
|
+
const domainRaw = await ask(chalk.bold(` Search domain [${DEFAULT_SEARCH_DOMAIN}] : `));
|
|
239
|
+
state.searchDomain = parseDnsField(domainRaw, DEFAULT_SEARCH_DOMAIN);
|
|
240
|
+
// Quick connectivity check
|
|
241
|
+
console.log('');
|
|
242
|
+
console.log(chalk.dim(' Quick connectivity check...'));
|
|
243
|
+
if (state.mode === 'static' && state.gateway) {
|
|
244
|
+
const ms = pingGateway(state.gateway);
|
|
245
|
+
if (ms !== null) {
|
|
246
|
+
console.log(chalk.green(` ✓ Gateway ${state.gateway} reachable (${ms}ms)`));
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
console.log(chalk.yellow(` ⚠ Gateway ${state.gateway} not reachable — may work after apply`));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// DNS resolve test
|
|
253
|
+
try {
|
|
254
|
+
const result = spawnSync('nslookup', ['google.com', state.dns1], {
|
|
255
|
+
encoding: 'utf-8',
|
|
256
|
+
timeout: 5000,
|
|
257
|
+
});
|
|
258
|
+
if (result.status === 0) {
|
|
259
|
+
console.log(chalk.green(` ✓ DNS resolution via ${state.dns1} working`));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
console.log(chalk.yellow(` ⚠ DNS resolution via ${state.dns1} failed — may work after apply`));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
console.log(chalk.yellow(` ⚠ DNS check skipped — nslookup not available`));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// ── Step 2: System Identity ──────────────────────────────────────────────────
|
|
270
|
+
async function stepIdentity(ask, state) {
|
|
271
|
+
console.log('');
|
|
272
|
+
console.log(chalk.bold.cyan(' Step 2 of 6 — System Identity'));
|
|
273
|
+
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
274
|
+
const currentHostname = getHostname();
|
|
275
|
+
// Hostname
|
|
276
|
+
let hostname = '';
|
|
277
|
+
while (true) {
|
|
278
|
+
const raw = await ask(chalk.bold(` Hostname [${currentHostname}]: `));
|
|
279
|
+
hostname = raw.trim() || currentHostname;
|
|
280
|
+
if (isValidHostname(hostname))
|
|
281
|
+
break;
|
|
282
|
+
console.log(chalk.red(' ✗ Invalid hostname — alphanumeric + hyphens, 1-63 chars, no leading/trailing hyphen'));
|
|
283
|
+
}
|
|
284
|
+
state.hostname = hostname;
|
|
285
|
+
// Show FQDN
|
|
286
|
+
const fqdn = buildFqdn(hostname, state.searchDomain);
|
|
287
|
+
console.log(chalk.dim(` FQDN: ${fqdn}`));
|
|
288
|
+
// Location
|
|
289
|
+
const locationRaw = await ask(chalk.bold(' Location (optional): '));
|
|
290
|
+
state.location = locationRaw.trim();
|
|
291
|
+
// Cert info
|
|
292
|
+
console.log('');
|
|
293
|
+
console.log(chalk.dim(` A self-signed SSL certificate will be generated for ${chalk.bold(fqdn)}`));
|
|
294
|
+
console.log(chalk.dim(' To import a CA-signed cert later:'));
|
|
295
|
+
console.log(chalk.dim(' config certs import <cert.pem> <key.pem>'));
|
|
296
|
+
}
|
|
297
|
+
// ── Step 3: Admin Credentials ────────────────────────────────────────────────
|
|
298
|
+
async function stepAdmin(ask, state) {
|
|
299
|
+
console.log('');
|
|
300
|
+
console.log(chalk.bold.cyan(' Step 3 of 6 — Admin Credentials'));
|
|
301
|
+
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
302
|
+
console.log(chalk.yellow(' Default passwords must be changed before production use.'));
|
|
303
|
+
console.log(chalk.dim(' Minimum 12 characters, uppercase, lowercase, and digit required.'));
|
|
304
|
+
console.log('');
|
|
305
|
+
while (true) {
|
|
306
|
+
const pw = (await ask(chalk.bold(' New admin password: '))).trim();
|
|
307
|
+
const check = isStrongPassword(pw);
|
|
308
|
+
if (!check.valid) {
|
|
309
|
+
console.log(chalk.red(` ✗ ${check.reason}`));
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const confirm = (await ask(chalk.bold(' Confirm password: '))).trim();
|
|
313
|
+
if (confirm !== pw) {
|
|
314
|
+
console.log(chalk.red(' ✗ Passwords do not match'));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
state.adminPassword = pw;
|
|
318
|
+
console.log(chalk.green(' ✓ Password accepted'));
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// ── Step 4: Timezone ─────────────────────────────────────────────────────────
|
|
323
|
+
async function stepTimezone(ask, state) {
|
|
324
|
+
console.log('');
|
|
325
|
+
console.log(chalk.bold.cyan(' Step 4 of 6 — Timezone'));
|
|
326
|
+
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
327
|
+
const currentTz = getSystemTimezone();
|
|
328
|
+
console.log(chalk.dim(` Current timezone: ${currentTz}`));
|
|
329
|
+
console.log('');
|
|
330
|
+
// Region menu
|
|
331
|
+
while (true) {
|
|
332
|
+
console.log(chalk.cyan(' Select region:'));
|
|
333
|
+
for (let i = 0; i < TIMEZONE_REGIONS.length; i++) {
|
|
334
|
+
console.log(chalk.dim(` ${i + 1} ${TIMEZONE_REGIONS[i].name}`));
|
|
335
|
+
}
|
|
336
|
+
console.log(chalk.dim(' 4 Other / Custom IANA zone'));
|
|
337
|
+
console.log(chalk.dim(' s Search'));
|
|
158
338
|
console.log('');
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
339
|
+
const choice = (await ask(chalk.bold(' Choice: '))).trim().toLowerCase();
|
|
340
|
+
if (choice === 's') {
|
|
341
|
+
// Search mode
|
|
342
|
+
const query = (await ask(chalk.bold(' Search timezone: '))).trim();
|
|
343
|
+
if (!query)
|
|
344
|
+
continue;
|
|
345
|
+
const results = searchTimezones(query);
|
|
346
|
+
if (results.length === 0) {
|
|
347
|
+
console.log(chalk.yellow(' No matches found.'));
|
|
348
|
+
console.log('');
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
165
351
|
console.log('');
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
352
|
+
for (let i = 0; i < results.length; i++) {
|
|
353
|
+
const tz = results[i];
|
|
354
|
+
const note = tz.note ? chalk.dim(` (${tz.note})`) : '';
|
|
355
|
+
console.log(chalk.dim(` ${i + 1} ${tz.label.padEnd(14)} ${tz.offset.padEnd(10)} ${tz.value}${note}`));
|
|
169
356
|
}
|
|
170
|
-
|
|
171
|
-
|
|
357
|
+
console.log(chalk.dim(' b Back'));
|
|
358
|
+
console.log('');
|
|
359
|
+
const pick = (await ask(chalk.bold(' Choice: '))).trim().toLowerCase();
|
|
360
|
+
if (pick === 'b')
|
|
361
|
+
continue;
|
|
362
|
+
const idx = parseInt(pick, 10) - 1;
|
|
363
|
+
if (idx >= 0 && idx < results.length) {
|
|
364
|
+
state.timezone = results[idx].value;
|
|
365
|
+
console.log(chalk.green(` ✓ Timezone: ${state.timezone}`));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
console.log(chalk.red(' ✗ Invalid selection'));
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (choice === '4') {
|
|
372
|
+
// Custom IANA zone
|
|
373
|
+
while (true) {
|
|
374
|
+
const tz = (await ask(chalk.bold(' IANA timezone (e.g. America/Chicago): '))).trim();
|
|
375
|
+
if (!tz)
|
|
376
|
+
break;
|
|
377
|
+
if (isValidTimezone(tz)) {
|
|
378
|
+
state.timezone = tz;
|
|
379
|
+
console.log(chalk.green(` ✓ Timezone: ${state.timezone}`));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
console.log(chalk.red(' ✗ Invalid timezone — must be a valid IANA zone'));
|
|
172
383
|
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const regionIdx = parseInt(choice, 10) - 1;
|
|
387
|
+
if (regionIdx < 0 || regionIdx >= TIMEZONE_REGIONS.length) {
|
|
388
|
+
console.log(chalk.red(' ✗ Invalid selection'));
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
// Show zones in selected region
|
|
392
|
+
const region = TIMEZONE_REGIONS[regionIdx];
|
|
393
|
+
console.log('');
|
|
394
|
+
console.log(chalk.cyan(` ${region.name}:`));
|
|
395
|
+
for (let i = 0; i < region.zones.length; i++) {
|
|
396
|
+
const tz = region.zones[i];
|
|
397
|
+
const note = tz.note ? chalk.dim(` (${tz.note})`) : '';
|
|
398
|
+
console.log(chalk.dim(` ${i + 1} ${tz.label.padEnd(14)} ${tz.offset.padEnd(10)} ${tz.value}${note}`));
|
|
399
|
+
}
|
|
400
|
+
console.log(chalk.dim(' b Back'));
|
|
401
|
+
console.log('');
|
|
402
|
+
const zonePick = (await ask(chalk.bold(' Choice: '))).trim().toLowerCase();
|
|
403
|
+
if (zonePick === 'b')
|
|
404
|
+
continue;
|
|
405
|
+
const zoneIdx = parseInt(zonePick, 10) - 1;
|
|
406
|
+
if (zoneIdx >= 0 && zoneIdx < region.zones.length) {
|
|
407
|
+
state.timezone = region.zones[zoneIdx].value;
|
|
408
|
+
console.log(chalk.green(` ✓ Timezone: ${state.timezone}`));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
console.log(chalk.red(' ✗ Invalid selection'));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// ── Step 5: NTP ──────────────────────────────────────────────────────────────
|
|
415
|
+
async function stepNtp(ask, state) {
|
|
416
|
+
console.log('');
|
|
417
|
+
console.log(chalk.bold.cyan(' Step 5 of 6 — NTP Time Servers'));
|
|
418
|
+
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
419
|
+
const ntp1Raw = await ask(chalk.bold(` NTP Server 1 [${DEFAULT_NTP[0]}]: `));
|
|
420
|
+
state.ntp1 = parseDnsField(ntp1Raw, DEFAULT_NTP[0]);
|
|
421
|
+
const ntp2Raw = await ask(chalk.bold(` NTP Server 2 [${DEFAULT_NTP[1]}]: `));
|
|
422
|
+
state.ntp2 = parseDnsField(ntp2Raw, DEFAULT_NTP[1]);
|
|
423
|
+
console.log(chalk.green(` ✓ NTP: ${state.ntp1}, ${state.ntp2}`));
|
|
424
|
+
}
|
|
425
|
+
// ── Step 6: Preferences ──────────────────────────────────────────────────────
|
|
426
|
+
async function stepPreferences(ask, state) {
|
|
427
|
+
console.log('');
|
|
428
|
+
console.log(chalk.bold.cyan(' Step 6 of 6 — Preferences'));
|
|
429
|
+
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
430
|
+
// Auto-updates
|
|
431
|
+
const autoUpdate = (await ask(chalk.bold(' Enable automatic CLI updates? [Y/n]: '))).trim().toLowerCase();
|
|
432
|
+
state.autoUpdate = autoUpdate !== 'n' && autoUpdate !== 'no';
|
|
433
|
+
console.log(state.autoUpdate ? chalk.green(' ✓ Auto-updates enabled') : chalk.dim(' ○ Auto-updates disabled'));
|
|
434
|
+
// IPv6
|
|
435
|
+
const ipv6 = (await ask(chalk.bold(' Enable IPv6 support? [y/N]: '))).trim().toLowerCase();
|
|
436
|
+
state.ipv6 = ipv6 === 'y' || ipv6 === 'yes';
|
|
437
|
+
console.log(state.ipv6 ? chalk.green(' ✓ IPv6 enabled') : chalk.dim(' ○ IPv6 disabled'));
|
|
438
|
+
// JTAPI
|
|
439
|
+
console.log('');
|
|
440
|
+
console.log(chalk.dim(' JTAPI enables greeting injection — play custom audio greetings'));
|
|
441
|
+
console.log(chalk.dim(' to callers via Cisco JTAPI phone control. Requires a CUCM user'));
|
|
442
|
+
console.log(chalk.dim(' with CTI permissions and a JTAPI license.'));
|
|
443
|
+
const jtapi = (await ask(chalk.bold(' Enable JTAPI greeting injection? [y/N]: '))).trim().toLowerCase();
|
|
444
|
+
state.jtapi = jtapi === 'y' || jtapi === 'yes';
|
|
445
|
+
console.log(state.jtapi ? chalk.green(' ✓ JTAPI greeting injection enabled') : chalk.dim(' ○ JTAPI greeting injection disabled'));
|
|
446
|
+
}
|
|
447
|
+
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
448
|
+
/** Render a full summary of the wizard state. Exported for testing. */
|
|
449
|
+
export function renderSummary(state) {
|
|
450
|
+
const lines = [];
|
|
451
|
+
const add = (s) => lines.push(s);
|
|
452
|
+
add('');
|
|
453
|
+
add(chalk.bold.cyan(' Configuration Summary'));
|
|
454
|
+
add(chalk.dim(' ════════════════════════════════════════'));
|
|
455
|
+
// NETWORK
|
|
456
|
+
add('');
|
|
457
|
+
add(chalk.cyan(' NETWORK'));
|
|
458
|
+
add(chalk.dim(' ────────────────────────────────────────'));
|
|
459
|
+
add(` Mode : ${chalk.bold(state.mode.toUpperCase())}`);
|
|
460
|
+
if (state.mode === 'static') {
|
|
461
|
+
add(` IP Address : ${chalk.bold(`${state.ip}/${state.prefix}`)}`);
|
|
462
|
+
add(` Gateway : ${chalk.bold(state.gateway)}`);
|
|
463
|
+
}
|
|
464
|
+
else if (state.ip) {
|
|
465
|
+
add(` IP Address : ${chalk.bold(state.ip)} ${chalk.dim('(DHCP)')}`);
|
|
466
|
+
}
|
|
467
|
+
add(` Primary DNS : ${chalk.bold(state.dns1)}`);
|
|
468
|
+
add(` Secondary DNS : ${chalk.bold(state.dns2)}`);
|
|
469
|
+
add(` Search Domain : ${chalk.bold(state.searchDomain)}`);
|
|
470
|
+
add(` Interface : ${chalk.dim(state.iface)}`);
|
|
471
|
+
// SYSTEM
|
|
472
|
+
add('');
|
|
473
|
+
add(chalk.cyan(' SYSTEM'));
|
|
474
|
+
add(chalk.dim(' ────────────────────────────────────────'));
|
|
475
|
+
add(` Hostname : ${chalk.bold(state.hostname)}`);
|
|
476
|
+
add(` FQDN : ${chalk.bold(buildFqdn(state.hostname, state.searchDomain))}`);
|
|
477
|
+
add(` Location : ${state.location ? chalk.bold(state.location) : chalk.dim('(not set)')}`);
|
|
478
|
+
add(` Admin Password : ${chalk.bold('(changed)')}`);
|
|
479
|
+
// TIME
|
|
480
|
+
add('');
|
|
481
|
+
add(chalk.cyan(' TIME'));
|
|
482
|
+
add(chalk.dim(' ────────────────────────────────────────'));
|
|
483
|
+
add(` Timezone : ${chalk.bold(state.timezone)}`);
|
|
484
|
+
add(` NTP Server 1 : ${chalk.bold(state.ntp1)}`);
|
|
485
|
+
add(` NTP Server 2 : ${chalk.bold(state.ntp2)}`);
|
|
486
|
+
// PREFERENCES
|
|
487
|
+
add('');
|
|
488
|
+
add(chalk.cyan(' PREFERENCES'));
|
|
489
|
+
add(chalk.dim(' ────────────────────────────────────────'));
|
|
490
|
+
add(` Auto-updates : ${state.autoUpdate ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
491
|
+
add(` IPv6 : ${state.ipv6 ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
492
|
+
add(` JTAPI Greetings: ${state.jtapi ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
493
|
+
add('');
|
|
494
|
+
return lines.join('\n');
|
|
495
|
+
}
|
|
496
|
+
// ── Apply phase ──────────────────────────────────────────────────────────────
|
|
497
|
+
async function applyWizardState(state) {
|
|
498
|
+
console.log('');
|
|
499
|
+
console.log(chalk.bold.cyan(' Applying configuration...'));
|
|
500
|
+
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
501
|
+
let allOk = true;
|
|
502
|
+
// 1. Hostname
|
|
503
|
+
const hostnameOk = applyHostname(state.hostname);
|
|
504
|
+
stepProgress('Set hostname', hostnameOk);
|
|
505
|
+
if (!hostnameOk)
|
|
506
|
+
allOk = false;
|
|
507
|
+
// 2. Network
|
|
508
|
+
if (state.mode === 'static') {
|
|
509
|
+
const ipPrefix = `${state.ip}/${state.prefix}`;
|
|
510
|
+
const netOk = applyStaticIp(state.iface, ipPrefix, state.gateway, state.dns1, state.dns2);
|
|
511
|
+
stepProgress('Apply static IP', netOk);
|
|
512
|
+
if (!netOk)
|
|
513
|
+
allOk = false;
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
const netOk = applyDhcp(state.iface);
|
|
517
|
+
stepProgress('Apply DHCP', netOk);
|
|
518
|
+
if (!netOk)
|
|
519
|
+
allOk = false;
|
|
520
|
+
}
|
|
521
|
+
// 3. Search domain
|
|
522
|
+
const dnsSearchResult = spawnSync('sudo', [
|
|
523
|
+
'nmcli', 'con', 'mod', 'ct-network', 'ipv4.dns-search', state.searchDomain,
|
|
524
|
+
], { stdio: 'pipe' });
|
|
525
|
+
const searchOk = dnsSearchResult.status === 0;
|
|
526
|
+
stepProgress('Set DNS search domain', searchOk);
|
|
527
|
+
if (!searchOk)
|
|
528
|
+
allOk = false;
|
|
529
|
+
// 4. Admin password
|
|
530
|
+
const pwOk = applyAdminPassword(state.adminPassword);
|
|
531
|
+
stepProgress('Set admin password', pwOk);
|
|
532
|
+
if (!pwOk)
|
|
533
|
+
allOk = false;
|
|
534
|
+
// 5. Timezone
|
|
535
|
+
const tzOk = applyTimezone(state.timezone);
|
|
536
|
+
stepProgress('Set timezone', tzOk);
|
|
537
|
+
if (!tzOk)
|
|
538
|
+
allOk = false;
|
|
539
|
+
// 6. NTP servers
|
|
540
|
+
const ntpOk = applyNtpServers(state.ntp1, state.ntp2);
|
|
541
|
+
stepProgress('Configure NTP servers', ntpOk);
|
|
542
|
+
if (!ntpOk)
|
|
543
|
+
allOk = false;
|
|
544
|
+
// 7. Self-signed certs
|
|
545
|
+
try {
|
|
546
|
+
const fqdn = buildFqdn(state.hostname, state.searchDomain);
|
|
547
|
+
await generateSelfSignedCerts(fqdn, state.ip);
|
|
548
|
+
stepProgress('Generate SSL certificate', true);
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
stepProgress('Generate SSL certificate', false);
|
|
552
|
+
allOk = false;
|
|
553
|
+
}
|
|
554
|
+
// 8. Save preferences
|
|
555
|
+
const prefs = {
|
|
556
|
+
autoUpdate: state.autoUpdate,
|
|
557
|
+
ipv6: state.ipv6,
|
|
558
|
+
otel: true,
|
|
559
|
+
jtapi: state.jtapi,
|
|
560
|
+
deploymentMode: 'docker-compose',
|
|
561
|
+
k8sNamespace: 'ct',
|
|
562
|
+
k8sEnvironment: 'ct-dev',
|
|
563
|
+
timezone: state.timezone,
|
|
564
|
+
ntpServers: [state.ntp1, state.ntp2],
|
|
565
|
+
hostname: state.hostname,
|
|
566
|
+
location: state.location || undefined,
|
|
567
|
+
};
|
|
568
|
+
savePrefs(prefs);
|
|
569
|
+
stepProgress('Save preferences', true);
|
|
570
|
+
// 9. Write sentinel
|
|
571
|
+
writeSentinel();
|
|
572
|
+
stepProgress('Mark setup complete', true);
|
|
573
|
+
return allOk;
|
|
574
|
+
}
|
|
575
|
+
// ── Verify phase ─────────────────────────────────────────────────────────────
|
|
576
|
+
function runVerification(state) {
|
|
577
|
+
console.log('');
|
|
578
|
+
console.log(chalk.bold.cyan(' Verification'));
|
|
579
|
+
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
580
|
+
// 1. Ping gateway (static only)
|
|
581
|
+
if (state.mode === 'static' && state.gateway) {
|
|
582
|
+
const ms = pingGateway(state.gateway);
|
|
583
|
+
if (ms !== null) {
|
|
584
|
+
console.log(chalk.green(` ✓ Gateway ${state.gateway} reachable (${ms}ms)`));
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
console.log(chalk.yellow(` ⚠ Gateway ${state.gateway} not reachable`));
|
|
588
|
+
console.log(chalk.dim(' Remediation: network address <ip/prefix> <gateway>'));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// 2. DNS resolution — search domain
|
|
592
|
+
try {
|
|
593
|
+
const result = spawnSync('nslookup', [state.searchDomain, state.dns1], {
|
|
594
|
+
encoding: 'utf-8',
|
|
595
|
+
timeout: 5000,
|
|
596
|
+
});
|
|
597
|
+
if (result.status === 0) {
|
|
598
|
+
console.log(chalk.green(` ✓ DNS resolves ${state.searchDomain} via ${state.dns1}`));
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
console.log(chalk.yellow(` ⚠ DNS cannot resolve ${state.searchDomain}`));
|
|
602
|
+
console.log(chalk.dim(' Remediation: network dns-servers <dns1> <dns2>'));
|
|
173
603
|
}
|
|
174
|
-
|
|
175
|
-
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
console.log(chalk.yellow(' ⚠ DNS search domain check skipped'));
|
|
607
|
+
}
|
|
608
|
+
// 3. External DNS
|
|
609
|
+
try {
|
|
610
|
+
const result = spawnSync('nslookup', ['google.com', state.dns1], {
|
|
611
|
+
encoding: 'utf-8',
|
|
612
|
+
timeout: 5000,
|
|
613
|
+
});
|
|
614
|
+
if (result.status === 0) {
|
|
615
|
+
console.log(chalk.green(` ✓ External DNS resolution working`));
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
console.log(chalk.yellow(' ⚠ External DNS resolution failed'));
|
|
619
|
+
console.log(chalk.dim(' Remediation: network dns-servers <dns1> <dns2>'));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
console.log(chalk.yellow(' ⚠ External DNS check skipped'));
|
|
624
|
+
}
|
|
625
|
+
// 4. NTP sync
|
|
626
|
+
const ntpSynced = checkNtpSync();
|
|
627
|
+
if (ntpSynced) {
|
|
628
|
+
console.log(chalk.green(' ✓ NTP synchronized'));
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
console.log(chalk.yellow(' ⚠ NTP not yet synchronized — may take a few minutes'));
|
|
632
|
+
console.log(chalk.dim(' Remediation: config ntp <server1> <server2>'));
|
|
633
|
+
}
|
|
634
|
+
// 5. Certificate check
|
|
635
|
+
const certInfo = getCertInfo();
|
|
636
|
+
if (certInfo) {
|
|
637
|
+
const fqdn = buildFqdn(state.hostname, state.searchDomain);
|
|
638
|
+
if (certInfo.cn === fqdn) {
|
|
639
|
+
console.log(chalk.green(` ✓ SSL certificate CN matches ${fqdn}`));
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
console.log(chalk.yellow(` ⚠ SSL certificate CN (${certInfo.cn}) does not match FQDN (${fqdn})`));
|
|
643
|
+
console.log(chalk.dim(' Remediation: config certs regenerate'));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
console.log(chalk.yellow(' ⚠ No SSL certificate found'));
|
|
648
|
+
console.log(chalk.dim(' Remediation: config certs regenerate'));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// ── Step runner mapping ──────────────────────────────────────────────────────
|
|
652
|
+
const STEP_FUNCTIONS = [
|
|
653
|
+
stepNetwork,
|
|
654
|
+
stepIdentity,
|
|
655
|
+
stepAdmin,
|
|
656
|
+
stepTimezone,
|
|
657
|
+
stepNtp,
|
|
658
|
+
stepPreferences,
|
|
659
|
+
];
|
|
660
|
+
const STEP_NAMES = [
|
|
661
|
+
'Network',
|
|
662
|
+
'System Identity',
|
|
663
|
+
'Admin Credentials',
|
|
664
|
+
'Timezone',
|
|
665
|
+
'NTP Time Servers',
|
|
666
|
+
'Preferences',
|
|
667
|
+
];
|
|
668
|
+
// ── Main entry point ─────────────────────────────────────────────────────────
|
|
669
|
+
/**
|
|
670
|
+
* Interactive first-boot setup wizard.
|
|
671
|
+
* Runs once (when NETWORK_SENTINEL does not exist) before the shell starts.
|
|
672
|
+
* Collects all configuration, then applies atomically.
|
|
673
|
+
*/
|
|
674
|
+
export async function runNetworkOnboarding() {
|
|
675
|
+
console.log('');
|
|
676
|
+
console.log(chalk.bold.cyan(' Call Telemetry — First-Boot Setup'));
|
|
677
|
+
console.log(chalk.dim(' ═══════════════════════════════════════'));
|
|
678
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
679
|
+
const ask = makeAsker(rl);
|
|
680
|
+
try {
|
|
681
|
+
// Initialize state with defaults
|
|
682
|
+
const state = {
|
|
683
|
+
mode: 'dhcp',
|
|
684
|
+
dns1: DEFAULT_DNS1,
|
|
685
|
+
dns2: DEFAULT_DNS2,
|
|
686
|
+
searchDomain: DEFAULT_SEARCH_DOMAIN,
|
|
687
|
+
iface: 'eth0',
|
|
688
|
+
hostname: getHostname(),
|
|
689
|
+
location: '',
|
|
690
|
+
adminPassword: '',
|
|
691
|
+
timezone: getSystemTimezone(),
|
|
692
|
+
ntp1: DEFAULT_NTP[0],
|
|
693
|
+
ntp2: DEFAULT_NTP[1],
|
|
694
|
+
autoUpdate: true,
|
|
695
|
+
ipv6: false,
|
|
696
|
+
jtapi: false,
|
|
697
|
+
};
|
|
698
|
+
// Run steps 1-6 sequentially
|
|
699
|
+
for (const stepFn of STEP_FUNCTIONS) {
|
|
700
|
+
await stepFn(ask, state);
|
|
701
|
+
}
|
|
702
|
+
// Show/edit/apply loop
|
|
703
|
+
while (true) {
|
|
704
|
+
// Show unified summary
|
|
705
|
+
console.log(renderSummary(state));
|
|
706
|
+
console.log(chalk.bold(' [A]pply [E]dit [C]ancel'));
|
|
707
|
+
console.log('');
|
|
708
|
+
const action = (await ask(chalk.bold(' Choice: '))).trim().toLowerCase();
|
|
709
|
+
if (action === 'c') {
|
|
710
|
+
console.log(chalk.yellow('\n Cancelled — no changes applied.\n'));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (action === 'e') {
|
|
714
|
+
console.log('');
|
|
715
|
+
for (let i = 0; i < STEP_NAMES.length; i++) {
|
|
716
|
+
console.log(chalk.dim(` ${i + 1} ${STEP_NAMES[i]}`));
|
|
717
|
+
}
|
|
718
|
+
console.log('');
|
|
719
|
+
const stepChoice = (await ask(chalk.bold(' Which step? [1-6]: '))).trim();
|
|
720
|
+
const stepIdx = parseInt(stepChoice, 10) - 1;
|
|
721
|
+
if (stepIdx >= 0 && stepIdx < STEP_FUNCTIONS.length) {
|
|
722
|
+
await STEP_FUNCTIONS[stepIdx](ask, state);
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
console.log(chalk.red(' ✗ Invalid step number'));
|
|
726
|
+
}
|
|
727
|
+
continue; // re-show summary
|
|
728
|
+
}
|
|
729
|
+
if (action === 'a' || action === '') {
|
|
730
|
+
// Apply all
|
|
731
|
+
const ok = await applyWizardState(state);
|
|
732
|
+
if (ok) {
|
|
733
|
+
console.log('');
|
|
734
|
+
console.log(chalk.green(' ✓ All configuration applied successfully'));
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
console.log('');
|
|
738
|
+
console.log(chalk.yellow(' ⚠ Some steps had errors — check messages above'));
|
|
739
|
+
}
|
|
740
|
+
// Verify
|
|
741
|
+
runVerification(state);
|
|
742
|
+
// Show Web UI + SSH info
|
|
743
|
+
const displayIp = state.ip ?? detectCurrentIp() ?? state.hostname;
|
|
744
|
+
console.log('');
|
|
745
|
+
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
746
|
+
console.log(chalk.bold(` Web UI : https://${displayIp}`));
|
|
747
|
+
console.log(chalk.bold(` SSH : ssh root@${displayIp}`));
|
|
748
|
+
console.log('');
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
console.log(chalk.red(' ✗ Invalid choice — enter A, E, or C'));
|
|
176
752
|
}
|
|
177
753
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
await runPreferencesOnboarding();
|
|
754
|
+
finally {
|
|
755
|
+
rl.close();
|
|
181
756
|
}
|
|
182
757
|
console.log(chalk.dim(' Continuing to management shell...\n'));
|
|
183
758
|
}
|
|
184
|
-
// ── Preferences
|
|
759
|
+
// ── Preferences-only onboarding ──────────────────────────────────────────────
|
|
185
760
|
/**
|
|
186
|
-
* Interactive preferences wizard for
|
|
187
|
-
*
|
|
761
|
+
* Interactive preferences wizard for when network is already configured
|
|
762
|
+
* but preferences haven't been set yet. Runs only Step 6.
|
|
188
763
|
*/
|
|
189
764
|
export async function runPreferencesOnboarding() {
|
|
190
765
|
console.log('');
|
|
191
|
-
console.log(chalk.bold.cyan('
|
|
192
|
-
console.log(chalk.dim('
|
|
766
|
+
console.log(chalk.bold.cyan(' Call Telemetry — Preferences Setup'));
|
|
767
|
+
console.log(chalk.dim(' ═══════════════════════════════════════'));
|
|
193
768
|
console.log(chalk.dim(' Configure optional features. Press Enter to accept defaults.'));
|
|
194
|
-
console.log('');
|
|
195
769
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
196
770
|
const ask = makeAsker(rl);
|
|
197
771
|
try {
|
|
198
|
-
const
|
|
199
|
-
|
|
772
|
+
const state = {
|
|
773
|
+
mode: 'dhcp',
|
|
774
|
+
dns1: DEFAULT_DNS1,
|
|
775
|
+
dns2: DEFAULT_DNS2,
|
|
776
|
+
searchDomain: DEFAULT_SEARCH_DOMAIN,
|
|
777
|
+
iface: 'eth0',
|
|
778
|
+
hostname: getHostname(),
|
|
779
|
+
location: '',
|
|
780
|
+
adminPassword: '',
|
|
781
|
+
timezone: getSystemTimezone(),
|
|
782
|
+
ntp1: DEFAULT_NTP[0],
|
|
783
|
+
ntp2: DEFAULT_NTP[1],
|
|
784
|
+
autoUpdate: true,
|
|
200
785
|
ipv6: false,
|
|
201
|
-
otel: false,
|
|
202
786
|
jtapi: false,
|
|
203
|
-
deploymentMode: 'docker-compose',
|
|
204
|
-
k8sNamespace: 'ct',
|
|
205
|
-
k8sEnvironment: 'ct-dev',
|
|
206
787
|
};
|
|
207
|
-
|
|
208
|
-
const autoUpdate = (await ask(chalk.bold(' Enable automatic CLI updates? [Y/n]: '))).trim().toLowerCase();
|
|
209
|
-
prefs.autoUpdate = autoUpdate !== 'n' && autoUpdate !== 'no';
|
|
210
|
-
console.log(prefs.autoUpdate ? chalk.green(' ✓ Auto-updates enabled') : chalk.dim(' ○ Auto-updates disabled'));
|
|
211
|
-
// IPv6
|
|
212
|
-
const ipv6 = (await ask(chalk.bold(' Enable IPv6 support? [y/N]: '))).trim().toLowerCase();
|
|
213
|
-
prefs.ipv6 = ipv6 === 'y' || ipv6 === 'yes';
|
|
214
|
-
console.log(prefs.ipv6 ? chalk.green(' ✓ IPv6 enabled') : chalk.dim(' ○ IPv6 disabled'));
|
|
215
|
-
// OpenTelemetry
|
|
216
|
-
const otel = (await ask(chalk.bold(' Enable OpenTelemetry tracing? [y/N]: '))).trim().toLowerCase();
|
|
217
|
-
prefs.otel = otel === 'y' || otel === 'yes';
|
|
218
|
-
console.log(prefs.otel ? chalk.green(' ✓ OpenTelemetry enabled') : chalk.dim(' ○ OpenTelemetry disabled'));
|
|
219
|
-
// JTAPI phone control
|
|
220
|
-
const jtapi = (await ask(chalk.bold(' Enable JTAPI phone control? [y/N]: '))).trim().toLowerCase();
|
|
221
|
-
prefs.jtapi = jtapi === 'y' || jtapi === 'yes';
|
|
222
|
-
console.log(prefs.jtapi ? chalk.green(' ✓ JTAPI enabled') : chalk.dim(' ○ JTAPI disabled'));
|
|
788
|
+
await stepPreferences(ask, state);
|
|
223
789
|
// Summary
|
|
224
790
|
console.log('');
|
|
225
791
|
console.log(chalk.cyan(' Preferences Summary'));
|
|
226
792
|
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
227
|
-
console.log(` Auto-updates
|
|
228
|
-
console.log(` IPv6
|
|
229
|
-
console.log(`
|
|
230
|
-
console.log(` JTAPI : ${prefs.jtapi ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
793
|
+
console.log(` Auto-updates : ${state.autoUpdate ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
794
|
+
console.log(` IPv6 : ${state.ipv6 ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
795
|
+
console.log(` JTAPI Greetings : ${state.jtapi ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
231
796
|
console.log('');
|
|
232
797
|
const confirm = (await ask(chalk.bold(' Save preferences? [Y/n]: '))).trim().toLowerCase();
|
|
233
798
|
if (confirm === 'n' || confirm === 'no') {
|
|
234
799
|
console.log(chalk.yellow(' Skipped — defaults will be used. Edit later with: config preferences'));
|
|
235
800
|
return;
|
|
236
801
|
}
|
|
802
|
+
const prefs = {
|
|
803
|
+
autoUpdate: state.autoUpdate,
|
|
804
|
+
ipv6: state.ipv6,
|
|
805
|
+
otel: true,
|
|
806
|
+
jtapi: state.jtapi,
|
|
807
|
+
deploymentMode: 'docker-compose',
|
|
808
|
+
k8sNamespace: 'ct',
|
|
809
|
+
k8sEnvironment: 'ct-dev',
|
|
810
|
+
};
|
|
237
811
|
savePrefs(prefs);
|
|
238
812
|
console.log(chalk.green(' ✓ Preferences saved'));
|
|
239
813
|
}
|
|
@@ -241,103 +815,4 @@ export async function runPreferencesOnboarding() {
|
|
|
241
815
|
rl.close();
|
|
242
816
|
}
|
|
243
817
|
}
|
|
244
|
-
async function runDhcp(_rl, _ask, iface) {
|
|
245
|
-
console.log('');
|
|
246
|
-
console.log(chalk.dim(' Requesting IP via DHCP...'));
|
|
247
|
-
const ok = applyDhcp(iface);
|
|
248
|
-
if (!ok) {
|
|
249
|
-
console.log(chalk.red(' ✗ DHCP configuration failed.'));
|
|
250
|
-
console.log(chalk.dim(' Retry from the shell: network dhcp'));
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
// Wait briefly for DHCP lease
|
|
254
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
255
|
-
const ip = detectCurrentIp();
|
|
256
|
-
if (ip) {
|
|
257
|
-
writeSentinel();
|
|
258
|
-
console.log(chalk.green(` ✓ Network ready (DHCP): ${ip}`));
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
console.log(chalk.yellow(' ⚠ No IP obtained via DHCP yet — lease may still be pending.'));
|
|
262
|
-
console.log(chalk.dim(' Check status: show interfaces'));
|
|
263
|
-
}
|
|
264
|
-
console.log('');
|
|
265
|
-
}
|
|
266
|
-
async function runStaticIp(_rl, ask, iface) {
|
|
267
|
-
console.log('');
|
|
268
|
-
console.log(chalk.cyan(' Static IP Configuration'));
|
|
269
|
-
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
270
|
-
// ── Collect fields ──────────────────────────────────────────────────
|
|
271
|
-
let ip = '';
|
|
272
|
-
while (!isValidIp(ip)) {
|
|
273
|
-
ip = (await ask(chalk.bold(' IP Address (e.g. 192.168.1.100) : '))).trim();
|
|
274
|
-
if (!isValidIp(ip))
|
|
275
|
-
console.log(chalk.red(' ✗ Invalid IP address — enter four octets, e.g. 192.168.1.100'));
|
|
276
|
-
}
|
|
277
|
-
let prefix = '';
|
|
278
|
-
while (!isValidPrefix(prefix)) {
|
|
279
|
-
prefix = (await ask(chalk.bold(' Prefix (e.g. 24 for /24) : '))).trim();
|
|
280
|
-
if (!isValidPrefix(prefix))
|
|
281
|
-
console.log(chalk.red(' ✗ Invalid prefix — enter a number between 1 and 30'));
|
|
282
|
-
}
|
|
283
|
-
let gateway = '';
|
|
284
|
-
while (!isValidGateway(gateway)) {
|
|
285
|
-
gateway = (await ask(chalk.bold(' Gateway (e.g. 192.168.1.1) : '))).trim();
|
|
286
|
-
if (!isValidGateway(gateway))
|
|
287
|
-
console.log(chalk.red(' ✗ Invalid gateway — enter a valid IPv4 address'));
|
|
288
|
-
}
|
|
289
|
-
const dns1Raw = await ask(chalk.bold(` Primary DNS [${DEFAULT_DNS1}] : `));
|
|
290
|
-
const dns1 = parseDnsField(dns1Raw, DEFAULT_DNS1);
|
|
291
|
-
const dns2Raw = await ask(chalk.bold(` Secondary DNS [${DEFAULT_DNS2}] : `));
|
|
292
|
-
const dns2 = parseDnsField(dns2Raw, DEFAULT_DNS2);
|
|
293
|
-
const ipPrefix = `${ip}/${prefix}`;
|
|
294
|
-
// ── Summary ─────────────────────────────────────────────────────────
|
|
295
|
-
console.log('');
|
|
296
|
-
console.log(chalk.cyan(' Summary'));
|
|
297
|
-
console.log(chalk.dim(' ────────────────────────────────────────'));
|
|
298
|
-
console.log(` IP Address : ${chalk.bold(ipPrefix)}`);
|
|
299
|
-
console.log(` Gateway : ${chalk.bold(gateway)}`);
|
|
300
|
-
console.log(` Primary DNS : ${chalk.bold(dns1)}`);
|
|
301
|
-
console.log(` Secondary : ${chalk.bold(dns2)}`);
|
|
302
|
-
console.log(` Interface : ${chalk.dim(iface)}`);
|
|
303
|
-
console.log('');
|
|
304
|
-
const confirm = (await ask(chalk.bold(' Apply? [Y/n]: '))).trim().toLowerCase();
|
|
305
|
-
if (confirm === 'n' || confirm === 'no') {
|
|
306
|
-
console.log(chalk.yellow('\n Cancelled. Run network address <ip/prefix> <gateway> to configure manually.\n'));
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
// ── Apply ────────────────────────────────────────────────────────────
|
|
310
|
-
console.log('');
|
|
311
|
-
console.log(chalk.dim(' Applying static IP...'));
|
|
312
|
-
const ok = applyStaticIp(iface, ipPrefix, gateway, dns1, dns2);
|
|
313
|
-
if (!ok) {
|
|
314
|
-
console.log(chalk.red(' ✗ Failed to apply network configuration.'));
|
|
315
|
-
console.log(chalk.dim(' Retry from the shell:'));
|
|
316
|
-
console.log(chalk.dim(` network address ${iface} ${ipPrefix} ${gateway}`));
|
|
317
|
-
console.log(chalk.dim(` network dns-servers ${dns1} ${dns2}`));
|
|
318
|
-
console.log('');
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
// ── Verify IP ────────────────────────────────────────────────────────
|
|
322
|
-
const newIp = detectCurrentIp();
|
|
323
|
-
if (newIp) {
|
|
324
|
-
writeSentinel();
|
|
325
|
-
console.log(chalk.green(` ✓ Network configured: ${newIp}`));
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
console.log(chalk.yellow(' ⚠ Configuration applied but no IP detected yet.'));
|
|
329
|
-
}
|
|
330
|
-
// ── Ping gateway ─────────────────────────────────────────────────────
|
|
331
|
-
process.stdout.write(chalk.dim(` Checking gateway ${gateway}...`));
|
|
332
|
-
const ms = pingGateway(gateway);
|
|
333
|
-
if (ms !== null) {
|
|
334
|
-
console.log(chalk.green(` ✓ reachable (${ms}ms)`));
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
console.log(chalk.yellow(' ⚠ no response — check gateway address'));
|
|
338
|
-
}
|
|
339
|
-
console.log('');
|
|
340
|
-
console.log(chalk.dim(` Web UI: https://${newIp ?? ip}`));
|
|
341
|
-
console.log('');
|
|
342
|
-
}
|
|
343
818
|
//# sourceMappingURL=network-onboarding.js.map
|