luci-app-dockerman: initial checkin

Inital commit version v0.5.13 from https://github.com/lisaac/luci-app-dockerman

Signed-off-by: Florian Eckert <fe@dev.tdt.de>
This commit is contained in:
Florian Eckert 2020-04-22 12:00:15 +02:00
parent 16f443bf4c
commit f68e5c1071
30 changed files with 4103 additions and 0 deletions

View file

@ -0,0 +1,19 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI Support for docker
LUCI_DEPENDS:=@(aarch64||arm||x86_64) \
+luci-compat \
+luci-lib-docker \
+docker-ce \
+ttyd
LUCI_PKGARCH:=all
PKG_LICENSE:=AGPL-3.0
PKG_MAINTAINER:=lisaac <lisaac.cn@gmail.com> \
Florian Eckert <fe@dev.tdt.de>
PKG_VERSION:=v0.5.13
include ../../luci.mk
# call BuildPackage - OpenWrt buildroot signature

View file

@ -0,0 +1,384 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
require "luci.util"
local docker = require "luci.model.docker"
-- local uci = require "luci.model.uci"
module("luci.controller.dockerman",package.seeall)
function index()
entry({"admin", "docker"}, firstchild(), "Docker", 40).dependent = false
entry({"admin","docker","overview"},cbi("dockerman/overview"),_("Overview"),0).leaf=true
local remote = luci.model.uci.cursor():get("dockerman", "local", "remote_endpoint")
if remote == nil then
local socket = luci.model.uci.cursor():get("dockerman", "local", "socket_path")
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","images"},form("dockerman/images"),_("Images"),2).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","events"},call("action_events"),_("Events"),5)
entry({"admin","docker","newcontainer"},form("dockerman/newcontainer")).leaf=true
entry({"admin","docker","newnetwork"},form("dockerman/newnetwork")).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_put_archive"},call("upload_archive")).leaf=true
entry({"admin","docker","images_save"},call("save_images")).leaf=true
entry({"admin","docker","images_load"},call("load_images")).leaf=true
entry({"admin","docker","images_import"},call("import_images")).leaf=true
entry({"admin","docker","images_get_tags"},call("get_image_tags")).leaf=true
entry({"admin","docker","images_tag"},call("tag_image")).leaf=true
entry({"admin","docker","images_untag"},call("untag_image")).leaf=true
entry({"admin","docker","confirm"},call("action_confirm")).leaf=true
end
function action_events()
local logs = ""
local dk = docker.new()
local query ={}
query["until"] = os.time()
local events = dk:events({query = query})
if events.code == 200 then
for _, v in ipairs(events.body) do
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 or "null") .. " Container ID:".. (v.Actor.ID or "null") .. " Container Name:" .. (v.Actor.Attributes.name or "null")
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 .. " Container ID:"..( v.Actor.Attributes.container or "null" ) .. " Network Name:" .. (v.Actor.Attributes.name or "null") .. " Network type:".. v.Actor.Attributes.type or ""
elseif v.Type == "image" 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")
end
end
end
luci.template.render("dockerman/logs", {self={syslog = logs, title="Events"}})
end
local calculate_cpu_percent = function(d)
if type(d) ~= "table" then return end
cpu_count = tonumber(d["cpu_stats"]["online_cpus"])
cpu_percent = 0.0
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"])
if system_delta > 0.0 then
cpu_percent = string.format("%.2f", cpu_delta / system_delta * 100.0 * cpu_count)
end
-- return cpu_percent .. "%"
return cpu_percent
end
local get_memory = function(d)
if type(d) ~= "table" then return end
-- local limit = string.format("%.2f", tonumber(d["memory_stats"]["limit"]) / 1024 / 1024)
-- local usage = string.format("%.2f", (tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"])) / 1024 / 1024)
-- return usage .. "MB / " .. limit.. "MB"
local limit =tonumber(d["memory_stats"]["limit"])
local usage = tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"])
return usage, limit
end
local get_rx_tx = function(d)
if type(d) ~="table" then return end
-- local data
-- if type(d["networks"]) == "table" then
-- for e, v in pairs(d["networks"]) do
-- data = (data and (data .. "<br>") or "") .. e .. " Total Tx:" .. string.format("%.2f",(tonumber(v.tx_bytes)/1024/1024)) .. "MB Total Rx: ".. string.format("%.2f",(tonumber(v.rx_bytes)/1024/1024)) .. "MB"
-- end
-- end
local data = {}
if type(d["networks"]) == "table" then
for e, v in pairs(d["networks"]) do
data[e] = {
bw_tx = tonumber(v.tx_bytes),
bw_rx = tonumber(v.rx_bytes)
}
end
end
return data
end
function action_get_container_stats(container_id)
if container_id then
local dk = docker.new()
local response = dk.containers:inspect({id = container_id})
if response.code == 200 and response.body.State.Running then
response = dk.containers:stats({id = container_id, query = {stream = false}})
if response.code == 200 then
local container_stats = response.body
local cpu_percent = calculate_cpu_percent(container_stats)
local mem_useage, mem_limit = get_memory(container_stats)
local bw_rxtx = get_rx_tx(container_stats)
luci.http.status(response.code, response.body.message)
luci.http.prepare_content("application/json")
luci.http.write_json({
cpu_percent = cpu_percent,
memory = {
mem_useage = mem_useage,
mem_limit = mem_limit
},
bw_rxtx = bw_rxtx
})
else
luci.http.status(response.code, response.body.message)
luci.http.prepare_content("text/plain")
luci.http.write(response.body.message)
end
else
if response.code == 200 then
luci.http.status(500, "container "..container_id.." not running")
luci.http.prepare_content("text/plain")
luci.http.write("Container "..container_id.." not running")
else
luci.http.status(response.code, response.body.message)
luci.http.prepare_content("text/plain")
luci.http.write(response.body.message)
end
end
else
luci.http.status(404, "No container name or id")
luci.http.prepare_content("text/plain")
luci.http.write("No container name or id")
end
end
function action_confirm()
local data = docker:read_status()
if data then
data = data:gsub("\n","<br>"):gsub(" ","&nbsp;")
code = 202
msg = data
else
code = 200
msg = "finish"
data = "finish"
end
luci.http.status(code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json({info = data})
end
function download_archive()
local id = luci.http.formvalue("id")
local path = luci.http.formvalue("path")
local dk = docker.new()
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="archive.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
local res = dk.containers:get_archive({id = id, query = {path = path}}, cb)
end
function upload_archive(container_id)
local path = luci.http.formvalue("upload-path")
local dk = docker.new()
local ltn12 = require "luci.ltn12"
local rec_send = function(sinkout)
luci.http.setfilehandler(function (meta, chunk, eof)
if chunk then
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
end
end)
end
local res = dk.containers:put_archive({id = container_id, query = {path = path}, body = rec_send})
local msg = res and res.body and res.body.message or nil
luci.http.status(res.code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg})
end
function save_images(container_id)
local names = luci.http.formvalue("names")
local dk = docker.new()
local first
local cb = function(res, chunk)
if res.code == 200 then
if not first then
first = true
luci.http.status(res.code, res.message)
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: saving" .. " " .. container_id .. "...")
local res = dk.images:get({id = container_id, query = {names = names}}, 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
function load_images()
local path = luci.http.formvalue("upload-path")
local dk = docker.new()
local ltn12 = require "luci.ltn12"
local rec_send = function(sinkout)
luci.http.setfilehandler(function (meta, chunk, eof)
if chunk then
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
end
end)
end
docker:write_status("Images: loading...")
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
if res.code == 200 and msg and msg:match("Loaded image ID") then
docker:clear_status()
luci.http.status(res.code, msg)
else
docker:append_status("code:" .. res.code.." ".. msg)
luci.http.status(300, msg)
end
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg})
end
function import_images()
local src = luci.http.formvalue("src")
local itag = luci.http.formvalue("tag")
local dk = docker.new()
local ltn12 = require "luci.ltn12"
local rec_send = function(sinkout)
luci.http.setfilehandler(function (meta, chunk, eof)
if chunk then
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
end
end)
end
docker:write_status("Images: importing".. " ".. itag .."...\n")
local repo = itag and itag:match("^([^:]+)")
local tag = itag and itag:match("^[^:]-:([^:]+)")
local res = dk.images:create({query = {fromSrc = src or "-", repo = repo or nil, tag = tag or nil }, body = not src and rec_send or nil}, docker.import_image_show_status_cb)
local msg = res and res.body and ( res.body.message )or nil
if not msg and #res.body == 0 then
-- res.body = {"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}
msg = res.body.status or res.body.error
elseif not msg and #res.body >= 1 then
-- res.body = [...{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}]
msg = res.body[#res.body].status or res.body[#res.body].error
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
function get_image_tags(image_id)
if not image_id then
luci.http.status(400, "no image id")
luci.http.prepare_content("application/json")
luci.http.write_json({message = "no image id"})
return
end
local dk = docker.new()
local res = dk.images:inspect({id = image_id})
local msg = res and res.body and res.body.message or nil
luci.http.status(res.code, msg)
luci.http.prepare_content("application/json")
if res.code == 200 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
function tag_image(image_id)
local src = luci.http.formvalue("tag")
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")
luci.http.prepare_content("application/json")
luci.http.write_json({message = "no image id or tag"})
return
end
local repo = src:match("^([^:]+)")
local tag = src:match("^[^:]-:([^:]+)")
local dk = docker.new()
local res = dk.images:tag({id = image_id, query={repo=repo, tag=tag}})
local msg = res and res.body and res.body.message or nil
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
function untag_image(tag)
local tag = tag or luci.http.formvalue("tag")
if not tag then
luci.http.status(400, "no tag name")
luci.http.prepare_content("application/json")
luci.http.write_json({message = "no tag name"})
return
end
local dk = docker.new()
local res = dk.images:inspect({name = tag})
if res.code == 200 then
local tags = res.body.RepoTags
if #tags > 1 then
local r = dk.images:remove({name = tag})
local msg = r and r.body and r.body.message or nil
luci.http.status(r.code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg})
else
luci.http.status(500, "Cannot remove the last tag")
luci.http.prepare_content("application/json")
luci.http.write_json({message = "Cannot remove the last tag"})
end
else
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

View file

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

View file

@ -0,0 +1,195 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
require "luci.util"
local http = require "luci.http"
local uci = luci.model.uci.cursor()
local docker = require "luci.model.docker"
local dk = docker.new()
local images, networks, containers
local res = dk.images:list()
if res.code <300 then images = res.body else return end
res = dk.networks:list()
if res.code <300 then networks = res.body 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
function get_containers()
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)
data[index]["_command"] = v.Command
end
return data
end
local c_lists = get_containers()
-- list Containers
-- m = Map("docker", translate("Docker"))
m = SimpleForm("docker", translate("Docker"))
m.submit=false
m.reset=false
docker_status = m:section(SimpleSection)
docker_status.template = "dockerman/apply_widget"
docker_status.err=docker:read_status()
docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end
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
local start_stop_remove = function(m,cmd)
local c_selected = {}
-- 遍历table中sectionid
local c_table_sids = c_table:cfgsections()
for _, c_table_sid in ipairs(c_table_sids) do
-- 得到选中项的名字
if c_lists[c_table_sid]._selected == 1 then
c_selected[#c_selected+1] = c_lists[c_table_sid].name --container_name:cfgvalue(c_table_sid)
end
end
if #c_selected >0 then
docker:clear_status()
local success = true
for _,cont in ipairs(c_selected) do
docker:append_status("Containers: " .. cmd .. " " .. cont .. "...")
local res = dk.containers[cmd](dk, {id = cont})
if res and res.code >= 300 then
success = false
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
else
docker:append_status("done\n")
end
end
if success then docker:clear_status() end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
end
end
action_section = m:section(Table,{{}})
action_section.notitle=true
action_section.rowcolors=false
action_section.template="cbi/nullsection"
btnnew=action_section:option(Button, "_new")
btnnew.inputtitle= translate("New")
btnnew.template = "dockerman/cbi/inlinebutton"
btnnew.inputstyle = "add"
btnnew.forcewrite = true
btnstart=action_section:option(Button, "_start")
btnstart.template = "dockerman/cbi/inlinebutton"
btnstart.inputtitle=translate("Start")
btnstart.inputstyle = "apply"
btnstart.forcewrite = true
btnrestart=action_section:option(Button, "_restart")
btnrestart.template = "dockerman/cbi/inlinebutton"
btnrestart.inputtitle=translate("Restart")
btnrestart.inputstyle = "reload"
btnrestart.forcewrite = true
btnstop=action_section:option(Button, "_stop")
btnstop.template = "dockerman/cbi/inlinebutton"
btnstop.inputtitle=translate("Stop")
btnstop.inputstyle = "reset"
btnstop.forcewrite = true
btnkill=action_section:option(Button, "_kill")
btnkill.template = "dockerman/cbi/inlinebutton"
btnkill.inputtitle=translate("Kill")
btnkill.inputstyle = "reset"
btnkill.forcewrite = true
btnremove=action_section:option(Button, "_remove")
btnremove.template = "dockerman/cbi/inlinebutton"
btnremove.inputtitle=translate("Remove")
btnremove.inputstyle = "remove"
btnremove.forcewrite = true
btnnew.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
end
btnstart.write = function(self, section)
start_stop_remove(m,"start")
end
btnrestart.write = function(self, section)
start_stop_remove(m,"restart")
end
btnremove.write = function(self, section)
start_stop_remove(m,"remove")
end
btnstop.write = function(self, section)
start_stop_remove(m,"stop")
end
btnkill.write = function(self, section)
start_stop_remove(m,"kill")
end
return m

View file

@ -0,0 +1,223 @@
--[[
LuCI - Lua Configuration Interface
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 dk = docker.new()
local containers, images
local res = dk.images:list()
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()
local data = {}
for i, v in ipairs(images) do
local index = v.Created .. v.Id
data[index]={}
data[index]["_selected"] = 0
data[index]["id"] = v.Id:sub(8)
data[index]["_id"] = '<a href="javascript:new_tag(\''..v.Id:sub(8,20)..'\')" class="dockerman-link" title="'..translate("New tag")..'">' .. v.Id:sub(8,20) .. '</a>'
if v.RepoTags and next(v.RepoTags)~=nil then
for i, v1 in ipairs(v.RepoTags) do
data[index]["_tags"] =(data[index]["_tags"] and ( data[index]["_tags"] .. "<br>" )or "") .. ((v1:match("<none>") or (#v.RepoTags == 1)) and v1 or ('<a href="javascript:un_tag(\''..v1..'\')" class="dockerman_link" title="'..translate("Remove tag")..'" >' .. v1 .. '</a>'))
if not data[index]["tag"] then
data[index]["tag"] = v1--:match("<none>") and nil or v1
end
end
else
data[index]["_tags"] = v.RepoDigests[1] and v.RepoDigests[1]:match("^(.-)@.+")
data[index]["_tags"] = (data[index]["_tags"] and data[index]["_tags"] or "<none>" ).. ":<none>"
end
data[index]["_tags"] = data[index]["_tags"]:gsub("<none>","&lt;none&gt;")
-- data[index]["_tags"] = '<a href="javascript:handle_tag(\''..data[index]["_id"]..'\')">' .. data[index]["_tags"] .. '</a>'
for ci,cv in ipairs(containers) do
if v.Id == cv.ImageID then
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>"
end
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
local image_list = get_images()
-- m = Map("docker", translate("Docker"))
m = SimpleForm("docker", translate("Docker"))
m.submit=false
m.reset=false
local pull_value={_image_tag_name="", _registry="index.docker.io"}
local pull_section = m:section(SimpleSection, translate("Pull Image"))
pull_section.template="cbi/nullsection"
local tag_name = pull_section:option(Value, "_image_tag_name")
tag_name.template = "dockerman/cbi/inlinevalue"
tag_name.placeholder="lisaac/luci:latest"
local action_pull = pull_section:option(Button, "_pull")
action_pull.inputtitle= translate("Pull")
action_pull.template = "dockerman/cbi/inlinebutton"
action_pull.inputstyle = "add"
tag_name.write = function(self, section, value)
local hastag = value:find(":")
if not hastag then
value = value .. ":latest"
end
pull_value["_image_tag_name"] = value
end
action_pull.write = function(self, section)
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
local import_section = m:section(SimpleSection, translate("Import Images"))
local im = import_section:option(DummyValue, "_image_import")
im.template = "dockerman/images_import"
local image_table = m:section(Table, image_list, translate("Images"))
local image_selecter = image_table:option(Flag, "_selected","")
image_selecter.disabled = 0
image_selecter.enabled = 1
image_selecter.default = 0
local image_id = image_table:option(DummyValue, "_id", translate("ID"))
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
local remove_action = function(force)
local image_selected = {}
-- 遍历table中sectionid
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]["_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
if next(image_selected) ~= nil then
local success = true
docker:clear_status()
for _,img in ipairs(image_selected) do
docker:append_status("Images: " .. "remove" .. " " .. img .. "...")
local query
if force then query = {force = true} end
local msg = dk.images:remove({id = img, query = query})
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: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
local docker_status = m:section(SimpleSection)
docker_status.template = "dockerman/apply_widget"
docker_status.err = docker:read_status()
docker_status.err = docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end
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
local btnforceremove = action:option(Button, "forceremove")
btnforceremove.inputtitle= translate("Force Remove")
btnforceremove.template = "dockerman/cbi/inlinebutton"
btnforceremove.inputstyle = "remove"
btnforceremove.forcewrite = true
btnforceremove.write = function(self, section)
remove_action(true)
end
local btnsave = action:option(Button, "save")
btnsave.inputtitle= translate("Save")
btnsave.template = "dockerman/cbi/inlinebutton"
btnsave.inputstyle = "edit"
btnsave.forcewrite = true
btnsave.write = function (self, section)
local image_selected = {}
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
local btnload = action:option(Button, "load")
btnload.inputtitle= translate("Load")
btnload.template = "dockerman/images_load"
btnload.inputstyle = "add"
return m

View file

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

View file

@ -0,0 +1,653 @@
--[[
LuCI - Lua Configuration Interface
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 dk = docker.new()
local cmd_line = table.concat(arg, '/')
local create_body = {}
local images = dk.images:list().body
local networks = dk.networks:list().body
local containers = dk.containers:list({query = {all=true}}).body
local is_quot_complete = function(str)
require "math"
if not str then return true end
local num = 0, w
for w in str:gmatch("\"") do
num = num + 1
end
if math.fmod(num, 2) ~= 0 then return false end
num = 0
for w in str:gmatch("\'") do
num = num + 1
end
if math.fmod(num, 2) ~= 0 then return false end
return true
end
local resolve_cli = function(cmd_line)
local config = {advance = 1}
local key_no_val = '|t|d|i|tty|rm|read_only|interactive|init|help|detach|privileged|P|publish_all|'
local key_with_val = '|sysctl|add_host|a|attach|blkio_weight_device|cap_add|cap_drop|device|device_cgroup_rule|device_read_bps|device_read_iops|device_write_bps|device_write_iops|dns|dns_option|dns_search|e|env|env_file|expose|group_add|l|label|label_file|link|link_local_ip|log_driver|log_opt|network_alias|p|publish|security_opt|storage_opt|tmpfs|v|volume|volumes_from|blkio_weight|cgroup_parent|cidfile|cpu_period|cpu_quota|cpu_rt_period|cpu_rt_runtime|c|cpu_shares|cpus|cpuset_cpus|cpuset_mems|detach_keys|disable_content_trust|domainname|entrypoint|gpus|health_cmd|health_interval|health_retries|health_start_period|health_timeout|h|hostname|ip|ip6|ipc|isolation|kernel_memory|log_driver|mac_address|m|memory|memory_reservation|memory_swap|memory_swappiness|mount|name|network|no_healthcheck|oom_kill_disable|oom_score_adj|pid|pids_limit|restart|runtime|shm_size|sig_proxy|stop_signal|stop_timeout|ulimit|u|user|userns|uts|volume_driver|w|workdir|'
local key_abb = {net='network',a='attach',c='cpu-shares',d='detach',e='env',h='hostname',i='interactive',l='label',m='memory',p='publish',P='publish_all',t='tty',u='user',v='volume',w='workdir'}
local key_with_list = '|sysctl|add_host|a|attach|blkio_weight_device|cap_add|cap_drop|device|device_cgroup_rule|device_read_bps|device_read_iops|device_write_bps|device_write_iops|dns|dns_option|dns_search|e|env|env_file|expose|group_add|l|label|label_file|link|link_local_ip|log_driver|log_opt|network_alias|p|publish|security_opt|storage_opt|tmpfs|v|volume|volumes_from|'
local key = nil
local _key = nil
local val = nil
local is_cmd = false
cmd_line = cmd_line:match("^DOCKERCLI%s+(.+)")
for w in cmd_line:gmatch("[^%s]+") do
if w =='\\' then
elseif not key and not _key and not is_cmd then
--key=val
key, val = w:match("^%-%-([%lP%-]-)=(.+)")
if not key then
--key val
key = w:match("^%-%-([%lP%-]+)")
if not key then
-- -v val
key = w:match("^%-([%lP%-]+)")
if key then
-- for -dit
if key:match("i") or key:match("t") or key:match("d") then
if key:match("i") then
config[key_abb["i"]] = true
key:gsub("i", "")
end
if key:match("t") then
config[key_abb["t"]] = true
key:gsub("t", "")
end
if key:match("d") then
config[key_abb["d"]] = true
key:gsub("d", "")
end
if key:match("P") then
config[key_abb["P"]] = true
key:gsub("P", "")
end
if key == "" then key = nil end
end
end
end
end
if key then
key = key:gsub("-","_")
key = key_abb[key] or key
if key_no_val:match("|"..key.."|") then
config[key] = true
val = nil
key = nil
elseif key_with_val:match("|"..key.."|") then
-- if key == "cap_add" then config.privileged = true end
else
key = nil
val = nil
end
else
config.image = w
key = nil
val = nil
is_cmd = true
end
elseif (key or _key) and not is_cmd then
if key == "mount" then
-- we need resolve mount options here
-- type=bind,source=/source,target=/app
local _type = w:match("^type=([^,]+),") or "bind"
local source = (_type ~= "tmpfs") and (w:match("source=([^,]+),") or w:match("src=([^,]+),")) or ""
local target = w:match(",target=([^,]+)") or w:match(",dst=([^,]+)") or w:match(",destination=([^,]+)") or ""
local ro = w:match(",readonly") and "ro" or nil
if source and target then
if _type ~= "tmpfs" then
-- bind or volume
local bind_propagation = (_type == "bind") and w:match(",bind%-propagation=([^,]+)") or nil
val = source..":"..target .. ((ro or bind_propagation) and (":" .. (ro and ro or "") .. (((ro and bind_propagation) and "," or "") .. (bind_propagation and bind_propagation or ""))or ""))
else
-- tmpfs
local tmpfs_mode = w:match(",tmpfs%-mode=([^,]+)") or nil
local tmpfs_size = w:match(",tmpfs%-size=([^,]+)") or nil
key = "tmpfs"
val = target .. ((tmpfs_mode or tmpfs_size) and (":" .. (tmpfs_mode and ("mode=" .. tmpfs_mode) or "") .. ((tmpfs_mode and tmpfs_size) and "," or "") .. (tmpfs_size and ("size=".. tmpfs_size) or "")) or "")
if not config[key] then config[key] = {} end
table.insert( config[key], val )
key = nil
val = nil
end
end
else
val = w
end
elseif is_cmd then
config["command"] = (config["command"] and (config["command"] .. " " )or "") .. w
end
if (key or _key) and val then
key = _key or key
if key_with_list:match("|"..key.."|") then
if not config[key] then config[key] = {} end
if _key then
config[key][#config[key]] = config[key][#config[key]] .. " " .. w
else
table.insert( config[key], val )
end
if is_quot_complete(config[key][#config[key]]) then
-- clear quotation marks
config[key][#config[key]] = config[key][#config[key]]:gsub("[\"\']", "")
_key = nil
else
_key = key
end
else
config[key] = (config[key] and (config[key] .. " ") or "") .. val
if is_quot_complete(config[key]) then
-- clear quotation marks
config[key] = config[key]:gsub("[\"\']", "")
_key = nil
else
_key = key
end
end
key = nil
val = nil
end
end
return config
end
-- reslvo default config
local default_config = {}
if cmd_line and cmd_line:match("^DOCKERCLI.+") then
default_config = resolve_cli(cmd_line)
elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then
local container_id = cmd_line:match("^duplicate/(.+)")
create_body = dk:containers_duplicate_config({id = container_id}) or {}
if not create_body.HostConfig then create_body.HostConfig = {} end
if next(create_body) ~= nil then
default_config.name = nil
default_config.image = create_body.Image
default_config.hostname = create_body.Hostname
default_config.tty = create_body.Tty and true or false
default_config.interactive = create_body.OpenStdin and true or false
default_config.privileged = create_body.HostConfig.Privileged and true or false
default_config.restart = create_body.HostConfig.RestartPolicy and create_body.HostConfig.RestartPolicy.name or nil
-- default_config.network = create_body.HostConfig.NetworkMode == "default" and "bridge" or create_body.HostConfig.NetworkMode
-- if container has leave original network, and add new network, .HostConfig.NetworkMode is INcorrect, so using first child of .NetworkingConfig.EndpointsConfig
default_config.network = create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and next(create_body.NetworkingConfig.EndpointsConfig) or nil
default_config.ip = default_config.network and default_config.network ~= "bridge" and default_config.network ~= "host" and default_config.network ~= "null" and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig.IPv4Address or nil
default_config.link = create_body.HostConfig.Links
default_config.env = create_body.Env
default_config.dns = create_body.HostConfig.Dns
default_config.volume = create_body.HostConfig.Binds
default_config.cap_add = create_body.HostConfig.CapAdd
default_config.publish_all = create_body.HostConfig.PublishAllPorts
if create_body.HostConfig.Sysctls and type(create_body.HostConfig.Sysctls) == "table" then
default_config.sysctl = {}
for k, v in pairs(create_body.HostConfig.Sysctls) do
table.insert( default_config.sysctl, k.."="..v )
end
end
if create_body.HostConfig.LogConfig and create_body.HostConfig.LogConfig.Config and type(create_body.HostConfig.LogConfig.Config) == "table" then
default_config.log_opt = {}
for k, v in pairs(create_body.HostConfig.LogConfig.Config) do
table.insert( default_config.log_opt, k.."="..v )
end
end
if create_body.HostConfig.PortBindings and type(create_body.HostConfig.PortBindings) == "table" then
default_config.publish = {}
for k, v in pairs(create_body.HostConfig.PortBindings) do
table.insert( default_config.publish, v[1].HostPort..":"..k:match("^(%d+)/.+").."/"..k:match("^%d+/(.+)") )
end
end
default_config.user = create_body.User or nil
default_config.command = create_body.Cmd and type(create_body.Cmd) == "table" and table.concat(create_body.Cmd, " ") or nil
default_config.advance = 1
default_config.cpus = create_body.HostConfig.NanoCPUs
default_config.cpu_shares = create_body.HostConfig.CpuShares
default_config.memory = create_body.HostConfig.Memory
default_config.blkio_weight = create_body.HostConfig.BlkioWeight
if create_body.HostConfig.Devices and type(create_body.HostConfig.Devices) == "table" then
default_config.device = {}
for _, v in ipairs(create_body.HostConfig.Devices) do
table.insert( default_config.device, v.PathOnHost..":"..v.PathInContainer..(v.CgroupPermissions ~= "" and (":" .. v.CgroupPermissions) or "") )
end
end
if create_body.HostConfig.Tmpfs and type(create_body.HostConfig.Tmpfs) == "table" then
default_config.tmpfs = {}
for k, v in pairs(create_body.HostConfig.Tmpfs) do
table.insert( default_config.tmpfs, k .. (v~="" and ":" or "")..v )
end
end
end
end
local m = SimpleForm("docker", translate("Docker"))
m.redirect = luci.dispatcher.build_url("admin", "docker", "containers")
-- m.reset = false
-- m.submit = false
-- new Container
docker_status = m:section(SimpleSection)
docker_status.template = "dockerman/apply_widget"
docker_status.err=docker:read_status()
docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end
local s = m:section(SimpleSection, translate("New Container"))
s.addremove = true
s.anonymous = true
local d = s:option(DummyValue,"cmd_line", translate("Resolve CLI"))
d.rawhtml = true
d.template = "dockerman/newcontainer_resolve"
d = s:option(Value, "name", translate("Container Name"))
d.rmempty = true
d.default = default_config.name or nil
d = s:option(Flag, "interactive", translate("Interactive (-i)"))
d.rmempty = true
d.disabled = 0
d.enabled = 1
d.default = default_config.interactive and 1 or 0
d = s:option(Flag, "tty", translate("TTY (-t)"))
d.rmempty = true
d.disabled = 0
d.enabled = 1
d.default = default_config.tty and 1 or 0
d = s:option(Value, "image", translate("Docker Image"))
d.rmempty = true
d.default = default_config.image or nil
for _, v in ipairs (images) do
if v.RepoTags then
d:value(v.RepoTags[1], v.RepoTags[1])
end
end
d = s:option(Flag, "_force_pull", translate("Always pull image first"))
d.rmempty = true
d.disabled = 0
d.enabled = 1
d.default = 0
d = s:option(Flag, "privileged", translate("Privileged"))
d.rmempty = true
d.disabled = 0
d.enabled = 1
d.default = default_config.privileged and 1 or 0
d = s:option(ListValue, "restart", translate("Restart Policy"))
d.rmempty = true
d:value("no", "No")
d:value("unless-stopped", "Unless stopped")
d:value("always", "Always")
d:value("on-failure", "On failure")
d.default = default_config.restart or "unless-stopped"
local d_network = s:option(ListValue, "network", translate("Networks"))
d_network.rmempty = true
d_network.default = default_config.network or "bridge"
local d_ip = s:option(Value, "ip", translate("IPv4 Address"))
d_ip.datatype="ip4addr"
d_ip:depends("network", "nil")
d_ip.default = default_config.ip or nil
d = s:option(DynamicList, "link", translate("Links with other containers"))
d.placeholder = "container_name:alias"
d.rmempty = true
d:depends("network", "bridge")
d.default = default_config.link or nil
d = s:option(DynamicList, "dns", translate("Set custom DNS servers"))
d.placeholder = "8.8.8.8"
d.rmempty = true
d.default = default_config.dns or nil
d = s:option(Value, "user", translate("User(-u)"), translate("The user that commands are run as inside the container.(format: name|uid[:group|gid])"))
d.placeholder = "1000:1000"
d.rmempty = true
d.default = default_config.user or nil
d = s:option(DynamicList, "env", translate("Environmental Variable(-e)"), translate("Set environment variables to inside the container"))
d.placeholder = "TZ=Asia/Shanghai"
d.rmempty = true
d.default = default_config.env or nil
d = s:option(DynamicList, "volume", translate("Bind Mount(-v)"), translate("Bind mount a volume"))
d.placeholder = "/media:/media:slave"
d.rmempty = true
d.default = default_config.volume or nil
local d_publish = s:option(DynamicList, "publish", translate("Exposed Ports(-p)"), translate("Publish container's port(s) to the host"))
d_publish.placeholder = "2200:22/tcp"
d_publish.rmempty = true
d_publish.default = default_config.publish or nil
d = s:option(Value, "command", translate("Run command"))
d.placeholder = "/bin/sh init.sh"
d.rmempty = true
d.default = default_config.command or nil
d = s:option(Flag, "advance", translate("Advance"))
d.rmempty = true
d.disabled = 0
d.enabled = 1
d.default = default_config.advance or 0
d = s:option(Value, "hostname", translate("Host Name"), translate("The hostname to use for the container"))
d.rmempty = true
d.default = default_config.hostname or nil
d:depends("advance", 1)
d = s:option(Flag, "publish_all", translate("Exposed All Ports(-P)"), translate("Allocates an ephemeral host port for all of a container's exposed ports"))
d.rmempty = true
d.disabled = 0
d.enabled = 1
d.default = default_config.publish_all and 1 or 0
d:depends("advance", 1)
d = s:option(DynamicList, "device", translate("Device(--device)"), translate("Add host device to the container"))
d.placeholder = "/dev/sda:/dev/xvdc:rwm"
d.rmempty = true
d:depends("advance", 1)
d.default = default_config.device or nil
d = s:option(DynamicList, "tmpfs", translate("Tmpfs(--tmpfs)"), translate("Mount tmpfs directory"))
d.placeholder = "/run:rw,noexec,nosuid,size=65536k"
d.rmempty = true
d:depends("advance", 1)
d.default = default_config.tmpfs or nil
d = s:option(DynamicList, "sysctl", translate("Sysctl(--sysctl)"), translate("Sysctls (kernel parameters) options"))
d.placeholder = "net.ipv4.ip_forward=1"
d.rmempty = true
d:depends("advance", 1)
d.default = default_config.sysctl or nil
d = s:option(DynamicList, "cap_add", translate("CAP-ADD(--cap-add)"), translate("A list of kernel capabilities to add to the container"))
d.placeholder = "NET_ADMIN"
d.rmempty = true
d:depends("advance", 1)
d.default = default_config.cap_add or nil
d = s:option(Value, "cpus", translate("CPUs"), translate("Number of CPUs. Number is a fractional number. 0.000 means no limit"))
d.placeholder = "1.5"
d.rmempty = true
d:depends("advance", 1)
d.datatype="ufloat"
d.default = default_config.cpus or nil
d = s:option(Value, "cpu_shares", translate("CPU Shares Weight"), translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024"))
d.placeholder = "1024"
d.rmempty = true
d:depends("advance", 1)
d.datatype="uinteger"
d.default = default_config.cpu_shares or nil
d = s:option(Value, "memory", translate("Memory"), translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M"))
d.placeholder = "128m"
d.rmempty = true
d:depends("advance", 1)
d.default = default_config.memory or nil
d = s:option(Value, "blkio_weight", translate("Block IO Weight"), translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000"))
d.placeholder = "500"
d.rmempty = true
d:depends("advance", 1)
d.datatype="uinteger"
d.default = default_config.blkio_weight or nil
d = s:option(DynamicList, "log_opt", translate("Log driver options"), translate("The logging configuration for this container"))
d.placeholder = "max-size=1m"
d.rmempty = true
d:depends("advance", 1)
d.default = default_config.log_opt or nil
for _, v in ipairs (networks) do
if v.Name then
local parent = v.Options and v.Options.parent or nil
local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
ipv6 = v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil
local network_name = v.Name .. " | " .. v.Driver .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "")
d_network:value(v.Name, network_name)
if v.Name ~= "none" and v.Name ~= "bridge" and v.Name ~= "host" then
d_ip:depends("network", v.Name)
end
if v.Driver == "bridge" then
d_publish:depends("network", v.Name)
end
end
end
m.handle = function(self, state, data)
if state ~= FORM_VALID then return end
local tmp
local name = data.name or ("luci_" .. os.date("%Y%m%d%H%M%S"))
local hostname = data.hostname
local tty = type(data.tty) == "number" and (data.tty == 1 and true or false) or default_config.tty or false
local publish_all = type(data.publish_all) == "number" and (data.publish_all == 1 and true or false) or default_config.publish_all or false
local interactive = type(data.interactive) == "number" and (data.interactive == 1 and true or false) or default_config.interactive or false
local image = data.image
local user = data.user
if image and not image:match(".-:.+") then
image = image .. ":latest"
end
local privileged = type(data.privileged) == "number" and (data.privileged == 1 and true or false) or default_config.privileged or false
local restart = data.restart
local env = data.env
local dns = data.dns
local cap_add = data.cap_add
local sysctl = {}
tmp = data.sysctl
if type(tmp) == "table" then
for i, v in ipairs(tmp) do
local k,v1 = v:match("(.-)=(.+)")
if k and v1 then
sysctl[k]=v1
end
end
end
local log_opt = {}
tmp = data.log_opt
if type(tmp) == "table" then
for i, v in ipairs(tmp) do
local k,v1 = v:match("(.-)=(.+)")
if k and v1 then
log_opt[k]=v1
end
end
end
local network = data.network
local ip = (network ~= "bridge" and network ~= "host" and network ~= "none") and data.ip or nil
local volume = data.volume
local memory = data.memory or 0
local cpu_shares = data.cpu_shares or 0
local cpus = data.cpus or 0
local blkio_weight = data.blkio_weight or 500
local portbindings = {}
local exposedports = {}
local tmpfs = {}
tmp = data.tmpfs
if type(tmp) == "table" then
for i, v in ipairs(tmp)do
local k= v:match("([^:]+)")
local v1 = v:match(".-:([^:]+)") or ""
if k then
tmpfs[k]=v1
end
end
end
local device = {}
tmp = data.device
if type(tmp) == "table" then
for i, v in ipairs(tmp) do
local t = {}
local _,_, h, c, p = v:find("(.-):(.-):(.+)")
if h and c then
t['PathOnHost'] = h
t['PathInContainer'] = c
t['CgroupPermissions'] = p or "rwm"
else
local _,_, h, c = v:find("(.-):(.+)")
if h and c then
t['PathOnHost'] = h
t['PathInContainer'] = c
t['CgroupPermissions'] = "rwm"
else
t['PathOnHost'] = v
t['PathInContainer'] = v
t['CgroupPermissions'] = "rwm"
end
end
if next(t) ~= nil then
table.insert( device, t )
end
end
end
tmp = data.publish or {}
for i, v in ipairs(tmp) do
for v1 ,v2 in string.gmatch(v, "(%d+):([^%s]+)") do
local _,_,p= v2:find("^%d+/(%w+)")
if p == nil then
v2=v2..'/tcp'
end
portbindings[v2] = {{HostPort=v1}}
exposedports[v2] = {HostPort=v1}
end
end
local link = data.link
tmp = data.command
local command = {}
if tmp ~= nil then
for v in string.gmatch(tmp, "[^%s]+") do
command[#command+1] = v
end
end
if memory ~= 0 then
_,_,n,unit = memory:find("([%d%.]+)([%l%u]+)")
if n then
unit = unit and unit:sub(1,1):upper() or "B"
if unit == "M" then
memory = tonumber(n) * 1024 * 1024
elseif unit == "G" then
memory = tonumber(n) * 1024 * 1024 * 1024
elseif unit == "K" then
memory = tonumber(n) * 1024
else
memory = tonumber(n)
end
end
end
create_body.Hostname = network ~= "host" and (hostname or name) or nil
create_body.Tty = tty and true or false
create_body.OpenStdin = interactive and true or false
create_body.User = user
create_body.Cmd = command
create_body.Env = env
create_body.Image = image
create_body.ExposedPorts = exposedports
create_body.HostConfig = create_body.HostConfig or {}
create_body.HostConfig.Dns = dns
create_body.HostConfig.Binds = volume
create_body.HostConfig.RestartPolicy = { Name = restart, MaximumRetryCount = 0 }
create_body.HostConfig.Privileged = privileged and true or false
create_body.HostConfig.PortBindings = portbindings
create_body.HostConfig.Memory = tonumber(memory)
create_body.HostConfig.CpuShares = tonumber(cpu_shares)
create_body.HostConfig.NanoCPUs = tonumber(cpus) * 10 ^ 9
create_body.HostConfig.BlkioWeight = tonumber(blkio_weight)
create_body.HostConfig.PublishAllPorts = publish_all
if create_body.HostConfig.NetworkMode ~= network then
-- network mode changed, need to clear duplicate config
create_body.NetworkingConfig = nil
end
create_body.HostConfig.NetworkMode = network
if ip then
if create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and type(create_body.NetworkingConfig.EndpointsConfig) == "table" then
-- ip + duplicate config
for k, v in pairs (create_body.NetworkingConfig.EndpointsConfig) do
if k == network and v.IPAMConfig and v.IPAMConfig.IPv4Address then
v.IPAMConfig.IPv4Address = ip
else
create_body.NetworkingConfig.EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } }
end
break
end
else
-- ip + no duplicate config
create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } }
end
elseif not create_body.NetworkingConfig then
-- no ip + no duplicate config
create_body.NetworkingConfig = nil
end
create_body["HostConfig"]["Tmpfs"] = tmpfs
create_body["HostConfig"]["Devices"] = device
create_body["HostConfig"]["Sysctls"] = sysctl
create_body["HostConfig"]["CapAdd"] = cap_add
create_body["HostConfig"]["LogConfig"] = next(log_opt) ~= nil and { Config = log_opt } or nil
if network == "bridge" then
create_body["HostConfig"]["Links"] = link
end
local pull_image = function(image)
local json_stringify = luci.jsonc and luci.jsonc.stringify
docker:append_status("Images: " .. "pulling" .. " " .. image .. "...\n")
local res = dk.images:create({query = {fromImage=image}}, docker.pull_image_show_status_cb)
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 ".. image or res.body[#res.body].status == "Status: Image is up to date for ".. image)) then
docker:append_status("done\n")
else
res.code = (res.code == 200) and 500 or res.code
docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
end
end
docker:clear_status()
local exist_image = false
if image then
for _, v in ipairs (images) do
if v.RepoTags and v.RepoTags[1] == image then
exist_image = true
break
end
end
if not exist_image then
pull_image(image)
elseif data._force_pull == 1 then
pull_image(image)
end
end
create_body = docker.clear_empty_tables(create_body)
docker:append_status("Container: " .. "create" .. " " .. name .. "...")
local res = dk.containers:create({name = name, body = create_body})
if res and res.code == 201 then
docker:clear_status()
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
else
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
end
end
return m

View file

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

View file

@ -0,0 +1,154 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
require "luci.util"
local docker = require "luci.model.docker"
local uci = require "luci.model.uci"
function byte_format(byte)
local suff = {"B", "KB", "MB", "GB", "TB"}
for i=1, 5 do
if byte > 1024 and i < 5 then
byte = byte / 1024
else
return string.format("%.2f %s", byte, suff[i])
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>]])
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['4ApiVersion'] = {_key=translate("Api Version"),_value='-'}
docker_info_table['5NCPU'] = {_key=translate("CPUs"),_value='-'}
docker_info_table['6MemTotal'] = {_key=translate("Total Memory"),_value='-'}
docker_info_table['7DockerRootDir'] = {_key=translate("Docker Root Dir"),_value='-'}
docker_info_table['8IndexServerAddress'] = {_key=translate("Index Server Address"),_value='-'}
docker_info_table['9RegistryMirrors'] = {_key=translate("Registry Mirrors"),_value='-'}
local s = map_dockerman:section(Table, docker_info_table)
s:option(DummyValue, "_key", translate("Info"))
s:option(DummyValue, "_value")
s = map_dockerman:section(SimpleSection)
s.containers_running = '-'
s.images_used = '-'
s.containers_total = '-'
s.images_total = '-'
s.networks_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
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
s.template = "dockerman/overview"
local section_dockerman = map_dockerman:section(NamedSection, "local", "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"))
socket_path.default = "/var/run/docker.sock"
socket_path.placeholder = "/var/run/docker.sock"
socket_path.rmempty = false
local remote_endpoint = section_dockerman:taboption("dockerman", Flag, "remote_endpoint", translate("Remote Endpoint"), translate("Dockerman connect to remote endpoint"))
remote_endpoint.rmempty = false
remote_endpoint.enabled = "true"
remote_endpoint.disabled = "false"
local remote_host = section_dockerman:taboption("dockerman", Value, "remote_host", translate("Remote Host"))
remote_host.placeholder = "10.1.1.2"
-- remote_host:depends("remote_endpoint", "true")
local remote_port = section_dockerman:taboption("dockerman", Value, "remote_port", translate("Remote Port"))
remote_port.placeholder = "2375"
remote_port.default = "2375"
-- remote_port:depends("remote_endpoint", "true")
-- 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
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"))
local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {}
for i, v in ipairs(interfaces) do
allowed_interface:value(v, v)
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"))
dockerd_enable.enabled = "true"
dockerd_enable.rmempty = true
local data_root = section_dockerman:taboption("daemon", Value, "daemon_data_root", translate("Docker Root Dir"))
data_root.placeholder = "/opt/docker/"
local registry_mirrors = section_dockerman:taboption("daemon", DynamicList, "daemon_registry_mirrors", translate("Registry Mirrors"))
registry_mirrors:value("https://hub-mirror.c.163.com", "https://hub-mirror.c.163.com")
local log_level = section_dockerman:taboption("daemon", ListValue, "daemon_log_level", translate("Log Level"), translate('Set the logging level'))
log_level:value("debug", "debug")
log_level:value("info", "info")
log_level:value("warn", "warn")
log_level:value("error", "error")
log_level:value("fatal", "fatal")
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'))
hosts:value("unix:///var/run/docker.sock", "unix:///var/run/docker.sock")
hosts:value("tcp://0.0.0.0:2375", "tcp://0.0.0.0:2375")
hosts.rmempty = true
end
return map_dockerman

View file

@ -0,0 +1,116 @@
--[[
LuCI - Lua Configuration Interface
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 dk = docker.new()
local containers, volumes
local 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
function get_volumes()
local data = {}
for i, v in ipairs(volumes) do
-- local index = v.CreatedAt .. v.Name
local index = v.Name
data[index]={}
data[index]["_selected"] = 0
data[index]["_nameraw"] = v.Name
data[index]["_name"] = v.Name:sub(1,12)
for ci,cv in ipairs(containers) do
if cv.Mounts and type(cv.Mounts) ~= "table" then break end
for vi, vv in ipairs(cv.Mounts) do
if v.Name == vv.Name then
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>'
end
end
end
data[index]["_driver"] = v.Driver
data[index]["_mountpoint"] = nil
for v1 in v.Mountpoint:gmatch('[^/]+') do
if v1 == index then
data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..."
else
data[index]["_mountpoint"] = (data[index]["_mountpoint"] and data[index]["_mountpoint"] or "").."/".. v1
end
end
data[index]["_created"] = v.CreatedAt
end
return data
end
local volume_list = get_volumes()
-- m = Map("docker", translate("Docker"))
m = SimpleForm("docker", translate("Docker"))
m.submit=false
m.reset=false
volume_table = m:section(Table, volume_list, translate("Volumes"))
volume_selecter = volume_table:option(Flag, "_selected","")
volume_selecter.disabled = 0
volume_selecter.enabled = 1
volume_selecter.default = 0
volume_id = volume_table:option(DummyValue, "_name", translate("Name"))
volume_table:option(DummyValue, "_driver", translate("Driver"))
volume_table:option(DummyValue, "_containers", translate("Containers")).rawhtml = true
volume_table:option(DummyValue, "_mountpoint", translate("Mount Point"))
volume_table:option(DummyValue, "_created", translate("Created"))
volume_selecter.write = function(self, section, value)
volume_list[section]._selected = value
end
docker_status = m:section(SimpleSection)
docker_status.template = "dockerman/apply_widget"
docker_status.err=docker:read_status()
docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if docker_status.err then docker:clear_status() end
action = m:section(Table,{{}})
action.notitle=true
action.rowcolors=false
action.template="cbi/nullsection"
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)
local volume_selected = {}
-- 遍历table中sectionid
local volume_table_sids = volume_table:cfgsections()
for _, volume_table_sid in ipairs(volume_table_sids) do
-- 得到选中项的名字
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
return m

View file

@ -0,0 +1,397 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
require "luci.util"
local docker = require "luci.docker"
local uci = (require "luci.model.uci").cursor()
local _docker = {}
--pull image and return iamge id
local update_image = function(self, image_name)
local json_stringify = luci.jsonc and luci.jsonc.stringify
_docker:append_status("Images: " .. "pulling" .. " " .. image_name .. "...\n")
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")
else
res.body.message = res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)
end
new_image_id = self.images:inspect({name = image_name}).body.Id
return new_image_id, res
end
local table_equal = function(t1, t2)
if not t1 then return true end
if not t2 then return false end
if #t1 ~= #t2 then return false end
for i, v in ipairs(t1) do
if t1[i] ~= t2[i] then return false end
end
return true
end
local table_subtract = function(t1, t2)
if not t1 or next(t1) == nil then return nil end
if not t2 or next(t2) == nil then return t1 end
local res = {}
for _, v1 in ipairs(t1) do
local found = false
for _, v2 in ipairs(t2) do
if v1 == v2 then
found= true
break
end
end
if not found then
table.insert(res, v1)
end
end
return next(res) == nil and nil or res
end
local map_subtract = function(t1, t2)
if not t1 or next(t1) == nil then return nil end
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
-- 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
end
_docker.clear_empty_tables = function ( t )
local k, v
if next(t) == nil then
t = nil
else
for k, v in pairs(t) do
if type(v) == 'table' then
t[k] = _docker.clear_empty_tables(v)
end
end
end
return t
end
-- return create_body, extra_network
local get_config = function(container_config, image_config)
local config = container_config.Config
local old_host_config = container_config.HostConfig
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
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
-- handle hostconfig
local host_config = old_host_config
-- if host_config.PortBindings and next(host_config.PortBindings) == nil then host_config.PortBindings = nil end
-- host_config.LogConfig = nil
host_config.Mounts = {}
-- 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
-- merge configs
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
local upgrade = function(self, request)
_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)
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 .. "...")
res = self.containers:stop({name = container_name})
if res and res.code < 305 then
_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
-- handle config
local image_config = self.images:inspect({id = old_image_id}).body.Config
local create_body, extra_network = get_config(container_info.body, image_config)
-- 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
local duplicate_config = function (self, request)
local container_info = self.containers:inspect({id = request.id})
if container_info.code > 300 and type(container_info.body) == "table" then return nil end
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
_docker.new = function(option)
local option = option or {}
local remote = uci:get("dockerman", "local", "remote_endpoint")
options = {
host = (remote == "true") and (option.host or uci:get("dockerman", "local", "remote_host")) or 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,
debug_path = option.debug_path or uci:get("dockerman", "local", "debug_path")
}
options.socket_path = (remote ~= "true" or not options.host or not options.port) and (option.socket_path or uci:get("dockerman", "local", "socket_path") or "/var/run/docker.sock") or nil
local _new = docker.new(options)
_new.options.status_path = uci:get("dockerman", "local", "status_path")
_new.containers_upgrade = upgrade
_new.containers_duplicate_config = duplicate_config
return _new
end
_docker.options={}
_docker.options.status_path = uci:get("dockerman", "local", "status_path")
_docker.append_status=function(self,val)
if not val then return end
local file_docker_action_status=io.open(self.options.status_path, "a+")
file_docker_action_status:write(val)
file_docker_action_status:close()
end
_docker.write_status=function(self,val)
if not val then return end
local file_docker_action_status=io.open(self.options.status_path, "w+")
file_docker_action_status:write(val)
file_docker_action_status:close()
end
_docker.read_status=function(self)
return nixio.fs.readfile(self.options.status_path)
end
_docker.clear_status=function(self)
nixio.fs.remove(self.options.status_path)
end
local status_cb = function(res, source, handler)
res.body = res.body or {}
while true do
local chunk = source()
if chunk then
--standard output to res.body
table.insert(res.body, chunk)
handler(chunk)
else
return
end
end
end
--{"status":"Pulling from library\/debian","id":"latest"}
--{"status":"Pulling fs layer","progressDetail":[],"id":"50e431f79093"}
--{"status":"Downloading","progressDetail":{"total":50381971,"current":2029978},"id":"50e431f79093","progress":"[==> ] 2.03MB\/50.38MB"}
--{"status":"Download complete","progressDetail":[],"id":"50e431f79093"}
--{"status":"Extracting","progressDetail":{"total":50381971,"current":17301504},"id":"50e431f79093","progress":"[=================> ] 17.3MB\/50.38MB"}
--{"status":"Pull complete","progressDetail":[],"id":"50e431f79093"}
--{"status":"Digest: sha256:a63d0b2ecbd723da612abf0a8bdb594ee78f18f691d7dc652ac305a490c9b71a"}
--{"status":"Status: Downloaded newer image for debian:latest"}
_docker.pull_image_show_status_cb = function(res, source)
return status_cb(res, source, function(chunk)
local json_parse = luci.jsonc.parse
local step = json_parse(chunk)
if type(step) == "table" then
local buf = _docker:read_status()
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"
if step.id then buf, num = buf:gsub("\t"..step.id .. ": .-\n", str) end
if num == 0 then
buf = buf .. str
end
_docker:write_status(buf)
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":"Importing","progressDetail":{"current":1572391,"total":3821714},"progress":"[====================\u003e ] 1.572MB/3.822MB"}
--{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}
_docker.import_image_show_status_cb = function(res, source)
return status_cb(res, source, function(chunk)
local json_parse = luci.jsonc.parse
local step = json_parse(chunk)
if type(step) == "table" then
local buf = _docker:read_status()
local num = 0
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 num == 0 then
buf = buf .. str
end
_docker:write_status(buf)
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)
if not nixio.fs.access("/etc/config/network") or not nixio.fs.access("/etc/config/firewall") then return end
if uci:get("dockerman", "local", "remote_endpoint") == "true" then return end
local ip = require "luci.ip"
local if_name = "docker_"..name
local dev_name = "macvlan_"..name
local net_mask = tostring(ip.new(subnet):mask())
local lan_interfaces
-- add macvlan device
uci:delete("network", dev_name)
uci:set("network", dev_name, "device")
uci:set("network", dev_name, "name", dev_name)
uci:set("network", dev_name, "ifname", device)
uci:set("network", dev_name, "type", "macvlan")
uci:set("network", dev_name, "mode", "bridge")
-- add macvlan interface
uci:delete("network", if_name)
uci:set("network", if_name, "interface")
uci:set("network", if_name, "proto", "static")
uci:set("network", if_name, "ifname", dev_name)
uci:set("network", if_name, "ipaddr", gateway)
uci:set("network", if_name, "netmask", net_mask)
uci:foreach("firewall", "zone", function(s)
if s.name == "lan" then
local interfaces
if type(s.network) == "table" then
interfaces = table.concat(s.network, " ")
uci:delete("firewall", s[".name"], "network")
else
interfaces = s.network and s.network or ""
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
_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 uci:get("dockerman", "local", "remote_endpoint") == "true" then return end
local if_name = "docker_"..name
local dev_name = "macvlan_"..name
uci:foreach("firewall", "zone", function(s)
if s.name == "lan" then
local interfaces
if type(s.network) == "table" then
interfaces = table.concat(s.network, " ")
else
interfaces = s.network and s.network or ""
end
interfaces = interfaces and interfaces:gsub(if_name, "")
interfaces = interfaces and interfaces:gsub("%s+", " ")
uci:set("firewall", s[".name"], "network", interfaces)
end
end)
uci:commit("firewall")
uci:delete("network", dev_name)
uci:delete("network", if_name)
uci:commit("network")
os.execute("ip link del " .. if_name)
end
return _docker

View file

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

View file

@ -0,0 +1,7 @@
<div style="display: inline-block;">
<% if self:cfgvalue(section) ~= false then %>
<input class="cbi-button cbi-button-<%=self.inputstyle or "button" %>" type="submit"" <% if self.disable then %>disabled <% end %><%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> />
<% else %>
-
<% end %>
</div>

View file

@ -0,0 +1,33 @@
<div style="display: inline-block;">
<!-- <%- if self.title then -%>
<label class="cbi-value-title"<%= attr("for", cbid) %>>
<%- if self.titleref then -%><a title="<%=self.titledesc or translate('Go to relevant configuration page')%>" class="cbi-title-ref" href="<%=self.titleref%>"><%- end -%>
<%-=self.title-%>
<%- if self.titleref then -%></a><%- end -%>
</label>
<%- end -%> -->
<%- if self.password then -%>
<input type="password" style="position:absolute; left:-100000px" aria-hidden="true"<%=
attr("name", "password." .. cbid)
%> />
<%- end -%>
<input data-update="change"<%=
attr("id", cbid) ..
attr("name", cbid) ..
attr("type", self.password and "password" or "text") ..
attr("class", self.password and "cbi-input-password" or "cbi-input-text") ..
attr("value", self:cfgvalue(section) or self.default) ..
ifattr(self.password, "autocomplete", "new-password") ..
ifattr(self.size, "size") ..
ifattr(self.placeholder, "placeholder") ..
ifattr(self.readonly, "readonly") ..
ifattr(self.maxlength, "maxlength") ..
ifattr(self.datatype, "data-type", self.datatype) ..
ifattr(self.datatype, "data-optional", self.optional or self.rmempty) ..
ifattr(self.combobox_manual, "data-manual", self.combobox_manual) ..
ifattr(#self.keylist > 0, "data-choices", { self.keylist, self.vallist })
%> />
<%- if self.password then -%>
<div class="cbi-button cbi-button-neutral" title="<%:Reveal/hide password%>" onclick="var e = this.previousElementSibling; e.type = (e.type === 'password') ? 'text' : 'password'"></div>
<% end %>
</div>

View file

@ -0,0 +1,9 @@
<% if self:cfgvalue(self.section) then section = self.section %>
<div class="cbi-section" id="cbi-<%=self.config%>-<%=section%>">
<%+cbi/tabmenu%>
<div class="cbi-section-node<% if self.tabs then %> cbi-section-node-tabbed<% end %>" id="cbi-<%=self.config%>-<%=section%>">
<%+cbi/ucisection%>
</div>
</div>
<% end %>
<!-- /nsection -->

View file

@ -0,0 +1,10 @@
<%+cbi/valueheader%>
<input type="hidden" value="1"<%=
attr("name", "cbi.cbe." .. self.config .. "." .. section .. "." .. self.option)
%> />
<input class="cbi-input-checkbox" data-update="click change" type="checkbox" <% if self.disable == 1 then %>disabled <% end %><%=
attr("id", cbid) .. attr("name", cbid) .. attr("value", self.enabled or 1) ..
ifattr((self:cfgvalue(section) or self.default) == self.enabled, "checked", "checked")
%> />
<label<%= attr("for", cbid)%>></label>
<%+cbi/valuefooter%>

View file

@ -0,0 +1,27 @@
<br>
<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_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_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_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>
</ul>
<script type="text/javascript">
let re = /\/admin\/docker\/container\//
let p = window.location.href
let path = p.split(re)
let container_id = path[1].split('/')[0] || path[1]
let action = path[1].split('/')[1] || "info"
let actions=["info","resources","stats","file","console","logs","inspect"]
actions.forEach(function(item) {
document.getElementById("a-cbi-tab-container_" + item).href= path[0]+"/admin/docker/container/"+container_id+'/'+item
if (action === item) {
document.getElementById("cbi-tab-container_" + item).className="cbi-tab"
} else {
document.getElementById("cbi-tab-container_" + item).className="cbi-tab-disabled"
}
})
</script>

View file

@ -0,0 +1,6 @@
<div class="cbi-map">
<iframe id="terminal" style="width: 100%; min-height: 500px; border: none; border-radius: 3px;"></iframe>
</div>
<script type="text/javascript">
document.getElementById("terminal").src = "http://" + window.location.hostname + ":7682";
</script>

View file

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

View file

@ -0,0 +1,80 @@
<script type="text/javascript">//<![CDATA[
let last_bw_tx
let last_bw_rx
let interval = 3
function progressbar(v, m, pc, np, f) {
m = m || 100
return String.format(
'<div style="width:100%%; max-width:500px; position:relative; border:1px solid #999999">' +
'<div style="background-color:#CCCCCC; width:%d%%; height:15px">' +
'<div style="position:absolute; left:0; top:0; text-align:center; width:100%%; color:#000000">' +
'<small>%s '+(f?f:'/')+' %s ' + (np ? "" : '(%d%%)') + '</small>' +
'</div>' +
'</div>' +
'</div>', pc, v, m, pc, f
);
}
function niceBytes(bytes, decimals) {
if (bytes == 0) return '0 Bytes';
var k = 1000,
dm = decimals + 1 || 3,
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
XHR.poll(interval, '<%=luci.dispatcher.build_url("admin/docker/container_stats")%>/<%=self.container_id%>', { status: 1 },
function (x, info) {
var e;
if (e = document.getElementById('cbi-table-cpu-value'))
e.innerHTML = progressbar(
(info.cpu_percent), 100, (info.cpu_percent ? info.cpu_percent : 0));
if (e = document.getElementById('cbi-table-memory-value'))
e.innerHTML = progressbar(
niceBytes(info.memory.mem_useage),
niceBytes(info.memory.mem_limit),
((100 / (info.memory.mem_limit ? info.memory.mem_limit : 100)) * (info.memory.mem_useage ? info.memory.mem_useage : 0))
);
for (var eth in info.bw_rxtx) {
if (!document.getElementById("cbi-table-speed_" + eth + "-value")) {
let tab = document.getElementById("cbi-table-cpu").parentNode
let div = document.getElementById('cbi-table-cpu').cloneNode(true);
div.id = "cbi-table-speed_" + eth;
div.children[0].innerHTML = "<%:Upload/Download%>: " + eth
div.children[1].id = "cbi-table-speed_" + eth + "-value"
tab.appendChild(div)
}
if (!document.getElementById("cbi-table-network_" + eth + "-value")) {
let tab = document.getElementById("cbi-table-cpu").parentNode
let div = document.getElementById('cbi-table-cpu').cloneNode(true);
div.id = "cbi-table-network_" + eth;
div.children[0].innerHTML = "<%:TX/RX%>: " + eth
div.children[1].id = "cbi-table-network_" + eth + "-value"
tab.appendChild(div)
}
e = document.getElementById("cbi-table-network_" + eth + "-value")
e.innerHTML = progressbar(
'↑'+niceBytes(info.bw_rxtx[eth].bw_tx),
'↓'+niceBytes(info.bw_rxtx[eth].bw_rx),
null,
true, " "
);
e = document.getElementById("cbi-table-speed_" + eth + "-value")
if (! last_bw_tx) last_bw_tx = info.bw_rxtx[eth].bw_tx
if (! last_bw_rx) last_bw_rx = info.bw_rxtx[eth].bw_rx
e.innerHTML = progressbar(
'↑'+niceBytes((info.bw_rxtx[eth].bw_tx - last_bw_tx)/interval)+'/s',
'↓'+niceBytes((info.bw_rxtx[eth].bw_rx - last_bw_rx)/interval)+'/s',
null,
true, " "
);
last_bw_tx = info.bw_rxtx[eth].bw_tx
last_bw_rx = info.bw_rxtx[eth].bw_rx
}
});
//]]></script>

View file

@ -0,0 +1,88 @@
<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" />
<div style="display: inline-block;">
<input type="button"" class=" 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;" />
</div>
<script type="text/javascript">
let btnImport = document.getElementById('btnimport')
let valISrc = document.getElementById('isrc')
let valITag = document.getElementById('itag')
btnImport.onclick = function (e) {
if (valISrc.value == "") {
document.getElementById("file_import").click()
return
} else {
let formData = new FormData()
formData.append('src', valISrc.value)
formData.append('tag', valITag.value)
let xhr = new XMLHttpRequest()
uci_confirm_docker()
xhr.open("POST", "<%=luci.dispatcher.build_url('admin/docker/images_import')%>", true)
xhr.onload = function () {
location.reload()
}
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 new_tag = prompt("<%:New tag%>\n<%:Image%>" + "ID: " + image_id + "\n<%:Please input new tag%>:", "")
if (new_tag) {
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_tag')%>",
{ id: image_id, tag: new_tag },
function (r) {
if (r.status == 201) {
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()
})
}
})
}
}
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>

View file

@ -0,0 +1,29 @@
<div style="display: inline-block;">
<input type="button"" class="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" />
</div>
<script type="text/javascript">
let btnLoad = document.getElementById('btnload')
btnLoad.onclick = function (e) {
document.getElementById("file_load").click()
e.preventDefault()
}
let fileLoad = document.getElementById('file_load')
fileLoad.onchange = function(e){
let fileLoad = document.getElementById('file_load')
if (!fileLoad.value) {
return
}
let fileName = fileLoad.files[0].name
let formData = new FormData()
formData.append('upload-filename', fileName)
formData.append('upload-archive', fileLoad.files[0])
let xhr = new XMLHttpRequest()
uci_confirm_docker()
xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/images_load")%>', true)
xhr.onload = function() {
location.reload()
}
xhr.send(formData)
}
</script>

View file

@ -0,0 +1,13 @@
<% if self.title == "Events" then %>
<%+header%>
<h2 name="content"><%:Docker%></h2>
<div class="cbi-section">
<h3><%:Events%></h3>
<% end %>
<div id="content_syslog">
<textarea readonly="readonly" wrap="off" rows="<%=self.syslog:cmatch('\n')+2%>" id="syslog"><%=self.syslog:pcdata()%></textarea>
</div>
<% if self.title == "Events" then %>
</div>
<%+footer%>
<% end %>

View file

@ -0,0 +1,95 @@
<style type="text/css">
#dialog_reslov {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
display: none;
z-index: 20000;
}
#dialog_reslov .dialog_box {
position: relative;
background: rgba(255, 255, 255);
top: 10%;
width: 50%;
margin: auto;
display: flex;
flex-wrap: wrap;
height:auto;
align-items: center;
}
#dialog_reslov .dialog_line {
margin-top: .5em;
margin-bottom: .5em;
margin-left: 2em;
margin-right: 2em;
}
#dialog_reslov .dialog_box>h4,
#dialog_reslov .dialog_box>p,
#dialog_reslov .dialog_box>div {
flex-basis: 100%;
}
#dialog_reslov .dialog_box>img {
margin-right: 1em;
flex-basis: 32px;
}
body.dialog-reslov-active {
overflow: hidden;
height: 100vh;
}
body.dialog-reslov-active #dialog_reslov {
display: block;
}
</style>
<script type="text/javascript">
function close_reslov_dialog() {
document.body.classList.remove('dialog-reslov-active')
document.documentElement.style.overflowY = 'scroll'
}
function reslov_container() {
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(){
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="cbi-button cbi-button-apply" type="submit" value="<%:Submit%>" onclick="reslov_container()" /> <input type="button" class="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>
<%+cbi/valueheader%>
<input type="button" class="cbi-button cbi-button-apply" value="<%:Command line%>" onclick="show_reslov_dialog()" />
<%+cbi/valuefooter%>

View file

@ -0,0 +1,280 @@
<style>
/*!
Pure v1.0.1
Copyright 2013 Yahoo!
Licensed under the BSD License.
https://github.com/pure-css/pure/blob/master/LICENSE.md
*/
.pure-g {
letter-spacing: -.31em;
text-rendering: optimizespeed;
font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-webkit-flex-flow: row wrap;
-ms-flex-flow: row wrap;
flex-flow: row wrap;
-webkit-align-content: flex-start;
-ms-flex-line-pack: start;
align-content: flex-start
}
.pure-u {
display: inline-block;
zoom: 1;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto
}
.pure-g [class*=pure-u] {
font-family: sans-serif
}
.pure-u-1-4,
.pure-u-2-5,
.pure-u-3-5 {
display: inline-block;
zoom: 1;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto
}
.pure-u-1-4 {
width: 25%
}
.pure-u-2-5 {
width: 40%
}
.pure-u-3-5 {
width: 60%
}
.status {
margin: 1rem -0.5rem 1rem -0.5rem;
}
.block {
margin: 0.5rem 0.5rem;
padding: 0;
font-weight: normal;
font-style: normal;
line-height: 1;
font-family: inherit;
min-width: inherit;
overflow-x: auto;
overflow-y: hidden;
border: 1px solid rgba(0, 0, 0, .05);
border-radius: .375rem;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .15);
}
.img-con {
margin: 1rem;
min-width: 4rem;
max-width: 4rem;
min-height: 4rem;
max-height: 4rem;
}
.block h4 {
font-size: .8125rem;
font-weight: 600;
margin: 1rem;
color: #8898aa !important;
line-height: 1.8em;
}
.cbi-section-table-cell {
position: relative;
}
@media screen and (max-width: 700px) {
.pure-u-1-4 {
width: 50%;
}
.cbi-button-add {
position: fixed;
padding: 0.3rem 0.5rem;
z-index: 1000;
width: 50px !important;
height: 50px;
bottom: 90px;
right: 5px;
font-size: 16px;
border-radius: 50%;
display: block;
background-color: #fb6340 !important;
border-color: #fb6340 !important;
box-shadow: 0 0 1rem 0 rgba(136, 152, 170, .75);
}
}
</style>
<div class="pure-g status">
<div class="pure-u-1-4">
<div class="block pure-g">
<div class="pure-u-2-5">
<div class="img-con">
<svg role="img" 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>
</div>
</div>
<div class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Containers%></h4>
<h4 style="text-align: right;">
<%- if self.containers_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/containers")%>'><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.containers_running%></span>
<span style="font-size: 1rem; color: #8898aa !important;">/<%=self.containers_total%></span>
<%- if self.containers_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 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>
</div>
</div>
<div class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Images%></h4>
<h4 style="text-align: right;">
<%- if self.images_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/images")%>'><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.images_used%></span>
<span style="font-size: 1rem; color: #8898aa !important;">/<%=self.images_total%></span>
<%- if self.images_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 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>
</div>
</div>
<div class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Networks%></h4>
<h4 style="text-align: right;">
<%- if self.networks_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/networks")%>'><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.networks_total%></span>
<!-- <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>

View file

@ -0,0 +1,10 @@
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

@ -0,0 +1,46 @@
#!/bin/sh /etc/rc.common
START=99
DOCKERD_CONF="/etc/docker/daemon.json"
config_load dockerman
config_get daemon_ea "local" daemon_ea
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
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

@ -0,0 +1,15 @@
#!/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

@ -0,0 +1,20 @@
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

@ -0,0 +1,52 @@
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