1
0
Fork 0
mirror of https://github.com/Ysurac/openmptcprouter-feeds.git synced 2025-03-09 15:40:03 +00:00
This commit is contained in:
suyuan 2023-01-18 21:21:55 +08:00
parent 6b7f0b4dba
commit fd8b7384d5
104 changed files with 13356 additions and 31 deletions

View file

@ -0,0 +1,614 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
-- local uci = (require "luci.model.uci").cursor()
module("luci.controller.dockerman",package.seeall)
function index()
entry({"admin", "docker"},
alias("admin", "docker", "config"),
_("Docker"),
40).acl_depends = { "luci-app-dockerman" }
entry({"admin", "docker", "config"},cbi("dockerman/configuration"),_("Configuration"), 8).leaf=true
-- local uci = (require "luci.model.uci").cursor()
-- if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
-- local host = uci:get("dockerd", "dockerman", "remote_host")
-- local port = uci:get("dockerd", "dockerman", "remote_port")
-- if not host or not port then
-- return
-- end
-- else
-- local socket = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
-- if socket and not nixio.fs.access(socket) then
-- return
-- end
-- end
-- if (require "luci.model.docker").new():_ping().code ~= 200 then
-- return
-- end
entry({"admin", "docker", "overview"}, form("dockerman/overview"),_("Overview"), 2).leaf=true
entry({"admin", "docker", "containers"}, form("dockerman/containers"), _("Containers"), 3).leaf=true
entry({"admin", "docker", "images"}, form("dockerman/images"), _("Images"), 4).leaf=true
entry({"admin", "docker", "networks"}, form("dockerman/networks"), _("Networks"), 5).leaf=true
entry({"admin", "docker", "volumes"}, form("dockerman/volumes"), _("Volumes"), 6).leaf=true
entry({"admin", "docker", "events"}, call("action_events"), _("Events"), 7)
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", "containers_stats"}, call("action_get_containers_stats")).leaf=true
entry({"admin", "docker", "get_system_df"}, call("action_get_system_df")).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", "container_list_file"}, call("list_file")).leaf=true
entry({"admin", "docker", "container_remove_file"}, call("remove_file")).leaf=true
entry({"admin", "docker", "container_rename_file"}, call("rename_file")).leaf=true
entry({"admin", "docker", "container_export"}, call("export_container")).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_get_system_df()
local res = docker.new():df()
luci.http.status(res.code, res.message)
luci.http.prepare_content("application/json")
luci.http.write_json(res.body)
end
function scandir(id, directory)
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
if not cmd_docker or cmd_docker:match("^%s+$") then
return
end
local i, t, popen = 0, {}, io.popen
local uci = (require "luci.model.uci").cursor()
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
if remote and host and port then
hosts = "tcp://" .. host .. ':'.. port
elseif socket_path then
hosts = "unix://" .. socket_path
else
return
end
local pfile = popen(cmd_docker .. ' -H "'.. hosts ..'" exec ' ..id .." ls -lh \""..directory.."\" | egrep -v '^total'")
for fileinfo in pfile:lines() do
i = i + 1
t[i] = fileinfo
end
pfile:close()
return t
end
function list_response(id, path, success)
luci.http.prepare_content("application/json")
local result
if success then
local rv = scandir(id, path)
result = {
ec = 0,
data = rv
}
else
result = {
ec = 1
}
end
luci.http.write_json(result)
end
function list_file(id)
local path = luci.http.formvalue("path")
list_response(id, path, true)
end
function rename_file(id)
local filepath = luci.http.formvalue("filepath")
local newpath = luci.http.formvalue("newpath")
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
if not cmd_docker or cmd_docker:match("^%s+$") then
return
end
local uci = (require "luci.model.uci").cursor()
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
if remote and host and port then
hosts = "tcp://" .. host .. ':'.. port
elseif socket_path then
hosts = "unix://" .. socket_path
else
return
end
local success = os.execute(cmd_docker .. ' -H "'.. hosts ..'" exec '.. id ..' mv "'..filepath..'" "'..newpath..'"')
list_response(nixio.fs.dirname(filepath), success)
end
function remove_file(id)
local path = luci.http.formvalue("path")
local isdir = luci.http.formvalue("isdir")
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
if not cmd_docker or cmd_docker:match("^%s+$") then
return
end
local uci = (require "luci.model.uci").cursor()
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
if remote and host and port then
hosts = "tcp://" .. host .. ':'.. port
elseif socket_path then
hosts = "unix://" .. socket_path
else
return
end
path = path:gsub("<>", "/")
path = path:gsub(" ", "\ ")
local success
if isdir then
success = os.execute(cmd_docker .. ' -H "'.. hosts ..'" exec '.. id ..' rm -r "'..path..'"')
else
success = os.remove(path)
end
list_response(nixio.fs.dirname(path), success)
end
function action_events()
local logs = ""
local query ={}
local dk = docker.new()
query["until"] = os.time()
local events = dk:events({query = query})
if events.code == 200 then
for _, v in ipairs(events.body) do
local date = "unknown"
if v and v.time then
date = os.date("%Y-%m-%d %H:%M:%S", v.time)
end
local name = v.Actor.Attributes.name or "unknown"
local action = v.Action or "unknown"
if v and v.Type == "container" then
local id = v.Actor.ID or "unknown"
logs = logs .. string.format("[%s] %s %s Container ID: %s Container Name: %s\n", date, v.Type, action, id, name)
elseif v.Type == "network" then
local container = v.Actor.Attributes.container or "unknown"
local network = v.Actor.Attributes.type or "unknown"
logs = logs .. string.format("[%s] %s %s Container ID: %s Network Name: %s Network type: %s\n", date, v.Type, action, container, name, network)
elseif v.Type == "image" then
local id = v.Actor.ID or "unknown"
logs = logs .. string.format("[%s] %s %s Image: %s Image name: %s\n", date, v.Type, action, id, name)
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
local cpu_count = tonumber(d["cpu_stats"]["online_cpus"])
local cpu_percent = 0.0
local cpu_delta = tonumber(d["cpu_stats"]["cpu_usage"]["total_usage"]) - tonumber(d["precpu_stats"]["cpu_usage"]["total_usage"])
local system_delta = tonumber(d["cpu_stats"]["system_cpu_usage"]) -- tonumber(d["precpu_stats"]["system_cpu_usage"])
if system_delta > 0.0 then
cpu_percent = string.format("%.2f", cpu_delta / system_delta * 100.0 * cpu_count)
end
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[e] = {
bw_tx = tonumber(v.tx_bytes),
bw_rx = tonumber(v.rx_bytes)
}
end
end
return data
end
local function get_stat(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, ["one-shot"] = true}})
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)
return response.code, response.body.message, {
cpu_percent = cpu_percent,
memory = {
mem_useage = mem_useage,
mem_limit = mem_limit
},
bw_rxtx = bw_rxtx
}
else
return response.code, response.body.message
end
else
if response.code == 200 then
return 500, "container "..container_id.." not running"
else
return response.code, response.body.message
end
end
else
return 404, "No container name or id"
end
end
function action_get_container_stats(container_id)
local code, msg, res = get_stat(container_id)
luci.http.status(code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json(res)
end
function action_get_containers_stats()
local res = luci.http.formvalue(containers) or ""
local stats = {}
res = luci.jsonc.parse(res.containers)
if res and type(res) == "table" then
for i, v in ipairs(res) do
_,_,stats[v] = get_stat(v)
end
end
luci.http.status(200, "OK")
luci.http.prepare_content("application/json")
luci.http.write_json(stats)
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 export_container(id)
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="'.. id ..'.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:export({id = id}, cb)
end
function download_archive()
local id = luci.http.formvalue("id")
local path = luci.http.formvalue("path")
local filename = luci.http.formvalue("filename") or "archive"
local dk = docker.new()
local first
local cb = function(res, chunk)
if res and res.code and res.code == 200 then
if not first then
first = true
luci.http.header('Content-Disposition', 'inline; filename="'.. filename .. '.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.status(res and res.code or 500, msg or "unknow")
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 = luci.http.urlencode(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 = luci.http.urlencode(path)
},
body = rec_send
})
local msg = res and res.message or res.body and res.body.message or nil
luci.http.status(res and res.code or 500, msg or "unknow")
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg or "unknow"})
end
-- function save_images()
-- 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" .. " " .. names .. "...")
-- local res = dk.images:get({
-- query = {
-- names = luci.http.urlencode(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 archive = luci.http.formvalue("upload-archive")
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})
local msg = res and res.body and ( res.body.message or res.body.stream or res.body.error ) or nil
if res and res.code == 200 and msg and msg:match("Loaded image ID") then
docker:clear_status()
else
docker:append_status("code:" .. (res and res.code or "500") .." ".. (msg or "unknow"))
end
luci.http.status(res and res.code or 500, msg or "unknow")
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg or "unknow"})
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 = luci.http.urlencode(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
msg = res.body.status or res.body.error
elseif not msg and #res.body >= 1 then
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 and res.code or "500") .." ".. (msg or "unknow"))
end
luci.http.status(res and res.code or 500, msg or "unknow")
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg or "unknow"})
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 and res.code or 500, msg or "unknow")
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 or "unknow"})
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 and res.code or 500, msg or "unknow")
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 or "unknow"})
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 and res.code or 500, msg or "unknow")
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg or "unknow"})
end
end

View file

