@pixlcore/xycli 1.0.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.
Files changed (63) hide show
  1. package/README.md +543 -0
  2. package/docs/help.md +2587 -0
  3. package/docs/screenshots.md +219 -0
  4. package/docs/sync.md +920 -0
  5. package/docs/tutorial.md +204 -0
  6. package/index.js +444 -0
  7. package/lib/alerts.js +735 -0
  8. package/lib/apikey.js +425 -0
  9. package/lib/buckets.js +607 -0
  10. package/lib/categories.js +323 -0
  11. package/lib/channels.js +290 -0
  12. package/lib/config.js +86 -0
  13. package/lib/dashboard.js +237 -0
  14. package/lib/doc.js +131 -0
  15. package/lib/events.js +1314 -0
  16. package/lib/groups.js +1038 -0
  17. package/lib/jobs.js +1254 -0
  18. package/lib/log.js +125 -0
  19. package/lib/marketplace.js +386 -0
  20. package/lib/monitors.js +412 -0
  21. package/lib/plugins.js +491 -0
  22. package/lib/secrets.js +441 -0
  23. package/lib/servers.js +1502 -0
  24. package/lib/snapshots.js +498 -0
  25. package/lib/sync.js +963 -0
  26. package/lib/system.js +612 -0
  27. package/lib/tags.js +250 -0
  28. package/lib/tickets.js +681 -0
  29. package/lib/transfer.js +447 -0
  30. package/lib/utils.js +2745 -0
  31. package/lib/webhook.js +414 -0
  32. package/logo.webp +0 -0
  33. package/package.json +38 -0
  34. package/test/README.md +97 -0
  35. package/test/api-validation.test.js +170 -0
  36. package/test/categories.test.js +246 -0
  37. package/test/channels.test.js +209 -0
  38. package/test/config.test.js +96 -0
  39. package/test/doc.test.js +84 -0
  40. package/test/groups.test.js +211 -0
  41. package/test/help.test.js +26 -0
  42. package/test/helpers/common.js +127 -0
  43. package/test/helpers/pagination-preload.cjs +18 -0
  44. package/test/log.test.js +103 -0
  45. package/test/markdown.test.js +61 -0
  46. package/test/marketplace.test.js +229 -0
  47. package/test/monitors.test.js +227 -0
  48. package/test/pagination.test.js +138 -0
  49. package/test/plugins.test.js +351 -0
  50. package/test/secrets.test.js +271 -0
  51. package/test/servers.test.js +303 -0
  52. package/test/snapshots.test.js +167 -0
  53. package/test/sync-unit.test.js +446 -0
  54. package/test/sync.test.js +542 -0
  55. package/test/system.test.js +94 -0
  56. package/test/tags.test.js +230 -0
  57. package/test/tickets.test.js +261 -0
  58. package/test/toast.test.js +73 -0
  59. package/test/transfer-apikey.test.js +96 -0
  60. package/test/transfer-workflow.test.js +122 -0
  61. package/test/transfer.test.js +314 -0
  62. package/test/webhook.test.js +332 -0
  63. package/test.js +49 -0
