Skip to content

Commit 56dbd3a

Browse files
committed
Feature: Dynamically fire an action on-demand during a job: Instant Job Actions
1 parent 84c2fef commit 56dbd3a

9 files changed

Lines changed: 88 additions & 18 deletions

File tree

docs/data.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2890,6 +2890,7 @@ Each action has a `condition` property which specifies when it should fire. The
28902890
| `tag:TAGID` | Fires on job completion only when a specific tag is present on the job. |
28912891
| `alert_new` | Fires when a new alert is triggered on a server. |
28922892
| `alert_cleared` | Fires when an active alert has cleared. |
2893+
| `instant` | Special condition used for firing dynamic actions on-demand during a job. |
28932894

28942895
#### Action.type
28952896

docs/plugins.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ If you want to have xyOps delete the files for you after uploading, specify an o
424424

425425
Note that if you send multiple messages with `files` properties, the previous list is overwritten (i.e. the latter prevails).
426426

427-
##### Tags
427+
##### Job Tags
428428

429429
To **add** tags to the current job, use the following "push" message format:
430430

@@ -439,7 +439,7 @@ To **add** tags to the current job, use the following "push" message format:
439439

440440
The `push` object is used here to instruct xyOps to "push" (append) tags onto the existing set (you cannot replace or delete tags). The tags themselves should be valid [Tag.id](data.md#tag-id)s, and duplicates are automatically removed.
441441

442-
##### Actions
442+
##### Job Actions
443443

444444
To **add** actions to the current job, use the following "push" message format. This example would send an email to a specific address when the job completes:
445445

@@ -448,7 +448,7 @@ To **add** actions to the current job, use the following "push" message format.
448448
"xy": 1,
449449
"push": {
450450
"actions": [
451-
{ "condition": "complete", "type": "email", "email": "admin@mycompany.com", "enabled": true }
451+
{ "condition": "complete", "type": "email", "email": "admin@mycompany.com", "users": [], "enabled": true }
452452
]
453453
}
454454
}
@@ -467,6 +467,19 @@ Here is another example which will launch a subsequent job when the current job
467467
}
468468
```
469469

470+
And if you want the action to run *instantly* (i.e. do not wait for the job to complete), use the special `instant` condition, like this:
471+
472+
```json
473+
{
474+
"xy": 1,
475+
"push": {
476+
"actions": [
477+
{ "condition": "instant", "type": "channel", "channel_id": "sev1", "enabled": true }
478+
]
479+
}
480+
}
481+
```
482+
470483
The `push` object is used here to instruct xyOps to "push" (append) actions onto the existing set (you cannot replace or delete actions). See [Action Types](actions.md#action-types) for all the possible action objects you can add here.
471484

472485
##### Server Data

docs/tags.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Notes:
4646
- Tag IDs must exist; define them first in the UI or via API.
4747
- Duplicate tags are removed automatically.
4848

49-
For details on plugin I/O, see [Plugins → Tags](plugins.md#tags).
49+
For details on plugin I/O, see [Plugins → Tags](plugins.md#job-tags).
5050

5151

5252
## Tag-Driven Actions

htdocs/js/pages/Base.class.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,6 +1729,7 @@ Page.Base = class Base extends Page {
17291729
var title = ucfirst(source);
17301730

17311731
switch (source) {
1732+
case 'job': icon = 'timer-outline'; break;
17321733
case 'event': icon = 'file-clock-outline'; break;
17331734
case 'workflow': icon = 'clipboard-flow-outline'; break;
17341735
case 'alert': icon = 'bell-outline'; break;

htdocs/js/pages/Job.class.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -551,11 +551,11 @@ Page.Job = class Job extends Page.PageUtils {
551551
this.getJobSnapshots();
552552
this.renderPluginParams('#d_job_params');
553553
this.renderEventParams();
554-
this.renderJobActions();
555554
this.renderJobTags();
556555
this.renderMediaSlideshow();
557556
}
558557

558+
this.renderJobActions();
559559
this.setupCharts();
560560
this.updateUserContent();
561561
this.getJobAlerts();
@@ -1524,7 +1524,7 @@ Page.Job = class Job extends Page.PageUtils {
15241524
// we're only interested in actions that actually fired (and aren't hidden)
15251525
var actions = this.actions = (job.actions || []).filter( function(action) { return !!(action.date && !action.hidden); } );
15261526

1527-
// for workflows, some actions may be 'orphaned' and not attached to any sub-job
1527+
// for workflows, include all sub-job actions as well
15281528
if (workflow && workflow.state && workflow.nodes) {
15291529
find_objects( workflow.nodes, { type: 'action' } ).forEach( function(node) {
15301530
var state = workflow.state[node.id];
@@ -1534,6 +1534,9 @@ Page.Job = class Job extends Page.PageUtils {
15341534
} );
15351535
}
15361536

1537+
// sort actions by date ascending
1538+
sort_by( actions, 'date', { type: 'number', dir: 1 } );
1539+
15371540
// decorate actions with idx, for linking
15381541
actions.forEach( function(action, idx) { action.idx = idx; } );
15391542

@@ -1546,7 +1549,7 @@ Page.Job = class Job extends Page.PageUtils {
15461549
var html = '';
15471550

15481551
var grid_args = {
1549-
rows: sort_by(actions, 'condition'), // sort in place, so idx works below
1552+
rows: actions,
15501553
cols: cols,
15511554
data_type: 'action'
15521555
};
@@ -1564,7 +1567,7 @@ Page.Job = class Job extends Page.PageUtils {
15641567
return [
15651568
'<button class="link nowrap" onClick="' + link + '"><b><i class="mdi mdi-' + disp.condition.icon + '"></i>' + disp.condition.title + '</b></button>',
15661569
'<i class="mdi mdi-' + disp.icon + '">&nbsp;</i>' + disp.type,
1567-
self.getNiceActionSource(item.source || 'event'),
1570+
self.getNiceActionSource(item.source || 'event') + ((item.count > 1) ? ` (x${commify(item.count)})` : ''),
15681571
disp.desc,
15691572
self.getRelativeDateTime(item.date, true),
15701573
'<i class="mdi mdi-clock-check-outline">&nbsp;</i>' + get_text_from_ms_round( Math.floor(item.elapsed_ms), true),
@@ -1583,12 +1586,15 @@ Page.Job = class Job extends Page.PageUtils {
15831586
var action = this.actions[idx];
15841587
var disp = self.getJobActionDisplayArgs(action); // condition, type, text, desc, icon
15851588
var details = action.details || "";
1586-
var nice_date = this.getRelativeDateTime(action.date).replace(/\&nbsp;<\/i>/g, '</i>');
1589+
var nice_date = this.getRelativeDateTime(action.date, true).replace(/\&nbsp;<\/i>/g, '</i>');
15871590

15881591
var header = "";
15891592
header += `- **Action Type:** ${disp.type}\n`;
1593+
header += `- **Description:** ${disp.desc}\n`;
15901594
header += `- **Condition:** ${disp.condition.title}\n`;
15911595
header += `- **Source:** ${ucfirst(action.source || 'event')}\n`;
1596+
if (action.id) header += `- **Node ID:** \`${action.id}\`\n`;
1597+
if (action.count && (action.count > 1)) header += `- **Invocations:** \`${commify(action.count)}\`\n`;
15921598
header += `- **Date/Time:** ${nice_date}\n`;
15931599
header += `- **Result:** ${action.description || 'OK'}\n`;
15941600

