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,386 +2,444 @@
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)
e.dependent = false
e.acl_depends = { "luci-app-dockerman" }
local e = entry({"admin", "docker"}, firstchild(), "Docker", 40) entry({"admin", "docker", "overview"},cbi("dockerman/overview"),_("Overview"),0).leaf=true
e.dependent = false
e.acl_depends = { "luci-app-dockerman" }
entry({"admin","docker","overview"},cbi("dockerman/overview"),_("Overview"),0).leaf=true local remote = luci.model.uci.cursor():get_bool("dockerd", "globals", "remote_endpoint")
if remote then
local host = luci.model.uci.cursor():get("dockerd", "globals", "remote_host")
local port = luci.model.uci.cursor():get("dockerd", "globals", "remote_port")
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
local remote = luci.model.uci.cursor():get("dockerman", "local", "remote_endpoint") if (require "luci.model.docker").new():_ping().code ~= 200 then
if remote == nil then return
local socket = luci.model.uci.cursor():get("dockerman", "local", "socket_path") end
if socket and not nixio.fs.access(socket) then return end
elseif remote == "true" then
local host = luci.model.uci.cursor():get("dockerman", "local", "remote_host")
local port = luci.model.uci.cursor():get("dockerman", "local", "remote_port")
if not host or not port then return end
end
if (require "luci.model.docker").new():_ping().code ~= 200 then return end entry({"admin", "docker", "containers"}, form("dockerman/containers"), _("Containers"),1).leaf=true
entry({"admin","docker","containers"},form("dockerman/containers"),_("Containers"),1).leaf=true entry({"admin", "docker", "images"}, form("dockerman/images"), _("Images"),2).leaf=true
entry({"admin","docker","images"},form("dockerman/images"),_("Images"),2).leaf=true entry({"admin", "docker", "networks"}, form("dockerman/networks"), _("Networks"),3).leaf=true
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","volumes"},form("dockerman/volumes"),_("Volumes"),4).leaf=true entry({"admin", "docker", "events"}, call("action_events"), _("Events"),5)
entry({"admin","docker","events"},call("action_events"),_("Events"),5)
entry({"admin","docker","newcontainer"},form("dockerman/newcontainer")).leaf=true entry({"admin", "docker", "newcontainer"}, form("dockerman/newcontainer")).leaf=true
entry({"admin","docker","newnetwork"},form("dockerman/newnetwork")).leaf=true entry({"admin", "docker", "newnetwork"}, form("dockerman/newnetwork")).leaf=true
entry({"admin","docker","container"},form("dockerman/container")).leaf=true entry({"admin", "docker", "container"}, form("dockerman/container")).leaf=true
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", "container_stats"}, call("action_get_container_stats")).leaf=true
entry({"admin","docker","container_put_archive"},call("upload_archive")).leaf=true entry({"admin", "docker", "container_get_archive"}, call("download_archive")).leaf=true
entry({"admin","docker","images_save"},call("save_images")).leaf=true entry({"admin", "docker", "container_put_archive"}, call("upload_archive")).leaf=true
entry({"admin","docker","images_load"},call("load_images")).leaf=true entry({"admin", "docker", "images_save"}, call("save_images")).leaf=true
entry({"admin","docker","images_import"},call("import_images")).leaf=true entry({"admin", "docker", "images_load"}, call("load_images")).leaf=true
entry({"admin","docker","images_get_tags"},call("get_image_tags")).leaf=true entry({"admin", "docker", "images_import"}, call("import_images")).leaf=true
entry({"admin","docker","images_tag"},call("tag_image")).leaf=true entry({"admin", "docker", "images_get_tags"}, call("get_image_tags")).leaf=true
entry({"admin","docker","images_untag"},call("untag_image")).leaf=true entry({"admin", "docker", "images_tag"}, call("tag_image")).leaf=true
entry({"admin","docker","confirm"},call("action_confirm")).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 ={}
query["until"] = os.time() local dk = docker.new()
local events = dk:events({query = query}) query["until"] = os.time()
if events.code == 200 then local events = dk:events({query = query})
for _, v in ipairs(events.body) do
if v and v.Type == "container" then if events.code == 200 then
logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. (v.Action or "null") .. " Container ID:".. (v.Actor.ID or "null") .. " Container Name:" .. (v.Actor.Attributes.name or "null") for _, v in ipairs(events.body) do
elseif v.Type == "network" then if v and v.Type == "container" then
logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. v.Action .. " Container ID:"..( v.Actor.Attributes.container or "null" ) .. " Network Name:" .. (v.Actor.Attributes.name or "null") .. " Network type:".. v.Actor.Attributes.type or "" logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. (v.Action or "null") .. " Container ID:".. (v.Actor.ID or "null") .. " Container Name:" .. (v.Actor.Attributes.name or "null")
elseif v.Type == "image" then elseif v.Type == "network" then
logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. v.Action .. " Image:".. (v.Actor.ID or "null").. " Image Name:" .. (v.Actor.Attributes.name or "null") logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. v.Action .. " Container ID:"..( v.Actor.Attributes.container or "null" ) .. " Network Name:" .. (v.Actor.Attributes.name or "null") .. " Network type:".. v.Actor.Attributes.type or ""
end elseif v.Type == "image" then
end logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. v.Action .. " Image:".. (v.Actor.ID or "null").. " Image Name:" .. (v.Actor.Attributes.name or "null")
end end
luci.template.render("dockerman/logs", {self={syslog = logs, title="Events"}}) end
end
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"])
if system_delta > 0.0 then local cpu_percent = 0.0
cpu_percent = string.format("%.2f", cpu_delta / system_delta * 100.0 * cpu_count) local cpu_delta = tonumber(d["cpu_stats"]["cpu_usage"]["total_usage"]) - tonumber(d["precpu_stats"]["cpu_usage"]["total_usage"])
end local system_delta = tonumber(d["cpu_stats"]["system_cpu_usage"]) - tonumber(d["precpu_stats"]["system_cpu_usage"])
-- return cpu_percent .. "%" if system_delta > 0.0 then
return cpu_percent cpu_percent = string.format("%.2f", cpu_delta / system_delta * 100.0 * cpu_count)
end
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" local data = {}
-- end if type(d["networks"]) == "table" then
-- end for e, v in pairs(d["networks"]) do
local data = {} data[e] = {
if type(d["networks"]) == "table" then bw_tx = tonumber(v.tx_bytes),
for e, v in pairs(d["networks"]) do bw_rx = tonumber(v.rx_bytes)
data[e] = { }
bw_tx = tonumber(v.tx_bytes), end
bw_rx = tonumber(v.rx_bytes) end
}
end return data
end
return data
end end
function action_get_container_stats(container_id) function action_get_container_stats(container_id)
if container_id then if container_id then
local dk = docker.new() local dk = docker.new()
local response = dk.containers:inspect({id = container_id}) local response = dk.containers:inspect({id = container_id})
if response.code == 200 and response.body.State.Running then if response.code == 200 and response.body.State.Running then
response = dk.containers:stats({id = container_id, query = {stream = false}}) response = dk.containers:stats({id = container_id, query = {stream = false}})
if response.code == 200 then if response.code == 200 then
local container_stats = response.body local container_stats = response.body
local cpu_percent = calculate_cpu_percent(container_stats) local cpu_percent = calculate_cpu_percent(container_stats)
local mem_useage, mem_limit = get_memory(container_stats) local mem_useage, mem_limit = get_memory(container_stats)
local bw_rxtx = get_rx_tx(container_stats) local bw_rxtx = get_rx_tx(container_stats)
luci.http.status(response.code, response.body.message) luci.http.status(response.code, response.body.message)
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
luci.http.write_json({ luci.http.write_json({
cpu_percent = cpu_percent, cpu_percent = cpu_percent,
memory = { memory = {
mem_useage = mem_useage, mem_useage = mem_useage,
mem_limit = mem_limit mem_limit = mem_limit
}, },
bw_rxtx = bw_rxtx bw_rxtx = bw_rxtx
}) })
else else
luci.http.status(response.code, response.body.message) luci.http.status(response.code, response.body.message)
luci.http.prepare_content("text/plain") luci.http.prepare_content("text/plain")
luci.http.write(response.body.message) luci.http.write(response.body.message)
end end
else else
if response.code == 200 then if response.code == 200 then
luci.http.status(500, "container "..container_id.." not running") luci.http.status(500, "container "..container_id.." not running")
luci.http.prepare_content("text/plain") luci.http.prepare_content("text/plain")
luci.http.write("Container "..container_id.." not running") luci.http.write("Container "..container_id.." not running")
else else
luci.http.status(response.code, response.body.message) luci.http.status(response.code, response.body.message)
luci.http.prepare_content("text/plain") luci.http.prepare_content("text/plain")
luci.http.write(response.body.message) luci.http.write(response.body.message)
end end
end end
else else
luci.http.status(404, "No container name or id") luci.http.status(404, "No container name or id")
luci.http.prepare_content("text/plain") luci.http.prepare_content("text/plain")
luci.http.write("No container name or id") luci.http.write("No container name or id")
end end
end end
function action_confirm() function action_confirm()
local data = docker:read_status() local data = docker:read_status()
if data then if data then
data = data:gsub("\n","<br>"):gsub(" ","&nbsp;") data = data:gsub("\n","<br>"):gsub(" ","&nbsp;")
code = 202 code = 202
msg = data msg = data
else else
code = 200 code = 200
msg = "finish" msg = "finish"
data = "finish" data = "finish"
end end
luci.http.status(code, msg)
luci.http.prepare_content("application/json") luci.http.status(code, msg)
luci.http.write_json({info = data}) luci.http.prepare_content("application/json")
luci.http.write_json({info = data})
end end
function download_archive() function download_archive()
local id = luci.http.formvalue("id") local id = luci.http.formvalue("id")
local path = luci.http.formvalue("path") local path = luci.http.formvalue("path")
local dk = docker.new() local dk = docker.new()
local first 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
first = true first = true
luci.http.header('Content-Disposition', 'inline; filename="archive.tar"') luci.http.header('Content-Disposition', 'inline; filename="archive.tar"')
luci.http.header('Content-Type', 'application\/x-tar') luci.http.header('Content-Type', 'application\/x-tar')
end end
luci.ltn12.pump.all(chunk, luci.http.write) luci.ltn12.pump.all(chunk, luci.http.write)
else else
if not first then if not first then
first = true first = true
luci.http.prepare_content("text/plain") luci.http.prepare_content("text/plain")
end end
luci.ltn12.pump.all(chunk, luci.http.write) luci.ltn12.pump.all(chunk, luci.http.write)
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)
local path = luci.http.formvalue("upload-path") local path = luci.http.formvalue("upload-path")
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
ltn12.pump.step(ltn12.source.string(chunk), sinkout) ltn12.pump.step(ltn12.source.string(chunk), sinkout)
end end
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({
local msg = res and res.body and res.body.message or nil id = container_id,
luci.http.status(res.code, msg) query = {
luci.http.prepare_content("application/json") path = path
luci.http.write_json({message = msg}) },
body = rec_send
})
local msg = res and res.body and res.body.message or nil
luci.http.status(res.code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg})
end end
function save_images(container_id) function save_images(container_id)
local names = luci.http.formvalue("names") local names = luci.http.formvalue("names")
local dk = docker.new() local dk = docker.new()
local first 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
first = true first = true
luci.http.status(res.code, res.message) luci.http.status(res.code, res.message)
luci.http.header('Content-Disposition', 'inline; filename="images.tar"') luci.http.header('Content-Disposition', 'inline; filename="images.tar"')
luci.http.header('Content-Type', 'application\/x-tar') luci.http.header('Content-Type', 'application\/x-tar')
end end
luci.ltn12.pump.all(chunk, luci.http.write) luci.ltn12.pump.all(chunk, luci.http.write)
else else
if not first then if not first then
first = true first = true
luci.http.prepare_content("text/plain") luci.http.prepare_content("text/plain")
end end
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 .. "...")
local res = dk.images:get({id = container_id, query = {names = names}}, cb) docker:write_status("Images: saving" .. " " .. container_id .. "...")
docker:clear_status() local res = dk.images:get({
local msg = res and res.body and res.body.message or nil id = container_id,
luci.http.status(res.code, msg) query = {
luci.http.prepare_content("application/json") names = names
luci.http.write_json({message = msg}) }
}, cb)
docker:clear_status()
local msg = res and res.body and res.body.message or nil
luci.http.status(res.code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg})
end end
function load_images() function load_images()
local path = luci.http.formvalue("upload-path") local path = luci.http.formvalue("upload-path")
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
ltn12.pump.step(ltn12.source.string(chunk), sinkout) ltn12.pump.step(ltn12.source.string(chunk), sinkout)
end end
end) end)
end end
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) else
else 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
function import_images() function import_images()
local src = luci.http.formvalue("src") local src = luci.http.formvalue("src")
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)
luci.http.setfilehandler(function (meta, chunk, eof) local rec_send = function(sinkout)
if chunk then luci.http.setfilehandler(function (meta, chunk, eof)
ltn12.pump.step(ltn12.source.string(chunk), sinkout) if chunk then
end ltn12.pump.step(ltn12.source.string(chunk), sinkout)
end) end
end end)
docker:write_status("Images: importing".. " ".. itag .."...\n") end
local repo = itag and itag:match("^([^:]+)")
local tag = itag and itag:match("^[^:]-:([^:]+)") docker:write_status("Images: importing".. " ".. itag .."...\n")
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 repo = itag and itag:match("^([^:]+)")
local msg = res and res.body and ( res.body.message )or nil local tag = itag and itag:match("^[^:]-:([^:]+)")
if not msg and #res.body == 0 then local res = dk.images:create({
-- res.body = {"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"} query = {
msg = res.body.status or res.body.error fromSrc = src or "-",
elseif not msg and #res.body >= 1 then repo = repo or nil,
-- res.body = [...{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}] tag = tag or nil
msg = res.body[#res.body].status or res.body[#res.body].error },
end body = not src and rec_send or nil
if res.code == 200 and msg and msg:match("sha256:") then }, docker.import_image_show_status_cb)
docker:clear_status()
else local msg = res and res.body and ( res.body.message )or nil
docker:append_status("code:" .. res.code.." ".. msg) if not msg and #res.body == 0 then
end msg = res.body.status or res.body.error
luci.http.status(res.code, msg) elseif not msg and #res.body >= 1 then
luci.http.prepare_content("application/json") msg = res.body[#res.body].status or res.body[#res.body].error
luci.http.write_json({message = msg}) end
if res.code == 200 and msg and msg:match("sha256:") then
docker:clear_status()
else
docker:append_status("code:" .. res.code.." ".. msg)
end
luci.http.status(res.code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg})
end end
function get_image_tags(image_id) function get_image_tags(image_id)
if not image_id then if not image_id then
luci.http.status(400, "no image id") luci.http.status(400, "no image id")
luci.http.prepare_content("application/json") luci.http.prepare_content("application/json")
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 res = dk.images:inspect({id = image_id}) local dk = docker.new()
local msg = res and res.body and res.body.message or nil local res = dk.images:inspect({
luci.http.status(res.code, msg) id = image_id
luci.http.prepare_content("application/json") })
if res.code == 200 then local msg = res and res.body and res.body.message or nil
local tags = res.body.RepoTags luci.http.status(res.code, msg)
luci.http.write_json({tags = tags}) luci.http.prepare_content("application/json")
else
local msg = res and res.body and res.body.message or nil if res.code == 200 then
luci.http.write_json({message = msg}) local tags = res.body.RepoTags
end luci.http.write_json({tags = tags})
else
local msg = res and res.body and res.body.message or nil
luci.http.write_json({message = msg})
end
end 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
luci.http.status(400, "no image id or tag") if type(src) ~= "string" or not image_id then
luci.http.prepare_content("application/json") luci.http.status(400, "no image id or tag")
luci.http.write_json({message = "no image id or tag"}) luci.http.prepare_content("application/json")
return luci.http.write_json({message = "no image id or tag"})
end return
local repo = src:match("^([^:]+)") end
local tag = src:match("^[^:]-:([^:]+)")
local dk = docker.new() local repo = src:match("^([^:]+)")
local res = dk.images:tag({id = image_id, query={repo=repo, tag=tag}}) local tag = src:match("^[^:]-:([^:]+)")
local msg = res and res.body and res.body.message or nil local dk = docker.new()
luci.http.status(res.code, msg) local res = dk.images:tag({
luci.http.prepare_content("application/json") id = image_id,
if res.code == 201 then query={
local tags = res.body.RepoTags repo=repo,
luci.http.write_json({tags = tags}) tag=tag
else }
local msg = res and res.body and res.body.message or nil })
luci.http.write_json({message = msg}) local msg = res and res.body and res.body.message or nil
end luci.http.status(res.code, msg)
luci.http.prepare_content("application/json")
if res.code == 201 then
local tags = res.body.RepoTags
luci.http.write_json({tags = tags})
else
local msg = res and res.body and res.body.message or nil
luci.http.write_json({message = msg})
end
end 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
luci.http.status(400, "no tag name") if not tag then
luci.http.prepare_content("application/json") luci.http.status(400, "no tag name")
luci.http.write_json({message = "no tag name"}) luci.http.prepare_content("application/json")
return luci.http.write_json({message = "no tag name"})
end return
local dk = docker.new() end
local res = dk.images:inspect({name = tag})
if res.code == 200 then local dk = docker.new()
local tags = res.body.RepoTags local res = dk.images:inspect({name = tag})
if #tags > 1 then
local r = dk.images:remove({name = tag}) if res.code == 200 then
local msg = r and r.body and r.body.message or nil local tags = res.body.RepoTags
luci.http.status(r.code, msg) if #tags > 1 then
luci.http.prepare_content("application/json") local r = dk.images:remove({name = tag})
luci.http.write_json({message = msg}) local msg = r and r.body and r.body.message or nil
else luci.http.status(r.code, msg)
luci.http.status(500, "Cannot remove the last tag") luci.http.prepare_content("application/json")
luci.http.prepare_content("application/json") luci.http.write_json({message = msg})
luci.http.write_json({message = "Cannot remove the last tag"}) else
end luci.http.status(500, "Cannot remove the last tag")
else luci.http.prepare_content("application/json")
local msg = res and res.body and res.body.message or nil luci.http.write_json({message = "Cannot remove the last tag"})
luci.http.status(res.code, msg) end
luci.http.prepare_content("application/json") else
luci.http.write_json({message = msg}) local msg = res and res.body and res.body.message or nil
end luci.http.status(res.code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg})
end
end end