@ -0,0 +1,152 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2021 Florian Eckert <fe@dev.tdt.de>
Copyright 2021 lisaac <lisaac.cn@gmail.com>
]]--
local uci = (require "luci.model.uci").cursor()
local m, s, o
m = Map("dockerd",
translate("Docker - Configuration"),
translate("DockerMan is a simple docker manager client for LuCI"))
if nixio.fs.access("/usr/bin/dockerd") and not m.uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
s = m:section(NamedSection, "globals", "section", translate("Docker Daemon settings"))
o = s:option(Flag, "auto_start", translate("Auto start"))
o.rmempty = false
o.write = function(self, section, value)
if value == "1" then
luci.util.exec("/etc/init.d/dockerd enable")
else
luci.util.exec("/etc/init.d/dockerd disable")
end
m.uci:set("dockerd", "globals", "auto_start", value)
end
o = s:option(Value, "data_root",
translate("Docker Root Dir"))
o.placeholder = "/opt/docker/"
o:depends("remote_endpoint", 0)
o = s:option(Value, "bip",
translate("Default bridge"),
translate("Configure the default bridge network"))
o.placeholder = "172.17.0.1/16"
o.datatype = "ipaddr"
o:depends("remote_endpoint", 0)
o = s:option(DynamicList, "registry_mirrors",
translate("Registry Mirrors"),
translate("It replaces the daemon registry mirrors with a new set of registry mirrors"))
o:value("https://hub-mirror.c.163.com", "https://hub-mirror.c.163.com")
o:depends("remote_endpoint", 0)
o.forcewrite = true
o = s:option(ListValue, "log_level",
translate("Log Level"),
translate('Set the logging level'))
o:value("debug", translate("Debug"))
o:value("", translate("Info")) -- This is the default debug level from the deamon is optin is not set
o:value("warn", translate("Warning"))
o:value("error", translate("Error"))
o:value("fatal", translate("Fatal"))
o.rmempty = true
o:depends("remote_endpoint", 0)
o = s:option(DynamicList, "hosts",
translate("Client connection"),
translate('Specifies where the Docker daemon will listen for client connections (default: unix:///var/run/docker.sock)'))
o:value("unix:///var/run/docker.sock", "unix:///var/run/docker.sock")
o:value("tcp://0.0.0.0:2375", "tcp://0.0.0.0:2375")
o.rmempty = true
o:depends("remote_endpoint", 0)
end
s = m:section(NamedSection, "dockerman", "section", translate("DockerMan settings"))
s:tab("ac", translate("Access Control"))
s:tab("dockerman", translate("DockerMan"))
o = s:taboption("dockerman", Flag, "remote_endpoint",
translate("Remote Endpoint"),
translate("Connect to remote docker endpoint"))
o.rmempty = false
o.validate = function(self, value, sid)
local res = luci.http.formvaluetable("cbid.dockerd")
if res["dockerman.remote_endpoint"] == "1" then
if res["dockerman.remote_port"] and res["dockerman.remote_port"] ~= "" and res["dockerman.remote_host"] and res["dockerman.remote_host"] ~= "" then
return 1
else
return nil, translate("Please input the PORT or HOST IP of remote docker instance!")
end
else
if not res["dockerman.socket_path"] then
return nil, translate("Please input the SOCKET PATH of docker daemon!")
end
end
return 0
end
o = s:taboption("dockerman", Value, "socket_path",
translate("Docker Socket Path"))
o.default = "/var/run/docker.sock"
o.placeholder = "/var/run/docker.sock"
o:depends("remote_endpoint", 0)
o = s:taboption("dockerman", Value, "remote_host",
translate("Remote Host"),
translate("Host or IP Address for the connection to a remote docker instance"))
o.datatype = "host"
o.placeholder = "10.1.1.2"
o:depends("remote_endpoint", 1)
o = s:taboption("dockerman", Value, "remote_port",
translate("Remote Port"))
o.placeholder = "2375"
o.datatype = "port"
o:depends("remote_endpoint", 1)
-- o = s:taboption("dockerman", Value, "status_path", translate("Action Status Tempfile Path"), translate("Where you want to save the docker status file"))
-- o = s:taboption("dockerman", Flag, "debug", translate("Enable Debug"), translate("For debug, It shows all docker API actions of luci-app-dockerman in Debug Tempfile Path"))
-- o.enabled="true"
-- o.disabled="false"
-- o = s: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") and not m.uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
o = s: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
o:value(v, v)
end
o = s:taboption("ac", DynamicList, "ac_allowed_ports", translate("Ports allowed to be accessed"), translate("Which Port(s) can be accessed, it's not restricted by the Allowed Access interfaces configuration. Use this configuration with caution!"))
o.placeholder = "8080/tcp"
local docker = require "luci.model.docker"
local containers, res, lost_state
local dk = docker.new()
if dk:_ping().code ~= 200 then
lost_state = true
else
lost_state = false
res = dk.containers:list()
if res and res.code and res.code < 300 then
containers = res.body
end
end
-- allowed_container.placeholder = "container name_or_id"
if containers then
for i, v in ipairs(containers) do
if v.State == "running" and v.Ports then
for _, port in ipairs(v.Ports) do
if port.PublicPort and port.IP and not string.find(port.IP,":") then
o:value(port.PublicPort.."/"..port.Type, v.Names[1]:sub(2) .. " | " .. port.PublicPort .. " | " .. port.Type)
end
end
end
end
end
end
return m

View file

