@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.
- package/README.md +543 -0
- package/docs/help.md +2587 -0
- package/docs/screenshots.md +219 -0
- package/docs/sync.md +920 -0
- package/docs/tutorial.md +204 -0
- package/index.js +444 -0
- package/lib/alerts.js +735 -0
- package/lib/apikey.js +425 -0
- package/lib/buckets.js +607 -0
- package/lib/categories.js +323 -0
- package/lib/channels.js +290 -0
- package/lib/config.js +86 -0
- package/lib/dashboard.js +237 -0
- package/lib/doc.js +131 -0
- package/lib/events.js +1314 -0
- package/lib/groups.js +1038 -0
- package/lib/jobs.js +1254 -0
- package/lib/log.js +125 -0
- package/lib/marketplace.js +386 -0
- package/lib/monitors.js +412 -0
- package/lib/plugins.js +491 -0
- package/lib/secrets.js +441 -0
- package/lib/servers.js +1502 -0
- package/lib/snapshots.js +498 -0
- package/lib/sync.js +963 -0
- package/lib/system.js +612 -0
- package/lib/tags.js +250 -0
- package/lib/tickets.js +681 -0
- package/lib/transfer.js +447 -0
- package/lib/utils.js +2745 -0
- package/lib/webhook.js +414 -0
- package/logo.webp +0 -0
- package/package.json +38 -0
- package/test/README.md +97 -0
- package/test/api-validation.test.js +170 -0
- package/test/categories.test.js +246 -0
- package/test/channels.test.js +209 -0
- package/test/config.test.js +96 -0
- package/test/doc.test.js +84 -0
- package/test/groups.test.js +211 -0
- package/test/help.test.js +26 -0
- package/test/helpers/common.js +127 -0
- package/test/helpers/pagination-preload.cjs +18 -0
- package/test/log.test.js +103 -0
- package/test/markdown.test.js +61 -0
- package/test/marketplace.test.js +229 -0
- package/test/monitors.test.js +227 -0
- package/test/pagination.test.js +138 -0
- package/test/plugins.test.js +351 -0
- package/test/secrets.test.js +271 -0
- package/test/servers.test.js +303 -0
- package/test/snapshots.test.js +167 -0
- package/test/sync-unit.test.js +446 -0
- package/test/sync.test.js +542 -0
- package/test/system.test.js +94 -0
- package/test/tags.test.js +230 -0
- package/test/tickets.test.js +261 -0
- package/test/toast.test.js +73 -0
- package/test/transfer-apikey.test.js +96 -0
- package/test/transfer-workflow.test.js +122 -0
- package/test/transfer.test.js +314 -0
- package/test/webhook.test.js +332 -0
- package/test.js +49 -0
package/lib/snapshots.js
ADDED
|
@@ -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
|
+
};
|