View file

@ -3,193 +3,229 @@ 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
for i, v in ipairs(containers) do
local index = v.Created .. v.Id
data[index]={}
data[index]["_selected"] = 0
data[index]["_id"] = v.Id:sub(1,12)
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]["_status"] = v.Status
if v.Status:find("^Up") then
data[index]["_status"] = '<font color="green">'.. data[index]["_status"] .. "</font>"
else
data[index]["_status"] = '<font color="red">'.. data[index]["_status"] .. "</font>"
end
if (type(v.NetworkSettings) == "table" and type(v.NetworkSettings.Networks) == "table") then
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 "")
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
data[index]["_ports"] = nil
for _,v2 in ipairs(v.Ports) do
data[index]["_ports"] = (data[index]["_ports"] and (data[index]["_ports"] .. ", ") or "")
.. ((v2.PublicPort and v2.Type and v2.Type == "tcp") and ('<a href="javascript:void(0);" onclick="window.open((window.location.origin.match(/^(.+):\\d+$/) && window.location.origin.match(/^(.+):\\d+$/)[1] || window.location.origin) + \':\' + '.. v2.PublicPort ..', \'_blank\');">') or "")
.. (v2.PublicPort and (v2.PublicPort .. ":") or "") .. (v2.PrivatePort and (v2.PrivatePort .."/") or "") .. (v2.Type and v2.Type or "")
.. ((v2.PublicPort and v2.Type and v2.Type == "tcp")and "</a>" or "")
end
end
for ii,iv in ipairs(images) do
if iv.Id == v.ImageID then
data[index]["_image"] = iv.RepoTags and iv.RepoTags[1] or (iv.RepoDigests[1]:gsub("(.-)@.+", "%1") .. ":<none>")
end
end
data[index]["_image_id"] = v.ImageID:sub(8,20) if type(containers) ~= "table" then
data[index]["_command"] = v.Command return nil
end end
return data
for i, v in ipairs(containers) do
local index = v.Created .. v.Id
data[index]={}
data[index]["_selected"] = 0
data[index]["_id"] = v.Id:sub(1,12)
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]["_status"] = v.Status
if v.Status:find("^Up") then
data[index]["_status"] = '<font color="green">'.. data[index]["_status"] .. "</font>"
else
data[index]["_status"] = '<font color="red">'.. data[index]["_status"] .. "</font>"
end
if (type(v.NetworkSettings) == "table" and type(v.NetworkSettings.Networks) == "table") then
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 "")
end
end
if v.Ports and next(v.Ports) ~= nil then
data[index]["_ports"] = nil
for _,v2 in ipairs(v.Ports) do
data[index]["_ports"] = (data[index]["_ports"] and (data[index]["_ports"] .. ", ") or "")
.. ((v2.PublicPort and v2.Type and v2.Type == "tcp") and ('<a href="javascript:void(0);" onclick="window.open((window.location.origin.match(/^(.+):\\d+$/) && window.location.origin.match(/^(.+):\\d+$/)[1] || window.location.origin) + \':\' + '.. v2.PublicPort ..', \'_blank\');">') or "")
.. (v2.PublicPort and (v2.PublicPort .. ":") or "") .. (v2.PrivatePort and (v2.PrivatePort .."/") or "") .. (v2.Type and v2.Type or "")
.. ((v2.PublicPort and v2.Type and v2.Type == "tcp")and "</a>" or "")
end
end
for ii,iv in ipairs(images) do
if iv.Id == v.ImageID then
data[index]["_image"] = iv.RepoTags and iv.RepoTags[1] or (iv.RepoDigests[1]:gsub("(.-)@.+", "%1") .. ":<none>")
end
end
data[index]["_image_id"] = v.ImageID:sub(8,20)
data[index]["_command"] = v.Command
end
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 end
c_selected[#c_selected+1] = c_lists[c_table_sid].name --container_name:cfgvalue(c_table_sid) end
end
end if #container_selected > 0 then
if #c_selected >0 then local success = true
docker:clear_status()
local success = true docker:clear_status()
for _,cont in ipairs(c_selected) do 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
success = false success = false
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")
else else
docker:append_status("done\n") docker:append_status("done\n")
end end
end end
if success then docker:clear_status() end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers")) if success then
end docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
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" luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
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"))
end end
btnstart.write = function(self, section)
start_stop_remove(m,"start") 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")
end end
btnrestart.write = function(self, section)
start_stop_remove(m,"restart") 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")
end end
btnremove.write = function(self, section)
start_stop_remove(m,"remove") o = s:option(Button, "_stop")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Stop")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"stop")
end end
btnstop.write = function(self, section)
start_stop_remove(m,"stop") 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")
end end
btnkill.write = function(self, section)
start_stop_remove(m,"kill") 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 end
return m return m

View file

@ -3,221 +3,272 @@ 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
local index = v.Created .. v.Id for i, v in ipairs(images) do
data[index]={} local index = v.Created .. v.Id
data[index]["_selected"] = 0
data[index]["id"] = v.Id:sub(8) data[index]={}
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]["_selected"] = 0
if v.RepoTags and next(v.RepoTags)~=nil then data[index]["id"] = v.Id:sub(8)
for i, v1 in ipairs(v.RepoTags) do 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]["_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 v.RepoTags and next(v.RepoTags)~=nil then
data[index]["tag"] = v1--:match("<none>") and nil or v1 for i, v1 in ipairs(v.RepoTags) do
end 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>'))
end
else if not data[index]["tag"] then
data[index]["_tags"] = v.RepoDigests[1] and v.RepoDigests[1]:match("^(.-)@.+") data[index]["tag"] = v1
data[index]["_tags"] = (data[index]["_tags"] and data[index]["_tags"] or "<none>" ).. ":<none>" end
end end
data[index]["_tags"] = data[index]["_tags"]:gsub("<none>","&lt;none&gt;") else
-- data[index]["_tags"] = '<a href="javascript:handle_tag(\''..data[index]["_id"]..'\')">' .. data[index]["_tags"] .. '</a>' data[index]["_tags"] = v.RepoDigests[1] and v.RepoDigests[1]:match("^(.-)@.+")
for ci,cv in ipairs(containers) do data[index]["_tags"] = (data[index]["_tags"] and data[index]["_tags"] or "<none>" ).. ":<none>"
if v.Id == cv.ImageID then end
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>" data[index]["_tags"] = data[index]["_tags"]:gsub("<none>","&lt;none&gt;")
end for ci,cv in ipairs(containers) do
end if v.Id == cv.ImageID then
data[index]["_size"] = string.format("%.2f", tostring(v.Size/1024/1024)).."MB" data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
data[index]["_created"] = os.date("%Y/%m/%d %H:%M:%S",v.Created) '<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
return data end
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)
end
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"
local hastag = value:find(":") o.write = function(self, section, value)
if not hastag then local hastag = value:find(":")
value = value .. ":latest"
end if not hastag then
pull_value["_image_tag_name"] = value value = value .. ":latest"
end end
action_pull.write = function(self, section) pull_value["_image_tag_name"] = value
local tag = pull_value["_image_tag_name"]
local json_stringify = luci.jsonc and luci.jsonc.stringify
if tag and tag ~= "" then
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)
-- {"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
docker:clear_status()
else
docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
end
else
docker:append_status("code: 400 please input the name of image name!")
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
end end
local import_section = m:section(SimpleSection, translate("Import Images")) o = s:option(Button, "_pull")
local im = import_section:option(DummyValue, "_image_import") o.inputtitle= translate("Pull")
im.template = "dockerman/images_import" o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "add"
o.write = function(self, section)
local tag = pull_value["_image_tag_name"]
local json_stringify = luci.jsonc and luci.jsonc.stringify
local image_table = m:section(Table, image_list, translate("Images")) if tag and tag ~= "" then
docker:write_status("Images: " .. "pulling" .. " " .. tag .. "...\n")
local res = dk.images:create({query = {fromImage=tag}}, docker.pull_image_show_status_cb)
local image_selecter = image_table:option(Flag, "_selected","") 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
image_selecter.disabled = 0 docker:clear_status()
image_selecter.enabled = 1 else
image_selecter.default = 0 docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
end
else
docker:append_status("code: 400 please input the name of image name!")
end
local image_id = image_table:option(DummyValue, "_id", translate("ID")) luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
image_id.rawhtml = true
image_table:option(DummyValue, "_tags", translate("RepoTags")).rawhtml = true
image_table:option(DummyValue, "_containers", translate("Containers")).rawhtml = true
image_table:option(DummyValue, "_size", translate("Size"))
image_table:option(DummyValue, "_created", translate("Created"))
image_selecter.write = function(self, section, value)
image_list[section]._selected = value
end end
s = m:section(SimpleSection, translate("Import Images"))
o = s:option(DummyValue, "_image_import")
o.template = "dockerman/images_import"
s = m:section(Table, image_list, translate("Images"))
o = s:option(Flag, "_selected","")
o.disabled = 0
o.enabled = 1
o.default = 0
o.write = function(self, section, value)
image_list[section]._selected = value
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 end
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 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 docker:append_status("Images: " .. "remove" .. " " .. img .. "...")
if force then query = {force = true} end
local msg = dk.images:remove({id = img, query = query}) if force then
if msg.code ~= 200 then query = {force = true}
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n") end
success = false
else local msg = dk.images:remove({
docker:append_status("done\n") id = img,
end query = query
end })
if success then docker:clear_status() end if msg.code ~= 200 then
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images")) docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
end success = false
else
docker:append_status("done\n")
end
end
if success then
docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
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()
local action = m:section(Table,{{}})
action.notitle=true
action.rowcolors=false
action.template="cbi/nullsection"
local btnremove = action:option(Button, "remove")
btnremove.inputtitle= translate("Remove")
btnremove.template = "dockerman/cbi/inlinebutton"
btnremove.inputstyle = "remove"
btnremove.forcewrite = true
btnremove.write = function(self, section)
remove_action()
end end
local btnforceremove = action:option(Button, "forceremove") s = m:section(Table,{{}})
btnforceremove.inputtitle= translate("Force Remove") s.notitle=true
btnforceremove.template = "dockerman/cbi/inlinebutton" s.rowcolors=false
btnforceremove.inputstyle = "remove" s.template="cbi/nullsection"
btnforceremove.forcewrite = true
btnforceremove.write = function(self, section) o = s:option(Button, "remove")
remove_action(true) o.inputtitle= translate("Remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
remove_action()
end end
local btnsave = action:option(Button, "save") o = s:option(Button, "forceremove")
btnsave.inputtitle= translate("Save") o.inputtitle= translate("Force Remove")
btnsave.template = "dockerman/cbi/inlinebutton" o.template = "dockerman/cbi/inlinebutton"
btnsave.inputstyle = "edit" o.inputstyle = "remove"
btnsave.forcewrite = true o.forcewrite = true
btnsave.write = function (self, section) o.write = function(self, section)
local image_selected = {} remove_action(true)
local image_table_sids = image_table:cfgsections()
for _, image_table_sid in ipairs(image_table_sids) do
if image_list[image_table_sid]._selected == 1 then
image_selected[#image_selected+1] = image_list[image_table_sid].id --image_id:cfgvalue(image_table_sid)
end
end
if next(image_selected) ~= nil then
local names
for _,img in ipairs(image_selected) do
names = names and (names .. "&names=".. img) or img
end
local first
local cb = function(res, chunk)
if res.code == 200 then
if not first then
first = true
luci.http.header('Content-Disposition', 'inline; filename="images.tar"')
luci.http.header('Content-Type', 'application\/x-tar')
end
luci.ltn12.pump.all(chunk, luci.http.write)
else
if not first then
first = true
luci.http.prepare_content("text/plain")
end
luci.ltn12.pump.all(chunk, luci.http.write)
end
end
docker:write_status("Images: " .. "save" .. " " .. table.concat(image_selected, "\n") .. "...")
local msg = dk.images:get({query = {names = names}}, cb)
if msg.code ~= 200 then
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
success = false
else
docker:clear_status()
end
end
end end
local btnload = action:option(Button, "load") o = s:option(Button, "save")
btnload.inputtitle= translate("Load") o.inputtitle= translate("Save")
btnload.template = "dockerman/images_load" o.template = "dockerman/cbi/inlinebutton"
btnload.inputstyle = "add" o.inputstyle = "edit"
o.forcewrite = true
o.write = function (self, section)
local image_selected = {}
for k in pairs(image_list) do
if image_list[k]._selected == 1 then
image_selected[#image_selected + 1] = image_list[k].id
end
end
if next(image_selected) ~= nil then
local names, first
for _, img in ipairs(image_selected) do
names = names and (names .. "&names=".. img) or img
end
local cb = function(res, chunk)
if res.code == 200 then
if not first then
first = true
luci.http.header('Content-Disposition', 'inline; filename="images.tar"')
luci.http.header('Content-Type', 'application\/x-tar')
end
luci.ltn12.pump.all(chunk, luci.http.write)
else
if not first then
first = true
luci.http.prepare_content("text/plain")
end
luci.ltn12.pump.all(chunk, luci.http.write)
end
end
docker:write_status("Images: " .. "save" .. " " .. table.concat(image_selected, "\n") .. "...")
local msg = dk.images:get({query = {names = names}}, cb)
if msg.code ~= 200 then
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
success = false
else
docker:clear_status()
end
end
end
o = s:option(Button, "load")
o.inputtitle= translate("Load")
o.template = "dockerman/images_load"
o.inputstyle = "add"
return m return m

View file

@ -3,128 +3,150 @@ 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
for i, v in ipairs(networks) do return nil
local index = v.Created .. v.Id end
data[index]={}
data[index]["_selected"] = 0 for i, v in ipairs(networks) do
data[index]["_id"] = v.Id:sub(1,12) local index = v.Created .. v.Id
data[index]["_name"] = v.Name
data[index]["_driver"] = v.Driver data[index]={}
if v.Driver == "bridge" then data[index]["_selected"] = 0
data[index]["_interface"] = v.Options["com.docker.network.bridge.name"] data[index]["_id"] = v.Id:sub(1,12)
elseif v.Driver == "macvlan" then data[index]["_name"] = v.Name
data[index]["_interface"] = v.Options.parent data[index]["_driver"] = v.Driver
end
data[index]["_subnet"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil if v.Driver == "bridge" then
data[index]["_gateway"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Gateway or nil data[index]["_interface"] = v.Options["com.docker.network.bridge.name"]
end elseif v.Driver == "macvlan" then
return data data[index]["_interface"] = v.Options.parent
end
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
end
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
o.write = function(self, section, value)
network_list[section]._selected = value
end end
network_name = network_table:option(DummyValue, "_name", translate("Network Name")) o = s:option(DummyValue, "_id", translate("ID"))
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) o = s:option(DummyValue, "_name", translate("Network Name"))
network_list[section]._selected = value
o = s:option(DummyValue, "_driver", translate("Driver"))
o = s:option(DummyValue, "_interface", translate("Parent Interface"))
o = s:option(DummyValue, "_subnet", translate("Subnet"))
o = s:option(DummyValue, "_gateway", translate("Gateway"))
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 end
docker_status = m:section(SimpleSection) s = m:section(Table,{{}})
docker_status.template = "dockerman/apply_widget" s.notitle=true
docker_status.err=docker:read_status() s.rowcolors=false
docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;") s.template="cbi/nullsection"
if docker_status.err then docker:clear_status() end
action = m:section(Table,{{}}) o = s:option(Button, "_new")
action.notitle=true o.inputtitle= translate("New")
action.rowcolors=false o.template = "dockerman/cbi/inlinebutton"
action.template="cbi/nullsection" o.notitle=true
btnnew=action:option(Button, "_new") o.inputstyle = "add"
btnnew.inputtitle= translate("New") o.forcewrite = true
btnnew.template = "dockerman/cbi/inlinebutton" o.write = function(self, section)
btnnew.notitle=true luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
btnnew.inputstyle = "add"
btnnew.forcewrite = true
btnnew.write = function(self, section)
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
local network_selected = {} o.write = function(self, section)
local network_name_selected = {} local network_selected = {}
local network_driver_selected = {} local network_name_selected = {}
-- 遍历table中sectionid local network_driver_selected = {}
local network_table_sids = network_table:cfgsections()
for _, network_table_sid in ipairs(network_table_sids) do for k in pairs(network_list) do
-- 得到选中项的名字 if network_list[k]._selected == 1 then
if network_list[network_table_sid]._selected == 1 then network_selected[#network_selected + 1] = network_list[k]._id
network_selected[#network_selected+1] = network_list[network_table_sid]._id --network_name:cfgvalue(network_table_sid) network_name_selected[#network_name_selected + 1] = network_list[k]._name
network_name_selected[#network_name_selected+1] = network_list[network_table_sid]._name network_driver_selected[#network_driver_selected + 1] = network_list[k]._driver
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
docker:append_status("Networks: " .. "remove" .. " " .. net .. "...") for ii, net in ipairs(network_selected) do
local res = dk.networks["remove"](dk, {id = net}) docker:append_status("Networks: " .. "remove" .. " " .. net .. "...")
if res and res.code >= 300 then local res = dk.networks["remove"](dk, {id = net})
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
success = false if res and res.code >= 300 then
else docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
docker:append_status("done\n") success = false
if network_driver_selected[ii] == "macvlan" then else
docker.remove_macvlan_interface(network_name_selected[ii]) docker:append_status("done\n")
end if network_driver_selected[ii] == "macvlan" then
end docker.remove_macvlan_interface(network_name_selected[ii])
end end
if success then end
docker:clear_status() end
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks")) if success then
end docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
end
end end
return m return m

View file

@ -3,219 +3,244 @@ 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
local name = data.name local name = data.name
local driver = data.dirver local driver = data.dirver
local internal = data.internal == 1 and true or false local internal = data.internal == 1 and true or false
local subnet = data.subnet local subnet = data.subnet
local gateway = data.gateway local gateway = data.gateway
local ip_range = data.ip_range local ip_range = data.ip_range
local aux_address = {} local aux_address = {}
local tmp = data.aux_address or {} local tmp = data.aux_address or {}
for i,v in ipairs(tmp) do for i,v in ipairs(tmp) do
_,_,k1,v1 = v:find("(.-)=(.+)") _,_,k1,v1 = v:find("(.-)=(.+)")
aux_address[k1] = v1 aux_address[k1] = v1
end end
local options = {} local options = {}
tmp = data.options or {} tmp = data.options or {}
for i,v in ipairs(tmp) do for i,v in ipairs(tmp) do
_,_,k1,v1 = v:find("(.-)=(.+)") _,_,k1,v1 = v:find("(.-)=(.+)")
options[k1] = v1 options[k1] = v1
end end
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,
IPAM = { IPAM = {
Driver= "default" Driver= "default"
}, },
Internal = internal Internal = internal
} }
if subnet or gateway or ip_range then if subnet or gateway or ip_range then
create_body["IPAM"]["Config"] = { create_body["IPAM"]["Config"] = {
{ {
Subnet = subnet, Subnet = subnet,
Gateway = gateway, Gateway = gateway,
IPRange = ip_range, IPRange = ip_range,
AuxAddress = aux_address, AuxAddress = aux_address,
AuxiliaryAddresses = aux_address AuxiliaryAddresses = aux_address
} }
} }
end end
if driver == "macvlan" then
create_body["Options"] = {
macvlan_mode = data.macvlan_mode,
parent = data.parent
}
elseif driver == "ipvlan" then
create_body["Options"] = {
ipvlan_mode = data.ipvlan_mode
}
elseif driver == "overlay" then
create_body["Ingress"] = data.ingerss == 1 and true or false
end
if ipv6 and data.subnet6 and data.subnet6 then if driver == "macvlan" then
if type(create_body["IPAM"]["Config"]) ~= "table" then create_body["Options"] = {
create_body["IPAM"]["Config"] = {} macvlan_mode = data.macvlan_mode,
end parent = data.parent
local index = #create_body["IPAM"]["Config"] }
create_body["IPAM"]["Config"][index+1] = { elseif driver == "ipvlan" then
Subnet = data.subnet6, create_body["Options"] = {
Gateway = data.gateway6 ipvlan_mode = data.ipvlan_mode
} }
end elseif driver == "overlay" then
create_body["Ingress"] = data.ingerss == 1 and true or false
end
if next(options) ~= nil then if ipv6 and data.subnet6 and data.subnet6 then
create_body["Options"] = create_body["Options"] or {} if type(create_body["IPAM"]["Config"]) ~= "table" then
for k, v in pairs(options) do create_body["IPAM"]["Config"] = {}
create_body["Options"][k] = v end
end local index = #create_body["IPAM"]["Config"]
end create_body["IPAM"]["Config"][index+1] = {
Subnet = data.subnet6,
Gateway = data.gateway6
}
end
create_body = docker.clear_empty_tables(create_body) if next(options) ~= nil then
docker:write_status("Network: " .. "create" .. " " .. create_body.Name .. "...") create_body["Options"] = create_body["Options"] or {}
local res = dk.networks:create({body = create_body}) for k, v in pairs(options) do
if res and res.code == 201 then create_body["Options"][k] = v
docker:write_status("Network: " .. "create macvlan interface...") end
res = dk.networks:inspect({ name = create_body.Name }) end
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] create_body = docker.clear_empty_tables(create_body)
and res.body.IPAM.Config[1].Gateway and res.body.IPAM.Config[1].Subnet then docker:write_status("Network: " .. "create" .. " " .. create_body.Name .. "...")
docker.create_macvlan_interface(data.name, data.parent, res.body.IPAM.Config[1].Gateway, res.body.IPAM.Config[1].Subnet)
end local res = dk.networks:create({
docker:clear_status() body = create_body
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks")) })
else
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n") if res and res.code == 201 then
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork")) docker:write_status("Network: " .. "create macvlan interface...")
end res = dk.networks:inspect({
end name = create_body.Name
})
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
docker:clear_status()
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
else
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
end
end
end end
return m return m

View file

@ -3,26 +3,29 @@ 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"}
for i=1, 5 do for i=1, 5 do
if byte > 1024 and i < 5 then if byte > 1024 and i < 5 then
byte = byte / 1024 byte = byte / 1024
else else
return string.format("%.2f %s", byte, suff[i]) return string.format("%.2f %s", byte, suff[i])
end end
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,124 +34,122 @@ 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 (require "luci.model.docker").new():_ping().code == 200 then
local dk = docker.new()
containers_list = dk.containers:list({query = {all=true}}).body
local images_list = dk.images:list().body
local vol = dk.volumes:list()
local volumes_list = vol and vol.body and vol.body.Volumes or {}
local networks_list = dk.networks:list().body or {}
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['4ApiVersion']._value = docker_info.headers["Api-Version"]
docker_info_table['5NCPU']._value = tostring(docker_info.body.NCPU)
docker_info_table['6MemTotal']._value = byte_format(docker_info.body.MemTotal)
if docker_info.body.DockerRootDir then
local statvfs = nixio.fs.statvfs(docker_info.body.DockerRootDir)
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") .. ")"
end
docker_info_table['8IndexServerAddress']._value = docker_info.body.IndexServerAddress
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)
end
s.images_used = 0 if docker.new():_ping().code == 200 then
for i, v in ipairs(images_list) do local dk = docker.new()
for ci,cv in ipairs(containers_list) do local containers_list = dk.containers:list({query = {all=true}}).body
if v.Id == cv.ImageID then local images_list = dk.images:list().body
s.images_used = s.images_used + 1 local vol = dk.volumes:list()
break local volumes_list = vol and vol.body and vol.body.Volumes or {}
end local networks_list = dk.networks:list().body or {}
end local docker_info = dk:info()
end
s.containers_running = tostring(docker_info.body.ContainersRunning) docker_info_table['3ServerVersion']._value = docker_info.body.ServerVersion
s.images_used = tostring(s.images_used) docker_info_table['4ApiVersion']._value = docker_info.headers["Api-Version"]
s.containers_total = tostring(docker_info.body.Containers) docker_info_table['5NCPU']._value = tostring(docker_info.body.NCPU)
s.images_total = tostring(#images_list) docker_info_table['6MemTotal']._value = byte_format(docker_info.body.MemTotal)
s.networks_total = tostring(#networks_list) if docker_info.body.DockerRootDir then
s.volumes_total = tostring(#volumes_list) local statvfs = nixio.fs.statvfs(docker_info.body.DockerRootDir)
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") .. ")"
end
docker_info_table['8IndexServerAddress']._value = docker_info.body.IndexServerAddress
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)
end
s.images_used = 0
for i, v in ipairs(images_list) do
for ci,cv in ipairs(containers_list) do
if v.Id == cv.ImageID then
s.images_used = s.images_used + 1
break
end
end
end
s.containers_running = tostring(docker_info.body.ContainersRunning)
s.images_used = tostring(s.images_used)
s.containers_total = tostring(docker_info.body.Containers)
s.images_total = tostring(#images_list)
s.networks_total = tostring(#networks_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,114 +3,140 @@ 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
for vi, vv in ipairs(cv.Mounts) do break
if v.Name == vv.Name then end
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "").. for vi, vv in ipairs(cv.Mounts) do
'<a href='..luci.dispatcher.build_url("admin/docker/container/"..cv.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. cv.Names[1]:sub(2)..'</a>' if v.Name == vv.Name then
end data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
end '<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
data[index]["_driver"] = v.Driver end
data[index]["_mountpoint"] = nil end
for v1 in v.Mountpoint:gmatch('[^/]+') do data[index]["_driver"] = v.Driver
if v1 == index then data[index]["_mountpoint"] = nil
data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..."
else for v1 in v.Mountpoint:gmatch('[^/]+') do
data[index]["_mountpoint"] = (data[index]["_mountpoint"] and data[index]["_mountpoint"] or "").."/".. v1 if v1 == index then
end data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..."
end else
data[index]["_created"] = v.CreatedAt data[index]["_mountpoint"] = (data[index]["_mountpoint"] and data[index]["_mountpoint"] or "").."/".. v1
end end
return data end
data[index]["_created"] = v.CreatedAt
end
return data
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 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_list[section]._selected = value
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
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)
local volume_selected = {} s.template = "dockerman/apply_widget"
-- 遍历table中sectionid s.err=docker:read_status()
local volume_table_sids = volume_table:cfgsections() s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
for _, volume_table_sid in ipairs(volume_table_sids) do if s.err then
-- 得到选中项的名字 docker:clear_status()
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
if next(volume_selected) ~= nil then
local success = true
docker:clear_status()
for _,vol in ipairs(volume_selected) do
docker:append_status("Volumes: " .. "remove" .. " " .. vol .. "...")
local msg = dk.volumes["remove"](dk, {id = vol})
if msg.code ~= 204 then
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
success = false
else
docker:append_status("done\n")
end
end
if success then docker:clear_status() end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/volumes"))
end
end 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 = {}
for k in pairs(volume_list) do
if volume_list[k]._selected == 1 then
volume_selected[#volume_selected+1] = k
end
end
if next(volume_selected) ~= nil then
local success = true
docker:clear_status()
for _,vol in ipairs(volume_selected) do
docker:append_status("Volumes: " .. "remove" .. " " .. vol .. "...")
local msg = dk.volumes["remove"](dk, {id = vol})
if msg.code ~= 204 then
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
success = false
else
docker:append_status("done\n")
end
end
if success then
docker:clear_status()
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/volumes"))
end
end
return m return m

View file

@ -3,276 +3,347 @@ 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
_docker:append_status("done\n") 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
else _docker:append_status("done\n")
res.body.message = res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message) else
end res.body.message = res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)
new_image_id = self.images:inspect({name = image_name}).body.Id end
return new_image_id, res
new_image_id = self.images:inspect({name = image_name}).body.Id
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 end
for i, v in ipairs(t1) do
if t1[i] ~= t2[i] then return false end if not t2 then
end return false
return true 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
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
local res = {} end
for _, v1 in ipairs(t1) do
local found = false if not t2 or next(t2) == nil then
for _, v2 in ipairs(t2) do return t1
if v1 == v2 then end
found= true
break local res = {}
end for _, v1 in ipairs(t1) do
end local found = false
if not found then for _, v2 in ipairs(t2) do
table.insert(res, v1) if v1 == v2 then
end found= true
end break
return next(res) == nil and nil or res end
end
if not found then
table.insert(res, v1)
end
end
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
local res = {} end
for k1, v1 in pairs(t1) do
local found = false
for k2, v2 in ipairs(t2) do
if k1 == k2 and luci.util.serialize_data(v1) == luci.util.serialize_data(v2) then
found= true
break
end
end
if not found then
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
return next(res) ~= nil and res or nil if not t2 or next(t2) == nil then
return t1
end
local res = {}
for k1, v1 in pairs(t1) do
local found = false
for k2, v2 in ipairs(t2) do
if k1 == k2 and luci.util.serialize_data(v1) == luci.util.serialize_data(v2) then
found= true
break
end
end
if not found then
res[k1] = v1
end
end
return next(res) ~= nil and res or nil
end end
_docker.clear_empty_tables = function ( t ) _docker.clear_empty_tables = function ( t )
local k, v local k, v
if next(t) == nil then
t = nil if next(t) == nil then
else t = nil
for k, v in pairs(t) do else
if type(v) == 'table' then for k, v in pairs(t) do
t[k] = _docker.clear_empty_tables(v) if type(v) == 'table' then
end t[k] = _docker.clear_empty_tables(v)
end end
end end
return t end
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 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.Labels = table_subtract(config.Labels, image_config.Labels)
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
config.ExposedPorts = {}
for p, v in pairs(old_host_config.PortBindings) do
config.ExposedPorts[p] = { HostPort=v[1] and v[1].HostPort }
end
end
-- handle network config, we need only one network, extras need to network connect action if config.WorkingDir == image_config.WorkingDir then
local network_setting = {} config.WorkingDir = ""
local multi_network = false end
local extra_network = {}
for k, v in pairs(old_network_setting) do
if multi_network then
extra_network[k] = v
else
network_setting[k] = v
end
multi_network = true
end
-- handle hostconfig if config.User == image_config.User then
local host_config = old_host_config config.User = ""
-- if host_config.PortBindings and next(host_config.PortBindings) == nil then host_config.PortBindings = nil end end
-- host_config.LogConfig = nil
host_config.Mounts = {}
-- for volumes
for i, v in ipairs(container_config.Mounts) do
if v.Type == "volume" then
table.insert(host_config.Mounts, {
Type = v.Type,
Target = v.Destination,
Source = v.Source:match("([^/]+)\/_data"),
BindOptions = (v.Type == "bind") and {Propagation = v.Propagation} or nil,
ReadOnly = not v.RW
})
end
end
if table_equal(config.Cmd, image_config.Cmd) then
config.Cmd = nil
end
-- merge configs if table_equal(config.Entrypoint, image_config.Entrypoint) then
local create_body = config config.Entrypoint = nil
create_body["HostConfig"] = host_config end
create_body["NetworkingConfig"] = {EndpointsConfig = network_setting}
create_body = _docker.clear_empty_tables(create_body) or {} if table_equal(config.ExposedPorts, image_config.ExposedPorts) then
extra_network = _docker.clear_empty_tables(extra_network) or {} config.ExposedPorts = nil
return create_body, extra_network end
config.Env = table_subtract(config.Env, image_config.Env)
config.Labels = table_subtract(config.Labels, image_config.Labels)
config.Volumes = map_subtract(config.Volumes, image_config.Volumes)
if old_host_config.PortBindings and next(old_host_config.PortBindings) ~= nil then
config.ExposedPorts = {}
for p, v in pairs(old_host_config.PortBindings) do
config.ExposedPorts[p] = { HostPort=v[1] and v[1].HostPort }
end
end
local network_setting = {}
local multi_network = false
local extra_network = {}
for k, v in pairs(old_network_setting) do
if multi_network then
extra_network[k] = v
else
network_setting[k] = v
end
multi_network = true
end
local host_config = old_host_config
host_config.Mounts = {}
for i, v in ipairs(container_config.Mounts) do
if v.Type == "volume" then
table.insert(host_config.Mounts, {
Type = v.Type,
Target = v.Destination,
Source = v.Source:match("([^/]+)\/_data"),
BindOptions = (v.Type == "bind") and {Propagation = v.Propagation} or nil,
ReadOnly = not v.RW
})
end
end
local create_body = config
create_body["HostConfig"] = host_config
create_body["NetworkingConfig"] = {EndpointsConfig = network_setting}
create_body = _docker.clear_empty_tables(create_body) or {}
extra_network = _docker.clear_empty_tables(extra_network) or {}
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})
if container_info.code > 300 and type(container_info.body) == "table" then
return container_info
end
local image_name = container_info.body.Config.Image
if not image_name:match(".-:.+") then image_name = image_name .. ":latest" end
local old_image_id = container_info.body.Image
local container_name = container_info.body.Name:sub(2)
local image_id, res = update_image(self, image_name) local container_info = self.containers:inspect({id = request.id})
if res and res.code ~= 200 then return res end
if image_id == old_image_id then
return {code = 305, body = {message = "Already up to date"}}
end
_docker:append_status("Container: " .. "Stop" .. " " .. container_name .. "...") if container_info.code > 300 and type(container_info.body) == "table" then
res = self.containers:stop({name = container_name}) return container_info
if res and res.code < 305 then end
_docker:append_status("done\n")
else
return res
end
_docker:append_status("Container: rename" .. " " .. container_name .. " to ".. container_name .. "_old ...") local image_name = container_info.body.Config.Image
res = self.containers:rename({name = container_name, query = { name = container_name .. "_old" }}) if not image_name:match(".-:.+") then
if res and res.code < 300 then image_name = image_name .. ":latest"
_docker:append_status("done\n") end
else
return res
end
-- handle config local old_image_id = container_info.body.Image
local image_config = self.images:inspect({id = old_image_id}).body.Config local container_name = container_info.body.Name:sub(2)
local create_body, extra_network = get_config(container_info.body, image_config)
-- create new container local image_id, res = update_image(self, image_name)
_docker:append_status("Container: Create" .. " " .. container_name .. "...") if res and res.code ~= 200 then
create_body = _docker.clear_empty_tables(create_body) return res
res = self.containers:create({name = container_name, body = create_body}) end
if res and res.code > 300 then return res end
_docker:append_status("done\n")
-- extra networks need to network connect action if image_id == old_image_id then
for k, v in pairs(extra_network) do return {code = 305, body = {message = "Already up to date"}}
_docker:append_status("Networks: Connect" .. " " .. container_name .. "...") end
res = self.networks:connect({id = k, body = {Container = container_name, EndpointConfig = v}})
if res.code > 300 then return res end
_docker:append_status("done\n") _docker:append_status("Container: " .. "Stop" .. " " .. container_name .. "...")
end res = self.containers:stop({name = container_name})
_docker:clear_status() if res and res.code < 305 then
return res _docker:append_status("done\n")
else
return res
end
_docker:append_status("Container: rename" .. " " .. container_name .. " to ".. container_name .. "_old ...")
res = self.containers:rename({name = container_name, query = { name = container_name .. "_old" }})
if res and res.code < 300 then
_docker:append_status("done\n")
else
return res
end
local image_config = self.images:inspect({id = old_image_id}).body.Config
local create_body, extra_network = get_config(container_info.body, image_config)
-- create new container
_docker:append_status("Container: Create" .. " " .. container_name .. "...")
create_body = _docker.clear_empty_tables(create_body)
res = self.containers:create({name = container_name, body = create_body})
if res and res.code > 300 then
return res
end
_docker:append_status("done\n")
-- extra networks need to network connect action
for k, v in pairs(extra_network) do
_docker:append_status("Networks: Connect" .. " " .. container_name .. "...")
res = self.networks:connect({id = k, body = {Container = container_name, EndpointConfig = v}})
if res.code > 300 then
return res
end
_docker:append_status("done\n")
end
_docker:clear_status()
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
local old_image_id = container_info.body.Image return nil
local image_config = self.images:inspect({id = old_image_id}).body.Config end
return get_config(container_info.body, image_config)
local old_image_id = container_info.body.Image
local image_config = self.images:inspect({id = old_image_id}).body.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
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 port = uci:get("dockerd", "globals", "remote_port") or nil
local _new = docker.new(options) else
_new.options.status_path = uci:get("dockerman", "local", "status_path") socket_path = uci:get("dockerd", "globals", "socket_path") or "/var/run/docker.sock"
_new.containers_upgrade = upgrade end
_new.containers_duplicate_config = duplicate_config
return _new 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
}
local _new = docker.new(_docker.options)
_new.containers_upgrade = upgrade
_new.containers_duplicate_config = duplicate_config
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
local file_docker_action_status=io.open(self.options.status_path, "a+") return
file_docker_action_status:write(val) end
file_docker_action_status:close() local file_docker_action_status=io.open(self.options.status_path, "a+")
file_docker_action_status:write(val)
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
local file_docker_action_status=io.open(self.options.status_path, "w+") return
file_docker_action_status:write(val) end
file_docker_action_status:close() local file_docker_action_status=io.open(self.options.status_path, "w+")
file_docker_action_status:write(val)
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)
res.body = res.body or {} res.body = res.body or {}
while true do while true do
local chunk = source() local chunk = source()
if chunk then if chunk then
--standard output to res.body --standard output to res.body
table.insert(res.body, chunk) table.insert(res.body, chunk)
handler(chunk) handler(chunk)
else else
return return
end end
end end
end end
--{"status":"Pulling from library\/debian","id":"latest"} --{"status":"Pulling from library\/debian","id":"latest"}
@ -284,114 +355,128 @@ end
--{"status":"Digest: sha256:a63d0b2ecbd723da612abf0a8bdb594ee78f18f691d7dc652ac305a490c9b71a"} --{"status":"Digest: sha256:a63d0b2ecbd723da612abf0a8bdb594ee78f18f691d7dc652ac305a490c9b71a"}
--{"status":"Status: Downloaded newer image for debian:latest"} --{"status":"Status: Downloaded newer image for debian:latest"}
_docker.pull_image_show_status_cb = function(res, source) _docker.pull_image_show_status_cb = function(res, source)
return status_cb(res, source, function(chunk) return status_cb(res, source, function(chunk)
local json_parse = luci.jsonc.parse local json_parse = luci.jsonc.parse
local step = json_parse(chunk) local step = json_parse(chunk)
if type(step) == "table" then if type(step) == "table" then
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
if num == 0 then buf, num = buf:gsub("\t"..step.id .. ": .-\n", str)
buf = buf .. str end
end if num == 0 then
_docker:write_status(buf) buf = buf .. str
end end
end) _docker:write_status(buf)
end
end)
end end
--{"status":"Downloading from https://downloads.openwrt.org/releases/19.07.0/targets/x86/64/openwrt-19.07.0-x86-64-generic-rootfs.tar.gz"} --{"status":"Downloading from https://downloads.openwrt.org/releases/19.07.0/targets/x86/64/openwrt-19.07.0-x86-64-generic-rootfs.tar.gz"}
--{"status":"Importing","progressDetail":{"current":1572391,"total":3821714},"progress":"[====================\u003e ] 1.572MB/3.822MB"} --{"status":"Importing","progressDetail":{"current":1572391,"total":3821714},"progress":"[====================\u003e ] 1.572MB/3.822MB"}
--{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"} --{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}
_docker.import_image_show_status_cb = function(res, source) _docker.import_image_show_status_cb = function(res, source)
return status_cb(res, source, function(chunk) return status_cb(res, source, function(chunk)
local json_parse = luci.jsonc.parse local json_parse = luci.jsonc.parse
local step = json_parse(chunk) local step = json_parse(chunk)
if type(step) == "table" then if type(step) == "table" then
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
if num == 0 then buf, num = buf:gsub("\t"..step.status .. " .-\n", str)
buf = buf .. str end
end if num == 0 then
_docker:write_status(buf) buf = buf .. str
end end
end _docker:write_status(buf)
) 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
local ip = require "luci.ip" end
local if_name = "docker_"..name
local dev_name = "macvlan_"..name if uci:get("dockerd", "globals", "remote_endpoint") == "true" then
local net_mask = tostring(ip.new(subnet):mask()) return
local lan_interfaces end
-- add macvlan device
uci:delete("network", dev_name) local ip = require "luci.ip"
uci:set("network", dev_name, "device") local if_name = "docker_"..name
uci:set("network", dev_name, "name", dev_name) local dev_name = "macvlan_"..name
uci:set("network", dev_name, "ifname", device) local net_mask = tostring(ip.new(subnet):mask())
uci:set("network", dev_name, "type", "macvlan") local lan_interfaces
uci:set("network", dev_name, "mode", "bridge")
-- add macvlan interface -- add macvlan device
uci:delete("network", if_name) uci:delete("network", dev_name)
uci:set("network", if_name, "interface") uci:set("network", dev_name, "device")
uci:set("network", if_name, "proto", "static") uci:set("network", dev_name, "name", dev_name)
uci:set("network", if_name, "ifname", dev_name) uci:set("network", dev_name, "ifname", device)
uci:set("network", if_name, "ipaddr", gateway) uci:set("network", dev_name, "type", "macvlan")
uci:set("network", if_name, "netmask", net_mask) uci:set("network", dev_name, "mode", "bridge")
uci:foreach("firewall", "zone", function(s)
if s.name == "lan" then -- add macvlan interface
local interfaces uci:delete("network", if_name)
if type(s.network) == "table" then uci:set("network", if_name, "interface")
interfaces = table.concat(s.network, " ") uci:set("network", if_name, "proto", "static")
uci:delete("firewall", s[".name"], "network") uci:set("network", if_name, "ifname", dev_name)
else uci:set("network", if_name, "ipaddr", gateway)
interfaces = s.network and s.network or "" uci:set("network", if_name, "netmask", net_mask)
end uci:foreach("firewall", "zone", function(s)
interfaces = interfaces .. " " .. if_name if s.name == "lan" then
interfaces = interfaces:gsub("%s+", " ") local interfaces
uci:set("firewall", s[".name"], "network", interfaces) if type(s.network) == "table" then
end interfaces = table.concat(s.network, " ")
end) uci:delete("firewall", s[".name"], "network")
uci:commit("firewall") else
uci:commit("network") interfaces = s.network and s.network or ""
os.execute("ifup " .. if_name) end
interfaces = interfaces .. " " .. if_name
interfaces = interfaces:gsub("%s+", " ")
uci:set("firewall", s[".name"], "network", interfaces)
end
end)
uci:commit("firewall")
uci:commit("network")
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
local if_name = "docker_"..name end
local dev_name = "macvlan_"..name
uci:foreach("firewall", "zone", function(s) if uci:get("dockerd", "globals", "remote_endpoint") == "true" then
if s.name == "lan" then return
local interfaces end
if type(s.network) == "table" then
interfaces = table.concat(s.network, " ") local if_name = "docker_"..name
else local dev_name = "macvlan_"..name
interfaces = s.network and s.network or "" uci:foreach("firewall", "zone", function(s)
end if s.name == "lan" then
interfaces = interfaces and interfaces:gsub(if_name, "") local interfaces
interfaces = interfaces and interfaces:gsub("%s+", " ") if type(s.network) == "table" then
uci:set("firewall", s[".name"], "network", interfaces) interfaces = table.concat(s.network, " ")
end else
end) interfaces = s.network and s.network or ""
uci:commit("firewall") end
uci:delete("network", dev_name) interfaces = interfaces and interfaces:gsub(if_name, "")
uci:delete("network", if_name) interfaces = interfaces and interfaces:gsub("%s+", " ")
uci:commit("network") uci:set("firewall", s[".name"], "network", interfaces)
os.execute("ip link del " .. if_name) end
end)
uci:delete("network", dev_name)
uci:delete("network", if_name)
uci:commit("network")
uci:commit("firewall")
os.execute("ip link del " .. if_name)
end end
return _docker return _docker

View file

@ -1,140 +1,147 @@
<style type="text/css"> <style type="text/css">
#docker_apply_overlay { #docker_apply_overlay {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
display: none; display: none;
z-index: 20000; z-index: 20000;
} }
#docker_apply_overlay .alert-message { #docker_apply_overlay .alert-message {
position: relative; position: relative;
top: 10%; top: 10%;
width: 60%; width: 60%;
margin: auto; margin: auto;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
min-height: 32px; min-height: 32px;
align-items: center; align-items: center;
} }
#docker_apply_overlay .alert-message > h4, #docker_apply_overlay .alert-message > h4,
#docker_apply_overlay .alert-message > p, #docker_apply_overlay .alert-message > p,
#docker_apply_overlay .alert-message > div { #docker_apply_overlay .alert-message > div {
flex-basis: 100%; flex-basis: 100%;
} }
#docker_apply_overlay .alert-message > img { #docker_apply_overlay .alert-message > img {
margin-right: 1em; margin-right: 1em;
flex-basis: 32px; flex-basis: 32px;
} }
body.apply-overlay-active { body.apply-overlay-active {
overflow: hidden; overflow: hidden;
height: 100vh; height: 100vh;
} }
body.apply-overlay-active #docker_apply_overlay { body.apply-overlay-active #docker_apply_overlay {
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)%>,
uci_apply_holdoff = <%=math.max(luci.config and luci.config.apply and luci.config.apply.holdoff or 4, 1)%>, uci_apply_holdoff = <%=math.max(luci.config and luci.config.apply and luci.config.apply.holdoff or 4, 1)%>,
uci_apply_timeout = <%=math.max(luci.config and luci.config.apply and luci.config.apply.timeout or 5, 1)%>, uci_apply_timeout = <%=math.max(luci.config and luci.config.apply and luci.config.apply.timeout or 5, 1)%>,
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');
if (message && type) { if (message && type) {
if (!message.classList.contains(type)) { if (!message.classList.contains(type)) {
message.classList.remove('notice'); message.classList.remove('notice');
message.classList.remove('warning'); message.classList.remove('warning');
message.classList.add(type); message.classList.add(type);
} }
if (content) if (content)
message.innerHTML = content; message.innerHTML = content;
document.body.classList.add('apply-overlay-active'); document.body.classList.add('apply-overlay-active');
document.body.scrollTop = document.documentElement.scrollTop = 0; document.body.scrollTop = document.documentElement.scrollTop = 0;
if (!was_xhr_poll_running) { if (!was_xhr_poll_running) {
was_xhr_poll_running = XHR.running(); was_xhr_poll_running = XHR.running();
XHR.halt(); XHR.halt();
} }
} }
else { else {
document.body.classList.remove('apply-overlay-active'); document.body.classList.remove('apply-overlay-active');
if (was_xhr_poll_running)
XHR.run();
}
}
if (was_xhr_poll_running) var loading_msg="Loading.."
XHR.run(); function uci_confirm_docker() {
} var tt;
} docker_status_message('notice');
var loading_msg="Loading.." var call = function(r, resjson, duration) {
function uci_confirm_docker() { if (r && r.status === 200 ) {
var tt; var indicator = document.querySelector('.uci_change_indicator');
docker_status_message('notice'); if (indicator)
var call = function(r, resjson, duration) { indicator.style.display = 'none';
if (r && r.status === 200 ) { docker_status_message('notice', '<%:Docker actions done.%>');
var indicator = document.querySelector('.uci_change_indicator'); document.body.classList.remove('apply-overlay-active');
if (indicator) indicator.style.display = 'none'; window.clearTimeout(tt);
docker_status_message('notice', '<%:Docker actions done.%>'); return;
document.body.classList.remove('apply-overlay-active'); }
window.clearTimeout(tt); loading_msg = resjson?resjson.info:loading_msg
return; // var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
} var delay =1000
loading_msg = resjson?resjson.info:loading_msg window.setTimeout(function() {
// var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0); xhr.get('<%=url("admin/docker/confirm")%>', null, call, uci_apply_timeout * 1000);
var delay =1000 },delay);
window.setTimeout(function() { };
xhr.get('<%=url("admin/docker/confirm")%>', null, call, uci_apply_timeout * 1000);
}, 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();
/* wait a few seconds for the settings to become effective */
window.setTimeout(call, Math.max(uci_apply_holdoff * 1000 , 1));
}
// document.getElementsByTagName("form")[0].addEventListener("submit", (e)=>{
// uci_confirm_docker()
// })
function fnSubmitForm(el){ tick();
if (el.id != "cbid.table.1._new") { /* wait a few seconds for the settings to become effective */
uci_confirm_docker() window.setTimeout(call, Math.max(uci_apply_holdoff * 1000 , 1));
} }
} // document.getElementsByTagName("form")[0].addEventListener("submit", (e)=>{
// uci_confirm_docker()
// })
<% if self.err then -%> function fnSubmitForm(el){
docker_status_message('warning', '<span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">'+`<%=self.err%>`+'</span>'); if (el.id != "cbid.table.1._new") {
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{ uci_confirm_docker()
docker_status_message() }
}) }
<%- end %>
window.onload= function (){ <% if self.err then -%>
var buttons = document.querySelectorAll('input[type="submit"]'); docker_status_message('warning', '<span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">'+`<%=self.err%>`+'</span>');
[].slice.call(buttons).forEach(function (el) { document.getElementById('docker_apply_overlay').addEventListener(
el.onclick = fnSubmitForm.bind(this, el); "click",
}); (e)=>{
} docker_status_message()
}
)
<%- end %>
window.onload= function (){
var buttons = document.querySelectorAll('input[type="submit"]');
[].slice.call(buttons).forEach(function (el) {
el.onclick = fnSubmitForm.bind(this, el);
});
}
//]]></script> //]]></script>

