diff --git a/ChangeLog b/ChangeLog index eed1671..8b95d97 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,34 @@ +2015-11-26 15:51 marc + + * html/manage.php: table fixed + +2015-11-25 15:26 marc + + * html/images.php, html/index.html.in, html/overview.php, + html/rotate.svg, html/servicedock.css, html/servicedock.js, + html/zoom.svg: allow image zoom and rotate + +2015-11-20 15:14 marc + + * html/images.php, html/servicedock.css: better image overview + +2015-11-20 13:49 marc + + * html/makefile.am: typo + +2015-11-19 13:13 marc + + * ax_init_standard_project.m4, configure.ac, html/about.php.in, + html/images.php, html/index.html.in, html/makefile.am, + html/overview.php, html/servicedock.css, html/servicedock.js: + added about and images + +2015-11-18 15:54 marc + + * ChangeLog, README, html/details.php, html/manage.php, + html/overview.php, html/servicedock.css: added doku for + installatin in README + 2015-11-18 13:24 marc * configure.ac, html/details.php, html/index.html.in, diff --git a/html/makefile.am b/html/makefile.am index b851f43..870cbdb 100644 --- a/html/makefile.am +++ b/html/makefile.am @@ -12,8 +12,9 @@ EXTRA_DIST = index.html.in about.php.in wwwdir = ${pkgdatadir}/html www_DATA = index.html about.php -dist_www_DATA = servicedock.css servicedock.js jquery.js viz.js \ - menu.svg overview.php details.php manage.php \ - action.php jquery-ui.js jquery-ui.css images.php +dist_www_DATA = servicedock.css servicedock.js jquery.js viz.js \ + menu.svg overview.php details.php manage.php \ + action.php jquery-ui.js jquery-ui.css images.php \ + zoom.svg rotate.svg MAINTAINERCLEANFILES = makefile.in diff --git a/html/servicedock.js b/html/servicedock.js index 439f09f..4495fe1 100644 --- a/html/servicedock.js +++ b/html/servicedock.js @@ -124,14 +124,26 @@ function zoom(incr = 0) { case 1: { $("#main svg").css("width", "100%"); $("#main svg").css("height", "auto"); - $("#main svg").css("max-width", "100%"); + $("#main svg").css("max-width", "none"); $("#main svg").css("max-height", "none"); } break; case 2: { $("#main.svg").css("width", "auto"); $("#main.svg").css("height", "100%"); $("#main.svg").css("max-width", "none"); - $("#main.svg").css("max-height", "100%"); + $("#main.svg").css("max-height", "none"); + } break; + case 3: { + $("#main svg").css("width", "200%"); + $("#main svg").css("height", "auto"); + $("#main svg").css("max-width", "none"); + $("#main svg").css("max-height", "none"); + } break; + case 4: { + $("#main.svg").css("width", "auto"); + $("#main.svg").css("height", "200%"); + $("#main.svg").css("max-width", "none"); + $("#main.svg").css("max-height", "none"); } break; } } diff --git a/nodejs/public/javascripts/servicedock.js b/nodejs/public/javascripts/servicedock.js index d410814..ad5f0cb 100644 --- a/nodejs/public/javascripts/servicedock.js +++ b/nodejs/public/javascripts/servicedock.js @@ -9,66 +9,219 @@ // 45678901234567890123456789012345678901234567890123456789012345678901234567890 var socket = io.connect(); +var focused = null; function DockerContainers() { var Status = Object.freeze({ - Error: "red", - Terminated: "yellow", - Restarting: "lightblue", - Paused: "lightgrey", - Running: "lightgreen" + Error: {color: "red", action1: "start", action2: "remove"}, + Terminated: {color: "yellow", action1: "start", action2: "remove"}, + Restarting: {color: "lightblue", action1: "start", action2: "remove"}, + Paused: {color: "lightgrey", action1: "unpause", action2: null}, + Running: {color: "lightgreen", action1: "pause", action2: "stop"} }); var containers = []; var nodes = []; - this.graph = function() { - var res = ""; - console.log("nodes["+nodes.length+"]=", nodes); - for (name in nodes) { + function protocol(port) { + if (port.toString().match("443")) return "https://"; + if (port.toString().match("3304")) return "mysql://"; + if (port.toString().match("22")) return "ssh://"; + return "http://"; + } + this.exists = function(name) { + if (nodes[name]) return true; + return false; + } + this.contextmenu = function(selector) { + $('a[xlink\\:href^=#]').click(function(e) { + name = $(this).attr("xlink:href").replace(/^#/, ""); var n = nodes[name]; - var label = n.name+'\\n'+n.image; - res += '"'+n.name+'"' - +' [label="'+label - +'",URL="details('+"'"+n.name+"'" - +')",style=filled,fillcolor='+n.status+"];\n"; + $(selector).prepend('') + $("#popup").empty(); + if (n.status.action1) { + $("#popup").append(''); + $("#popup1").click(function() { + socket.emit(n.status.action1, name); + }); + } + $("#popup").append(''); + $("#popup2").click(function() { + if (focused) overview(); else details(name); + }); + if (n.status.action2) { + $("#popup").append(''); + $("#popup3").click(function() { + socket.emit(n.status.action2, name); + }); + } + $("#popup").append('
'); + $("#popup").append(''); + $("#popup").css("position", "fixed"); + $("#popup").css("top", e.pageY-$("#popup").height()/4); + $("#popup").css("left", e.pageX-$("#popup").width()/2); + $("#popup").mouseleave(function() { + $("#popup").hide(); + }).click(function() { + $("#popup").hide(); + }); + $("#popup").show(); + }) + } + this.details = function(name) { + var res = ` +
+ +
+ + + + + + + + + + + + + + `; + var n = nodes[name]; + res += ` + +
NamePortsVolumesLinksEnvironmentsImageCommand
+
+
+
+
+
`;
+        res += JSON.stringify(containers[nodes[name].id], null, 4);
+        res += `
+                    
+
+
+ + `; + return res; + } + function getIps(n, ips) { + n.ports.forEach(function(p) { + if (!ips[p.ip]) ips[p.ip] = []; + ips[p.ip].push(p); + }); + } + function graphIpClusters(ips) { + var res = "newrank=true;\n"; + var i = 0; + for (ip in ips) { + res += "subgraph clusterIp"+(++i)+' {\nlabel="'+ip+'";\n'; + ips[ip].forEach(function(p) { + res += '"'+p.ip+":"+p.external + +'" [label="'+p.external+'",URL="' + +protocol(p.internal)+p.ip+':'+p.external+'",shape=box];\n'; + }); + res+="}\n"; } res += "{rank=same;\n"; - for (name in nodes) { - var n = nodes[name]; - n.volumes.forEach(function(v) { - res += '"'+v.id+'" [label="'+v.inside+'",shape=box];\n'; + for (ip in ips) { + ips[ip].forEach(function(p) { + res += '"'+p.ip+":"+p.external+'";\n'; }); } res+="}\n"; + return res; + } + function graphNode(n) { + var res = ""; + var label = n.name+'\\n'+n.image; + res += '"'+n.name+'"' + +' [label="'+label + +'",URL="#'+n.name + +'",style=filled,fillcolor='+n.status.color+"];\n"; + n.ports.forEach(function(p) { + res += '"'+(p.ip?p.ip+":":"")+p.external+'" -> "'+n.name + +'" [label="'+p.internal+'"];\n'; + }); + n.links.forEach(function(l) { + res += '"'+n.name+'" -> "'+l.to+'" [label="link: '+l.link+'"];\n' + }); + return res; + } + function graphVolumesInside(n) { + var res = ""; + n.volumes.forEach(function(v) { + res += '"'+v.id+'" [label="'+v.inside+'",shape=box];\n'; + }); + return res; + } + function graphVolumesOutside(n) { + var res = ""; + n.volumes.forEach(function(v) { + if (v.host) + res += '"'+v.outside+'" [label="'+v.host+'",shape=box];\n'; + }); + return res; + } + function graphVolumesConnections(n) { + var res = ""; + n.volumes.forEach(function(v) { + if (v.host) + res += '"'+v.id+'" -> "'+v.outside+'" [label="mounted from"]\n'; + res += '"'+n.name+'" -> "'+v.id+'" [label="volume/'+v.rw+'"]\n'; + }); + n.volumesfrom.forEach(function(o) { + res += '"'+n.name+'" -> "'+nodes[o].name+'" [label="volumes from"]\n'; + }); + return res; + } + this.graph = function(n) { + var res = ""; + var ips = []; + n = n || nodes; + for (name in n) getIps(n[name], ips); + res += graphIpClusters(ips); + for (name in n) res += graphNode(n[name]); res += "{rank=same;\n"; - for (name in nodes) { - var n = nodes[name]; - n.volumes.forEach(function(v) { - if (v.host) - res += '"'+v.outside+'" [label="'+v.host+'",shape=box];\n'; - }); - } + for (name in n) res += graphVolumesInside(n[name]); res+="}\n"; - for (name in nodes) { - var n = nodes[name]; - n.volumes.forEach(function(v) { - if (v.host) - res += '"'+v.id+'" -> "'+v.outside+'"\n'; - }); - } - for (name in nodes) { - var n = nodes[name]; - n.volumes.forEach(function(v) { - res += '"'+n.name+'" -> "'+v.id+'"\n'; - }); - } + res += "{rank=same;\n"; + for (name in n) res += graphVolumesOutside(n[name]); + res+="}\n"; + for (name in n) res += graphVolumesConnections(n[name]); return res; } + function addNodes(ns, name) { + var n = nodes[name]; + ns[name] = n; + n.links.forEach(function(peer) { + if (!ns[peer.to]) addNodes(ns, peer.to); + }); + n.usedby.forEach(function(peer) { + if (!ns[peer]) addNodes(ns, peer); + }); + n.volumesfrom.forEach(function(peer) { + if (!ns[peer]) addNodes(ns, peer); + }); + n.volumesto.forEach(function(peer) { + if (!ns[peer]) addNodes(ns, peer); + }); + } + this.subgraph = function(name) { + var ns = []; + addNodes(ns, name); + return this.graph(ns); + } function setup() { - delete nodes; nodes=[]; - containers.forEach(function(c) { + delete nodes; nodes = []; + containers.forEach(function(c, i) { var name = c.Name.replace(/^\//, ""); - nodes[name] = {}; - console.log("container: "+name); + if (!nodes[name]) nodes[name] = {}; + nodes[name].id = i; nodes[name].name = name; nodes[name].image = c.Config.Image; nodes[name].ports = []; @@ -78,43 +231,80 @@ function DockerContainers() { if (ports[port]) for (var expose in ports[port]) { var ip = ports[port][expose].HostIp; - if (ip==""||ip=="127.0.0.1"||ip=="0.0.0.0") ip=null; + if (!ip||ip==""||ip=="0.0.0.0"||ip==0) ip=window.location.hostname; nodes[name].ports.push({ internal: port, external: ports[port][expose].HostPort, ip: ip }); } - if (c.State.Running) nodes[name].status = Status.Running; - else if (c.State.Paused) nodes[name].status = Status.Paused; + if (c.State.Paused) nodes[name].status = Status.Paused; + else if (c.State.Running) nodes[name].status = Status.Running; else if (c.State.Restarting) nodes[name].status = Status.Restarting; else if (c.State.ExitCode == 0) nodes[name].status = Status.Terminated; else nodes[name].status = Status.Error; + console.log("STATUS", name, c.State, nodes[name].status); nodes[name].volumes = []; var volumes = c.Volumes || c.Config.Volumes; nodes[name].volumes = []; if (volumes) for (var volume in volumes) { + var rw = "rw"; var outside = (typeof volumes[volume]=="string")?volumes[volume]:null; + if (c.Mounts) c.Mounts.forEach(function(mnt) { + if (mnt.Destination==volume) { + outside = mnt.Source; + rw = mnt.RW ? "rw" : "ro"; + } + }); nodes[name].volumes.push({ id: volume+':'+(outside?outside:name), + rw:rw, inside: volume, outside: outside, host: outside && !outside.match(/^\/var\/lib\/docker/) - ? volumes[volume] : null + ? outside : null }); } - nodes[name].volumesfrom = c.VolumesFrom; + nodes[name].volumesfrom = []; + if (!nodes[name].volumesto) nodes[name].volumesto = []; + if (c.HostConfig.VolumesFrom) c.HostConfig.VolumesFrom.forEach(function(id) { + containers.forEach(function(c) { + if (c.Id == id || c.Name == "/"+id || c.Name == id) { + var src = c.Name.replace(/^\//, ""); + nodes[name].volumesfrom.push(src); + if (!nodes[src]) nodes[src] = {}; + if (!nodes[src].volumesto) nodes[src].volumesto = []; + nodes[src].volumesto.push(name); + } + }); + }); nodes[name].links = []; + if (!nodes[name].usedby) nodes[name].usedby = []; if (c.HostConfig && c.HostConfig.Links) c.HostConfig.Links.forEach(function(l) { - nodes[name].links.push({ + var target = { to: l.replace(/^\/?([^:]*).*$/, "$1"), link: l.replace(new RegExp("^.*:/?"+name+"/"), "") - }); + }; + nodes[name].links.push(target); + if (!nodes[target.to]) nodes[target.to] = {}; + if (!nodes[target.to].usedby) nodes[target.to].usedby = []; + nodes[target.to].usedby.push(name); }); - console.log(nodes[name]); }); + for (name in nodes) { // cleanup duplicate links to volumes when using volumes-from + var n = nodes[name]; + n.volumesfrom.forEach(function(other) { + var o = nodes[other]; + o.volumes.forEach(function(ovol) { + n.volumes.reduceRight(function(x, nvol, i, arr) { + if (nvol.id == ovol.id) + arr.splice(i, 1); + }, []) + }) + }) + } } this.setContainers = function(c) { if (typeof c == "string") c = JSON.parse(c); @@ -131,7 +321,7 @@ var dc = new DockerContainers(); @param data (optional) The error can be a string or any structure. Strings are shown to the user, structures are logged only. @param stay (optional) If not given as @c true, reloads page after 5s. */ -function error(data, stay) { +function error(data) { $("#status").fadeOut("slow", function() { $("#status").addClass("error") $("#status").removeClass("notice") @@ -149,7 +339,6 @@ function error(data, stay) { console.log("error"); } $("#status").fadeIn("slow"); - if (!stay) setTimeout(start, 5000); }); } @@ -195,14 +384,14 @@ function success(text) { /** @param text Text is a message or some complex HTML from the server. @param msg The success message text */ function status(text, msg) { - $("#main").fadeOut("slow", function() { - $("#main").html(text); - if (msg) success(msg); - else setTimeout("$('#status').fadeOut('slow')", 5000); - $("#main").fadeIn("slow", function() { - $("form input:first-child").focus(); - }) - }); + $("#main").hide(); + $("#main").html(text); + $("#popup").hide(); + if (msg) success(msg); + else setTimeout("$('#status').fadeOut('slow')", 5000); + $("#main").show(); + $("form input:first-child").focus(); + dc.contextmenu("#main"); } function emit(signal, data) { @@ -278,6 +467,7 @@ function zoom(incr = 0) { } var viz = null; +var vizmore = null; var rankdir = "LR"; function rotateviz() { if (!viz) return; @@ -285,16 +475,21 @@ function rotateviz() { rankdir = "TB"; else rankdir = "LR"; - showviz(viz); + showviz(); } -function showviz(vizpath) { +function showviz(vizpath, more) { $("#imagetools").show(); - viz = vizpath; - console.log("DRAW: "+viz); + if (!vizpath) { + vizpath = viz; + more = vizmore; + } else { + viz = vizpath; + vizmore = more; + } res = "digraph {\n"+" rankdir="+rankdir+";\n"+viz+"\n}"; try { zoomlevel = 0; - status(Viz(res)); + status(more?Viz(res)+more:Viz(res)); } catch(e) { (res = res.split("\n")).forEach(function(v, i, a) { a[i] = ("000"+(i+1)).slice(-3)+": "+v; @@ -303,18 +498,10 @@ function showviz(vizpath) { } } -function details(c) { - $("#imagetools").hide(); - $.ajax({url: "details.php?container="+c, success: function(res) { - try { - status(res); - } catch(e) { - status("
"+res+"
"); - error("Exception Caught: "+e); - } - }}).fail(function() { - error("offline"); - }); +function details(name) { + if (name) focused = name; + else if (!focused) return overview(); + showviz(dc.subgraph(focused)); } function action(container, action) { @@ -337,11 +524,6 @@ function manage() { }); } -/** Show an Overview of all Docker Services */ -function overview() { - $("#imagetools").hide(); -} - /** Show an Overview of all Docker Images */ function imgs() { $("#imagetools").hide(); @@ -362,6 +544,14 @@ function imgs() { function containers(c) { console.log("->rcv containers"); dc.setContainers(c); + if (focused && dc.exists(focused)) + details(focused); + else + overview(); +} + +function overview() { + focused = null; showviz(dc.graph()); } @@ -380,11 +570,14 @@ function start() { } function init() { - socket.io.on("connect", connected); - socket.io.on("reconnect", connected); - socket.io.on("disconnect", disconnected); - socket.io.on("error", disconnected); - socket.on("containers", containers); + socket.io + .on("connect", connected) + .on("reconnect", connected) + .on("disconnect", disconnected) + .on("error", disconnected); + socket + .on("fail", error) + .on("containers", containers); start(); } diff --git a/nodejs/public/stylesheets/servicedock.css b/nodejs/public/stylesheets/servicedock.css index a2cb4ea..d126f0e 100644 --- a/nodejs/public/stylesheets/servicedock.css +++ b/nodejs/public/stylesheets/servicedock.css @@ -22,6 +22,8 @@ svg { max-height: 100%; width: auto; height: auto; + z-index: -1; + position: relative; } form { @@ -238,6 +240,13 @@ table.docker li+li { z-index: 0; } +#popup { + position: fixed; + background-color: lightblue; + border: .1ex solid blue; + text-align: center; +} + .clear { clear: both; } diff --git a/nodejs/sockets/index.js b/nodejs/sockets/index.js index 456bcab..a5670e3 100644 --- a/nodejs/sockets/index.js +++ b/nodejs/sockets/index.js @@ -4,13 +4,13 @@ module.exports = function() { module.connection = function(socket) { - var sys = require('sys'); - var exec = require('child_process').exec; + //var sys = require('sys'); + var proc = require('child_process'); console.log("new client"); function emit(signal, data, info) { - if (typeof data == 'string') { + if (typeof data == 'string' && !data.match("\n")) { console.log("<- signal: "+signal+"("+data+")"); } else { console.log("<- signal: "+signal); @@ -24,31 +24,86 @@ module.exports = function() { socket.broadcast.emit(signal, data); } + function exec(cmd, callback) { + console.log("== "+cmd); + proc.exec(cmd, callback); + } + + function fail(txt, data) { + console.log("** "+txt, data); + emit("fail", txt); + } + function containerinspect(error, stdout, stderr) { - console.log(error); - if (!error && !stderr) { - // var res = {}; - // JSON.parse(stdout).forEach(function(c) { - // res[c.Id] = c; - // }); - emit("containers", stdout); - } + if (error || stderr) + return fail("inspect docker containers failed", { + error: error, stderr: stderr, stdout: stdout + }); + emit("containers", stdout); } function containerlist(error, stdout, stderr) { - console.log(error); - console.log("docker inspect "+stdout.trim().replace(/\n/g, " ")); - if (!error && !stderr) - exec("docker inspect "+stdout.trim().replace(/\n/g, " "), - containerinspect); + if (error || stderr) + return fail("list docker containers failed", { + error: error, stderr: stderr, stdout: stdout + }); + exec("docker inspect "+stdout.trim().replace(/\n/g, " "), containerinspect); } - socket.on("containers", function() { + function updatecontainers(error, stdout, stderr) { + if (error || stderr) + return fail("update docker container failed", { + error: error, stderr: stderr, stdout: stdout + }); + exec("docker ps -aq", containerlist); + } + + function modify(cmd, name) { + if (!name.match(/^[a-z0-9][-_:.+a-z0-9]*$/i)) + return fail("illegal instance name", { + error: error, stderr: stderr, stdout: stdout + }); + exec("docker "+cmd+" "+name, updatecontainers); + } + + function containers() { console.log("-> containers"); - exec("docker ps -aq", - containerlist); - }); + updatecontainers(); + } + function start(name) { + console.log("-> start("+name+")"); + modify("start", name); + } + + function stop(name) { + console.log("-> stop("+name+")"); + modify("stop", name); + } + + function pause(name) { + console.log("-> pause("+name+")"); + modify("pause", name); + } + + function unpause(name) { + console.log("-> unpause("+name+")"); + modify("unpause", name); + } + + function remove(name) { + console.log("-> remove("+name+")"); + modify("rm", name); + } + + socket + .on("containers", containers) + .on("start", start) + .on("stop", stop) + .on("pause", pause) + .on("unpause", unpause) + .on("remove", remove); + } return module; diff --git a/nodejs/views/index.ejs b/nodejs/views/index.ejs index 9525a4c..5934c35 100644 --- a/nodejs/views/index.ejs +++ b/nodejs/views/index.ejs @@ -43,9 +43,12 @@

start up engine, please wait ...

+ +
+ + + +
-
- - - -
+ +