Merge pull request #4283 from TDT-AG/pr/20200717-luci-app-dockerman

luci-app-dockerman: refactoring and update coding style
This commit is contained in:
Florian Eckert 2020-07-30 08:07:08 +02:00 committed by GitHub
commit 8f54db8cc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 4344 additions and 3664 deletions

View file

@ -0,0 +1,7 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>Docker icon</title>
<path d="M4.82 17.275c-.684 0-1.304-.56-1.304-1.24s.56-1.243 1.305-1.243c.748 0 1.31.56 1.31 1.242s-.622 1.24-1.305 1.24zm16.012-6.763c-.135-.992-.75-1.8-1.56-2.42l-.315-.25-.254.31c-.494.56-.69 1.553-.63 2.295.06.562.24 1.12.554 1.554-.254.13-.568.25-.81.377-.57.187-1.124.25-1.68.25H.097l-.06.37c-.12 1.182.06 2.42.562 3.54l.244.435v.06c1.5 2.483 4.17 3.6 7.078 3.6 5.594 0 10.182-2.42 12.357-7.633 1.425.062 2.864-.31 3.54-1.676l.18-.31-.3-.187c-.81-.494-1.92-.56-2.85-.31l-.018.002zm-8.008-.992h-2.428v2.42h2.43V9.518l-.002.003zm0-3.043h-2.428v2.42h2.43V6.48l-.002-.003zm0-3.104h-2.428v2.42h2.43v-2.42h-.002zm2.97 6.147H13.38v2.42h2.42V9.518l-.007.003zm-8.998 0H4.383v2.42h2.422V9.518l-.01.003zm3.03 0h-2.4v2.42H9.84V9.518l-.015.003zm-6.03 0H1.4v2.42h2.428V9.518l-.03.003zm6.03-3.043h-2.4v2.42H9.84V6.48l-.015-.003zm-3.045 0H4.387v2.42H6.8V6.48l-.016-.003z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" id="icon-hub" viewBox="0 -4 42 50" stroke-width="2" fill-rule="nonzero" width="100%" height="100%">
<path d="M37.176371,36.2324812 C37.1920117,36.8041095 36.7372743,37.270685 36.1684891,37.270685 L3.74335204,37.2703476 C3.17827583,37.2703476 2.72400056,36.8091818 2.72400056,36.2397767 L2.72400056,19.6131383 C1.4312007,18.4881431 0.662551336,16.8884326 0.662551336,15.1618249 L0.664207893,14.69503 C0.63774183,14.4532127 0.650524255,14.2942438 0.711604827,14.1238231 L5.10793246,1.20935468 C5.24853286,0.797020623 5.63848594,0.511627907 6.06681069,0.511627907 L34.0728364,0.511627907 C34.5091607,0.511627907 34.889927,0.793578201 35.0316653,1.20921034 L39.4428567,14.1234095 C39.4871296,14.273204 39.5020782,14.4249444 39.4884726,14.5493649 L39.4884726,15.1505835 C39.4884726,16.9959517 38.6190601,18.6883031 37.1764746,19.7563084 L37.176371,36.2324812 Z M35.1376208,35.209311 L35.1376208,20.7057152 C34.7023924,20.8097593 34.271333,20.8633641 33.8336069,20.8633641 C32.0046019,20.8633641 30.3013756,19.9547008 29.2437221,18.4771538 C28.1860473,19.954695 26.4828515,20.8633641 24.6538444,20.8633641 C22.824803,20.8633641 21.1216155,19.9547157 20.0639591,18.4771544 C19.0062842,19.9546953 17.3030887,20.8633641 15.4740818,20.8633641 C13.6450404,20.8633641 11.9418529,19.9547157 10.8841965,18.4771544 C9.82652161,19.9546953 8.12332608,20.8633641 6.29431919,20.8633641 C5.76735555,20.8633641 5.24095778,20.7883418 4.73973398,20.644674 L4.73973398,35.209311 L35.1376208,35.209311 Z M30.2720226,15.6557626 C30.5154632,17.4501192 32.0503909,18.8018554 33.845083,18.8018554 C35.7286794,18.8018554 37.285413,17.3395134 37.4474599,15.4751932 L30.2280765,15.4751932 C30.2470638,15.532987 30.2617919,15.5932958 30.2720226,15.6557626 Z M21.0484306,15.4751932 C21.0674179,15.532987 21.0821459,15.5932958 21.0923767,15.6557626 C21.3358173,17.4501192 22.8707449,18.8018554 24.665437,18.8018554 C26.4601001,18.8018554 27.9950169,17.4501481 28.2378191,15.6611556 C28.2451225,15.5981318 28.2590045,15.5358056 28.2787375,15.4751932 L21.0484306,15.4751932 Z M11.9238102,15.6557626 C12.1672508,17.4501192 13.7021785,18.8018554 15.4968705,18.8018554 C17.2915336,18.8018554 18.8264505,17.4501481 19.0692526,15.6611556 C19.0765561,15.5981318 19.0904381,15.5358056 19.110171,15.4751932 L11.8798641,15.4751932 C11.8988514,15.532987 11.9135795,15.5932958 11.9238102,15.6557626 Z M6.31682805,18.8018317 C8.11149114,18.8018317 9.64640798,17.4501244 9.88921012,15.6611319 C9.89651357,15.5981081 9.91039559,15.5357819 9.93012856,15.4751696 L2.70318796,15.4751696 C2.86612006,17.3346852 4.42809696,18.8018317 6.31682805,18.8018317 Z M3.09670082,13.4139924 L37.04257,13.4139924 L33.3489482,2.57204736 L6.80119239,2.57204736 L3.09670082,13.4139924 Z"
id="Fill-1"></path>
<rect id="Rectangle-3" x="14" y="26" width="6" height="10"></rect>
<path d="M20,26 L20,36 L26,36 L26,26 L20,26 Z" id="Rectangle-3"></path>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="100%" height="100%" viewBox="0 0 48.723 48.723" xml:space="preserve">
<path d="M7.452,24.152h3.435v5.701h0.633c0.001,0,0.001,0,0.002,0h0.636v-5.701h3.51v-1.059h17.124v1.104h3.178v5.656h0.619 c0,0,0,0,0.002,0h0.619v-5.656h3.736v-0.856c0-0.012,0.006-0.021,0.006-0.032c0-0.072,0-0.143,0-0.215h5.721v-1.316h-5.721 c0-0.054,0-0.108,0-0.164c0-0.011-0.006-0.021-0.006-0.032v-0.832h-8.154v1.028h-7.911v-2.652h-0.689c-0.001,0-0.001,0-0.002,0 h-0.678v2.652h-7.846v-1.104H7.452v1.104H1.114v1.316h6.338V24.152z" />
<path d="M21.484,16.849h5.204v-2.611h7.133V1.555H14.588v12.683h6.896V16.849z M16.537,12.288V3.505h15.335v8.783H16.537z" />
<rect x="18.682" y="16.898" width="10.809" height="0.537" />
<path d="M0,43.971h6.896v2.611H12.1v-2.611h7.134V31.287H0V43.971z M1.95,33.236h15.334v8.785H1.95V33.236z" />
<rect x="4.095" y="46.631" width="10.808" height="0.537" />
<path d="M29.491,30.994v12.684h6.895v2.611h5.205v-2.611h7.133V30.994H29.491z M46.774,41.729H31.44v-8.783h15.334V41.729z" />
<rect x="33.584" y="46.338" width="10.809" height="0.537" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

View file

