summaryrefslogtreecommitdiffhomepage
path: root/applications/luci-app-dockerman/luasrc/model/cbi
diff options
context:
space:
mode:
authorFlorian Eckert <fe@dev.tdt.de>2020-06-10 10:56:12 +0200
committerGitHub <noreply@github.com>2020-06-10 10:56:12 +0200
commit490596a623d9fd0e763e593ee388ec81477a892b (patch)
treee8940a7900596e7e659c4a60b1c18dd8eac1004c /applications/luci-app-dockerman/luasrc/model/cbi
parentaed292ad45d09407f6008d68b236ceaa79270ab2 (diff)
parent7aabe27c00323870ddab5e2a4e658a1c5902e03f (diff)
Merge pull request #4073 from TDT-AG/pr/20200427-luci-app-dockerman
luci-app-dockerman: add package
Diffstat (limited to 'applications/luci-app-dockerman/luasrc/model/cbi')
-rw-r--r--applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua588
-rw-r--r--applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua195
-rw-r--r--applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua223
-rw-r--r--applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua130
-rw-r--r--applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua653
-rw-r--r--applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua221
-rw-r--r--applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua154
-rw-r--r--applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua116
8 files changed, 2280 insertions, 0 deletions
diff --git a/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua
new file mode 100644
index 0000000000..7c0c969336
--- /dev/null
+++ b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua
@@ -0,0 +1,588 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
+]]--
+
+require "luci.util"
+local docker = require "luci.model.docker"
+local dk = docker.new()
+container_id = arg[1]
+local action = arg[2] or "info"
+
+local images, networks, container_info
+if not container_id then return end
+local res = dk.containers:inspect({id = container_id})
+if res.code < 300 then container_info = res.body else return end
+res = dk.networks:list()
+if res.code < 300 then networks = res.body else return end
+
+local get_ports = function(d)
+ local data
+ if d.HostConfig and d.HostConfig.PortBindings then
+ for inter, out in pairs(d.HostConfig.PortBindings) do
+ data = (data and (data .. "<br>") or "") .. out[1]["HostPort"] .. ":" .. inter
+ end
+ end
+ return data
+end
+
+local get_env = function(d)
+ local data
+ if d.Config and d.Config.Env then
+ for _,v in ipairs(d.Config.Env) do
+ data = (data and (data .. "<br>") or "") .. v
+ end
+ end
+ return data
+end
+
+local get_command = function(d)
+ local data
+ if d.Config and d.Config.Cmd then
+ for _,v in ipairs(d.Config.Cmd) do
+ data = (data and (data .. " ") or "") .. v
+ end
+ end
+ return data
+end
+
+local get_mounts = function(d)
+ local data
+ if d.Mounts then
+ for _,v in ipairs(d.Mounts) do
+ local v_sorce_d, v_dest_d
+ local v_sorce = ""
+ local v_dest = ""
+ for v_sorce_d in v["Source"]:gmatch('[^/]+') do
+ if v_sorce_d and #v_sorce_d > 12 then
+ v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,12) .. "..."
+ else
+ v_sorce = v_sorce .."/".. v_sorce_d
+ end
+ end
+ for v_dest_d in v["Destination"]:gmatch('[^/]+') do
+ if v_dest_d and #v_dest_d > 12 then
+ v_dest = v_dest .. "/" .. v_dest_d:sub(1,12) .. "..."
+ else
+ v_dest = v_dest .."/".. v_dest_d
+ end
+ end
+ data = (data and (data .. "<br>") or "") .. v_sorce .. ":" .. v["Destination"] .. (v["Mode"] ~= "" and (":" .. v["Mode"]) or "")
+ end
+ end
+ return data
+end
+
+local get_device = function(d)
+ local data
+ if d.HostConfig and d.HostConfig.Devices then
+ for _,v in ipairs(d.HostConfig.Devices) do
+ data = (data and (data .. "<br>") or "") .. v["PathOnHost"] .. ":" .. v["PathInContainer"] .. (v["CgroupPermissions"] ~= "" and (":" .. v["CgroupPermissions"]) or "")
+ end
+ end
+ return data
+end
+
+local get_links = function(d)
+ local data
+ if d.HostConfig and d.HostConfig.Links then
+ for _,v in ipairs(d.HostConfig.Links) do
+ data = (data and (data .. "<br>") or "") .. v
+ end
+ end
+ return data
+end
+
+local get_tmpfs = function(d)
+ local data
+ if d.HostConfig and d.HostConfig.Tmpfs then
+ for k, v in pairs(d.HostConfig.Tmpfs) do
+ data = (data and (data .. "<br>") or "") .. k .. (v~="" and ":" or "")..v
+ end
+ end
+ return data
+end
+
+local get_dns = function(d)
+ local data
+ if d.HostConfig and d.HostConfig.Dns then
+ for _, v in ipairs(d.HostConfig.Dns) do
+ data = (data and (data .. "<br>") or "") .. v
+ end
+ end
+ return data
+end
+
+local get_sysctl = function(d)
+ local data
+ if d.HostConfig and d.HostConfig.Sysctls then
+ for k, v in pairs(d.HostConfig.Sysctls) do
+ data = (data and (data .. "<br>") or "") .. k..":"..v
+ end
+ end
+ return data
+end
+
+local get_networks = function(d)
+ local data={}
+ if d.NetworkSettings and d.NetworkSettings.Networks and type(d.NetworkSettings.Networks) == "table" then
+ for k,v in pairs(d.NetworkSettings.Networks) do
+ data[k] = v.IPAddress or ""
+ end
+ end
+ return data
+end
+
+
+local start_stop_remove = function(m, cmd)
+ docker:clear_status()
+ docker:append_status("Containers: " .. cmd .. " " .. container_id .. "...")
+ local res
+ if cmd ~= "upgrade" then
+ res = dk.containers[cmd](dk, {id = container_id})
+ else
+ res = dk.containers_upgrade(dk, {id = container_id})
+ end
+ if res and res.code >= 300 then
+ docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
+ else
+ docker:clear_status()
+ if cmd ~= "remove" and cmd ~= "upgrade" then
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
+ else
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
+ end
+ end
+end
+
+m=SimpleForm("docker", container_info.Name:sub(2), translate("Docker Container") )
+m.redirect = luci.dispatcher.build_url("admin/docker/containers")
+-- m:append(Template("dockerman/container"))
+docker_status = m:section(SimpleSection)
+docker_status.template = "dockerman/apply_widget"
+docker_status.err=docker:read_status()
+docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
+if docker_status.err then docker:clear_status() end
+
+
+action_section = m:section(Table,{{}})
+action_section.notitle=true
+action_section.rowcolors=false
+action_section.template = "cbi/nullsection"
+
+btnstart=action_section:option(Button, "_start")
+btnstart.template = "dockerman/cbi/inlinebutton"
+btnstart.inputtitle=translate("Start")
+btnstart.inputstyle = "apply"
+btnstart.forcewrite = true
+btnrestart=action_section:option(Button, "_restart")
+btnrestart.template = "dockerman/cbi/inlinebutton"
+btnrestart.inputtitle=translate("Restart")
+btnrestart.inputstyle = "reload"
+btnrestart.forcewrite = true
+btnstop=action_section:option(Button, "_stop")
+btnstop.template = "dockerman/cbi/inlinebutton"
+btnstop.inputtitle=translate("Stop")
+btnstop.inputstyle = "reset"
+btnstop.forcewrite = true
+btnkill=action_section:option(Button, "_kill")
+btnkill.template = "dockerman/cbi/inlinebutton"
+btnkill.inputtitle=translate("Kill")
+btnkill.inputstyle = "reset"
+btnkill.forcewrite = true
+btnupgrade=action_section:option(Button, "_upgrade")
+btnupgrade.template = "dockerman/cbi/inlinebutton"
+btnupgrade.inputtitle=translate("Upgrade")
+btnupgrade.inputstyle = "reload"
+btnstop.forcewrite = true
+btnduplicate=action_section:option(Button, "_duplicate")
+btnduplicate.template = "dockerman/cbi/inlinebutton"
+btnduplicate.inputtitle=translate("Duplicate/Edit")
+btnduplicate.inputstyle = "add"
+btnstop.forcewrite = true
+btnremove=action_section:option(Button, "_remove")
+btnremove.template = "dockerman/cbi/inlinebutton"
+btnremove.inputtitle=translate("Remove")
+btnremove.inputstyle = "remove"
+btnremove.forcewrite = true
+
+btnstart.write = function(self, section)
+ start_stop_remove(m,"start")
+end
+btnrestart.write = function(self, section)
+ start_stop_remove(m,"restart")
+end
+btnupgrade.write = function(self, section)
+ start_stop_remove(m,"upgrade")
+end
+btnremove.write = function(self, section)
+ start_stop_remove(m,"remove")
+end
+btnstop.write = function(self, section)
+ start_stop_remove(m,"stop")
+end
+btnkill.write = function(self, section)
+ start_stop_remove(m,"kill")
+end
+btnduplicate.write = function(self, section)
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer/duplicate/"..container_id))
+end
+
+tab_section = m:section(SimpleSection)
+tab_section.template = "dockerman/container"
+
+if action == "info" then
+ m.submit = false
+ m.reset = false
+ table_info = {
+ ["01name"] = {_key = translate("Name"), _value = container_info.Name:sub(2) or "-", _button=translate("Update")},
+ ["02id"] = {_key = translate("ID"), _value = container_info.Id or "-"},
+ ["03image"] = {_key = translate("Image"), _value = container_info.Config.Image .. "<br>" .. container_info.Image},
+ ["04status"] = {_key = translate("Status"), _value = container_info.State and container_info.State.Status or "-"},
+ ["05created"] = {_key = translate("Created"), _value = container_info.Created or "-"},
+ }
+ table_info["06start"] = container_info.State.Status == "running" and {_key = translate("Start Time"), _value = container_info.State and container_info.State.StartedAt or "-"} or {_key = translate("Finish Time"), _value = container_info.State and container_info.State.FinishedAt or "-"}
+ table_info["07healthy"] = {_key = translate("Healthy"), _value = container_info.State and container_info.State.Health and container_info.State.Health.Status or "-"}
+ table_info["08restart"] = {_key = translate("Restart Policy"), _value = container_info.HostConfig and container_info.HostConfig.RestartPolicy and container_info.HostConfig.RestartPolicy.Name or "-", _button=translate("Update")}
+ table_info["081user"] = {_key = translate("User"), _value = container_info.Config and (container_info.Config.User ~="" and container_info.Config.User or "-") or "-"}
+ table_info["09mount"] = {_key = translate("Mount/Volume"), _value = get_mounts(container_info) or "-"}
+ table_info["10cmd"] = {_key = translate("Command"), _value = get_command(container_info) or "-"}
+ table_info["11env"] = {_key = translate("Env"), _value = get_env(container_info) or "-"}
+ table_info["12ports"] = {_key = translate("Ports"), _value = get_ports(container_info) or "-"}
+ table_info["13links"] = {_key = translate("Links"), _value = get_links(container_info) or "-"}
+ table_info["14device"] = {_key = translate("Device"), _value = get_device(container_info) or "-"}
+ table_info["15tmpfs"] = {_key = translate("Tmpfs"), _value = get_tmpfs(container_info) or "-"}
+ table_info["16dns"] = {_key = translate("DNS"), _value = get_dns(container_info) or "-"}
+ table_info["17sysctl"] = {_key = translate("Sysctl"), _value = get_sysctl(container_info) or "-"}
+ info_networks = get_networks(container_info)
+ list_networks = {}
+ for _, v in ipairs (networks) do
+ if v.Name then
+ local parent = v.Options and v.Options.parent or nil
+ local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
+ ipv6 = v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil
+ local network_name = v.Name .. " | " .. v.Driver .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "")
+ list_networks[v.Name] = network_name
+ end
+ end
+
+ if type(info_networks)== "table" then
+ for k,v in pairs(info_networks) do
+ table_info["14network"..k] = {
+ _key = translate("Network"), _value = k.. (v~="" and (" | ".. v) or ""), _button=translate("Disconnect")
+ }
+ list_networks[k]=nil
+ end
+ end
+
+ table_info["15connect"] = {_key = translate("Connect Network"), _value = list_networks ,_opts = "", _button=translate("Connect")}
+
+
+ d_info = m:section(Table,table_info)
+ d_info.nodescr=true
+ d_info.formvalue=function(self, section)
+ return table_info
+ end
+ dv_key = d_info:option(DummyValue, "_key", translate("Info"))
+ dv_key.width = "20%"
+ dv_value = d_info:option(ListValue, "_value")
+ dv_value.render = function(self, section, scope)
+ if table_info[section]._key == translate("Name") then
+ self:reset_values()
+ self.template = "cbi/value"
+ self.size = 30
+ self.keylist = {}
+ self.vallist = {}
+ self.default=table_info[section]._value
+ Value.render(self, section, scope)
+ elseif table_info[section]._key == translate("Restart Policy") then
+ self.template = "cbi/lvalue"
+ self:reset_values()
+ self.size = nil
+ self:value("no", "No")
+ self:value("unless-stopped", "Unless stopped")
+ self:value("always", "Always")
+ self:value("on-failure", "On failure")
+ self.default=table_info[section]._value
+ ListValue.render(self, section, scope)
+ elseif table_info[section]._key == translate("Connect Network") then
+ self.template = "cbi/lvalue"
+ self:reset_values()
+ self.size = nil
+ for k,v in pairs(list_networks) do
+ if k ~= "host" then
+ self:value(k,v)
+ end
+ end
+ self.default=table_info[section]._value
+ ListValue.render(self, section, scope)
+ else
+ self:reset_values()
+ self.rawhtml=true
+ self.template = "cbi/dvalue"
+ self.default=table_info[section]._value
+ DummyValue.render(self, section, scope)
+ end
+ end
+ dv_value.forcewrite = true -- for write function using simpleform
+ dv_value.write = function(self, section, value)
+ table_info[section]._value=value
+ end
+ dv_value.validate = function(self, value)
+ return value
+ end
+ dv_opts = d_info:option(Value, "_opts")
+ dv_opts.forcewrite = true -- for write function using simpleform
+ dv_opts.write = function(self, section, value)
+
+ table_info[section]._opts=value
+ end
+ dv_opts.validate = function(self, value)
+ return value
+ end
+ dv_opts.render = function(self, section, scope)
+ if table_info[section]._key==translate("Connect Network") then
+ self.template = "cbi/value"
+ self.keylist = {}
+ self.vallist = {}
+ self.placeholder = "10.1.1.254"
+ self.datatype = "ip4addr"
+ self.default=table_info[section]._opts
+ Value.render(self, section, scope)
+ else
+ self.rawhtml=true
+ self.template = "cbi/dvalue"
+ self.default=table_info[section]._opts
+ DummyValue.render(self, section, scope)
+ end
+ end
+ btn_update = d_info:option(Button, "_button")
+ btn_update.forcewrite = true
+ btn_update.render = function(self, section, scope)
+ if table_info[section]._button and table_info[section]._value ~= nil then
+ btn_update.inputtitle=table_info[section]._button
+ self.template = "cbi/button"
+ self.inputstyle = "edit"
+ Button.render(self, section, scope)
+ else
+ self.template = "cbi/dvalue"
+ self.default=""
+ DummyValue.render(self, section, scope)
+ end
+ end
+ btn_update.write = function(self, section, value)
+ local res
+ docker:clear_status()
+ if section == "01name" then
+ docker:append_status("Containers: rename " .. container_id .. "...")
+ local new_name = table_info[section]._value
+ res = dk.containers:rename({id = container_id, query = {name=new_name}})
+ elseif section == "08restart" then
+ docker:append_status("Containers: update " .. container_id .. "...")
+ local new_restart = table_info[section]._value
+ res = dk.containers:update({id = container_id, body = {RestartPolicy = {Name = new_restart}}})
+ elseif table_info[section]._key == translate("Network") then
+ local _,_,leave_network = table_info[section]._value:find("(.-) | .+")
+ leave_network = leave_network or table_info[section]._value
+ docker:append_status("Network: disconnect " .. leave_network .. container_id .. "...")
+ res = dk.networks:disconnect({name = leave_network, body = {Container = container_id}})
+ elseif section == "15connect" then
+ local connect_network = table_info[section]._value
+ local network_opiton
+ if connect_network ~= "none" and connect_network ~= "bridge" and connect_network ~= "host" then
+ network_opiton = table_info[section]._opts ~= "" and {
+ IPAMConfig={
+ IPv4Address=table_info[section]._opts
+ }
+ } or nil
+ end
+ docker:append_status("Network: connect " .. connect_network .. container_id .. "...")
+ res = dk.networks:connect({name = connect_network, body = {Container = container_id, EndpointConfig= network_opiton}})
+ end
+ if res and res.code > 300 then
+ docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
+ else
+ docker:clear_status()
+ end
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/info"))
+ end
+
+-- info end
+elseif action == "resources" then
+ local resources_section= m:section(SimpleSection)
+ d = resources_section:option( Value, "cpus", translate("CPUs"), translate("Number of CPUs. Number is a fractional number. 0.000 means no limit."))
+ d.placeholder = "1.5"
+ d.rmempty = true
+ d.datatype="ufloat"
+ d.default = container_info.HostConfig.NanoCpus / (10^9)
+
+ d = resources_section:option(Value, "cpushares", translate("CPU Shares Weight"), translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024."))
+ d.placeholder = "1024"
+ d.rmempty = true
+ d.datatype="uinteger"
+ d.default = container_info.HostConfig.CpuShares
+
+ d = resources_section:option(Value, "memory", translate("Memory"), translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M."))
+ d.placeholder = "128m"
+ d.rmempty = true
+ d.default = container_info.HostConfig.Memory ~=0 and ((container_info.HostConfig.Memory / 1024 /1024) .. "M") or 0
+
+ d = resources_section:option(Value, "blkioweight", translate("Block IO Weight"), translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000."))
+ d.placeholder = "500"
+ d.rmempty = true
+ d.datatype="uinteger"
+ d.default = container_info.HostConfig.BlkioWeight
+
+ m.handle = function(self, state, data)
+ if state == FORM_VALID then
+ local memory = data.memory
+ if memory and memory ~= 0 then
+ _,_,n,unit = memory:find("([%d%.]+)([%l%u]+)")
+ if n then
+ unit = unit and unit:sub(1,1):upper() or "B"
+ if unit == "M" then
+ memory = tonumber(n) * 1024 * 1024
+ elseif unit == "G" then
+ memory = tonumber(n) * 1024 * 1024 * 1024
+ elseif unit == "K" then
+ memory = tonumber(n) * 1024
+ else
+ memory = tonumber(n)
+ end
+ end
+ end
+ request_body = {
+ BlkioWeight = tonumber(data.blkioweight),
+ NanoCPUs = tonumber(data.cpus)*10^9,
+ Memory = tonumber(memory),
+ CpuShares = tonumber(data.cpushares)
+ }
+ docker:write_status("Containers: update " .. container_id .. "...")
+ local res = dk.containers:update({id = container_id, body = request_body})
+ if res and res.code >= 300 then
+ docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
+ else
+ docker:clear_status()
+ end
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/resources"))
+ end
+ end
+elseif action == "file" then
+ local filesection= m:section(SimpleSection)
+ m.submit = false
+ m.reset = false
+ filesection.template = "dockerman/container_file"
+ filesection.container = container_id
+elseif action == "inspect" then
+ local inspectsection= m:section(SimpleSection)
+ inspectsection.syslog = luci.jsonc.stringify(container_info, true)
+ inspectsection.title = translate("Container Inspect")
+ inspectsection.template = "dockerman/logs"
+ m.submit = false
+ m.reset = false
+elseif action == "logs" then
+ local logsection= m:section(SimpleSection)
+ local logs = ""
+ local query ={
+ stdout = 1,
+ stderr = 1,
+ tail = 1000
+ }
+ local logs = dk.containers:logs({id = container_id, query = query})
+ if logs.code == 200 then
+ logsection.syslog=logs.body
+ else
+ logsection.syslog="Get Logs ERROR\n"..logs.code..": "..logs.body
+ end
+ logsection.title=translate("Container Logs")
+ logsection.template = "dockerman/logs"
+ m.submit = false
+ m.reset = false
+elseif action == "console" then
+ m.submit = false
+ m.reset = false
+ local cmd_docker = luci.util.exec("which docker"):match("^.+docker") or nil
+ local cmd_ttyd = luci.util.exec("which ttyd"):match("^.+ttyd") or nil
+ if cmd_docker and cmd_ttyd and container_info.State.Status == "running" then
+ local consolesection= m:section(SimpleSection)
+ local cmd = "/bin/sh"
+ local uid
+ local vcommand = consolesection:option(Value, "command", translate("Command"))
+ vcommand:value("/bin/sh", "/bin/sh")
+ vcommand:value("/bin/ash", "/bin/ash")
+ vcommand:value("/bin/bash", "/bin/bash")
+ vcommand.default = "/bin/sh"
+ vcommand.forcewrite = true
+ vcommand.write = function(self, section, value)
+ cmd = value
+ end
+ local vuid = consolesection:option(Value, "uid", translate("UID"))
+ vuid.forcewrite = true
+ vuid.write = function(self, section, value)
+ uid = value
+ end
+ local btn_connect = consolesection:option(Button, "connect")
+ btn_connect.render = function(self, section, scope)
+ self.inputstyle = "add"
+ self.title = " "
+ self.inputtitle = translate("Connect")
+ Button.render(self, section, scope)
+ end
+ btn_connect.write = function(self, section)
+ local cmd_docker = luci.util.exec("which docker"):match("^.+docker") or nil
+ local cmd_ttyd = luci.util.exec("which ttyd"):match("^.+ttyd") or nil
+ if not cmd_docker or not cmd_ttyd or cmd_docker:match("^%s+$") or cmd_ttyd:match("^%s+$") then return end
+ local kill_ttyd = 'netstat -lnpt | grep ":7682[ \t].*ttyd$" | awk \'{print $NF}\' | awk -F\'/\' \'{print "kill -9 " $1}\' | sh > /dev/null'
+ luci.util.exec(kill_ttyd)
+ local hosts
+ local uci = (require "luci.model.uci").cursor()
+ local remote = uci:get("dockerman", "local", "remote_endpoint")
+ local socket_path = (remote == "false" or not remote) and uci:get("dockerman", "local", "socket_path") or nil
+ local host = (remote == "true") and uci:get("dockerman", "local", "remote_host") or nil
+ local port = (remote == "true") and uci:get("dockerman", "local", "remote_port") or nil
+ if remote and host and port then
+ hosts = host .. ':'.. port
+ elseif socket_path then
+ hosts = "unix://" .. socket_path
+ else
+ return
+ end
+ local start_cmd = cmd_ttyd .. ' -d 2 --once -p 7682 '.. cmd_docker .. ' -H "'.. hosts ..'" exec -it ' .. (uid and uid ~= "" and (" -u ".. uid .. ' ') or "").. container_id .. ' ' .. cmd .. ' &'
+ os.execute(start_cmd)
+ local console = consolesection:option(DummyValue, "console")
+ console.container_id = container_id
+ console.template = "dockerman/container_console"
+ end
+ end
+elseif action == "stats" then
+ local response = dk.containers:top({id = container_id, query = {ps_args="-aux"}})
+ local container_top
+ if response.code == 200 then
+ container_top=response.body
+ else
+ response = dk.containers:top({id = container_id})
+ if response.code == 200 then
+ container_top=response.body
+ end
+ end
+
+ if type(container_top) == "table" then
+ container_top=response.body
+ stat_section = m:section(SimpleSection)
+ stat_section.container_id = container_id
+ stat_section.template = "dockerman/container_stats"
+ table_stats = {cpu={key=translate("CPU Useage"),value='-'},memory={key=translate("Memory Useage"),value='-'}}
+ stat_section = m:section(Table, table_stats, translate("Stats"))
+ stat_section:option(DummyValue, "key", translate("Stats")).width="33%"
+ stat_section:option(DummyValue, "value")
+ top_section= m:section(Table, container_top.Processes, translate("TOP"))
+ for i, v in ipairs(container_top.Titles) do
+ top_section:option(DummyValue, i, translate(v))
+ end
+end
+m.submit = false
+m.reset = false
+end
+
+return m
diff --git a/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua
new file mode 100644
index 0000000000..2187de4662
--- /dev/null
+++ b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua
@@ -0,0 +1,195 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
+]]--
+
+require "luci.util"
+local http = require "luci.http"
+local uci = luci.model.uci.cursor()
+local docker = require "luci.model.docker"
+local dk = docker.new()
+
+local images, networks, containers
+local res = dk.images:list()
+if res.code <300 then images = res.body else return end
+res = dk.networks:list()
+if res.code <300 then networks = res.body else return end
+res = dk.containers:list({query = {all=true}})
+if res.code <300 then containers = res.body else return end
+
+local urlencode = luci.http.protocol and luci.http.protocol.urlencode or luci.util.urlencode
+
+function get_containers()
+ local data = {}
+ if type(containers) ~= "table" then return nil end
+ for i, v in ipairs(containers) do
+ local index = v.Created .. v.Id
+ data[index]={}
+ data[index]["_selected"] = 0
+ data[index]["_id"] = v.Id:sub(1,12)
+ data[index]["name"] = v.Names[1]:sub(2)
+ data[index]["_name"] = '<a href='..luci.dispatcher.build_url("admin/docker/container/"..v.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. v.Names[1]:sub(2).."</a>"
+ data[index]["_status"] = v.Status
+ if v.Status:find("^Up") then
+ data[index]["_status"] = '<font color="green">'.. data[index]["_status"] .. "</font>"
+ else
+ data[index]["_status"] = '<font color="red">'.. data[index]["_status"] .. "</font>"
+ end
+ if (type(v.NetworkSettings) == "table" and type(v.NetworkSettings.Networks) == "table") then
+ for networkname, netconfig in pairs(v.NetworkSettings.Networks) do
+ data[index]["_network"] = (data[index]["_network"] ~= nil and (data[index]["_network"] .." | ") or "").. networkname .. (netconfig.IPAddress ~= "" and (": " .. netconfig.IPAddress) or "")
+ end
+ end
+ -- networkmode = v.HostConfig.NetworkMode ~= "default" and v.HostConfig.NetworkMode or "bridge"
+ -- data[index]["_network"] = v.NetworkSettings.Networks[networkmode].IPAddress or nil
+ -- local _, _, image = v.Image:find("^sha256:(.+)")
+ -- if image ~= nil then
+ -- image=image:sub(1,12)
+ -- end
+ if v.Ports and next(v.Ports) ~= nil then
+ data[index]["_ports"] = nil
+ for _,v2 in ipairs(v.Ports) do
+ data[index]["_ports"] = (data[index]["_ports"] and (data[index]["_ports"] .. ", ") or "")
+ .. ((v2.PublicPort and v2.Type and v2.Type == "tcp") and ('<a href="javascript:void(0);" onclick="window.open((window.location.origin.match(/^(.+):\\d+$/) && window.location.origin.match(/^(.+):\\d+$/)[1] || window.location.origin) + \':\' + '.. v2.PublicPort ..', \'_blank\');">') or "")
+ .. (v2.PublicPort and (v2.PublicPort .. ":") or "") .. (v2.PrivatePort and (v2.PrivatePort .."/") or "") .. (v2.Type and v2.Type or "")
+ .. ((v2.PublicPort and v2.Type and v2.Type == "tcp")and "</a>" or "")
+ end
+ end
+ for ii,iv in ipairs(images) do
+ if iv.Id == v.ImageID then
+ data[index]["_image"] = iv.RepoTags and iv.RepoTags[1] or (iv.RepoDigests[1]:gsub("(.-)@.+", "%1") .. ":<none>")
+ end
+ end
+
+ data[index]["_image_id"] = v.ImageID:sub(8,20)
+ data[index]["_command"] = v.Command
+ end
+ return data
+end
+
+local c_lists = get_containers()
+-- list Containers
+-- m = Map("docker", translate("Docker"))
+m = SimpleForm("docker", translate("Docker"))
+m.submit=false
+m.reset=false
+
+docker_status = m:section(SimpleSection)
+docker_status.template = "dockerman/apply_widget"
+docker_status.err=docker:read_status()
+docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
+if docker_status.err then docker:clear_status() end
+
+c_table = m:section(Table, c_lists, translate("Containers"))
+c_table.nodescr=true
+-- v.template = "cbi/tblsection"
+-- v.sortable = true
+container_selecter = c_table:option(Flag, "_selected","")
+container_selecter.disabled = 0
+container_selecter.enabled = 1
+container_selecter.default = 0
+
+container_id = c_table:option(DummyValue, "_id", translate("ID"))
+container_id.width="10%"
+container_name = c_table:option(DummyValue, "_name", translate("Container Name"))
+container_name.rawhtml = true
+container_status = c_table:option(DummyValue, "_status", translate("Status"))
+container_status.width="15%"
+container_status.rawhtml=true
+container_ip = c_table:option(DummyValue, "_network", translate("Network"))
+container_ip.width="15%"
+container_ports = c_table:option(DummyValue, "_ports", translate("Ports"))
+container_ports.width="10%"
+container_ports.rawhtml = true
+container_image = c_table:option(DummyValue, "_image", translate("Image"))
+container_image.width="10%"
+container_command = c_table:option(DummyValue, "_command", translate("Command"))
+container_command.width="20%"
+
+container_selecter.write=function(self, section, value)
+ c_lists[section]._selected = value
+end
+
+local start_stop_remove = function(m,cmd)
+ local c_selected = {}
+ -- 遍历table中sectionid
+ local c_table_sids = c_table:cfgsections()
+ for _, c_table_sid in ipairs(c_table_sids) do
+ -- 得到选中项的名字
+ if c_lists[c_table_sid]._selected == 1 then
+ c_selected[#c_selected+1] = c_lists[c_table_sid].name --container_name:cfgvalue(c_table_sid)
+ end
+ end
+ if #c_selected >0 then
+ docker:clear_status()
+ local success = true
+ for _,cont in ipairs(c_selected) do
+ docker:append_status("Containers: " .. cmd .. " " .. cont .. "...")
+ local res = dk.containers[cmd](dk, {id = cont})
+ if res and res.code >= 300 then
+ success = false
+ docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
+ else
+ docker:append_status("done\n")
+ end
+ end
+ if success then docker:clear_status() end
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
+ end
+end
+
+action_section = m:section(Table,{{}})
+action_section.notitle=true
+action_section.rowcolors=false
+action_section.template="cbi/nullsection"
+
+btnnew=action_section:option(Button, "_new")
+btnnew.inputtitle= translate("New")
+btnnew.template = "dockerman/cbi/inlinebutton"
+btnnew.inputstyle = "add"
+btnnew.forcewrite = true
+btnstart=action_section:option(Button, "_start")
+btnstart.template = "dockerman/cbi/inlinebutton"
+btnstart.inputtitle=translate("Start")
+btnstart.inputstyle = "apply"
+btnstart.forcewrite = true
+btnrestart=action_section:option(Button, "_restart")
+btnrestart.template = "dockerman/cbi/inlinebutton"
+btnrestart.inputtitle=translate("Restart")
+btnrestart.inputstyle = "reload"
+btnrestart.forcewrite = true
+btnstop=action_section:option(Button, "_stop")
+btnstop.template = "dockerman/cbi/inlinebutton"
+btnstop.inputtitle=translate("Stop")
+btnstop.inputstyle = "reset"
+btnstop.forcewrite = true
+btnkill=action_section:option(Button, "_kill")
+btnkill.template = "dockerman/cbi/inlinebutton"
+btnkill.inputtitle=translate("Kill")
+btnkill.inputstyle = "reset"
+btnkill.forcewrite = true
+btnremove=action_section:option(Button, "_remove")
+btnremove.template = "dockerman/cbi/inlinebutton"
+btnremove.inputtitle=translate("Remove")
+btnremove.inputstyle = "remove"
+btnremove.forcewrite = true
+btnnew.write = function(self, section)
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
+end
+btnstart.write = function(self, section)
+ start_stop_remove(m,"start")
+end
+btnrestart.write = function(self, section)
+ start_stop_remove(m,"restart")
+end
+btnremove.write = function(self, section)
+ start_stop_remove(m,"remove")
+end
+btnstop.write = function(self, section)
+ start_stop_remove(m,"stop")
+end
+btnkill.write = function(self, section)
+ start_stop_remove(m,"kill")
+end
+
+return m
diff --git a/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua
new file mode 100644
index 0000000000..29d4a63573
--- /dev/null
+++ b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua
@@ -0,0 +1,223 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
+]]--
+
+require "luci.util"
+local uci = luci.model.uci.cursor()
+local docker = require "luci.model.docker"
+local dk = docker.new()
+
+local containers, images
+local res = dk.images:list()
+if res.code <300 then images = res.body else return end
+res = dk.containers:list({query = {all=true}})
+if res.code <300 then containers = res.body else return end
+
+function get_images()
+ local data = {}
+ for i, v in ipairs(images) do
+ local index = v.Created .. v.Id
+ data[index]={}
+ data[index]["_selected"] = 0
+ data[index]["id"] = v.Id:sub(8)
+ data[index]["_id"] = '<a href="javascript:new_tag(\''..v.Id:sub(8,20)..'\')" class="dockerman-link" title="'..translate("New tag")..'">' .. v.Id:sub(8,20) .. '</a>'
+ if v.RepoTags and next(v.RepoTags)~=nil then
+ for i, v1 in ipairs(v.RepoTags) do
+ data[index]["_tags"] =(data[index]["_tags"] and ( data[index]["_tags"] .. "<br>" )or "") .. ((v1:match("<none>") or (#v.RepoTags == 1)) and v1 or ('<a href="javascript:un_tag(\''..v1..'\')" class="dockerman_link" title="'..translate("Remove tag")..'" >' .. v1 .. '</a>'))
+ if not data[index]["tag"] then
+ data[index]["tag"] = v1--:match("<none>") and nil or v1
+ end
+ end
+ else
+ data[index]["_tags"] = v.RepoDigests[1] and v.RepoDigests[1]:match("^(.-)@.+")
+ data[index]["_tags"] = (data[index]["_tags"] and data[index]["_tags"] or "<none>" ).. ":<none>"
+ end
+ data[index]["_tags"] = data[index]["_tags"]:gsub("<none>","&lt;none&gt;")
+ -- data[index]["_tags"] = '<a href="javascript:handle_tag(\''..data[index]["_id"]..'\')">' .. data[index]["_tags"] .. '</a>'
+ for ci,cv in ipairs(containers) do
+ if v.Id == cv.ImageID then
+ data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
+ '<a href='..luci.dispatcher.build_url("admin/docker/container/"..cv.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. cv.Names[1]:sub(2).."</a>"
+ end
+ end
+ data[index]["_size"] = string.format("%.2f", tostring(v.Size/1024/1024)).."MB"
+ data[index]["_created"] = os.date("%Y/%m/%d %H:%M:%S",v.Created)
+ end
+ return data
+end
+
+local image_list = get_images()
+
+-- m = Map("docker", translate("Docker"))
+m = SimpleForm("docker", translate("Docker"))
+m.submit=false
+m.reset=false
+
+local pull_value={_image_tag_name="", _registry="index.docker.io"}
+local pull_section = m:section(SimpleSection, translate("Pull Image"))
+pull_section.template="cbi/nullsection"
+local tag_name = pull_section:option(Value, "_image_tag_name")
+tag_name.template = "dockerman/cbi/inlinevalue"
+tag_name.placeholder="lisaac/luci:latest"
+local action_pull = pull_section:option(Button, "_pull")
+action_pull.inputtitle= translate("Pull")
+action_pull.template = "dockerman/cbi/inlinebutton"
+action_pull.inputstyle = "add"
+tag_name.write = function(self, section, value)
+ local hastag = value:find(":")
+ if not hastag then
+ value = value .. ":latest"
+ end
+ pull_value["_image_tag_name"] = value
+end
+action_pull.write = function(self, section)
+ local tag = pull_value["_image_tag_name"]
+ local json_stringify = luci.jsonc and luci.jsonc.stringify
+ if tag and tag ~= "" then
+ docker:write_status("Images: " .. "pulling" .. " " .. tag .. "...\n")
+ -- local x_auth = nixio.bin.b64encode(json_stringify({serveraddress= server})) , header={["X-Registry-Auth"] = x_auth}
+ local res = dk.images:create({query = {fromImage=tag}}, docker.pull_image_show_status_cb)
+ -- {"errorDetail": {"message": "failed to register layer: ApplyLayer exit status 1 stdout: stderr: write \/docker: no space left on device" }, "error": "failed to register layer: ApplyLayer exit status 1 stdout: stderr: write \/docker: no space left on device" }
+ if res and res.code == 200 and (res.body[#res.body] and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. tag)) then
+ docker:clear_status()
+ else
+ docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
+ end
+ else
+ docker:append_status("code: 400 please input the name of image name!")
+ end
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
+end
+
+local import_section = m:section(SimpleSection, translate("Import Images"))
+local im = import_section:option(DummyValue, "_image_import")
+im.template = "dockerman/images_import"
+
+local image_table = m:section(Table, image_list, translate("Images"))
+
+local image_selecter = image_table:option(Flag, "_selected","")
+image_selecter.disabled = 0
+image_selecter.enabled = 1
+image_selecter.default = 0
+
+local image_id = image_table:option(DummyValue, "_id", translate("ID"))
+image_id.rawhtml = true
+image_table:option(DummyValue, "_tags", translate("RepoTags")).rawhtml = true
+image_table:option(DummyValue, "_containers", translate("Containers")).rawhtml = true
+image_table:option(DummyValue, "_size", translate("Size"))
+image_table:option(DummyValue, "_created", translate("Created"))
+image_selecter.write = function(self, section, value)
+ image_list[section]._selected = value
+end
+
+local remove_action = function(force)
+ local image_selected = {}
+ -- 遍历table中sectionid
+ local image_table_sids = image_table:cfgsections()
+ for _, image_table_sid in ipairs(image_table_sids) do
+ -- 得到选中项的名字
+ if image_list[image_table_sid]._selected == 1 then
+ image_selected[#image_selected+1] = (image_list[image_table_sid]["_tags"]:match("<br>") or image_list[image_table_sid]["_tags"]:match("&lt;none&gt;")) and image_list[image_table_sid].id or image_list[image_table_sid].tag
+ end
+ end
+ if next(image_selected) ~= nil then
+ local success = true
+ docker:clear_status()
+ for _,img in ipairs(image_selected) do
+ docker:append_status("Images: " .. "remove" .. " " .. img .. "...")
+ local query
+ if force then query = {force = true} end
+ local msg = dk.images:remove({id = img, query = query})
+ if msg.code ~= 200 then
+ docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
+ success = false
+ else
+ docker:append_status("done\n")
+ end
+ end
+ if success then docker:clear_status() end
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
+ end
+end
+
+local docker_status = m:section(SimpleSection)
+docker_status.template = "dockerman/apply_widget"
+docker_status.err = docker:read_status()
+docker_status.err = docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
+if docker_status.err then docker:clear_status() end
+
+local action = m:section(Table,{{}})
+action.notitle=true
+action.rowcolors=false
+action.template="cbi/nullsection"
+
+local btnremove = action:option(Button, "remove")
+btnremove.inputtitle= translate("Remove")
+btnremove.template = "dockerman/cbi/inlinebutton"
+btnremove.inputstyle = "remove"
+btnremove.forcewrite = true
+btnremove.write = function(self, section)
+ remove_action()
+end
+
+local btnforceremove = action:option(Button, "forceremove")
+btnforceremove.inputtitle= translate("Force Remove")
+btnforceremove.template = "dockerman/cbi/inlinebutton"
+btnforceremove.inputstyle = "remove"
+btnforceremove.forcewrite = true
+btnforceremove.write = function(self, section)
+ remove_action(true)
+end
+
+local btnsave = action:option(Button, "save")
+btnsave.inputtitle= translate("Save")
+btnsave.template = "dockerman/cbi/inlinebutton"
+btnsave.inputstyle = "edit"
+btnsave.forcewrite = true
+btnsave.write = function (self, section)
+ local image_selected = {}
+ local image_table_sids = image_table:cfgsections()
+ for _, image_table_sid in ipairs(image_table_sids) do
+ if image_list[image_table_sid]._selected == 1 then
+ image_selected[#image_selected+1] = image_list[image_table_sid].id --image_id:cfgvalue(image_table_sid)
+ end
+ end
+ if next(image_selected) ~= nil then
+ local names
+ for _,img in ipairs(image_selected) do
+ names = names and (names .. "&names=".. img) or img
+ end
+ local first
+ local cb = function(res, chunk)
+ if res.code == 200 then
+ if not first then
+ first = true
+ luci.http.header('Content-Disposition', 'inline; filename="images.tar"')
+ luci.http.header('Content-Type', 'application\/x-tar')
+ end
+ luci.ltn12.pump.all(chunk, luci.http.write)
+ else
+ if not first then
+ first = true
+ luci.http.prepare_content("text/plain")
+ end
+ luci.ltn12.pump.all(chunk, luci.http.write)
+ end
+ end
+ docker:write_status("Images: " .. "save" .. " " .. table.concat(image_selected, "\n") .. "...")
+ local msg = dk.images:get({query = {names = names}}, cb)
+ if msg.code ~= 200 then
+ docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
+ success = false
+ else
+ docker:clear_status()
+ end
+ end
+end
+
+local btnload = action:option(Button, "load")
+btnload.inputtitle= translate("Load")
+btnload.template = "dockerman/images_load"
+btnload.inputstyle = "add"
+return m
diff --git a/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua
new file mode 100644
index 0000000000..e8e392f6ab
--- /dev/null
+++ b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua
@@ -0,0 +1,130 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
+]]--
+
+require "luci.util"
+local uci = luci.model.uci.cursor()
+local docker = require "luci.model.docker"
+local dk = docker.new()
+local networks
+local res = dk.networks:list()
+if res.code < 300 then networks = res.body else return end
+
+local get_networks = function ()
+ local data = {}
+
+ if type(networks) ~= "table" then return nil end
+ for i, v in ipairs(networks) do
+ local index = v.Created .. v.Id
+ data[index]={}
+ data[index]["_selected"] = 0
+ data[index]["_id"] = v.Id:sub(1,12)
+ data[index]["_name"] = v.Name
+ data[index]["_driver"] = v.Driver
+ if v.Driver == "bridge" then
+ data[index]["_interface"] = v.Options["com.docker.network.bridge.name"]
+ elseif v.Driver == "macvlan" then
+ data[index]["_interface"] = v.Options.parent
+ end
+ data[index]["_subnet"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
+ data[index]["_gateway"] = v.IPAM and v.IPAM.Config[1] and v.IPAM.Config[1].Gateway or nil
+ end
+ return data
+end
+
+local network_list = get_networks()
+-- m = Map("docker", translate("Docker"))
+m = SimpleForm("docker", translate("Docker"))
+m.submit=false
+m.reset=false
+
+network_table = m:section(Table, network_list, translate("Networks"))
+network_table.nodescr=true
+
+network_selecter = network_table:option(Flag, "_selected","")
+network_selecter.template = "dockerman/cbi/xfvalue"
+network_id = network_table:option(DummyValue, "_id", translate("ID"))
+network_selecter.disabled = 0
+network_selecter.enabled = 1
+network_selecter.default = 0
+network_selecter.render = function(self, section, scope)
+ self.disable = 0
+ if network_list[section]["_name"] == "bridge" or network_list[section]["_name"] == "none" or network_list[section]["_name"] == "host" then
+ self.disable = 1
+ end
+ Flag.render(self, section, scope)
+end
+
+network_name = network_table:option(DummyValue, "_name", translate("Network Name"))
+network_driver = network_table:option(DummyValue, "_driver", translate("Driver"))
+network_interface = network_table:option(DummyValue, "_interface", translate("Parent Interface"))
+network_subnet = network_table:option(DummyValue, "_subnet", translate("Subnet"))
+network_gateway = network_table:option(DummyValue, "_gateway", translate("Gateway"))
+
+network_selecter.write = function(self, section, value)
+ network_list[section]._selected = value
+end
+
+docker_status = m:section(SimpleSection)
+docker_status.template = "dockerman/apply_widget"
+docker_status.err=docker:read_status()
+docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
+if docker_status.err then docker:clear_status() end
+
+action = m:section(Table,{{}})
+action.notitle=true
+action.rowcolors=false
+action.template="cbi/nullsection"
+btnnew=action:option(Button, "_new")
+btnnew.inputtitle= translate("New")
+btnnew.template = "dockerman/cbi/inlinebutton"
+btnnew.notitle=true
+btnnew.inputstyle = "add"
+btnnew.forcewrite = true
+btnnew.write = function(self, section)
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
+end
+btnremove = action:option(Button, "_remove")
+btnremove.inputtitle= translate("Remove")
+btnremove.template = "dockerman/cbi/inlinebutton"
+btnremove.inputstyle = "remove"
+btnremove.forcewrite = true
+btnremove.write = function(self, section)
+ local network_selected = {}
+ local network_name_selected = {}
+ local network_driver_selected = {}
+ -- 遍历table中sectionid
+ local network_table_sids = network_table:cfgsections()
+ for _, network_table_sid in ipairs(network_table_sids) do
+ -- 得到选中项的名字
+ if network_list[network_table_sid]._selected == 1 then
+ network_selected[#network_selected+1] = network_list[network_table_sid]._id --network_name:cfgvalue(network_table_sid)
+ network_name_selected[#network_name_selected+1] = network_list[network_table_sid]._name
+ network_driver_selected[#network_driver_selected+1] = network_list[network_table_sid]._driver
+ end
+ end
+ if next(network_selected) ~= nil then
+ local success = true
+ docker:clear_status()
+ for ii, net in ipairs(network_selected) do
+ docker:append_status("Networks: " .. "remove" .. " " .. net .. "...")
+ local res = dk.networks["remove"](dk, {id = net})
+ if res and res.code >= 300 then
+ docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
+ success = false
+ else
+ docker:append_status("done\n")
+ if network_driver_selected[ii] == "macvlan" then
+ docker.remove_macvlan_interface(network_name_selected[ii])
+ end
+ end
+ end
+ if success then
+ docker:clear_status()
+ end
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
+ end
+end
+
+return m
diff --git a/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua
new file mode 100644
index 0000000000..324fc6dd70
--- /dev/null
+++ b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua
@@ -0,0 +1,653 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
+]]--
+
+require "luci.util"
+local uci = luci.model.uci.cursor()
+local docker = require "luci.model.docker"
+local dk = docker.new()
+local cmd_line = table.concat(arg, '/')
+local create_body = {}
+
+local images = dk.images:list().body
+local networks = dk.networks:list().body
+local containers = dk.containers:list({query = {all=true}}).body
+
+local is_quot_complete = function(str)
+ require "math"
+ if not str then return true end
+ local num = 0, w
+ for w in str:gmatch("\"") do
+ num = num + 1
+ end
+ if math.fmod(num, 2) ~= 0 then return false end
+ num = 0
+ for w in str:gmatch("\'") do
+ num = num + 1
+ end
+ if math.fmod(num, 2) ~= 0 then return false end
+ return true
+end
+
+local resolve_cli = function(cmd_line)
+ local config = {advance = 1}
+ local key_no_val = '|t|d|i|tty|rm|read_only|interactive|init|help|detach|privileged|P|publish_all|'
+ local key_with_val = '|sysctl|add_host|a|attach|blkio_weight_device|cap_add|cap_drop|device|device_cgroup_rule|device_read_bps|device_read_iops|device_write_bps|device_write_iops|dns|dns_option|dns_search|e|env|env_file|expose|group_add|l|label|label_file|link|link_local_ip|log_driver|log_opt|network_alias|p|publish|security_opt|storage_opt|tmpfs|v|volume|volumes_from|blkio_weight|cgroup_parent|cidfile|cpu_period|cpu_quota|cpu_rt_period|cpu_rt_runtime|c|cpu_shares|cpus|cpuset_cpus|cpuset_mems|detach_keys|disable_content_trust|domainname|entrypoint|gpus|health_cmd|health_interval|health_retries|health_start_period|health_timeout|h|hostname|ip|ip6|ipc|isolation|kernel_memory|log_driver|mac_address|m|memory|memory_reservation|memory_swap|memory_swappiness|mount|name|network|no_healthcheck|oom_kill_disable|oom_score_adj|pid|pids_limit|restart|runtime|shm_size|sig_proxy|stop_signal|stop_timeout|ulimit|u|user|userns|uts|volume_driver|w|workdir|'
+ local key_abb = {net='network',a='attach',c='cpu-shares',d='detach',e='env',h='hostname',i='interactive',l='label',m='memory',p='publish',P='publish_all',t='tty',u='user',v='volume',w='workdir'}
+ local key_with_list = '|sysctl|add_host|a|attach|blkio_weight_device|cap_add|cap_drop|device|device_cgroup_rule|device_read_bps|device_read_iops|device_write_bps|device_write_iops|dns|dns_option|dns_search|e|env|env_file|expose|group_add|l|label|label_file|link|link_local_ip|log_driver|log_opt|network_alias|p|publish|security_opt|storage_opt|tmpfs|v|volume|volumes_from|'
+ local key = nil
+ local _key = nil
+ local val = nil
+ local is_cmd = false
+
+ cmd_line = cmd_line:match("^DOCKERCLI%s+(.+)")
+ for w in cmd_line:gmatch("[^%s]+") do
+ if w =='\\' then
+ elseif not key and not _key and not is_cmd then
+ --key=val
+ key, val = w:match("^%-%-([%lP%-]-)=(.+)")
+ if not key then
+ --key val
+ key = w:match("^%-%-([%lP%-]+)")
+ if not key then
+ -- -v val
+ key = w:match("^%-([%lP%-]+)")
+ if key then
+ -- for -dit
+ if key:match("i") or key:match("t") or key:match("d") then
+ if key:match("i") then
+ config[key_abb["i"]] = true
+ key:gsub("i", "")
+ end
+ if key:match("t") then
+ config[key_abb["t"]] = true
+ key:gsub("t", "")
+ end
+ if key:match("d") then
+ config[key_abb["d"]] = true
+ key:gsub("d", "")
+ end
+ if key:match("P") then
+ config[key_abb["P"]] = true
+ key:gsub("P", "")
+ end
+ if key == "" then key = nil end
+ end
+ end
+ end
+ end
+ if key then
+ key = key:gsub("-","_")
+ key = key_abb[key] or key
+ if key_no_val:match("|"..key.."|") then
+ config[key] = true
+ val = nil
+ key = nil
+ elseif key_with_val:match("|"..key.."|") then
+ -- if key == "cap_add" then config.privileged = true end
+ else
+ key = nil
+ val = nil
+ end
+ else
+ config.image = w
+ key = nil
+ val = nil
+ is_cmd = true
+ end
+ elseif (key or _key) and not is_cmd then
+ if key == "mount" then
+ -- we need resolve mount options here
+ -- type=bind,source=/source,target=/app
+ local _type = w:match("^type=([^,]+),") or "bind"
+ local source = (_type ~= "tmpfs") and (w:match("source=([^,]+),") or w:match("src=([^,]+),")) or ""
+ local target = w:match(",target=([^,]+)") or w:match(",dst=([^,]+)") or w:match(",destination=([^,]+)") or ""
+ local ro = w:match(",readonly") and "ro" or nil
+ if source and target then
+ if _type ~= "tmpfs" then
+ -- bind or volume
+ local bind_propagation = (_type == "bind") and w:match(",bind%-propagation=([^,]+)") or nil
+ val = source..":"..target .. ((ro or bind_propagation) and (":" .. (ro and ro or "") .. (((ro and bind_propagation) and "," or "") .. (bind_propagation and bind_propagation or ""))or ""))
+ else
+ -- tmpfs
+ local tmpfs_mode = w:match(",tmpfs%-mode=([^,]+)") or nil
+ local tmpfs_size = w:match(",tmpfs%-size=([^,]+)") or nil
+ key = "tmpfs"
+ val = target .. ((tmpfs_mode or tmpfs_size) and (":" .. (tmpfs_mode and ("mode=" .. tmpfs_mode) or "") .. ((tmpfs_mode and tmpfs_size) and "," or "") .. (tmpfs_size and ("size=".. tmpfs_size) or "")) or "")
+ if not config[key] then config[key] = {} end
+ table.insert( config[key], val )
+ key = nil
+ val = nil
+ end
+ end
+ else
+ val = w
+ end
+ elseif is_cmd then
+ config["command"] = (config["command"] and (config["command"] .. " " )or "") .. w
+ end
+ if (key or _key) and val then
+ key = _key or key
+ if key_with_list:match("|"..key.."|") then
+ if not config[key] then config[key] = {} end
+ if _key then
+ config[key][#config[key]] = config[key][#config[key]] .. " " .. w
+ else
+ table.insert( config[key], val )
+ end
+ if is_quot_complete(config[key][#config[key]]) then
+ -- clear quotation marks
+ config[key][#config[key]] = config[key][#config[key]]:gsub("[\"\']", "")
+ _key = nil
+ else
+ _key = key
+ end
+ else
+ config[key] = (config[key] and (config[key] .. " ") or "") .. val
+ if is_quot_complete(config[key]) then
+ -- clear quotation marks
+ config[key] = config[key]:gsub("[\"\']", "")
+ _key = nil
+ else
+ _key = key
+ end
+ end
+ key = nil
+ val = nil
+ end
+ end
+ return config
+end
+-- reslvo default config
+local default_config = {}
+if cmd_line and cmd_line:match("^DOCKERCLI.+") then
+ default_config = resolve_cli(cmd_line)
+elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then
+ local container_id = cmd_line:match("^duplicate/(.+)")
+ create_body = dk:containers_duplicate_config({id = container_id}) or {}
+ if not create_body.HostConfig then create_body.HostConfig = {} end
+ if next(create_body) ~= nil then
+ default_config.name = nil
+ default_config.image = create_body.Image
+ default_config.hostname = create_body.Hostname
+ default_config.tty = create_body.Tty and true or false
+ default_config.interactive = create_body.OpenStdin and true or false
+ default_config.privileged = create_body.HostConfig.Privileged and true or false
+ default_config.restart = create_body.HostConfig.RestartPolicy and create_body.HostConfig.RestartPolicy.name or nil
+ -- default_config.network = create_body.HostConfig.NetworkMode == "default" and "bridge" or create_body.HostConfig.NetworkMode
+ -- if container has leave original network, and add new network, .HostConfig.NetworkMode is INcorrect, so using first child of .NetworkingConfig.EndpointsConfig
+ default_config.network = create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and next(create_body.NetworkingConfig.EndpointsConfig) or nil
+ default_config.ip = default_config.network and default_config.network ~= "bridge" and default_config.network ~= "host" and default_config.network ~= "null" and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig.IPv4Address or nil
+ default_config.link = create_body.HostConfig.Links
+ default_config.env = create_body.Env
+ default_config.dns = create_body.HostConfig.Dns
+ default_config.volume = create_body.HostConfig.Binds
+ default_config.cap_add = create_body.HostConfig.CapAdd
+ default_config.publish_all = create_body.HostConfig.PublishAllPorts
+
+ if create_body.HostConfig.Sysctls and type(create_body.HostConfig.Sysctls) == "table" then
+ default_config.sysctl = {}
+ for k, v in pairs(create_body.HostConfig.Sysctls) do
+ table.insert( default_config.sysctl, k.."="..v )
+ end
+ end
+
+ if create_body.HostConfig.LogConfig and create_body.HostConfig.LogConfig.Config and type(create_body.HostConfig.LogConfig.Config) == "table" then
+ default_config.log_opt = {}
+ for k, v in pairs(create_body.HostConfig.LogConfig.Config) do
+ table.insert( default_config.log_opt, k.."="..v )
+ end
+ end
+
+ if create_body.HostConfig.PortBindings and type(create_body.HostConfig.PortBindings) == "table" then
+ default_config.publish = {}
+ for k, v in pairs(create_body.HostConfig.PortBindings) do
+ table.insert( default_config.publish, v[1].HostPort..":"..k:match("^(%d+)/.+").."/"..k:match("^%d+/(.+)") )
+ end
+ end
+
+ default_config.user = create_body.User or nil
+ default_config.command = create_body.Cmd and type(create_body.Cmd) == "table" and table.concat(create_body.Cmd, " ") or nil
+ default_config.advance = 1
+ default_config.cpus = create_body.HostConfig.NanoCPUs
+ default_config.cpu_shares = create_body.HostConfig.CpuShares
+ default_config.memory = create_body.HostConfig.Memory
+ default_config.blkio_weight = create_body.HostConfig.BlkioWeight
+
+ if create_body.HostConfig.Devices and type(create_body.HostConfig.Devices) == "table" then
+ default_config.device = {}
+ for _, v in ipairs(create_body.HostConfig.Devices) do
+ table.insert( default_config.device, v.PathOnHost..":"..v.PathInContainer..(v.CgroupPermissions ~= "" and (":" .. v.CgroupPermissions) or "") )
+ end
+ end
+ if create_body.HostConfig.Tmpfs and type(create_body.HostConfig.Tmpfs) == "table" then
+ default_config.tmpfs = {}
+ for k, v in pairs(create_body.HostConfig.Tmpfs) do
+ table.insert( default_config.tmpfs, k .. (v~="" and ":" or "")..v )
+ end
+ end
+ end
+end
+
+local m = SimpleForm("docker", translate("Docker"))
+m.redirect = luci.dispatcher.build_url("admin", "docker", "containers")
+-- m.reset = false
+-- m.submit = false
+-- new Container
+
+docker_status = m:section(SimpleSection)
+docker_status.template = "dockerman/apply_widget"
+docker_status.err=docker:read_status()
+docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
+if docker_status.err then docker:clear_status() end
+
+local s = m:section(SimpleSection, translate("New Container"))
+s.addremove = true
+s.anonymous = true
+
+local d = s:option(DummyValue,"cmd_line", translate("Resolve CLI"))
+d.rawhtml = true
+d.template = "dockerman/newcontainer_resolve"
+
+d = s:option(Value, "name", translate("Container Name"))
+d.rmempty = true
+d.default = default_config.name or nil
+
+d = s:option(Flag, "interactive", translate("Interactive (-i)"))
+d.rmempty = true
+d.disabled = 0
+d.enabled = 1
+d.default = default_config.interactive and 1 or 0
+
+d = s:option(Flag, "tty", translate("TTY (-t)"))
+d.rmempty = true
+d.disabled = 0
+d.enabled = 1
+d.default = default_config.tty and 1 or 0
+
+d = s:option(Value, "image", translate("Docker Image"))
+d.rmempty = true
+d.default = default_config.image or nil
+for _, v in ipairs (images) do
+ if v.RepoTags then
+ d:value(v.RepoTags[1], v.RepoTags[1])
+ end
+end
+
+d = s:option(Flag, "_force_pull", translate("Always pull image first"))
+d.rmempty = true
+d.disabled = 0
+d.enabled = 1
+d.default = 0
+
+d = s:option(Flag, "privileged", translate("Privileged"))
+d.rmempty = true
+d.disabled = 0
+d.enabled = 1
+d.default = default_config.privileged and 1 or 0
+
+d = s:option(ListValue, "restart", translate("Restart Policy"))
+d.rmempty = true
+
+d:value("no", "No")
+d:value("unless-stopped", "Unless stopped")
+d:value("always", "Always")
+d:value("on-failure", "On failure")
+d.default = default_config.restart or "unless-stopped"
+
+local d_network = s:option(ListValue, "network", translate("Networks"))
+d_network.rmempty = true
+d_network.default = default_config.network or "bridge"
+
+local d_ip = s:option(Value, "ip", translate("IPv4 Address"))
+d_ip.datatype="ip4addr"
+d_ip:depends("network", "nil")
+d_ip.default = default_config.ip or nil
+
+d = s:option(DynamicList, "link", translate("Links with other containers"))
+d.placeholder = "container_name:alias"
+d.rmempty = true
+d:depends("network", "bridge")
+d.default = default_config.link or nil
+
+d = s:option(DynamicList, "dns", translate("Set custom DNS servers"))
+d.placeholder = "8.8.8.8"
+d.rmempty = true
+d.default = default_config.dns or nil
+
+d = s:option(Value, "user", translate("User(-u)"), translate("The user that commands are run as inside the container.(format: name|uid[:group|gid])"))
+d.placeholder = "1000:1000"
+d.rmempty = true
+d.default = default_config.user or nil
+
+d = s:option(DynamicList, "env", translate("Environmental Variable(-e)"), translate("Set environment variables to inside the container"))
+d.placeholder = "TZ=Asia/Shanghai"
+d.rmempty = true
+d.default = default_config.env or nil
+
+d = s:option(DynamicList, "volume", translate("Bind Mount(-v)"), translate("Bind mount a volume"))
+d.placeholder = "/media:/media:slave"
+d.rmempty = true
+d.default = default_config.volume or nil
+
+local d_publish = s:option(DynamicList, "publish", translate("Exposed Ports(-p)"), translate("Publish container's port(s) to the host"))
+d_publish.placeholder = "2200:22/tcp"
+d_publish.rmempty = true
+d_publish.default = default_config.publish or nil
+
+d = s:option(Value, "command", translate("Run command"))
+d.placeholder = "/bin/sh init.sh"
+d.rmempty = true
+d.default = default_config.command or nil
+
+d = s:option(Flag, "advance", translate("Advance"))
+d.rmempty = true
+d.disabled = 0
+d.enabled = 1
+d.default = default_config.advance or 0
+
+d = s:option(Value, "hostname", translate("Host Name"), translate("The hostname to use for the container"))
+d.rmempty = true
+d.default = default_config.hostname or nil
+d:depends("advance", 1)
+
+d = s:option(Flag, "publish_all", translate("Exposed All Ports(-P)"), translate("Allocates an ephemeral host port for all of a container's exposed ports"))
+d.rmempty = true
+d.disabled = 0
+d.enabled = 1
+d.default = default_config.publish_all and 1 or 0
+d:depends("advance", 1)
+
+d = s:option(DynamicList, "device", translate("Device(--device)"), translate("Add host device to the container"))
+d.placeholder = "/dev/sda:/dev/xvdc:rwm"
+d.rmempty = true
+d:depends("advance", 1)
+d.default = default_config.device or nil
+
+d = s:option(DynamicList, "tmpfs", translate("Tmpfs(--tmpfs)"), translate("Mount tmpfs directory"))
+d.placeholder = "/run:rw,noexec,nosuid,size=65536k"
+d.rmempty = true
+d:depends("advance", 1)
+d.default = default_config.tmpfs or nil
+
+d = s:option(DynamicList, "sysctl", translate("Sysctl(--sysctl)"), translate("Sysctls (kernel parameters) options"))
+d.placeholder = "net.ipv4.ip_forward=1"
+d.rmempty = true
+d:depends("advance", 1)
+d.default = default_config.sysctl or nil
+
+d = s:option(DynamicList, "cap_add", translate("CAP-ADD(--cap-add)"), translate("A list of kernel capabilities to add to the container"))
+d.placeholder = "NET_ADMIN"
+d.rmempty = true
+d:depends("advance", 1)
+d.default = default_config.cap_add or nil
+
+d = s:option(Value, "cpus", translate("CPUs"), translate("Number of CPUs. Number is a fractional number. 0.000 means no limit"))
+d.placeholder = "1.5"
+d.rmempty = true
+d:depends("advance", 1)
+d.datatype="ufloat"
+d.default = default_config.cpus or nil
+
+d = s:option(Value, "cpu_shares", translate("CPU Shares Weight"), translate("CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024"))
+d.placeholder = "1024"
+d.rmempty = true
+d:depends("advance", 1)
+d.datatype="uinteger"
+d.default = default_config.cpu_shares or nil
+
+d = s:option(Value, "memory", translate("Memory"), translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M"))
+d.placeholder = "128m"
+d.rmempty = true
+d:depends("advance", 1)
+d.default = default_config.memory or nil
+
+d = s:option(Value, "blkio_weight", translate("Block IO Weight"), translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000"))
+d.placeholder = "500"
+d.rmempty = true
+d:depends("advance", 1)
+d.datatype="uinteger"
+d.default = default_config.blkio_weight or nil
+
+d = s:option(DynamicList, "log_opt", translate("Log driver options"), translate("The logging configuration for this container"))
+d.placeholder = "max-size=1m"
+d.rmempty = true
+d:depends("advance", 1)
+d.default = default_config.log_opt or nil
+
+for _, v in ipairs (networks) do
+ if v.Name then
+ local parent = v.Options and v.Options.parent or nil
+ local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
+ ipv6 = v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil
+ local network_name = v.Name .. " | " .. v.Driver .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "")
+ d_network:value(v.Name, network_name)
+
+ if v.Name ~= "none" and v.Name ~= "bridge" and v.Name ~= "host" then
+ d_ip:depends("network", v.Name)
+ end
+
+ if v.Driver == "bridge" then
+ d_publish:depends("network", v.Name)
+ end
+ end
+end
+
+m.handle = function(self, state, data)
+ if state ~= FORM_VALID then return end
+ local tmp
+ local name = data.name or ("luci_" .. os.date("%Y%m%d%H%M%S"))
+ local hostname = data.hostname
+ local tty = type(data.tty) == "number" and (data.tty == 1 and true or false) or default_config.tty or false
+ local publish_all = type(data.publish_all) == "number" and (data.publish_all == 1 and true or false) or default_config.publish_all or false
+ local interactive = type(data.interactive) == "number" and (data.interactive == 1 and true or false) or default_config.interactive or false
+ local image = data.image
+ local user = data.user
+ if image and not image:match(".-:.+") then
+ image = image .. ":latest"
+ end
+ local privileged = type(data.privileged) == "number" and (data.privileged == 1 and true or false) or default_config.privileged or false
+ local restart = data.restart
+ local env = data.env
+ local dns = data.dns
+ local cap_add = data.cap_add
+ local sysctl = {}
+ tmp = data.sysctl
+ if type(tmp) == "table" then
+ for i, v in ipairs(tmp) do
+ local k,v1 = v:match("(.-)=(.+)")
+ if k and v1 then
+ sysctl[k]=v1
+ end
+ end
+ end
+ local log_opt = {}
+ tmp = data.log_opt
+ if type(tmp) == "table" then
+ for i, v in ipairs(tmp) do
+ local k,v1 = v:match("(.-)=(.+)")
+ if k and v1 then
+ log_opt[k]=v1
+ end
+ end
+ end
+ local network = data.network
+ local ip = (network ~= "bridge" and network ~= "host" and network ~= "none") and data.ip or nil
+ local volume = data.volume
+ local memory = data.memory or 0
+ local cpu_shares = data.cpu_shares or 0
+ local cpus = data.cpus or 0
+ local blkio_weight = data.blkio_weight or 500
+
+ local portbindings = {}
+ local exposedports = {}
+ local tmpfs = {}
+ tmp = data.tmpfs
+ if type(tmp) == "table" then
+ for i, v in ipairs(tmp)do
+ local k= v:match("([^:]+)")
+ local v1 = v:match(".-:([^:]+)") or ""
+ if k then
+ tmpfs[k]=v1
+ end
+ end
+ end
+
+ local device = {}
+ tmp = data.device
+ if type(tmp) == "table" then
+ for i, v in ipairs(tmp) do
+ local t = {}
+ local _,_, h, c, p = v:find("(.-):(.-):(.+)")
+ if h and c then
+ t['PathOnHost'] = h
+ t['PathInContainer'] = c
+ t['CgroupPermissions'] = p or "rwm"
+ else
+ local _,_, h, c = v:find("(.-):(.+)")
+ if h and c then
+ t['PathOnHost'] = h
+ t['PathInContainer'] = c
+ t['CgroupPermissions'] = "rwm"
+ else
+ t['PathOnHost'] = v
+ t['PathInContainer'] = v
+ t['CgroupPermissions'] = "rwm"
+ end
+ end
+ if next(t) ~= nil then
+ table.insert( device, t )
+ end
+ end
+ end
+
+ tmp = data.publish or {}
+ for i, v in ipairs(tmp) do
+ for v1 ,v2 in string.gmatch(v, "(%d+):([^%s]+)") do
+ local _,_,p= v2:find("^%d+/(%w+)")
+ if p == nil then
+ v2=v2..'/tcp'
+ end
+ portbindings[v2] = {{HostPort=v1}}
+ exposedports[v2] = {HostPort=v1}
+ end
+ end
+
+ local link = data.link
+ tmp = data.command
+ local command = {}
+ if tmp ~= nil then
+ for v in string.gmatch(tmp, "[^%s]+") do
+ command[#command+1] = v
+ end
+ end
+ if memory ~= 0 then
+ _,_,n,unit = memory:find("([%d%.]+)([%l%u]+)")
+ if n then
+ unit = unit and unit:sub(1,1):upper() or "B"
+ if unit == "M" then
+ memory = tonumber(n) * 1024 * 1024
+ elseif unit == "G" then
+ memory = tonumber(n) * 1024 * 1024 * 1024
+ elseif unit == "K" then
+ memory = tonumber(n) * 1024
+ else
+ memory = tonumber(n)
+ end
+ end
+ end
+
+ create_body.Hostname = network ~= "host" and (hostname or name) or nil
+ create_body.Tty = tty and true or false
+ create_body.OpenStdin = interactive and true or false
+ create_body.User = user
+ create_body.Cmd = command
+ create_body.Env = env
+ create_body.Image = image
+ create_body.ExposedPorts = exposedports
+ create_body.HostConfig = create_body.HostConfig or {}
+ create_body.HostConfig.Dns = dns
+ create_body.HostConfig.Binds = volume
+ create_body.HostConfig.RestartPolicy = { Name = restart, MaximumRetryCount = 0 }
+ create_body.HostConfig.Privileged = privileged and true or false
+ create_body.HostConfig.PortBindings = portbindings
+ create_body.HostConfig.Memory = tonumber(memory)
+ create_body.HostConfig.CpuShares = tonumber(cpu_shares)
+ create_body.HostConfig.NanoCPUs = tonumber(cpus) * 10 ^ 9
+ create_body.HostConfig.BlkioWeight = tonumber(blkio_weight)
+ create_body.HostConfig.PublishAllPorts = publish_all
+ if create_body.HostConfig.NetworkMode ~= network then
+ -- network mode changed, need to clear duplicate config
+ create_body.NetworkingConfig = nil
+ end
+ create_body.HostConfig.NetworkMode = network
+ if ip then
+ if create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and type(create_body.NetworkingConfig.EndpointsConfig) == "table" then
+ -- ip + duplicate config
+ for k, v in pairs (create_body.NetworkingConfig.EndpointsConfig) do
+ if k == network and v.IPAMConfig and v.IPAMConfig.IPv4Address then
+ v.IPAMConfig.IPv4Address = ip
+ else
+ create_body.NetworkingConfig.EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } }
+ end
+ break
+ end
+ else
+ -- ip + no duplicate config
+ create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } }
+ end
+ elseif not create_body.NetworkingConfig then
+ -- no ip + no duplicate config
+ create_body.NetworkingConfig = nil
+ end
+ create_body["HostConfig"]["Tmpfs"] = tmpfs
+ create_body["HostConfig"]["Devices"] = device
+ create_body["HostConfig"]["Sysctls"] = sysctl
+ create_body["HostConfig"]["CapAdd"] = cap_add
+ create_body["HostConfig"]["LogConfig"] = next(log_opt) ~= nil and { Config = log_opt } or nil
+
+ if network == "bridge" then
+ create_body["HostConfig"]["Links"] = link
+ end
+ local pull_image = function(image)
+ local json_stringify = luci.jsonc and luci.jsonc.stringify
+ docker:append_status("Images: " .. "pulling" .. " " .. image .. "...\n")
+ local res = dk.images:create({query = {fromImage=image}}, docker.pull_image_show_status_cb)
+ if res and res.code == 200 and (res.body[#res.body] and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. image or res.body[#res.body].status == "Status: Image is up to date for ".. image)) then
+ docker:append_status("done\n")
+ else
+ res.code = (res.code == 200) and 500 or res.code
+ docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
+ end
+ end
+ docker:clear_status()
+ local exist_image = false
+ if image then
+ for _, v in ipairs (images) do
+ if v.RepoTags and v.RepoTags[1] == image then
+ exist_image = true
+ break
+ end
+ end
+ if not exist_image then
+ pull_image(image)
+ elseif data._force_pull == 1 then
+ pull_image(image)
+ end
+ end
+
+ create_body = docker.clear_empty_tables(create_body)
+ docker:append_status("Container: " .. "create" .. " " .. name .. "...")
+ local res = dk.containers:create({name = name, body = create_body})
+ if res and res.code == 201 then
+ docker:clear_status()
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
+ else
+ docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
+ end
+end
+
+return m
diff --git a/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua
new file mode 100644
index 0000000000..bdadbaf881
--- /dev/null
+++ b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua
@@ -0,0 +1,221 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
+]]--
+
+require "luci.util"
+local docker = require "luci.model.docker"
+local dk = docker.new()
+
+m = SimpleForm("docker", translate("Docker"))
+m.redirect = luci.dispatcher.build_url("admin", "docker", "networks")
+
+docker_status = m:section(SimpleSection)
+docker_status.template = "dockerman/apply_widget"
+docker_status.err=docker:read_status()
+docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
+if docker_status.err then docker:clear_status() end
+
+s = m:section(SimpleSection, translate("New Network"))
+s.addremove = true
+s.anonymous = true
+
+d = s:option(Value, "name", translate("Network Name"))
+d.rmempty = true
+
+d = s:option(ListValue, "dirver", translate("Driver"))
+d.rmempty = true
+d:value("bridge", "bridge")
+d:value("macvlan", "macvlan")
+d:value("ipvlan", "ipvlan")
+d:value("overlay", "overlay")
+
+d = s:option(Value, "parent", translate("Parent Interface"))
+d.rmempty = true
+d:depends("dirver", "macvlan")
+local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {}
+for _, v in ipairs(interfaces) do
+ d:value(v, v)
+end
+d.default="br-lan"
+d.placeholder="br-lan"
+
+d = s:option(Value, "macvlan_mode", translate("Macvlan Mode"))
+d.rmempty = true
+d:depends("dirver", "macvlan")
+d.default="bridge"
+d:value("bridge", "bridge")
+d:value("private", "private")
+d:value("vepa", "vepa")
+d:value("passthru", "passthru")
+
+d = s:option(Value, "ipvlan_mode", translate("Ipvlan Mode"))
+d.rmempty = true
+d:depends("dirver", "ipvlan")
+d.default="l3"
+d:value("l2", "l2")
+d:value("l3", "l3")
+
+d = s:option(Flag, "ingress", translate("Ingress"), translate("Ingress network is the network which provides the routing-mesh in swarm mode"))
+d.rmempty = true
+d.disabled = 0
+d.enabled = 1
+d.default = 0
+d:depends("dirver", "overlay")
+
+d = s:option(DynamicList, "options", translate("Options"))
+d.rmempty = true
+d.placeholder="com.docker.network.driver.mtu=1500"
+
+d = s:option(Flag, "internal", translate("Internal"), translate("Restrict external access to the network"))
+d.rmempty = true
+d:depends("dirver", "overlay")
+d.disabled = 0
+d.enabled = 1
+d.default = 0
+
+if nixio.fs.access("/etc/config/network") and nixio.fs.access("/etc/config/firewall")then
+ d = s:option(Flag, "op_macvlan", translate("Create macvlan interface"), translate("Auto create macvlan interface in Openwrt"))
+ d:depends("dirver", "macvlan")
+ d.disabled = 0
+ d.enabled = 1
+ d.default = 1
+end
+
+d = s:option(Value, "subnet", translate("Subnet"))
+d.rmempty = true
+d.placeholder="10.1.0.0/16"
+d.datatype="ip4addr"
+
+d = s:option(Value, "gateway", translate("Gateway"))
+d.rmempty = true
+d.placeholder="10.1.1.1"
+d.datatype="ip4addr"
+
+d = s:option(Value, "ip_range", translate("IP range"))
+d.rmempty = true
+d.placeholder="10.1.1.0/24"
+d.datatype="ip4addr"
+
+d = s:option(DynamicList, "aux_address", translate("Exclude IPs"))
+d.rmempty = true
+d.placeholder="my-route=10.1.1.1"
+
+d = s:option(Flag, "ipv6", translate("Enable IPv6"))
+d.rmempty = true
+d.disabled = 0
+d.enabled = 1
+d.default = 0
+
+d = s:option(Value, "subnet6", translate("IPv6 Subnet"))
+d.rmempty = true
+d.placeholder="fe80::/10"
+d.datatype="ip6addr"
+d:depends("ipv6", 1)
+
+d = s:option(Value, "gateway6", translate("IPv6 Gateway"))
+d.rmempty = true
+d.placeholder="fe80::1"
+d.datatype="ip6addr"
+d:depends("ipv6", 1)
+
+m.handle = function(self, state, data)
+ if state == FORM_VALID then
+ local name = data.name
+ local driver = data.dirver
+
+ local internal = data.internal == 1 and true or false
+
+ local subnet = data.subnet
+ local gateway = data.gateway
+ local ip_range = data.ip_range
+
+ local aux_address = {}
+ local tmp = data.aux_address or {}
+ for i,v in ipairs(tmp) do
+ _,_,k1,v1 = v:find("(.-)=(.+)")
+ aux_address[k1] = v1
+ end
+
+ local options = {}
+ tmp = data.options or {}
+ for i,v in ipairs(tmp) do
+ _,_,k1,v1 = v:find("(.-)=(.+)")
+ options[k1] = v1
+ end
+
+ local ipv6 = data.ipv6 == 1 and true or false
+
+ local create_body={
+ Name = name,
+ Driver = driver,
+ EnableIPv6 = ipv6,
+ IPAM = {
+ Driver= "default"
+ },
+ Internal = internal
+ }
+
+ if subnet or gateway or ip_range then
+ create_body["IPAM"]["Config"] = {
+ {
+ Subnet = subnet,
+ Gateway = gateway,
+ IPRange = ip_range,
+ AuxAddress = aux_address,
+ AuxiliaryAddresses = aux_address
+ }
+ }
+ end
+ if driver == "macvlan" then
+ create_body["Options"] = {
+ macvlan_mode = data.macvlan_mode,
+ parent = data.parent
+ }
+ elseif driver == "ipvlan" then
+ create_body["Options"] = {
+ ipvlan_mode = data.ipvlan_mode
+ }
+ elseif driver == "overlay" then
+ create_body["Ingress"] = data.ingerss == 1 and true or false
+ end
+
+ if ipv6 and data.subnet6 and data.subnet6 then
+ if type(create_body["IPAM"]["Config"]) ~= "table" then
+ create_body["IPAM"]["Config"] = {}
+ end
+ local index = #create_body["IPAM"]["Config"]
+ create_body["IPAM"]["Config"][index+1] = {
+ Subnet = data.subnet6,
+ Gateway = data.gateway6
+ }
+ end
+
+ if next(options) ~= nil then
+ create_body["Options"] = create_body["Options"] or {}
+ for k, v in pairs(options) do
+ create_body["Options"][k] = v
+ end
+ end
+
+ create_body = docker.clear_empty_tables(create_body)
+ docker:write_status("Network: " .. "create" .. " " .. create_body.Name .. "...")
+ local res = dk.networks:create({body = create_body})
+ if res and res.code == 201 then
+ docker:write_status("Network: " .. "create macvlan interface...")
+ res = dk.networks:inspect({ name = create_body.Name })
+ if driver == "macvlan" and data.op_macvlan ~= 0 and res.code == 200
+ and res.body and res.body.IPAM and res.body.IPAM.Config and res.body.IPAM.Config[1]
+ and res.body.IPAM.Config[1].Gateway and res.body.IPAM.Config[1].Subnet then
+ docker.create_macvlan_interface(data.name, data.parent, res.body.IPAM.Config[1].Gateway, res.body.IPAM.Config[1].Subnet)
+ end
+ docker:clear_status()
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
+ else
+ docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
+ end
+ end
+end
+
+return m
diff --git a/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua
new file mode 100644
index 0000000000..5515aacc72
--- /dev/null
+++ b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua
@@ -0,0 +1,154 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
+]]--
+
+require "luci.util"
+local docker = require "luci.model.docker"
+local uci = require "luci.model.uci"
+
+function byte_format(byte)
+ local suff = {"B", "KB", "MB", "GB", "TB"}
+ for i=1, 5 do
+ if byte > 1024 and i < 5 then
+ byte = byte / 1024
+ else
+ return string.format("%.2f %s", byte, suff[i])
+ end
+ end
+end
+
+local map_dockerman = Map("dockerman", translate("Docker"), translate("DockerMan is a Simple Docker manager client for LuCI, If you have any issue please visit:") .. " ".. [[<a href="https://github.com/lisaac/luci-app-dockerman" target="_blank">]] ..translate("Github") .. [[</a>]])
+local docker_info_table = {}
+-- docker_info_table['0OperatingSystem'] = {_key=translate("Operating System"),_value='-'}
+-- docker_info_table['1Architecture'] = {_key=translate("Architecture"),_value='-'}
+-- docker_info_table['2KernelVersion'] = {_key=translate("Kernel Version"),_value='-'}
+docker_info_table['3ServerVersion'] = {_key=translate("Docker Version"),_value='-'}
+docker_info_table['4ApiVersion'] = {_key=translate("Api Version"),_value='-'}
+docker_info_table['5NCPU'] = {_key=translate("CPUs"),_value='-'}
+docker_info_table['6MemTotal'] = {_key=translate("Total Memory"),_value='-'}
+docker_info_table['7DockerRootDir'] = {_key=translate("Docker Root Dir"),_value='-'}
+docker_info_table['8IndexServerAddress'] = {_key=translate("Index Server Address"),_value='-'}
+docker_info_table['9RegistryMirrors'] = {_key=translate("Registry Mirrors"),_value='-'}
+
+local s = map_dockerman:section(Table, docker_info_table)
+s:option(DummyValue, "_key", translate("Info"))
+s:option(DummyValue, "_value")
+s = map_dockerman:section(SimpleSection)
+s.containers_running = '-'
+s.images_used = '-'
+s.containers_total = '-'
+s.images_total = '-'
+s.networks_total = '-'
+s.volumes_total = '-'
+local containers_list
+-- local socket = luci.model.uci.cursor():get("dockerman", "local", "socket_path")
+if (require "luci.model.docker").new():_ping().code == 200 then
+ local dk = docker.new()
+ containers_list = dk.containers:list({query = {all=true}}).body
+ local images_list = dk.images:list().body
+ local vol = dk.volumes:list()
+ local volumes_list = vol and vol.body and vol.body.Volumes or {}
+ local networks_list = dk.networks:list().body or {}
+ local docker_info = dk:info()
+ -- docker_info_table['0OperatingSystem']._value = docker_info.body.OperatingSystem
+ -- docker_info_table['1Architecture']._value = docker_info.body.Architecture
+ -- docker_info_table['2KernelVersion']._value = docker_info.body.KernelVersion
+ docker_info_table['3ServerVersion']._value = docker_info.body.ServerVersion
+ docker_info_table['4ApiVersion']._value = docker_info.headers["Api-Version"]
+ docker_info_table['5NCPU']._value = tostring(docker_info.body.NCPU)
+ docker_info_table['6MemTotal']._value = byte_format(docker_info.body.MemTotal)
+ if docker_info.body.DockerRootDir then
+ local statvfs = nixio.fs.statvfs(docker_info.body.DockerRootDir)
+ local size = statvfs and (statvfs.bavail * statvfs.bsize) or 0
+ docker_info_table['7DockerRootDir']._value = docker_info.body.DockerRootDir .. " (" .. tostring(byte_format(size)) .. " " .. translate("Available") .. ")"
+ end
+ docker_info_table['8IndexServerAddress']._value = docker_info.body.IndexServerAddress
+ for i, v in ipairs(docker_info.body.RegistryConfig.Mirrors) do
+ docker_info_table['9RegistryMirrors']._value = docker_info_table['9RegistryMirrors']._value == "-" and v or (docker_info_table['9RegistryMirrors']._value .. ", " .. v)
+ end
+
+ s.images_used = 0
+ for i, v in ipairs(images_list) do
+ for ci,cv in ipairs(containers_list) do
+ if v.Id == cv.ImageID then
+ s.images_used = s.images_used + 1
+ break
+ end
+ end
+ end
+ s.containers_running = tostring(docker_info.body.ContainersRunning)
+ s.images_used = tostring(s.images_used)
+ s.containers_total = tostring(docker_info.body.Containers)
+ s.images_total = tostring(#images_list)
+ s.networks_total = tostring(#networks_list)
+ s.volumes_total = tostring(#volumes_list)
+end
+s.template = "dockerman/overview"
+
+local section_dockerman = map_dockerman:section(NamedSection, "local", "section", translate("Setting"))
+section_dockerman:tab("daemon", translate("Docker Daemon"))
+section_dockerman:tab("ac", translate("Access Control"))
+section_dockerman:tab("dockerman", translate("DockerMan"))
+
+local socket_path = section_dockerman:taboption("dockerman", Value, "socket_path", translate("Docker Socket Path"))
+socket_path.default = "/var/run/docker.sock"
+socket_path.placeholder = "/var/run/docker.sock"
+socket_path.rmempty = false
+
+local remote_endpoint = section_dockerman:taboption("dockerman", Flag, "remote_endpoint", translate("Remote Endpoint"), translate("Dockerman connect to remote endpoint"))
+remote_endpoint.rmempty = false
+remote_endpoint.enabled = "true"
+remote_endpoint.disabled = "false"
+
+local remote_host = section_dockerman:taboption("dockerman", Value, "remote_host", translate("Remote Host"))
+remote_host.placeholder = "10.1.1.2"
+-- remote_host:depends("remote_endpoint", "true")
+
+local remote_port = section_dockerman:taboption("dockerman", Value, "remote_port", translate("Remote Port"))
+remote_port.placeholder = "2375"
+remote_port.default = "2375"
+-- remote_port:depends("remote_endpoint", "true")
+
+-- local status_path = section_dockerman:taboption("dockerman", Value, "status_path", translate("Action Status Tempfile Path"), translate("Where you want to save the docker status file"))
+-- local debug = section_dockerman:taboption("dockerman", Flag, "debug", translate("Enable Debug"), translate("For debug, It shows all docker API actions of luci-app-dockerman in Debug Tempfile Path"))
+-- debug.enabled="true"
+-- debug.disabled="false"
+-- local debug_path = section_dockerman:taboption("dockerman", Value, "debug_path", translate("Debug Tempfile Path"), translate("Where you want to save the debug tempfile"))
+
+if nixio.fs.access("/usr/bin/dockerd") then
+ local allowed_interface = section_dockerman:taboption("ac", DynamicList, "ac_allowed_interface", translate("Allowed access interfaces"), translate("Which interface(s) can access containers under the bridge network, fill-in Interface Name"))
+ local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {}
+ for i, v in ipairs(interfaces) do
+ allowed_interface:value(v, v)
+ end
+ local allowed_container = section_dockerman:taboption("ac", DynamicList, "ac_allowed_container", translate("Containers allowed to be accessed"), translate("Which container(s) under bridge network can be accessed, even from interfaces that are not allowed, fill-in Container Id or Name"))
+ -- allowed_container.placeholder = "container name_or_id"
+ if containers_list then
+ for i, v in ipairs(containers_list) do
+ if v.State == "running" and v.NetworkSettings and v.NetworkSettings.Networks and v.NetworkSettings.Networks.bridge and v.NetworkSettings.Networks.bridge.IPAddress then
+ allowed_container:value(v.Id:sub(1,12), v.Names[1]:sub(2) .. " | " .. v.NetworkSettings.Networks.bridge.IPAddress)
+ end
+ end
+ end
+
+ local dockerd_enable = section_dockerman:taboption("daemon", Flag, "daemon_ea", translate("Enable"))
+ dockerd_enable.enabled = "true"
+ dockerd_enable.rmempty = true
+ local data_root = section_dockerman:taboption("daemon", Value, "daemon_data_root", translate("Docker Root Dir"))
+ data_root.placeholder = "/opt/docker/"
+ local registry_mirrors = section_dockerman:taboption("daemon", DynamicList, "daemon_registry_mirrors", translate("Registry Mirrors"))
+ registry_mirrors:value("https://hub-mirror.c.163.com", "https://hub-mirror.c.163.com")
+
+ local log_level = section_dockerman:taboption("daemon", ListValue, "daemon_log_level", translate("Log Level"), translate('Set the logging level'))
+ log_level:value("debug", "debug")
+ log_level:value("info", "info")
+ log_level:value("warn", "warn")
+ log_level:value("error", "error")
+ log_level:value("fatal", "fatal")
+ local hosts = section_dockerman:taboption("daemon", DynamicList, "daemon_hosts", translate("Server Host"), translate('Daemon unix socket (unix:///var/run/docker.sock) or TCP Remote Hosts (tcp://0.0.0.0:2375), default: unix:///var/run/docker.sock'))
+ hosts:value("unix:///var/run/docker.sock", "unix:///var/run/docker.sock")
+ hosts:value("tcp://0.0.0.0:2375", "tcp://0.0.0.0:2375")
+ hosts.rmempty = true
+end
+return map_dockerman
diff --git a/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua
new file mode 100644
index 0000000000..1685027203
--- /dev/null
+++ b/applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua
@@ -0,0 +1,116 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
+]]--
+
+require "luci.util"
+local uci = luci.model.uci.cursor()
+local docker = require "luci.model.docker"
+local dk = docker.new()
+
+local containers, volumes
+local res = dk.volumes:list()
+if res.code <300 then volumes = res.body.Volumes else return end
+res = dk.containers:list({query = {all=true}})
+if res.code <300 then containers = res.body else return end
+
+function get_volumes()
+ local data = {}
+ for i, v in ipairs(volumes) do
+ -- local index = v.CreatedAt .. v.Name
+ local index = v.Name
+ data[index]={}
+ data[index]["_selected"] = 0
+ data[index]["_nameraw"] = v.Name
+ data[index]["_name"] = v.Name:sub(1,12)
+ for ci,cv in ipairs(containers) do
+ if cv.Mounts and type(cv.Mounts) ~= "table" then break end
+ for vi, vv in ipairs(cv.Mounts) do
+ if v.Name == vv.Name then
+ data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
+ '<a href='..luci.dispatcher.build_url("admin/docker/container/"..cv.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. cv.Names[1]:sub(2)..'</a>'
+ end
+ end
+ end
+ data[index]["_driver"] = v.Driver
+ data[index]["_mountpoint"] = nil
+ for v1 in v.Mountpoint:gmatch('[^/]+') do
+ if v1 == index then
+ data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..."
+ else
+ data[index]["_mountpoint"] = (data[index]["_mountpoint"] and data[index]["_mountpoint"] or "").."/".. v1
+ end
+ end
+ data[index]["_created"] = v.CreatedAt
+ end
+ return data
+end
+
+local volume_list = get_volumes()
+
+-- m = Map("docker", translate("Docker"))
+m = SimpleForm("docker", translate("Docker"))
+m.submit=false
+m.reset=false
+
+
+volume_table = m:section(Table, volume_list, translate("Volumes"))
+
+volume_selecter = volume_table:option(Flag, "_selected","")
+volume_selecter.disabled = 0
+volume_selecter.enabled = 1
+volume_selecter.default = 0
+
+volume_id = volume_table:option(DummyValue, "_name", translate("Name"))
+volume_table:option(DummyValue, "_driver", translate("Driver"))
+volume_table:option(DummyValue, "_containers", translate("Containers")).rawhtml = true
+volume_table:option(DummyValue, "_mountpoint", translate("Mount Point"))
+volume_table:option(DummyValue, "_created", translate("Created"))
+volume_selecter.write = function(self, section, value)
+ volume_list[section]._selected = value
+end
+
+docker_status = m:section(SimpleSection)
+docker_status.template = "dockerman/apply_widget"
+docker_status.err=docker:read_status()
+docker_status.err=docker_status.err and docker_status.err:gsub("\n","<br>"):gsub(" ","&nbsp;")
+if docker_status.err then docker:clear_status() end
+
+action = m:section(Table,{{}})
+action.notitle=true
+action.rowcolors=false
+action.template="cbi/nullsection"
+btnremove = action:option(Button, "remove")
+btnremove.inputtitle= translate("Remove")
+btnremove.template = "dockerman/cbi/inlinebutton"
+btnremove.inputstyle = "remove"
+btnremove.forcewrite = true
+btnremove.write = function(self, section)
+ local volume_selected = {}
+ -- 遍历table中sectionid
+ local volume_table_sids = volume_table:cfgsections()
+ for _, volume_table_sid in ipairs(volume_table_sids) do
+ -- 得到选中项的名字
+ if volume_list[volume_table_sid]._selected == 1 then
+ -- volume_selected[#volume_selected+1] = volume_id:cfgvalue(volume_table_sid)
+ volume_selected[#volume_selected+1] = volume_table_sid
+ end
+ end
+ if next(volume_selected) ~= nil then
+ local success = true
+ docker:clear_status()
+ for _,vol in ipairs(volume_selected) do
+ docker:append_status("Volumes: " .. "remove" .. " " .. vol .. "...")
+ local msg = dk.volumes["remove"](dk, {id = vol})
+ if msg.code ~= 204 then
+ docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
+ success = false
+ else
+ docker:append_status("done\n")
+ end
+ end
+ if success then docker:clear_status() end
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/volumes"))
+ end
+end
+return m