Skip to content

Commit 6cc0188

Browse files
committed
Feature: Docker container monitoring and basic management on server/group pages.
1 parent 63ece0b commit 6cc0188

6 files changed

Lines changed: 369 additions & 3 deletions

File tree

htdocs/css/responsive.css

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
.data_grid.event_grid { grid-template-columns: auto auto auto 0px 0px auto auto auto !important; }
3939
.data_grid.event_grid > ul > div:nth-child(4) { visibility: hidden; }
4040
.data_grid.event_grid > ul > div:nth-child(5) { visibility: hidden; }
41+
42+
/* 'Container Name', 'ID', 'Server', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions' */
43+
.data_grid.grp_cont_grid { grid-template-columns: auto auto auto auto auto auto auto auto 0px 0px auto !important; }
44+
.data_grid.grp_cont_grid > ul > div:nth-child(9) { visibility: hidden; }
45+
.data_grid.grp_cont_grid > ul > div:nth-child(10) { visibility: hidden; }
4146
}
4247

4348
@media only screen and (min-width: 1281px) and (max-width: 1450px) {
@@ -101,6 +106,18 @@
101106
/* 'Job ID', 'Event', 'Category', 'Server', 'State', 'Elapsed', 'Progress', 'Remaining', 'Actions' */
102107
.data_grid.dash_active_grid { grid-template-columns: auto auto auto auto auto 0px auto auto auto !important; }
103108
.data_grid.dash_active_grid > ul > div:nth-child(6) { visibility: hidden; }
109+
110+
/* 'Container Name', 'ID', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions' */
111+
.data_grid.container_grid { grid-template-columns: auto auto auto auto auto auto auto 0px 0px auto !important; }
112+
.data_grid.container_grid > ul > div:nth-child(8) { visibility: hidden; }
113+
.data_grid.container_grid > ul > div:nth-child(9) { visibility: hidden; }
114+
115+
/* 'Container Name', 'ID', 'Server', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions' */
116+
.data_grid.grp_cont_grid { grid-template-columns: auto auto auto auto auto auto 0px 0px 0px 0px auto !important; }
117+
.data_grid.grp_cont_grid > ul > div:nth-child(7) { visibility: hidden; }
118+
.data_grid.grp_cont_grid > ul > div:nth-child(8) { visibility: hidden; }
119+
.data_grid.grp_cont_grid > ul > div:nth-child(9) { visibility: hidden; }
120+
.data_grid.grp_cont_grid > ul > div:nth-child(10) { visibility: hidden; }
104121
}
105122

106123
@media only screen and (min-width: 1025px) and (max-width: 1280px) {
@@ -412,6 +429,21 @@
412429
/* 'Title / Description', 'Author', 'License', 'Type', 'Modified', 'Version' */
413430
.data_grid.product_grid { grid-template-columns: auto auto 0px auto auto auto !important; }
414431
.data_grid.product_grid > ul > div:nth-child(3) { visibility: hidden; }
432+
433+
/* 'Container Name', 'ID', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions' */
434+
.data_grid.container_grid { grid-template-columns: auto auto auto auto auto 0px 0px 0px 0px auto !important; }
435+
.data_grid.container_grid > ul > div:nth-child(6) { visibility: hidden; }
436+
.data_grid.container_grid > ul > div:nth-child(7) { visibility: hidden; }
437+
.data_grid.container_grid > ul > div:nth-child(8) { visibility: hidden; }
438+
.data_grid.container_grid > ul > div:nth-child(9) { visibility: hidden; }
439+
440+
/* 'Container Name', 'ID', 'Server', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions' */
441+
.data_grid.grp_cont_grid { grid-template-columns: auto auto auto auto auto 0px 0px 0px 0px 0px auto !important; }
442+
.data_grid.grp_cont_grid > ul > div:nth-child(6) { visibility: hidden; }
443+
.data_grid.grp_cont_grid > ul > div:nth-child(7) { visibility: hidden; }
444+
.data_grid.grp_cont_grid > ul > div:nth-child(8) { visibility: hidden; }
445+
.data_grid.grp_cont_grid > ul > div:nth-child(9) { visibility: hidden; }
446+
.data_grid.grp_cont_grid > ul > div:nth-child(10) { visibility: hidden; }
415447
}
416448