View file

@ -1,12 +1,12 @@
<br> <br>
<ul class="cbi-tabmenu"> <ul class="cbi-tabmenu">
<li id="cbi-tab-container_info"><a id="a-cbi-tab-container_info" href=""><%:Info%></a></li> <li id="cbi-tab-container_info"><a id="a-cbi-tab-container_info" href=""><%:Info%></a></li>
<li id="cbi-tab-container_resources"><a id="a-cbi-tab-container_resources" href=""><%:Resources%></a></li> <li id="cbi-tab-container_resources"><a id="a-cbi-tab-container_resources" href=""><%:Resources%></a></li>
<li id="cbi-tab-container_stats"><a id="a-cbi-tab-container_stats" href=""><%:Stats%></a></li> <li id="cbi-tab-container_stats"><a id="a-cbi-tab-container_stats" href=""><%:Stats%></a></li>
<li id="cbi-tab-container_file"><a id="a-cbi-tab-container_file" href=""><%:File%></a></li> <li id="cbi-tab-container_file"><a id="a-cbi-tab-container_file" href=""><%:File%></a></li>
<li id="cbi-tab-container_console"><a id="a-cbi-tab-container_console" href=""><%:Console%></a></li> <li id="cbi-tab-container_console"><a id="a-cbi-tab-container_console" href=""><%:Console%></a></li>
<li id="cbi-tab-container_inspect"><a id="a-cbi-tab-container_inspect" href=""><%:Inspect%></a></li> <li id="cbi-tab-container_inspect"><a id="a-cbi-tab-container_inspect" href=""><%:Inspect%></a></li>
<li id="cbi-tab-container_logs"><a id="a-cbi-tab-container_logs" href=""><%:Logs%></a></li> <li id="cbi-tab-container_logs"><a id="a-cbi-tab-container_logs" href=""><%:Logs%></a></li>
</ul> </ul>
<script type="text/javascript"> <script type="text/javascript">
@ -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,6 +1,6 @@
<div class="cbi-map"> <div class="cbi-map">
<iframe id="terminal" style="width: 100%; min-height: 500px; border: none; border-radius: 3px;"></iframe> <iframe id="terminal" style="width: 100%; min-height: 500px; border: none; border-radius: 3px;"></iframe>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
document.getElementById("terminal").src = "http://" + window.location.hostname + ":7682"; document.getElementById("terminal").src = "http://" + window.location.hostname + ":7682";
</script> </script>

