@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/alerts.js
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
// Alert Layer
|
|
2
|
+
//
|
|
3
|
+
// xyOps uses the word "alert" for both reusable alert definitions and the
|
|
4
|
+
// historical invocations they produce. The CLI keeps the short word, and uses
|
|
5
|
+
// the operation to select the resource wherever possible. Ambiguous get and
|
|
6
|
+
// delete operations resolve an exact definition ID first, then an invocation
|
|
7
|
+
// ID. The two ID namespaces are designed not to collide.
|
|
8
|
+
|
|
9
|
+
const cli = require('pixl-cli');
|
|
10
|
+
const Tools = cli.Tools;
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
|
|
14
|
+
async cmd_alerts() {
|
|
15
|
+
// The plural command defaults to definitions. Searching is the one
|
|
16
|
+
// collection operation that always targets alert invocations.
|
|
17
|
+
var cmd = this.args.other[0];
|
|
18
|
+
if (cmd == 'list') {
|
|
19
|
+
this.args.other.shift();
|
|
20
|
+
return await this.cmd_get_alert_definitions();
|
|
21
|
+
}
|
|
22
|
+
else if (cmd == 'search') {
|
|
23
|
+
this.args.other.shift();
|
|
24
|
+
return await this.cmd_search_alert_invocations();
|
|
25
|
+
}
|
|
26
|
+
else return await this.cmd_get_alert_definitions();
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
async cmd_alert() {
|
|
30
|
+
// Singular router, e.g. `xy alert create` or `xy alert INVOCATION_ID`.
|
|
31
|
+
var cmd = this.args.other.shift();
|
|
32
|
+
if (!cmd) return this.dieUsage('alert');
|
|
33
|
+
|
|
34
|
+
switch (cmd) {
|
|
35
|
+
case 'list': await this.cmd_get_alert_definitions(); break;
|
|
36
|
+
case 'search': await this.cmd_search_alert_invocations(); break;
|
|
37
|
+
case 'get': await this.cmd_get_alert(); break;
|
|
38
|
+
case 'test': await this.cmd_test_alert_definition(); break;
|
|
39
|
+
|
|
40
|
+
case 'create': await this.cmd_create_alert_definition(); break;
|
|
41
|
+
case 'update': await this.cmd_update_alert_definition(); break;
|
|
42
|
+
case 'delete': await this.cmd_delete_alert(); break;
|
|
43
|
+
|
|
44
|
+
default:
|
|
45
|
+
// A bare selector opens either a definition or an invocation.
|
|
46
|
+
this.args.other.unshift(cmd);
|
|
47
|
+
await this.cmd_get_alert();
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async cmd_get_alert_definitions() {
|
|
53
|
+
// List and locally filter the definitions already returned by getMultiple.
|
|
54
|
+
this.prepSearchArgs();
|
|
55
|
+
await this.getMultiple();
|
|
56
|
+
var alerts = this.alerts.slice(0);
|
|
57
|
+
var is_filtered = false;
|
|
58
|
+
|
|
59
|
+
if (this.args.other.length) {
|
|
60
|
+
var search = this.args.other.join(' ');
|
|
61
|
+
alerts = this.findObjectsFuzzy(alerts, {
|
|
62
|
+
id: search,
|
|
63
|
+
title: search,
|
|
64
|
+
expression: search,
|
|
65
|
+
message: search
|
|
66
|
+
}, 1);
|
|
67
|
+
is_filtered = true;
|
|
68
|
+
}
|
|
69
|
+
delete this.args.other;
|
|
70
|
+
|
|
71
|
+
if ('disabled' in this.args) {
|
|
72
|
+
var disabled = this.parseAlertBoolean(this.args.disabled, 'disabled');
|
|
73
|
+
alerts = alerts.filter( alert => !alert.enabled === disabled );
|
|
74
|
+
delete this.args.disabled;
|
|
75
|
+
is_filtered = true;
|
|
76
|
+
}
|
|
77
|
+
if ('enabled' in this.args) {
|
|
78
|
+
var enabled = this.parseAlertBoolean(this.args.enabled, 'enabled');
|
|
79
|
+
alerts = alerts.filter( alert => !!alert.enabled === enabled );
|
|
80
|
+
delete this.args.enabled;
|
|
81
|
+
is_filtered = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if ('group' in this.args) {
|
|
85
|
+
var groups = this.parseAlertList(this.args.group).map( selector => {
|
|
86
|
+
var group = this.findObjectFuzzy(this.groups, {
|
|
87
|
+
id: selector,
|
|
88
|
+
title: selector
|
|
89
|
+
}, 1);
|
|
90
|
+
if (!group) this.die("Could not find server group based on your criteria: " + selector);
|
|
91
|
+
return group.id;
|
|
92
|
+
});
|
|
93
|
+
alerts = alerts.filter( alert => this.includesAll(alert.groups || [], groups) );
|
|
94
|
+
delete this.args.group;
|
|
95
|
+
is_filtered = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if ('monitor' in this.args) {
|
|
99
|
+
var monitor = this.findObjectFuzzy(this.monitors, {
|
|
100
|
+
id: this.args.monitor,
|
|
101
|
+
title: this.args.monitor
|
|
102
|
+
}, 1);
|
|
103
|
+
if (!monitor) return this.die("Could not find monitor based on your criteria: " + this.args.monitor);
|
|
104
|
+
alerts = alerts.filter( alert => alert.monitor_id == monitor.id );
|
|
105
|
+
delete this.args.monitor;
|
|
106
|
+
is_filtered = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (Tools.numKeys(this.args)) {
|
|
110
|
+
// Remaining options are ordinary fuzzy property filters.
|
|
111
|
+
alerts = this.findObjectsFuzzy(alerts, this.args);
|
|
112
|
+
is_filtered = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
alerts.sort( function(a, b) {
|
|
116
|
+
return String(a.title || '').toLowerCase().localeCompare(String(b.title || '').toLowerCase());
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
if (this.format.match(/json/)) return this.jsonOutput(alerts);
|
|
120
|
+
|
|
121
|
+
var total = alerts.length;
|
|
122
|
+
var rows = alerts.slice(this.offset, this.offset + this.limit);
|
|
123
|
+
this.printPaginatedBoxTable({
|
|
124
|
+
title: is_filtered ? 'Filtered Alert Definitions' : 'All Alert Definitions',
|
|
125
|
+
header: ['Alert ID', 'Title', 'Expression', 'Groups', 'Status', 'Modified'],
|
|
126
|
+
rows: rows,
|
|
127
|
+
list: { length: total },
|
|
128
|
+
offset: this.offset,
|
|
129
|
+
limit: this.limit
|
|
130
|
+
}, alert => {
|
|
131
|
+
return [
|
|
132
|
+
this.color('theme').bold(alert.id),
|
|
133
|
+
bold(alert.title),
|
|
134
|
+
String(alert.expression || '').replace(/\s+/g, ' ').trim(),
|
|
135
|
+
this.getNiceAlertGroups(alert.groups, '(All)'),
|
|
136
|
+
this.getNiceEnabled(alert.enabled),
|
|
137
|
+
this.getRelativeDateTime(alert.modified, true)
|
|
138
|
+
];
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
this.printSuggestedCommands({
|
|
142
|
+
"View an alert": "xy alert ID_OR_TITLE",
|
|
143
|
+
"Create an alert definition": "xy alert create --title \"My Alert\" --expression \"...\" --message \"...\"",
|
|
144
|
+
"Search alert invocations": "xy alerts search [QUERY]"
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
async cmd_search_alert_invocations() {
|
|
149
|
+
// Build an invocation query from friendly resource selectors and flags.
|
|
150
|
+
this.prepSearchArgs();
|
|
151
|
+
await this.getMultiple();
|
|
152
|
+
var query_parts = [];
|
|
153
|
+
var sort_dir = -1;
|
|
154
|
+
|
|
155
|
+
if (this.args.other.length) query_parts.push(this.args.other.join(' '));
|
|
156
|
+
delete this.args.other;
|
|
157
|
+
if (this.args.query) query_parts.push(this.args.query);
|
|
158
|
+
delete this.args.query;
|
|
159
|
+
|
|
160
|
+
if (this.args.alert) {
|
|
161
|
+
var alert = this.findObjectFuzzy(this.alerts, {
|
|
162
|
+
id: this.args.alert,
|
|
163
|
+
title: this.args.alert
|
|
164
|
+
}, 1);
|
|
165
|
+
if (!alert) return this.die("Could not find alert definition based on your criteria: " + this.args.alert);
|
|
166
|
+
query_parts.push('alert:' + alert.id);
|
|
167
|
+
delete this.args.alert;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (this.args.server) {
|
|
171
|
+
var server = this.findObjectFuzzy(Object.values(this.servers), {
|
|
172
|
+
id: this.args.server,
|
|
173
|
+
title: this.args.server,
|
|
174
|
+
hostname: this.args.server,
|
|
175
|
+
ip: this.args.server
|
|
176
|
+
}, 1);
|
|
177
|
+
if (!server) return this.die("Could not find server based on your criteria: " + this.args.server);
|
|
178
|
+
query_parts.push('server:' + server.id);
|
|
179
|
+
delete this.args.server;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this.args.group) {
|
|
183
|
+
var group = this.findObjectFuzzy(this.groups, {
|
|
184
|
+
id: this.args.group,
|
|
185
|
+
title: this.args.group
|
|
186
|
+
}, 1);
|
|
187
|
+
if (!group) return this.die("Could not find server group based on your criteria: " + this.args.group);
|
|
188
|
+
query_parts.push('groups:' + group.id);
|
|
189
|
+
delete this.args.group;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if ('active' in this.args) {
|
|
193
|
+
query_parts.push('active:' + this.parseAlertBoolean(this.args.active, 'active'));
|
|
194
|
+
delete this.args.active;
|
|
195
|
+
}
|
|
196
|
+
if ('cleared' in this.args) {
|
|
197
|
+
var cleared = this.parseAlertBoolean(this.args.cleared, 'cleared');
|
|
198
|
+
query_parts.push('active:' + !cleared);
|
|
199
|
+
delete this.args.cleared;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (this.args.date) {
|
|
203
|
+
var date_query = this.getDateRangeQuery('start', this.args.date);
|
|
204
|
+
if (!date_query) {
|
|
205
|
+
var date_text = String(this.args.date);
|
|
206
|
+
if (date_text.match(/^\d{4}\-\d{2}\-\d{2}$/)) {
|
|
207
|
+
// A calendar date means the entire local day, not only midnight.
|
|
208
|
+
var start_epoch = Tools.parseDate(date_text + ' 00:00:00');
|
|
209
|
+
var next_day = new Date(start_epoch * 1000);
|
|
210
|
+
next_day.setDate(next_day.getDate() + 1);
|
|
211
|
+
date_query = 'start:>=' + start_epoch + ' start:<' + Math.floor(next_day.getTime() / 1000);
|
|
212
|
+
}
|
|
213
|
+
else date_query = date_text.includes(':') ? date_text : 'start:' + date_text;
|
|
214
|
+
}
|
|
215
|
+
query_parts.push(date_query);
|
|
216
|
+
delete this.args.date;
|
|
217
|
+
}
|
|
218
|
+
if (this.args.oldest) {
|
|
219
|
+
if (this.parseAlertBoolean(this.args.oldest, 'oldest')) sort_dir = 1;
|
|
220
|
+
delete this.args.oldest;
|
|
221
|
+
}
|
|
222
|
+
if (this.args.sort) {
|
|
223
|
+
if (String(this.args.sort).match(/^(oldest|asc|ascending)$/i)) sort_dir = 1;
|
|
224
|
+
else if (!String(this.args.sort).match(/^(newest|desc|descending)$/i)) {
|
|
225
|
+
return this.die("Invalid alert invocation sort order: " + this.args.sort);
|
|
226
|
+
}
|
|
227
|
+
delete this.args.sort;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// These IDs may refer to deleted resources, so pass them through without
|
|
231
|
+
// requiring that the related object still exists in a current global list.
|
|
232
|
+
['job', 'ticket'].forEach( key => {
|
|
233
|
+
if (this.args[key]) query_parts.push(key + 's:' + this.args[key]);
|
|
234
|
+
delete this.args[key];
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Preserve access to the complete xyOps search language. Any additional
|
|
238
|
+
// named option becomes a field:value criterion, matching the jobs command.
|
|
239
|
+
Object.keys(this.args).forEach( key => {
|
|
240
|
+
query_parts.push(key + ':' + this.args[key]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
var query = query_parts.join(' ').trim();
|
|
244
|
+
await this.printAlertInvocations({
|
|
245
|
+
title: query ? 'Alert Invocation Search Results' : 'All Alert Invocations',
|
|
246
|
+
query: query,
|
|
247
|
+
sort_dir: sort_dir
|
|
248
|
+
});
|
|
249
|
+
if (this.format.match(/json/)) return;
|
|
250
|
+
|
|
251
|
+
this.printSuggestedCommands({
|
|
252
|
+
"Active invocations": "xy alerts search --active",
|
|
253
|
+
"Cleared invocations": "xy alerts search --cleared",
|
|
254
|
+
"Invocations from today": "xy alerts search --date today",
|
|
255
|
+
"View an invocation": "xy alert INVOCATION_ID"
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
async cmd_get_alert() {
|
|
260
|
+
// Resolve an ambiguous selector in the documented order: exact definition
|
|
261
|
+
// ID, exact invocation ID, then fuzzy definition title.
|
|
262
|
+
await this.getMultiple();
|
|
263
|
+
var selector = this.getAlertSelector();
|
|
264
|
+
var resolved = await this.resolveAlert(selector, true);
|
|
265
|
+
if (!resolved) return this.die("Could not find an alert definition or invocation based on your criteria: " + selector);
|
|
266
|
+
// Invocation details include paginated related records; definitions do not.
|
|
267
|
+
if (resolved.type == 'invocation') this.prepSearchArgs();
|
|
268
|
+
if (Tools.numKeys(this.args)) return this.die("Unexpected alert option: --" + Tools.firstKey(this.args));
|
|
269
|
+
|
|
270
|
+
if (this.format.match(/json/)) return this.jsonOutput(resolved.alert);
|
|
271
|
+
if (resolved.type == 'definition') this.printAlertDefinition(resolved.alert);
|
|
272
|
+
else await this.printAlertInvocation(resolved.alert);
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
async cmd_create_alert_definition() {
|
|
276
|
+
// Create a definition with the same safe defaults used by the web UI.
|
|
277
|
+
await this.getMultiple();
|
|
278
|
+
if ('delete' in this.args) return this.die("The '--delete' option is only available for update commands.");
|
|
279
|
+
|
|
280
|
+
var params = {
|
|
281
|
+
title: '',
|
|
282
|
+
expression: '',
|
|
283
|
+
message: '',
|
|
284
|
+
groups: [],
|
|
285
|
+
actions: [],
|
|
286
|
+
monitor_id: '',
|
|
287
|
+
enabled: true,
|
|
288
|
+
samples: 1,
|
|
289
|
+
exclusive_actions: false,
|
|
290
|
+
limit_jobs: false,
|
|
291
|
+
abort_jobs: false,
|
|
292
|
+
notes: ''
|
|
293
|
+
};
|
|
294
|
+
this.mergeDotArgs(params, this.args);
|
|
295
|
+
delete params.other;
|
|
296
|
+
this.processAlertDefinitionUpdates(params);
|
|
297
|
+
this.validateAlertDefinition(params);
|
|
298
|
+
|
|
299
|
+
var data = await this.callStandardAPI('createAlert', params, {
|
|
300
|
+
text: 'Creating Alert Definition...'
|
|
301
|
+
});
|
|
302
|
+
if (this.dry) return;
|
|
303
|
+
if (this.format.match(/json/)) return this.jsonOutput(data.alert);
|
|
304
|
+
|
|
305
|
+
this.toast('✅', 'green', "Successfully created Alert Definition: #" + data.alert.id);
|
|
306
|
+
this.printSuggestedCommands({
|
|
307
|
+
"View alert definition": `xy alert ${data.alert.id}`,
|
|
308
|
+
"Test alert definition": `xy alert test ${data.alert.id} --server SERVER_ID_OR_TITLE`,
|
|
309
|
+
"Update alert definition": `xy alert update ${data.alert.id} [--KEY VALUE, ...]`,
|
|
310
|
+
"List alert definitions": "xy alerts"
|
|
311
|
+
});
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
async cmd_update_alert_definition() {
|
|
315
|
+
// Updates always target definitions and require an exact, safe internal ID.
|
|
316
|
+
await this.getMultiple();
|
|
317
|
+
var id = this.getAlertIDArgument('Alert Definition');
|
|
318
|
+
var alert = Tools.findObject(this.alerts, { id: id });
|
|
319
|
+
if (!alert) return this.die("Could not find Alert Definition from ID: " + id);
|
|
320
|
+
|
|
321
|
+
delete this.args.id;
|
|
322
|
+
delete this.args.other;
|
|
323
|
+
|
|
324
|
+
this.printMutationSummary({
|
|
325
|
+
title: 'Update Alert Definition',
|
|
326
|
+
rows: [
|
|
327
|
+
[ 'Alert ID', gray(alert.id) ],
|
|
328
|
+
[ 'Title', this.color('theme').bold(alert.title) ]
|
|
329
|
+
]
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (!await this.confirmSyncUpdate('alert', alert.id, 'alert')) return;
|
|
333
|
+
if (!Tools.numKeys(this.args)) return this.die("No updates specified for Alert Definition.");
|
|
334
|
+
this.printUpdateData(this.args);
|
|
335
|
+
|
|
336
|
+
// Seed every editable field from the current definition. This preserves
|
|
337
|
+
// siblings when dotted options or generic --delete paths modify a nested
|
|
338
|
+
// action object, despite the API's shallow top-level merge behavior.
|
|
339
|
+
var params = { id: alert.id };
|
|
340
|
+
[
|
|
341
|
+
'title', 'expression', 'message', 'groups', 'actions', 'monitor_id',
|
|
342
|
+
'enabled', 'samples', 'exclusive_actions', 'limit_jobs', 'abort_jobs',
|
|
343
|
+
'icon', 'notes'
|
|
344
|
+
].forEach( key => {
|
|
345
|
+
if (key in alert) params[key] = Tools.copyHash(alert[key], true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
this.mergeDotArgs(params, this.args);
|
|
349
|
+
this.applyDeleteArgs(params, this.args);
|
|
350
|
+
this.processAlertDefinitionUpdates(params);
|
|
351
|
+
this.validateAlertDefinition(params);
|
|
352
|
+
|
|
353
|
+
await this.callStandardAPI('updateAlert', params, {
|
|
354
|
+
text: 'Updating Alert Definition...'
|
|
355
|
+
});
|
|
356
|
+
if (this.dry) return;
|
|
357
|
+
if (this.format.match(/json/)) return this.jsonOutput({ id: id, updated: true });
|
|
358
|
+
this.toast('✅', 'green', "Successfully updated Alert Definition: #" + id);
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
async cmd_test_alert_definition() {
|
|
362
|
+
// Test a stored definition, with optional expression or message overrides.
|
|
363
|
+
await this.getMultiple();
|
|
364
|
+
var selector = '';
|
|
365
|
+
if (this.args.other.length) selector = this.args.other.join(' ');
|
|
366
|
+
delete this.args.other;
|
|
367
|
+
|
|
368
|
+
var alert = null;
|
|
369
|
+
if (selector) {
|
|
370
|
+
alert = Tools.findObject(this.alerts, { id: selector }) ||
|
|
371
|
+
this.findObjectFuzzy(this.alerts, { title: selector });
|
|
372
|
+
if (!alert) return this.die("Could not find Alert Definition based on your criteria: " + selector);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
var server_selector = this.args.server;
|
|
376
|
+
delete this.args.server;
|
|
377
|
+
if (!server_selector) return this.dieUsage('alert test');
|
|
378
|
+
var server = this.findObjectFuzzy(Object.values(this.servers), {
|
|
379
|
+
id: server_selector,
|
|
380
|
+
title: server_selector,
|
|
381
|
+
hostname: server_selector,
|
|
382
|
+
ip: server_selector
|
|
383
|
+
}, 1);
|
|
384
|
+
if (!server) return this.die("Could not find server based on your criteria: " + server_selector);
|
|
385
|
+
|
|
386
|
+
var params = {
|
|
387
|
+
server: server.id,
|
|
388
|
+
expression: ('expression' in this.args) ? String(this.args.expression) : String((alert && alert.expression) || ''),
|
|
389
|
+
message: ('message' in this.args) ? String(this.args.message) : String((alert && alert.message) || '')
|
|
390
|
+
};
|
|
391
|
+
delete this.args.expression;
|
|
392
|
+
delete this.args.message;
|
|
393
|
+
if (Tools.numKeys(this.args)) return this.die("Unexpected alert test option: --" + Tools.firstKey(this.args));
|
|
394
|
+
if (!params.expression.trim() || !params.message.trim()) return this.dieUsage('alert test');
|
|
395
|
+
|
|
396
|
+
var data = await this.callStandardAPI('testAlert', params, {
|
|
397
|
+
text: 'Testing Alert Definition...'
|
|
398
|
+
});
|
|
399
|
+
if (this.dry) return;
|
|
400
|
+
if (this.format.match(/json/)) return this.jsonOutput({ result: !!data.result, message: data.message });
|
|
401
|
+
|
|
402
|
+
this.printBoxList({
|
|
403
|
+
title: 'Alert Definition Test Result',
|
|
404
|
+
rows: [
|
|
405
|
+
[ 'Definition', alert ? alert.title + gray(' (' + alert.id + ')') : gray('(Ad Hoc)') ],
|
|
406
|
+
[ 'Server', this.getNiceServer(server) + gray(' (' + server.id + ')') ],
|
|
407
|
+
[ 'Triggered', data.result ? green.bold('Yes') : gray('No') ],
|
|
408
|
+
[ 'Message', data.message ]
|
|
409
|
+
]
|
|
410
|
+
});
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
async cmd_delete_alert() {
|
|
414
|
+
// Deletes use exact IDs only. Definition IDs take precedence.
|
|
415
|
+
await this.getMultiple();
|
|
416
|
+
var id = this.getAlertIDArgument('Alert');
|
|
417
|
+
var resolved = await this.resolveAlert(id, false);
|
|
418
|
+
if (!resolved) return this.die("Could not find Alert Definition or Alert Invocation from ID: " + id);
|
|
419
|
+
|
|
420
|
+
if (resolved.type == 'definition') {
|
|
421
|
+
this.printMutationSummary({
|
|
422
|
+
title: 'Delete Alert Definition',
|
|
423
|
+
rows: [
|
|
424
|
+
[ 'Alert ID', gray(resolved.alert.id) ],
|
|
425
|
+
[ 'Title', this.color('theme').bold(resolved.alert.title) ]
|
|
426
|
+
]
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
this.printMutationSummary({
|
|
431
|
+
title: 'Delete Alert Invocation',
|
|
432
|
+
rows: [
|
|
433
|
+
[ 'Invocation ID', gray(resolved.alert.id) ],
|
|
434
|
+
[ 'Message', this.color('theme').bold(resolved.alert.message || '(None)') ]
|
|
435
|
+
]
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (this.args.confirm !== true) {
|
|
440
|
+
if (resolved.type == 'definition') {
|
|
441
|
+
this.toast('⚠️', 'orange', "Deleting Alert Definition #" + id + " also clears its live state and starts background deletion of all its Alert Invocations. Add '--confirm' to continue.");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
this.toast('⚠️', 'orange', "Deleting Alert Invocation #" + id + " is permanent. Add '--confirm' to continue.");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (resolved.type == 'definition') {
|
|
449
|
+
await this.callStandardAPI('deleteAlert', { id: id }, {
|
|
450
|
+
text: 'Deleting Alert Definition and its invocations...'
|
|
451
|
+
});
|
|
452
|
+
if (this.dry) return;
|
|
453
|
+
if (this.format.match(/json/)) return this.jsonOutput({ id: id, type: 'definition', deleted: true });
|
|
454
|
+
this.toast('✅', 'green', "Deleted Alert Definition #" + id + "; invocation cleanup started in the background.");
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
await this.callStandardAPI('deleteAlertInvocation', { id: id }, {
|
|
458
|
+
text: 'Deleting Alert Invocation...'
|
|
459
|
+
});
|
|
460
|
+
if (this.dry) return;
|
|
461
|
+
if (this.format.match(/json/)) return this.jsonOutput({ id: id, type: 'invocation', deleted: true });
|
|
462
|
+
this.toast('✅', 'green', "Successfully deleted Alert Invocation: #" + id);
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
getAlertSelector() {
|
|
467
|
+
// Consume a free-form selector for read operations.
|
|
468
|
+
var selector = '';
|
|
469
|
+
if (this.args.other && this.args.other.length) selector = this.args.other.join(' ');
|
|
470
|
+
else if (this.args.id) selector = this.args.id;
|
|
471
|
+
else if (this.args.title) selector = this.args.title;
|
|
472
|
+
if (!selector) this.dieUsage('alert get');
|
|
473
|
+
delete this.args.other;
|
|
474
|
+
delete this.args.id;
|
|
475
|
+
delete this.args.title;
|
|
476
|
+
return String(selector);
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
getAlertIDArgument(label) {
|
|
480
|
+
// Mutation targets must be one exact internal ID, never a fuzzy title.
|
|
481
|
+
var id = this.args.id;
|
|
482
|
+
if (this.args.other && this.args.other.length) id = this.args.other.shift();
|
|
483
|
+
if (!id) this.die("Missing required " + label + " ID argument.");
|
|
484
|
+
if (this.args.other && this.args.other.length) this.die("Unexpected argument after " + label + " ID: " + this.args.other[0]);
|
|
485
|
+
return String(id);
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
async resolveAlert(selector, allow_fuzzy) {
|
|
489
|
+
// Definition IDs are checked first because definitions are already cached
|
|
490
|
+
// locally. Invocation fetch errors are represented as per-item stubs.
|
|
491
|
+
var definition = Tools.findObject(this.alerts, { id: selector });
|
|
492
|
+
if (definition) return { type: 'definition', alert: definition };
|
|
493
|
+
|
|
494
|
+
var invocation = await this.fetchAlertInvocation(selector);
|
|
495
|
+
if (invocation) return { type: 'invocation', alert: invocation };
|
|
496
|
+
|
|
497
|
+
if (allow_fuzzy) {
|
|
498
|
+
var fuzzy_definition = this.findObjectFuzzy(this.alerts, { title: selector });
|
|
499
|
+
if (fuzzy_definition) return { type: 'definition', alert: fuzzy_definition };
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
async fetchAlertInvocation(id) {
|
|
505
|
+
// getAlertInvocations keeps positional error stubs inside a successful
|
|
506
|
+
// response, so normalize those to a simple not-found result here.
|
|
507
|
+
cli.progress.start();
|
|
508
|
+
var { err, data } = await this.api.getAlertInvocations({ ids: id });
|
|
509
|
+
cli.progress.end();
|
|
510
|
+
if (err) this.die(err);
|
|
511
|
+
var alert = data.alerts && data.alerts[0];
|
|
512
|
+
return (alert && !alert.err) ? alert : null;
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
parseAlertBoolean(value, name) {
|
|
516
|
+
// pixl-cli handles literal true and false globally; include conventional
|
|
517
|
+
// flag and numeric spellings for values arriving from JSON or config files.
|
|
518
|
+
if ((value === true) || (value === 1) || (value === '1')) return true;
|
|
519
|
+
if ((value === false) || (value === 0) || (value === '0')) return false;
|
|
520
|
+
if (String(value).match(/^(yes|on)$/i)) return true;
|
|
521
|
+
if (String(value).match(/^(no|off)$/i)) return false;
|
|
522
|
+
this.die("Invalid boolean value for '" + name + "': " + value);
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
parseAlertList(value) {
|
|
526
|
+
// Accept JSON arrays, repeated options, or convenient comma-separated IDs.
|
|
527
|
+
if ((value === undefined) || (value === null) || (value === '')) return [];
|
|
528
|
+
return Tools.alwaysArray(value).reduce( function(list, item) {
|
|
529
|
+
if (Array.isArray(item)) return list.concat(item);
|
|
530
|
+
return list.concat(String(item).split(/\s*,\s*/));
|
|
531
|
+
}, [] ).filter( item => !!item );
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
processAlertDefinitionUpdates(alert) {
|
|
535
|
+
// Convert short CLI option names into the xyOps Alert data model.
|
|
536
|
+
var aliases = {
|
|
537
|
+
monitor: 'monitor_id',
|
|
538
|
+
'limit-jobs': 'limit_jobs',
|
|
539
|
+
'abort-jobs': 'abort_jobs',
|
|
540
|
+
exclusive: 'exclusive_actions'
|
|
541
|
+
};
|
|
542
|
+
Object.keys(aliases).forEach( alias => {
|
|
543
|
+
if (alias in alert) {
|
|
544
|
+
alert[aliases[alias]] = alert[alias];
|
|
545
|
+
delete alert[alias];
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
if ('group' in alert) {
|
|
550
|
+
alert.groups = this.parseAlertList(alert.groups).concat(this.parseAlertList(alert.group));
|
|
551
|
+
delete alert.group;
|
|
552
|
+
}
|
|
553
|
+
else alert.groups = this.parseAlertList(alert.groups);
|
|
554
|
+
alert.groups = Array.from(new Set(alert.groups));
|
|
555
|
+
|
|
556
|
+
if (!Array.isArray(alert.actions)) {
|
|
557
|
+
if (!alert.actions) alert.actions = [];
|
|
558
|
+
else alert.actions = [alert.actions];
|
|
559
|
+
}
|
|
560
|
+
if ('action' in alert) {
|
|
561
|
+
alert.actions = alert.actions.concat(Tools.alwaysArray(alert.action));
|
|
562
|
+
delete alert.action;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
['enabled', 'exclusive_actions', 'limit_jobs', 'abort_jobs'].forEach( key => {
|
|
566
|
+
if (key in alert) alert[key] = this.parseAlertBoolean(alert[key], key);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
alert.samples = Number(alert.samples || 1);
|
|
570
|
+
if (!Number.isInteger(alert.samples) || (alert.samples < 1) || (alert.samples > 59)) {
|
|
571
|
+
this.die("Alert samples must be an integer from 1 through 59.");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if ((alert.monitor_id === false) || (alert.monitor_id === null) || (alert.monitor_id == 'none')) alert.monitor_id = '';
|
|
575
|
+
if ((alert.icon === false) || (alert.icon === null) || (alert.icon == 'none')) alert.icon = '';
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
validateAlertDefinition(alert) {
|
|
579
|
+
// Fail early with CLI-specific guidance before asking the API to validate
|
|
580
|
+
// expression syntax, message macros, groups, and action structures.
|
|
581
|
+
['title', 'expression', 'message'].forEach( key => {
|
|
582
|
+
if (!alert[key] || !String(alert[key]).trim()) this.die("Alert Definition requires a non-empty '" + key + "'.");
|
|
583
|
+
alert[key] = String(alert[key]).trim();
|
|
584
|
+
});
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
getNiceAlertGroups(groups, empty_text) {
|
|
588
|
+
// Alert definitions with no group restriction apply to every server group.
|
|
589
|
+
if (!groups || !groups.length) return gray(empty_text || '(None)');
|
|
590
|
+
return groups.map( id => this.getNiceGroup(id) ).join(', ');
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
printAlertDefinition(alert) {
|
|
594
|
+
// Human-readable definition detail, explicitly labeled to avoid confusing
|
|
595
|
+
// it with an invocation record.
|
|
596
|
+
var monitor = Tools.findObject(this.monitors, { id: alert.monitor_id });
|
|
597
|
+
var nice_monitor = monitor ? monitor.title + gray(' (' + monitor.id + ')') : (alert.monitor_id || gray('(None)'));
|
|
598
|
+
this.printBoxList({
|
|
599
|
+
title: 'Alert Definition Summary',
|
|
600
|
+
rows: [
|
|
601
|
+
[ 'Alert ID', gray(alert.id) ],
|
|
602
|
+
[ 'Title', this.color('theme').bold(alert.title) ],
|
|
603
|
+
[ 'Status', this.getNiceEnabled(alert.enabled) ],
|
|
604
|
+
[ 'Expression', alert.expression ],
|
|
605
|
+
[ 'Message', alert.message ],
|
|
606
|
+
[ 'Groups', this.getNiceAlertGroups(alert.groups, '(All)') ],
|
|
607
|
+
[ 'Samples', Tools.commify(alert.samples || 1) ],
|
|
608
|
+
[ 'Monitor Overlay', nice_monitor ],
|
|
609
|
+
[ 'Limit Jobs', alert.limit_jobs ? green('Yes') : gray('No') ],
|
|
610
|
+
[ 'Abort Jobs', alert.abort_jobs ? red('Yes') : gray('No') ],
|
|
611
|
+
[ 'Exclusive Actions', alert.exclusive_actions ? green('Yes') : gray('No') ],
|
|
612
|
+
[ 'Author', alert.username || gray('(Unknown)') ],
|
|
613
|
+
[ 'Created', this.getNiceDateTime(alert.created, true, true) ],
|
|
614
|
+
[ 'Modified', this.getNiceDateTime(alert.modified, true, true) ],
|
|
615
|
+
[ 'Revision', alert.revision || 1 ]
|
|
616
|
+
]
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// User notes may contain multiple lines, so keep them out of the summary box.
|
|
620
|
+
if (alert.notes) {
|
|
621
|
+
this.printUserNotes('ALERT DEFINITION NOTES', alert.notes);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
this.printAlertDefinitionActions(alert);
|
|
625
|
+
this.printSuggestedCommands({
|
|
626
|
+
"Test alert definition": `xy alert test ${alert.id} --server SERVER_ID_OR_TITLE`,
|
|
627
|
+
"Export alert definition": `xy alert ${alert.id} --export alert.json`,
|
|
628
|
+
"Update alert definition": `xy alert update ${alert.id} [--KEY VALUE, ...]`,
|
|
629
|
+
"Find its invocations": `xy alerts search --alert ${alert.id}`,
|
|
630
|
+
"Delete alert definition": `xy alert delete ${alert.id}`
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
printAlertDefinitionActions(alert) {
|
|
635
|
+
// Configured actions describe future behavior and do not have result data.
|
|
636
|
+
var actions = alert.actions || [];
|
|
637
|
+
if (!actions.length) return;
|
|
638
|
+
this.printBoxTable({
|
|
639
|
+
title: 'Alert Definition Actions',
|
|
640
|
+
header: ['Condition', 'Type', 'Description', 'Status'],
|
|
641
|
+
rows: actions.map( action => {
|
|
642
|
+
var disp = this.getJobActionDisplayArgs(action);
|
|
643
|
+
return [
|
|
644
|
+
this.color(disp.condition.color).bold(disp.condition.title),
|
|
645
|
+
disp.type || Tools.ucfirst(action.type || 'Unknown'),
|
|
646
|
+
disp.desc || 'n/a',
|
|
647
|
+
this.getNiceEnabled(action.enabled)
|
|
648
|
+
];
|
|
649
|
+
})
|
|
650
|
+
});
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
async printAlertInvocation(alert) {
|
|
654
|
+
// Match the web UI's core invocation summary, then include the useful
|
|
655
|
+
// related records that can be rendered with existing CLI helpers.
|
|
656
|
+
var definition = Tools.findObject(this.alerts, { id: alert.alert });
|
|
657
|
+
var nice_definition = definition ? definition.title + gray(' (' + definition.id + ')') : alert.alert;
|
|
658
|
+
var completed = alert.active ? this.epoch : (alert.modified || alert.date);
|
|
659
|
+
var elapsed = Math.max(0, Math.floor(completed - alert.date));
|
|
660
|
+
|
|
661
|
+
this.printBoxList({
|
|
662
|
+
title: 'Alert Invocation Summary',
|
|
663
|
+
rows: [
|
|
664
|
+
[ 'Invocation ID', gray(alert.id) ],
|
|
665
|
+
[ 'Alert Definition', nice_definition ],
|
|
666
|
+
[ 'Message', this.color('theme').bold(alert.message || '(None)') ],
|
|
667
|
+
[ 'Server', this.getNiceServer(alert.server) + gray(' (' + alert.server + ')') ],
|
|
668
|
+
[ 'Status', this.getNiceAlertStatus(alert) ],
|
|
669
|
+
[ 'Started', this.getNiceDateTime(alert.date, true, true) ],
|
|
670
|
+
[ 'Duration', Tools.getTextFromSeconds(elapsed, false, true) ],
|
|
671
|
+
[ 'Expression', alert.exp || gray('(Unavailable)') ],
|
|
672
|
+
[ 'Groups', this.getNiceAlertGroups(alert.groups) ],
|
|
673
|
+
[ 'Notified', alert.notified ? green('Yes') : gray('No') ],
|
|
674
|
+
[ 'Sample Count', Tools.commify(alert.count || 0) ]
|
|
675
|
+
]
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
this.printAlertInvocationActions(alert);
|
|
679
|
+
await this.printSnapshots({
|
|
680
|
+
title: 'Alert Invocation Snapshots',
|
|
681
|
+
query: 'alerts:' + alert.id,
|
|
682
|
+
offset: 0,
|
|
683
|
+
hide_empty: true
|
|
684
|
+
});
|
|
685
|
+
await this.printTickets({
|
|
686
|
+
title: 'Alert Invocation Tickets',
|
|
687
|
+
ids: alert.tickets || [],
|
|
688
|
+
offset: 0,
|
|
689
|
+
hide_empty: true
|
|
690
|
+
});
|
|
691
|
+
await this.printCompletedJobs({
|
|
692
|
+
title: 'Alert Invocation Jobs',
|
|
693
|
+
ids: alert.jobs || [],
|
|
694
|
+
offset: 0,
|
|
695
|
+
hide_empty: true
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
this.printSuggestedCommands({
|
|
699
|
+
"View alert definition": `xy alert ${alert.alert}`,
|
|
700
|
+
"Similar invocations": `xy alerts search --alert ${alert.alert} --server ${alert.server}`,
|
|
701
|
+
"Delete this invocation": `xy alert delete ${alert.id}`
|
|
702
|
+
});
|
|
703
|
+
},
|
|
704
|
+
|
|
705
|
+
printAlertInvocationActions(alert) {
|
|
706
|
+
// Invocation actions include only executions that actually ran.
|
|
707
|
+
var actions = (alert.actions || []).filter( function(action) {
|
|
708
|
+
return !!(action.date && !action.hidden);
|
|
709
|
+
});
|
|
710
|
+
if (!actions.length) return;
|
|
711
|
+
actions.sort( function(a, b) { return a.date - b.date; } );
|
|
712
|
+
|
|
713
|
+
this.printBoxTable({
|
|
714
|
+
title: 'Alert Invocation Actions',
|
|
715
|
+
header: ['Condition', 'Type', 'Source', 'Description', 'Date / Time', 'Result', 'Elapsed'],
|
|
716
|
+
rows: actions.map( action => {
|
|
717
|
+
var disp = this.getJobActionDisplayArgs(action);
|
|
718
|
+
var elapsed_ms = Math.max(0, Math.floor(action.elapsed_ms || 0));
|
|
719
|
+
var nice_elapsed = (elapsed_ms < 1000) ? Tools.commify(elapsed_ms) + ' ms' : Tools.getTextFromSeconds(elapsed_ms / 1000, true, true);
|
|
720
|
+
var result = action.code ? cli.emoji('🛑') + ' ' + red.bold('Error') : cli.emoji('✅') + ' ' + green.bold('Success');
|
|
721
|
+
if (action.code && action.description) result += gray(' (' + String(action.description).replace(/\s+/g, ' ').trim() + ')');
|
|
722
|
+
return [
|
|
723
|
+
this.color(disp.condition.color).bold(disp.condition.title),
|
|
724
|
+
disp.type || Tools.ucfirst(action.type || 'Unknown'),
|
|
725
|
+
Tools.ucfirst(String(action.source || 'alert')),
|
|
726
|
+
disp.desc || 'n/a',
|
|
727
|
+
this.getRelativeDateTime(action.date, true),
|
|
728
|
+
result,
|
|
729
|
+
nice_elapsed
|
|
730
|
+
];
|
|
731
|
+
})
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
}; // module.exports
|