@@ -1597,6 +1603,10 @@ Page.Job = class Job extends Page.PageUtils {
15971603

15981604
details = header + details;
15991605

1606+
if (action.count && (action.count > 1)) {
1607+
details = details.trim() + `\n\n**Note:** This action was invoked ${commify(action.count)} separate times, but only the latest invocation details are displayed here. See the workflow sub-jobs for each individual action invocation details.`;
1608+
}
1609+
16001610
var title = "Job Action Details: " + disp.type;
16011611
if (action.code) title = '<span style="color:var(--red);">' + title + '</span>';
16021612

@@ -3472,6 +3482,13 @@ Page.Job = class Job extends Page.PageUtils {
34723482
this.renderJobLimits();
34733483
this.getJobSnapshots();
34743484
break;
3485+
3486+
case 'action_append':
3487+
var action = pdata.action;
3488+
if (!this.job.actions) this.job.actions = [];
3489+
this.job.actions.push( action );
3490+
this.renderJobActions();
3491+
break;
34753492
} // switch
34763493
}
34773494

htdocs/js/pages/PageUtils.class.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1892,6 +1892,9 @@ Page.PageUtils = class PageUtils extends Page.Base {
18921892
disp.condition = { title: "On " + tag.title };
18931893
disp.condition.icon = tag.icon || 'tag-outline';
18941894
}
1895+
else if (!disp.condition && (action.condition == 'instant')) {
1896+
disp.condition = { title: "Instant", icon: 'creation' };
1897+
}
18951898

18961899
switch (action.type) {
18971900
case 'email':

lib/action.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,20 @@ class Actions {
9696
function(action, callback) {
9797
self.runJobAction(job, action, callback);
9898
},
99-
callback
99+
function() {
100+
if (self.shut) return callback();
101+
102+
// wait for any instant actions to complete
103+
async.whilst(
104+
function() {
105+
return !!Tools.findObject( job.actions, { condition: 'instant', enabled: true, active: true } ) && !self.shut;
106+
},
107+
function(callback) {
108+
setTimeout( function() { callback(); }, 250 );
109+
},
110+
callback
111+
); // whilst
112+
}
100113
); // async.each
101114
}
102115