View file

@ -1,63 +1,73 @@
<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">
<input type="file" name="upload_archive" accept="application/x-tar" id="upload_archive" /> <input type="file" name="upload_archive" accept="application/x-tar" id="upload_archive" />
</div> </div>
<br> <br>
<label class="cbi-value-title" for="path"><%:Path%></label> <label class="cbi-value-title" for="path"><%:Path%></label>
<div class="cbi-value-field"> <div class="cbi-value-field">
<input type="text" class="cbi-input-text" name="path" value="/tmp/" id="path" /> <input type="text" class="cbi-input-text" name="path" value="/tmp/" id="path" />
</div> </div>
<br> <br>
<div class="cbi-value-field"> <div class="cbi-value-field">
<input type="button"" class="btn cbi-button cbi-button-action important" id="upload" name="upload" value="<%:Upload%>" /> <input type="button"" class="btn cbi-button cbi-button-action important" id="upload" name="upload" value="<%:Upload%>" />
<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) {
let uploadArchive = document.getElementById('upload_archive') let uploadArchive = document.getElementById('upload_archive')
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(
docker_status_message() "click",
}) (e)=>{
return docker_status_message()
} }
let fileName = uploadArchive.files[0].name )
let formData = new FormData() return
formData.append('upload-filename', fileName) }
formData.append('upload-path', uploadPath) let fileName = uploadArchive.files[0].name
formData.append('upload-archive', uploadArchive.files[0]) let formData = new FormData()
let xhr = new XMLHttpRequest() formData.append('upload-filename', fileName)
xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/container_put_archive")%>/<%=self.container%>', true) formData.append('upload-path', uploadPath)
xhr.onload = function() { formData.append('upload-archive', uploadArchive.files[0])
if (xhr.status == 200) { let xhr = new XMLHttpRequest()
uploadArchive.value = '' xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/container_put_archive")%>/<%=self.container%>', true)
docker_status_message('notice', "<%:Upload Success%>") xhr.onload = function() {
} if (xhr.status == 200) {
else { uploadArchive.value = ''
docker_status_message('warning', "<%:Upload Error%>:" + xhr.statusText) docker_status_message('notice', "<%:Upload Success%>")
} }
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{ else {
docker_status_message() docker_status_message('warning', "<%:Upload Error%>:" + xhr.statusText)
}) }
} document.getElementById('docker_apply_overlay').addEventListener(
xhr.send(formData) "click",
} (e)=>{
let btnDownload = document.getElementById('download') docker_status_message()
btnDownload.onclick = function (e) { }
let downloadPath = document.getElementById('path').value )
if (!downloadPath) { }
docker_status_message('warning', "<%:Please input the PATH !%>") xhr.send(formData)
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{ }
docker_status_message()
}) let btnDownload = document.getElementById('download')
return btnDownload.onclick = function (e) {
} let downloadPath = document.getElementById('path').value
window.open('<%=luci.dispatcher.build_url("admin/docker/container_get_archive")%>?id=<%=self.container%>&path=' + encodeURIComponent(downloadPath)) if (!downloadPath) {
} docker_status_message('warning', "<%:Please input the PATH !%>")
document.getElementById('docker_apply_overlay').addEventListener(
"click",
(e)=>{
docker_status_message()
}
)
return
}
window.open('<%=luci.dispatcher.build_url("admin/docker/container_get_archive")%>?id=<%=self.container%>&path=' + encodeURIComponent(downloadPath))
}
</script> </script>

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