@@ -0,0 +1,498 @@
1
+ // Snapshot Management Layer
2
+
3
+ const cli = require('pixl-cli');
4
+ const Tools = cli.Tools;
5
+
6
+ const SNAPSHOT_SOURCES = ['alert', 'user', 'watch', 'job'];
7
+ const SNAPSHOT_SEARCH_FILTERS = ['source', 'server', 'group'];
8
+
9
+ module.exports = {
10
+
11
+ async cmd_snapshots() {
12
+ // Snapshot history has three exact indexed filters and deliberately does
13
+ // not accept free-form search text. This keeps the CLI aligned with the
14
+ // focused search controls in the web interface.
15
+ this.prepSearchArgs();
16
+ if (this.args.other.length) return this.dieUsage('snapshots');
17
+ delete this.args.other;
18
+
19
+ Object.keys(this.args).forEach( key => {
20
+ if (!SNAPSHOT_SEARCH_FILTERS.includes(key)) {
21
+ var suggestion = this.findClosestString(key, SNAPSHOT_SEARCH_FILTERS);
22
+ this.die('Unsupported Snapshot search option: "--' + key + '".' + (suggestion ? ' Did you mean "--' + suggestion + '"?' : ''));
23
+ }
24
+ });
25
+
26
+ var query = [];
27
+ if ('source' in this.args) {
28
+ var source = String(this.args.source).toLowerCase();
29
+ if (!SNAPSHOT_SOURCES.includes(source)) this.die('Snapshot source must be one of: ' + SNAPSHOT_SOURCES.join(', ') + '.');
30
+ query.push('source:' + source);
31
+ }
32
+ if ('server' in this.args) query.push('server:' + this.validateSnapshotFilterID(this.args.server, 'Server'));
33
+ if ('group' in this.args) query.push('groups:' + this.validateSnapshotFilterID(this.args.group, 'Group'));
34
+
35
+ await this.getMultiple();
36
+ await this.printSnapshots({
37
+ query: query.join(' ') || '*'
38
+ });
39
+ if (this.format.match(/json/)) return;
40
+
41
+ this.printSuggestedCommands({
42
+ 'View a snapshot': 'xy snapshot SNAPSHOT_ID',
43
+ 'Show user snapshots': 'xy snapshots --source user',
44
+ 'Show server snapshots': 'xy snapshots --server SERVER_ID',
45
+ 'Show group snapshots': 'xy snapshots --group GROUP_ID'
46
+ });
47
+ },
48
+
49
+ async cmd_snapshot() {
50
+ // A single route handles both Server and Group snapshots. Load the saved
51
+ // record first, then dispatch according to its documented type property.
52
+ this.prepSearchArgs();
53
+ var id = this.args.other.shift();
54
+ if (!id || this.args.other.length) return this.dieUsage('snapshot');
55
+ delete this.args.other;
56
+
57
+ id = this.validateSnapshotID(id);
58
+ var show_monitors = this.verbose || !!this.args.monitors;
59
+ var show_processes = this.verbose || !!this.args.processes;
60
+ var show_connections = this.verbose || !!this.args.connections;
61
+ var deleting = !!this.args.delete;
62
+
63
+ ['monitors', 'processes', 'connections', 'delete'].forEach( key => {
64
+ if ((key in this.args) && (typeof(this.args[key]) != 'boolean')) this.die('Snapshot --' + key + ' must be true or false.');
65
+ delete this.args[key];
66
+ });
67
+
68
+ if (deleting && (show_monitors || show_processes || show_connections)) {
69
+ this.die('Snapshot --delete cannot be combined with detail view options.');
70
+ }
71
+ if (!deleting && ('confirm' in this.args)) this.die('Snapshot --confirm is only supported with --delete.');
72
+ if (!deleting && Tools.numKeys(this.args)) {
73
+ var key = Tools.firstKey(this.args);
74
+ var choices = ['monitors', 'processes', 'connections', 'delete'];
75
+ var suggestion = this.findClosestString(key, choices);
76
+ this.die('Unsupported Snapshot view option: "--' + key + '".' + (suggestion ? ' Did you mean "--' + suggestion + '"?' : ''));
77
+ }
78
+
79
+ await this.getMultiple();
80
+ var snapshot = await this.fetchSnapshot(id);
81
+ this.epoch = snapshot.date || this.epoch;
82
+
83
+ if (deleting) return this.deleteSnapshotFromCLI(snapshot);
84
+ if (snapshot.type == 'group') {
85
+ return this.printGroupSnapshot(snapshot, {
86
+ monitors: show_monitors,
87
+ processes: show_processes,
88
+ connections: show_connections
89
+ });
90
+ }
91
+ return this.printServerSnapshot(snapshot, {
92
+ monitors: show_monitors,
93
+ processes: show_processes,
94
+ connections: show_connections
95
+ });
96
+ },
97
+
98
+ validateSnapshotID(value) {
99
+ value = String(value || '');
100
+ if (!value.match(/^[a-z0-9]+$/i)) this.die('Snapshot ID must contain only letters and numbers.');
101
+ return value;
102
+ },
103
+
104
+ validateSnapshotFilterID(value, label) {
105
+ // Server and Group database IDs use the same conservative alphanumeric
106
+ // formats accepted by their resource commands. Reject query syntax in
107
+ // option values so these switches can only select their documented field.
108
+ var description = label == 'Group' ? 'letters, numbers, and underscores' : 'letters and numbers';
109
+ var pattern = label == 'Group' ? /^[a-z0-9_]+$/i : /^[a-z0-9]+$/i;
110
+ if ((typeof(value) != 'string') && (typeof(value) != 'number')) this.die(label + ' ID must contain only ' + description + '.');
111
+ value = String(value);
112
+ if (!value.match(pattern)) this.die(label + ' ID must contain only ' + description + '.');
113
+ return value;
114
+ },
115
+
116
+ async fetchSnapshot(id) {
117
+ cli.progress.start({ amount: 1, pct: false, text: gray('\u2192 Loading snapshot...') });
118
+ var { err, data } = await this.api.searchSnapshots({
119
+ query: '#id:' + id,
120
+ offset: 0,
121
+ limit: 1,
122
+ verbose: true
123
+ });
124
+ cli.progress.end();
125
+ if (err) {
126
+ var message = (typeof(err) == 'object') ? err.message : String(err);
127
+ if (String(message).match(/not found/i)) this.die('Could not find Snapshot: ' + id);
128
+ this.die(err);
129
+ }
130
+
131
+ var snapshot = (data.rows || [])[0];
132
+ if (!snapshot) this.die('Could not find Snapshot: ' + id);
133
+ return snapshot;
134
+ },
135
+
136
+ async deleteSnapshotFromCLI(snapshot) {
137
+ var target = snapshot.type == 'group' ?
138
+ this.getNiceGroup(snapshot.group_def || (snapshot.groups || [])[0]) :
139
+ this.getNiceServer(this.getSnapshotServer(snapshot));
140
+
141
+ this.printMutationSummary({
142
+ title: 'Delete ' + (snapshot.type == 'group' ? 'Group ' : '') + 'Snapshot',
143
+ rows: [
144
+ ['Snapshot ID', this.color('theme').bold(snapshot.id)],
145
+ ['Type', Tools.ucfirst(snapshot.type || 'server')],
146
+ ['Target', target],
147
+ ['Source', this.getSnapshotSource(snapshot)],
148
+ ['Date / Time', this.getRelativeDateTime(snapshot.date, true)]
149
+ ]
150
+ });
151
+
152
+ if (this.args.confirm !== true) {
153
+ this.toast('\u26a0\ufe0f', 'orange', "This will permanently delete the snapshot. Review it above, then add '--confirm' to continue.");
154
+ return;
155
+ }
156
+ delete this.args.confirm;
157
+ if (Tools.numKeys(this.args)) this.die('Unsupported Snapshot delete option: --' + Tools.firstKey(this.args));
158
+
159
+ var data = await this.callStandardAPI('deleteSnapshot', { id: snapshot.id }, { text: 'Deleting snapshot...' });
160
+ if (this.dry) return;
161
+ if (this.format.match(/json/)) return this.jsonOutput(data);
162
+
163
+ this.toast('\u2705', 'green', 'Successfully deleted snapshot: #' + snapshot.id);
164
+ this.printSuggestedCommands({ 'List snapshots': 'xy snapshots' });
165
+ },
166
+
167
+ async fetchSnapshotRecords(snapshot) {
168
+ // Snapshots store Alert and Job IDs so the associated records can continue
169
+ // to use the standard CLI tables. Preserve API ordering, and discard only
170
+ // error stubs for records that have since been deleted.
171
+ var alert_ids = snapshot.alerts || [];
172
+ var job_ids = snapshot.jobs || [];
173
+ var requests = [];
174
+ var alerts = [];
175
+ var jobs = [];
176
+
177
+ if (alert_ids.length) requests.push( this.api.getAlertInvocations({ ids: alert_ids }).then( result => {
178
+ if (result.err) throw result.err;
179
+ alerts = (result.data.alerts || []).filter( item => item && !item.err );
180
+ }) );
181
+ if (job_ids.length) requests.push( this.api.getJobs({ ids: job_ids }).then( result => {
182
+ if (result.err) throw result.err;
183
+ jobs = (result.data.jobs || []).filter( item => item && !item.err );
184
+ }) );
185
+
186
+ if (requests.length) {
187
+ cli.progress.start({ amount: 1, pct: false, text: gray('\u2192 Loading snapshot jobs and alerts...') });
188
+ try { await Promise.all(requests); }
189
+ catch (error) { cli.progress.end(); this.die(error); }
190
+ cli.progress.end();
191
+ }
192
+ return { alerts: alerts, jobs: jobs };
193
+ },
194
+
195
+ getSnapshotMonitorRequest(server_id, date) {
196
+ // Historical samples are minute-based. Request 60 samples ending at the
197
+ // normalized snapshot minute so the snap is the far-right chart point.
198
+ var snapshot_minute = Math.floor(date / 60) * 60;
199
+ return {
200
+ server: server_id,
201
+ sys: 'hourly',
202
+ date: snapshot_minute - (59 * 60),
203
+ limit: 60
204
+ };
205
+ },
206
+
207
+ async fetchServerSnapshotMonitors(server_id, date) {
208
+ cli.progress.start({ amount: 1, pct: false, text: gray('\u2192 Loading snapshot monitor history...') });
209
+ var { err, data } = await this.api.getHistoricalMonitorData(this.getSnapshotMonitorRequest(server_id, date));
210
+ cli.progress.end();
211
+ if (err) this.die(err);
212
+ return (data.rows || []).filter( row => row && row.date && row.totals );
213
+ },
214
+
215
+ async fetchGroupSnapshotMonitors(servers, date) {
216
+ // Reuse the Group view's bounded worker pool so a large saved fleet never
217
+ // sends more than six historical requests at once.
218
+ return this.mapGroupServersLimit(servers, async server => {
219
+ var result = await this.api.getHistoricalMonitorData(this.getSnapshotMonitorRequest(server.id, date));
220
+ if (result.err) throw result.err;
221
+ return {
222
+ server: server,
223
+ rows: (result.data.rows || []).filter( row => row && row.date && row.totals )
224
+ };
225
+ }, 'Loading snapshot monitor history...');
226
+ },
227
+
228
+ getSnapshotServer(snapshot) {
229
+ // Prefer the current/cached label when it still exists, while keeping the
230
+ // captured hostname, groups and hardware data authoritative for the view.
231
+ var current = this.servers[snapshot.server] || this.serverCache[snapshot.server] || {};
232
+ var data = snapshot.data || {};
233
+ var info = Object.assign({}, current.info || {}, {
234
+ ip: snapshot.ip || (current.info || {}).ip,
235
+ arch: data.arch || (current.info || {}).arch,
236
+ platform: data.platform || (current.info || {}).platform,
237
+ release: data.release || (current.info || {}).release,
238
+ os: data.os || (current.info || {}).os,
239
+ memory: data.memory || data.mem || (current.info || {}).memory,
240
+ cpu: data.cpu || (current.info || {}).cpu
241
+ });
242
+ return Object.assign({}, current, {
243
+ id: snapshot.server,
244
+ title: current.title || '',
245
+ hostname: snapshot.hostname || current.hostname || snapshot.server,
246
+ groups: snapshot.groups || current.groups || [],
247
+ info: info,
248
+ offline: false
249
+ });
250
+ },
251
+
252
+ getGroupSnapshotServers(snapshot) {
253
+ // Group arrays are positional by contract. Decorate copies of the saved
254
+ // Server definitions with their matching point-in-time monitor data.
255
+ return (snapshot.servers || []).map( (saved_server, idx) => {
256
+ var record = (snapshot.snapshots || [])[idx] || {};
257
+ var data = record.data || {};
258
+ var info = Object.assign({}, saved_server.info || {}, {
259
+ ip: record.ip || (saved_server.info || {}).ip,
260
+ arch: data.arch || (saved_server.info || {}).arch,
261
+ platform: data.platform || (saved_server.info || {}).platform,
262
+ release: data.release || (saved_server.info || {}).release,
263
+ os: data.os || (saved_server.info || {}).os,
264
+ memory: data.memory || data.mem || (saved_server.info || {}).memory,
265
+ cpu: data.cpu || (saved_server.info || {}).cpu,
266
+ booted: snapshot.date - (data.uptime_sec || 0)
267
+ });
268
+ return Object.assign({}, saved_server, {
269
+ groups: record.groups || saved_server.groups || [],
270
+ info: info,
271
+ snapshot_record: record,
272
+ quickmon: (snapshot.quickmons || [])[idx] || []
273
+ });
274
+ }).sort(this.sortGroupServers);
275
+ },
276
+
277
+ getSnapshotSource(snapshot) {
278
+ var source = Tools.ucfirst(String(snapshot.source || 'unknown').toLowerCase());
279
+ if ((snapshot.source == 'user') && snapshot.username) source += ' (' + snapshot.username + ')';
280
+ return source;
281
+ },
282
+
283
+ getSnapshotOS(data) {
284
+ var os = data.os || {};
285
+ return [os.distro || os.platform || data.platform, os.release || data.release].filter( item => !!item ).join(' ') || gray('(Unknown)');
286
+ },
287
+
288
+ getSnapshotIP(ip) {
289
+ return String(ip || '').replace(/^::ffff:/, '') || gray('(Unknown)');
290
+ },
291
+
292
+ printServerSnapshotSummary(snapshot, server) {
293
+ var data = snapshot.data || {};
294
+ var memory = data.memory || data.mem || {};
295
+ var cpu = data.cpu || {};
296
+ var groups = (snapshot.groups || []).map( id => this.getNiceGroup(id) ).join(', ') || gray('(None)');
297
+ var cpu_type = cpu.combo || [cpu.vendor, cpu.brand].filter( item => !!item ).join(' ') || gray('(Unknown)');
298
+
299
+ this.printBoxList({
300
+ title: 'Snapshot Summary',
301
+ rows: [
302
+ ['Snapshot ID', this.color('theme').bold(snapshot.id)],
303
+ ['Server', bold(this.getNiceServer(server))],
304
+ ['Source', this.getSnapshotSource(snapshot)],
305
+ ['Date / Time', this.getRelativeDateTime(snapshot.date, true)],
306
+ ['Groups', groups],
307
+ ['IP Address', this.getSnapshotIP(snapshot.ip)],
308
+ ['Architecture', data.arch || (data.os || {}).arch || gray('(Unknown)')],
309
+ ['Operating System', this.getSnapshotOS(data)],
310
+ ['Total RAM', this.formatServerBytes(memory.total || 0)],
311
+ ['CPU Cores', Tools.commify(cpu.physicalCores || 0) + ' physical, ' + Tools.commify(cpu.cores || 0) + ' virtual'],
312
+ ['CPU Type', cpu_type],
313
+ ['Server Uptime', Tools.getTextFromSeconds(data.uptime_sec || 0, false, true)]
314
+ ]
315
+ });
316
+ },
317
+
318
+ printGroupSnapshotSummary(snapshot, group, servers) {
319
+ var info = servers.map( server => this.getServerRecordInfo(server) );
320
+ var unique = values => Array.from(new Set(values.filter( value => !!value ))).join(', ') || gray('(None)');
321
+ var oses = info.map( item => [item.distro || item.platform, item.release].filter( value => !!value ).join(' ') );
322
+ var cpu_types = info.map( item => item.cpu.combo || item.cpu.brand || item.cpu.vendor );
323
+ var virts = info.map( item => item.virt.vendor || item.virt.type || 'None' );
324
+
325
+ this.printBoxList({
326
+ title: 'Group Snapshot Summary',
327
+ rows: [
328
+ ['Snapshot ID', this.color('theme').bold(snapshot.id)],
329
+ ['Group', bold(this.getNiceGroup(group))],
330
+ ['Source', this.getSnapshotSource(snapshot)],
331
+ ['Date / Time', this.getRelativeDateTime(snapshot.date, true)],
332
+ ['Hostname Match', group.hostname_match == '(?!)' ? gray('(None)') : group.hostname_match],
333
+ ['Servers', Tools.commify(servers.length)],
334
+ ['Alert Actions', Tools.commify((group.alert_actions || []).length)],
335
+ ['Author', group.username || gray('(Unknown)')],
336
+ ['Architectures', unique(info.map( item => item.arch ))],
337
+ ['Operating Systems', unique(oses)],
338
+ ['CPU Types', unique(cpu_types)],
339
+ ['Virtualization', unique(virts)]
340
+ ]
341
+ });
342
+ },
343
+
344
+ printSnapshotAlerts(alerts, group_mode) {
345
+ // Alert tables match the existing Server and Group views. Server snapshots
346
+ // omit the redundant Server column, while Group snapshots retain it.
347
+ if (!alerts.length) return;
348
+ alerts.sort( (a, b) => (b.date || 0) - (a.date || 0) );
349
+ var header = ['Alert ID', 'Title', 'Message'];
350
+ if (group_mode) header.push('Server');
351
+ header.push('Status', 'Started', 'Duration');
352
+
353
+ this.printPaginatedBoxTable({
354
+ title: 'Snapshot Alerts',
355
+ header: header,
356
+ rows: alerts.slice(this.offset, this.offset + this.limit),
357
+ list: { length: alerts.length },
358
+ offset: this.offset,
359
+ limit: this.limit
360
+ }, alert => {
361
+ var row = [
362
+ this.color('theme').bold(alert.id || '(Unknown)'),
363
+ this.getNiceAlert(alert.alert),
364
+ alert.message || ''
365
+ ];
366
+ if (group_mode) row.push(this.getNiceServer(alert.server));
367
+ row.push(
368
+ this.getNiceAlertStatus(alert),
369
+ alert.date ? this.getNiceDateTime(alert.date) : gray('(Unknown)'),
370
+ Tools.getTextFromSeconds(Math.max(0, (alert.end || this.epoch) - (alert.date || this.epoch)), true, true)
371
+ );
372
+ return row;
373
+ });
374
+ },
375
+
376
+ printSnapshotJobs(jobs) {
377
+ jobs.sort( (a, b) => (b.started || 0) - (a.started || 0) );
378
+ this.printActiveJobs({
379
+ title: 'Snapshot Jobs',
380
+ rows: jobs.slice(this.offset, this.offset + this.limit)
381
+ });
382
+ },
383
+
384
+ async printServerSnapshot(snapshot, options) {
385
+ var server = this.getSnapshotServer(snapshot);
386
+ var related = await this.fetchSnapshotRecords(snapshot);
387
+ var monitor_rows = options.monitors ? await this.fetchServerSnapshotMonitors(server.id, snapshot.date) : [];
388
+ var output = {
389
+ snapshot: snapshot,
390
+ server: server,
391
+ alerts: related.alerts,
392
+ jobs: related.jobs,
393
+ monitors: monitor_rows
394
+ };
395
+ if (this.format.match(/json/)) return this.jsonOutput(output);
396
+
397
+ println('\n ' + green.bold('SERVER SNAPSHOT - POINT-IN-TIME VIEW'));
398
+ this.printServerSnapshotSummary(snapshot, server);
399
+ this.printSnapshotAlerts(related.alerts, false);
400
+ this.printSnapshotJobs(related.jobs);
401
+ if ((snapshot.quickmon || []).length) this.printServerQuickmonCharts(server, snapshot.quickmon, 'QUICK LOOK - MINUTE LEADING TO SNAPSHOT');
402
+
403
+ var data = snapshot.data || {};
404
+ if (!data.dummy) {
405
+ this.printServerMemory(data);
406
+ this.printServerCPU(data);
407
+ this.printServerMonitorGrid(server, data);
408
+
409
+ if (options.monitors) this.printServerMonitorCharts(server, monitor_rows, false, 'SERVER MONITORS - HOUR LEADING TO SNAPSHOT');
410
+ else this.printServerExpansionNotice('Snapshot Monitors', '--monitors');
411
+
412
+ this.printServerContainers(data);
413
+
414
+ if (options.processes) this.printServerProcesses(data);
415
+ else this.printServerExpansionNotice('Snapshot Processes', '--processes');
416
+
417
+ if (options.connections) this.printServerConnections(data);
418
+ else this.printServerExpansionNotice('Snapshot Network Connections', '--connections');
419
+
420
+ this.printServerInterfaces(data);
421
+ this.printServerFilesystems(data);
422
+ }
423
+
424
+ this.printSuggestedCommands({
425
+ 'Show snapshot monitors': options.monitors ? '' : 'xy snapshot ' + snapshot.id + ' --monitors',
426
+ 'Show snapshot processes': options.processes ? '' : 'xy snapshot ' + snapshot.id + ' --processes',
427
+ 'Show snapshot connections': options.connections ? '' : 'xy snapshot ' + snapshot.id + ' --connections',
428
+ 'Show all snapshot details': this.verbose ? '' : 'xy snapshot ' + snapshot.id + ' --verbose',
429
+ 'Delete this snapshot': 'xy snapshot ' + snapshot.id + ' --delete',
430
+ 'List snapshots': 'xy snapshots'
431
+ });
432
+ },
433
+
434
+ async printGroupSnapshot(snapshot, options) {
435
+ var group = snapshot.group_def || { id: (snapshot.groups || [])[0], title: (snapshot.groups || [])[0] };
436
+ var servers = this.getGroupSnapshotServers(snapshot);
437
+ var related = await this.fetchSnapshotRecords(snapshot);
438
+ var historical = options.monitors ? await this.fetchGroupSnapshotMonitors(servers, snapshot.date) : [];
439
+ var history_by_server = {};
440
+ historical.forEach( item => { history_by_server[item.server.id] = item.rows; } );
441
+
442
+ var saved_by_server = {};
443
+ (snapshot.snapshots || []).forEach( record => { saved_by_server[record.server] = record; } );
444
+ var data_rows = servers.map( server => ({
445
+ server: server,
446
+ data: (saved_by_server[server.id] || {}).data || {},
447
+ rows: history_by_server[server.id] || []
448
+ }));
449
+ var quickmon = {};
450
+ servers.forEach( server => { quickmon[server.id] = server.quickmon || []; } );
451
+
452
+ var output = {
453
+ snapshot: snapshot,
454
+ group: group,
455
+ servers: servers,
456
+ alerts: related.alerts,
457
+ jobs: related.jobs,
458
+ monitors: historical
459
+ };
460
+ if (this.format.match(/json/)) return this.jsonOutput(output);
461
+
462
+ // The existing Group tables derive counts from these collections. Replace
463
+ // the live collections with the records captured by this saved snapshot.
464
+ this.activeAlerts = Object.fromEntries(related.alerts.map( item => [item.id, item] ));
465
+ this.activeJobs = Object.fromEntries(related.jobs.map( item => [item.id, item] ));
466
+
467
+ println('\n ' + green.bold('GROUP SNAPSHOT - POINT-IN-TIME VIEW'));
468
+ this.printGroupSnapshotSummary(snapshot, group, servers);
469
+ this.printGroupServers(servers, false);
470
+ this.printSnapshotAlerts(related.alerts, true);
471
+ this.printSnapshotJobs(related.jobs);
472
+ this.printGroupQuickmonCharts(group, servers, quickmon, 'average', 'GROUP QUICK LOOK - MINUTE LEADING TO SNAPSHOT', true);
473
+ this.printGroupMemory(data_rows, 'average');
474
+ this.printGroupCPU(data_rows, 'average');
475
+ this.printGroupMonitorGrid(group, data_rows, 'average');
476
+
477
+ if (options.monitors) this.printGroupMonitorCharts(group, data_rows, 'average', 'live', null, 'GROUP MONITORS - HOUR LEADING TO SNAPSHOT');
478
+ else this.printServerExpansionNotice('Snapshot Monitors', '--monitors');
479
+
480
+ this.printGroupContainers(data_rows);
481
+
482
+ if (options.processes) this.printGroupProcesses(data_rows);
483
+ else this.printServerExpansionNotice('Snapshot Processes', '--processes');
484
+
485
+ if (options.connections) this.printGroupConnections(data_rows);
486
+ else this.printServerExpansionNotice('Snapshot Network Connections', '--connections');
487
+
488
+ this.printSuggestedCommands({
489
+ 'Show snapshot monitors': options.monitors ? '' : 'xy snapshot ' + snapshot.id + ' --monitors',
490
+ 'Show snapshot processes': options.processes ? '' : 'xy snapshot ' + snapshot.id + ' --processes',
491
+ 'Show snapshot connections': options.connections ? '' : 'xy snapshot ' + snapshot.id + ' --connections',
492
+ 'Show all snapshot details': this.verbose ? '' : 'xy snapshot ' + snapshot.id + ' --verbose',
493
+ 'Delete this snapshot': 'xy snapshot ' + snapshot.id + ' --delete',
494
+ 'List snapshots': 'xy snapshots'
495
+ });
496
+ }
497
+
498
+ };