@ -0,0 +1,810 @@
--[[
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 m, s, o
local images, networks, container_info, res
if not container_id then
return
end
res = dk.containers:inspect({id = container_id})
if res.code < 300 then
container_info = res.body
else
return
end
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)
local res
docker:clear_status()
docker:append_status("Containers: " .. cmd .. " " .. container_id .. "...")
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",
translatef("Docker - Container (%s)", container_info.Name:sub(2)),
translate("On this page, the selected container can be managed."))
m.redirect = luci.dispatcher.build_url("admin/docker/containers")
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template = "cbi/nullsection"
o = s:option(Button, "_start")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Start")
o.inputstyle = "apply"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"start")
end
o = s:option(Button, "_restart")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Restart")
o.inputstyle = "reload"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"restart")
end
o = s:option(Button, "_stop")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Stop")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"stop")
end
o = s:option(Button, "_kill")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Kill")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"kill")
end
o = s:option(Button, "_export")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Export")
o.inputstyle = "apply"
o.forcewrite = true
o.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container_export/"..container_id))
end
o = s:option(Button, "_upgrade")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Upgrade")
o.inputstyle = "reload"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"upgrade")
end
o = s:option(Button, "_duplicate")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Duplicate/Edit")
o.inputstyle = "add"
o.forcewrite = true
o.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer/duplicate/"..container_id))
end
o = s:option(Button, "_remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle=translate("Remove")
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"remove")
end
s = m:section(SimpleSection)
s.template = "dockerman/container"
if action == "info" then
res = dk.networks:list()
if res.code < 300 then
networks = res.body
else
return
end
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 "-"
},
}
if container_info.State.Status == "running" then
table_info["06start"] = {
_key = translate("Start Time"),
_value = container_info.State and container_info.State.StartedAt or "-"
}
else
table_info["06start"] = {
_key = translate("Finish Time"),
_value = container_info.State and container_info.State.FinishedAt or "-"
}
end
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 and 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")
}
s = m:section(Table,table_info)
s.nodescr=true
s.formvalue=function(self, section)
return table_info
end
o = s:option(DummyValue, "_key", translate("Info"))
o.width = "20%"
o = s:option(ListValue, "_value")
o.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
o.forcewrite = true
o.write = function(self, section, value)
table_info[section]._value=value
end
o.validate = function(self, value)
return value
end
o = s:option(Value, "_opts")
o.forcewrite = true
o.write = function(self, section, value)
table_info[section]._opts=value
end
o.validate = function(self, value)
return value
end
o.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
o = s:option(Button, "_button")
o.forcewrite = true
o.render = function(self, section, scope)
if table_info[section]._button and table_info[section]._value ~= nil then
self.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
o.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
_, _, 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
elseif action == "resources" then
s = m:section(SimpleSection)
o = s:option( Value, "cpus",
translate("CPUs"),
translate("Number of CPUs. Number is a fractional number. 0.000 means no limit."))
o.placeholder = "1.5"
o.rmempty = true
o.datatype="ufloat"
o.default = container_info.HostConfig.NanoCpus / (10^9)
o = s:option(Value, "cpushares",
translate("CPU Shares Weight"),
translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024."))
o.placeholder = "1024"
o.rmempty = true
o.datatype="uinteger"
o.default = container_info.HostConfig.CpuShares
o = s:option(Value, "memory",
translate("Memory"),
translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M."))
o.placeholder = "128m"
o.rmempty = true
o.default = container_info.HostConfig.Memory ~=0 and ((container_info.HostConfig.Memory / 1024 /1024) .. "M") or 0
o = s:option(Value, "blkioweight",
translate("Block IO Weight"),
translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000."))
o.placeholder = "500"
o.rmempty = true
o.datatype="uinteger"
o.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
m.submit = false
m.reset = false
s= m:section(SimpleSection)
s.template = "dockerman/container_file_manager"
s.container = container_id
m.redirect = nil
elseif action == "inspect" then
s = m:section(SimpleSection)
s.syslog = luci.jsonc.stringify(container_info, true)
s.title = translate("Container Inspect")
s.template = "dockerman/logs"
m.submit = false
m.reset = false
elseif action == "logs" then
local logs = ""
local query ={
stdout = 1,
stderr = 1,
tail = 1000
}
s = m:section(SimpleSection)
logs = dk.containers:logs({id = container_id, query = query})
if logs.code == 200 then
s.syslog=logs.body
else
s.syslog="Get Logs ERROR\n"..logs.code..": "..logs.body
end
s.title=translate("Container Logs")
s.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("command -v docker"):match("^.+docker") or nil
local cmd_ttyd = luci.util.exec("command -v ttyd"):match("^.+ttyd") or nil
if cmd_docker and cmd_ttyd and container_info.State.Status == "running" then
local cmd = "/bin/sh"
local uid
s = m:section(SimpleSection)
o = s:option(Value, "command", translate("Command"))
o:value("/bin/sh", "/bin/sh")
o:value("/bin/ash", "/bin/ash")
o:value("/bin/bash", "/bin/bash")
o.default = "/bin/sh"
o.forcewrite = true
o.write = function(self, section, value)
cmd = value
end
o = s:option(Value, "uid", translate("UID"))
o.forcewrite = true
o.write = function(self, section, value)
uid = value
end
o = s:option(Button, "connect")
o.render = function(self, section, scope)
self.inputstyle = "add"
self.title = " "
self.inputtitle = translate("Connect")
Button.render(self, section, scope)
end
o.write = function(self, section)
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
local cmd_ttyd = luci.util.exec("command -v 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 uci = (require "luci.model.uci").cursor()
local ttyd_ssl = uci:get("ttyd", "@ttyd[0]", "ssl")
local ttyd_ssl_key = uci:get("ttyd", "@ttyd[0]", "ssl_key")
local ttyd_ssl_cert = uci:get("ttyd", "@ttyd[0]", "ssl_cert")
if ttyd_ssl == "1" and ttyd_ssl_cert and ttyd_ssl_key then
cmd_ttyd = string.format('%s -S -C %s -K %s', cmd_ttyd, ttyd_ssl_cert, ttyd_ssl_key)
end
local pid = luci.util.trim(luci.util.exec("netstat -lnpt | grep :7682 | grep ttyd | tr -s ' ' | cut -d ' ' -f7 | cut -d'/' -f1"))
if pid and pid ~= "" then
luci.util.exec("kill -9 " .. pid)
end
local hosts
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint") or false
local host = nil
local port = nil
local socket = nil
if remote then
host = uci:get("dockerd", "dockerman", "remote_host") or nil
port = uci:get("dockerd", "dockerman", "remote_port") or nil
else
socket = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
end
if remote and host and port then
hosts = "tcp://" .. host .. ':'.. port
elseif socket then
hosts = "unix://" .. socket
else
return
end
if uid and uid ~= "" then
uid = "-u " .. uid
else
uid = ""
end
local start_cmd = string.format('%s -d 2 --once -p 7682 %s -H "%s" exec -it %s %s %s&', cmd_ttyd, cmd_docker, hosts, uid, container_id, cmd)
os.execute(start_cmd)
o = s:option(DummyValue, "console")
o.container_id = container_id
o.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
s = m:section(SimpleSection)
s.container_id = container_id
s.template = "dockerman/container_stats"
table_stats = {
cpu={
key=translate("CPU Useage"),
value='-'
},
memory={
key=translate("Memory Useage"),
value='-'
}
}
container_top = response.body
s = m:section(Table, table_stats, translate("Stats"))
s:option(DummyValue, "key", translate("Stats")).width="33%"
s:option(DummyValue, "value")
top_section = m:section(Table, container_top.Processes, translate("TOP"))
for i, v in ipairs(container_top.Titles) do
top_section:option(DummyValue, i, translate(v))
end
end
m.submit = false
m.reset = false
end
return m

View file

@ -0,0 +1,284 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local http = require "luci.http"
local docker = require "luci.model.docker"
local m, s, o
local images, networks, containers, res, lost_state
local urlencode = luci.http.protocol and luci.http.protocol.urlencode or luci.util.urlencode
local dk = docker.new()
if dk:_ping().code ~= 200 then
lost_state = true
else
res = dk.images:list()
if res and res.code and res.code < 300 then
images = res.body
end
res = dk.networks:list()
if res and res.code and res.code < 300 then
networks = res.body
end
res = dk.containers:list({
query = {
all = true
}
})
if res and res.code and res.code < 300 then
containers = res.body
end
end
function get_containers()
local data = {}
if type(containers) ~= "table" then
return nil
end
for i, v in ipairs(containers) do
local index = (10^12 - v.Created) .. "_id_" .. 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]["_status"] = v.Status
if v.Status:find("^Up") then
data[index]["_name"] = "<font color='green'>"..v.Names[1]:sub(2).."</font>"
data[index]["_status"] = "<a href='"..luci.dispatcher.build_url("admin/docker/container/"..v.Id).."/stats'><font color='green'>".. data[index]["_status"] .. "</font>" .. "<br><font color='#9f9f9f' class='container_cpu_status'></font><br><font color='#9f9f9f' class='container_mem_status'></font><br><font color='#9f9f9f' class='container_network_status'></font></a>"
else
data[index]["_name"] = "<font color='red'>"..v.Names[1]:sub(2).."</font>"
data[index]["_status"] = '<font class="container_not_running" 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
local ip = require "luci.ip"
for _,v2 in ipairs(v.Ports) do
-- display ipv4 only
if ip.new(v2.IP or "0.0.0.0"):is4() then
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
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") .. ":&lt;none&gt;")
end
end
data[index]["_id_name"] = '<a href='..luci.dispatcher.build_url("admin/docker/container/"..v.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. data[index]["_name"] .. "<br><font color='#9f9f9f'>ID: " .. data[index]["_id"]
.. "</font></a><br>Image: " .. (data[index]["_image"] or "&lt;none&gt;")
.. "<br><font color='#9f9f9f' class='container_size_".. v.Id .."'></font>"
if type(v.Mounts) == "table" and next(v.Mounts) then
for _, v2 in pairs(v.Mounts) do
if v2.Type ~= "volume" then
local v_sorce_d, v_dest_d
local v_sorce = ""
local v_dest = ""
for v_sorce_d in v2["Source"]:gmatch('[^/]+') do
if v_sorce_d and #v_sorce_d > 12 then
v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,8) .. ".."
else
v_sorce = v_sorce .."/".. v_sorce_d
end
end
for v_dest_d in v2["Destination"]:gmatch('[^/]+') do
if v_dest_d and #v_dest_d > 12 then
v_dest = v_dest .. "/" .. v_dest_d:sub(1,8) .. ".."
else
v_dest = v_dest .."/".. v_dest_d
end
end
data[index]["_mounts"] = (data[index]["_mounts"] and (data[index]["_mounts"] .. "<br>") or "") .. '<span title="'.. v2.Source.. "" .. v2.Destination .. '" ><a href="'..luci.dispatcher.build_url("admin/docker/container/"..v.Id)..'/file?path='..v2["Destination"]..'">' .. v_sorce .. "" .. v_dest..'</a></span>'
end
end
end
data[index]["_image_id"] = v.ImageID:sub(8,20)
data[index]["_command"] = v.Command
end
return data
end
local container_list = not lost_state and get_containers() or {}
m = SimpleForm("docker",
translate("Docker - Containers"),
translate("This page displays all containers that have been created on the connected docker host."))
m.submit=false
m.reset=false
m:append(Template("dockerman/containers_running_stats"))
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table, container_list, translate("Containers"))
s.nodescr=true
s.config="containers"
o = s:option(Flag, "_selected","")
o.disabled = 0
o.enabled = 1
o.default = 0
o.width = "1%"
o.write=function(self, section, value)
container_list[section]._selected = value
end
-- o = s:option(DummyValue, "_id", translate("ID"))
-- o.width="10%"
-- o = s:option(DummyValue, "_name", translate("Container Name"))
-- o.rawhtml = true
o = s:option(DummyValue, "_id_name", translate("Container Info"))
o.rawhtml = true
o.width="15%"
o = s:option(DummyValue, "_status", translate("Status"))
o.width="15%"
o.rawhtml=true
o = s:option(DummyValue, "_network", translate("Network"))
o.width="10%"
o = s:option(DummyValue, "_ports", translate("Ports"))
o.width="5%"
o.rawhtml = true
o = s:option(DummyValue, "_mounts", translate("Mounts"))
o.width="25%"
o.rawhtml = true
-- o = s:option(DummyValue, "_image", translate("Image"))
-- o.width="8%"
o = s:option(DummyValue, "_command", translate("Command"))
o.width="15%"
local start_stop_remove = function(m, cmd)
local container_selected = {}
-- 遍历table中sectionid
for k in pairs(container_list) do
-- 得到选中项的名字
if container_list[k]._selected == 1 then
container_selected[#container_selected + 1] = container_list[k]["_id"]
end
end
if #container_selected > 0 then
local success = true
docker:clear_status()
for _, cont in ipairs(container_selected) do
docker:append_status("Containers: " .. cmd .. " " .. cont .. "...")
local res = dk.containers[cmd](dk, {id = cont})
if res and res.code 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
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template="cbi/nullsection"
o = s:option(Button, "_new")
o.inputtitle = translate("Add")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "add"
o.forcewrite = true
o.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
end
o.disable = lost_state
o = s:option(Button, "_start")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Start")
o.inputstyle = "apply"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"start")
end
o.disable = lost_state
o = s:option(Button, "_restart")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Restart")
o.inputstyle = "reload"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"restart")
end
o.disable = lost_state
o = s:option(Button, "_stop")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Stop")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"stop")
end
o.disable = lost_state
o = s:option(Button, "_kill")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Kill")
o.inputstyle = "reset"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m,"kill")
end
o.disable = lost_state
o = s:option(Button, "_remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Remove")
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
start_stop_remove(m, "remove")
end
o.disable = lost_state
return m