@ -1,88 +1,104 @@
<input type="text" class="cbi-input-text" name="isrc" placeholder="http://host/image.tar" id="isrc" /> <input type="text" class="cbi-input-text" name="isrc" placeholder="http://host/image.tar" id="isrc" />
<input type="text" class="cbi-input-text" name="itag" placeholder="repository:tag" id="itag" /> <input type="text" class="cbi-input-text" name="itag" placeholder="repository:tag" id="itag" />
<div style="display: inline-block;"> <div style="display: inline-block;">
<input type="button"" class="btn cbi-button cbi-button-add" id="btnimport" name="import" value="<%:Import%>" /> <input type="button"" class="btn cbi-button cbi-button-add" id="btnimport" name="import" value="<%:Import%>" />
<input type="file" id="file_import" style="visibility:hidden; position: absolute;top: 0px; left: 0px;" /> <input type="file" id="file_import" style="visibility:hidden; position: absolute;top: 0px; left: 0px;" />
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
let btnImport = document.getElementById('btnimport') let btnImport = document.getElementById('btnimport')
let valISrc = document.getElementById('isrc') let valISrc = document.getElementById('isrc')
let valITag = document.getElementById('itag') let valITag = document.getElementById('itag')
btnImport.onclick = function (e) { btnImport.onclick = function (e) {
if (valISrc.value == "") { if (valISrc.value == "") {
document.getElementById("file_import").click() document.getElementById("file_import").click()
return return
} else { }
let formData = new FormData() else {
formData.append('src', valISrc.value) let formData = new FormData()
formData.append('tag', valITag.value) formData.append('src', valISrc.value)
let xhr = new XMLHttpRequest() formData.append('tag', valITag.value)
uci_confirm_docker() let xhr = new XMLHttpRequest()
xhr.open("POST", "<%=luci.dispatcher.build_url('admin/docker/images_import')%>", true) uci_confirm_docker()
xhr.onload = function () { xhr.open("POST", "<%=luci.dispatcher.build_url('admin/docker/images_import')%>", true)
location.reload() xhr.onload = function () {
} location.reload()
xhr.send(formData) }
} xhr.send(formData)
} }
let fileimport = document.getElementById('file_import') }
fileimport.onchange = function (e) {
let fileimport = document.getElementById('file_import')
if (!fileimport.value) {
return
}
let valITag = document.getElementById('itag')
let fileName = fileimport.files[0].name
let formData = new FormData()
formData.append('upload-filename', fileName)
formData.append('tag', valITag.value)
formData.append('upload-archive', fileimport.files[0])
let xhr = new XMLHttpRequest()
uci_confirm_docker()
xhr.open("POST", "<%=luci.dispatcher.build_url('admin/docker/images_import')%>", true)
xhr.onload = function () {
fileimport.value = ''
location.reload()
}
xhr.send(formData)
}
let new_tag = function (image_id) { let fileimport = document.getElementById('file_import')
let new_tag = prompt("<%:New tag%>\n<%:Image%>" + "ID: " + image_id + "\n<%:Please input new tag%>:", "") fileimport.onchange = function (e) {
if (new_tag) { let fileimport = document.getElementById('file_import')
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_tag')%>", if (!fileimport.value) {
{ id: image_id, tag: new_tag }, return
function (r) { }
if (r.status == 201) { let valITag = document.getElementById('itag')
location.reload() let fileName = fileimport.files[0].name
} let formData = new FormData()
else { formData.append('upload-filename', fileName)
docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText); formData.append('tag', valITag.value)
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{ formData.append('upload-archive', fileimport.files[0])
docker_status_message() let xhr = new XMLHttpRequest()
}) uci_confirm_docker()
} xhr.open("POST", "<%=luci.dispatcher.build_url('admin/docker/images_import')%>", true)
}) xhr.onload = function () {
} fileimport.value = ''
} location.reload()
}
xhr.send(formData)
}
let un_tag = function (tag) { let new_tag = function (image_id) {
if (tag.match("<none>")) return let new_tag = prompt("<%:New tag%>\n<%:Image%>" + "ID: " + image_id + "\n<%:Please input new tag%>:", "")
if (confirm("<%:Remove tag%>: " + tag + " ?")) { if (new_tag) {
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_untag')%>", (new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_tag')%>",
{ tag: tag }, {
function (r) { id: image_id,
if (r.status == 200) { tag: new_tag
location.reload() },
} function (r) {
else { if (r.status == 201) {
docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText); location.reload()
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{ }
docker_status_message() else {
}) docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText);
} document.getElementById('docker_apply_overlay').addEventListener(
}) "click",
} (e)=>{
} docker_status_message()
}
)
}
}
)
}
}
let un_tag = function (tag) {
if (tag.match("<none>"))
return
if (confirm("<%:Remove tag%>: " + tag + " ?")) {
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_untag')%>",
{
tag: tag
},
function (r) {
if (r.status == 200) {
location.reload()
}
else {
docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText);
document.getElementById('docker_apply_overlay').addEventListener(
"click",
(e)=>{
docker_status_message()
}
)
}
}
)
}
}
</script> </script>