@ -2,57 +2,66 @@
LuCI - Lua Configuration Interface LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman> Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]-- ]]--
require "luci.util"
local docker = require "luci.model.docker" local docker = require "luci.model.docker"
-- local uci = require "luci.model.uci"
module("luci.controller.dockerman",package.seeall) module("luci.controller.dockerman",package.seeall)
function index() function index()
local e = entry({"admin", "docker"}, firstchild(), "Docker", 40) local e = entry({"admin", "docker"}, firstchild(), "Docker", 40)
e.dependent = false e.dependent = false
e.acl_depends = { "luci-app-dockerman" } e.acl_depends = { "luci-app-dockerman" }
entry({"admin","docker","overview"},cbi("dockerman/overview"),_("Overview"),0).leaf=true entry({"admin", "docker", "overview"},cbi("dockerman/overview"),_("Overview"),0).leaf=true
local remote = luci.model.uci.cursor():get("dockerman", "local", "remote_endpoint") local remote = luci.model.uci.cursor():get_bool("dockerd", "globals", "remote_endpoint")
if remote == nil then if remote then
local socket = luci.model.uci.cursor():get("dockerman", "local", "socket_path") local host = luci.model.uci.cursor():get("dockerd", "globals", "remote_host")
if socket and not nixio.fs.access(socket) then return end local port = luci.model.uci.cursor():get("dockerd", "globals", "remote_port")
elseif remote == "true" then if not host or not port then
local host = luci.model.uci.cursor():get("dockerman", "local", "remote_host") return
local port = luci.model.uci.cursor():get("dockerman", "local", "remote_port") end
if not host or not port then return end else
local socket = luci.model.uci.cursor():get("dockerd", "globals", "socket_path")
if socket and not nixio.fs.access(socket) then
return
end
end end
if (require "luci.model.docker").new():_ping().code ~= 200 then return end if (require "luci.model.docker").new():_ping().code ~= 200 then
entry({"admin","docker","containers"},form("dockerman/containers"),_("Containers"),1).leaf=true return
entry({"admin","docker","images"},form("dockerman/images"),_("Images"),2).leaf=true end
entry({"admin","docker","networks"},form("dockerman/networks"),_("Networks"),3).leaf=true
entry({"admin","docker","volumes"},form("dockerman/volumes"),_("Volumes"),4).leaf=true entry({"admin", "docker", "containers"}, form("dockerman/containers"), _("Containers"),1).leaf=true
entry({"admin","docker","events"},call("action_events"),_("Events"),5) entry({"admin", "docker", "images"}, form("dockerman/images"), _("Images"),2).leaf=true
entry({"admin","docker","newcontainer"},form("dockerman/newcontainer")).leaf=true entry({"admin", "docker", "networks"}, form("dockerman/networks"), _("Networks"),3).leaf=true
entry({"admin","docker","newnetwork"},form("dockerman/newnetwork")).leaf=true entry({"admin", "docker", "volumes"}, form("dockerman/volumes"), _("Volumes"),4).leaf=true
entry({"admin","docker","container"},form("dockerman/container")).leaf=true entry({"admin", "docker", "events"}, call("action_events"), _("Events"),5)
entry({"admin","docker","container_stats"},call("action_get_container_stats")).leaf=true
entry({"admin","docker","container_get_archive"},call("download_archive")).leaf=true entry({"admin", "docker", "newcontainer"}, form("dockerman/newcontainer")).leaf=true
entry({"admin","docker","container_put_archive"},call("upload_archive")).leaf=true entry({"admin", "docker", "newnetwork"}, form("dockerman/newnetwork")).leaf=true
entry({"admin","docker","images_save"},call("save_images")).leaf=true entry({"admin", "docker", "container"}, form("dockerman/container")).leaf=true
entry({"admin","docker","images_load"},call("load_images")).leaf=true
entry({"admin","docker","images_import"},call("import_images")).leaf=true entry({"admin", "docker", "container_stats"}, call("action_get_container_stats")).leaf=true
entry({"admin","docker","images_get_tags"},call("get_image_tags")).leaf=true entry({"admin", "docker", "container_get_archive"}, call("download_archive")).leaf=true
entry({"admin","docker","images_tag"},call("tag_image")).leaf=true entry({"admin", "docker", "container_put_archive"}, call("upload_archive")).leaf=true
entry({"admin","docker","images_untag"},call("untag_image")).leaf=true entry({"admin", "docker", "images_save"}, call("save_images")).leaf=true
entry({"admin","docker","confirm"},call("action_confirm")).leaf=true entry({"admin", "docker", "images_load"}, call("load_images")).leaf=true
entry({"admin", "docker", "images_import"}, call("import_images")).leaf=true
entry({"admin", "docker", "images_get_tags"}, call("get_image_tags")).leaf=true
entry({"admin", "docker", "images_tag"}, call("tag_image")).leaf=true
entry({"admin", "docker", "images_untag"}, call("untag_image")).leaf=true
entry({"admin", "docker", "confirm"}, call("action_confirm")).leaf=true
end end
function action_events() function action_events()
local logs = "" local logs = ""
local dk = docker.new()
local query ={} local query ={}
local dk = docker.new()
query["until"] = os.time() query["until"] = os.time()
local events = dk:events({query = query}) local events = dk:events({query = query})
if events.code == 200 then if events.code == 200 then
for _, v in ipairs(events.body) do for _, v in ipairs(events.body) do
if v and v.Type == "container" then if v and v.Type == "container" then
@ -64,40 +73,42 @@ function action_events()
end end
end end
end end
luci.template.render("dockerman/logs", {self={syslog = logs, title="Events"}}) luci.template.render("dockerman/logs", {self={syslog = logs, title="Events"}})
end end
local calculate_cpu_percent = function(d) local calculate_cpu_percent = function(d)
if type(d) ~= "table" then return end if type(d) ~= "table" then
cpu_count = tonumber(d["cpu_stats"]["online_cpus"]) return
cpu_percent = 0.0 end
cpu_delta = tonumber(d["cpu_stats"]["cpu_usage"]["total_usage"]) - tonumber(d["precpu_stats"]["cpu_usage"]["total_usage"])
system_delta = tonumber(d["cpu_stats"]["system_cpu_usage"]) - tonumber(d["precpu_stats"]["system_cpu_usage"]) local cpu_count = tonumber(d["cpu_stats"]["online_cpus"])
local cpu_percent = 0.0
local cpu_delta = tonumber(d["cpu_stats"]["cpu_usage"]["total_usage"]) - tonumber(d["precpu_stats"]["cpu_usage"]["total_usage"])
local system_delta = tonumber(d["cpu_stats"]["system_cpu_usage"]) - tonumber(d["precpu_stats"]["system_cpu_usage"])
if system_delta > 0.0 then if system_delta > 0.0 then
cpu_percent = string.format("%.2f", cpu_delta / system_delta * 100.0 * cpu_count) cpu_percent = string.format("%.2f", cpu_delta / system_delta * 100.0 * cpu_count)
end end
-- return cpu_percent .. "%"
return cpu_percent return cpu_percent
end end
local get_memory = function(d) local get_memory = function(d)
if type(d) ~= "table" then return end if type(d) ~= "table" then
-- local limit = string.format("%.2f", tonumber(d["memory_stats"]["limit"]) / 1024 / 1024) return
-- local usage = string.format("%.2f", (tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"])) / 1024 / 1024) end
-- return usage .. "MB / " .. limit.. "MB"
local limit =tonumber(d["memory_stats"]["limit"]) local limit =tonumber(d["memory_stats"]["limit"])
local usage = tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"]) local usage = tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"])
return usage, limit return usage, limit
end end
local get_rx_tx = function(d) local get_rx_tx = function(d)
if type(d) ~="table" then return end if type(d) ~="table" then
-- local data return
-- if type(d["networks"]) == "table" then end
-- for e, v in pairs(d["networks"]) do
-- data = (data and (data .. "<br>") or "") .. e .. " Total Tx:" .. string.format("%.2f",(tonumber(v.tx_bytes)/1024/1024)) .. "MB Total Rx: ".. string.format("%.2f",(tonumber(v.rx_bytes)/1024/1024)) .. "MB"
-- end
-- end
local data = {} local data = {}
if type(d["networks"]) == "table" then if type(d["networks"]) == "table" then
for e, v in pairs(d["networks"]) do for e, v in pairs(d["networks"]) do
@ -107,6 +118,7 @@ local get_rx_tx = function(d)
} }
end end
end end
return data return data
end end
@ -165,6 +177,7 @@ function action_confirm()
msg = "finish" msg = "finish"
data = "finish" data = "finish"
end end
luci.http.status(code, msg) luci.http.status(code, msg)
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
luci.http.write_json({info = data}) luci.http.write_json({info = data})
@ -193,7 +206,12 @@ function download_archive()
end end
end end
local res = dk.containers:get_archive({id = id, query = {path = path}}, cb) local res = dk.containers:get_archive({
id = id,
query = {
path = path
}
}, cb)
end end
function upload_archive(container_id) function upload_archive(container_id)
@ -209,7 +227,14 @@ function upload_archive(container_id)
end) end)
end end
local res = dk.containers:put_archive({id = container_id, query = {path = path}, body = rec_send}) local res = dk.containers:put_archive({
id = container_id,
query = {
path = path
},
body = rec_send
})
local msg = res and res.body and res.body.message or nil local msg = res and res.body and res.body.message or nil
luci.http.status(res.code, msg) luci.http.status(res.code, msg)
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
@ -238,9 +263,16 @@ function save_images(container_id)
luci.ltn12.pump.all(chunk, luci.http.write) luci.ltn12.pump.all(chunk, luci.http.write)
end end
end end
docker:write_status("Images: saving" .. " " .. container_id .. "...") docker:write_status("Images: saving" .. " " .. container_id .. "...")
local res = dk.images:get({id = container_id, query = {names = names}}, cb) local res = dk.images:get({
id = container_id,
query = {
names = names
}
}, cb)
docker:clear_status() docker:clear_status()
local msg = res and res.body and res.body.message or nil local msg = res and res.body and res.body.message or nil
luci.http.status(res.code, msg) luci.http.status(res.code, msg)
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
@ -262,8 +294,7 @@ function load_images()
docker:write_status("Images: loading...") docker:write_status("Images: loading...")
local res = dk.images:load({body = rec_send}) local res = dk.images:load({body = rec_send})
-- res.body = {"stream":"Loaded image ID: sha256:1399d3d81f80d68832e85ed6ba5f94436ca17966539ba715f661bd36f3caf08f\n"} local msg = res and res.body and ( res.body.message or res.body.stream or res.body.error ) or nil
local msg = res and res.body and ( res.body.message or res.body.stream or res.body.error)or nil
if res.code == 200 and msg and msg:match("Loaded image ID") then if res.code == 200 and msg and msg:match("Loaded image ID") then
docker:clear_status() docker:clear_status()
luci.http.status(res.code, msg) luci.http.status(res.code, msg)
@ -271,6 +302,7 @@ function load_images()
docker:append_status("code:" .. res.code.." ".. msg) docker:append_status("code:" .. res.code.." ".. msg)
luci.http.status(300, msg) luci.http.status(300, msg)
end end
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
luci.http.write_json({message = msg}) luci.http.write_json({message = msg})
end end
@ -280,6 +312,7 @@ function import_images()
local itag = luci.http.formvalue("tag") local itag = luci.http.formvalue("tag")
local dk = docker.new() local dk = docker.new()
local ltn12 = require "luci.ltn12" local ltn12 = require "luci.ltn12"
local rec_send = function(sinkout) local rec_send = function(sinkout)
luci.http.setfilehandler(function (meta, chunk, eof) luci.http.setfilehandler(function (meta, chunk, eof)
if chunk then if chunk then
@ -287,23 +320,32 @@ function import_images()
end end
end) end)
end end
docker:write_status("Images: importing".. " ".. itag .."...\n") docker:write_status("Images: importing".. " ".. itag .."...\n")
local repo = itag and itag:match("^([^:]+)") local repo = itag and itag:match("^([^:]+)")
local tag = itag and itag:match("^[^:]-:([^:]+)") local tag = itag and itag:match("^[^:]-:([^:]+)")
local res = dk.images:create({query = {fromSrc = src or "-", repo = repo or nil, tag = tag or nil }, body = not src and rec_send or nil}, docker.import_image_show_status_cb) local res = dk.images:create({
query = {
fromSrc = src or "-",
repo = repo or nil,
tag = tag or nil
},
body = not src and rec_send or nil
}, docker.import_image_show_status_cb)
local msg = res and res.body and ( res.body.message )or nil local msg = res and res.body and ( res.body.message )or nil
if not msg and #res.body == 0 then if not msg and #res.body == 0 then
-- res.body = {"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}
msg = res.body.status or res.body.error msg = res.body.status or res.body.error
elseif not msg and #res.body >= 1 then elseif not msg and #res.body >= 1 then
-- res.body = [...{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}]
msg = res.body[#res.body].status or res.body[#res.body].error msg = res.body[#res.body].status or res.body[#res.body].error
end end
if res.code == 200 and msg and msg:match("sha256:") then if res.code == 200 and msg and msg:match("sha256:") then
docker:clear_status() docker:clear_status()
else else
docker:append_status("code:" .. res.code.." ".. msg) docker:append_status("code:" .. res.code.." ".. msg)
end end
luci.http.status(res.code, msg) luci.http.status(res.code, msg)
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
luci.http.write_json({message = msg}) luci.http.write_json({message = msg})
@ -316,11 +358,15 @@ function get_image_tags(image_id)
luci.http.write_json({message = "no image id"}) luci.http.write_json({message = "no image id"})
return return
end end
local dk = docker.new() local dk = docker.new()
local res = dk.images:inspect({id = image_id}) local res = dk.images:inspect({
id = image_id
})
local msg = res and res.body and res.body.message or nil local msg = res and res.body and res.body.message or nil
luci.http.status(res.code, msg) luci.http.status(res.code, msg)
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
if res.code == 200 then if res.code == 200 then
local tags = res.body.RepoTags local tags = res.body.RepoTags
luci.http.write_json({tags = tags}) luci.http.write_json({tags = tags})
@ -333,19 +379,28 @@ end
function tag_image(image_id) function tag_image(image_id)
local src = luci.http.formvalue("tag") local src = luci.http.formvalue("tag")
local image_id = image_id or luci.http.formvalue("id") local image_id = image_id or luci.http.formvalue("id")
if type(src) ~= "string" or not image_id then if type(src) ~= "string" or not image_id then
luci.http.status(400, "no image id or tag") luci.http.status(400, "no image id or tag")
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
luci.http.write_json({message = "no image id or tag"}) luci.http.write_json({message = "no image id or tag"})
return return
end end
local repo = src:match("^([^:]+)") local repo = src:match("^([^:]+)")
local tag = src:match("^[^:]-:([^:]+)") local tag = src:match("^[^:]-:([^:]+)")
local dk = docker.new() local dk = docker.new()
local res = dk.images:tag({id = image_id, query={repo=repo, tag=tag}}) local res = dk.images:tag({
id = image_id,
query={
repo=repo,
tag=tag
}
})
local msg = res and res.body and res.body.message or nil local msg = res and res.body and res.body.message or nil
luci.http.status(res.code, msg) luci.http.status(res.code, msg)
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
if res.code == 201 then if res.code == 201 then
local tags = res.body.RepoTags local tags = res.body.RepoTags
luci.http.write_json({tags = tags}) luci.http.write_json({tags = tags})
@ -357,14 +412,17 @@ end
function untag_image(tag) function untag_image(tag)
local tag = tag or luci.http.formvalue("tag") local tag = tag or luci.http.formvalue("tag")
if not tag then if not tag then
luci.http.status(400, "no tag name") luci.http.status(400, "no tag name")
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
luci.http.write_json({message = "no tag name"}) luci.http.write_json({message = "no tag name"})
return return
end end
local dk = docker.new() local dk = docker.new()
local res = dk.images:inspect({name = tag}) local res = dk.images:inspect({name = tag})
if res.code == 200 then if res.code == 200 then
local tags = res.body.RepoTags local tags = res.body.RepoTags
if #tags > 1 then if #tags > 1 then

View file

@ -4,50 +4,73 @@ Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]-- ]]--
require "luci.util" require "luci.util"
local docker = require "luci.model.docker" local docker = require "luci.model.docker"
local dk = docker.new() local dk = docker.new()
container_id = arg[1] container_id = arg[1]
local action = arg[2] or "info" local action = arg[2] or "info"
local images, networks, container_info local m, s, o
if not container_id then return end local images, networks, container_info, res
local res = dk.containers:inspect({id = container_id})
if res.code < 300 then container_info = res.body else return end if not container_id then
return
end
res = dk.containers:inspect({id = container_id})
if res.code < 300 then
container_info = res.body
else
return
end
res = dk.networks:list() res = dk.networks:list()
if res.code < 300 then networks = res.body else return end if res.code < 300 then
networks = res.body
else
return
end
local get_ports = function(d) local get_ports = function(d)
local data local data
if d.HostConfig and d.HostConfig.PortBindings then if d.HostConfig and d.HostConfig.PortBindings then
for inter, out in pairs(d.HostConfig.PortBindings) do for inter, out in pairs(d.HostConfig.PortBindings) do
data = (data and (data .. "<br>") or "") .. out[1]["HostPort"] .. ":" .. inter data = (data and (data .. "<br>") or "") .. out[1]["HostPort"] .. ":" .. inter
end end
end end
return data return data
end end
local get_env = function(d) local get_env = function(d)
local data local data
if d.Config and d.Config.Env then if d.Config and d.Config.Env then
for _,v in ipairs(d.Config.Env) do for _,v in ipairs(d.Config.Env) do
data = (data and (data .. "<br>") or "") .. v data = (data and (data .. "<br>") or "") .. v
end end
end end
return data return data
end end
local get_command = function(d) local get_command = function(d)
local data local data
if d.Config and d.Config.Cmd then if d.Config and d.Config.Cmd then
for _,v in ipairs(d.Config.Cmd) do for _,v in ipairs(d.Config.Cmd) do
data = (data and (data .. " ") or "") .. v data = (data and (data .. " ") or "") .. v
end end
end end
return data return data
end end
local get_mounts = function(d) local get_mounts = function(d)
local data local data
if d.Mounts then if d.Mounts then
for _,v in ipairs(d.Mounts) do for _,v in ipairs(d.Mounts) do
local v_sorce_d, v_dest_d local v_sorce_d, v_dest_d
@ -70,79 +93,95 @@ local get_mounts = function(d)
data = (data and (data .. "<br>") or "") .. v_sorce .. ":" .. v["Destination"] .. (v["Mode"] ~= "" and (":" .. v["Mode"]) or "") data = (data and (data .. "<br>") or "") .. v_sorce .. ":" .. v["Destination"] .. (v["Mode"] ~= "" and (":" .. v["Mode"]) or "")
end end
end end
return data return data
end end
local get_device = function(d) local get_device = function(d)
local data local data
if d.HostConfig and d.HostConfig.Devices then if d.HostConfig and d.HostConfig.Devices then
for _,v in ipairs(d.HostConfig.Devices) do for _,v in ipairs(d.HostConfig.Devices) do
data = (data and (data .. "<br>") or "") .. v["PathOnHost"] .. ":" .. v["PathInContainer"] .. (v["CgroupPermissions"] ~= "" and (":" .. v["CgroupPermissions"]) or "") data = (data and (data .. "<br>") or "") .. v["PathOnHost"] .. ":" .. v["PathInContainer"] .. (v["CgroupPermissions"] ~= "" and (":" .. v["CgroupPermissions"]) or "")
end end
end end
return data return data
end end
local get_links = function(d) local get_links = function(d)
local data local data
if d.HostConfig and d.HostConfig.Links then if d.HostConfig and d.HostConfig.Links then
for _,v in ipairs(d.HostConfig.Links) do for _,v in ipairs(d.HostConfig.Links) do
data = (data and (data .. "<br>") or "") .. v data = (data and (data .. "<br>") or "") .. v
end end
end end
return data return data
end end
local get_tmpfs = function(d) local get_tmpfs = function(d)
local data local data
if d.HostConfig and d.HostConfig.Tmpfs then if d.HostConfig and d.HostConfig.Tmpfs then
for k, v in pairs(d.HostConfig.Tmpfs) do for k, v in pairs(d.HostConfig.Tmpfs) do
data = (data and (data .. "<br>") or "") .. k .. (v~="" and ":" or "")..v data = (data and (data .. "<br>") or "") .. k .. (v~="" and ":" or "")..v
end end
end end
return data return data
end end
local get_dns = function(d) local get_dns = function(d)
local data local data
if d.HostConfig and d.HostConfig.Dns then if d.HostConfig and d.HostConfig.Dns then
for _, v in ipairs(d.HostConfig.Dns) do for _, v in ipairs(d.HostConfig.Dns) do
data = (data and (data .. "<br>") or "") .. v data = (data and (data .. "<br>") or "") .. v
end end
end end
return data return data
end end
local get_sysctl = function(d) local get_sysctl = function(d)
local data local data
if d.HostConfig and d.HostConfig.Sysctls then if d.HostConfig and d.HostConfig.Sysctls then
for k, v in pairs(d.HostConfig.Sysctls) do for k, v in pairs(d.HostConfig.Sysctls) do
data = (data and (data .. "<br>") or "") .. k..":"..v data = (data and (data .. "<br>") or "") .. k..":"..v
end end
end end
return data return data
end end
local get_networks = function(d) local get_networks = function(d)
local data={} local data={}
if d.NetworkSettings and d.NetworkSettings.Networks and type(d.NetworkSettings.Networks) == "table" then if d.NetworkSettings and d.NetworkSettings.Networks and type(d.NetworkSettings.Networks) == "table" then
for k,v in pairs(d.NetworkSettings.Networks) do for k,v in pairs(d.NetworkSettings.Networks) do
data[k] = v.IPAddress or "" data[k] = v.IPAddress or ""
end end
end end
return data return data
end end
local start_stop_remove = function(m, cmd) local start_stop_remove = function(m, cmd)
local res
docker:clear_status() docker:clear_status()
docker:append_status("Containers: " .. cmd .. " " .. container_id .. "...") docker:append_status("Containers: " .. cmd .. " " .. container_id .. "...")
local res
if cmd ~= "upgrade" then if cmd ~= "upgrade" then
res = dk.containers[cmd](dk, {id = container_id}) res = dk.containers[cmd](dk, {id = container_id})
else else
res = dk.containers_upgrade(dk, {id = container_id}) res = dk.containers_upgrade(dk, {id = container_id})
end end
if res and res.code >= 300 then if res and res.code >= 300 then
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message)) docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id)) luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
@ -158,103 +197,175 @@ end
m=SimpleForm("docker", container_info.Name:sub(2), translate("Docker Container") ) m=SimpleForm("docker", container_info.Name:sub(2), translate("Docker Container") )
m.redirect = luci.dispatcher.build_url("admin/docker/containers") m.redirect = luci.dispatcher.build_url("admin/docker/containers")
-- m:append(Template("dockerman/container"))
docker_status = m:section(SimpleSection)
docker_status.template = "dockerman/apply_widget"
docker_status.err=docker:read_status()
docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
action_section = m:section(Table,{{}}) s = m:section(Table,{{}})
action_section.notitle=true s.notitle=true
action_section.rowcolors=false s.rowcolors=false
action_section.template = "cbi/nullsection" s.template = "cbi/nullsection"
btnstart=action_section:option(Button, "_start") o = s:option(Button, "_start")
btnstart.template = "dockerman/cbi/inlinebutton" o.template = "dockerman/cbi/inlinebutton"
btnstart.inputtitle=translate("Start") o.inputtitle=translate("Start")
btnstart.inputstyle = "apply" o.inputstyle = "apply"
btnstart.forcewrite = true o.forcewrite = true
btnrestart=action_section:option(Button, "_restart") o.write = function(self, section)
btnrestart.template = "dockerman/cbi/inlinebutton"
btnrestart.inputtitle=translate("Restart")
btnrestart.inputstyle = "reload"
btnrestart.forcewrite = true
btnstop=action_section:option(Button, "_stop")
btnstop.template = "dockerman/cbi/inlinebutton"
btnstop.inputtitle=translate("Stop")
btnstop.inputstyle = "reset"
btnstop.forcewrite = true
btnkill=action_section:option(Button, "_kill")
btnkill.template = "dockerman/cbi/inlinebutton"
btnkill.inputtitle=translate("Kill")
btnkill.inputstyle = "reset"
btnkill.forcewrite = true
btnupgrade=action_section:option(Button, "_upgrade")
btnupgrade.template = "dockerman/cbi/inlinebutton"
btnupgrade.inputtitle=translate("Upgrade")
btnupgrade.inputstyle = "reload"
btnstop.forcewrite = true
btnduplicate=action_section:option(Button, "_duplicate")
btnduplicate.template = "dockerman/cbi/inlinebutton"
btnduplicate.inputtitle=translate("Duplicate/Edit")
btnduplicate.inputstyle = "add"
btnstop.forcewrite = true
btnremove=action_section:option(Button, "_remove")
btnremove.template = "dockerman/cbi/inlinebutton"
btnremove.inputtitle=translate("Remove")
btnremove.inputstyle = "remove"
btnremove.forcewrite = true
btnstart.write = function(self, section)
start_stop_remove(m,"start") start_stop_remove(m,"start")
end end
btnrestart.write = function(self, section)
o = s:option(Button, "_restart")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Restart")
o.inputstyle = "reload"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"restart") start_stop_remove(m,"restart")
end end
btnupgrade.write = function(self, section)
start_stop_remove(m,"upgrade") o = s:option(Button, "_stop")
end o.template = "dockerman/cbi/inlinebutton"
btnremove.write = function(self, section) o.inputtitle=translate("Stop")
start_stop_remove(m,"remove") o.inputstyle = "reset"
end o.forcewrite = true
btnstop.write = function(self, section) o.write = function(self, section)
start_stop_remove(m,"stop") start_stop_remove(m,"stop")
end end
btnkill.write = function(self, section)
o = s:option(Button, "_kill")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Kill")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"kill") start_stop_remove(m,"kill")
end end
btnduplicate.write = function(self, section)
o = s:option(Button, "_upgrade")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Upgrade")
o.inputstyle = "reload"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"upgrade")
end
o = s:option(Button, "_duplicate")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Duplicate/Edit")
o.inputstyle = "add"
o.forcewrite = true
o.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer/duplicate/"..container_id)) luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer/duplicate/"..container_id))
end end
tab_section = m:section(SimpleSection) o = s:option(Button, "_remove")
tab_section.template = "dockerman/container" o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Remove")
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"remove")
end
s = m:section(SimpleSection)
s.template = "dockerman/container"
if action == "info" then if action == "info" then
m.submit = false m.submit = false
m.reset = false m.reset = false
table_info = { table_info = {
["01name"] = {_key = translate("Name"), _value = container_info.Name:sub(2) or "-", _button=translate("Update")}, ["01name"] = {
["02id"] = {_key = translate("ID"), _value = container_info.Id or "-"}, _key = translate("Name"),
["03image"] = {_key = translate("Image"), _value = container_info.Config.Image .. "<br>" .. container_info.Image}, _value = container_info.Name:sub(2) or "-",
["04status"] = {_key = translate("Status"), _value = container_info.State and container_info.State.Status or "-"}, _button=translate("Update")
["05created"] = {_key = translate("Created"), _value = container_info.Created or "-"}, },
["02id"] = {
_key = translate("ID"),
_value = container_info.Id or "-"
},
["03image"] = {
_key = translate("Image"),
_value = container_info.Config.Image .. "<br>" .. container_info.Image
},
["04status"] = {
_key = translate("Status"),
_value = container_info.State and container_info.State.Status or "-"
},
["05created"] = {
_key = translate("Created"),
_value = container_info.Created or "-"
},
} }
table_info["06start"] = container_info.State.Status == "running" and {_key = translate("Start Time"), _value = container_info.State and container_info.State.StartedAt or "-"} or {_key = translate("Finish Time"), _value = container_info.State and container_info.State.FinishedAt or "-"}
table_info["07healthy"] = {_key = translate("Healthy"), _value = container_info.State and container_info.State.Health and container_info.State.Health.Status or "-"} if container_info.State.Status == "running" then
table_info["08restart"] = {_key = translate("Restart Policy"), _value = container_info.HostConfig and container_info.HostConfig.RestartPolicy and container_info.HostConfig.RestartPolicy.Name or "-", _button=translate("Update")} table_info["06start"] = {
table_info["081user"] = {_key = translate("User"), _value = container_info.Config and (container_info.Config.User ~="" and container_info.Config.User or "-") or "-"} _key = translate("Start Time"),
table_info["09mount"] = {_key = translate("Mount/Volume"), _value = get_mounts(container_info) or "-"} _value = container_info.State and container_info.State.StartedAt or "-"
table_info["10cmd"] = {_key = translate("Command"), _value = get_command(container_info) or "-"} }
table_info["11env"] = {_key = translate("Env"), _value = get_env(container_info) or "-"} else
table_info["12ports"] = {_key = translate("Ports"), _value = get_ports(container_info) or "-"} table_info["06start"] = {
table_info["13links"] = {_key = translate("Links"), _value = get_links(container_info) or "-"} _key = translate("Finish Time"),
table_info["14device"] = {_key = translate("Device"), _value = get_device(container_info) or "-"} _value = container_info.State and container_info.State.FinishedAt or "-"
table_info["15tmpfs"] = {_key = translate("Tmpfs"), _value = get_tmpfs(container_info) or "-"} }
table_info["16dns"] = {_key = translate("DNS"), _value = get_dns(container_info) or "-"} end
table_info["17sysctl"] = {_key = translate("Sysctl"), _value = get_sysctl(container_info) or "-"}
table_info["07healthy"] = {
_key = translate("Healthy"),
_value = container_info.State and container_info.State.Health and container_info.State.Health.Status or "-"
}
table_info["08restart"] = {
_key = translate("Restart Policy"),
_value = container_info.HostConfig and container_info.HostConfig.RestartPolicy and container_info.HostConfig.RestartPolicy.Name or "-",
_button=translate("Update")
}
table_info["081user"] = {
_key = translate("User"),
_value = container_info.Config and (container_info.Config.User ~="" and container_info.Config.User or "-") or "-"
}
table_info["09mount"] = {
_key = translate("Mount/Volume"),
_value = get_mounts(container_info) or "-"
}
table_info["10cmd"] = {
_key = translate("Command"),
_value = get_command(container_info) or "-"
}
table_info["11env"] = {
_key = translate("Env"),
_value = get_env(container_info) or "-"
}
table_info["12ports"] = {
_key = translate("Ports"),
_value = get_ports(container_info) or "-"
}
table_info["13links"] = {
_key = translate("Links"),
_value = get_links(container_info) or "-"
}
table_info["14device"] = {
_key = translate("Device"),
_value = get_device(container_info) or "-"
}
table_info["15tmpfs"] = {
_key = translate("Tmpfs"),
_value = get_tmpfs(container_info) or "-"
}
table_info["16dns"] = {
_key = translate("DNS"),
_value = get_dns(container_info) or "-"
}
table_info["17sysctl"] = {
_key = translate("Sysctl"),
_value = get_sysctl(container_info) or "-"
}
info_networks = get_networks(container_info) info_networks = get_networks(container_info)
list_networks = {} list_networks = {}
for _, v in ipairs (networks) do for _, v in ipairs (networks) do
@ -270,24 +381,31 @@ if action == "info" then
if type(info_networks)== "table" then if type(info_networks)== "table" then
for k,v in pairs(info_networks) do for k,v in pairs(info_networks) do
table_info["14network"..k] = { table_info["14network"..k] = {
_key = translate("Network"), _value = k.. (v~="" and (" | ".. v) or ""), _button=translate("Disconnect") _key = translate("Network"),
value = k.. (v~="" and (" | ".. v) or ""),
_button=translate("Disconnect")
} }
list_networks[k]=nil list_networks[k]=nil
end end
end end
table_info["15connect"] = {_key = translate("Connect Network"), _value = list_networks ,_opts = "", _button=translate("Connect")} table_info["15connect"] = {
_key = translate("Connect Network"),
_value = list_networks ,_opts = "",
_button=translate("Connect")
}
s = m:section(Table,table_info)
d_info = m:section(Table,table_info) s.nodescr=true
d_info.nodescr=true s.formvalue=function(self, section)
d_info.formvalue=function(self, section)
return table_info return table_info
end end
dv_key = d_info:option(DummyValue, "_key", translate("Info"))
dv_key.width = "20%" o = s:option(DummyValue, "_key", translate("Info"))
dv_value = d_info:option(ListValue, "_value") o.width = "20%"
dv_value.render = function(self, section, scope)
o = s:option(ListValue, "_value")
o.render = function(self, section, scope)
if table_info[section]._key == translate("Name") then if table_info[section]._key == translate("Name") then
self:reset_values() self:reset_values()
self.template = "cbi/value" self.template = "cbi/value"
@ -325,23 +443,23 @@ if action == "info" then
DummyValue.render(self, section, scope) DummyValue.render(self, section, scope)
end end
end end
dv_value.forcewrite = true -- for write function using simpleform o.forcewrite = true
dv_value.write = function(self, section, value) o.write = function(self, section, value)
table_info[section]._value=value table_info[section]._value=value
end end
dv_value.validate = function(self, value) o.validate = function(self, value)
return value return value
end end
dv_opts = d_info:option(Value, "_opts")
dv_opts.forcewrite = true -- for write function using simpleform
dv_opts.write = function(self, section, value)
o = s:option(Value, "_opts")
o.forcewrite = true
o.write = function(self, section, value)
table_info[section]._opts=value table_info[section]._opts=value
end end
dv_opts.validate = function(self, value) o.validate = function(self, value)
return value return value
end end
dv_opts.render = function(self, section, scope) o.render = function(self, section, scope)
if table_info[section]._key==translate("Connect Network") then if table_info[section]._key==translate("Connect Network") then
self.template = "cbi/value" self.template = "cbi/value"
self.keylist = {} self.keylist = {}
@ -357,11 +475,12 @@ if action == "info" then
DummyValue.render(self, section, scope) DummyValue.render(self, section, scope)
end end
end end
btn_update = d_info:option(Button, "_button")
btn_update.forcewrite = true o = s:option(Button, "_button")
btn_update.render = function(self, section, scope) o.forcewrite = true
o.render = function(self, section, scope)
if table_info[section]._button and table_info[section]._value ~= nil then if table_info[section]._button and table_info[section]._value ~= nil then
btn_update.inputtitle=table_info[section]._button self.inputtitle=table_info[section]._button
self.template = "cbi/button" self.template = "cbi/button"
self.inputstyle = "edit" self.inputstyle = "edit"
Button.render(self, section, scope) Button.render(self, section, scope)
@ -371,26 +490,50 @@ if action == "info" then
DummyValue.render(self, section, scope) DummyValue.render(self, section, scope)
end end
end end
btn_update.write = function(self, section, value) o.write = function(self, section, value)
local res local res
docker:clear_status() docker:clear_status()
if section == "01name" then if section == "01name" then
docker:append_status("Containers: rename " .. container_id .. "...") docker:append_status("Containers: rename " .. container_id .. "...")
local new_name = table_info[section]._value local new_name = table_info[section]._value
res = dk.containers:rename({id = container_id, query = {name=new_name}}) res = dk.containers:rename({
id = container_id,
query = {
name=new_name
}
})
elseif section == "08restart" then elseif section == "08restart" then
docker:append_status("Containers: update " .. container_id .. "...") docker:append_status("Containers: update " .. container_id .. "...")
local new_restart = table_info[section]._value local new_restart = table_info[section]._value
res = dk.containers:update({id = container_id, body = {RestartPolicy = {Name = new_restart}}}) res = dk.containers:update({
id = container_id,
body = {
RestartPolicy = {
Name = new_restart
}
}
})
elseif table_info[section]._key == translate("Network") then elseif table_info[section]._key == translate("Network") then
local _,_,leave_network = table_info[section]._value:find("(.-) | .+") local _,_,leave_network
_, _, leave_network = table_info[section]._value:find("(.-) | .+")
leave_network = leave_network or table_info[section]._value leave_network = leave_network or table_info[section]._value
docker:append_status("Network: disconnect " .. leave_network .. container_id .. "...") docker:append_status("Network: disconnect " .. leave_network .. container_id .. "...")
res = dk.networks:disconnect({name = leave_network, body = {Container = container_id}}) res = dk.networks:disconnect({
name = leave_network,
body = {
Container = container_id
}
})
elseif section == "15connect" then elseif section == "15connect" then
local connect_network = table_info[section]._value local connect_network = table_info[section]._value
local network_opiton local network_opiton
if connect_network ~= "none" and connect_network ~= "bridge" and connect_network ~= "host" then if connect_network ~= "none"
and connect_network ~= "bridge"
and connect_network ~= "host" then
network_opiton = table_info[section]._opts ~= "" and { network_opiton = table_info[section]._opts ~= "" and {
IPAMConfig={ IPAMConfig={
IPv4Address=table_info[section]._opts IPv4Address=table_info[section]._opts
@ -398,8 +541,15 @@ if action == "info" then
} or nil } or nil
end end
docker:append_status("Network: connect " .. connect_network .. container_id .. "...") docker:append_status("Network: connect " .. connect_network .. container_id .. "...")
res = dk.networks:connect({name = connect_network, body = {Container = container_id, EndpointConfig= network_opiton}}) res = dk.networks:connect({
name = connect_network,
body = {
Container = container_id,
EndpointConfig= network_opiton
}
})
end end
if res and res.code > 300 then if res and res.code > 300 then
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message)) docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
else else
@ -407,32 +557,38 @@ if action == "info" then
end end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/info")) luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/info"))
end end
-- info end
elseif action == "resources" then elseif action == "resources" then
local resources_section= m:section(SimpleSection) s = m:section(SimpleSection)
d = resources_section:option( Value, "cpus", translate("CPUs"), translate("Number of CPUs. Number is a fractional number. 0.000 means no limit.")) o = s:option( Value, "cpus",
d.placeholder = "1.5" translate("CPUs"),
d.rmempty = true translate("Number of CPUs. Number is a fractional number. 0.000 means no limit."))
d.datatype="ufloat" o.placeholder = "1.5"
d.default = container_info.HostConfig.NanoCpus / (10^9) o.rmempty = true
o.datatype="ufloat"
o.default = container_info.HostConfig.NanoCpus / (10^9)
d = resources_section:option(Value, "cpushares", translate("CPU Shares Weight"), translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024.")) o = s:option(Value, "cpushares",
d.placeholder = "1024" translate("CPU Shares Weight"),
d.rmempty = true translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024."))
d.datatype="uinteger" o.placeholder = "1024"
d.default = container_info.HostConfig.CpuShares o.rmempty = true
o.datatype="uinteger"
o.default = container_info.HostConfig.CpuShares
d = resources_section:option(Value, "memory", translate("Memory"), translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M.")) o = s:option(Value, "memory",
d.placeholder = "128m" translate("Memory"),
d.rmempty = true translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M."))
d.default = container_info.HostConfig.Memory ~=0 and ((container_info.HostConfig.Memory / 1024 /1024) .. "M") or 0 o.placeholder = "128m"
o.rmempty = true
o.default = container_info.HostConfig.Memory ~=0 and ((container_info.HostConfig.Memory / 1024 /1024) .. "M") or 0
d = resources_section:option(Value, "blkioweight", translate("Block IO Weight"), translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000.")) o = s:option(Value, "blkioweight",
d.placeholder = "500" translate("Block IO Weight"),
d.rmempty = true translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000."))
d.datatype="uinteger" o.placeholder = "500"
d.default = container_info.HostConfig.BlkioWeight o.rmempty = true
o.datatype="uinteger"
o.default = container_info.HostConfig.BlkioWeight
m.handle = function(self, state, data) m.handle = function(self, state, data)
if state == FORM_VALID then if state == FORM_VALID then
@ -452,12 +608,14 @@ elseif action == "resources" then
end end
end end
end end
request_body = { request_body = {
BlkioWeight = tonumber(data.blkioweight), BlkioWeight = tonumber(data.blkioweight),
NanoCPUs = tonumber(data.cpus)*10^9, NanoCPUs = tonumber(data.cpus)*10^9,
Memory = tonumber(memory), Memory = tonumber(memory),
CpuShares = tonumber(data.cpushares) CpuShares = tonumber(data.cpushares)
} }
docker:write_status("Containers: update " .. container_id .. "...") docker:write_status("Containers: update " .. container_id .. "...")
local res = dk.containers:update({id = container_id, body = request_body}) local res = dk.containers:update({id = container_id, body = request_body})
if res and res.code >= 300 then if res and res.code >= 300 then
@ -468,35 +626,39 @@ elseif action == "resources" then
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/resources")) luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/resources"))
end end
end end
elseif action == "file" then elseif action == "file" then
local filesection= m:section(SimpleSection) s = m:section(SimpleSection)
s.template = "dockerman/container_file"
s.container = container_id
m.submit = false m.submit = false
m.reset = false m.reset = false
filesection.template = "dockerman/container_file"
filesection.container = container_id
elseif action == "inspect" then elseif action == "inspect" then
local inspectsection= m:section(SimpleSection) s = m:section(SimpleSection)
inspectsection.syslog = luci.jsonc.stringify(container_info, true) s.syslog = luci.jsonc.stringify(container_info, true)
inspectsection.title = translate("Container Inspect") s.title = translate("Container Inspect")
inspectsection.template = "dockerman/logs" s.template = "dockerman/logs"
m.submit = false m.submit = false
m.reset = false m.reset = false
elseif action == "logs" then elseif action == "logs" then
local logsection= m:section(SimpleSection)
local logs = "" local logs = ""
local query ={ local query ={
stdout = 1, stdout = 1,
stderr = 1, stderr = 1,
tail = 1000 tail = 1000
} }
local logs = dk.containers:logs({id = container_id, query = query})
s = m:section(SimpleSection)
logs = dk.containers:logs({id = container_id, query = query})
if logs.code == 200 then if logs.code == 200 then
logsection.syslog=logs.body s.syslog=logs.body
else else
logsection.syslog="Get Logs ERROR\n"..logs.code..": "..logs.body s.syslog="Get Logs ERROR\n"..logs.code..": "..logs.body
end end
logsection.title=translate("Container Logs")
logsection.template = "dockerman/logs" s.title=translate("Container Logs")
s.template = "dockerman/logs"
m.submit = false m.submit = false
m.reset = false m.reset = false
elseif action == "console" then elseif action == "console" then
@ -504,60 +666,80 @@ elseif action == "console" then
m.reset = false m.reset = false
local cmd_docker = luci.util.exec("which docker"):match("^.+docker") or nil local cmd_docker = luci.util.exec("which docker"):match("^.+docker") or nil
local cmd_ttyd = luci.util.exec("which ttyd"):match("^.+ttyd") or nil local cmd_ttyd = luci.util.exec("which ttyd"):match("^.+ttyd") or nil
if cmd_docker and cmd_ttyd and container_info.State.Status == "running" then if cmd_docker and cmd_ttyd and container_info.State.Status == "running" then
local consolesection= m:section(SimpleSection)
local cmd = "/bin/sh" local cmd = "/bin/sh"
local uid local uid
local vcommand = consolesection:option(Value, "command", translate("Command"))
vcommand:value("/bin/sh", "/bin/sh") s = m:section(SimpleSection)
vcommand:value("/bin/ash", "/bin/ash")
vcommand:value("/bin/bash", "/bin/bash") o = s:option(Value, "command", translate("Command"))
vcommand.default = "/bin/sh" o:value("/bin/sh", "/bin/sh")
vcommand.forcewrite = true o:value("/bin/ash", "/bin/ash")
vcommand.write = function(self, section, value) o:value("/bin/bash", "/bin/bash")
o.default = "/bin/sh"
o.forcewrite = true
o.write = function(self, section, value)
cmd = value cmd = value
end end
local vuid = consolesection:option(Value, "uid", translate("UID"))
vuid.forcewrite = true o = s:option(Value, "uid", translate("UID"))
vuid.write = function(self, section, value) o.forcewrite = true
o.write = function(self, section, value)
uid = value uid = value
end end
local btn_connect = consolesection:option(Button, "connect")
btn_connect.render = function(self, section, scope) o = s:option(Button, "connect")
o.render = function(self, section, scope)
self.inputstyle = "add" self.inputstyle = "add"
self.title = " " self.title = " "
self.inputtitle = translate("Connect") self.inputtitle = translate("Connect")
Button.render(self, section, scope) Button.render(self, section, scope)
end end
btn_connect.write = function(self, section) o.write = function(self, section)
local cmd_docker = luci.util.exec("which docker"):match("^.+docker") or nil local cmd_docker = luci.util.exec("which docker"):match("^.+docker") or nil
local cmd_ttyd = luci.util.exec("which ttyd"):match("^.+ttyd") or nil local cmd_ttyd = luci.util.exec("which ttyd"):match("^.+ttyd") or nil
if not cmd_docker or not cmd_ttyd or cmd_docker:match("^%s+$") or cmd_ttyd:match("^%s+$") then return end
if not cmd_docker or not cmd_ttyd or cmd_docker:match("^%s+$") or cmd_ttyd:match("^%s+$")then
return
end
local kill_ttyd = 'netstat -lnpt | grep ":7682[ \t].*ttyd$" | awk \'{print $NF}\' | awk -F\'/\' \'{print "kill -9 " $1}\' | sh > /dev/null' local kill_ttyd = 'netstat -lnpt | grep ":7682[ \t].*ttyd$" | awk \'{print $NF}\' | awk -F\'/\' \'{print "kill -9 " $1}\' | sh > /dev/null'
luci.util.exec(kill_ttyd) luci.util.exec(kill_ttyd)
local hosts local hosts
local uci = (require "luci.model.uci").cursor() local uci = (require "luci.model.uci").cursor()
local remote = uci:get("dockerman", "local", "remote_endpoint") local remote = uci:get_bool("dockerd", "globals", "remote_endpoint")
local socket_path = (remote == "false" or not remote) and uci:get("dockerman", "local", "socket_path") or nil local host = nil
local host = (remote == "true") and uci:get("dockerman", "local", "remote_host") or nil local port = nil
local port = (remote == "true") and uci:get("dockerman", "local", "remote_port") or nil local socket = nil
if remote then
host = uci:get("dockerd", "globals", "remote_host") or nil
port = uci:get("dockerd", "globals", "remote_port") or nil
else
socket = uci:get("dockerd", "globals", "socket_path") or nil
end
if remote and host and port then if remote and host and port then
hosts = host .. ':'.. port hosts = host .. ':'.. port
elseif socket_path then elseif socket_path then
hosts = "unix://" .. socket_path hosts = socket_path
else else
return return
end end
local start_cmd = cmd_ttyd .. ' -d 2 --once -p 7682 '.. cmd_docker .. ' -H "'.. hosts ..'" exec -it ' .. (uid and uid ~= "" and (" -u ".. uid .. ' ') or "").. container_id .. ' ' .. cmd .. ' &' local start_cmd = cmd_ttyd .. ' -d 2 --once -p 7682 '.. cmd_docker .. ' -H "'.. hosts ..'" exec -it ' .. (uid and uid ~= "" and (" -u ".. uid .. ' ') or "").. container_id .. ' ' .. cmd .. ' &'
os.execute(start_cmd) os.execute(start_cmd)
local console = consolesection:option(DummyValue, "console")
console.container_id = container_id o = s:option(DummyValue, "console")
console.template = "dockerman/container_console" o.container_id = container_id
o.template = "dockerman/container_console"
end end
end end
elseif action == "stats" then elseif action == "stats" then
local response = dk.containers:top({id = container_id, query = {ps_args="-aux"}}) local response = dk.containers:top({id = container_id, query = {ps_args="-aux"}})
local container_top local container_top
if response.code == 200 then if response.code == 200 then
container_top=response.body container_top=response.body
else else
@ -568,21 +750,32 @@ elseif action == "stats" then
end end
if type(container_top) == "table" then if type(container_top) == "table" then
container_top=response.body s = m:section(SimpleSection)
stat_section = m:section(SimpleSection) s.container_id = container_id
stat_section.container_id = container_id s.template = "dockerman/container_stats"
stat_section.template = "dockerman/container_stats" table_stats = {
table_stats = {cpu={key=translate("CPU Useage"),value='-'},memory={key=translate("Memory Useage"),value='-'}} cpu={
stat_section = m:section(Table, table_stats, translate("Stats")) key=translate("CPU Useage"),
stat_section:option(DummyValue, "key", translate("Stats")).width="33%" value='-'
stat_section:option(DummyValue, "value") },
top_section= m:section(Table, container_top.Processes, translate("TOP")) memory={
key=translate("Memory Useage"),
value='-'
}
}
container_top = response.body
s = m:section(Table, table_stats, translate("Stats"))
s:option(DummyValue, "key", translate("Stats")).width="33%"
s:option(DummyValue, "value")
top_section = m:section(Table, container_top.Processes, translate("TOP"))
for i, v in ipairs(container_top.Titles) do for i, v in ipairs(container_top.Titles) do
top_section:option(DummyValue, i, translate(v)) top_section:option(DummyValue, i, translate(v))
end end
end end
m.submit = false
m.reset = false m.submit = false
m.reset = false
end end
return m return m

View file

@ -3,49 +3,69 @@ LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman> Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]-- ]]--
require "luci.util"
local http = require "luci.http" local http = require "luci.http"
local uci = luci.model.uci.cursor()
local docker = require "luci.model.docker" local docker = require "luci.model.docker"
local dk = docker.new()
local images, networks, containers local m, s, o
local res = dk.images:list() local images, networks, containers, res
if res.code <300 then images = res.body else return end
local dk = docker.new()
res = dk.images:list()
if res.code <300 then
images = res.body
else
return
end
res = dk.networks:list() res = dk.networks:list()
if res.code <300 then networks = res.body else return end if res.code <300 then
res = dk.containers:list({query = {all=true}}) networks = res.body
if res.code <300 then containers = res.body else return end else
return
end
res = dk.containers:list({
query = {
all=true
}
})
if res.code <300 then
containers = res.body
else
return
end
local urlencode = luci.http.protocol and luci.http.protocol.urlencode or luci.util.urlencode local urlencode = luci.http.protocol and luci.http.protocol.urlencode or luci.util.urlencode
function get_containers() function get_containers()
local data = {} local data = {}
if type(containers) ~= "table" then return nil end
if type(containers) ~= "table" then
return nil
end
for i, v in ipairs(containers) do for i, v in ipairs(containers) do
local index = v.Created .. v.Id local index = v.Created .. v.Id
data[index]={} data[index]={}
data[index]["_selected"] = 0 data[index]["_selected"] = 0
data[index]["_id"] = v.Id:sub(1,12) data[index]["_id"] = v.Id:sub(1,12)
data[index]["name"] = v.Names[1]:sub(2) data[index]["name"] = v.Names[1]:sub(2)
data[index]["_name"] = '<a href='..luci.dispatcher.build_url("admin/docker/container/"..v.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. v.Names[1]:sub(2).."</a>" data[index]["_name"] = '<a href='..luci.dispatcher.build_url("admin/docker/container/"..v.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. v.Names[1]:sub(2).."</a>"
data[index]["_status"] = v.Status data[index]["_status"] = v.Status
if v.Status:find("^Up") then if v.Status:find("^Up") then
data[index]["_status"] = '<font color="green">'.. data[index]["_status"] .. "</font>" data[index]["_status"] = '<font color="green">'.. data[index]["_status"] .. "</font>"
else else
data[index]["_status"] = '<font color="red">'.. data[index]["_status"] .. "</font>" data[index]["_status"] = '<font color="red">'.. data[index]["_status"] .. "</font>"
end end
if (type(v.NetworkSettings) == "table" and type(v.NetworkSettings.Networks) == "table") then if (type(v.NetworkSettings) == "table" and type(v.NetworkSettings.Networks) == "table") then
for networkname, netconfig in pairs(v.NetworkSettings.Networks) do for networkname, netconfig in pairs(v.NetworkSettings.Networks) do
data[index]["_network"] = (data[index]["_network"] ~= nil and (data[index]["_network"] .." | ") or "").. networkname .. (netconfig.IPAddress ~= "" and (": " .. netconfig.IPAddress) or "") data[index]["_network"] = (data[index]["_network"] ~= nil and (data[index]["_network"] .." | ") or "").. networkname .. (netconfig.IPAddress ~= "" and (": " .. netconfig.IPAddress) or "")
end end
end end
-- networkmode = v.HostConfig.NetworkMode ~= "default" and v.HostConfig.NetworkMode or "bridge"
-- data[index]["_network"] = v.NetworkSettings.Networks[networkmode].IPAddress or nil
-- local _, _, image = v.Image:find("^sha256:(.+)")
-- if image ~= nil then
-- image=image:sub(1,12)
-- end
if v.Ports and next(v.Ports) ~= nil then if v.Ports and next(v.Ports) ~= nil then
data[index]["_ports"] = nil data[index]["_ports"] = nil
for _,v2 in ipairs(v.Ports) do for _,v2 in ipairs(v.Ports) do
@ -55,6 +75,7 @@ function get_containers()
.. ((v2.PublicPort and v2.Type and v2.Type == "tcp")and "</a>" or "") .. ((v2.PublicPort and v2.Type and v2.Type == "tcp")and "</a>" or "")
end end
end end
for ii,iv in ipairs(images) do for ii,iv in ipairs(images) do
if iv.Id == v.ImageID then if iv.Id == v.ImageID then
data[index]["_image"] = iv.RepoTags and iv.RepoTags[1] or (iv.RepoDigests[1]:gsub("(.-)@.+", "%1") .. ":<none>") data[index]["_image"] = iv.RepoTags and iv.RepoTags[1] or (iv.RepoDigests[1]:gsub("(.-)@.+", "%1") .. ":<none>")
@ -64,66 +85,72 @@ function get_containers()
data[index]["_image_id"] = v.ImageID:sub(8,20) data[index]["_image_id"] = v.ImageID:sub(8,20)
data[index]["_command"] = v.Command data[index]["_command"] = v.Command
end end
return data return data
end end
local c_lists = get_containers() local container_list = get_containers()
-- list Containers
-- m = Map("docker", translate("Docker"))
m = SimpleForm("docker", translate("Docker")) m = SimpleForm("docker", translate("Docker"))
m.submit=false m.submit=false
m.reset=false m.reset=false
docker_status = m:section(SimpleSection) s = m:section(SimpleSection)
docker_status.template = "dockerman/apply_widget" s.template = "dockerman/apply_widget"
docker_status.err=docker:read_status() s.err=docker:read_status()
docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;") s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end if s.err then
docker:clear_status()
c_table = m:section(Table, c_lists, translate("Containers"))
c_table.nodescr=true
-- v.template = "cbi/tblsection"
-- v.sortable = true
container_selecter = c_table:option(Flag, "_selected","")
container_selecter.disabled = 0
container_selecter.enabled = 1
container_selecter.default = 0
container_id = c_table:option(DummyValue, "_id", translate("ID"))
container_id.width="10%"
container_name = c_table:option(DummyValue, "_name", translate("Container Name"))
container_name.rawhtml = true
container_status = c_table:option(DummyValue, "_status", translate("Status"))
container_status.width="15%"
container_status.rawhtml=true
container_ip = c_table:option(DummyValue, "_network", translate("Network"))
container_ip.width="15%"
container_ports = c_table:option(DummyValue, "_ports", translate("Ports"))
container_ports.width="10%"
container_ports.rawhtml = true
container_image = c_table:option(DummyValue, "_image", translate("Image"))
container_image.width="10%"
container_command = c_table:option(DummyValue, "_command", translate("Command"))
container_command.width="20%"
container_selecter.write=function(self, section, value)
c_lists[section]._selected = value
end end
s = m:section(Table, container_list, translate("Containers"))
s.nodescr=true
o = s:option(Flag, "_selected","")
o.disabled = 0
o.enabled = 1
o.default = 0
o.write=function(self, section, value)
container_list[section]._selected = value
end
o = s:option(DummyValue, "_id", translate("ID"))
o.width="10%"
o = s:option(DummyValue, "_name", translate("Container Name"))
o.rawhtml = true
o = s:option(DummyValue, "_status", translate("Status"))
o.width="15%"
o.rawhtml=true
o = s:option(DummyValue, "_network", translate("Network"))
o.width="15%"
o = s:option(DummyValue, "_ports", translate("Ports"))
o.width="10%"
o.rawhtml = true
o = s:option(DummyValue, "_image", translate("Image"))
o.width="10%"
o = s:option(DummyValue, "_command", translate("Command"))
o.width="20%"
local start_stop_remove = function(m,cmd) local start_stop_remove = function(m,cmd)
local c_selected = {} local container_selected = {}
-- 遍历table中sectionid
local c_table_sids = c_table:cfgsections() for k in pairs(container_list) do
for _, c_table_sid in ipairs(c_table_sids) do if container_list[k]._selected == 1 then
-- 得到选中项的名字 container_selected[#container_selected + 1] = container_list[k].name
if c_lists[c_table_sid]._selected == 1 then
c_selected[#c_selected+1] = c_lists[c_table_sid].name --container_name:cfgvalue(c_table_sid)
end end
end end
if #c_selected >0 then
docker:clear_status() if #container_selected > 0 then
local success = true local success = true
for _,cont in ipairs(c_selected) do
docker:clear_status()
for _, cont in ipairs(container_selected) do
docker:append_status("Containers: " .. cmd .. " " .. cont .. "...") docker:append_status("Containers: " .. cmd .. " " .. cont .. "...")
local res = dk.containers[cmd](dk, {id = cont}) local res = dk.containers[cmd](dk, {id = cont})
if res and res.code >= 300 then if res and res.code >= 300 then
@ -133,63 +160,72 @@ local start_stop_remove = function(m,cmd)
docker:append_status("done\n") docker:append_status("done\n")
end end
end end
if success then docker:clear_status() end
if success then
docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers")) luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
end end
end end
action_section = m:section(Table,{{}}) s = m:section(Table,{{}})
action_section.notitle=true s.notitle=true
action_section.rowcolors=false s.rowcolors=false
action_section.template="cbi/nullsection" s.template="cbi/nullsection"
btnnew=action_section:option(Button, "_new") o = s:option(Button, "_new")
btnnew.inputtitle= translate("New") o.inputtitle= translate("New")
btnnew.template = "dockerman/cbi/inlinebutton" o.template = "dockerman/cbi/inlinebutton"
btnnew.inputstyle = "add" o.inputstyle = "add"
btnnew.forcewrite = true o.forcewrite = true
btnstart=action_section:option(Button, "_start") o.write = function(self, section)
btnstart.template = "dockerman/cbi/inlinebutton"
btnstart.inputtitle=translate("Start")
btnstart.inputstyle = "apply"
btnstart.forcewrite = true
btnrestart=action_section:option(Button, "_restart")
btnrestart.template = "dockerman/cbi/inlinebutton"
btnrestart.inputtitle=translate("Restart")
btnrestart.inputstyle = "reload"
btnrestart.forcewrite = true
btnstop=action_section:option(Button, "_stop")
btnstop.template = "dockerman/cbi/inlinebutton"
btnstop.inputtitle=translate("Stop")
btnstop.inputstyle = "reset"
btnstop.forcewrite = true
btnkill=action_section:option(Button, "_kill")
btnkill.template = "dockerman/cbi/inlinebutton"
btnkill.inputtitle=translate("Kill")
btnkill.inputstyle = "reset"
btnkill.forcewrite = true
btnremove=action_section:option(Button, "_remove")
btnremove.template = "dockerman/cbi/inlinebutton"
btnremove.inputtitle=translate("Remove")
btnremove.inputstyle = "remove"
btnremove.forcewrite = true
btnnew.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer")) luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
end end
btnstart.write = function(self, section)
o = s:option(Button, "_start")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Start")
o.inputstyle = "apply"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"start") start_stop_remove(m,"start")
end end
btnrestart.write = function(self, section)
o = s:option(Button, "_restart")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Restart")
o.inputstyle = "reload"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"restart") start_stop_remove(m,"restart")
end end
btnremove.write = function(self, section)
start_stop_remove(m,"remove") o = s:option(Button, "_stop")
end o.template = "dockerman/cbi/inlinebutton"
btnstop.write = function(self, section) o.inputtitle=translate("Stop")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"stop") start_stop_remove(m,"stop")
end end
btnkill.write = function(self, section)
o = s:option(Button, "_kill")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Kill")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"kill") start_stop_remove(m,"kill")
end end
o = s:option(Button, "_remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Remove")
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"remove")
end
return m return m