417449
/* Tablets */
@@ -881,6 +913,23 @@
881913
.data_grid.product_grid { grid-template-columns: auto auto 0px 0px auto auto !important; }
882914
.data_grid.product_grid > ul > div:nth-child(3) { visibility: hidden; }
883915
.data_grid.product_grid > ul > div:nth-child(4) { visibility: hidden; }
916+
917+
/* 'Container Name', 'ID', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions' */
918+
.data_grid.container_grid { grid-template-columns: auto auto auto auto 0px 0px 0px 0px 0px auto !important; }
919+
.data_grid.container_grid > ul > div:nth-child(5) { visibility: hidden; }
920+
.data_grid.container_grid > ul > div:nth-child(6) { visibility: hidden; }
921+
.data_grid.container_grid > ul > div:nth-child(7) { visibility: hidden; }
922+
.data_grid.container_grid > ul > div:nth-child(8) { visibility: hidden; }
923+
.data_grid.container_grid > ul > div:nth-child(9) { visibility: hidden; }
924+
925+
/* 'Container Name', 'ID', 'Server', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions' */
926+
.data_grid.grp_cont_grid { grid-template-columns: auto 0px auto auto auto 0px 0px 0px 0px 0px auto !important; }
927+
.data_grid.grp_cont_grid > ul > div:nth-child(2) { visibility: hidden; }
928+
.data_grid.grp_cont_grid > ul > div:nth-child(6) { visibility: hidden; }
929+
.data_grid.grp_cont_grid > ul > div:nth-child(7) { visibility: hidden; }
930+
.data_grid.grp_cont_grid > ul > div:nth-child(8) { visibility: hidden; }
931+
.data_grid.grp_cont_grid > ul > div:nth-child(9) { visibility: hidden; }
932+
.data_grid.grp_cont_grid > ul > div:nth-child(10) { visibility: hidden; }
884933
}
885934

886935
/* Phones */
@@ -1510,6 +1559,26 @@
15101559
.data_grid.product_grid > ul > div:nth-child(3) { visibility: hidden; }
15111560
.data_grid.product_grid > ul > div:nth-child(4) { visibility: hidden; }
15121561
.data_grid.product_grid > ul > div:nth-child(5) { visibility: hidden; }
1562+
1563+
/* 'Container Name', 'ID', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions' */
1564+
.data_grid.container_grid { grid-template-columns: auto 0px auto auto 0px 0px 0px 0px 0px 0px !important; }
1565+
.data_grid.container_grid > ul > div:nth-child(2) { visibility: hidden; }
1566+
.data_grid.container_grid > ul > div:nth-child(5) { visibility: hidden; }
1567+
.data_grid.container_grid > ul > div:nth-child(6) { visibility: hidden; }
1568+
.data_grid.container_grid > ul > div:nth-child(7) { visibility: hidden; }
1569+
.data_grid.container_grid > ul > div:nth-child(8) { visibility: hidden; }
1570+
.data_grid.container_grid > ul > div:nth-child(9) { visibility: hidden; }
1571+
.data_grid.container_grid > ul > div:nth-child(10) { visibility: hidden; }
1572+
1573+
/* 'Container Name', 'ID', 'Server', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions' */
1574+
.data_grid.grp_cont_grid { grid-template-columns: auto 0px auto auto auto 0px 0px 0px 0px 0px 0px !important; }
1575+
.data_grid.grp_cont_grid > ul > div:nth-child(2) { visibility: hidden; }
1576+
.data_grid.grp_cont_grid > ul > div:nth-child(6) { visibility: hidden; }
1577+
.data_grid.grp_cont_grid > ul > div:nth-child(7) { visibility: hidden; }
1578+
.data_grid.grp_cont_grid > ul > div:nth-child(8) { visibility: hidden; }
1579+
.data_grid.grp_cont_grid > ul > div:nth-child(9) { visibility: hidden; }
1580+
.data_grid.grp_cont_grid > ul > div:nth-child(10) { visibility: hidden; }
1581+
.data_grid.grp_cont_grid > ul > div:nth-child(11) { visibility: hidden; }
15131582
}
15141583