View file

@ -1,29 +1,30 @@
<div style="display: inline-block;"> <div style="display: inline-block;">
<input type="button"" class="btn cbi-button cbi-button-add" id="btnload" name="load" value="<%:Load%>" /> <input type="button"" class="btn cbi-button cbi-button-add" id="btnload" name="load" value="<%:Load%>" />
<input type="file" id="file_load" style="visibility:hidden; position: absolute;top: 0px; left: 0px;" accept="application/x-tar" /> <input type="file" id="file_load" style="visibility:hidden; position: absolute;top: 0px; left: 0px;" accept="application/x-tar" />
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
let btnLoad = document.getElementById('btnload') let btnLoad = document.getElementById('btnload')
btnLoad.onclick = function (e) { btnLoad.onclick = function (e) {
document.getElementById("file_load").click() document.getElementById("file_load").click()
e.preventDefault() e.preventDefault()
} }
let fileLoad = document.getElementById('file_load')
fileLoad.onchange = function(e){ let fileLoad = document.getElementById('file_load')
let fileLoad = document.getElementById('file_load') fileLoad.onchange = function(e){
if (!fileLoad.value) { let fileLoad = document.getElementById('file_load')
return if (!fileLoad.value) {
} return
let fileName = fileLoad.files[0].name }
let formData = new FormData() let fileName = fileLoad.files[0].name
formData.append('upload-filename', fileName) let formData = new FormData()
formData.append('upload-archive', fileLoad.files[0]) formData.append('upload-filename', fileName)
let xhr = new XMLHttpRequest() formData.append('upload-archive', fileLoad.files[0])
uci_confirm_docker() let xhr = new XMLHttpRequest()
xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/images_load")%>', true) uci_confirm_docker()
xhr.onload = function() { xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/images_load")%>', true)
location.reload() xhr.onload = function() {
} location.reload()
xhr.send(formData) }
} xhr.send(formData)
}
</script> </script>

View file