View file

@ -3,82 +3,107 @@ LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman> Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]-- ]]--
require "luci.util"
local uci = luci.model.uci.cursor()
local docker = require "luci.model.docker" local docker = require "luci.model.docker"
local dk = docker.new() local dk = docker.new()
local containers, images local containers, images, res
local res = dk.images:list() local m, s, o
if res.code <300 then images = res.body else return end
res = dk.containers:list({query = {all=true}}) res = dk.images:list()
if res.code <300 then containers = res.body else return end if res.code < 300 then
images = res.body
else
return
end
res = dk.containers:list({
query = {
all=true
}
})
if res.code < 300 then
containers = res.body
else
return
end
function get_images() function get_images()
local data = {} local data = {}
for i, v in ipairs(images) do for i, v in ipairs(images) do
local index = v.Created .. v.Id local index = v.Created .. v.Id
data[index]={} data[index]={}
data[index]["_selected"] = 0 data[index]["_selected"] = 0
data[index]["id"] = v.Id:sub(8) data[index]["id"] = v.Id:sub(8)
data[index]["_id"] = '<a href="javascript:new_tag(\''..v.Id:sub(8,20)..'\')" class="dockerman-link" title="'..translate("New tag")..'">' .. v.Id:sub(8,20) .. '</a>' data[index]["_id"] = '<a href="javascript:new_tag(\''..v.Id:sub(8,20)..'\')" class="dockerman-link" title="'..translate("New tag")..'">' .. v.Id:sub(8,20) .. '</a>'
if v.RepoTags and next(v.RepoTags)~=nil then if v.RepoTags and next(v.RepoTags)~=nil then
for i, v1 in ipairs(v.RepoTags) do for i, v1 in ipairs(v.RepoTags) do
data[index]["_tags"] =(data[index]["_tags"] and ( data[index]["_tags"] .. "<br>" )or "") .. ((v1:match("<none>") or (#v.RepoTags == 1)) and v1 or ('<a href="javascript:un_tag(\''..v1..'\')" class="dockerman_link" title="'..translate("Remove tag")..'" >' .. v1 .. '</a>')) data[index]["_tags"] =(data[index]["_tags"] and ( data[index]["_tags"] .. "<br>" )or "") .. ((v1:match("<none>") or (#v.RepoTags == 1)) and v1 or ('<a href="javascript:un_tag(\''..v1..'\')" class="dockerman_link" title="'..translate("Remove tag")..'" >' .. v1 .. '</a>'))
if not data[index]["tag"] then if not data[index]["tag"] then
data[index]["tag"] = v1--:match("<none>") and nil or v1 data[index]["tag"] = v1
end end
end end
else else
data[index]["_tags"] = v.RepoDigests[1] and v.RepoDigests[1]:match("^(.-)@.+") data[index]["_tags"] = v.RepoDigests[1] and v.RepoDigests[1]:match("^(.-)@.+")
data[index]["_tags"] = (data[index]["_tags"] and data[index]["_tags"] or "<none>" ).. ":<none>" data[index]["_tags"] = (data[index]["_tags"] and data[index]["_tags"] or "<none>" ).. ":<none>"
end end
data[index]["_tags"] = data[index]["_tags"]:gsub("<none>","&lt;none&gt;") data[index]["_tags"] = data[index]["_tags"]:gsub("<none>","&lt;none&gt;")
-- data[index]["_tags"] = '<a href="javascript:handle_tag(\''..data[index]["_id"]..'\')">' .. data[index]["_tags"] .. '</a>'
for ci,cv in ipairs(containers) do for ci,cv in ipairs(containers) do
if v.Id == cv.ImageID then if v.Id == cv.ImageID then
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "").. data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
'<a href='..luci.dispatcher.build_url("admin/docker/container/"..cv.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. cv.Names[1]:sub(2).."</a>" '<a href='..luci.dispatcher.build_url("admin/docker/container/"..cv.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. cv.Names[1]:sub(2).."</a>"
end end
end end
data[index]["_size"] = string.format("%.2f", tostring(v.Size/1024/1024)).."MB" data[index]["_size"] = string.format("%.2f", tostring(v.Size/1024/1024)).."MB"
data[index]["_created"] = os.date("%Y/%m/%d %H:%M:%S",v.Created) data[index]["_created"] = os.date("%Y/%m/%d %H:%M:%S",v.Created)
end end
return data return data
end end
local image_list = get_images() local image_list = get_images()
-- m = Map("docker", translate("Docker"))
m = SimpleForm("docker", translate("Docker")) m = SimpleForm("docker", translate("Docker"))
m.submit=false m.submit=false
m.reset=false m.reset=false
local pull_value={_image_tag_name="", _registry="index.docker.io"} local pull_value={
local pull_section = m:section(SimpleSection, translate("Pull Image")) _image_tag_name="",
pull_section.template="cbi/nullsection" _registry="index.docker.io"
local tag_name = pull_section:option(Value, "_image_tag_name") }
tag_name.template = "dockerman/cbi/inlinevalue"
tag_name.placeholder="lisaac/luci:latest" s = m:section(SimpleSection, translate("Pull Image"))
local action_pull = pull_section:option(Button, "_pull") s.template="cbi/nullsection"
action_pull.inputtitle= translate("Pull")
action_pull.template = "dockerman/cbi/inlinebutton" o = s:option(Value, "_image_tag_name")
action_pull.inputstyle = "add" o.template = "dockerman/cbi/inlinevalue"
tag_name.write = function(self, section, value) o.placeholder="lisaac/luci:latest"
o.write = function(self, section, value)
local hastag = value:find(":") local hastag = value:find(":")
if not hastag then if not hastag then
value = value .. ":latest" value = value .. ":latest"
end end
pull_value["_image_tag_name"] = value pull_value["_image_tag_name"] = value
end end
action_pull.write = function(self, section)
o = s:option(Button, "_pull")
o.inputtitle= translate("Pull")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "add"
o.write = function(self, section)
local tag = pull_value["_image_tag_name"] local tag = pull_value["_image_tag_name"]
local json_stringify = luci.jsonc and luci.jsonc.stringify local json_stringify = luci.jsonc and luci.jsonc.stringify
if tag and tag ~= "" then if tag and tag ~= "" then
docker:write_status("Images: " .. "pulling" .. " " .. tag .. "...\n") docker:write_status("Images: " .. "pulling" .. " " .. tag .. "...\n")
-- local x_auth = nixio.bin.b64encode(json_stringify({serveraddress= server})) , header={["X-Registry-Auth"] = x_auth}
local res = dk.images:create({query = {fromImage=tag}}, docker.pull_image_show_status_cb) local res = dk.images:create({query = {fromImage=tag}}, docker.pull_image_show_status_cb)
-- {"errorDetail": {"message": "failed to register layer: ApplyLayer exit status 1 stdout: stderr: write \/docker: no space left on device" }, "error": "failed to register layer: ApplyLayer exit status 1 stdout: stderr: write \/docker: no space left on device" }
if res and res.code == 200 and (res.body[#res.body] and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. tag)) then if res and res.code == 200 and (res.body[#res.body] and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. tag)) then
docker:clear_status() docker:clear_status()
else else
@ -87,48 +112,63 @@ action_pull.write = function(self, section)
else else
docker:append_status("code: 400 please input the name of image name!") docker:append_status("code: 400 please input the name of image name!")
end end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images")) luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
end end
local import_section = m:section(SimpleSection, translate("Import Images")) s = m:section(SimpleSection, translate("Import Images"))
local im = import_section:option(DummyValue, "_image_import")
im.template = "dockerman/images_import"
local image_table = m:section(Table, image_list, translate("Images")) o = s:option(DummyValue, "_image_import")
o.template = "dockerman/images_import"
local image_selecter = image_table:option(Flag, "_selected","") s = m:section(Table, image_list, translate("Images"))
image_selecter.disabled = 0
image_selecter.enabled = 1
image_selecter.default = 0
local image_id = image_table:option(DummyValue, "_id", translate("ID")) o = s:option(Flag, "_selected","")
image_id.rawhtml = true o.disabled = 0
image_table:option(DummyValue, "_tags", translate("RepoTags")).rawhtml = true o.enabled = 1
image_table:option(DummyValue, "_containers", translate("Containers")).rawhtml = true o.default = 0
image_table:option(DummyValue, "_size", translate("Size")) o.write = function(self, section, value)
image_table:option(DummyValue, "_created", translate("Created"))
image_selecter.write = function(self, section, value)
image_list[section]._selected = value image_list[section]._selected = value
end end
o = s:option(DummyValue, "_tags", translate("RepoTags"))
o.rawhtml = true
o = s:option(DummyValue, "_containers", translate("Containers"))
o.rawhtml = true
o = s:option(DummyValue, "_size", translate("Size"))
o = s:option(DummyValue, "_created", translate("Created"))
o = s:option(DummyValue, "_id", translate("ID"))
o.rawhtml = true
local remove_action = function(force) local remove_action = function(force)
local image_selected = {} local image_selected = {}
-- 遍历table中sectionid
local image_table_sids = image_table:cfgsections() for k in pairs(image_list) do
for _, image_table_sid in ipairs(image_table_sids) do if image_list[k]._selected == 1 then
-- 得到选中项的名字 image_selected[#image_selected+1] = (image_list[k]["_tags"]:match("<br>") or image_list[k]["_tags"]:match("&lt;none&gt;")) and image_list[k].id or image_list[k].tag
if image_list[image_table_sid]._selected == 1 then
image_selected[#image_selected+1] = (image_list[image_table_sid]["_tags"]:match("<br>") or image_list[image_table_sid]["_tags"]:match("&lt;none&gt;")) and image_list[image_table_sid].id or image_list[image_table_sid].tag
end end
end end
if next(image_selected) ~= nil then if next(image_selected) ~= nil then
local success = true local success = true
docker:clear_status() docker:clear_status()
for _,img in ipairs(image_selected) do for _, img in ipairs(image_selected) do
docker:append_status("Images: " .. "remove" .. " " .. img .. "...")
local query local query
if force then query = {force = true} end docker:append_status("Images: " .. "remove" .. " " .. img .. "...")
local msg = dk.images:remove({id = img, query = query})
if force then
query = {force = true}
end
local msg = dk.images:remove({
id = img,
query = query
})
if msg.code ~= 200 then if msg.code ~= 200 then
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n") docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
success = false success = false
@ -136,59 +176,67 @@ local remove_action = function(force)
docker:append_status("done\n") docker:append_status("done\n")
end end
end end
if success then docker:clear_status() end
if success then
docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images")) luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
end end
end end
local docker_status = m:section(SimpleSection) s = m:section(SimpleSection)
docker_status.template = "dockerman/apply_widget" s.template = "dockerman/apply_widget"
docker_status.err = docker:read_status() s.err = docker:read_status()
docker_status.err = docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;") s.err = s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end if s.err then
docker:clear_status()
end
local action = m:section(Table,{{}}) s = m:section(Table,{{}})
action.notitle=true s.notitle=true
action.rowcolors=false s.rowcolors=false
action.template="cbi/nullsection" s.template="cbi/nullsection"
local btnremove = action:option(Button, "remove") o = s:option(Button, "remove")
btnremove.inputtitle= translate("Remove") o.inputtitle= translate("Remove")
btnremove.template = "dockerman/cbi/inlinebutton" o.template = "dockerman/cbi/inlinebutton"
btnremove.inputstyle = "remove" o.inputstyle = "remove"
btnremove.forcewrite = true o.forcewrite = true
btnremove.write = function(self, section) o.write = function(self, section)
remove_action() remove_action()
end end
local btnforceremove = action:option(Button, "forceremove") o = s:option(Button, "forceremove")
btnforceremove.inputtitle= translate("Force Remove") o.inputtitle= translate("Force Remove")
btnforceremove.template = "dockerman/cbi/inlinebutton" o.template = "dockerman/cbi/inlinebutton"
btnforceremove.inputstyle = "remove" o.inputstyle = "remove"
btnforceremove.forcewrite = true o.forcewrite = true
btnforceremove.write = function(self, section) o.write = function(self, section)
remove_action(true) remove_action(true)
end end
local btnsave = action:option(Button, "save") o = s:option(Button, "save")
btnsave.inputtitle= translate("Save") o.inputtitle= translate("Save")
btnsave.template = "dockerman/cbi/inlinebutton" o.template = "dockerman/cbi/inlinebutton"
btnsave.inputstyle = "edit" o.inputstyle = "edit"
btnsave.forcewrite = true o.forcewrite = true
btnsave.write = function (self, section) o.write = function (self, section)
local image_selected = {} local image_selected = {}
local image_table_sids = image_table:cfgsections()
for _, image_table_sid in ipairs(image_table_sids) do for k in pairs(image_list) do
if image_list[image_table_sid]._selected == 1 then if image_list[k]._selected == 1 then
image_selected[#image_selected+1] = image_list[image_table_sid].id --image_id:cfgvalue(image_table_sid) image_selected[#image_selected + 1] = image_list[k].id
end end
end end
if next(image_selected) ~= nil then if next(image_selected) ~= nil then
local names local names, first
for _,img in ipairs(image_selected) do
for _, img in ipairs(image_selected) do
names = names and (names .. "&names=".. img) or img names = names and (names .. "&names=".. img) or img
end end
local first
local cb = function(res, chunk) local cb = function(res, chunk)
if res.code == 200 then if res.code == 200 then
if not first then if not first then
@ -205,8 +253,10 @@ btnsave.write = function (self, section)
luci.ltn12.pump.all(chunk, luci.http.write) luci.ltn12.pump.all(chunk, luci.http.write)
end end
end end
docker:write_status("Images: " .. "save" .. " " .. table.concat(image_selected, "\n") .. "...") docker:write_status("Images: " .. "save" .. " " .. table.concat(image_selected, "\n") .. "...")
local msg = dk.images:get({query = {names = names}}, cb) local msg = dk.images:get({query = {names = names}}, cb)
if msg.code ~= 200 then if msg.code ~= 200 then
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n") docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
success = false success = false
@ -216,8 +266,9 @@ btnsave.write = function (self, section)
end end
end end
local btnload = action:option(Button, "load") o = s:option(Button, "load")
btnload.inputtitle= translate("Load") o.inputtitle= translate("Load")
btnload.template = "dockerman/images_load" o.template = "dockerman/images_load"
btnload.inputstyle = "add" o.inputstyle = "add"
return m return m

View file

@ -3,113 +3,134 @@ LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman> Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]-- ]]--
require "luci.util"
local uci = luci.model.uci.cursor()
local docker = require "luci.model.docker" local docker = require "luci.model.docker"
local dk = docker.new()
local networks local m, s, o
local res = dk.networks:list() local networks, dk, res
if res.code < 300 then networks = res.body else return end
dk = docker.new()
res = dk.networks:list()
if res.code < 300 then
networks = res.body
else
return
end
local get_networks = function () local get_networks = function ()
local data = {} local data = {}
if type(networks) ~= "table" then return nil end if type(networks) ~= "table" then
return nil
end
for i, v in ipairs(networks) do for i, v in ipairs(networks) do
local index = v.Created .. v.Id local index = v.Created .. v.Id
data[index]={} data[index]={}
data[index]["_selected"] = 0 data[index]["_selected"] = 0
data[index]["_id"] = v.Id:sub(1,12) data[index]["_id"] = v.Id:sub(1,12)
data[index]["_name"] = v.Name data[index]["_name"] = v.Name
data[index]["_driver"] = v.Driver data[index]["_driver"] = v.Driver
if v.Driver == "bridge" then if v.Driver == "bridge" then
data[index]["_interface"] = v.Options["com.docker.network.bridge.name"] data[index]["_interface"] = v.Options["com.docker.network.bridge.name"]
elseif v.Driver == "macvlan" then elseif v.Driver == "macvlan" then
data[index]["_interface"] = v.Options.parent data[index]["_interface"] = v.Options.parent
end end
data[index]["_subnet"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil data[index]["_subnet"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
data[index]["_gateway"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Gateway or nil data[index]["_gateway"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Gateway or nil
end end
return data return data
end end
local network_list = get_networks() local network_list = get_networks()
-- m = Map("docker", translate("Docker"))
m = SimpleForm("docker", translate("Docker")) m = SimpleForm("docker", translate("Docker"))
m.submit=false m.submit=false
m.reset=false m.reset=false
network_table = m:section(Table, network_list, translate("Networks")) s = m:section(Table, network_list, translate("Networks"))
network_table.nodescr=true s.nodescr=true
network_selecter = network_table:option(Flag, "_selected","") o = s:option(Flag, "_selected","")
network_selecter.template = "dockerman/cbi/xfvalue" o.template = "dockerman/cbi/xfvalue"
network_id = network_table:option(DummyValue, "_id", translate("ID")) o.disabled = 0
network_selecter.disabled = 0 o.enabled = 1
network_selecter.enabled = 1 o.default = 0
network_selecter.default = 0 o.render = function(self, section, scope)
network_selecter.render = function(self, section, scope)
self.disable = 0 self.disable = 0
if network_list[section]["_name"] == "bridge" or network_list[section]["_name"] == "none" or network_list[section]["_name"] == "host" then if network_list[section]["_name"] == "bridge" or network_list[section]["_name"] == "none" or network_list[section]["_name"] == "host" then
self.disable = 1 self.disable = 1
end end
Flag.render(self, section, scope) Flag.render(self, section, scope)
end end
o.write = function(self, section, value)
network_name = network_table:option(DummyValue, "_name", translate("Network Name"))
network_driver = network_table:option(DummyValue, "_driver", translate("Driver"))
network_interface = network_table:option(DummyValue, "_interface", translate("Parent Interface"))
network_subnet = network_table:option(DummyValue, "_subnet", translate("Subnet"))
network_gateway = network_table:option(DummyValue, "_gateway", translate("Gateway"))
network_selecter.write = function(self, section, value)
network_list[section]._selected = value network_list[section]._selected = value
end end
docker_status = m:section(SimpleSection) o = s:option(DummyValue, "_id", translate("ID"))
docker_status.template = "dockerman/apply_widget"
docker_status.err=docker:read_status()
docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end
action = m:section(Table,{{}}) o = s:option(DummyValue, "_name", translate("Network Name"))
action.notitle=true
action.rowcolors=false o = s:option(DummyValue, "_driver", translate("Driver"))
action.template="cbi/nullsection"
btnnew=action:option(Button, "_new") o = s:option(DummyValue, "_interface", translate("Parent Interface"))
btnnew.inputtitle= translate("New")
btnnew.template = "dockerman/cbi/inlinebutton" o = s:option(DummyValue, "_subnet", translate("Subnet"))
btnnew.notitle=true
btnnew.inputstyle = "add" o = s:option(DummyValue, "_gateway", translate("Gateway"))
btnnew.forcewrite = true
btnnew.write = function(self, section) s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err = docker:read_status()
s.err = s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template="cbi/nullsection"
o = s:option(Button, "_new")
o.inputtitle= translate("New")
o.template = "dockerman/cbi/inlinebutton"
o.notitle=true
o.inputstyle = "add"
o.forcewrite = true
o.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork")) luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
end end
btnremove = action:option(Button, "_remove")
btnremove.inputtitle= translate("Remove") o = s:option(Button, "_remove")
btnremove.template = "dockerman/cbi/inlinebutton" o.inputtitle= translate("Remove")
btnremove.inputstyle = "remove" o.template = "dockerman/cbi/inlinebutton"
btnremove.forcewrite = true o.inputstyle = "remove"
btnremove.write = function(self, section) o.forcewrite = true
o.write = function(self, section)
local network_selected = {} local network_selected = {}
local network_name_selected = {} local network_name_selected = {}
local network_driver_selected = {} local network_driver_selected = {}
-- 遍历table中sectionid
local network_table_sids = network_table:cfgsections() for k in pairs(network_list) do
for _, network_table_sid in ipairs(network_table_sids) do if network_list[k]._selected == 1 then
-- 得到选中项的名字 network_selected[#network_selected + 1] = network_list[k]._id
if network_list[network_table_sid]._selected == 1 then network_name_selected[#network_name_selected + 1] = network_list[k]._name
network_selected[#network_selected+1] = network_list[network_table_sid]._id --network_name:cfgvalue(network_table_sid) network_driver_selected[#network_driver_selected + 1] = network_list[k]._driver
network_name_selected[#network_name_selected+1] = network_list[network_table_sid]._name
network_driver_selected[#network_driver_selected+1] = network_list[network_table_sid]._driver
end end
end end
if next(network_selected) ~= nil then if next(network_selected) ~= nil then
local success = true local success = true
docker:clear_status() docker:clear_status()
for ii, net in ipairs(network_selected) do for ii, net in ipairs(network_selected) do
docker:append_status("Networks: " .. "remove" .. " " .. net .. "...") docker:append_status("Networks: " .. "remove" .. " " .. net .. "...")
local res = dk.networks["remove"](dk, {id = net}) local res = dk.networks["remove"](dk, {id = net})
if res and res.code >= 300 then if res and res.code >= 300 then
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n") docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
success = false success = false
@ -120,6 +141,7 @@ btnremove.write = function(self, section)
end end
end end
end end
if success then if success then
docker:clear_status() docker:clear_status()
end end

View file

@ -3,39 +3,237 @@ LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman> Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]-- ]]--
require "luci.util"
local uci = luci.model.uci.cursor()
local docker = require "luci.model.docker" local docker = require "luci.model.docker"
local m, s, o
local dk = docker.new() local dk = docker.new()
local cmd_line = table.concat(arg, '/') local cmd_line = table.concat(arg, '/')
local create_body = {} local create_body = {}
local images = dk.images:list().body local images = dk.images:list().body
local networks = dk.networks:list().body local networks = dk.networks:list().body
local containers = dk.containers:list({query = {all=true}}).body local containers = dk.containers:list({
query = {
all=true
}
}).body
local is_quot_complete = function(str) local is_quot_complete = function(str)
local num = 0, w
require "math" require "math"
if not str then return true end
if not str then
return true
end
local num = 0, w local num = 0, w
for w in str:gmatch("\"") do for w in str:gmatch("\"") do
num = num + 1 num = num + 1
end end
if math.fmod(num, 2) ~= 0 then return false end
if math.fmod(num, 2) ~= 0 then
return false
end
num = 0 num = 0
for w in str:gmatch("\'") do for w in str:gmatch("\'") do
num = num + 1 num = num + 1
end end
if math.fmod(num, 2) ~= 0 then return false end
if math.fmod(num, 2) ~= 0 then
return false
end
return true return true
end end
function contains(list, x)
for _, v in pairs(list) do
if v == x then
return true
end
end
return false
end
local resolve_cli = function(cmd_line) local resolve_cli = function(cmd_line)
local config = {advance = 1} local config = {
local key_no_val = '|t|d|i|tty|rm|read_only|interactive|init|help|detach|privileged|P|publish_all|' advance = 1
local key_with_val = '|sysctl|add_host|a|attach|blkio_weight_device|cap_add|cap_drop|device|device_cgroup_rule|device_read_bps|device_read_iops|device_write_bps|device_write_iops|dns|dns_option|dns_search|e|env|env_file|expose|group_add|l|label|label_file|link|link_local_ip|log_driver|log_opt|network_alias|p|publish|security_opt|storage_opt|tmpfs|v|volume|volumes_from|blkio_weight|cgroup_parent|cidfile|cpu_period|cpu_quota|cpu_rt_period|cpu_rt_runtime|c|cpu_shares|cpus|cpuset_cpus|cpuset_mems|detach_keys|disable_content_trust|domainname|entrypoint|gpus|health_cmd|health_interval|health_retries|health_start_period|health_timeout|h|hostname|ip|ip6|ipc|isolation|kernel_memory|log_driver|mac_address|m|memory|memory_reservation|memory_swap|memory_swappiness|mount|name|network|no_healthcheck|oom_kill_disable|oom_score_adj|pid|pids_limit|restart|runtime|shm_size|sig_proxy|stop_signal|stop_timeout|ulimit|u|user|userns|uts|volume_driver|w|workdir|' }
local key_abb = {net='network',a='attach',c='cpu-shares',d='detach',e='env',h='hostname',i='interactive',l='label',m='memory',p='publish',P='publish_all',t='tty',u='user',v='volume',w='workdir'}
local key_with_list = '|sysctl|add_host|a|attach|blkio_weight_device|cap_add|cap_drop|device|device_cgroup_rule|device_read_bps|device_read_iops|device_write_bps|device_write_iops|dns|dns_option|dns_search|e|env|env_file|expose|group_add|l|label|label_file|link|link_local_ip|log_driver|log_opt|network_alias|p|publish|security_opt|storage_opt|tmpfs|v|volume|volumes_from|' local key_no_val = {
't',
'd',
'i',
'tty',
'rm',
'read_only',
'interactive',
'init',
'help',
'detach',
'privileged',
'P',
'publish_all',
}
local key_with_val = {
'sysctl',
'add_host',
'a',
'attach',
'blkio_weight_device',
'cap_add',
'cap_drop',
'device',
'device_cgroup_rule',
'device_read_bps',
'device_read_iops',
'device_write_bps',
'device_write_iops',
'dns',
'dns_option',
'dns_search',
'e',
'env',
'env_file',
'expose',
'group_add',
'l',
'label',
'label_file',
'link',
'link_local_ip',
'log_driver',
'log_opt',
'network_alias',
'p',
'publish',
'security_opt',
'storage_opt',
'tmpfs',
'v',
'volume',
'volumes_from',
'blkio_weight',
'cgroup_parent',
'cidfile',
'cpu_period',
'cpu_quota',
'cpu_rt_period',
'cpu_rt_runtime',
'c',
'cpu_shares',
'cpus',
'cpuset_cpus',
'cpuset_mems',
'detach_keys',
'disable_content_trust',
'domainname',
'entrypoint',
'gpus',
'health_cmd',
'health_interval',
'health_retries',
'health_start_period',
'health_timeout',
'h',
'hostname',
'ip',
'ip6',
'ipc',
'isolation',
'kernel_memory',
'log_driver',
'mac_address',
'm',
'memory',
'memory_reservation',
'memory_swap',
'memory_swappiness',
'mount',
'name',
'network',
'no_healthcheck',
'oom_kill_disable',
'oom_score_adj',
'pid',
'pids_limit',
'restart',
'runtime',
'shm_size',
'sig_proxy',
'stop_signal',
'stop_timeout',
'ulimit',
'u',
'user',
'userns',
'uts',
'volume_driver',
'w',
'workdir'
}
local key_abb = {
net='network',
a='attach',
c='cpu-shares',
d='detach',
e='env',
h='hostname',
i='interactive',
l='label',
m='memory',
p='publish',
P='publish_all',
t='tty',
u='user',
v='volume',
w='workdir'
}
local key_with_list = {
'sysctl',
'add_host',
'a',
'attach',
'blkio_weight_device',
'cap_add',
'cap_drop',
'device',
'device_cgroup_rule',
'device_read_bps',
'device_read_iops',
'device_write_bps',
'device_write_iops',
'dns',
'dns_optiondns_search',
'e',
'env',
'env_file',
'expose',
'group_add',
'l',
'label',
'label_file',
'link',
'link_local_ip',
'log_driver',
'log_opt',
'network_alias',
'p',
'publish',
'security_opt',
'storage_opt',
'tmpfs',
'v',
'volume',
'volumes_from',
}
local key = nil local key = nil
local _key = nil local _key = nil
local val = nil local val = nil
@ -72,7 +270,9 @@ local resolve_cli = function(cmd_line)
config[key_abb["P"]] = true config[key_abb["P"]] = true
key:gsub("P", "") key:gsub("P", "")
end end
if key == "" then key = nil end if key == "" then
key = nil
end
end end
end end
end end
@ -80,11 +280,11 @@ local resolve_cli = function(cmd_line)
if key then if key then
key = key:gsub("-","_") key = key:gsub("-","_")
key = key_abb[key] or key key = key_abb[key] or key
if key_no_val:match("|"..key.."|") then if contains(key_no_val, key) then
config[key] = true config[key] = true
val = nil val = nil
key = nil key = nil
elseif key_with_val:match("|"..key.."|") then elseif contains(key_with_val, key) then
-- if key == "cap_add" then config.privileged = true end -- if key == "cap_add" then config.privileged = true end
else else
key = nil key = nil
@ -104,18 +304,19 @@ local resolve_cli = function(cmd_line)
local source = (_type ~= "tmpfs") and (w:match("source=([^,]+),") or w:match("src=([^,]+),")) or "" local source = (_type ~= "tmpfs") and (w:match("source=([^,]+),") or w:match("src=([^,]+),")) or ""
local target = w:match(",target=([^,]+)") or w:match(",dst=([^,]+)") or w:match(",destination=([^,]+)") or "" local target = w:match(",target=([^,]+)") or w:match(",dst=([^,]+)") or w:match(",destination=([^,]+)") or ""
local ro = w:match(",readonly") and "ro" or nil local ro = w:match(",readonly") and "ro" or nil
if source and target then if source and target then
if _type ~= "tmpfs" then if _type ~= "tmpfs" then
-- bind or volume
local bind_propagation = (_type == "bind") and w:match(",bind%-propagation=([^,]+)") or nil local bind_propagation = (_type == "bind") and w:match(",bind%-propagation=([^,]+)") or nil
val = source..":"..target .. ((ro or bind_propagation) and (":" .. (ro and ro or "") .. (((ro and bind_propagation) and "," or "") .. (bind_propagation and bind_propagation or ""))or "")) val = source..":"..target .. ((ro or bind_propagation) and (":" .. (ro and ro or "") .. (((ro and bind_propagation) and "," or "") .. (bind_propagation and bind_propagation or ""))or ""))
else else
-- tmpfs
local tmpfs_mode = w:match(",tmpfs%-mode=([^,]+)") or nil local tmpfs_mode = w:match(",tmpfs%-mode=([^,]+)") or nil
local tmpfs_size = w:match(",tmpfs%-size=([^,]+)") or nil local tmpfs_size = w:match(",tmpfs%-size=([^,]+)") or nil
key = "tmpfs" key = "tmpfs"
val = target .. ((tmpfs_mode or tmpfs_size) and (":" .. (tmpfs_mode and ("mode=" .. tmpfs_mode) or "") .. ((tmpfs_mode and tmpfs_size) and "," or "") .. (tmpfs_size and ("size=".. tmpfs_size) or "")) or "") val = target .. ((tmpfs_mode or tmpfs_size) and (":" .. (tmpfs_mode and ("mode=" .. tmpfs_mode) or "") .. ((tmpfs_mode and tmpfs_size) and "," or "") .. (tmpfs_size and ("size=".. tmpfs_size) or "")) or "")
if not config[key] then config[key] = {} end if not config[key] then
config[key] = {}
end
table.insert( config[key], val ) table.insert( config[key], val )
key = nil key = nil
val = nil val = nil
@ -129,15 +330,16 @@ local resolve_cli = function(cmd_line)
end end
if (key or _key) and val then if (key or _key) and val then
key = _key or key key = _key or key
if key_with_list:match("|"..key.."|") then if contains(key_with_list, key) then
if not config[key] then config[key] = {} end if not config[key] then
config[key] = {}
end
if _key then if _key then
config[key][#config[key]] = config[key][#config[key]] .. " " .. w config[key][#config[key]] = config[key][#config[key]] .. " " .. w
else else
table.insert( config[key], val ) table.insert( config[key], val )
end end
if is_quot_complete(config[key][#config[key]]) then if is_quot_complete(config[key][#config[key]]) then
-- clear quotation marks
config[key][#config[key]] = config[key][#config[key]]:gsub("[\"\']", "") config[key][#config[key]] = config[key][#config[key]]:gsub("[\"\']", "")
_key = nil _key = nil
else else
@ -146,7 +348,6 @@ local resolve_cli = function(cmd_line)
else else
config[key] = (config[key] and (config[key] .. " ") or "") .. val config[key] = (config[key] and (config[key] .. " ") or "") .. val
if is_quot_complete(config[key]) then if is_quot_complete(config[key]) then
-- clear quotation marks
config[key] = config[key]:gsub("[\"\']", "") config[key] = config[key]:gsub("[\"\']", "")
_key = nil _key = nil
else else
@ -157,16 +358,22 @@ local resolve_cli = function(cmd_line)
val = nil val = nil
end end
end end
return config return config
end end
-- reslvo default config
local default_config = {} local default_config = {}
if cmd_line and cmd_line:match("^DOCKERCLI.+") then if cmd_line and cmd_line:match("^DOCKERCLI.+") then
default_config = resolve_cli(cmd_line) default_config = resolve_cli(cmd_line)
elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then
local container_id = cmd_line:match("^duplicate/(.+)") local container_id = cmd_line:match("^duplicate/(.+)")
create_body = dk:containers_duplicate_config({id = container_id}) or {} create_body = dk:containers_duplicate_config({id = container_id}) or {}
if not create_body.HostConfig then create_body.HostConfig = {} end
if not create_body.HostConfig then
create_body.HostConfig = {}
end
if next(create_body) ~= nil then if next(create_body) ~= nil then
default_config.name = nil default_config.name = nil
default_config.image = create_body.Image default_config.image = create_body.Image
@ -175,8 +382,6 @@ elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then
default_config.interactive = create_body.OpenStdin and true or false default_config.interactive = create_body.OpenStdin and true or false
default_config.privileged = create_body.HostConfig.Privileged and true or false default_config.privileged = create_body.HostConfig.Privileged and true or false
default_config.restart = create_body.HostConfig.RestartPolicy and create_body.HostConfig.RestartPolicy.name or nil default_config.restart = create_body.HostConfig.RestartPolicy and create_body.HostConfig.RestartPolicy.name or nil
-- default_config.network = create_body.HostConfig.NetworkMode == "default" and "bridge" or create_body.HostConfig.NetworkMode
-- if container has leave original network, and add new network, .HostConfig.NetworkMode is INcorrect, so using first child of .NetworkingConfig.EndpointsConfig
default_config.network = create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and next(create_body.NetworkingConfig.EndpointsConfig) or nil default_config.network = create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and next(create_body.NetworkingConfig.EndpointsConfig) or nil
default_config.ip = default_config.network and default_config.network ~= "bridge" and default_config.network ~= "host" and default_config.network ~= "null" and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig.IPv4Address or nil default_config.ip = default_config.network and default_config.network ~= "bridge" and default_config.network ~= "host" and default_config.network ~= "null" and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig.IPv4Address or nil
default_config.link = create_body.HostConfig.Links default_config.link = create_body.HostConfig.Links
@ -221,6 +426,7 @@ elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then
table.insert( default_config.device, v.PathOnHost..":"..v.PathInContainer..(v.CgroupPermissions ~= "" and (":" .. v.CgroupPermissions) or "") ) table.insert( default_config.device, v.PathOnHost..":"..v.PathInContainer..(v.CgroupPermissions ~= "" and (":" .. v.CgroupPermissions) or "") )
end end
end end
if create_body.HostConfig.Tmpfs and type(create_body.HostConfig.Tmpfs) == "table" then if create_body.HostConfig.Tmpfs and type(create_body.HostConfig.Tmpfs) == "table" then
default_config.tmpfs = {} default_config.tmpfs = {}
for k, v in pairs(create_body.HostConfig.Tmpfs) do for k, v in pairs(create_body.HostConfig.Tmpfs) do
@ -230,71 +436,69 @@ elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then
end end
end end
local m = SimpleForm("docker", translate("Docker")) m = SimpleForm("docker", translate("Docker"))
m.redirect = luci.dispatcher.build_url("admin", "docker", "containers") m.redirect = luci.dispatcher.build_url("admin", "docker", "containers")
-- m.reset = false
-- m.submit = false
-- new Container
docker_status = m:section(SimpleSection) s = m:section(SimpleSection)
docker_status.template = "dockerman/apply_widget" s.template = "dockerman/apply_widget"
docker_status.err=docker:read_status() s.err=docker:read_status()
docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;") s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end if s.err then
docker:clear_status()
end
local s = m:section(SimpleSection, translate("New Container")) s = m:section(SimpleSection, translate("New Container"))
s.addremove = true s.addremove = true
s.anonymous = true s.anonymous = true
local d = s:option(DummyValue,"cmd_line", translate("Resolve CLI")) o = s:option(DummyValue,"cmd_line", translate("Resolve CLI"))
d.rawhtml = true o.rawhtml = true
d.template = "dockerman/newcontainer_resolve" o.template = "dockerman/newcontainer_resolve"
d = s:option(Value, "name", translate("Container Name")) o = s:option(Value, "name", translate("Container Name"))
d.rmempty = true o.rmempty = true
d.default = default_config.name or nil o.default = default_config.name or nil
d = s:option(Flag, "interactive", translate("Interactive (-i)")) o = s:option(Flag, "interactive", translate("Interactive (-i)"))
d.rmempty = true o.rmempty = true
d.disabled = 0 o.disabled = 0
d.enabled = 1 o.enabled = 1
d.default = default_config.interactive and 1 or 0 o.default = default_config.interactive and 1 or 0
d = s:option(Flag, "tty", translate("TTY (-t)")) o = s:option(Flag, "tty", translate("TTY (-t)"))
d.rmempty = true o.rmempty = true
d.disabled = 0 o.disabled = 0
d.enabled = 1 o.enabled = 1
d.default = default_config.tty and 1 or 0 o.default = default_config.tty and 1 or 0
d = s:option(Value, "image", translate("Docker Image")) o = s:option(Value, "image", translate("Docker Image"))
d.rmempty = true o.rmempty = true
d.default = default_config.image or nil o.default = default_config.image or nil
for _, v in ipairs (images) do for _, v in ipairs (images) do
if v.RepoTags then if v.RepoTags then
d:value(v.RepoTags[1], v.RepoTags[1]) o:value(v.RepoTags[1], v.RepoTags[1])
end end
end end
d = s:option(Flag, "_force_pull", translate("Always pull image first")) o = s:option(Flag, "_force_pull", translate("Always pull image first"))
d.rmempty = true o.rmempty = true
d.disabled = 0 o.disabled = 0
d.enabled = 1 o.enabled = 1
d.default = 0 o.default = 0
d = s:option(Flag, "privileged", translate("Privileged")) o = s:option(Flag, "privileged", translate("Privileged"))
d.rmempty = true o.rmempty = true
d.disabled = 0 o.disabled = 0
d.enabled = 1 o.enabled = 1
d.default = default_config.privileged and 1 or 0 o.default = default_config.privileged and 1 or 0
d = s:option(ListValue, "restart", translate("Restart Policy")) o = s:option(ListValue, "restart", translate("Restart Policy"))
d.rmempty = true o.rmempty = true
o:value("no", "No")
d:value("no", "No") o:value("unless-stopped", "Unless stopped")
d:value("unless-stopped", "Unless stopped") o:value("always", "Always")
d:value("always", "Always") o:value("on-failure", "On failure")
d:value("on-failure", "On failure") o.default = default_config.restart or "unless-stopped"
d.default = default_config.restart or "unless-stopped"
local d_network = s:option(ListValue, "network", translate("Networks")) local d_network = s:option(ListValue, "network", translate("Networks"))
d_network.rmempty = true d_network.rmempty = true
@ -305,116 +509,146 @@ d_ip.datatype="ip4addr"
d_ip:depends("network", "nil") d_ip:depends("network", "nil")
d_ip.default = default_config.ip or nil d_ip.default = default_config.ip or nil
d = s:option(DynamicList, "link", translate("Links with other containers")) o = s:option(DynamicList, "link", translate("Links with other containers"))
d.placeholder = "container_name:alias" o.placeholder = "container_name:alias"
d.rmempty = true o.rmempty = true
d:depends("network", "bridge") o:depends("network", "bridge")
d.default = default_config.link or nil o.default = default_config.link or nil
d = s:option(DynamicList, "dns", translate("Set custom DNS servers")) o = s:option(DynamicList, "dns", translate("Set custom DNS servers"))
d.placeholder = "8.8.8.8" o.placeholder = "8.8.8.8"
d.rmempty = true o.rmempty = true
d.default = default_config.dns or nil o.default = default_config.dns or nil
d = s:option(Value, "user", translate("User(-u)"), translate("The user that commands are run as inside the container.(format: name|uid[:group|gid])")) o = s:option(Value, "user",
d.placeholder = "1000:1000" translate("User(-u)"),
d.rmempty = true translate("The user that commands are run as inside the container.(format: name|uid[:group|gid])"))
d.default = default_config.user or nil o.placeholder = "1000:1000"
o.rmempty = true
o.default = default_config.user or nil
d = s:option(DynamicList, "env", translate("Environmental Variable(-e)"), translate("Set environment variables to inside the container")) o = s:option(DynamicList, "env",
d.placeholder = "TZ=Asia/Shanghai" translate("Environmental Variable(-e)"),
d.rmempty = true translate("Set environment variables to inside the container"))
d.default = default_config.env or nil o.placeholder = "TZ=Asia/Shanghai"
o.rmempty = true
o.default = default_config.env or nil
d = s:option(DynamicList, "volume", translate("Bind Mount(-v)"), translate("Bind mount a volume")) o = s:option(DynamicList, "volume",
d.placeholder = "/media:/media:slave" translate("Bind Mount(-v)"),
d.rmempty = true translate("Bind mount a volume"))
d.default = default_config.volume or nil o.placeholder = "/media:/media:slave"
o.rmempty = true
o.default = default_config.volume or nil
local d_publish = s:option(DynamicList, "publish", translate("Exposed Ports(-p)"), translate("Publish container's port(s) to the host")) local d_publish = s:option(DynamicList, "publish",
translate("Exposed Ports(-p)"),
translate("Publish container's port(s) to the host"))
d_publish.placeholder = "2200:22/tcp" d_publish.placeholder = "2200:22/tcp"
d_publish.rmempty = true d_publish.rmempty = true
d_publish.default = default_config.publish or nil d_publish.default = default_config.publish or nil
d = s:option(Value, "command", translate("Run command")) o = s:option(Value, "command", translate("Run command"))
d.placeholder = "/bin/sh init.sh" o.placeholder = "/bin/sh init.sh"
d.rmempty = true o.rmempty = true
d.default = default_config.command or nil o.default = default_config.command or nil
d = s:option(Flag, "advance", translate("Advance")) o = s:option(Flag, "advance", translate("Advance"))
d.rmempty = true o.rmempty = true
d.disabled = 0 o.disabled = 0
d.enabled = 1 o.enabled = 1
d.default = default_config.advance or 0 o.default = default_config.advance or 0
d = s:option(Value, "hostname", translate("Host Name"), translate("The hostname to use for the container")) o = s:option(Value, "hostname",
d.rmempty = true translate("Host Name"),
d.default = default_config.hostname or nil translate("The hostname to use for the container"))
d:depends("advance", 1) o.rmempty = true
o.default = default_config.hostname or nil
o:depends("advance", 1)
d = s:option(Flag, "publish_all", translate("Exposed All Ports(-P)"), translate("Allocates an ephemeral host port for all of a container's exposed ports")) o = s:option(Flag, "publish_all",
d.rmempty = true translate("Exposed All Ports(-P)"),
d.disabled = 0 translate("Allocates an ephemeral host port for all of a container's exposed ports"))
d.enabled = 1 o.rmempty = true
d.default = default_config.publish_all and 1 or 0 o.disabled = 0
d:depends("advance", 1) o.enabled = 1
o.default = default_config.publish_all and 1 or 0
o:depends("advance", 1)
d = s:option(DynamicList, "device", translate("Device(--device)"), translate("Add host device to the container")) o = s:option(DynamicList, "device",
d.placeholder = "/dev/sda:/dev/xvdc:rwm" translate("Device(--device)"),
d.rmempty = true translate("Add host device to the container"))
d:depends("advance", 1) o.placeholder = "/dev/sda:/dev/xvdc:rwm"
d.default = default_config.device or nil o.rmempty = true
o:depends("advance", 1)
o.default = default_config.device or nil
d = s:option(DynamicList, "tmpfs", translate("Tmpfs(--tmpfs)"), translate("Mount tmpfs directory")) o = s:option(DynamicList, "tmpfs",
d.placeholder = "/run:rw,noexec,nosuid,size=65536k" translate("Tmpfs(--tmpfs)"),
d.rmempty = true translate("Mount tmpfs directory"))
d:depends("advance", 1) o.placeholder = "/run:rw,noexec,nosuid,size=65536k"
d.default = default_config.tmpfs or nil o.rmempty = true
o:depends("advance", 1)
o.default = default_config.tmpfs or nil
d = s:option(DynamicList, "sysctl", translate("Sysctl(--sysctl)"), translate("Sysctls (kernel parameters) options")) o = s:option(DynamicList, "sysctl",
d.placeholder = "net.ipv4.ip_forward=1" translate("Sysctl(--sysctl)"),
d.rmempty = true translate("Sysctls (kernel parameters) options"))
d:depends("advance", 1) o.placeholder = "net.ipv4.ip_forward=1"
d.default = default_config.sysctl or nil o.rmempty = true
o:depends("advance", 1)
o.default = default_config.sysctl or nil
d = s:option(DynamicList, "cap_add", translate("CAP-ADD(--cap-add)"), translate("A list of kernel capabilities to add to the container")) o = s:option(DynamicList, "cap_add",
d.placeholder = "NET_ADMIN" translate("CAP-ADD(--cap-add)"),
d.rmempty = true translate("A list of kernel capabilities to add to the container"))
d:depends("advance", 1) o.placeholder = "NET_ADMIN"
d.default = default_config.cap_add or nil o.rmempty = true
o:depends("advance", 1)
o.default = default_config.cap_add or nil
d = s:option(Value, "cpus", translate("CPUs"), translate("Number of CPUs. Number is a fractional number. 0.000 means no limit")) o = s:option(Value, "cpus",
d.placeholder = "1.5" translate("CPUs"),
d.rmempty = true translate("Number of CPUs. Number is a fractional number. 0.000 means no limit"))
d:depends("advance", 1) o.placeholder = "1.5"
d.datatype="ufloat" o.rmempty = true
d.default = default_config.cpus or nil o:depends("advance", 1)
o.datatype="ufloat"
o.default = default_config.cpus or nil
d = s:option(Value, "cpu_shares", translate("CPU Shares Weight"), translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024")) o = s:option(Value, "cpu_shares",
d.placeholder = "1024" translate("CPU Shares Weight"),
d.rmempty = true translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024"))
d:depends("advance", 1) o.placeholder = "1024"
d.datatype="uinteger" o.rmempty = true
d.default = default_config.cpu_shares or nil o:depends("advance", 1)
o.datatype="uinteger"
o.default = default_config.cpu_shares or nil
d = s:option(Value, "memory", translate("Memory"), translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M")) o = s:option(Value, "memory",
d.placeholder = "128m" translate("Memory"),
d.rmempty = true translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M"))
d:depends("advance", 1) o.placeholder = "128m"
d.default = default_config.memory or nil o.rmempty = true
o:depends("advance", 1)
o.default = default_config.memory or nil
d = s:option(Value, "blkio_weight", translate("Block IO Weight"), translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000")) o = s:option(Value, "blkio_weight",
d.placeholder = "500" translate("Block IO Weight"),
d.rmempty = true translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000"))
d:depends("advance", 1) o.placeholder = "500"
d.datatype="uinteger" o.rmempty = true
d.default = default_config.blkio_weight or nil o:depends("advance", 1)
o.datatype="uinteger"
o.default = default_config.blkio_weight or nil
d = s:option(DynamicList, "log_opt", translate("Log driver options"), translate("The logging configuration for this container")) o = s:option(DynamicList, "log_opt",
d.placeholder = "max-size=1m" translate("Log driver options"),
d.rmempty = true translate("The logging configuration for this container"))
d:depends("advance", 1) o.placeholder = "max-size=1m"
d.default = default_config.log_opt or nil o.rmempty = true
o:depends("advance", 1)
o.default = default_config.log_opt or nil
for _, v in ipairs (networks) do for _, v in ipairs (networks) do
if v.Name then if v.Name then
@ -435,7 +669,10 @@ for _, v in ipairs (networks) do
end end
m.handle = function(self, state, data) m.handle = function(self, state, data)
if state ~= FORM_VALID then return end if state ~= FORM_VALID then
return
end
local tmp local tmp
local name = data.name or ("luci_" .. os.date("%Y%m%d%H%M%S")) local name = data.name or ("luci_" .. os.date("%Y%m%d%H%M%S"))
local hostname = data.hostname local hostname = data.hostname
@ -444,15 +681,18 @@ m.handle = function(self, state, data)
local interactive = type(data.interactive) == "number" and (data.interactive == 1 and true or false) or default_config.interactive or false local interactive = type(data.interactive) == "number" and (data.interactive == 1 and true or false) or default_config.interactive or false
local image = data.image local image = data.image
local user = data.user local user = data.user
if image and not image:match(".-:.+") then if image and not image:match(".-:.+") then
image = image .. ":latest" image = image .. ":latest"
end end
local privileged = type(data.privileged) == "number" and (data.privileged == 1 and true or false) or default_config.privileged or false local privileged = type(data.privileged) == "number" and (data.privileged == 1 and true or false) or default_config.privileged or false
local restart = data.restart local restart = data.restart
local env = data.env local env = data.env
local dns = data.dns local dns = data.dns
local cap_add = data.cap_add local cap_add = data.cap_add
local sysctl = {} local sysctl = {}
tmp = data.sysctl tmp = data.sysctl
if type(tmp) == "table" then if type(tmp) == "table" then
for i, v in ipairs(tmp) do for i, v in ipairs(tmp) do
@ -462,6 +702,7 @@ m.handle = function(self, state, data)
end end
end end
end end
local log_opt = {} local log_opt = {}
tmp = data.log_opt tmp = data.log_opt
if type(tmp) == "table" then if type(tmp) == "table" then
@ -472,6 +713,7 @@ m.handle = function(self, state, data)
end end
end end
end end
local network = data.network local network = data.network
local ip = (network ~= "bridge" and network ~= "host" and network ~= "none") and data.ip or nil local ip = (network ~= "bridge" and network ~= "host" and network ~= "none") and data.ip or nil
local volume = data.volume local volume = data.volume
@ -482,6 +724,7 @@ m.handle = function(self, state, data)
local portbindings = {} local portbindings = {}
local exposedports = {} local exposedports = {}
local tmpfs = {} local tmpfs = {}
tmp = data.tmpfs tmp = data.tmpfs
if type(tmp) == "table" then if type(tmp) == "table" then
@ -516,6 +759,7 @@ m.handle = function(self, state, data)
t['CgroupPermissions'] = "rwm" t['CgroupPermissions'] = "rwm"
end end
end end
if next(t) ~= nil then if next(t) ~= nil then
table.insert( device, t ) table.insert( device, t )
end end
@ -542,6 +786,7 @@ m.handle = function(self, state, data)
command[#command+1] = v command[#command+1] = v
end end
end end
if memory ~= 0 then if memory ~= 0 then
_,_,n,unit = memory:find("([%d%.]+)([%l%u]+)") _,_,n,unit = memory:find("([%d%.]+)([%l%u]+)")
if n then if n then
@ -577,14 +822,15 @@ m.handle = function(self, state, data)
create_body.HostConfig.NanoCPUs = tonumber(cpus) * 10 ^ 9 create_body.HostConfig.NanoCPUs = tonumber(cpus) * 10 ^ 9
create_body.HostConfig.BlkioWeight = tonumber(blkio_weight) create_body.HostConfig.BlkioWeight = tonumber(blkio_weight)
create_body.HostConfig.PublishAllPorts = publish_all create_body.HostConfig.PublishAllPorts = publish_all
if create_body.HostConfig.NetworkMode ~= network then if create_body.HostConfig.NetworkMode ~= network then
-- network mode changed, need to clear duplicate config
create_body.NetworkingConfig = nil create_body.NetworkingConfig = nil
end end
create_body.HostConfig.NetworkMode = network create_body.HostConfig.NetworkMode = network
if ip then if ip then
if create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and type(create_body.NetworkingConfig.EndpointsConfig) == "table" then if create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and type(create_body.NetworkingConfig.EndpointsConfig) == "table" then
-- ip + duplicate config
for k, v in pairs (create_body.NetworkingConfig.EndpointsConfig) do for k, v in pairs (create_body.NetworkingConfig.EndpointsConfig) do
if k == network and v.IPAMConfig and v.IPAMConfig.IPv4Address then if k == network and v.IPAMConfig and v.IPAMConfig.IPv4Address then
v.IPAMConfig.IPv4Address = ip v.IPAMConfig.IPv4Address = ip
@ -594,13 +840,12 @@ m.handle = function(self, state, data)
break break
end end
else else
-- ip + no duplicate config
create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } } create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } }
end end
elseif not create_body.NetworkingConfig then elseif not create_body.NetworkingConfig then
-- no ip + no duplicate config
create_body.NetworkingConfig = nil create_body.NetworkingConfig = nil
end end
create_body["HostConfig"]["Tmpfs"] = tmpfs create_body["HostConfig"]["Tmpfs"] = tmpfs
create_body["HostConfig"]["Devices"] = device create_body["HostConfig"]["Devices"] = device
create_body["HostConfig"]["Sysctls"] = sysctl create_body["HostConfig"]["Sysctls"] = sysctl
@ -610,6 +855,7 @@ m.handle = function(self, state, data)
if network == "bridge" then if network == "bridge" then
create_body["HostConfig"]["Links"] = link create_body["HostConfig"]["Links"] = link
end end
local pull_image = function(image) local pull_image = function(image)
local json_stringify = luci.jsonc and luci.jsonc.stringify local json_stringify = luci.jsonc and luci.jsonc.stringify
docker:append_status("Images: " .. "pulling" .. " " .. image .. "...\n") docker:append_status("Images: " .. "pulling" .. " " .. image .. "...\n")
@ -622,8 +868,10 @@ m.handle = function(self, state, data)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer")) luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
end end
end end
docker:clear_status() docker:clear_status()
local exist_image = false local exist_image = false
if image then if image then
for _, v in ipairs (images) do for _, v in ipairs (images) do
if v.RepoTags and v.RepoTags[1] == image then if v.RepoTags and v.RepoTags[1] == image then
@ -639,6 +887,7 @@ m.handle = function(self, state, data)
end end
create_body = docker.clear_empty_tables(create_body) create_body = docker.clear_empty_tables(create_body)
docker:append_status("Container: " .. "create" .. " " .. name .. "...") docker:append_status("Container: " .. "create" .. " " .. name .. "...")
local res = dk.containers:create({name = name, body = create_body}) local res = dk.containers:create({name = name, body = create_body})
if res and res.code == 201 then if res and res.code == 201 then

View file

@ -3,121 +3,127 @@ LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman> Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]-- ]]--
require "luci.util"
local docker = require "luci.model.docker" local docker = require "luci.model.docker"
local m, s, o
local dk = docker.new() local dk = docker.new()
m = SimpleForm("docker", translate("Docker")) m = SimpleForm("docker", translate("Docker"))
m.redirect = luci.dispatcher.build_url("admin", "docker", "networks") m.redirect = luci.dispatcher.build_url("admin", "docker", "networks")
docker_status = m:section(SimpleSection) s = m:section(SimpleSection)
docker_status.template = "dockerman/apply_widget" s.template = "dockerman/apply_widget"
docker_status.err=docker:read_status() s.err=docker:read_status()
docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;") s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end if s.err then
docker:clear_status()
end
s = m:section(SimpleSection, translate("New Network")) s = m:section(SimpleSection, translate("New Network"))
s.addremove = true s.addremove = true
s.anonymous = true s.anonymous = true
d = s:option(Value, "name", translate("Network Name")) o = s:option(Value, "name", translate("Network Name"))
d.rmempty = true o.rmempty = true
d = s:option(ListValue, "dirver", translate("Driver")) o = s:option(ListValue, "dirver", translate("Driver"))
d.rmempty = true o.rmempty = true
d:value("bridge", "bridge") o:value("bridge", "bridge")
d:value("macvlan", "macvlan") o:value("macvlan", "macvlan")
d:value("ipvlan", "ipvlan") o:value("ipvlan", "ipvlan")
d:value("overlay", "overlay") o:value("overlay", "overlay")
d = s:option(Value, "parent", translate("Parent Interface")) o = s:option(Value, "parent", translate("Parent Interface"))
d.rmempty = true o.rmempty = true
d:depends("dirver", "macvlan") o:depends("dirver", "macvlan")
local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {} local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {}
for _, v in ipairs(interfaces) do for _, v in ipairs(interfaces) do
d:value(v, v) o:value(v, v)
end end
d.default="br-lan" o.default="br-lan"
d.placeholder="br-lan" o.placeholder="br-lan"
d = s:option(Value, "macvlan_mode", translate("Macvlan Mode")) o = s:option(Value, "macvlan_mode", translate("Macvlan Mode"))
d.rmempty = true o.rmempty = true
d:depends("dirver", "macvlan") o:depends("dirver", "macvlan")
d.default="bridge" o.default="bridge"
d:value("bridge", "bridge") o:value("bridge", "bridge")
d:value("private", "private") o:value("private", "private")
d:value("vepa", "vepa") o:value("vepa", "vepa")
d:value("passthru", "passthru") o:value("passthru", "passthru")
d = s:option(Value, "ipvlan_mode", translate("Ipvlan Mode")) o = s:option(Value, "ipvlan_mode", translate("Ipvlan Mode"))
d.rmempty = true o.rmempty = true
d:depends("dirver", "ipvlan") o:depends("dirver", "ipvlan")
d.default="l3" o.default="l3"
d:value("l2", "l2") o:value("l2", "l2")
d:value("l3", "l3") o:value("l3", "l3")
d = s:option(Flag, "ingress", translate("Ingress"), translate("Ingress network is the network which provides the routing-mesh in swarm mode")) o = s:option(Flag, "ingress",
d.rmempty = true translate("Ingress"),
d.disabled = 0 translate("Ingress network is the network which provides the routing-mesh in swarm mode"))
d.enabled = 1 o.rmempty = true
d.default = 0 o.disabled = 0
d:depends("dirver", "overlay") o.enabled = 1
o.default = 0
o:depends("dirver", "overlay")
d = s:option(DynamicList, "options", translate("Options")) o = s:option(DynamicList, "options", translate("Options"))
d.rmempty = true o.rmempty = true
d.placeholder="com.docker.network.driver.mtu=1500" o.placeholder="com.docker.network.driver.mtu=1500"
d = s:option(Flag, "internal", translate("Internal"), translate("Restrict external access to the network")) o = s:option(Flag, "internal", translate("Internal"), translate("Restrict external access to the network"))
d.rmempty = true o.rmempty = true
d:depends("dirver", "overlay") o:depends("dirver", "overlay")
d.disabled = 0 o.disabled = 0
d.enabled = 1 o.enabled = 1
d.default = 0 o.default = 0
if nixio.fs.access("/etc/config/network") and nixio.fs.access("/etc/config/firewall")then if nixio.fs.access("/etc/config/network") and nixio.fs.access("/etc/config/firewall")then
d = s:option(Flag, "op_macvlan", translate("Create macvlan interface"), translate("Auto create macvlan interface in Openwrt")) o = s:option(Flag, "op_macvlan", translate("Create macvlan interface"), translate("Auto create macvlan interface in Openwrt"))
d:depends("dirver", "macvlan") o:depends("dirver", "macvlan")
d.disabled = 0 o.disabled = 0
d.enabled = 1 o.enabled = 1
d.default = 1 o.default = 1
end end
d = s:option(Value, "subnet", translate("Subnet")) o = s:option(Value, "subnet", translate("Subnet"))
d.rmempty = true o.rmempty = true
d.placeholder="10.1.0.0/16" o.placeholder="10.1.0.0/16"
d.datatype="ip4addr" o.datatype="ip4addr"
d = s:option(Value, "gateway", translate("Gateway")) o = s:option(Value, "gateway", translate("Gateway"))
d.rmempty = true o.rmempty = true
d.placeholder="10.1.1.1" o.placeholder="10.1.1.1"
d.datatype="ip4addr" o.datatype="ip4addr"
d = s:option(Value, "ip_range", translate("IP range")) o = s:option(Value, "ip_range", translate("IP range"))
d.rmempty = true o.rmempty = true
d.placeholder="10.1.1.0/24" o.placeholder="10.1.1.0/24"
d.datatype="ip4addr" o.datatype="ip4addr"
d = s:option(DynamicList, "aux_address", translate("Exclude IPs")) o = s:option(DynamicList, "aux_address", translate("Exclude IPs"))
d.rmempty = true o.rmempty = true
d.placeholder="my-route=10.1.1.1" o.placeholder="my-route=10.1.1.1"
d = s:option(Flag, "ipv6", translate("Enable IPv6")) o = s:option(Flag, "ipv6", translate("Enable IPv6"))
d.rmempty = true o.rmempty = true
d.disabled = 0 o.disabled = 0
d.enabled = 1 o.enabled = 1
d.default = 0 o.default = 0
d = s:option(Value, "subnet6", translate("IPv6 Subnet")) o = s:option(Value, "subnet6", translate("IPv6 Subnet"))
d.rmempty = true o.rmempty = true
d.placeholder="fe80::/10" o.placeholder="fe80::/10"
d.datatype="ip6addr" o.datatype="ip6addr"
d:depends("ipv6", 1) o:depends("ipv6", 1)
d = s:option(Value, "gateway6", translate("IPv6 Gateway")) o = s:option(Value, "gateway6", translate("IPv6 Gateway"))
d.rmempty = true o.rmempty = true
d.placeholder="fe80::1" o.placeholder="fe80::1"
d.datatype="ip6addr" o.datatype="ip6addr"
d:depends("ipv6", 1) o:depends("ipv6", 1)
m.handle = function(self, state, data) m.handle = function(self, state, data)
if state == FORM_VALID then if state == FORM_VALID then
@ -146,7 +152,7 @@ m.handle = function(self, state, data)
local ipv6 = data.ipv6 == 1 and true or false local ipv6 = data.ipv6 == 1 and true or false
local create_body={ local create_body = {
Name = name, Name = name,
Driver = driver, Driver = driver,
EnableIPv6 = ipv6, EnableIPv6 = ipv6,
@ -167,6 +173,7 @@ m.handle = function(self, state, data)
} }
} }
end end
if driver == "macvlan" then if driver == "macvlan" then
create_body["Options"] = { create_body["Options"] = {
macvlan_mode = data.macvlan_mode, macvlan_mode = data.macvlan_mode,
@ -200,15 +207,33 @@ m.handle = function(self, state, data)
create_body = docker.clear_empty_tables(create_body) create_body = docker.clear_empty_tables(create_body)
docker:write_status("Network: " .. "create" .. " " .. create_body.Name .. "...") docker:write_status("Network: " .. "create" .. " " .. create_body.Name .. "...")
local res = dk.networks:create({body = create_body})
local res = dk.networks:create({
body = create_body
})
if res and res.code == 201 then if res and res.code == 201 then
docker:write_status("Network: " .. "create macvlan interface...") docker:write_status("Network: " .. "create macvlan interface...")
res = dk.networks:inspect({ name = create_body.Name }) res = dk.networks:inspect({
if driver == "macvlan" and data.op_macvlan ~= 0 and res.code == 200 name = create_body.Name
and res.body and res.body.IPAM and res.body.IPAM.Config and res.body.IPAM.Config[1] })
and res.body.IPAM.Config[1].Gateway and res.body.IPAM.Config[1].Subnet then
docker.create_macvlan_interface(data.name, data.parent, res.body.IPAM.Config[1].Gateway, res.body.IPAM.Config[1].Subnet) if driver == "macvlan" and
data.op_macvlan ~= 0 and
res.code == 200 and
res.body and
res.body.IPAM and
res.body.IPAM.Config and
res.body.IPAM.Config[1] and
res.body.IPAM.Config[1].Gateway and
res.body.IPAM.Config[1].Subnet then
docker.create_macvlan_interface(data.name,
data.parent,
res.body.IPAM.Config[1].Gateway,
res.body.IPAM.Config[1].Subnet)
end end
docker:clear_status() docker:clear_status()
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks")) luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
else else

View file

@ -3,9 +3,9 @@ LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman> Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]-- ]]--
require "luci.util"
local docker = require "luci.model.docker" local docker = require "luci.model.docker"
local uci = require "luci.model.uci"
local m, s, o
function byte_format(byte) function byte_format(byte)
local suff = {"B", "KB", "MB", "GB", "TB"} local suff = {"B", "KB", "MB", "GB", "TB"}
@ -18,11 +18,14 @@ function byte_format(byte)
end end
end end
local map_dockerman = Map("dockerman", translate("Docker"), translate("DockerMan is a Simple Docker manager client for LuCI, If you have any issue please visit:") .. " ".. [[<a href="https://github.com/lisaac/luci-app-dockerman" target="_blank">]] ..translate("Github") .. [[</a>]]) m = Map("dockerd", translate("Docker"),
translate("DockerMan is a Simple Docker manager client for LuCI, If you have any issue please visit:") ..
" " ..
[[<a href="https://github.com/lisaac/luci-app-dockerman" target="_blank">]] ..
translate("Github") ..
[[</a>]])
local docker_info_table = {} local docker_info_table = {}
-- docker_info_table['0OperatingSystem'] = {_key=translate("Operating System"),_value='-'}
-- docker_info_table['1Architecture'] = {_key=translate("Architecture"),_value='-'}
-- docker_info_table['2KernelVersion'] = {_key=translate("Kernel Version"),_value='-'}
docker_info_table['3ServerVersion'] = {_key=translate("Docker Version"),_value='-'} docker_info_table['3ServerVersion'] = {_key=translate("Docker Version"),_value='-'}
docker_info_table['4ApiVersion'] = {_key=translate("Api Version"),_value='-'} docker_info_table['4ApiVersion'] = {_key=translate("Api Version"),_value='-'}
docker_info_table['5NCPU'] = {_key=translate("CPUs"),_value='-'} docker_info_table['5NCPU'] = {_key=translate("CPUs"),_value='-'}
@ -31,29 +34,29 @@ docker_info_table['7DockerRootDir'] = {_key=translate("Docker Root Dir"),_value=
docker_info_table['8IndexServerAddress'] = {_key=translate("Index Server Address"),_value='-'} docker_info_table['8IndexServerAddress'] = {_key=translate("Index Server Address"),_value='-'}
docker_info_table['9RegistryMirrors'] = {_key=translate("Registry Mirrors"),_value='-'} docker_info_table['9RegistryMirrors'] = {_key=translate("Registry Mirrors"),_value='-'}
local s = map_dockerman:section(Table, docker_info_table) s = m:section(Table, docker_info_table)
s:option(DummyValue, "_key", translate("Info")) s:option(DummyValue, "_key", translate("Info"))
s:option(DummyValue, "_value") s:option(DummyValue, "_value")
s = map_dockerman:section(SimpleSection)
s = m:section(SimpleSection)
s.template = "dockerman/overview"
s.containers_running = '-' s.containers_running = '-'
s.images_used = '-' s.images_used = '-'
s.containers_total = '-' s.containers_total = '-'
s.images_total = '-' s.images_total = '-'
s.networks_total = '-' s.networks_total = '-'
s.volumes_total = '-' s.volumes_total = '-'
local containers_list
-- local socket = luci.model.uci.cursor():get("dockerman", "local", "socket_path") if docker.new():_ping().code == 200 then
if (require "luci.model.docker").new():_ping().code == 200 then
local dk = docker.new() local dk = docker.new()
containers_list = dk.containers:list({query = {all=true}}).body local containers_list = dk.containers:list({query = {all=true}}).body
local images_list = dk.images:list().body local images_list = dk.images:list().body
local vol = dk.volumes:list() local vol = dk.volumes:list()
local volumes_list = vol and vol.body and vol.body.Volumes or {} local volumes_list = vol and vol.body and vol.body.Volumes or {}
local networks_list = dk.networks:list().body or {} local networks_list = dk.networks:list().body or {}
local docker_info = dk:info() local docker_info = dk:info()
-- docker_info_table['0OperatingSystem']._value = docker_info.body.OperatingSystem
-- docker_info_table['1Architecture']._value = docker_info.body.Architecture
-- docker_info_table['2KernelVersion']._value = docker_info.body.KernelVersion
docker_info_table['3ServerVersion']._value = docker_info.body.ServerVersion docker_info_table['3ServerVersion']._value = docker_info.body.ServerVersion
docker_info_table['4ApiVersion']._value = docker_info.headers["Api-Version"] docker_info_table['4ApiVersion']._value = docker_info.headers["Api-Version"]
docker_info_table['5NCPU']._value = tostring(docker_info.body.NCPU) docker_info_table['5NCPU']._value = tostring(docker_info.body.NCPU)
@ -63,6 +66,7 @@ if (require "luci.model.docker").new():_ping().code == 200 then
local size = statvfs and (statvfs.bavail * statvfs.bsize) or 0 local size = statvfs and (statvfs.bavail * statvfs.bsize) or 0
docker_info_table['7DockerRootDir']._value = docker_info.body.DockerRootDir .. " (" .. tostring(byte_format(size)) .. " " .. translate("Available") .. ")" docker_info_table['7DockerRootDir']._value = docker_info.body.DockerRootDir .. " (" .. tostring(byte_format(size)) .. " " .. translate("Available") .. ")"
end end
docker_info_table['8IndexServerAddress']._value = docker_info.body.IndexServerAddress docker_info_table['8IndexServerAddress']._value = docker_info.body.IndexServerAddress
for i, v in ipairs(docker_info.body.RegistryConfig.Mirrors) do for i, v in ipairs(docker_info.body.RegistryConfig.Mirrors) do
docker_info_table['9RegistryMirrors']._value = docker_info_table['9RegistryMirrors']._value == "-" and v or (docker_info_table['9RegistryMirrors']._value .. ", " .. v) docker_info_table['9RegistryMirrors']._value = docker_info_table['9RegistryMirrors']._value == "-" and v or (docker_info_table['9RegistryMirrors']._value .. ", " .. v)
@ -77,6 +81,7 @@ if (require "luci.model.docker").new():_ping().code == 200 then
end end
end end
end end
s.containers_running = tostring(docker_info.body.ContainersRunning) s.containers_running = tostring(docker_info.body.ContainersRunning)
s.images_used = tostring(s.images_used) s.images_used = tostring(s.images_used)
s.containers_total = tostring(docker_info.body.Containers) s.containers_total = tostring(docker_info.body.Containers)
@ -84,71 +89,67 @@ if (require "luci.model.docker").new():_ping().code == 200 then
s.networks_total = tostring(#networks_list) s.networks_total = tostring(#networks_list)
s.volumes_total = tostring(#volumes_list) s.volumes_total = tostring(#volumes_list)
end end
s.template = "dockerman/overview"
local section_dockerman = map_dockerman:section(NamedSection, "local", "section", translate("Setting")) s = m:section(NamedSection, "globals", "section", translate("Setting"))
section_dockerman:tab("daemon", translate("Docker Daemon"))
section_dockerman:tab("ac", translate("Access Control"))
section_dockerman:tab("dockerman", translate("DockerMan"))
local socket_path = section_dockerman:taboption("dockerman", Value, "socket_path", translate("Docker Socket Path")) o = s:option(Flag, "remote_endpoint",
socket_path.default = "/var/run/docker.sock" translate("Remote Endpoint"),
socket_path.placeholder = "/var/run/docker.sock" translate("Connect to remote endpoint"))
socket_path.rmempty = false o.rmempty = false
local remote_endpoint = section_dockerman:taboption("dockerman", Flag, "remote_endpoint", translate("Remote Endpoint"), translate("Dockerman connect to remote endpoint")) o = s:option(Value, "socket_path",
remote_endpoint.rmempty = false translate("Docker Socket Path"))
remote_endpoint.enabled = "true" o.default = "unix://var/run/docker.sock"
remote_endpoint.disabled = "false" o.placeholder = "unix://var/run/docker.sock"
o:depends("remote_endpoint", 1)
local remote_host = section_dockerman:taboption("dockerman", Value, "remote_host", translate("Remote Host")) o = s:option(Value, "remote_host",
remote_host.placeholder = "10.1.1.2" translate("Remote Host"))
-- remote_host:depends("remote_endpoint", "true") o.placeholder = "10.1.1.2"
o:depends("remote_endpoint", 1)
local remote_port = section_dockerman:taboption("dockerman", Value, "remote_port", translate("Remote Port")) o = s:option(Value, "remote_port",
remote_port.placeholder = "2375" translate("Remote Port"))
remote_port.default = "2375" o.placeholder = "2375"
-- remote_port:depends("remote_endpoint", "true") o.default = "2375"
o:depends("remote_endpoint", 1)
-- local status_path = section_dockerman:taboption("dockerman", Value, "status_path", translate("Action Status Tempfile Path"), translate("Where you want to save the docker status file"))
-- local debug = section_dockerman:taboption("dockerman", Flag, "debug", translate("Enable Debug"), translate("For debug, It shows all docker API actions of luci-app-dockerman in Debug Tempfile Path"))
-- debug.enabled="true"
-- debug.disabled="false"
-- local debug_path = section_dockerman:taboption("dockerman", Value, "debug_path", translate("Debug Tempfile Path"), translate("Where you want to save the debug tempfile"))
if nixio.fs.access("/usr/bin/dockerd") then if nixio.fs.access("/usr/bin/dockerd") then
local allowed_interface = section_dockerman:taboption("ac", DynamicList, "ac_allowed_interface", translate("Allowed access interfaces"), translate("Which interface(s) can access containers under the bridge network, fill-in Interface Name")) o = s:option(Value, "data_root",
local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {} translate("Docker Root Dir"))
for i, v in ipairs(interfaces) do o.placeholder = "/opt/docker/"
allowed_interface:value(v, v) o:depends("remote_endpoint", 0)
end
local allowed_container = section_dockerman:taboption("ac", DynamicList, "ac_allowed_container", translate("Containers allowed to be accessed"), translate("Which container(s) under bridge network can be accessed, even from interfaces that are not allowed, fill-in Container Id or Name"))
-- allowed_container.placeholder = "container name_or_id"
if containers_list then
for i, v in ipairs(containers_list) do
if v.State == "running" and v.NetworkSettings and v.NetworkSettings.Networks and v.NetworkSettings.Networks.bridge and v.NetworkSettings.Networks.bridge.IPAddress then
allowed_container:value(v.Id:sub(1,12), v.Names[1]:sub(2) .. " | " .. v.NetworkSettings.Networks.bridge.IPAddress)
end
end
end
local dockerd_enable = section_dockerman:taboption("daemon", Flag, "daemon_ea", translate("Enable")) o = s:option(Value, "bip",
dockerd_enable.enabled = "true" translate("Default bridge"),
dockerd_enable.rmempty = true translate("Configure the default bridge network"))
local data_root = section_dockerman:taboption("daemon", Value, "daemon_data_root", translate("Docker Root Dir")) o.placeholder = "172.17.0.1/16"
data_root.placeholder = "/opt/docker/" o.default = "172.17.0.1/16"
local registry_mirrors = section_dockerman:taboption("daemon", DynamicList, "daemon_registry_mirrors", translate("Registry Mirrors")) o.datatype = "ipaddr"
registry_mirrors:value("https://hub-mirror.c.163.com", "https://hub-mirror.c.163.com") o:depends("remote_endpoint", 0)
local log_level = section_dockerman:taboption("daemon", ListValue, "daemon_log_level", translate("Log Level"), translate('Set the logging level')) o = s:option(DynamicList, "registry_mirrors",
log_level:value("debug", "debug") translate("Registry Mirrors"))
log_level:value("info", "info") o:value("https://hub-mirror.c.163.com", "https://hub-mirror.c.163.com")
log_level:value("warn", "warn") o:depends("remote_endpoint", 0)
log_level:value("error", "error")
log_level:value("fatal", "fatal") o = s:option(ListValue, "log_level",
local hosts = section_dockerman:taboption("daemon", DynamicList, "daemon_hosts", translate("Server Host"), translate('Daemon unix socket (unix:///var/run/docker.sock) or TCP Remote Hosts (tcp://0.0.0.0:2375), default: unix:///var/run/docker.sock')) translate("Log Level"),
hosts:value("unix:///var/run/docker.sock", "unix:///var/run/docker.sock") translate('Set the logging level'))
hosts:value("tcp://0.0.0.0:2375", "tcp://0.0.0.0:2375") o:value("debug", "debug")
hosts.rmempty = true o:value("info", "info")
o:value("warn", "warn")
o:value("error", "error")
o:value("fatal", "fatal")
o:depends("remote_endpoint", 0)
o = s:option(DynamicList, "hosts",
translate("Client connection"),
translate('Specifies where the Docker daemon will listen for client connections'))
o:value("unix://var/run/docker.sock", "unix://var/run/docker.sock")
o:value("tcp://0.0.0.0:2375", "tcp://0.0.0.0:2375")
o.rmempty = true
o:depends("remote_endpoint", 0)
end end
return map_dockerman
return m

View file

@ -3,28 +3,26 @@ LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman> Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]-- ]]--
require "luci.util"
local uci = luci.model.uci.cursor()
local docker = require "luci.model.docker" local docker = require "luci.model.docker"
local dk = docker.new() local dk = docker.new()
local containers, volumes local m, s, o
local res = dk.volumes:list()
if res.code <300 then volumes = res.body.Volumes else return end local res, containers, volumes
res = dk.containers:list({query = {all=true}})
if res.code <300 then containers = res.body else return end
function get_volumes() function get_volumes()
local data = {} local data = {}
for i, v in ipairs(volumes) do for i, v in ipairs(volumes) do
-- local index = v.CreatedAt .. v.Name
local index = v.Name local index = v.Name
data[index]={} data[index]={}
data[index]["_selected"] = 0 data[index]["_selected"] = 0
data[index]["_nameraw"] = v.Name data[index]["_nameraw"] = v.Name
data[index]["_name"] = v.Name:sub(1,12) data[index]["_name"] = v.Name:sub(1,12)
for ci,cv in ipairs(containers) do for ci,cv in ipairs(containers) do
if cv.Mounts and type(cv.Mounts) ~= "table" then break end if cv.Mounts and type(cv.Mounts) ~= "table" then
break
end
for vi, vv in ipairs(cv.Mounts) do for vi, vv in ipairs(cv.Mounts) do
if v.Name == vv.Name then if v.Name == vv.Name then
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "").. data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
@ -34,6 +32,7 @@ function get_volumes()
end end
data[index]["_driver"] = v.Driver data[index]["_driver"] = v.Driver
data[index]["_mountpoint"] = nil data[index]["_mountpoint"] = nil
for v1 in v.Mountpoint:gmatch('[^/]+') do for v1 in v.Mountpoint:gmatch('[^/]+') do
if v1 == index then if v1 == index then
data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..." data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..."
@ -43,59 +42,82 @@ function get_volumes()
end end
data[index]["_created"] = v.CreatedAt data[index]["_created"] = v.CreatedAt
end end
return data return data
end end
res = dk.volumes:list()
if res.code <300 then
volumes = res.body.Volumes
else
return
end
res = dk.containers:list({
query = {
all=true
}
})
if res.code <300 then
containers = res.body
else
return
end
local volume_list = get_volumes() local volume_list = get_volumes()
-- m = Map("docker", translate("Docker"))
m = SimpleForm("docker", translate("Docker")) m = SimpleForm("docker", translate("Docker"))
m.submit=false m.submit=false
m.reset=false m.reset=false
s = m:section(Table, volume_list, translate("Volumes"))
volume_table = m:section(Table, volume_list, translate("Volumes")) o = s:option(Flag, "_selected","")
o.disabled = 0
volume_selecter = volume_table:option(Flag, "_selected","") o.enabled = 1
volume_selecter.disabled = 0 o.default = 0
volume_selecter.enabled = 1 o.write = function(self, section, value)
volume_selecter.default = 0
volume_id = volume_table:option(DummyValue, "_name", translate("Name"))
volume_table:option(DummyValue, "_driver", translate("Driver"))
volume_table:option(DummyValue, "_containers", translate("Containers")).rawhtml = true
volume_table:option(DummyValue, "_mountpoint", translate("Mount Point"))
volume_table:option(DummyValue, "_created", translate("Created"))
volume_selecter.write = function(self, section, value)
volume_list[section]._selected = value volume_list[section]._selected = value
end end
docker_status = m:section(SimpleSection) o = s:option(DummyValue, "_name", translate("Name"))
docker_status.template = "dockerman/apply_widget"
docker_status.err=docker:read_status()
docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end
action = m:section(Table,{{}}) o = s:option(DummyValue, "_driver", translate("Driver"))
action.notitle=true
action.rowcolors=false o = s:option(DummyValue, "_containers", translate("Containers"))
action.template="cbi/nullsection" o.rawhtml = true
btnremove = action:option(Button, "remove")
btnremove.inputtitle= translate("Remove") o = s:option(DummyValue, "_mountpoint", translate("Mount Point"))
btnremove.template = "dockerman/cbi/inlinebutton"
btnremove.inputstyle = "remove" o = s:option(DummyValue, "_created", translate("Created"))
btnremove.forcewrite = true
btnremove.write = function(self, section) s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template="cbi/nullsection"
o = s:option(Button, "remove")
o.inputtitle= translate("Remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
local volume_selected = {} local volume_selected = {}
-- 遍历table中sectionid
local volume_table_sids = volume_table:cfgsections() for k in pairs(volume_list) do
for _, volume_table_sid in ipairs(volume_table_sids) do if volume_list[k]._selected == 1 then
-- 得到选中项的名字 volume_selected[#volume_selected+1] = k
if volume_list[volume_table_sid]._selected == 1 then
-- volume_selected[#volume_selected+1] = volume_id:cfgvalue(volume_table_sid)
volume_selected[#volume_selected+1] = volume_table_sid
end end
end end
if next(volume_selected) ~= nil then if next(volume_selected) ~= nil then
local success = true local success = true
docker:clear_status() docker:clear_status()
@ -109,8 +131,12 @@ btnremove.write = function(self, section)
docker:append_status("done\n") docker:append_status("done\n")
end end
end end
if success then docker:clear_status() end
if success then
docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/volumes")) luci.http.redirect(luci.dispatcher.build_url("admin/docker/volumes"))
end end
end end
return m return m

View file

@ -3,39 +3,60 @@ LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman> Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]-- ]]--
require "luci.util"
local docker = require "luci.docker" local docker = require "luci.docker"
local fs = require "nixio.fs"
local uci = (require "luci.model.uci").cursor() local uci = (require "luci.model.uci").cursor()
local _docker = {} local _docker = {}
_docker.options = {}
--pull image and return iamge id --pull image and return iamge id
local update_image = function(self, image_name) local update_image = function(self, image_name)
local json_stringify = luci.jsonc and luci.jsonc.stringify local json_stringify = luci.jsonc and luci.jsonc.stringify
_docker:append_status("Images: " .. "pulling" .. " " .. image_name .. "...\n") _docker:append_status("Images: " .. "pulling" .. " " .. image_name .. "...\n")
local res = self.images:create({query = {fromImage=image_name}}, _docker.pull_image_show_status_cb) local res = self.images:create({query = {fromImage=image_name}}, _docker.pull_image_show_status_cb)
if res and res.code == 200 and (#res.body > 0 and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. image_name)) then if res and res.code == 200 and (#res.body > 0 and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. image_name)) then
_docker:append_status("done\n") _docker:append_status("done\n")
else else
res.body.message = res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message) res.body.message = res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)
end end
new_image_id = self.images:inspect({name = image_name}).body.Id new_image_id = self.images:inspect({name = image_name}).body.Id
return new_image_id, res return new_image_id, res
end end
local table_equal = function(t1, t2) local table_equal = function(t1, t2)
if not t1 then return true end if not t1 then
if not t2 then return false end return true
if #t1 ~= #t2 then return false end
for i, v in ipairs(t1) do
if t1[i] ~= t2[i] then return false end
end end
if not t2 then
return false
end
if #t1 ~= #t2 then
return false
end
for i, v in ipairs(t1) do
if t1[i] ~= t2[i] then
return false
end
end
return true return true
end end
local table_subtract = function(t1, t2) local table_subtract = function(t1, t2)
if not t1 or next(t1) == nil then return nil end if not t1 or next(t1) == nil then
if not t2 or next(t2) == nil then return t1 end return nil
end
if not t2 or next(t2) == nil then
return t1
end
local res = {} local res = {}
for _, v1 in ipairs(t1) do for _, v1 in ipairs(t1) do
local found = false local found = false
@ -49,12 +70,19 @@ local table_subtract = function(t1, t2)
table.insert(res, v1) table.insert(res, v1)
end end
end end
return next(res) == nil and nil or res return next(res) == nil and nil or res
end end
local map_subtract = function(t1, t2) local map_subtract = function(t1, t2)
if not t1 or next(t1) == nil then return nil end if not t1 or next(t1) == nil then
if not t2 or next(t2) == nil then return t1 end return nil
end
if not t2 or next(t2) == nil then
return t1
end
local res = {} local res = {}
for k1, v1 in pairs(t1) do for k1, v1 in pairs(t1) do
local found = false local found = false
@ -64,15 +92,9 @@ local map_subtract = function(t1, t2)
break break
end end
end end
if not found then if not found then
res[k1] = v1 res[k1] = v1
-- if v1 and type(v1) == "table" then
-- if next(v1) == nil then
-- res[k1] = { k = 'v' }
-- else
-- res[k1] = v1
-- end
-- end
end end
end end
@ -81,6 +103,7 @@ end
_docker.clear_empty_tables = function ( t ) _docker.clear_empty_tables = function ( t )
local k, v local k, v
if next(t) == nil then if next(t) == nil then
t = nil t = nil
else else
@ -90,23 +113,39 @@ _docker.clear_empty_tables = function ( t )
end end
end end
end end
return t return t
end end
-- return create_body, extra_network
local get_config = function(container_config, image_config) local get_config = function(container_config, image_config)
local config = container_config.Config local config = container_config.Config
local old_host_config = container_config.HostConfig local old_host_config = container_config.HostConfig
local old_network_setting = container_config.NetworkSettings.Networks or {} local old_network_setting = container_config.NetworkSettings.Networks or {}
if config.WorkingDir == image_config.WorkingDir then config.WorkingDir = "" end
if config.User == image_config.User then config.User = "" end if config.WorkingDir == image_config.WorkingDir then
if table_equal(config.Cmd, image_config.Cmd) then config.Cmd = nil end config.WorkingDir = ""
if table_equal(config.Entrypoint, image_config.Entrypoint) then config.Entrypoint = nil end end
if table_equal(config.ExposedPorts, image_config.ExposedPorts) then config.ExposedPorts = nil end
if config.User == image_config.User then
config.User = ""
end
if table_equal(config.Cmd, image_config.Cmd) then
config.Cmd = nil
end
if table_equal(config.Entrypoint, image_config.Entrypoint) then
config.Entrypoint = nil
end
if table_equal(config.ExposedPorts, image_config.ExposedPorts) then
config.ExposedPorts = nil
end
config.Env = table_subtract(config.Env, image_config.Env) config.Env = table_subtract(config.Env, image_config.Env)
config.Labels = table_subtract(config.Labels, image_config.Labels) config.Labels = table_subtract(config.Labels, image_config.Labels)
config.Volumes = map_subtract(config.Volumes, image_config.Volumes) config.Volumes = map_subtract(config.Volumes, image_config.Volumes)
-- subtract ports exposed in image from container
if old_host_config.PortBindings and next(old_host_config.PortBindings) ~= nil then if old_host_config.PortBindings and next(old_host_config.PortBindings) ~= nil then
config.ExposedPorts = {} config.ExposedPorts = {}
for p, v in pairs(old_host_config.PortBindings) do for p, v in pairs(old_host_config.PortBindings) do
@ -114,10 +153,10 @@ local get_config = function(container_config, image_config)
end end
end end
-- handle network config, we need only one network, extras need to network connect action
local network_setting = {} local network_setting = {}
local multi_network = false local multi_network = false
local extra_network = {} local extra_network = {}
for k, v in pairs(old_network_setting) do for k, v in pairs(old_network_setting) do
if multi_network then if multi_network then
extra_network[k] = v extra_network[k] = v
@ -127,12 +166,8 @@ local get_config = function(container_config, image_config)
multi_network = true multi_network = true
end end
-- handle hostconfig
local host_config = old_host_config local host_config = old_host_config
-- if host_config.PortBindings and next(host_config.PortBindings) == nil then host_config.PortBindings = nil end
-- host_config.LogConfig = nil
host_config.Mounts = {} host_config.Mounts = {}
-- for volumes
for i, v in ipairs(container_config.Mounts) do for i, v in ipairs(container_config.Mounts) do
if v.Type == "volume" then if v.Type == "volume" then
table.insert(host_config.Mounts, { table.insert(host_config.Mounts, {
@ -145,30 +180,37 @@ local get_config = function(container_config, image_config)
end end
end end
-- merge configs
local create_body = config local create_body = config
create_body["HostConfig"] = host_config create_body["HostConfig"] = host_config
create_body["NetworkingConfig"] = {EndpointsConfig = network_setting} create_body["NetworkingConfig"] = {EndpointsConfig = network_setting}
create_body = _docker.clear_empty_tables(create_body) or {} create_body = _docker.clear_empty_tables(create_body) or {}
extra_network = _docker.clear_empty_tables(extra_network) or {} extra_network = _docker.clear_empty_tables(extra_network) or {}
return create_body, extra_network return create_body, extra_network
end end
local upgrade = function(self, request) local upgrade = function(self, request)
_docker:clear_status() _docker:clear_status()
-- get image name, image id, container name, configuration information
local container_info = self.containers:inspect({id = request.id}) local container_info = self.containers:inspect({id = request.id})
if container_info.code > 300 and type(container_info.body) == "table" then if container_info.code > 300 and type(container_info.body) == "table" then
return container_info return container_info
end end
local image_name = container_info.body.Config.Image local image_name = container_info.body.Config.Image
if not image_name:match(".-:.+") then image_name = image_name .. ":latest" end if not image_name:match(".-:.+") then
image_name = image_name .. ":latest"
end
local old_image_id = container_info.body.Image local old_image_id = container_info.body.Image
local container_name = container_info.body.Name:sub(2) local container_name = container_info.body.Name:sub(2)
local image_id, res = update_image(self, image_name) local image_id, res = update_image(self, image_name)
if res and res.code ~= 200 then return res end if res and res.code ~= 200 then
return res
end
if image_id == old_image_id then if image_id == old_image_id then
return {code = 305, body = {message = "Already up to date"}} return {code = 305, body = {message = "Already up to date"}}
end end
@ -189,7 +231,6 @@ local upgrade = function(self, request)
return res return res
end end
-- handle config
local image_config = self.images:inspect({id = old_image_id}).body.Config local image_config = self.images:inspect({id = old_image_id}).body.Config
local create_body, extra_network = get_config(container_info.body, image_config) local create_body, extra_network = get_config(container_info.body, image_config)
@ -197,68 +238,98 @@ local upgrade = function(self, request)
_docker:append_status("Container: Create" .. " " .. container_name .. "...") _docker:append_status("Container: Create" .. " " .. container_name .. "...")
create_body = _docker.clear_empty_tables(create_body) create_body = _docker.clear_empty_tables(create_body)
res = self.containers:create({name = container_name, body = create_body}) res = self.containers:create({name = container_name, body = create_body})
if res and res.code > 300 then return res end if res and res.code > 300 then
return res
end
_docker:append_status("done\n") _docker:append_status("done\n")
-- extra networks need to network connect action -- extra networks need to network connect action
for k, v in pairs(extra_network) do for k, v in pairs(extra_network) do
_docker:append_status("Networks: Connect" .. " " .. container_name .. "...") _docker:append_status("Networks: Connect" .. " " .. container_name .. "...")
res = self.networks:connect({id = k, body = {Container = container_name, EndpointConfig = v}}) res = self.networks:connect({id = k, body = {Container = container_name, EndpointConfig = v}})
if res.code > 300 then return res end if res.code > 300 then
return res
end
_docker:append_status("done\n") _docker:append_status("done\n")
end end
_docker:clear_status() _docker:clear_status()
return res return res
end end
local duplicate_config = function (self, request) local duplicate_config = function (self, request)
local container_info = self.containers:inspect({id = request.id}) local container_info = self.containers:inspect({id = request.id})
if container_info.code > 300 and type(container_info.body) == "table" then return nil end if container_info.code > 300 and type(container_info.body) == "table" then
return nil
end
local old_image_id = container_info.body.Image local old_image_id = container_info.body.Image
local image_config = self.images:inspect({id = old_image_id}).body.Config local image_config = self.images:inspect({id = old_image_id}).body.Config
return get_config(container_info.body, image_config) return get_config(container_info.body, image_config)
end end
_docker.new = function(option) _docker.new = function()
local option = option or {} local host = nil
local remote = uci:get("dockerman", "local", "remote_endpoint") local port = nil
options = { local socket_path = nil
host = (remote == "true") and (option.host or uci:get("dockerman", "local", "remote_host")) or nil, local debug_path = nil
port = (remote == "true") and (option.port or uci:get("dockerman", "local", "remote_port")) or nil,
debug = option.debug or uci:get("dockerman", "local", "debug") == 'true' and true or false, local remote = uci:get_bool("dockerd", "globals", "remote_endpoint")
debug_path = option.debug_path or uci:get("dockerman", "local", "debug_path") if remote then
host = uci:get("dockerd", "globals", "remote_host") or nil
port = uci:get("dockerd", "globals", "remote_port") or nil
else
socket_path = uci:get("dockerd", "globals", "socket_path") or "/var/run/docker.sock"
end
local debug = uci:get_bool("dockerd", "globals", "debug")
if debug then
debug_path = uci:get("dockerd", "globals", "debug_path") or "/tmp/.docker_debug"
end
local status_path = uci:get("dockerd", "globals", "status_path") or "/tmp/.docker_status"
_docker.options = {
host = host,
port = port,
socket_path = socket_path,
debug = debug,
debug_path = debug_path,
status_path = status_path
} }
options.socket_path = (remote ~= "true" or not options.host or not options.port) and (option.socket_path or uci:get("dockerman", "local", "socket_path") or "/var/run/docker.sock") or nil
local _new = docker.new(options) local _new = docker.new(_docker.options)
_new.options.status_path = uci:get("dockerman", "local", "status_path")
_new.containers_upgrade = upgrade _new.containers_upgrade = upgrade
_new.containers_duplicate_config = duplicate_config _new.containers_duplicate_config = duplicate_config
return _new return _new
end end
_docker.options={}
_docker.options.status_path = uci:get("dockerman", "local", "status_path")
_docker.append_status=function(self,val) _docker.append_status=function(self,val)
if not val then return end if not val then
return
end
local file_docker_action_status=io.open(self.options.status_path, "a+") local file_docker_action_status=io.open(self.options.status_path, "a+")
file_docker_action_status:write(val) file_docker_action_status:write(val)
file_docker_action_status:close() file_docker_action_status:close()
end end
_docker.write_status=function(self,val) _docker.write_status=function(self,val)
if not val then return end if not val then
return
end
local file_docker_action_status=io.open(self.options.status_path, "w+") local file_docker_action_status=io.open(self.options.status_path, "w+")
file_docker_action_status:write(val) file_docker_action_status:write(val)
file_docker_action_status:close() file_docker_action_status:close()
end end
_docker.read_status=function(self) _docker.read_status=function(self)
return nixio.fs.readfile(self.options.status_path) return fs.readfile(self.options.status_path)
end end
_docker.clear_status=function(self) _docker.clear_status=function(self)
nixio.fs.remove(self.options.status_path) fs.remove(self.options.status_path)
end end
local status_cb = function(res, source, handler) local status_cb = function(res, source, handler)
@ -291,7 +362,9 @@ _docker.pull_image_show_status_cb = function(res, source)
local buf = _docker:read_status() local buf = _docker:read_status()
local num = 0 local num = 0
local str = '\t' .. (step.id and (step.id .. ": ") or "") .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n" local str = '\t' .. (step.id and (step.id .. ": ") or "") .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n"
if step.id then buf, num = buf:gsub("\t"..step.id .. ": .-\n", str) end if step.id then
buf, num = buf:gsub("\t"..step.id .. ": .-\n", str)
end
if num == 0 then if num == 0 then
buf = buf .. str buf = buf .. str
end end
@ -311,31 +384,32 @@ _docker.import_image_show_status_cb = function(res, source)
local buf = _docker:read_status() local buf = _docker:read_status()
local num = 0 local num = 0
local str = '\t' .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n" local str = '\t' .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n"
if step.status then buf, num = buf:gsub("\t"..step.status .. " .-\n", str) end if step.status then
buf, num = buf:gsub("\t"..step.status .. " .-\n", str)
end
if num == 0 then if num == 0 then
buf = buf .. str buf = buf .. str
end end
_docker:write_status(buf) _docker:write_status(buf)
end end
end end)
)
end end
-- _docker.print_status_cb = function(res, source)
-- return status_cb(res, source, function(step)
-- luci.util.perror(step)
-- end
-- )
-- end
_docker.create_macvlan_interface = function(name, device, gateway, subnet) _docker.create_macvlan_interface = function(name, device, gateway, subnet)
if not nixio.fs.access("/etc/config/network") or not nixio.fs.access("/etc/config/firewall") then return end if not fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then
if uci:get("dockerman", "local", "remote_endpoint") == "true" then return end return
end
if uci:get("dockerd", "globals", "remote_endpoint") == "true" then
return
end
local ip = require "luci.ip" local ip = require "luci.ip"
local if_name = "docker_"..name local if_name = "docker_"..name
local dev_name = "macvlan_"..name local dev_name = "macvlan_"..name
local net_mask = tostring(ip.new(subnet):mask()) local net_mask = tostring(ip.new(subnet):mask())
local lan_interfaces local lan_interfaces
-- add macvlan device -- add macvlan device
uci:delete("network", dev_name) uci:delete("network", dev_name)
uci:set("network", dev_name, "device") uci:set("network", dev_name, "device")
@ -343,6 +417,7 @@ _docker.create_macvlan_interface = function(name, device, gateway, subnet)
uci:set("network", dev_name, "ifname", device) uci:set("network", dev_name, "ifname", device)
uci:set("network", dev_name, "type", "macvlan") uci:set("network", dev_name, "type", "macvlan")
uci:set("network", dev_name, "mode", "bridge") uci:set("network", dev_name, "mode", "bridge")
-- add macvlan interface -- add macvlan interface
uci:delete("network", if_name) uci:delete("network", if_name)
uci:set("network", if_name, "interface") uci:set("network", if_name, "interface")
@ -364,14 +439,22 @@ _docker.create_macvlan_interface = function(name, device, gateway, subnet)
uci:set("firewall", s[".name"], "network", interfaces) uci:set("firewall", s[".name"], "network", interfaces)
end end
end) end)
uci:commit("firewall") uci:commit("firewall")
uci:commit("network") uci:commit("network")
os.execute("ifup " .. if_name) os.execute("ifup " .. if_name)
end end
_docker.remove_macvlan_interface = function(name) _docker.remove_macvlan_interface = function(name)
if not nixio.fs.access("/etc/config/network") or not nixio.fs.access("/etc/config/firewall") then return end if not fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then
if uci:get("dockerman", "local", "remote_endpoint") == "true" then return end return
end
if uci:get("dockerd", "globals", "remote_endpoint") == "true" then
return
end
local if_name = "docker_"..name local if_name = "docker_"..name
local dev_name = "macvlan_"..name local dev_name = "macvlan_"..name
uci:foreach("firewall", "zone", function(s) uci:foreach("firewall", "zone", function(s)
@ -387,10 +470,12 @@ _docker.remove_macvlan_interface = function(name)
uci:set("firewall", s[".name"], "network", interfaces) uci:set("firewall", s[".name"], "network", interfaces)
end end
end) end)
uci:commit("firewall")
uci:delete("network", dev_name) uci:delete("network", dev_name)
uci:delete("network", if_name) uci:delete("network", if_name)
uci:commit("network") uci:commit("network")
uci:commit("firewall")
os.execute("ip link del " .. if_name) os.execute("ip link del " .. if_name)
end end

View file

@ -41,6 +41,7 @@
display: block; display: block;
} }
</style> </style>
<script type="text/javascript">//<![CDATA[ <script type="text/javascript">//<![CDATA[
var xhr = new XHR(), var xhr = new XHR(),
uci_apply_rollback = <%=math.max(luci.config and luci.config.apply and luci.config.apply.rollback or 30, 30)%>, uci_apply_rollback = <%=math.max(luci.config and luci.config.apply and luci.config.apply.rollback or 30, 30)%>,
@ -49,7 +50,7 @@
uci_apply_display = <%=math.max(luci.config and luci.config.apply and luci.config.apply.display or 1.5, 1)%>, uci_apply_display = <%=math.max(luci.config and luci.config.apply and luci.config.apply.display or 1.5, 1)%>,
was_xhr_poll_running = false; was_xhr_poll_running = false;
function docker_status_message(type, content) { function docker_status_message(type, content) {
document.getElementById('docker_apply_overlay') || document.body.insertAdjacentHTML("beforeend",'<div id="docker_apply_overlay"><div class="alert-message"></div></div>') document.getElementById('docker_apply_overlay') || document.body.insertAdjacentHTML("beforeend",'<div id="docker_apply_overlay"><div class="alert-message"></div></div>')
var overlay = document.getElementById('docker_apply_overlay') var overlay = document.getElementById('docker_apply_overlay')
message = overlay.querySelector('.alert-message'); message = overlay.querySelector('.alert-message');
@ -73,19 +74,20 @@ function docker_status_message(type, content) {
} }
else { else {
document.body.classList.remove('apply-overlay-active'); document.body.classList.remove('apply-overlay-active');
if (was_xhr_poll_running) if (was_xhr_poll_running)
XHR.run(); XHR.run();
} }
} }
var loading_msg="Loading.."
function uci_confirm_docker() { var loading_msg="Loading.."
function uci_confirm_docker() {
var tt; var tt;
docker_status_message('notice'); docker_status_message('notice');
var call = function(r, resjson, duration) { var call = function(r, resjson, duration) {
if (r && r.status === 200 ) { if (r && r.status === 200 ) {
var indicator = document.querySelector('.uci_change_indicator'); var indicator = document.querySelector('.uci_change_indicator');
if (indicator) indicator.style.display = 'none'; if (indicator)
indicator.style.display = 'none';
docker_status_message('notice', '<%:Docker actions done.%>'); docker_status_message('notice', '<%:Docker actions done.%>');
document.body.classList.remove('apply-overlay-active'); document.body.classList.remove('apply-overlay-active');
window.clearTimeout(tt); window.clearTimeout(tt);
@ -96,19 +98,21 @@ function uci_confirm_docker() {
var delay =1000 var delay =1000
window.setTimeout(function() { window.setTimeout(function() {
xhr.get('<%=url("admin/docker/confirm")%>', null, call, uci_apply_timeout * 1000); xhr.get('<%=url("admin/docker/confirm")%>', null, call, uci_apply_timeout * 1000);
}, delay); },delay);
}; };
var tick = function() { var tick = function() {
var now = Date.now(); var now = Date.now();
docker_status_message('notice', docker_status_message(
'<img src="<%=resource%>/icons/loading.gif" alt="" style="vertical-align:middle" /> <span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' + 'notice',
loading_msg + '</span>'); '<img src="<%=resource%>/icons/loading.gif" alt="" style="vertical-align:middle" /> <span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' + loading_msg + '</span>'
);
tt = window.setTimeout(tick, 200); tt = window.setTimeout(tick, 200);
ts = now; ts = now;
}; };
tick(); tick();
/* wait a few seconds for the settings to become effective */ /* wait a few seconds for the settings to become effective */
window.setTimeout(call, Math.max(uci_apply_holdoff * 1000 , 1)); window.setTimeout(call, Math.max(uci_apply_holdoff * 1000 , 1));
@ -117,24 +121,27 @@ function uci_confirm_docker() {
// uci_confirm_docker() // uci_confirm_docker()
// }) // })
function fnSubmitForm(el){ function fnSubmitForm(el){
if (el.id != "cbid.table.1._new") { if (el.id != "cbid.table.1._new") {
uci_confirm_docker() uci_confirm_docker()
} }
} }
<% if self.err then -%> <% if self.err then -%>
docker_status_message('warning', '<span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">'+`<%=self.err%>`+'</span>'); docker_status_message('warning', '<span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">'+`<%=self.err%>`+'</span>');
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{ document.getElementById('docker_apply_overlay').addEventListener(
"click",
(e)=>{
docker_status_message() docker_status_message()
}) }
<%- end %> )
<%- end %>
window.onload= function (){ window.onload= function (){
var buttons = document.querySelectorAll('input[type="submit"]'); var buttons = document.querySelectorAll('input[type="submit"]');
[].slice.call(buttons).forEach(function (el) { [].slice.call(buttons).forEach(function (el) {
el.onclick = fnSubmitForm.bind(this, el); el.onclick = fnSubmitForm.bind(this, el);
}); });
} }
//]]></script> //]]></script>

View file

@ -20,7 +20,8 @@
document.getElementById("a-cbi-tab-container_" + item).href= path[0]+"/admin/docker/container/"+container_id+'/'+item document.getElementById("a-cbi-tab-container_" + item).href= path[0]+"/admin/docker/container/"+container_id+'/'+item
if (action === item) { if (action === item) {
document.getElementById("cbi-tab-container_" + item).className="cbi-tab" document.getElementById("cbi-tab-container_" + item).className="cbi-tab"
} else { }
else {
document.getElementById("cbi-tab-container_" + item).className="cbi-tab-disabled" document.getElementById("cbi-tab-container_" + item).className="cbi-tab-disabled"
} }
}) })

View file

@ -1,4 +1,3 @@
<div id="upload-container" class="cbi-value cbi-value-last"> <div id="upload-container" class="cbi-value cbi-value-last">
<label class="cbi-value-title" for="archive"><%:Upload%></label> <label class="cbi-value-title" for="archive"><%:Upload%></label>
<div class="cbi-value-field"> <div class="cbi-value-field">
@ -15,6 +14,7 @@
<input type="button"" class="btn cbi-button cbi-button-action important" id="download" name="download" value="<%:Download%>" /> <input type="button"" class="btn cbi-button cbi-button-action important" id="download" name="download" value="<%:Download%>" />
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
let btnUpload = document.getElementById('upload') let btnUpload = document.getElementById('upload')
btnUpload.onclick = function (e) { btnUpload.onclick = function (e) {
@ -22,9 +22,12 @@
let uploadPath = document.getElementById('path').value let uploadPath = document.getElementById('path').value
if (!uploadArchive.value || !uploadPath) { if (!uploadArchive.value || !uploadPath) {
docker_status_message('warning', "<%:Please input the PATH and select the file !%>") docker_status_message('warning', "<%:Please input the PATH and select the file !%>")
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{ document.getElementById('docker_apply_overlay').addEventListener(
"click",
(e)=>{
docker_status_message() docker_status_message()
}) }
)
return return
} }
let fileName = uploadArchive.files[0].name let fileName = uploadArchive.files[0].name
@ -42,20 +45,27 @@
else { else {
docker_status_message('warning', "<%:Upload Error%>:" + xhr.statusText) docker_status_message('warning', "<%:Upload Error%>:" + xhr.statusText)
} }
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{ document.getElementById('docker_apply_overlay').addEventListener(
"click",
(e)=>{
docker_status_message() docker_status_message()
}) }
)
} }
xhr.send(formData) xhr.send(formData)
} }
let btnDownload = document.getElementById('download') let btnDownload = document.getElementById('download')
btnDownload.onclick = function (e) { btnDownload.onclick = function (e) {
let downloadPath = document.getElementById('path').value let downloadPath = document.getElementById('path').value
if (!downloadPath) { if (!downloadPath) {
docker_status_message('warning', "<%:Please input the PATH !%>") docker_status_message('warning', "<%:Please input the PATH !%>")
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{ document.getElementById('docker_apply_overlay').addEventListener(
"click",
(e)=>{
docker_status_message() docker_status_message()
}) }
)
return return
} }
window.open('<%=luci.dispatcher.build_url("admin/docker/container_get_archive")%>?id=<%=self.container%>&path=' + encodeURIComponent(downloadPath)) window.open('<%=luci.dispatcher.build_url("admin/docker/container_get_archive")%>?id=<%=self.container%>&path=' + encodeURIComponent(downloadPath))

View file

@ -2,6 +2,7 @@
let last_bw_tx let last_bw_tx
let last_bw_rx let last_bw_rx
let interval = 3 let interval = 3
function progressbar(v, m, pc, np, f) { function progressbar(v, m, pc, np, f) {
m = m || 100 m = m || 100

View file

@ -13,7 +13,8 @@
if (valISrc.value == "") { if (valISrc.value == "") {
document.getElementById("file_import").click() document.getElementById("file_import").click()
return return
} else { }
else {
let formData = new FormData() let formData = new FormData()
formData.append('src', valISrc.value) formData.append('src', valISrc.value)
formData.append('tag', valITag.value) formData.append('tag', valITag.value)
@ -26,6 +27,7 @@
xhr.send(formData) xhr.send(formData)
} }
} }
let fileimport = document.getElementById('file_import') let fileimport = document.getElementById('file_import')
fileimport.onchange = function (e) { fileimport.onchange = function (e) {
let fileimport = document.getElementById('file_import') let fileimport = document.getElementById('file_import')
@ -52,37 +54,51 @@
let new_tag = prompt("<%:New tag%>\n<%:Image%>" + "ID: " + image_id + "\n<%:Please input new tag%>:", "") let new_tag = prompt("<%:New tag%>\n<%:Image%>" + "ID: " + image_id + "\n<%:Please input new tag%>:", "")
if (new_tag) { if (new_tag) {
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_tag')%>", (new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_tag')%>",
{ id: image_id, tag: new_tag }, {
id: image_id,
tag: new_tag
},
function (r) { function (r) {
if (r.status == 201) { if (r.status == 201) {
location.reload() location.reload()
} }
else { else {
docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText); docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText);
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{ document.getElementById('docker_apply_overlay').addEventListener(
"click",
(e)=>{
docker_status_message() docker_status_message()
})
} }
}) )
}
}
)
} }
} }
let un_tag = function (tag) { let un_tag = function (tag) {
if (tag.match("<none>")) return if (tag.match("<none>"))
return
if (confirm("<%:Remove tag%>: " + tag + " ?")) { if (confirm("<%:Remove tag%>: " + tag + " ?")) {
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_untag')%>", (new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_untag')%>",
{ tag: tag }, {
tag: tag
},
function (r) { function (r) {
if (r.status == 200) { if (r.status == 200) {
location.reload() location.reload()
} }
else { else {
docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText); docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText);
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{ document.getElementById('docker_apply_overlay').addEventListener(
"click",
(e)=>{
docker_status_message() docker_status_message()
})
} }
}) )
}
}
)
} }
} }
</script> </script>