15151584
/* Smaller Phones */
@@ -2023,4 +2092,26 @@
20232092
.data_grid.product_grid > ul > div:nth-child(4) { visibility: hidden; }
20242093
.data_grid.product_grid > ul > div:nth-child(5) { visibility: hidden; }
20252094
.data_grid.product_grid > ul > div:nth-child(6) { visibility: hidden; }
2095+
2096+
/* 'Container Name', 'ID', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions' */
2097+
.data_grid.container_grid { grid-template-columns: auto 0px auto auto 0px 0px 0px 0px 0px 0px !important; }
2098+
.data_grid.container_grid > ul > div:nth-child(2) { visibility: hidden; }
2099+
.data_grid.container_grid > ul > div:nth-child(5) { visibility: hidden; }
2100+
.data_grid.container_grid > ul > div:nth-child(6) { visibility: hidden; }
2101+
.data_grid.container_grid > ul > div:nth-child(7) { visibility: hidden; }
2102+
.data_grid.container_grid > ul > div:nth-child(8) { visibility: hidden; }
2103+
.data_grid.container_grid > ul > div:nth-child(9) { visibility: hidden; }
2104+
.data_grid.container_grid > ul > div:nth-child(10) { visibility: hidden; }
2105+
2106+
/* 'Container Name', 'ID', 'Server', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions' */
2107+
.data_grid.grp_cont_grid { grid-template-columns: auto 0px auto 0px 0px 0px 0px 0px 0px 0px 0px !important; }
2108+
.data_grid.grp_cont_grid > ul > div:nth-child(2) { visibility: hidden; }
2109+
.data_grid.grp_cont_grid > ul > div:nth-child(4) { visibility: hidden; }
2110+
.data_grid.grp_cont_grid > ul > div:nth-child(5) { visibility: hidden; }
2111+
.data_grid.grp_cont_grid > ul > div:nth-child(6) { visibility: hidden; }
2112+
.data_grid.grp_cont_grid > ul > div:nth-child(7) { visibility: hidden; }
2113+
.data_grid.grp_cont_grid > ul > div:nth-child(8) { visibility: hidden; }
2114+
.data_grid.grp_cont_grid > ul > div:nth-child(9) { visibility: hidden; }
2115+
.data_grid.grp_cont_grid > ul > div:nth-child(10) { visibility: hidden; }
2116+
.data_grid.grp_cont_grid > ul > div:nth-child(11) { visibility: hidden; }
20262117
}

htdocs/js/pages/ServerUtils.class.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,147 @@ Page.ServerUtils = class ServerUtils extends Page.PageUtils {
632632
this.div.find('#d_vs_jobs').show();
633633
}
634634