View file

@ -0,0 +1,284 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local dk = docker.new()
local containers, images, res, lost_state
local m, s, o
if dk:_ping().code ~= 200 then
lost_state = true
else
res = dk.images:list()
if res and res.code and res.code < 300 then
images = res.body
end
res = dk.containers:list({ query = { all = true } })
if res and res.code and res.code < 300 then
containers = res.body
end
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
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;")
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 = not lost_state and get_images() or {}
m = SimpleForm("docker",
translate("Docker - Images"),
translate("On this page all images are displayed that are available on the system and with which a container can be created."))
m.submit=false
m.reset=false
local pull_value={
_image_tag_name="",
_registry="index.docker.io"
}
s = m:section(SimpleSection,
translate("Pull Image"),
translate("By entering a valid image name with the corresponding version, the docker image can be downloaded from the configured registry."))
s.template="cbi/nullsection"
o = s:option(Value, "_image_tag_name")
o.template = "dockerman/cbi/inlinevalue"
o.placeholder="lisaac/luci:latest"
o.write = function(self, section, value)
local hastag = value:find(":")
if not hastag then
value = value .. ":latest"
end
pull_value["_image_tag_name"] = value
end
o = s:option(Button, "_pull")
o.inputtitle= translate("Pull")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "add"
o.disable = lost_state
o.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 res = dk.images:create({query = {fromImage=tag}}, docker.pull_image_show_status_cb)
if res and res.code 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
s = m:section(SimpleSection,
translate("Import Image"),
translate("When pressing the Import button, both a local image can be loaded onto the system and a valid image tar can be downloaded from remote."))
o = s:option(DummyValue, "_image_import")
o.template = "dockerman/images_import"
o.disable = lost_state
s = m:section(Table, image_list, translate("Images overview"))
o = s:option(Flag, "_selected","")
o.disabled = 0
o.enabled = 1
o.default = 0
o.write = function(self, section, value)
image_list[section]._selected = value
end
o = s:option(DummyValue, "_id", translate("ID"))
o.rawhtml = true
o = s:option(DummyValue, "_tags", translate("RepoTags"))
o.rawhtml = true
o = s:option(DummyValue, "_containers", translate("Containers"))
o.rawhtml = true
o = s:option(DummyValue, "_size", translate("Size"))
o = s:option(DummyValue, "_created", translate("Created"))
local remove_action = function(force)
local image_selected = {}
for k in pairs(image_list) do
if image_list[k]._selected == 1 then
image_selected[#image_selected+1] = (image_list[k]["_tags"]:match("<br>") or image_list[k]["_tags"]:match("&lt;none&gt;")) and image_list[k].id or image_list[k].tag
end
end
if next(image_selected) ~= nil then
local success = true
docker:clear_status()
for _, img in ipairs(image_selected) do
local query
docker:append_status("Images: " .. "remove" .. " " .. img .. "...")
if force then
query = {force = true}
end
local msg = dk.images:remove({
id = img,
query = query
})
if msg and 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
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err = docker:read_status()
s.err = s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template="cbi/nullsection"
o = s:option(Button, "remove")
o.inputtitle= translate("Remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
remove_action()
end
o.disable = lost_state
o = s:option(Button, "forceremove")
o.inputtitle= translate("Force Remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "remove"
o.forcewrite = true
o.write = function(self, section)
remove_action(true)
end
o.disable = lost_state
o = s:option(Button, "save")
o.inputtitle= translate("Save")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "edit"
o.disable = lost_state
o.forcewrite = true
o.write = function (self, section)
local image_selected = {}
for k in pairs(image_list) do
if image_list[k]._selected == 1 then
image_selected[#image_selected + 1] = image_list[k].id
end
end
if next(image_selected) ~= nil then
local names, first, show_name
for _, img in ipairs(image_selected) do
names = names and (names .. "&names=".. img) or img
end
if #image_selected > 1 then
show_name = "images"
else
show_name = image_selected[1]
end
local cb = function(res, chunk)
if res and res.code and res.code == 200 then
if not first then
first = true
luci.http.header('Content-Disposition', 'inline; filename="'.. show_name .. '.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 and msg.code and msg.code ~= 200 then
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
else
docker:clear_status()
end
end
end
o = s:option(Button, "load")
o.inputtitle= translate("Load")
o.template = "dockerman/images_load"
o.inputstyle = "add"
o.disable = lost_state
return m

View file

@ -0,0 +1,159 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local m, s, o
local networks, dk, res, lost_state
dk = docker.new()
if dk:_ping().code ~= 200 then
lost_state = true
else
res = dk.networks:list()
if res and res.code and res.code < 300 then
networks = res.body
end
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 = not lost_state and get_networks() or {}
m = SimpleForm("docker",
translate("Docker - Networks"),
translate("This page displays all docker networks that have been created on the connected docker host."))
m.submit=false
m.reset=false
s = m:section(Table, network_list, translate("Networks overview"))
s.nodescr=true
o = s:option(Flag, "_selected","")
o.template = "dockerman/cbi/xfvalue"
o.disabled = 0
o.enabled = 1
o.default = 0
o.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
o.write = function(self, section, value)
network_list[section]._selected = value
end
o = s:option(DummyValue, "_id", translate("ID"))
o = s:option(DummyValue, "_name", translate("Network Name"))
o = s:option(DummyValue, "_driver", translate("Driver"))
o = s:option(DummyValue, "_interface", translate("Parent Interface"))
o = s:option(DummyValue, "_subnet", translate("Subnet"))
o = s:option(DummyValue, "_gateway", translate("Gateway"))
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err = docker:read_status()
s.err = s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template="cbi/nullsection"
o = s:option(Button, "_new")
o.inputtitle= translate("New")
o.template = "dockerman/cbi/inlinebutton"
o.notitle=true
o.inputstyle = "add"
o.forcewrite = true
o.disable = lost_state
o.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
end
o = s:option(Button, "_remove")
o.inputtitle= translate("Remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "remove"
o.forcewrite = true
o.disable = lost_state
o.write = function(self, section)
local network_selected = {}
local network_name_selected = {}
local network_driver_selected = {}
for k in pairs(network_list) do
if network_list[k]._selected == 1 then
network_selected[#network_selected + 1] = network_list[k]._id
network_name_selected[#network_name_selected + 1] = network_list[k]._name
network_driver_selected[#network_driver_selected + 1] = network_list[k]._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 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,923 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local m, s, o
local dk = docker.new()
local cmd_line = table.concat(arg, '/')
local images, networks
local create_body = {}
if dk:_ping().code ~= 200 then
lost_state = true
images = {}
networks = {}
else
images = dk.images:list().body
networks = dk.networks:list().body
end
local is_quot_complete = function(str)
local num = 0, w
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
function contains(list, x)
for _, v in pairs(list) do
if v == x then
return true
end
end
return false
end
local resolve_cli = function(cmd_line)
local 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',
'mac_address',
'm',
'memory',
'memory_reservation',
'memory_swap',
'memory_swappiness',
'mount',
'name',
'network',
'no_healthcheck',
'oom_kill_disable',
'oom_score_adj',
'pid',
'pids_limit',
'restart',
'runtime',
'shm_size',
'sig_proxy',
'stop_signal',
'stop_timeout',
'ulimit',
'u',
'user',
'userns',
'uts',
'volume_driver',
'w',
'workdir'
}
local key_abb = {
net='network',
a='attach',
c='cpu-shares',
d='detach',
e='env',
h='hostname',
i='interactive',
l='label',
m='memory',
p='publish',
P='publish_all',
t='tty',
u='user',
v='volume',
w='workdir'
}
local key_with_list = {
'sysctl',
'add_host',
'a',
'attach',
'blkio_weight_device',
'cap_add',
'cap_drop',
'device',
'device_cgroup_rule',
'device_read_bps',
'device_read_iops',
'device_write_bps',
'device_write_iops',
'dns',
'dns_optiondns_search',
'e',
'env',
'env_file',
'expose',
'group_add',
'l',
'label',
'label_file',
'link',
'link_local_ip',
'log_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 contains(key_no_val, key) then
config[key] = true
val = nil
key = nil
elseif contains(key_with_val, 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
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
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 contains(key_with_list, 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
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
config[key] = config[key]:gsub("[\"\']", "")
_key = nil
else
_key = key
end
end
key = nil
val = nil
end
end
return config
end
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 then
if 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
default_config.log_driver = create_body.HostConfig.LogConfig.Type or nil
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
for x, y in ipairs(v) do
table.insert( default_config.publish, y.HostPort..":"..k:match("^(%d+)/.+").."/"..k:match("^%d+/(.+)") )
end
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
m = SimpleForm("docker", translate("Docker - Containers"))
m.redirect = luci.dispatcher.build_url("admin", "docker", "containers")
if lost_state then
m.submit=false
m.reset=false
end
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(SimpleSection, translate("Create new docker container"))
s.addremove = true
s.anonymous = true
o = s:option(DummyValue,"cmd_line", translate("Resolve CLI"))
o.rawhtml = true
o.template = "dockerman/newcontainer_resolve"
o = s:option(Value, "name", translate("Container Name"))
o.rmempty = true
o.default = default_config.name or nil
o = s:option(Flag, "interactive", translate("Interactive (-i)"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = default_config.interactive and 1 or 0
o = s:option(Flag, "tty", translate("TTY (-t)"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = default_config.tty and 1 or 0
o = s:option(Value, "image", translate("Docker Image"))
o.rmempty = true
o.default = default_config.image or nil
for _, v in ipairs (images) do
if v.RepoTags then
o:value(v.RepoTags[1], v.RepoTags[1])
end
end
o = s:option(Flag, "_force_pull", translate("Always pull image first"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = 0
o = s:option(Flag, "privileged", translate("Privileged"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = default_config.privileged and 1 or 0
o = s:option(ListValue, "restart", translate("Restart Policy"))
o.rmempty = true
o:value("no", "No")
o:value("unless-stopped", "Unless stopped")
o:value("always", "Always")
o:value("on-failure", "On failure")
o.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
o = s:option(DynamicList, "link", translate("Links with other containers"))
o.placeholder = "container_name:alias"
o.rmempty = true
o:depends("network", "bridge")
o.default = default_config.link or nil
o = s:option(DynamicList, "dns", translate("Set custom DNS servers"))
o.placeholder = "8.8.8.8"
o.rmempty = true
o.default = default_config.dns or nil
o = s:option(Value, "user",
translate("User(-u)"),
translate("The user that commands are run as inside the container.(format: name|uid[:group|gid])"))
o.placeholder = "1000:1000"
o.rmempty = true
o.default = default_config.user or nil
o = s:option(DynamicList, "env",
translate("Environmental Variable(-e)"),
translate("Set environment variables to inside the container"))
o.placeholder = "TZ=Asia/Shanghai"
o.rmempty = true
o.default = default_config.env or nil
o = s:option(DynamicList, "volume",
translate("Bind Mount(-v)"),
translate("Bind mount a volume"))
o.placeholder = "/media:/media:slave"
o.rmempty = true
o.default = default_config.volume or nil
local d_publish = s:option(DynamicList, "publish",
translate("Exposed Ports(-p)"),
translate("Publish container's port(s) to the host"))
d_publish.placeholder = "2200:22/tcp"
d_publish.rmempty = true
d_publish.default = default_config.publish or nil
o = s:option(Value, "command", translate("Run command"))
o.placeholder = "/bin/sh init.sh"
o.rmempty = true
o.default = default_config.command or nil
o = s:option(Flag, "advance", translate("Advance"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = default_config.advance or 0
o = s:option(Value, "hostname",
translate("Host Name"),
translate("The hostname to use for the container"))
o.rmempty = true
o.default = default_config.hostname or nil
o:depends("advance", 1)
o = s:option(Flag, "publish_all",
translate("Exposed All Ports(-P)"),
translate("Allocates an ephemeral host port for all of a container's exposed ports"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = default_config.publish_all and 1 or 0
o:depends("advance", 1)
o = s:option(DynamicList, "device",
translate("Device(--device)"),
translate("Add host device to the container"))
o.placeholder = "/dev/sda:/dev/xvdc:rwm"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.device or nil
o = s:option(DynamicList, "tmpfs",
translate("Tmpfs(--tmpfs)"),
translate("Mount tmpfs directory"))
o.placeholder = "/run:rw,noexec,nosuid,size=65536k"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.tmpfs or nil
o = s:option(DynamicList, "sysctl",
translate("Sysctl(--sysctl)"),
translate("Sysctls (kernel parameters) options"))
o.placeholder = "net.ipv4.ip_forward=1"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.sysctl or nil
o = s:option(DynamicList, "cap_add",
translate("CAP-ADD(--cap-add)"),
translate("A list of kernel capabilities to add to the container"))
o.placeholder = "NET_ADMIN"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.cap_add or nil
o = s:option(Value, "cpus",
translate("CPUs"),
translate("Number of CPUs. Number is a fractional number. 0.000 means no limit"))
o.placeholder = "1.5"
o.rmempty = true
o:depends("advance", 1)
o.datatype="ufloat"
o.default = default_config.cpus or nil
o = s:option(Value, "cpu_shares",
translate("CPU Shares Weight"),
translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024"))
o.placeholder = "1024"
o.rmempty = true
o:depends("advance", 1)
o.datatype="uinteger"
o.default = default_config.cpu_shares or nil
o = s:option(Value, "memory",
translate("Memory"),
translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M"))
o.placeholder = "128m"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.memory or nil
o = s:option(Value, "blkio_weight",
translate("Block IO Weight"),
translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000"))
o.placeholder = "500"
o.rmempty = true
o:depends("advance", 1)
o.datatype="uinteger"
o.default = default_config.blkio_weight or nil
o = s:option(Value, "log_driver",
translate("Logging driver"),
translate("The logging driver for the container"))
o.placeholder = "json-file"
o.rmempty = true
o:depends("advance", 1)
o.default = default_config.log_driver or nil
o = s:option(DynamicList, "log_opt",
translate("Log driver options"),
translate("The logging configuration for this container"))
o.placeholder = "max-size=1m"
o.rmempty = true
o:depends("advance", 1)
o.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 = {}
local log_driver = data.log_driver
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 nil
local cpu_shares = data.cpu_shares or nil
local cpus = data.cpus or nil
local blkio_weight = data.blkio_weight or nil
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 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
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 = memory and tonumber(memory)
create_body.HostConfig.CpuShares = cpu_shares and tonumber(cpu_shares)
create_body.HostConfig.NanoCPUs = cpus and tonumber(cpus) * 10 ^ 9
create_body.HostConfig.BlkioWeight = blkio_weight and tonumber(blkio_weight)
create_body.HostConfig.PublishAllPorts = publish_all
if create_body.HostConfig.NetworkMode ~= network then
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
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
create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } }
end
elseif not create_body.NetworkingConfig then
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"] = {
Config = log_opt,
Type = log_driver
}
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 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 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,258 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local m, s, o
local dk = docker.new()
if dk:_ping().code ~= 200 then
lost_state = true
end
m = SimpleForm("docker", translate("Docker - Network"))
m.redirect = luci.dispatcher.build_url("admin", "docker", "networks")
if lost_state then
m.submit=false
m.reset=false
end
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(SimpleSection, translate("Create new docker network"))
s.addremove = true
s.anonymous = true
o = s:option(Value, "name",
translate("Network Name"),
translate("Name of the network that can be selected during container creation"))
o.rmempty = true
o = s:option(ListValue, "driver", translate("Driver"))
o.rmempty = true
o:value("bridge", translate("Bridge device"))
o:value("macvlan", translate("MAC VLAN"))
o:value("ipvlan", translate("IP VLAN"))
o:value("overlay", translate("Overlay network"))
o = s:option(Value, "parent", translate("Base device"))
o.rmempty = true
o:depends("driver", "macvlan")
local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {}
for _, v in ipairs(interfaces) do
o:value(v, v)
end
o.default="br-lan"
o.placeholder="br-lan"
o = s:option(ListValue, "macvlan_mode", translate("Mode"))
o.rmempty = true
o:depends("driver", "macvlan")
o.default="bridge"
o:value("bridge", translate("Bridge (Support direct communication between MAC VLANs)"))
o:value("private", translate("Private (Prevent communication between MAC VLANs)"))
o:value("vepa", translate("VEPA (Virtual Ethernet Port Aggregator)"))
o:value("passthru", translate("Pass-through (Mirror physical device to single MAC VLAN)"))
o = s:option(ListValue, "ipvlan_mode", translate("Ipvlan Mode"))
o.rmempty = true
o:depends("driver", "ipvlan")
o.default="l3"
o:value("l2", translate("L2 bridge"))
o:value("l3", translate("L3 bridge"))
o = s:option(Flag, "ingress",
translate("Ingress"),
translate("Ingress network is the network which provides the routing-mesh in swarm mode"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = 0
o:depends("driver", "overlay")
o = s:option(DynamicList, "options", translate("Options"))
o.rmempty = true
o.placeholder="com.docker.network.driver.mtu=1500"
o = s:option(Flag, "internal", translate("Internal"), translate("Restrict external access to the network"))
o.rmempty = true
o:depends("driver", "overlay")
o.disabled = 0
o.enabled = 1
o.default = 0
if nixio.fs.access("/etc/config/network") and nixio.fs.access("/etc/config/firewall")then
o = s:option(Flag, "op_macvlan", translate("Create macvlan interface"), translate("Auto create macvlan interface in Openwrt"))
o:depends("driver", "macvlan")
o.disabled = 0
o.enabled = 1
o.default = 1
end
o = s:option(Value, "subnet", translate("Subnet"))
o.rmempty = true
o.placeholder="10.1.0.0/16"
o.datatype="ip4addr"
o = s:option(Value, "gateway", translate("Gateway"))
o.rmempty = true
o.placeholder="10.1.1.1"
o.datatype="ip4addr"
o = s:option(Value, "ip_range", translate("IP range"))
o.rmempty = true
o.placeholder="10.1.1.0/24"
o.datatype="ip4addr"
o = s:option(DynamicList, "aux_address", translate("Exclude IPs"))
o.rmempty = true
o.placeholder="my-route=10.1.1.1"
o = s:option(Flag, "ipv6", translate("Enable IPv6"))
o.rmempty = true
o.disabled = 0
o.enabled = 1
o.default = 0
o = s:option(Value, "subnet6", translate("IPv6 Subnet"))
o.rmempty = true
o.placeholder="fe80::/10"
o.datatype="ip6addr"
o:depends("ipv6", 1)
o = s:option(Value, "gateway6", translate("IPv6 Gateway"))
o.rmempty = true
o.placeholder="fe80::1"
o.datatype="ip6addr"
o:depends("ipv6", 1)
m.handle = function(self, state, data)
if state == FORM_VALID then
local name = data.name
local driver = data.driver
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 and
res.code 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,151 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local uci = (require "luci.model.uci").cursor()
local m, s, o, lost_state
local dk = docker.new()
if dk:_ping().code ~= 200 then
lost_state = true
end
m = SimpleForm("dockerd",
translate("Docker - Overview"),
translate("An overview with the relevant data is displayed here with which the LuCI docker client is connected.")
..
" " ..
[[<a href="https://github.com/lisaac/luci-app-dockerman" target="_blank">]] ..
translate("Github") ..
[[</a>]])
m.submit=false
m.reset=false
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='-'}
if nixio.fs.access("/usr/bin/dockerd") and not uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template = "cbi/nullsection"
o = s:option(Button, "_start")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = lost_state and translate("Start") or translate("Stop")
o.inputstyle = lost_state and "add" or "remove"
o.forcewrite = true
o.write = function(self, section)
docker:clear_status()
if lost_state then
docker:append_status("Docker daemon: starting")
luci.util.exec("/etc/init.d/dockerd start")
luci.util.exec("sleep 5")
luci.util.exec("/etc/init.d/dockerman start")
else
docker:append_status("Docker daemon: stopping")
luci.util.exec("/etc/init.d/dockerd stop")
end
docker:clear_status()
luci.http.redirect(luci.dispatcher.build_url("admin/docker/overview"))
end
o = s:option(Button, "_restart")
o.template = "dockerman/cbi/inlinebutton"
o.inputtitle = translate("Restart")
o.inputstyle = "reload"
o.forcewrite = true
o.write = function(self, section)
docker:clear_status()
docker:append_status("Docker daemon: restarting")
luci.util.exec("/etc/init.d/dockerd restart")
luci.util.exec("sleep 5")
luci.util.exec("/etc/init.d/dockerman start")
docker:clear_status()
luci.http.redirect(luci.dispatcher.build_url("admin/docker/overview"))
end
end
s = m:section(Table, docker_info_table)
s:option(DummyValue, "_key", translate("Info"))
s:option(DummyValue, "_value")
s = m:section(SimpleSection)
s.template = "dockerman/overview"
s.containers_running = '-'
s.images_used = '-'
s.containers_total = '-'
s.images_total = '-'
s.networks_total = '-'
s.volumes_total = '-'
-- local socket = luci.model.uci.cursor():get("dockerd", "dockerman", "socket_path")
if not lost_state then
local 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 = docker.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(docker.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)
else
docker_info_table['3ServerVersion']._value = translate("Can NOT connect to docker daemon, please check!!")
end
return m

View file

@ -0,0 +1,142 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.model.docker"
local dk = docker.new()
local m, s, o
local res, containers, volumes, lost_state
function get_volumes()
local data = {}
for i, v in ipairs(volumes) do
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
data[index]["_size"] = "<font class='volume_size_" .. v.Name .. "'>-</font>"
end
return data
end
if dk:_ping().code ~= 200 then
lost_state = true
else
res = dk.volumes:list()
if res and res.code and res.code <300 then
volumes = res.body.Volumes
end
res = dk.containers:list({
query = {
all=true
}
})
if res and res.code and res.code <300 then
containers = res.body
end
end
local volume_list = not lost_state and get_volumes() or {}
m = SimpleForm("docker", translate("Docker - Volumes"))
m.submit=false
m.reset=false
m:append(Template("dockerman/volume_size"))
s = m:section(Table, volume_list, translate("Volumes overview"))
o = s:option(Flag, "_selected","")
o.disabled = 0
o.enabled = 1
o.default = 0
o.write = function(self, section, value)
volume_list[section]._selected = value
end
o = s:option(DummyValue, "_name", translate("Name"))
o = s:option(DummyValue, "_driver", translate("Driver"))
o = s:option(DummyValue, "_containers", translate("Containers"))
o.rawhtml = true
o = s:option(DummyValue, "_mountpoint", translate("Mount Point"))
o = s:option(DummyValue, "_size", translate("Size"))
o.rawhtml = true
o = s:option(DummyValue, "_created", translate("Created"))
s = m:section(SimpleSection)
s.template = "dockerman/apply_widget"
s.err=docker:read_status()
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
if s.err then
docker:clear_status()
end
s = m:section(Table,{{}})
s.notitle=true
s.rowcolors=false
s.template="cbi/nullsection"
o = s:option(Button, "remove")
o.inputtitle= translate("Remove")
o.template = "dockerman/cbi/inlinebutton"
o.inputstyle = "remove"
o.forcewrite = true
o.disable = lost_state
o.write = function(self, section)
local volume_selected = {}
for k in pairs(volume_list) do
if volume_list[k]._selected == 1 then
volume_selected[#volume_selected+1] = k
end
end
if next(volume_selected) ~= nil then
local success = true
docker:clear_status()
for _,vol in ipairs(volume_selected) do
docker:append_status("Volumes: " .. "remove" .. " " .. vol .. "...")
local msg = dk.volumes["remove"](dk, {id = vol})
if msg and msg.code and 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,507 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
]]--
local docker = require "luci.docker"
local fs = require "nixio.fs"
local uci = (require "luci.model.uci").cursor()
local _docker = {}
_docker.options = {}
--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 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
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)
if t[k] and next(t[k]) == nil then
t[k] = nil
end
end
end
end
return t
end
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)
if old_host_config.PortBindings and next(old_host_config.PortBindings) ~= nil then
config.ExposedPorts = {}
for p, v in pairs(old_host_config.PortBindings) do
config.ExposedPorts[p] = { HostPort=v[1] and v[1].HostPort }
end
end
local network_setting = {}
local multi_network = false
local extra_network = {}
for k, v in pairs(old_network_setting) do
if multi_network then
extra_network[k] = v
else
network_setting[k] = v
end
multi_network = true
end
local host_config = old_host_config
host_config.Mounts = {}
for i, v in ipairs(container_config.Mounts) do
if v.Type == "volume" then
table.insert(host_config.Mounts, {
Type = v.Type,
Target = v.Destination,
Source = v.Source:match("([^/]+)\/_data"),
BindOptions = (v.Type == "bind") and {Propagation = v.Propagation} or nil,
ReadOnly = not v.RW
})
end
end
local create_body = config
create_body["HostConfig"] = host_config
create_body["NetworkingConfig"] = {EndpointsConfig = network_setting}
create_body = _docker.clear_empty_tables(create_body) or {}
extra_network = _docker.clear_empty_tables(extra_network) or {}
return create_body, extra_network
end
local upgrade = function(self, request)
_docker:clear_status()
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 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
local t = os.date("%Y%m%d%H%M%S")
_docker:append_status("Container: rename" .. " " .. container_name .. " to ".. container_name .. "_old_".. t .. "...")
res = self.containers:rename({name = container_name, query = { name = container_name .. "_old_" ..t }})
if res and res.code and res.code < 300 then
_docker:append_status("done\n")
else
return res
end
local image_config = self.images:inspect({id = old_image_id}).body.Config
local create_body, extra_network = get_config(container_info.body, image_config)
-- create new container
_docker:append_status("Container: Create" .. " " .. container_name .. "...")
create_body = _docker.clear_empty_tables(create_body)
res = self.containers:create({name = container_name, body = create_body})
if res and res.code 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 and res.code and res.code > 300 then
return res
end
_docker:append_status("done\n")
end
_docker:append_status("Container: " .. "Stop" .. " " .. container_name .. "_old_".. t .. "...")
res = self.containers:stop({name = container_name .. "_old_" ..t })
if res and res.code and res.code < 305 then
_docker:append_status("done\n")
else
return res
end
_docker:append_status("Container: " .. "Start" .. " " .. container_name .. "...")
res = self.containers:start({name = container_name})
if res and res.code and res.code < 305 then
_docker:append_status("done\n")
else
return res
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()
local host = nil
local port = nil
local socket_path = nil
local debug_path = nil
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
host = uci:get("dockerd", "dockerman", "remote_host") or nil
port = uci:get("dockerd", "dockerman", "remote_port") or nil
else
socket_path = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
end
local debug = uci:get_bool("dockerd", "dockerman", "debug")
if debug then
debug_path = uci:get("dockerd", "dockerman", "debug_path") or "/tmp/.docker_debug"
end
local status_path = uci:get("dockerd", "dockerman", "status_path") or "/tmp/.docker_action_status"
_docker.options = {
host = host,
port = port,
socket_path = socket_path,
debug = debug,
debug_path = debug_path,
status_path = status_path
}
local _new = docker.new(_docker.options)
_new.containers_upgrade = upgrade
_new.containers_duplicate_config = duplicate_config
return _new
end
_docker.options.status_path = uci:get("dockerd", "dockerman", "status_path") or "/tmp/.docker_action_status"
_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 fs.readfile(self.options.status_path)
end
_docker.clear_status=function(self)
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.create_macvlan_interface = function(name, device, gateway, subnet)
if not fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then
return
end
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") 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 fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then
return
end
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") 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:delete("network", dev_name)
uci:delete("network", if_name)
uci:commit("network")
uci:commit("firewall")
os.execute("ip link del " .. if_name)
end
_docker.byte_format = function (byte)
if not byte then return 'NaN' end
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
return _docker

View file

@ -0,0 +1,147 @@
<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 90, 90)%>,
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);
});
if(typeof(fnWindowLoad) == "function"){
fnWindowLoad()
}
}
//]]></script>

View file

@ -0,0 +1,7 @@
<div style="display: inline-block;">
<% if self:cfgvalue(section) ~= false then %>
<input class="btn 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="btn 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,28 @@
<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 = window.location.protocol + "//" + window.location.hostname + ":7682";
</script>

View file

@ -0,0 +1,332 @@
<link rel="stylesheet" href="/luci-static/resources/dockerman/file-manager.css?v=@ver">
<fieldset class="cbi-section fb-container">
<input id="current-path" type="text" class="current-path cbi-input-text" value="/" />
<div class="panel-container">
<input type="file" name="upload_archive" accept="*/*"
style="visibility:hidden; position: absolute;top: 0px; left: 0px;" multiple="multiple" id="upload_archive" />
<button id="upload-file" class="upload-toggle cbi-button cbi-button-edit"><%:Upload%></button>
</div>
<div id="list-content"></div>
</fieldset>
<script type="text/javascript" src="<%=resource%>/dockerman/tar.min.js"></script>
<script>
String.prototype.replaceAll = function (search, replacement) {
var target = this;
return target.replace(new RegExp(search, 'g'), replacement);
};
(function () {
var iwxhr = new XHR();
var listElem = document.getElementById("list-content");
listElem.onclick = handleClick;
var currentPath;
var pathElem = document.getElementById("current-path");
pathElem.onblur = function () {
update_list(this.value.trim());
};
pathElem.onkeydown = function (evt) {
if (evt.keyCode == 13) {
this.blur()
evt.preventDefault()
}
};
function removePath(filename, isdir) {
var c = confirm('!!!<%:DELETING%> ' + filename + ' ... <%:PLEASE CONFIRM%>!!!');
if (c) {
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_remove_file")%>/<%=self.container%>',
{
path: concatPath(currentPath, filename),
isdir: isdir
},
function (x, res) {
if (res.ec === 0) {
refresh_list(res.data, currentPath);
}
});
}
}
function renamePath(filename) {
var newname = prompt('%:Please input new filename%>: ', filename);
if (newname) {
newname = newname.trim();
if (newname != filename) {
var newpath = concatPath(currentPath, newname);
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_rename_file")%>/<%=self.container%>',
{
filepath: concatPath(currentPath, filename),
newpath: newpath
},
function (x, res) {
if (res.ec === 0) {
refresh_list(res.data, currentPath);
}
}
);
}
}
}
function openpath(filename, dirname) {
dirname = dirname || currentPath;
window.open('<%=luci.dispatcher.build_url("admin/docker/container_get_archive")%>?id=<%=self.container%>&path='
+ encodeURIComponent(dirname + '/' + filename) + '&filename='
+ encodeURIComponent(filename))
}
function getFileElem(elem) {
if (elem.className.indexOf('-icon') > -1) {
return elem;
}
else if (elem.parentNode.className.indexOf('-icon') > -1) {
return elem.parentNode;
}
}
function concatPath(path, filename) {
if (path === '/') {
return path + filename;
}
else {
return path.replace(/\/$/, '') + '/' + filename;
}
}
function handleClick(evt) {
// evt.preventDefault();
var targetElem = evt.target;
var infoElem;
if (targetElem.className.indexOf('cbi-button-remove') > -1) {
infoElem = targetElem.parentNode.parentNode;
removePath(infoElem.dataset['filename'], infoElem.dataset['isdir'])
evt.preventDefault();
location.reload()
}
else if (targetElem.className.indexOf('cbi-button-download') > -1) {
infoElem = targetElem.parentNode.parentNode;
openpath(targetElem.parentNode.parentNode.dataset['filename']);
evt.preventDefault();
}
else if (targetElem.className.indexOf('cbi-button-rename') > -1) {
renamePath(targetElem.parentNode.parentNode.dataset['filename']);
evt.preventDefault();
location.reload()
}
else if (targetElem = getFileElem(targetElem)) {
if (targetElem.className.indexOf('parent-icon') > -1) {
update_list(currentPath.replace(/\/[^/]+($|\/$)/, ''));
}
else if (targetElem.className.indexOf('file-icon') > -1) {
openpath(targetElem.parentNode.dataset['filename']);
}
else if (targetElem.className.indexOf('link-icon') > -1) {
infoElem = targetElem.parentNode;
var filepath = infoElem.dataset['linktarget'];
if (filepath) {
if (infoElem.dataset['isdir'] === "1") {
update_list(filepath);
}
else {
var lastSlash = filepath.lastIndexOf('/')
openpath(filepath.substring(lastSlash + 1), filepath.substring(0, lastSlash));
}
}
}
else if (targetElem.className.indexOf('folder-icon') > -1) {
update_list(concatPath(currentPath, targetElem.parentNode.dataset['filename']))
}
}
}
function refresh_list(filenames, path) {
var listHtml = '<table class="cbi-section-table"><tbody>';
if (path !== '/') {
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-2"><td class="parent-icon" colspan="6"><strong>..</strong></td></tr>';
}
if (filenames) {
for (var i = 0; i < filenames.length; i++) {
var line = filenames[i]
if (line) {
var f = line.match(/(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+([\S\s]+)/);
var isLink = f[1][0] === 'z' || f[1][0] === 'l' || f[1][0] === 'x';
var o = {
displayname: f[9],
filename: isLink ? f[9].split(' -> ')[0] : f[9],
perms: f[1],
date: f[7] + ' ' + f[6] + ' ' + f[8],
size: f[5],
owner: f[3],
icon: (f[1][0] === 'd') ? "folder-icon" : (isLink ? "link-icon" : "file-icon")
};
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-' + (1 + i % 2)
+ '" data-filename="' + o.filename + '" data-isdir="' + Number(f[1][0] === 'd' || f[1][0] === 'z') + '"'
+ ((f[1][0] === 'z' || f[1][0] === 'l') ? (' data-linktarget="' + f[9].split(' -> ')[1]) : '')
+ '">'
+ '<td class="cbi-value-field ' + o.icon + '">'
+ '<strong>' + o.displayname + '</strong>'
+ '</td>'
+ '<td class="cbi-value-field cbi-value-owner">' + o.owner + '</td>'
+ '<td class="cbi-value-field cbi-value-date">' + o.date + '</td>'
+ '<td class="cbi-value-field cbi-value-size">' + o.size + '</td>'
+ '<td class="cbi-value-field cbi-value-perm">' + o.perms + '</td>'
+ '<td class="cbi-section-table-cell">\
<button class="btn cbi-button cbi-button-rename cbi-button-edit">'+ "<%:Rename%>" + '</button>\
<button class="btn cbi-button cbi-button-download cbi-button-add">'+ "<%:Download%>" + '</button>\
<button class="btn cbi-button cbi-button-remove">'+ "<%:Remove%>" + '</button>\
</td>'
+ '</tr>';
}
}
}
listHtml += "</table>";
listElem.innerHTML = listHtml;
}
function update_list(path, opt) {
opt = opt || {};
path = concatPath(path, '');
if (currentPath != path) {
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_list_file")%>/<%=self.container%>',
{ path: path },
function (x, res) {
if (res.ec === 0) {
refresh_list(res.data, path);
}
else {
refresh_list([], path);
}
}
);
if (!opt.popState) {
history.pushState({ path: path }, null, '?path=' + path);
}
currentPath = path;
pathElem.value = currentPath;
}
};
async function file2Tar(tarFile, fileToLoad) {
if (! fileToLoad) return
function file2Byte(file) {
return new Promise((resolve, reject) => {
var fileReader = new FileReader();
fileReader.onerror = () => {
fileReader.abort();
reject(new DOMException("Problem parsing input file."));
};
fileReader.onload = (fileLoadedEvent) => {
resolve(ByteHelper.stringUTF8ToBytes(fileLoadedEvent.target.result));
}
fileReader.readAsBinaryString(file);
})
}
const x = await file2Byte(fileToLoad)
return fileByte2Tar(tarFile, fileToLoad.name, x).downloadAs(fileToLoad.name + ".tar")
}
function fileByte2Tar(tarFile, fileName, fileBytes) {
if (!tarFile) tarFile = TarFile.create(fileName)
var tarHeader = TarFileEntryHeader.default();
var tarFileEntryHeader = new TarFileEntryHeader
(
// ByteHelper.bytesToStringUTF8(fileName),
fileName,
tarHeader.fileMode,
tarHeader.userIDOfOwner,
tarHeader.userIDOfGroup,
fileBytes.length, // fileSizeInBytes,
tarHeader.timeModifiedInUnixFormat, // todo
0, // checksum,
TarFileTypeFlag.Instances().Normal,
tarHeader.nameOfLinkedFile,
tarHeader.uStarIndicator,
tarHeader.uStarVersion,
tarHeader.userNameOfOwner,
tarHeader.groupNameOfOwner,
tarHeader.deviceNumberMajor,
tarHeader.deviceNumberMinor,
tarHeader.filenamePrefix
);
tarFileEntryHeader.checksumCalculate();
var entryForFileToAdd = new TarFileEntry
(
tarFileEntryHeader,
fileBytes
);
tarFile.entries.push(entryForFileToAdd);
return tarFile
}
var btnUpload = document.getElementById('upload-file');
btnUpload.onclick = function (e) {
document.getElementById("upload_archive").click()
e.preventDefault()
}
let fileLoad = document.getElementById('upload_archive')
fileLoad.onchange = async function (e) {
let uploadArchive = document.getElementById('upload_archive')
// let uploadPath = document.getElementById('path').value
if (!uploadArchive.value) {
docker_status_message('warning', "<%:Please input the PATH and select the file !%>")
document.getElementById('docker_apply_overlay').addEventListener("click", (e) => {
docker_status_message()
})
return
}
docker_status_message('notice',
'<img src="<%=resource%>/icons/loading.gif" style="vertical-align:middle" /> <span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' +
'Uploading...' + '</span>');
Globals.Instance.tarFile = TarFile.create("Archive.tar")
let bytesToWriteAsBlob
for (let i = 0; i < uploadArchive.files.length; i++) {
let fileName = uploadArchive.files[i].name
bytesToWriteAsBlob = await file2Tar(Globals.Instance.tarFile, uploadArchive.files[i])
}
let formData = new FormData()
formData.append('upload-filename', "Archive.tar")
formData.append('upload-path', concatPath(currentPath, ''))
formData.append('upload-archive', bytesToWriteAsBlob)
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%>")
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time))
}
sleep(800).then(() => {
location.reload()
})
}
else {
// docker_status_message('warning', "<%:Upload Error%>:" + xhr.statusText)
docker_status_message('warning', "<%:Upload Error%>:" + '<span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' +
JSON.parse(xhr.response).message + '</span>')
}
document.getElementById('docker_apply_overlay').addEventListener("click", (e) => {
docker_status_message()
})
}
xhr.send(formData)
}
document.addEventListener('DOMContentLoaded', function (evt) {
var initPath = '/';
if (/path=([/\w\.\-\_]+)/.test(location.search)) {
initPath = RegExp.$1;
}
update_list(initPath, { popState: true });
});
window.addEventListener('popstate', function (evt) {
var path = '/';
if (evt.state && evt.state.path) {
path = evt.state.path;
}
update_list(path, { popState: true });
});
})();
</script>

View file

@ -0,0 +1,81 @@
<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,91 @@
<script type="text/javascript">
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
function niceBytes(x) {
let l = 0, n = parseInt(x, 10) || 0;
while (n >= 1024 && ++l) {
n = n / 1024;
}
return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
}
fnWindowLoad = function () {
XHR.get('<%=luci.dispatcher.build_url("admin/docker/get_system_df")%>/', null, (x, info)=>{
if(!info || !info.Containers || !info.Containers.forEach) return
info.Containers.forEach(item=>{
const size_c = document.getElementsByClassName("container_size_" + item.Id)
size_c[0].title = "RW Size: " + niceBytes(item.SizeRw) + " / RootFS Size(Include Image): " + niceBytes(item.SizeRootFs)
size_c[0].innerText = "Size: " + niceBytes(item.SizeRw) + "/" + niceBytes(item.SizeRootFs)
})
})
let lines = document.querySelectorAll('[id^=cbi-containers-]')
let last_bw_tx = {}
let last_bw_rx = {}
let interval = 30
let containers = []
lines.forEach((item) => {
let containerId = item.id.match(/cbi-containers-.+_id_(.*)/)
if (!containerId) { return }
containerId = containerId[1]
if (item.getElementsByClassName("container_not_running").length > 0) { return }
XHR.poll(interval, '<%=luci.dispatcher.build_url("admin/docker/container_stats")%>/' + containerId, null, (x, info) => {
// handle stats info
if (!info) { return }
item.childNodes.forEach((cell) => {
if (cell && cell.attributes) {
if (cell.getAttribute("data-name") == "_status" || cell.childNodes[1] && cell.childNodes[1].id.match(/_status/)) {
let runningStats = cell.getElementsByClassName("container_cpu_status")
runningStats[0].innerText = "CPU: " + info.cpu_percent + "%"
runningStats = cell.getElementsByClassName("container_mem_status")
runningStats[0].innerText = "MEM: " + niceBytes(info.memory.mem_useage)
runningStats = cell.getElementsByClassName("container_network_status")
for (var eth in info.bw_rxtx) {
if (last_bw_tx[containerId] != undefined && last_bw_rx[containerId] != undefined) {
runningStats[0].innerText = '↑' + niceBytes((info.bw_rxtx[eth].bw_tx - last_bw_tx[containerId]) / interval) + '/s ↓' + niceBytes((info.bw_rxtx[eth].bw_rx - last_bw_rx[containerId]) / interval) + '/s'
}
last_bw_rx[containerId] = info.bw_rxtx[eth].bw_rx
last_bw_tx[containerId] = info.bw_rxtx[eth].bw_tx
}
}
}
})
})
// containers.push(containerId)
})
// XHR.post('<%=luci.dispatcher.build_url("admin/docker/containers_stats")%>', {
// containers: JSON.stringify(containers)
// }, (x, info) => {
// lines.forEach((item) => {
// if (!info) { return }
// let containerId = item.id.match(/cbi-containers-.+_id_(.*)/)
// if (!containerId) { return }
// containerId = containerId[1]
// if (!info[containerId]) { return }
// infoC = info[containerId]
// if (item.getElementsByClassName("container_not_running").length > 0) { return }
// item.childNodes.forEach((cell) => {
// if (cell && cell.attributes) {
// if (cell.getAttribute("data-name") == "_status" || cell.childNodes[1] && cell.childNodes[1].id.match(/_status/)) {
// let runningStats = cell.getElementsByClassName("container_cpu_status")
// runningStats[0].innerText = "CPU: " + infoC.cpu_percent + "%"
// runningStats = cell.getElementsByClassName("container_mem_status")
// runningStats[0].innerText = "MEM: " + niceBytes(infoC.memory.mem_useage)
// runningStats = cell.getElementsByClassName("container_network_status")
// for (var eth in infoC.bw_rxtx) {
// if (last_bw_tx[containerId] != undefined && last_bw_rx[containerId] != undefined) {
// runningStats[0].innerText = '↑' + niceBytes((infoC.bw_rxtx[eth].bw_tx - last_bw_tx[containerId]) / interval) + '/s ↓' + niceBytes((infoC.bw_rxtx[eth].bw_rx - last_bw_rx[containerId]) / interval) + '/s'
// }
// last_bw_rx[containerId] = infoC.bw_rxtx[eth].bw_rx
// last_bw_tx[containerId] = infoC.bw_rxtx[eth].bw_tx
// }
// }
// }
// })
// })
// })
XHR.run()
XHR.halt()
}
</script>

View file

@ -0,0 +1,104 @@
<input type="text" class="cbi-input-text" name="isrc" placeholder="http://host/image.tar" id="isrc" />
<input type="text" class="cbi-input-text" name="itag" placeholder="repository:tag" id="itag" />
<div style="display: inline-block;">
<input type="button"" class="btn cbi-button cbi-button-add" id="btnimport" name="import" value="<%:Import%>" <% if self.disable then %>disabled <% end %>/>
<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,40 @@
<div style="display: inline-block;">
<input type="button"" class="btn cbi-button cbi-button-add" id="btnload" name="load" value="<%:Load%>" <% if self.disable then %>disabled <% end %>/>
<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()
xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/images_load")%>', true)
xhr.onload = function() {
if (xhr.status == 200) {
docker_status_message('notice', xhr.statusText)
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time))
}
sleep(1500).then(() => {
location.reload()
})
} else {
location.reload()
}
}
uci_confirm_docker()
xhr.send(formData)
}
</script>

View file

@ -0,0 +1,13 @@
<% if self.title == "Events" then %>
<%+header%>
<h2 name="content"><%:Docker - Events%></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,102 @@
<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()" placeholder="docker run -d alpine sh"></textarea></div><div class="dialog_line" style="text-align: right;"><input type="button" class="btn cbi-button cbi-button-apply" type="submit" value="<%:Submit%>" onclick="reslov_container()"/> <input type="button" class="btn cbi-button cbi-button-reset" type="reset" value="<%:Cancel%>" onclick="close_reslov_dialog()" /></div><div class="dialog_line"></div></div></div>')
document.body.classList.add('dialog-reslov-active')
let s = document.getElementById('cmd-line-status')
s.innerHTML = ""
document.documentElement.style.overflowY = 'hidden'
}
</script>
<%+cbi/valueheader%>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Command line%>" onclick="show_reslov_dialog()" />
<%+cbi/valuefooter%>

View file

@ -0,0 +1,197 @@
<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">
<img src="<%=resource%>/dockerman/containers.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">
<img src="<%=resource%>/dockerman/images.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">
<img src="<%=resource%>/dockerman/networks.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">
<img src="<%=resource%>/dockerman/volumes.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,21 @@
<script type="text/javascript">
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
function niceBytes(x) {
let l = 0, n = parseInt(x, 10) || 0;
while (n >= 1024 && ++l) {
n = n / 1024;
}
return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
}
fnWindowLoad = function () {
XHR.get('<%=luci.dispatcher.build_url("admin/docker/get_system_df")%>/', null, (x, info)=>{
if(!info || !info.Volumes || !info.Volumes.forEach) return
info.Volumes.forEach(item=>{
console.log(info)
const size_c = document.getElementsByClassName("volume_size_" + item.Name)
size_c[0].innerText = item.UsageData ? niceBytes(item.UsageData.Size) : '-'
})
})
}
</script>