View file

@ -8,6 +8,7 @@
document.getElementById("file_load").click() document.getElementById("file_load").click()
e.preventDefault() e.preventDefault()
} }
let fileLoad = document.getElementById('file_load') let fileLoad = document.getElementById('file_load')
fileLoad.onchange = function(e){ fileLoad.onchange = function(e){
let fileLoad = document.getElementById('file_load') let fileLoad = document.getElementById('file_load')

View file

@ -49,6 +49,7 @@
display: block; display: block;
} }
</style> </style>
<script type="text/javascript"> <script type="text/javascript">
function close_reslov_dialog() { function close_reslov_dialog() {
document.body.classList.remove('dialog-reslov-active') document.body.classList.remove('dialog-reslov-active')
@ -57,16 +58,21 @@
function reslov_container() { function reslov_container() {
let s = document.getElementById('cmd-line-status') let s = document.getElementById('cmd-line-status')
if (!s) return
if (!s)
return
let cmd_line = document.getElementById("dialog_reslov_text").value; let cmd_line = document.getElementById("dialog_reslov_text").value;
if (cmd_line == null || cmd_line == "") { if (cmd_line == null || cmd_line == "") {
return return
} }
cmd_line = cmd_line.replace(/(^\s*)/g,"") cmd_line = cmd_line.replace(/(^\s*)/g,"")
if (!cmd_line.match(/^docker\s+(run|create)/)) { if (!cmd_line.match(/^docker\s+(run|create)/)) {
s.innerHTML = "<font color='red'><%:Command line Error%></font>" s.innerHTML = "<font color='red'><%:Command line Error%></font>"
return return
} }
let reg_space = /\s+/g let reg_space = /\s+/g
let reg_muti_line= /\\\s*\n/g let reg_muti_line= /\\\s*\n/g
// reg_rem =/(?<!\\)`#.+(?<!\\)`/g // the command has `# ` // reg_rem =/(?<!\\)`#.+(?<!\\)`/g // the command has `# `
@ -90,6 +96,7 @@
} }
</script> </script>
<%+cbi/valueheader%> <%+cbi/valueheader%>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Command line%>" onclick="show_reslov_dialog()" /> <input type="button" class="btn cbi-button cbi-button-apply" value="<%:Command line%>" onclick="show_reslov_dialog()" />
<%+cbi/valuefooter%> <%+cbi/valuefooter%>

View file

@ -1,10 +1,10 @@
<style> <style>
/*! /*!
Pure v1.0.1 Pure v1.0.1
Copyright 2013 Yahoo! Copyright 2013 Yahoo!
Licensed under the BSD License. Licensed under the BSD License.
https://github.com/pure-css/pure/blob/master/LICENSE.md https://github.com/pure-css/pure/blob/master/LICENSE.md
*/ */
.pure-g { .pure-g {
letter-spacing: -.31em; letter-spacing: -.31em;
text-rendering: optimizespeed; text-rendering: optimizespeed;
@ -126,11 +126,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
<div class="block pure-g"> <div class="block pure-g">
<div class="pure-u-2-5"> <div class="pure-u-2-5">
<div class="img-con"> <div class="img-con">
<svg role="img" viewBox="0 0 24 24"> <img src="<%=resource%>/dockerman/containers.svg" />
<title>Docker icon</title>
<path
d="M4.82 17.275c-.684 0-1.304-.56-1.304-1.24s.56-1.243 1.305-1.243c.748 0 1.31.56 1.31 1.242s-.622 1.24-1.305 1.24zm16.012-6.763c-.135-.992-.75-1.8-1.56-2.42l-.315-.25-.254.31c-.494.56-.69 1.553-.63 2.295.06.562.24 1.12.554 1.554-.254.13-.568.25-.81.377-.57.187-1.124.25-1.68.25H.097l-.06.37c-.12 1.182.06 2.42.562 3.54l.244.435v.06c1.5 2.483 4.17 3.6 7.078 3.6 5.594 0 10.182-2.42 12.357-7.633 1.425.062 2.864-.31 3.54-1.676l.18-.31-.3-.187c-.81-.494-1.92-.56-2.85-.31l-.018.002zm-8.008-.992h-2.428v2.42h2.43V9.518l-.002.003zm0-3.043h-2.428v2.42h2.43V6.48l-.002-.003zm0-3.104h-2.428v2.42h2.43v-2.42h-.002zm2.97 6.147H13.38v2.42h2.42V9.518l-.007.003zm-8.998 0H4.383v2.42h2.422V9.518l-.01.003zm3.03 0h-2.4v2.42H9.84V9.518l-.015.003zm-6.03 0H1.4v2.42h2.428V9.518l-.03.003zm6.03-3.043h-2.4v2.42H9.84V6.48l-.015-.003zm-3.045 0H4.387v2.42H6.8V6.48l-.016-.003z" />
</svg>
</div> </div>
</div> </div>
<div class="pure-u-3-5"> <div class="pure-u-3-5">
@ -148,13 +144,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
<div class="block pure-g"> <div class="block pure-g">
<div class="pure-u-2-5"> <div class="pure-u-2-5">
<div class="img-con"> <div class="img-con">
<svg id="icon-hub" viewBox="0 -4 42 50" stroke-width="2" fill-rule="nonzero" width="100%" height="100%"> <img src="<%=resource%>/dockerman/images.svg" />
<path
d="M37.176371,36.2324812 C37.1920117,36.8041095 36.7372743,37.270685 36.1684891,37.270685 L3.74335204,37.2703476 C3.17827583,37.2703476 2.72400056,36.8091818 2.72400056,36.2397767 L2.72400056,19.6131383 C1.4312007,18.4881431 0.662551336,16.8884326 0.662551336,15.1618249 L0.664207893,14.69503 C0.63774183,14.4532127 0.650524255,14.2942438 0.711604827,14.1238231 L5.10793246,1.20935468 C5.24853286,0.797020623 5.63848594,0.511627907 6.06681069,0.511627907 L34.0728364,0.511627907 C34.5091607,0.511627907 34.889927,0.793578201 35.0316653,1.20921034 L39.4428567,14.1234095 C39.4871296,14.273204 39.5020782,14.4249444 39.4884726,14.5493649 L39.4884726,15.1505835 C39.4884726,16.9959517 38.6190601,18.6883031 37.1764746,19.7563084 L37.176371,36.2324812 Z M35.1376208,35.209311 L35.1376208,20.7057152 C34.7023924,20.8097593 34.271333,20.8633641 33.8336069,20.8633641 C32.0046019,20.8633641 30.3013756,19.9547008 29.2437221,18.4771538 C28.1860473,19.954695 26.4828515,20.8633641 24.6538444,20.8633641 C22.824803,20.8633641 21.1216155,19.9547157 20.0639591,18.4771544 C19.0062842,19.9546953 17.3030887,20.8633641 15.4740818,20.8633641 C13.6450404,20.8633641 11.9418529,19.9547157 10.8841965,18.4771544 C9.82652161,19.9546953 8.12332608,20.8633641 6.29431919,20.8633641 C5.76735555,20.8633641 5.24095778,20.7883418 4.73973398,20.644674 L4.73973398,35.209311 L35.1376208,35.209311 Z M30.2720226,15.6557626 C30.5154632,17.4501192 32.0503909,18.8018554 33.845083,18.8018554 C35.7286794,18.8018554 37.285413,17.3395134 37.4474599,15.4751932 L30.2280765,15.4751932 C30.2470638,15.532987 30.2617919,15.5932958 30.2720226,15.6557626 Z M21.0484306,15.4751932 C21.0674179,15.532987 21.0821459,15.5932958 21.0923767,15.6557626 C21.3358173,17.4501192 22.8707449,18.8018554 24.665437,18.8018554 C26.4601001,18.8018554 27.9950169,17.4501481 28.2378191,15.6611556 C28.2451225,15.5981318 28.2590045,15.5358056 28.2787375,15.4751932 L21.0484306,15.4751932 Z M11.9238102,15.6557626 C12.1672508,17.4501192 13.7021785,18.8018554 15.4968705,18.8018554 C17.2915336,18.8018554 18.8264505,17.4501481 19.0692526,15.6611556 C19.0765561,15.5981318 19.0904381,15.5358056 19.110171,15.4751932 L11.8798641,15.4751932 C11.8988514,15.532987 11.9135795,15.5932958 11.9238102,15.6557626 Z M6.31682805,18.8018317 C8.11149114,18.8018317 9.64640798,17.4501244 9.88921012,15.6611319 C9.89651357,15.5981081 9.91039559,15.5357819 9.93012856,15.4751696 L2.70318796,15.4751696 C2.86612006,17.3346852 4.42809696,18.8018317 6.31682805,18.8018317 Z M3.09670082,13.4139924 L37.04257,13.4139924 L33.3489482,2.57204736 L6.80119239,2.57204736 L3.09670082,13.4139924 Z"
id="Fill-1"></path>
<rect id="Rectangle-3" x="14" y="26" width="6" height="10"></rect>
<path d="M20,26 L20,36 L26,36 L26,26 L20,26 Z" id="Rectangle-3"></path>
</svg>
</div> </div>
</div> </div>
<div class="pure-u-3-5"> <div class="pure-u-3-5">
@ -172,21 +162,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
<div class="block pure-g"> <div class="block pure-g">
<div class="pure-u-2-5"> <div class="pure-u-2-5">
<div class="img-con"> <div class="img-con">
<svg version="1.1" x="0px" y="0px" width="100%" height="100%" viewBox="0 0 48.723 48.723" xml:space="preserve"> <img src="<%=resource%>/dockerman/networks.svg" />
<path d="M7.452,24.152h3.435v5.701h0.633c0.001,0,0.001,0,0.002,0h0.636v-5.701h3.51v-1.059h17.124v1.104h3.178v5.656h0.619
c0,0,0,0,0.002,0h0.619v-5.656h3.736v-0.856c0-0.012,0.006-0.021,0.006-0.032c0-0.072,0-0.143,0-0.215h5.721v-1.316h-5.721
c0-0.054,0-0.108,0-0.164c0-0.011-0.006-0.021-0.006-0.032v-0.832h-8.154v1.028h-7.911v-2.652h-0.689c-0.001,0-0.001,0-0.002,0
h-0.678v2.652h-7.846v-1.104H7.452v1.104H1.114v1.316h6.338V24.152z" />
<path
d="M21.484,16.849h5.204v-2.611h7.133V1.555H14.588v12.683h6.896V16.849z M16.537,12.288V3.505h15.335v8.783H16.537z" />
<rect x="18.682" y="16.898" width="10.809" height="0.537" />
<path
d="M0,43.971h6.896v2.611H12.1v-2.611h7.134V31.287H0V43.971z M1.95,33.236h15.334v8.785H1.95V33.236z" />
<rect x="4.095" y="46.631" width="10.808" height="0.537" />
<path
d="M29.491,30.994v12.684h6.895v2.611h5.205v-2.611h7.133V30.994H29.491z M46.774,41.729H31.44v-8.783h15.334V41.729z" />
<rect x="33.584" y="46.338" width="10.809" height="0.537" />
</svg>
</div> </div>
</div> </div>
<div class="pure-u-3-5"> <div class="pure-u-3-5">
@ -204,66 +180,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
<div class="block pure-g"> <div class="block pure-g">
<div class="pure-u-2-5"> <div class="pure-u-2-5">
<div class="img-con"> <div class="img-con">
<svg x="0px" y="0px" viewBox="0 0 55 55" style="enable-background:new 0 0 55 55;" xml:space="preserve"> <img src="<%=resource%>/dockerman/volumes.svg" />
<path
d="M52.354,8.51C51.196,4.22,42.577,0,27.5,0C12.423,0,3.803,4.22,2.646,8.51C2.562,8.657,2.5,8.818,2.5,9v0.5V21v0.5V22v11
v0.5V34v12c0,0.162,0.043,0.315,0.117,0.451C3.798,51.346,14.364,55,27.5,55c13.106,0,23.655-3.639,24.875-8.516
C52.455,46.341,52.5,46.176,52.5,46V34v-0.5V33V22v-0.5V21V9.5V9C52.5,8.818,52.438,8.657,52.354,8.51z M50.421,33.985
c-0.028,0.121-0.067,0.241-0.116,0.363c-0.04,0.099-0.089,0.198-0.143,0.297c-0.067,0.123-0.142,0.246-0.231,0.369
c-0.066,0.093-0.141,0.185-0.219,0.277c-0.111,0.131-0.229,0.262-0.363,0.392c-0.081,0.079-0.17,0.157-0.26,0.236
c-0.164,0.143-0.335,0.285-0.526,0.426c-0.082,0.061-0.17,0.12-0.257,0.18c-0.226,0.156-0.462,0.311-0.721,0.463
c-0.068,0.041-0.141,0.08-0.212,0.12c-0.298,0.168-0.609,0.335-0.945,0.497c-0.043,0.021-0.088,0.041-0.132,0.061
c-0.375,0.177-0.767,0.351-1.186,0.519c-0.012,0.005-0.024,0.009-0.036,0.014c-2.271,0.907-5.176,1.67-8.561,2.17
c-0.017,0.002-0.034,0.004-0.051,0.007c-0.658,0.097-1.333,0.183-2.026,0.259c-0.113,0.012-0.232,0.02-0.346,0.032
c-0.605,0.063-1.217,0.121-1.847,0.167c-0.288,0.021-0.59,0.031-0.883,0.049c-0.474,0.028-0.943,0.059-1.429,0.076
C29.137,40.984,28.327,41,27.5,41s-1.637-0.016-2.432-0.044c-0.486-0.017-0.955-0.049-1.429-0.076
c-0.293-0.017-0.595-0.028-0.883-0.049c-0.63-0.046-1.242-0.104-1.847-0.167c-0.114-0.012-0.233-0.02-0.346-0.032
c-0.693-0.076-1.368-0.163-2.026-0.259c-0.017-0.002-0.034-0.004-0.051-0.007c-3.385-0.5-6.29-1.263-8.561-2.17
c-0.012-0.004-0.024-0.009-0.036-0.014c-0.419-0.168-0.812-0.342-1.186-0.519c-0.043-0.021-0.089-0.041-0.132-0.061
c-0.336-0.162-0.647-0.328-0.945-0.497c-0.07-0.04-0.144-0.079-0.212-0.12c-0.259-0.152-0.495-0.307-0.721-0.463
c-0.086-0.06-0.175-0.119-0.257-0.18c-0.191-0.141-0.362-0.283-0.526-0.426c-0.089-0.078-0.179-0.156-0.26-0.236
c-0.134-0.13-0.252-0.26-0.363-0.392c-0.078-0.092-0.153-0.184-0.219-0.277c-0.088-0.123-0.163-0.246-0.231-0.369
c-0.054-0.099-0.102-0.198-0.143-0.297c-0.049-0.121-0.088-0.242-0.116-0.363C4.541,33.823,4.5,33.661,4.5,33.5
c0-0.113,0.013-0.226,0.031-0.338c0.025-0.151,0.011-0.302-0.031-0.445v-7.424c0.028,0.026,0.063,0.051,0.092,0.077
c0.218,0.192,0.44,0.383,0.69,0.567C9.049,28.786,16.582,31,27.5,31c10.872,0,18.386-2.196,22.169-5.028
c0.302-0.22,0.574-0.447,0.83-0.678l0.001-0.001v7.424c-0.042,0.143-0.056,0.294-0.031,0.445c0.019,0.112,0.031,0.225,0.031,0.338
C50.5,33.661,50.459,33.823,50.421,33.985z M50.5,13.293v7.424c-0.042,0.143-0.056,0.294-0.031,0.445
c0.019,0.112,0.031,0.225,0.031,0.338c0,0.161-0.041,0.323-0.079,0.485c-0.028,0.121-0.067,0.241-0.116,0.363
c-0.04,0.099-0.089,0.198-0.143,0.297c-0.067,0.123-0.142,0.246-0.231,0.369c-0.066,0.093-0.141,0.185-0.219,0.277
c-0.111,0.131-0.229,0.262-0.363,0.392c-0.081,0.079-0.17,0.157-0.26,0.236c-0.164,0.143-0.335,0.285-0.526,0.426
c-0.082,0.061-0.17,0.12-0.257,0.18c-0.226,0.156-0.462,0.311-0.721,0.463c-0.068,0.041-0.141,0.08-0.212,0.12
c-0.298,0.168-0.609,0.335-0.945,0.497c-0.043,0.021-0.088,0.041-0.132,0.061c-0.375,0.177-0.767,0.351-1.186,0.519
c-0.012,0.005-0.024,0.009-0.036,0.014c-2.271,0.907-5.176,1.67-8.561,2.17c-0.017,0.002-0.034,0.004-0.051,0.007
c-0.658,0.097-1.333,0.183-2.026,0.259c-0.113,0.012-0.232,0.02-0.346,0.032c-0.605,0.063-1.217,0.121-1.847,0.167
c-0.288,0.021-0.59,0.031-0.883,0.049c-0.474,0.028-0.943,0.059-1.429,0.076C29.137,28.984,28.327,29,27.5,29
s-1.637-0.016-2.432-0.044c-0.486-0.017-0.955-0.049-1.429-0.076c-0.293-0.017-0.595-0.028-0.883-0.049
c-0.63-0.046-1.242-0.104-1.847-0.167c-0.114-0.012-0.233-0.02-0.346-0.032c-0.693-0.076-1.368-0.163-2.026-0.259
c-0.017-0.002-0.034-0.004-0.051-0.007c-3.385-0.5-6.29-1.263-8.561-2.17c-0.012-0.004-0.024-0.009-0.036-0.014
c-0.419-0.168-0.812-0.342-1.186-0.519c-0.043-0.021-0.089-0.041-0.132-0.061c-0.336-0.162-0.647-0.328-0.945-0.497
c-0.07-0.04-0.144-0.079-0.212-0.12c-0.259-0.152-0.495-0.307-0.721-0.463c-0.086-0.06-0.175-0.119-0.257-0.18
c-0.191-0.141-0.362-0.283-0.526-0.426c-0.089-0.078-0.179-0.156-0.26-0.236c-0.134-0.13-0.252-0.26-0.363-0.392
c-0.078-0.092-0.153-0.184-0.219-0.277c-0.088-0.123-0.163-0.246-0.231-0.369c-0.054-0.099-0.102-0.198-0.143-0.297
c-0.049-0.121-0.088-0.242-0.116-0.363C4.541,21.823,4.5,21.661,4.5,21.5c0-0.113,0.013-0.226,0.031-0.338
c0.025-0.151,0.011-0.302-0.031-0.445v-7.424c0.12,0.109,0.257,0.216,0.387,0.324c0.072,0.06,0.139,0.12,0.215,0.18
c0.3,0.236,0.624,0.469,0.975,0.696c0.073,0.047,0.155,0.093,0.231,0.14c0.294,0.183,0.605,0.362,0.932,0.538
c0.121,0.065,0.242,0.129,0.367,0.193c0.365,0.186,0.748,0.367,1.151,0.542c0.066,0.029,0.126,0.059,0.193,0.087
c0.469,0.199,0.967,0.389,1.485,0.573c0.143,0.051,0.293,0.099,0.44,0.149c0.412,0.139,0.838,0.272,1.279,0.401
c0.159,0.046,0.315,0.094,0.478,0.138c0.585,0.162,1.189,0.316,1.823,0.458c0.087,0.02,0.181,0.036,0.269,0.055
c0.559,0.122,1.139,0.235,1.735,0.341c0.202,0.036,0.407,0.07,0.613,0.104c0.567,0.093,1.151,0.178,1.75,0.256
c0.154,0.02,0.301,0.043,0.457,0.062c0.744,0.09,1.514,0.167,2.305,0.233c0.195,0.016,0.398,0.028,0.596,0.042
c0.633,0.046,1.28,0.084,1.942,0.114c0.241,0.011,0.481,0.022,0.727,0.031C25.712,18.979,26.59,19,27.5,19s1.788-0.021,2.65-0.05
c0.245-0.009,0.485-0.02,0.727-0.031c0.662-0.03,1.309-0.068,1.942-0.114c0.198-0.015,0.4-0.026,0.596-0.042
c0.791-0.065,1.561-0.143,2.305-0.233c0.156-0.019,0.303-0.042,0.457-0.062c0.599-0.078,1.182-0.163,1.75-0.256
c0.206-0.034,0.411-0.068,0.613-0.104c0.596-0.106,1.176-0.219,1.735-0.341c0.088-0.019,0.182-0.036,0.269-0.055
c0.634-0.142,1.238-0.297,1.823-0.458c0.163-0.045,0.319-0.092,0.478-0.138c0.441-0.129,0.867-0.262,1.279-0.401
c0.147-0.05,0.297-0.098,0.44-0.149c0.518-0.184,1.017-0.374,1.485-0.573c0.067-0.028,0.127-0.058,0.193-0.087
c0.403-0.176,0.786-0.356,1.151-0.542c0.125-0.064,0.247-0.128,0.367-0.193c0.327-0.175,0.638-0.354,0.932-0.538
c0.076-0.047,0.158-0.093,0.231-0.14c0.351-0.227,0.675-0.459,0.975-0.696c0.075-0.06,0.142-0.12,0.215-0.18
C50.243,13.509,50.38,13.402,50.5,13.293z M27.5,2c13.555,0,23,3.952,23,7.5s-9.445,7.5-23,7.5s-23-3.952-23-7.5S13.945,2,27.5,2z
M50.5,45.703c-0.014,0.044-0.024,0.089-0.032,0.135C49.901,49.297,40.536,53,27.5,53S5.099,49.297,4.532,45.838
c-0.008-0.045-0.019-0.089-0.032-0.131v-8.414c0.028,0.026,0.063,0.051,0.092,0.077c0.218,0.192,0.44,0.383,0.69,0.567
C9.049,40.786,16.582,43,27.5,43c10.872,0,18.386-2.196,22.169-5.028c0.302-0.22,0.574-0.447,0.83-0.678l0.001-0.001V45.703z" />
</svg>
</div> </div>
</div> </div>
<div class="pure-u-3-5"> <div class="pure-u-3-5">

View file

@ -1,10 +0,0 @@
config section 'local'
option socket_path '/var/run/docker.sock'
option status_path '/tmp/.docker_action_status'
option debug 'false'
option debug_path '/tmp/.docker_debug'
option remote_endpoint 'false'
option daemon_ea 'true'
option daemon_data_root '/opt/docker'
option daemon_log_level 'warn'
list ac_allowed_interface 'br-lan'

View file

@ -1,47 +0,0 @@
#!/bin/sh /etc/rc.common
START=99
DOCKERD_CONF="/etc/docker/daemon.json"
init_dockerman_chain(){
iptables -N DOCKER-MAN >/dev/null 2>&1
iptables -F DOCKER-MAN >/dev/null 2>&1
iptables -D DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1
iptables -I DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1
}
add_allowed_interface(){
iptables -A DOCKER-MAN -i $1 -o docker0 -j RETURN
}
add_allowed_ip(){
iptables -A DOCKER-MAN -d $1 -o docker0 -j RETURN
}
handle_allowed_interface(){
#config_list_foreach "local" allowed_ip add_allowed_ip
config_list_foreach "local" ac_allowed_interface add_allowed_interface
iptables -A DOCKER-MAN -m conntrack --ctstate ESTABLISHED,RELATED -o docker0 -j RETURN >/dev/null 2>&1
iptables -A DOCKER-MAN -m conntrack --ctstate NEW,INVALID -o docker0 -j DROP >/dev/null 2>&1
iptables -A DOCKER-MAN -j RETURN >/dev/null 2>&1
}
start(){
[ ! -x "/etc/init.d/dockerd" ] && return 0
config_load dockerman
config_get daemon_ea "local" daemon_ea
init_dockerman_chain
if [ -n "$daemon_ea" ]; then
handle_allowed_interface
lua /usr/share/dockerman/dockerd-config.lua "$DOCKERD_CONF" && /etc/init.d/dockerd restart && sleep 5 || {
# 1 running, 0 stopped
STATE=$([ -n "$(ps |grep /usr/bin/dockerd | grep -v grep)" ] && echo 1 || echo 0)
[ "$STATE" == "0" ] && /etc/init.d/dockerd start && sleep 5
}
lua /usr/share/dockerman/dockerd-ac.lua
else
/etc/init.d/dockerd stop
fi
}

View file

@ -1,15 +0,0 @@
#!/bin/sh
uci -q batch <<-EOF >/dev/null
set uhttpd.main.script_timeout="360"
commit uhttpd
delete ucitrack.@dockerman[-1]
add ucitrack dockerman
set ucitrack.@dockerman[-1].exec='/etc/init.d/dockerman start'
commit ucitrack
EOF
[ -x "$(which dockerd)" ] && chmod +x /etc/init.d/dockerman && /etc/init.d/dockerd disable && /etc/init.d/dockerman enable >/dev/null 2>&1
sed -i 's/self:cfgvalue(section) or {}/self:cfgvalue(section) or self.default or {}/' /usr/lib/lua/luci/view/cbi/dynlist.htm
/etc/init.d/uhttpd restart >/dev/null 2>&1
rm -fr /tmp/luci-indexcache /tmp/luci-modulecache >/dev/null 2>&1
exit 0

View file

@ -1,20 +0,0 @@
require "luci.util"
docker = require "luci.docker"
uci = (require "luci.model.uci").cursor()
dk = docker.new({socket_path = "/var/run/docker.sock"})
if dk:_ping().code ~= 200 then return end
containers_list = dk.containers:list({query = {all=true}}).body
allowed_container = uci:get("dockerman", "local", "ac_allowed_container")
if not allowed_container or next(allowed_container)==nil then return end
allowed_ip = {}
for i, v in ipairs(containers_list) do
for ii, vv in ipairs(allowed_container) do
if v.Id:sub(1,12) == vv and v.NetworkSettings and v.NetworkSettings.Networks and v.NetworkSettings.Networks.bridge and v.NetworkSettings.Networks.bridge.IPAddress then
print(v.NetworkSettings.Networks.bridge.IPAddress)
luci.util.exec("iptables -I DOCKER-MAN -d "..v.NetworkSettings.Networks.bridge.IPAddress.." -o docker0 -j RETURN")
table.remove(allowed_container, ii)
end
end
end

View file

@ -1,52 +0,0 @@
require "luci.util"
fs = require "nixio.fs"
uci = (require "luci.model.uci").cursor()
raw_file_dir = arg[1]
raw_json_str = fs.readfile(raw_file_dir) or "[]"
raw_json = luci.jsonc.parse(raw_json_str) or {}
new_json = {}
new_json["data-root"] = uci:get("dockerman", "local", "daemon_data_root")
new_json["hosts"] = uci:get("dockerman", "local", "daemon_hosts") or {}
new_json["registry-mirrors"] = uci:get("dockerman", "local", "daemon_registry_mirrors") or {}
new_json["log-level"] = uci:get("dockerman", "local", "daemon_log_level")
function comp(raw, new)
for k, v in pairs(new) do
if type(v) == "table" and raw[k] then
if #v == #raw[k] then
comp(raw[k], v)
else
changed = true
raw[k] = v
end
elseif raw[k] ~= v then
changed = true
raw[k] = v
end
end
for k, v in ipairs(new) do
if type(v) == "table" and raw[k] then
if #v == #raw[k] then
comp(raw[k], v)
else
changed = true
raw[k] = v
end
elseif raw[k] ~= v then
changed = true
raw[k] = v
end
end
end
comp(raw_json, new_json)
if changed then
if next(raw_json["registry-mirrors"]) == nil then raw_json["registry-mirrors"] = nil end
if next(raw_json["hosts"]) == nil then raw_json["hosts"] = nil end
fs.writefile(raw_file_dir, luci.jsonc.stringify(raw_json, true):gsub("\\", ""))
os.exit(0)
else
os.exit(1)
end

View file

@ -2,10 +2,10 @@
"luci-app-dockerman": { "luci-app-dockerman": {
"description": "Grant UCI access for luci-app-dockerman", "description": "Grant UCI access for luci-app-dockerman",
"read": { "read": {
"uci": [ "dockerman" ] "uci": [ "dockerd" ]
}, },
"write": { "write": {
"uci": [ "dockerman" ] "uci": [ "dockerd" ]
} }
} }
} }

View file

@ -2,6 +2,7 @@
LuCI - Lua Configuration Interface LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-lib-docker> Copyright 2019 lisaac <https://github.com/lisaac/luci-lib-docker>
]]-- ]]--
require "nixio.util" require "nixio.util"
require "luci.util" require "luci.util"
local jsonc = require "luci.jsonc" local jsonc = require "luci.jsonc"
@ -15,44 +16,55 @@ local json_parse = jsonc.parse
local chunksource = function(sock, buffer) local chunksource = function(sock, buffer)
buffer = buffer or "" buffer = buffer or ""
return function() return function()
local output local output
local _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n") local _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n")
if not count then if not count then
local newblock, code = sock:recv(1024) local newblock, code = sock:recv(1024)
if not newblock then return nil, code end if not newblock then
return nil, code
end
buffer = buffer .. newblock buffer = buffer .. newblock
_, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n") _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n")
end end
count = tonumber(count, 16) count = tonumber(count, 16)
if not count then if not count then
return nil, -1, "invalid encoding" return nil, -1, "invalid encoding"
elseif count == 0 then -- finial elseif count == 0 then -- finial
return nil return nil
elseif count <= #buffer - endp then elseif count <= #buffer - endp then -- data >= count
--data >= count
output = buffer:sub(endp + 1, endp + count) output = buffer:sub(endp + 1, endp + count)
if count == #buffer - endp then -- [data] if count == #buffer - endp then -- [data]
buffer = buffer:sub(endp + count + 1) buffer = buffer:sub(endp + count + 1)
count, code = sock:recvall(2) --read \r\n count, code = sock:recvall(2) --read \r\n
if not count then return nil, code end if not count then
return nil, code
end
elseif count + 1 == #buffer - endp then -- [data]\r elseif count + 1 == #buffer - endp then -- [data]\r
buffer = buffer:sub(endp + count + 2) buffer = buffer:sub(endp + count + 2)
count, code = sock:recvall(1) --read \n count, code = sock:recvall(1) --read \n
if not count then return nil, code end if not count then
return nil, code
end
else -- [data]\r\n[count]\r\n[data]... else -- [data]\r\n[count]\r\n[data]...
buffer = buffer:sub(endp + count + 3) -- cut buffer buffer = buffer:sub(endp + count + 3) -- cut buffer
end end
return output return output
else else -- data < count
-- data < count
output = buffer:sub(endp + 1, endp + count) output = buffer:sub(endp + 1, endp + count)
buffer = buffer:sub(endp + count + 1) buffer = buffer:sub(endp + count + 1)
local remain, code = sock:recvall(count - #output) --need read remaining local remain, code = sock:recvall(count - #output) --need read remaining
if not remain then return nil, code end if not remain then
return nil, code
end
output = output .. remain output = output .. remain
count, code = sock:recvall(2) --read \r\n count, code = sock:recvall(2) --read \r\n
if not count then return nil, code end if not count then
return nil, code
end
return output return output
end end
end end
@ -74,21 +86,23 @@ local docker_stream_filter = function(buffer)
return "" return ""
end end
local stream_type = ((string.byte(buffer, 1) == 1) and "stdout") or ((string.byte(buffer, 1) == 2) and "stderr") or ((string.byte(buffer, 1) == 0) and "stdin") or "stream_err" local stream_type = ((string.byte(buffer, 1) == 1) and "stdout") or ((string.byte(buffer, 1) == 2) and "stderr") or ((string.byte(buffer, 1) == 0) and "stdin") or "stream_err"
local valid_length = local valid_length = tonumber(string.byte(buffer, 5)) * 256 * 256 * 256 + tonumber(string.byte(buffer, 6)) * 256 * 256 + tonumber(string.byte(buffer, 7)) * 256 + tonumber(string.byte(buffer, 8))
tonumber(string.byte(buffer, 5)) * 256 * 256 * 256 + tonumber(string.byte(buffer, 6)) * 256 * 256 + tonumber(string.byte(buffer, 7)) * 256 + tonumber(string.byte(buffer, 8))
if valid_length > #buffer + 8 then if valid_length > #buffer + 8 then
return "" return ""
end end
return stream_type .. ": " .. string.sub(buffer, 9, valid_length + 8) return stream_type .. ": " .. string.sub(buffer, 9, valid_length + 8)
-- return string.sub(buffer, 9, valid_length + 8)
end end
local open_socket = function(req_options) local open_socket = function(req_options)
local socket local socket
if type(req_options) ~= "table" then return socket end if type(req_options) ~= "table" then
return socket
end
if req_options.socket_path then if req_options.socket_path then
socket = nixio.socket("unix", "stream") socket = nixio.socket("unix", "stream")
if socket:connect(req_options.socket_path) ~= true then return nil end if socket:connect(req_options.socket_path) ~= true then
return nil
end
elseif req_options.host and req_options.port then elseif req_options.host and req_options.port then
socket = nixio.connect(req_options.host, req_options.port) socket = nixio.connect(req_options.host, req_options.port)
end end
@ -99,11 +113,17 @@ local open_socket = function(req_options)
end end
end end
local send_http_socket = function(docker_socket, req_header, req_body, callback) local send_http_socket = function(options, docker_socket, req_header, req_body, callback)
if docker_socket:send(req_header) == 0 then if docker_socket:send(req_header) == 0 then
return { return {
headers={code=498,message="bad path", protocol="HTTP/1.1"}, headers={
body={message="can\'t send data to socket"} code=498,
message="bad path",
protocol="HTTP/1.1"
},
body={
message="can\'t send data to socket"
}
} }
end end
@ -116,36 +136,55 @@ local send_http_socket = function(docker_socket, req_header, req_body, callback)
elseif req_body and type(req_body) == "table" then elseif req_body and type(req_body) == "table" then
-- json -- json
docker_socket:send(json_stringify(req_body)) docker_socket:send(json_stringify(req_body))
if options.debug then io.popen("echo '".. json_stringify(req_body) .. "' >> " .. options.debug_path) end if options.debug then
io.popen("echo '".. json_stringify(req_body) .. "' >> " .. options.debug_path)
end
elseif req_body then elseif req_body then
docker_socket:send(req_body) docker_socket:send(req_body)
if options.debug then io.popen("echo '".. req_body .. "' >> " .. options.debug_path) end if options.debug then
io.popen("echo '".. req_body .. "' >> " .. options.debug_path)
end
end end
local linesrc = docker_socket:linesource() local linesrc = docker_socket:linesource()
-- read socket using source http://w3.impa.br/~diego/software/luasocket/ltn12.html -- read socket using source http://w3.impa.br/~diego/software/luasocket/ltn12.html
--http://lua-users.org/wiki/FiltersSourcesAndSinks -- http://lua-users.org/wiki/FiltersSourcesAndSinks
-- handle response header -- handle response header
local line = linesrc() local line = linesrc()
if not line then if not line then
docker_socket:close() docker_socket:close()
return { return {
headers = {code=499, message="bad socket path", protocol="HTTP/1.1"}, headers = {
body = {message="no data receive from socket"} code=499,
message="bad socket path",
protocol="HTTP/1.1"
},
body = {
message="no data receive from socket"
}
} }
end end
local response = {code = 0, headers = {}, body = {}}
local response = {
code = 0,
headers = {},
body = {}
}
local p, code, msg = line:match("^([%w./]+) ([0-9]+) (.*)") local p, code, msg = line:match("^([%w./]+) ([0-9]+) (.*)")
response.protocol = p response.protocol = p
response.code = tonumber(code) response.code = tonumber(code)
response.message = msg response.message = msg
line = linesrc() line = linesrc()
while line and line ~= "" do while line and line ~= "" do
local key, val = line:match("^([%w-]+)%s?:%s?(.*)") local key, val = line:match("^([%w-]+)%s?:%s?(.*)")
if key and key ~= "Status" then if key and key ~= "Status" then
if type(response.headers[key]) == "string" then if type(response.headers[key]) == "string" then
response.headers[key] = {response.headers[key], val} response.headers[key] = {
response.headers[key],
val
}
elseif type(response.headers[key]) == "table" then elseif type(response.headers[key]) == "table" then
response.headers[key][#response.headers[key] + 1] = val response.headers[key][#response.headers[key] + 1] = val
else else
@ -154,9 +193,11 @@ local send_http_socket = function(docker_socket, req_header, req_body, callback)
end end
line = linesrc() line = linesrc()
end end
-- handle response body -- handle response body
local body_buffer = linesrc(true) local body_buffer = linesrc(true)
response.body = {} response.body = {}
if type(callback) ~= "function" then if type(callback) ~= "function" then
if response.headers["Transfer-Encoding"] == "chunked" then if response.headers["Transfer-Encoding"] == "chunked" then
local source = chunksource(docker_socket, body_buffer) local source = chunksource(docker_socket, body_buffer)
@ -196,6 +237,7 @@ local gen_header = function(options, http_method, api_group, api_action, name_or
end end
end end
end end
path = (api_group and ("/" .. api_group) or "") .. (name_or_id and ("/" .. name_or_id) or "") .. (api_action and ("/" .. api_action) or "") .. (query or "") path = (api_group and ("/" .. api_group) or "") .. (name_or_id and ("/" .. name_or_id) or "") .. (api_action and ("/" .. api_action) or "") .. (query or "")
header = (http_method or "GET") .. " " .. path .. " " .. options.protocol .. "\r\n" header = (http_method or "GET") .. " " .. path .. " " .. options.protocol .. "\r\n"
header = header .. "Host: " .. options.host .. "\r\n" header = header .. "Host: " .. options.host .. "\r\n"
@ -221,32 +263,51 @@ local gen_header = function(options, http_method, api_group, api_action, name_or
elseif request and request.body and type(request.body) == "string" then elseif request and request.body and type(request.body) == "string" then
header = header .. "Content-Length: " .. #request.body .. "\r\n" header = header .. "Content-Length: " .. #request.body .. "\r\n"
end end
header = header .. "\r\n" header = header .. "\r\n"
if options.debug then io.popen("echo '".. header .. "' >> " .. options.debug_path) end if options.debug then
io.popen("echo '".. header .. "' >> " .. options.debug_path)
end
return header return header
end end
local call_docker = function(options, http_method, api_group, api_action, name_or_id, request, callback) local call_docker = function(options, http_method, api_group, api_action, name_or_id, request, callback)
local req_options = setmetatable({}, {__index = options}) local req_options = setmetatable({}, {
__index = options
})
local req_header = gen_header(req_options,
http_method,
api_group,
api_action,
name_or_id,
request)
local req_header = gen_header(req_options, http_method, api_group, api_action, name_or_id, request)
local req_body = request and request.body or nil local req_body = request and request.body or nil
local docker_socket = open_socket(req_options) local docker_socket = open_socket(req_options)
if docker_socket then if docker_socket then
return send_http_socket(docker_socket, req_header, req_body, callback) return send_http_socket(options, docker_socket, req_header, req_body, callback)
else else
return { return {
headers = {code=497, message="bad socket path or host", protocol="HTTP/1.1"}, headers = {
body = {message="can\'t connect to socket"} code=497,
message="bad socket path or host",
protocol="HTTP/1.1"
},
body = {
message="can\'t connect to socket"
}
} }
end end
end end
local gen_api = function(_table, http_method, api_group, api_action) local gen_api = function(_table, http_method, api_group, api_action)
local _api_action local _api_action
if api_action == "get_archive" or api_action == "put_archive" then if api_action == "get_archive" or api_action == "put_archive" then
_api_action = "archive" api_action = "archive"
elseif api_action == "df" then elseif api_action == "df" then
_api_action = "system/df" _api_action = "system/df"
elseif api_action ~= "list" and api_action ~= "inspect" and api_action ~= "remove" then elseif api_action ~= "list" and api_action ~= "inspect" and api_action ~= "remove" then
@ -257,6 +318,7 @@ local gen_api = function(_table, http_method, api_group, api_action)
local fp = function(self, request, callback) local fp = function(self, request, callback)
local name_or_id = request and (request.name or request.id or request.name_or_id) or nil local name_or_id = request and (request.name or request.id or request.name_or_id) or nil
if api_action == "list" then if api_action == "list" then
if (name_or_id ~= "" and name_or_id ~= nil) then if (name_or_id ~= "" and name_or_id ~= nil) then
if api_group == "images" then if api_group == "images" then
@ -277,7 +339,13 @@ local gen_api = function(_table, http_method, api_group, api_action)
end end
elseif api_action == "logs" then elseif api_action == "logs" then
local body_buffer = "" local body_buffer = ""
local response = call_docker(self.options, http_method, api_group, _api_action, name_or_id, request, callback) local response = call_docker(self.options,
http_method,
api_group,
_api_action,
name_or_id,
request,
callback)
if response.code >= 200 and response.code < 300 then if response.code >= 200 and response.code < 300 then
for i, v in ipairs(response.body) do for i, v in ipairs(response.body) do
body_buffer = body_buffer .. docker_stream_filter(response.body[i]) body_buffer = body_buffer .. docker_stream_filter(response.body[i])
@ -286,7 +354,9 @@ local gen_api = function(_table, http_method, api_group, api_action)
end end
return response return response
end end
local response = call_docker(self.options, http_method, api_group, _api_action, name_or_id, request, callback) local response = call_docker(self.options, http_method, api_group, _api_action, name_or_id, request, callback)
if response.headers and response.headers["Content-Type"] == "application/json" then if response.headers and response.headers["Content-Type"] == "application/json" then
if #response.body == 1 then if #response.body == 1 then
response.body = json_parse(response.body[1]) response.body = json_parse(response.body[1])
@ -308,7 +378,13 @@ local gen_api = function(_table, http_method, api_group, api_action)
end end
end end
local _docker = {containers = {}, exec = {}, images = {}, networks = {}, volumes = {}} local _docker = {
containers = {},
exec = {},
images = {},
networks = {},
volumes = {}
}
gen_api(_docker, "GET", "containers", "list") gen_api(_docker, "GET", "containers", "list")
gen_api(_docker, "POST", "containers", "create") gen_api(_docker, "POST", "containers", "create")
@ -370,6 +446,7 @@ gen_api(_docker, "GET", nil, "df")
function _docker.new(options) function _docker.new(options)
local docker = {} local docker = {}
local _options = options or {} local _options = options or {}
docker.options = { docker.options = {
socket_path = _options.socket_path or nil, socket_path = _options.socket_path or nil,
host = _options.socket_path and "localhost" or _options.host, host = _options.socket_path and "localhost" or _options.host,
@ -384,6 +461,7 @@ function _docker.new(options)
debug = _options.debug or false, debug = _options.debug or false,
debug_path = _options.debug and _options.debug_path or nil debug_path = _options.debug and _options.debug_path or nil
} }
setmetatable( setmetatable(
docker, docker,
{ {
@ -396,6 +474,7 @@ function _docker.new(options)
end end
} }
) )
setmetatable( setmetatable(
docker.containers, docker.containers,
{ {
@ -406,6 +485,7 @@ function _docker.new(options)
end end
} }
) )
setmetatable( setmetatable(
docker.networks, docker.networks,
{ {
@ -416,6 +496,7 @@ function _docker.new(options)
end end
} }
) )
setmetatable( setmetatable(
docker.images, docker.images,
{ {
@ -426,6 +507,7 @@ function _docker.new(options)
end end
} }
) )
setmetatable( setmetatable(
docker.volumes, docker.volumes,
{ {
@ -436,6 +518,7 @@ function _docker.new(options)
end end
} }
) )
setmetatable( setmetatable(
docker.exec, docker.exec,
{ {
@ -446,6 +529,7 @@ function _docker.new(options)
end end
} }
) )
return docker return docker
end end