635+
getContainerTable(snapshot) {
636+
// get initial html for sortable container table
637+
var self = this;
638+
var containers = get_path(snapshot, 'data.docker.containers') || [];
639+
640+
var cont_opts = {
641+
id: 't_snap_conts',
642+
item_name: 'container',
643+
sort_by: 'cpu',
644+
sort_dir: -1,
645+
filter: '',
646+
column_ids: ['name', 'id', 'cpu', 'mem_usage', 'mem_total', 'net_read', 'net_write', 'disk_read', 'disk_write', ''],
647+
column_labels: ['Container Name', 'ID', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions']
648+
};
649+
650+
return this.getSortableTable( containers, cont_opts, function(cont) {
651+
var actions = [];
652+
if (app.isAdmin() && self.ID.match(/^(Servers|Groups)$/)) {
653+
actions = [
654+
`<button class="link danger" data-id="${cont.id}" data-name="${encode_attrib_entities(cont.name)}" onClick="$P().restartContainer(this)"><b>Restart</b></button>`,
655+
`<button class="link danger" data-id="${cont.id}" data-name="${encode_attrib_entities(cont.name)}" onClick="$P().stopContainer(this)"><b>Stop</b></button>`
656+
];
657+
}
658+
return [
659+
'<b><i class="mdi mdi-docker">&nbsp;</i>' + cont.name + '</b>',
660+
'<span class="monospace">' + cont.id +'</span>',
661+
pct( cont.cpu, 100 ),
662+
get_text_from_bytes( cont.mem_usage ),
663+
get_text_from_bytes( cont.mem_total ),
664+
get_text_from_bytes( cont.net_read ),
665+
get_text_from_bytes( cont.net_write ),
666+
get_text_from_bytes( cont.disk_read ),
667+
get_text_from_bytes( cont.disk_write ),
668+
actions.join(' | ') || '-'
669+
];
670+
});
671+
}
672+
673+
restartContainer(elem) {
674+
// restart selected container
675+
var self = this;
676+
var $elem = $(elem);
677+
var cont_id = $elem.data('id');
678+
var cont_name = $elem.data('name');
679+
var server_id = $elem.data('server') || this.server.id;
680+
681+
var msg = `Are you sure you want to restart the container &ldquo;<b>${cont_name}</b>&rdquo; running on the &ldquo;<b>${self.getNiceServerText(server_id)}</b>&rdquo; host?`;
682+
683+
Dialog.confirmDanger( 'Restart Container', msg, ['restart', 'Restart'], function(result) {
684+
if (!result) return;
685+
Dialog.hide();
686+
app.clearError();
687+
self.sendContainerCommand(server_id, cont_id, cont_name, 'restart');
688+
} ); // confirm
689+
}
690+
691+
stopContainer(elem) {
692+
// stop selected container
693+
var self = this;
694+
var $elem = $(elem);
695+
var cont_id = $elem.data('id');
696+
var cont_name = $elem.data('name');
697+
var server_id = $elem.data('server') || this.server.id;
698+
699+
var msg = `Are you sure you want to stop the container &ldquo;<b>${cont_name}</b>&rdquo; running on the &ldquo;<b>${self.getNiceServerText(server_id)}</b>&rdquo; host?`;
700+
701+
Dialog.confirmDanger( 'Stop Container', msg, ['power-standby', 'Stop'], function(result) {
702+
if (!result) return;
703+
Dialog.hide();
704+
app.clearError();
705+
self.sendContainerCommand(server_id, cont_id, cont_name, 'stop');
706+
} ); // confirm
707+
}
708+
709+
sendContainerCommand(server_id, cont_id, cont_name, cmd) {
710+
// send command to specified container on specified server
711+
var self = this;
712+
var server = app.servers[server_id];
713+
var cat_def = find_object( app.categories, { id: 'general' } ) || app.categories[0];
714+
715+
if (!server) return app.doError("Server not found in active list: " + server_id);
716+
if (!cat_def) return app.doError("No categories found. Please add a category before killing processes.");
717+
if (!find_object(app.plugins, { id: 'shellplug' })) return app.doError("The built-in Shell Plugin was not found.");
718+
if (!cont_id.match(/^\w+$/)) return app.doError("Malformed Container ID: " + cont_id);
719+
if (!cmd.match(/^\w+$/)) return app.doError("Malformed Container Command: " + cmd);
720+
721+
var script = `#!/bin/sh\nset -e\necho "${ucfirst(cmd)} container ${cont_name} (${cont_id})..."\ndocker ${cmd} ${cont_id}\necho "Done."\n`;
722+
723+
var event = {
724+
enabled: true,
725+
title: ucfirst(cmd) + " Container",
726+
icon: 'docker',
727+
category: cat_def.id,
728+
targets: [ server_id ],
729+
algo: 'random',
730+
plugin: 'shellplug',
731+
params: {
732+
script: script
733+
},
734+
triggers: [
735+
{ type: "manual", enabled: true }
736+
],
737+
actions: [
738+
{ type: "delete", enabled: true, condition: "complete" }
739+
],
740+
limits: [],
741+
fields: [],
742+
tags: [],
743+
notes: ""
744+
};
745+
746+
var job = deep_copy_object(event);
747+
job.label = ucfirst(cmd) + " Container";
748+
749+
// pre-open new window/tab for job details
750+
var win = window.open('', '_blank');
751+
752+
app.api.post( 'app/create_event', event, function(resp) {
753+
// now run the job
754+
if (!self.active) return; // sanity
755+
job.event = resp.event.id;
756+
757+
app.api.post( 'app/run_event', job, function(resp) {
758+
// Dialog.hideProgress();
759+
if (!self.active) return; // sanity
760+
761+
// jump immediately to live details page in new window
762+
win.location.href = '#Job?id=' + resp.id;
763+
},
764+
function(err) {
765+
// capture error so we can close the window we just opened
766+
win.close();
767+
app.doError("API Error: " + err.description);
768+
}); // run_event error
769+
},
770+
function(err) {
771+
win.close();
772+
app.doError("API Error: " + err.description);
773+
} ); // create_event error
774+
}
775+
635776
getProcessTable(snapshot) {
636777
// get initial html for sortable proc table
637778
var self = this;
@@ -1680,6 +1821,71 @@ Page.ServerUtils = class ServerUtils extends Page.PageUtils {
16801821
return true; // show
16811822
}
16821823

1824+
renderGroupContainerTable() {
1825+
// render html for sortable container table
1826+
var self = this;
1827+
1828+
var cont_opts = {
1829+
id: 't_grp_conts',
1830+
item_name: 'container',
1831+
attribs: {
1832+
class: 'data_grid grp_cont_grid' // TODO: css
1833+
},
1834+
sort_by: 'cpu',
1835+
sort_dir: -1,
1836+
filter: '',
1837+
column_ids: ['name', 'id', 'server_label', 'cpu', 'mem_usage', 'mem_total', 'net_read', 'net_write', 'disk_read', 'disk_write', ''],
1838+
column_labels: ['Container Name', 'ID', 'Server', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write', 'Actions']
1839+
};
1840+
1841+
var cont_list = [];
1842+
this.servers.forEach( function(server) {
1843+
if (server.offline) return; // skip offline servers
1844+
if (!self.visibleServerIDs[ server.id ]) return; // skip hidden servers
1845+
1846+
var snapshot = server.snapshot || {};
1847+
if (!snapshot.data || !snapshot.data.docker) return;
1848+
1849+
snapshot.data.docker.containers.forEach( function(cont) {
1850+
cont_list.push({ ...cont,
1851+
server_label: server.title || server.hostname || server.id,
1852+
server: server.id
1853+
});
1854+
} );
1855+
} );
1856+
1857+
if (!cont_list.length) {
1858+
this.div.find('#d_vg_conts').hide();
1859+
return;
1860+
}
1861+
1862+
var html = this.getSortableTable( cont_list, cont_opts, function(cont) {
1863+
var actions = [];
1864+
if (app.isAdmin() && self.ID.match(/^(Servers|Groups)$/)) {
1865+
actions = [
1866+
`<button class="link danger" data-id="${cont.id}" data-server="${cont.server}" data-name="${encode_attrib_entities(cont.name)}" onClick="$P().restartContainer(this)"><b>Restart</b></button>`,
1867+
`<button class="link danger" data-id="${cont.id}" data-server="${cont.server}" data-name="${encode_attrib_entities(cont.name)}" onClick="$P().stopContainer(this)"><b>Stop</b></button>`
1868+
];
1869+
}
1870+
return [
1871+
'<b><i class="mdi mdi-docker">&nbsp;</i>' + cont.name + '</b>',
1872+
'<span class="monospace">' + cont.id +'</span>',
1873+
self.getNiceServer(cont.server, true),
1874+
pct( cont.cpu, 100 ),
1875+
get_text_from_bytes( cont.mem_usage ),
1876+
get_text_from_bytes( cont.mem_total ),
1877+
get_text_from_bytes( cont.net_read ),
1878+
get_text_from_bytes( cont.net_write ),
1879+
get_text_from_bytes( cont.disk_read ),
1880+
get_text_from_bytes( cont.disk_write ),
1881+
actions.join(' | ') || '-'
1882+
];
1883+
});
1884+
1885+
this.div.find('#d_vg_conts > .box_content').html( html );
1886+
this.div.find('#d_vg_conts').show();
1887+
}
1888+
16831889
renderGroupProcessTable() {
16841890
// render html for sortable proc table
16851891
var self = this;

0 commit comments

Comments
 (0)