@ -1,95 +1,102 @@
<style type="text/css"> <style type="text/css">
#dialog_reslov { #dialog_reslov {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
display: none; display: none;
z-index: 20000; z-index: 20000;
} }
#dialog_reslov .dialog_box { #dialog_reslov .dialog_box {
position: relative; position: relative;
background: rgba(255, 255, 255); background: rgba(255, 255, 255);
top: 10%; top: 10%;
width: 50%; width: 50%;
margin: auto; margin: auto;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
height:auto; height:auto;
align-items: center; align-items: center;
} }
#dialog_reslov .dialog_line { #dialog_reslov .dialog_line {
margin-top: .5em; margin-top: .5em;
margin-bottom: .5em; margin-bottom: .5em;
margin-left: 2em; margin-left: 2em;
margin-right: 2em; margin-right: 2em;
} }
#dialog_reslov .dialog_box>h4, #dialog_reslov .dialog_box>h4,
#dialog_reslov .dialog_box>p, #dialog_reslov .dialog_box>p,
#dialog_reslov .dialog_box>div { #dialog_reslov .dialog_box>div {
flex-basis: 100%; flex-basis: 100%;
} }
#dialog_reslov .dialog_box>img { #dialog_reslov .dialog_box>img {
margin-right: 1em; margin-right: 1em;
flex-basis: 32px; flex-basis: 32px;
} }
body.dialog-reslov-active { body.dialog-reslov-active {
overflow: hidden; overflow: hidden;
height: 100vh; height: 100vh;
} }
body.dialog-reslov-active #dialog_reslov { body.dialog-reslov-active #dialog_reslov {
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')
document.documentElement.style.overflowY = 'scroll' document.documentElement.style.overflowY = 'scroll'
} }
function reslov_container() { function reslov_container() {
let s = document.getElementById('cmd-line-status') let s = document.getElementById('cmd-line-status')
if (!s) return
let cmd_line = document.getElementById("dialog_reslov_text").value;
if (cmd_line == null || cmd_line == "") {
return
}
cmd_line = cmd_line.replace(/(^\s*)/g,"")
if (!cmd_line.match(/^docker\s+(run|create)/)) {
s.innerHTML = "<font color='red'><%:Command line Error%></font>"
return
}
let reg_space = /\s+/g
let reg_muti_line= /\\\s*\n/g
// reg_rem =/(?<!\\)`#.+(?<!\\)`/g // the command has `# `
let reg_rem =/`#.+`/g// the command has `# `
cmd_line = cmd_line.replace(/^docker\s+(run|create)/,"DOCKERCLI").replace(reg_rem, " ").replace(reg_muti_line, " ").replace(reg_space, " ")
console.log(cmd_line)
window.location.href = '<%=luci.dispatcher.build_url("admin/docker/newcontainer")%>/' + encodeURI(cmd_line)
}
function clear_text(){ if (!s)
let s = document.getElementById('cmd-line-status') return
s.innerHTML = ""
}
function show_reslov_dialog() { let cmd_line = document.getElementById("dialog_reslov_text").value;
document.getElementById('dialog_reslov') || document.body.insertAdjacentHTML("beforeend", '<div id="dialog_reslov"><div class="dialog_box"><div class="dialog_line"></div><div class="dialog_line"><span><%:Plese input <docker create/run> command line:%></span><br><span id="cmd-line-status"></span></div><div class="dialog_line"><textarea class="cbi-input-textarea" id="dialog_reslov_text" style="width: 100%; height:100%;" rows="15" onkeyup="clear_text()"></textarea></div><div class="dialog_line" style="text-align: right;"><input type="button" class="btn cbi-button cbi-button-apply" type="submit" value="<%:Submit%>" onclick="reslov_container()" /> <input type="button" class="btn cbi-button cbi-button-reset" type="reset" value="<%:Cancel%>" onclick="close_reslov_dialog()" /></div><div class="dialog_line"></div></div></div>') if (cmd_line == null || cmd_line == "") {
document.body.classList.add('dialog-reslov-active') return
let s = document.getElementById('cmd-line-status') }
s.innerHTML = ""
document.documentElement.style.overflowY = 'hidden' cmd_line = cmd_line.replace(/(^\s*)/g,"")
} if (!cmd_line.match(/^docker\s+(run|create)/)) {
s.innerHTML = "<font color='red'><%:Command line Error%></font>"
return
}
let reg_space = /\s+/g
let reg_muti_line= /\\\s*\n/g
// reg_rem =/(?<!\\)`#.+(?<!\\)`/g // the command has `# `
let reg_rem =/`#.+`/g// the command has `# `
cmd_line = cmd_line.replace(/^docker\s+(run|create)/,"DOCKERCLI").replace(reg_rem, " ").replace(reg_muti_line, " ").replace(reg_space, " ")
console.log(cmd_line)
window.location.href = '<%=luci.dispatcher.build_url("admin/docker/newcontainer")%>/' + encodeURI(cmd_line)
}
function clear_text(){
let s = document.getElementById('cmd-line-status')
s.innerHTML = ""
}
function show_reslov_dialog() {
document.getElementById('dialog_reslov') || document.body.insertAdjacentHTML("beforeend", '<div id="dialog_reslov"><div class="dialog_box"><div class="dialog_line"></div><div class="dialog_line"><span><%:Plese input <docker create/run> command line:%></span><br><span id="cmd-line-status"></span></div><div class="dialog_line"><textarea class="cbi-input-textarea" id="dialog_reslov_text" style="width: 100%; height:100%;" rows="15" onkeyup="clear_text()"></textarea></div><div class="dialog_line" style="text-align: right;"><input type="button" class="btn cbi-button cbi-button-apply" type="submit" value="<%:Submit%>" onclick="reslov_container()" /> <input type="button" class="btn cbi-button cbi-button-reset" type="reset" value="<%:Cancel%>" onclick="close_reslov_dialog()" /></div><div class="dialog_line"></div></div></div>')
document.body.classList.add('dialog-reslov-active')
let s = document.getElementById('cmd-line-status')
s.innerHTML = ""
document.documentElement.style.overflowY = 'hidden'
}
</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,280 +1,197 @@
<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;
font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif; font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
-webkit-box-orient: horizontal; -webkit-box-orient: horizontal;
-webkit-box-direction: normal; -webkit-box-direction: normal;
-webkit-flex-flow: row wrap; -webkit-flex-flow: row wrap;
-ms-flex-flow: row wrap; -ms-flex-flow: row wrap;
flex-flow: row wrap; flex-flow: row wrap;
-webkit-align-content: flex-start; -webkit-align-content: flex-start;
-ms-flex-line-pack: start; -ms-flex-line-pack: start;
align-content: flex-start align-content: flex-start
} }
.pure-u { .pure-u {
display: inline-block; display: inline-block;
zoom: 1; zoom: 1;
letter-spacing: normal; letter-spacing: normal;
word-spacing: normal; word-spacing: normal;
vertical-align: top; vertical-align: top;
text-rendering: auto text-rendering: auto
} }
.pure-g [class*=pure-u] { .pure-g [class*=pure-u] {
font-family: sans-serif font-family: sans-serif
} }
.pure-u-1-4, .pure-u-1-4,
.pure-u-2-5, .pure-u-2-5,
.pure-u-3-5 { .pure-u-3-5 {
display: inline-block; display: inline-block;
zoom: 1; zoom: 1;
letter-spacing: normal; letter-spacing: normal;
word-spacing: normal; word-spacing: normal;
vertical-align: top; vertical-align: top;
text-rendering: auto text-rendering: auto
} }
.pure-u-1-4 { .pure-u-1-4 {
width: 25% width: 25%
} }
.pure-u-2-5 { .pure-u-2-5 {
width: 40% width: 40%
} }
.pure-u-3-5 { .pure-u-3-5 {
width: 60% width: 60%
} }
.status { .status {
margin: 1rem -0.5rem 1rem -0.5rem; margin: 1rem -0.5rem 1rem -0.5rem;
} }
.block { .block {
margin: 0.5rem 0.5rem; margin: 0.5rem 0.5rem;
padding: 0; padding: 0;
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
line-height: 1; line-height: 1;
font-family: inherit; font-family: inherit;
min-width: inherit; min-width: inherit;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
border: 1px solid rgba(0, 0, 0, .05); border: 1px solid rgba(0, 0, 0, .05);
border-radius: .375rem; border-radius: .375rem;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .15); box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .15);
} }
.img-con { .img-con {
margin: 1rem; margin: 1rem;
min-width: 4rem; min-width: 4rem;
max-width: 4rem; max-width: 4rem;
min-height: 4rem; min-height: 4rem;
max-height: 4rem; max-height: 4rem;
} }
.block h4 { .block h4 {
font-size: .8125rem; font-size: .8125rem;
font-weight: 600; font-weight: 600;
margin: 1rem; margin: 1rem;
color: #8898aa !important; color: #8898aa !important;
line-height: 1.8em; line-height: 1.8em;
} }
.cbi-section-table-cell { .cbi-section-table-cell {
position: relative; position: relative;
} }
@media screen and (max-width: 700px) { @media screen and (max-width: 700px) {
.pure-u-1-4 { .pure-u-1-4 {
width: 50%; width: 50%;
} }
.cbi-button-add { .cbi-button-add {
position: fixed; position: fixed;
padding: 0.3rem 0.5rem; padding: 0.3rem 0.5rem;
z-index: 1000; z-index: 1000;
width: 50px !important; width: 50px !important;
height: 50px; height: 50px;
bottom: 90px; bottom: 90px;
right: 5px; right: 5px;
font-size: 16px; font-size: 16px;
border-radius: 50%; border-radius: 50%;
display: block; display: block;
background-color: #fb6340 !important; background-color: #fb6340 !important;
border-color: #fb6340 !important; border-color: #fb6340 !important;
box-shadow: 0 0 1rem 0 rgba(136, 152, 170, .75); box-shadow: 0 0 1rem 0 rgba(136, 152, 170, .75);
} }
} }
</style> </style>
<div class="pure-g status"> <div class="pure-g status">
<div class="pure-u-1-4"> <div class="pure-u-1-4">
<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> </div>
<path </div>
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" /> <div class="pure-u-3-5">
</svg> <h4 style="text-align: right; font-size: 1rem"><%:Containers%></h4>
</div> <h4 style="text-align: right;">
</div> <%- if self.containers_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/containers")%>'><%- end -%>
<div class="pure-u-3-5"> <span style="font-size: 2rem; color: #2dce89;"><%=self.containers_running%></span>
<h4 style="text-align: right; font-size: 1rem"><%:Containers%></h4> <span style="font-size: 1rem; color: #8898aa !important;">/<%=self.containers_total%></span>
<h4 style="text-align: right;"> <%- if self.containers_total ~= "-" then -%></a><%- end -%>
<%- if self.containers_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/containers")%>'><%- end -%> </h4>
<span style="font-size: 2rem; color: #2dce89;"><%=self.containers_running%></span> </div>
<span style="font-size: 1rem; color: #8898aa !important;">/<%=self.containers_total%></span> </div>
<%- if self.containers_total ~= "-" then -%></a><%- end -%> </div>
</h4> <div class="pure-u-1-4">
</div> <div class="block pure-g">
</div> <div class="pure-u-2-5">
</div> <div class="img-con">
<div class="pure-u-1-4"> <img src="<%=resource%>/dockerman/images.svg" />
<div class="block pure-g"> </div>
<div class="pure-u-2-5"> </div>
<div class="img-con"> <div class="pure-u-3-5">
<svg id="icon-hub" viewBox="0 -4 42 50" stroke-width="2" fill-rule="nonzero" width="100%" height="100%"> <h4 style="text-align: right; font-size: 1rem"><%:Images%></h4>
<path <h4 style="text-align: right;">
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" <%- if self.images_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/images")%>'><%- end -%>
id="Fill-1"></path> <span style="font-size: 2rem; color: #2dce89;"><%=self.images_used%></span>
<rect id="Rectangle-3" x="14" y="26" width="6" height="10"></rect> <span style="font-size: 1rem; color: #8898aa !important;">/<%=self.images_total%></span>
<path d="M20,26 L20,36 L26,36 L26,26 L20,26 Z" id="Rectangle-3"></path> <%- if self.images_total ~= "-" then -%></a><%- end -%>
</svg> </h4>
</div> </div>
</div> </div>
<div class="pure-u-3-5"> </div>
<h4 style="text-align: right; font-size: 1rem"><%:Images%></h4> <div class="pure-u-1-4">
<h4 style="text-align: right;"> <div class="block pure-g">
<%- if self.images_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/images")%>'><%- end -%> <div class="pure-u-2-5">
<span style="font-size: 2rem; color: #2dce89;"><%=self.images_used%></span> <div class="img-con">
<span style="font-size: 1rem; color: #8898aa !important;">/<%=self.images_total%></span> <img src="<%=resource%>/dockerman/networks.svg" />
<%- if self.images_total ~= "-" then -%></a><%- end -%> </div>
</h4> </div>
</div> <div class="pure-u-3-5">
</div> <h4 style="text-align: right; font-size: 1rem"><%:Networks%></h4>
</div> <h4 style="text-align: right;">
<div class="pure-u-1-4"> <%- if self.networks_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/networks")%>'><%- end -%>
<div class="block pure-g"> <span style="font-size: 2rem; color: #2dce89;"><%=self.networks_total%></span>
<div class="pure-u-2-5"> <!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
<div class="img-con"> <%- if self.networks_total ~= "-" then -%></a><%- end -%>
<svg version="1.1" x="0px" y="0px" width="100%" height="100%" viewBox="0 0 48.723 48.723" xml:space="preserve"> </h4>
<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 </div>
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 </div>
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 </div>
h-0.678v2.652h-7.846v-1.104H7.452v1.104H1.114v1.316h6.338V24.152z" /> <div class="pure-u-1-4">
<path <div class="block pure-g">
d="M21.484,16.849h5.204v-2.611h7.133V1.555H14.588v12.683h6.896V16.849z M16.537,12.288V3.505h15.335v8.783H16.537z" /> <div class="pure-u-2-5">
<rect x="18.682" y="16.898" width="10.809" height="0.537" /> <div class="img-con">
<path <img src="<%=resource%>/dockerman/volumes.svg" />
d="M0,43.971h6.896v2.611H12.1v-2.611h7.134V31.287H0V43.971z M1.95,33.236h15.334v8.785H1.95V33.236z" /> </div>
<rect x="4.095" y="46.631" width="10.808" height="0.537" /> </div>
<path <div class="pure-u-3-5">
d="M29.491,30.994v12.684h6.895v2.611h5.205v-2.611h7.133V30.994H29.491z M46.774,41.729H31.44v-8.783h15.334V41.729z" /> <h4 style="text-align: right; font-size: 1rem"><%:Volumes%></h4>
<rect x="33.584" y="46.338" width="10.809" height="0.537" /> <h4 style="text-align: right;">
</svg> <%- if self.volumes_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/volumes")%>'><%- end -%>
</div> <span style="font-size: 2rem; color: #2dce89;"><%=self.volumes_total%></span>
</div> <!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
<div class="pure-u-3-5"> <%- if self.volumes_total ~= "-" then -%></a><%- end -%>
<h4 style="text-align: right; font-size: 1rem"><%:Networks%></h4> </h4>
<h4 style="text-align: right;"> </div>
<%- if self.networks_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/networks")%>'><%- end -%> </div>
<span style="font-size: 2rem; color: #2dce89;"><%=self.networks_total%></span> </div>
<!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
<%- if self.networks_total ~= "-" then -%></a><%- end -%>
</h4>
</div>
</div>
</div>
<div class="pure-u-1-4">
<div class="block pure-g">
<div class="pure-u-2-5">
<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">
<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 class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Volumes%></h4>
<h4 style="text-align: right;">
<%- if self.volumes_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/volumes")%>'><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.volumes_total%></span>
<!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
<%- if self.volumes_total ~= "-" then -%></a><%- end -%>
</h4>
</div>
</div>
</div>
</div> </div>

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"
@ -14,301 +15,376 @@ local json_stringify = jsonc.stringify
local json_parse = jsonc.parse local json_parse = jsonc.parse
local chunksource = function(sock, buffer) local chunksource = function(sock, buffer)
buffer = buffer or "" buffer = buffer or ""
return function()
local output return function()
local _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n") local output
if not count then local _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n")
local newblock, code = sock:recv(1024)
if not newblock then return nil, code end if not count then
buffer = buffer .. newblock local newblock, code = sock:recv(1024)
_, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n") if not newblock then
end return nil, code
count = tonumber(count, 16) end
if not count then buffer = buffer .. newblock
return nil, -1, "invalid encoding" _, endp, count = buffer:find("^([0-9a-fA-F]+)\r\n")
elseif count == 0 then -- finial end
return nil
elseif count <= #buffer - endp then count = tonumber(count, 16)
--data >= count if not count then
output = buffer:sub(endp + 1, endp + count) return nil, -1, "invalid encoding"
if count == #buffer - endp then -- [data] elseif count == 0 then -- finial
buffer = buffer:sub(endp + count + 1) return nil
count, code = sock:recvall(2) --read \r\n elseif count <= #buffer - endp then -- data >= count
if not count then return nil, code end output = buffer:sub(endp + 1, endp + count)
elseif count + 1 == #buffer - endp then -- [data]\r if count == #buffer - endp then -- [data]
buffer = buffer:sub(endp + count + 2) buffer = buffer:sub(endp + count + 1)
count, code = sock:recvall(1) --read \n count, code = sock:recvall(2) --read \r\n
if not count then return nil, code end if not count then
else -- [data]\r\n[count]\r\n[data]... return nil, code
buffer = buffer:sub(endp + count + 3) -- cut buffer end
end elseif count + 1 == #buffer - endp then -- [data]\r
return output buffer = buffer:sub(endp + count + 2)
else count, code = sock:recvall(1) --read \n
-- data < count if not count then
output = buffer:sub(endp + 1, endp + count) return nil, code
buffer = buffer:sub(endp + count + 1) end
local remain, code = sock:recvall(count - #output) --need read remaining else -- [data]\r\n[count]\r\n[data]...
if not remain then return nil, code end buffer = buffer:sub(endp + count + 3) -- cut buffer
output = output .. remain end
count, code = sock:recvall(2) --read \r\n return output
if not count then return nil, code end else -- data < count
return output output = buffer:sub(endp + 1, endp + count)
end buffer = buffer:sub(endp + count + 1)
end local remain, code = sock:recvall(count - #output) --need read remaining
if not remain then
return nil, code
end
output = output .. remain
count, code = sock:recvall(2) --read \r\n
if not count then
return nil, code
end
return output
end
end
end end
local chunksink = function (sock) local chunksink = function (sock)
return function(chunk, err) return function(chunk, err)
if not chunk then if not chunk then
return sock:writeall("0\r\n\r\n") return sock:writeall("0\r\n\r\n")
else else
return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, tostring(chunk))) return sock:writeall(("%X\r\n%s\r\n"):format(#chunk, tostring(chunk)))
end end
end end
end end
local docker_stream_filter = function(buffer) local docker_stream_filter = function(buffer)
buffer = buffer or "" buffer = buffer or ""
if #buffer < 8 then if #buffer < 8 then
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
if req_options.socket_path then return socket
socket = nixio.socket("unix", "stream") end
if socket:connect(req_options.socket_path) ~= true then return nil end if req_options.socket_path then
elseif req_options.host and req_options.port then socket = nixio.socket("unix", "stream")
socket = nixio.connect(req_options.host, req_options.port) if socket:connect(req_options.socket_path) ~= true then
end return nil
if socket then end
return socket elseif req_options.host and req_options.port then
else socket = nixio.connect(req_options.host, req_options.port)
return nil end
end if socket then
return socket
else
return nil
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",
end protocol="HTTP/1.1"
},
body={
message="can\'t send data to socket"
}
}
end
if req_body and type(req_body) == "function" and req_header and req_header:match("chunked") then if req_body and type(req_body) == "function" and req_header and req_header:match("chunked") then
-- chunked send -- chunked send
req_body(chunksink(docker_socket)) req_body(chunksink(docker_socket))
elseif req_body and type(req_body) == "function" then elseif req_body and type(req_body) == "function" then
-- normal send by req_body function -- normal send by req_body function
req_body(docker_socket) req_body(docker_socket)
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
elseif req_body then io.popen("echo '".. json_stringify(req_body) .. "' >> " .. options.debug_path)
docker_socket:send(req_body) end
if options.debug then io.popen("echo '".. req_body .. "' >> " .. options.debug_path) end elseif req_body then
end docker_socket:send(req_body)
if options.debug then
io.popen("echo '".. req_body .. "' >> " .. options.debug_path)
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",
end protocol="HTTP/1.1"
local response = {code = 0, headers = {}, body = {}} },
body = {
message="no data receive from socket"
}
}
end
local p, code, msg = line:match("^([%w./]+) ([0-9]+) (.*)") local response = {
response.protocol = p code = 0,
response.code = tonumber(code) headers = {},
response.message = msg body = {}
line = linesrc() }
while line and line ~= "" do
local key, val = line:match("^([%w-]+)%s?:%s?(.*)") local p, code, msg = line:match("^([%w./]+) ([0-9]+) (.*)")
if key and key ~= "Status" then response.protocol = p
if type(response.headers[key]) == "string" then response.code = tonumber(code)
response.headers[key] = {response.headers[key], val} response.message = msg
elseif type(response.headers[key]) == "table" then line = linesrc()
response.headers[key][#response.headers[key] + 1] = val
else while line and line ~= "" do
response.headers[key] = val local key, val = line:match("^([%w-]+)%s?:%s?(.*)")
end if key and key ~= "Status" then
end if type(response.headers[key]) == "string" then
line = linesrc() response.headers[key] = {
end response.headers[key],
-- handle response body val
local body_buffer = linesrc(true) }
response.body = {} elseif type(response.headers[key]) == "table" then
if type(callback) ~= "function" then response.headers[key][#response.headers[key] + 1] = val
if response.headers["Transfer-Encoding"] == "chunked" then else
local source = chunksource(docker_socket, body_buffer) response.headers[key] = val
code = ltn12.pump.all(source, (ltn12.sink.table(response.body))) and response.code or 555 end
response.code = code end
else line = linesrc()
local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource()) end
code = ltn12.pump.all(body_source, (ltn12.sink.table(response.body))) and response.code or 555
response.code = code -- handle response body
end local body_buffer = linesrc(true)
else response.body = {}
if response.headers["Transfer-Encoding"] == "chunked" then
local source = chunksource(docker_socket, body_buffer) if type(callback) ~= "function" then
callback(response, source) if response.headers["Transfer-Encoding"] == "chunked" then
else local source = chunksource(docker_socket, body_buffer)
local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource()) code = ltn12.pump.all(source, (ltn12.sink.table(response.body))) and response.code or 555
callback(response, body_source) response.code = code
end else
end local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource())
docker_socket:close() code = ltn12.pump.all(body_source, (ltn12.sink.table(response.body))) and response.code or 555
return response response.code = code
end
else
if response.headers["Transfer-Encoding"] == "chunked" then
local source = chunksource(docker_socket, body_buffer)
callback(response, source)
else
local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource())
callback(response, body_source)
end
end
docker_socket:close()
return response
end end
local gen_header = function(options, http_method, api_group, api_action, name_or_id, request) local gen_header = function(options, http_method, api_group, api_action, name_or_id, request)
local header, query, path local header, query, path
name_or_id = (name_or_id ~= "") and name_or_id or nil name_or_id = (name_or_id ~= "") and name_or_id or nil
if request and type(request.query) == "table" then if request and type(request.query) == "table" then
local k, v local k, v
for k, v in pairs(request.query) do for k, v in pairs(request.query) do
if type(v) == "table" then if type(v) == "table" then
query = (query and query .. "&" or "?") .. k .. "=" .. urlencode(json_stringify(v)) query = (query and query .. "&" or "?") .. k .. "=" .. urlencode(json_stringify(v))
elseif type(v) == "boolean" then elseif type(v) == "boolean" then
query = (query and query .. "&" or "?") .. k .. "=" .. (v and "true" or "false") query = (query and query .. "&" or "?") .. k .. "=" .. (v and "true" or "false")
elseif type(v) == "number" or type(v) == "string" then elseif type(v) == "number" or type(v) == "string" then
query = (query and query .. "&" or "?") .. k .. "=" .. v query = (query and query .. "&" or "?") .. k .. "=" .. v
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 "")
header = (http_method or "GET") .. " " .. path .. " " .. options.protocol .. "\r\n"
header = header .. "Host: " .. options.host .. "\r\n"
header = header .. "User-Agent: " .. options.user_agent .. "\r\n"
header = header .. "Connection: close\r\n"
if request and type(request.header) == "table" then path = (api_group and ("/" .. api_group) or "") .. (name_or_id and ("/" .. name_or_id) or "") .. (api_action and ("/" .. api_action) or "") .. (query or "")
local k, v header = (http_method or "GET") .. " " .. path .. " " .. options.protocol .. "\r\n"
for k, v in pairs(request.header) do header = header .. "Host: " .. options.host .. "\r\n"
header = header .. k .. ": " .. v .. "\r\n" header = header .. "User-Agent: " .. options.user_agent .. "\r\n"
end header = header .. "Connection: close\r\n"
end
-- when requst_body is function, we need to custom header using custom header if request and type(request.header) == "table" then
if request and request.body and type(request.body) == "function" then local k, v
if not header:match("Content-Length:") then for k, v in pairs(request.header) do
header = header .. "Transfer-Encoding: chunked\r\n" header = header .. k .. ": " .. v .. "\r\n"
end end
elseif http_method == "POST" and request and request.body and type(request.body) == "table" then end
local conetnt_json = json_stringify(request.body)
header = header .. "Content-Type: application/json\r\n" -- when requst_body is function, we need to custom header using custom header
header = header .. "Content-Length: " .. #conetnt_json .. "\r\n" if request and request.body and type(request.body) == "function" then
elseif request and request.body and type(request.body) == "string" then if not header:match("Content-Length:") then
header = header .. "Content-Length: " .. #request.body .. "\r\n" header = header .. "Transfer-Encoding: chunked\r\n"
end end
header = header .. "\r\n" elseif http_method == "POST" and request and request.body and type(request.body) == "table" then
if options.debug then io.popen("echo '".. header .. "' >> " .. options.debug_path) end local conetnt_json = json_stringify(request.body)
return header header = header .. "Content-Type: application/json\r\n"
header = header .. "Content-Length: " .. #conetnt_json .. "\r\n"
elseif request and request.body and type(request.body) == "string" then
header = header .. "Content-Length: " .. #request.body .. "\r\n"
end
header = header .. "\r\n"
if options.debug then
io.popen("echo '".. header .. "' >> " .. options.debug_path)
end
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,
local req_body = request and request.body or nil http_method,
local docker_socket = open_socket(req_options) api_group,
api_action,
name_or_id,
request)
if docker_socket then local req_body = request and request.body or nil
return send_http_socket(docker_socket, req_header, req_body, callback) local docker_socket = open_socket(req_options)
else
return { if docker_socket then
headers = {code=497, message="bad socket path or host", protocol="HTTP/1.1"}, return send_http_socket(options, docker_socket, req_header, req_body, callback)
body = {message="can\'t connect to socket"} else
} return {
end headers = {
code=497,
message="bad socket path or host",
protocol="HTTP/1.1"
},
body = {
message="can\'t connect to socket"
}
}
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
_api_action = "archive"
elseif api_action == "df" then
_api_action = "system/df"
elseif api_action ~= "list" and api_action ~= "inspect" and api_action ~= "remove" then
_api_action = api_action
elseif (api_group == "containers" or api_group == "images" or api_group == "exec") and (api_action == "list" or api_action == "inspect") then
_api_action = "json"
end
local fp = function(self, request, callback) if api_action == "get_archive" or api_action == "put_archive" then
local name_or_id = request and (request.name or request.id or request.name_or_id) or nil api_action = "archive"
if api_action == "list" then elseif api_action == "df" then
if (name_or_id ~= "" and name_or_id ~= nil) then _api_action = "system/df"
if api_group == "images" then elseif api_action ~= "list" and api_action ~= "inspect" and api_action ~= "remove" then
name_or_id = nil _api_action = api_action
else elseif (api_group == "containers" or api_group == "images" or api_group == "exec") and (api_action == "list" or api_action == "inspect") then
request.query = request and request.query or {} _api_action = "json"
request.query.filters = request.query.filters or {} end
request.query.filters.name = request.query.filters.name or {}
request.query.filters.name[#request.query.filters.name + 1] = name_or_id
name_or_id = nil
end
end
elseif api_action == "create" then
if (name_or_id ~= "" and name_or_id ~= nil) then
request.query = request and request.query or {}
request.query.name = request.query.name or name_or_id
name_or_id = nil
end
elseif api_action == "logs" then
local body_buffer = ""
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
for i, v in ipairs(response.body) do
body_buffer = body_buffer .. docker_stream_filter(response.body[i])
end
response.body = body_buffer
end
return response
end
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.body == 1 then
response.body = json_parse(response.body[1])
else
local tmp = {}
for _, v in ipairs(response.body) do
tmp[#tmp+1] = json_parse(v)
end
response.body = tmp
end
end
return response
end
if api_group then local fp = function(self, request, callback)
_table[api_group][api_action] = fp local name_or_id = request and (request.name or request.id or request.name_or_id) or nil
else
_table[api_action] = fp if api_action == "list" then
end if (name_or_id ~= "" and name_or_id ~= nil) then
if api_group == "images" then
name_or_id = nil
else
request.query = request and request.query or {}
request.query.filters = request.query.filters or {}
request.query.filters.name = request.query.filters.name or {}
request.query.filters.name[#request.query.filters.name + 1] = name_or_id
name_or_id = nil
end
end
elseif api_action == "create" then
if (name_or_id ~= "" and name_or_id ~= nil) then
request.query = request and request.query or {}
request.query.name = request.query.name or name_or_id
name_or_id = nil
end
elseif api_action == "logs" then
local body_buffer = ""
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
for i, v in ipairs(response.body) do
body_buffer = body_buffer .. docker_stream_filter(response.body[i])
end
response.body = body_buffer
end
return response
end
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.body == 1 then
response.body = json_parse(response.body[1])
else
local tmp = {}
for _, v in ipairs(response.body) do
tmp[#tmp+1] = json_parse(v)
end
response.body = tmp
end
end
return response
end
if api_group then
_table[api_group][api_action] = fp
else
_table[api_action] = fp
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")
@ -368,85 +444,93 @@ gen_api(_docker, "GET", nil, "_ping")
gen_api(_docker, "GET", nil, "df") 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 = {
socket_path = _options.socket_path or nil, docker.options = {
host = _options.socket_path and "localhost" or _options.host, socket_path = _options.socket_path or nil,
port = not _options.socket_path and _options.port or nil, host = _options.socket_path and "localhost" or _options.host,
tls = _options.tls or nil, port = not _options.socket_path and _options.port or nil,
tls_cacert = _options.tls and _options.tls_cacert or nil, tls = _options.tls or nil,
tls_cert = _options.tls and _options.tls_cert or nil, tls_cacert = _options.tls and _options.tls_cacert or nil,
tls_key = _options.tls and _options.tls_key or nil, tls_cert = _options.tls and _options.tls_cert or nil,
version = _options.version or "v1.40", tls_key = _options.tls and _options.tls_key or nil,
user_agent = _options.user_agent or "LuCI", version = _options.version or "v1.40",
protocol = _options.protocol or "HTTP/1.1", user_agent = _options.user_agent or "LuCI",
debug = _options.debug or false, protocol = _options.protocol or "HTTP/1.1",
debug_path = _options.debug and _options.debug_path or nil debug = _options.debug or false,
} debug_path = _options.debug and _options.debug_path or nil
setmetatable( }
docker,
{ setmetatable(
__index = function(t, key) docker,
if _docker[key] ~= nil then {
return _docker[key] __index = function(t, key)
else if _docker[key] ~= nil then
return _docker.containers[key] return _docker[key]
end else
end return _docker.containers[key]
} end
) end
setmetatable( }
docker.containers, )
{
__index = function(t, key) setmetatable(
if key == "options" then docker.containers,
return docker.options {
end __index = function(t, key)
end if key == "options" then
} return docker.options
) end
setmetatable( end
docker.networks, }
{ )
__index = function(t, key)
if key == "options" then setmetatable(
return docker.options docker.networks,
end {
end __index = function(t, key)
} if key == "options" then
) return docker.options
setmetatable( end
docker.images, end
{ }
__index = function(t, key) )
if key == "options" then
return docker.options setmetatable(
end docker.images,
end {
} __index = function(t, key)
) if key == "options" then
setmetatable( return docker.options
docker.volumes, end
{ end
__index = function(t, key) }
if key == "options" then )
return docker.options
end setmetatable(
end docker.volumes,
} {
) __index = function(t, key)
setmetatable( if key == "options" then
docker.exec, return docker.options
{ end
__index = function(t, key) end
if key == "options" then }
return docker.options )
end
end setmetatable(
} docker.exec,
) {
return docker __index = function(t, key)
if key == "options" then
return docker.options
end
end
}
)
return docker
end end
return _docker return _docker