@@ -155,14 +168,20 @@ class Actions {
155168
if (action.code) self.logError( action.type, action.description, { job: job.id } );
156169
else self.logAction( 8, action.type + ": " + action.description, { job: job.id } );
157170

158-
// cleanup workflow node state
171+
// update workflow node state
172+
// note: for multiplex/repeat/split nodes, this clobbers (latter prevails)
159173
if (wf_node_state) {
160174
Tools.mergeHashInto(wf_node_state, action);
161175
wf_node_state.active = false;
162176
wf_node_state.completed = Tools.timeNow();
177+
wf_node_state.count = (wf_node_state.count || 0) + 1;
163178
if (wf_conn_state) wf_conn_state.completed = Tools.timeNow();
179+
self.doPageBroadcast( 'Job?id=' + job.workflow.job, 'action_append', { action } );
164180
} // workflow
165181

182+
// notify appropriate page to refresh action table
183+
self.doPageBroadcast( 'Job?id=' + job.id, 'action_append', { action } );
184+
166185
callback();
167186
});
168187
}
@@ -264,7 +283,7 @@ class Actions {
264283
if (!callback) callback = noop;
265284

266285
var hook_data = this.getJobHookData(job, action);
267-
this.appendMetaLog(job, "Firing web hook: " + action.web_hook);
286+
this.appendMetaLog(job, "Firing web hook: #" + action.web_hook);
268287
this.logAction(9, "Firing job web hook for " + action.condition + ": " + action.web_hook);
269288

270289
// allow action to append to the text param
@@ -604,7 +623,7 @@ class Actions {
604623
// add one or more tags to the job
605624
if (!callback) callback = noop;
606625

607-
this.appendMetaLog(job, "Adding tags to job: " + action.tags.join(', '));
626+
this.appendMetaLog(job, "Adding tags to job: {" + action.tags.join(', ') + "}");
608627
if (!job.tags) job.tags = [];
609628
job.tags = job.tags.concat( action.tags );
610629

@@ -650,7 +669,7 @@ class Actions {
650669
return callback();
651670
}
652671

653-
this.appendMetaLog(job, "Taking snapshot of server: " + job.server);
672+
this.appendMetaLog(job, "Taking snapshot of server: #" + job.server);
654673

655674
this.createSnapshot(job.server, { source: 'job' }, function(err, id) {
656675
if (err) {
@@ -1125,7 +1144,7 @@ class Actions {
11251144
}
11261145
}
11271146

1128-
this.appendMetaLog(job, "Running custom action Plugin: " + plugin.id);
1147+
this.appendMetaLog(job, "Running custom action Plugin: #" + plugin.id);
11291148
this.logAction(9, "Firing action Plugin for " + action.condition + ": " + plugin.id + ": " + child_cmd);
11301149

11311150
var child = cp.exec( child_cmd, child_opts, function(err, stdout, stderr) {

lib/job.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class Jobs {
5252

5353
if (!job.limits) job.limits = [];
5454
if (!job.actions) job.actions = [];
55+
if (!job.tags) job.tags = [];
5556

5657
// unique id for job
5758
job.id = Tools.generateShortID('j');
@@ -1229,13 +1230,23 @@ class Jobs {
12291230
delete updates.tags;
12301231

12311232
// push system (for adding actions, limits, tags, etc.)
1232-
if (updates.push) {
1233+
if (updates.push && Tools.isaHash(updates.push)) {
12331234
for (var key in updates.push) {
1234-
if (!job[key]) job[key] = [];
12351235
if (Array.isArray(job[key])) job[key] = job[key].concat( updates.push[key] );
12361236
}
1237+
1238+
// make sure all pushed actions (regardless of condition) have a source set
1239+
Tools.findObjects( updates.push.actions || [], { enabled: true } ).forEach( function(action) {
1240+
action.source = 'job';
1241+
} );
1242+
1243+
// run instant actions instantly
1244+
Tools.findObjects( updates.push.actions || [], { condition: 'instant', enabled: true } ).forEach( function(action) {
1245+
self.runJobAction(job, action);
1246+
} );
1247+
12371248
delete updates.push;
1238-
}
1249+
} // updates.push
12391250

12401251
var context = { job, config: { base_app_url: self.config.get('base_app_url') } };
12411252

lib/multi.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,11 @@ class Multi {
764764

765765
if (!self.jobDetails[job.id]) self.jobDetails[job.id] = {}; // sanity
766766

767+
// cancel any actions that got cut off in the middle
768+
(job.actions || []).forEach( function(action) {
769+
if (action.active) delete action.active;
770+
} );
771+
767772
// set starting jobs to 'ready', so they get re-started on the next tick
768773
if (job.state == 'starting') {
769774
self.logJob(6, `Changing job state from starting to ready: ${job.id}`);

0 commit comments

Comments
 (0)