luci-app-dockerman/luci-lib-docker: add packages

This commit is contained in:
CN_SZTL 2019-12-14 17:53:09 +08:00
parent 75432f3cfd
commit dca83141e6
No known key found for this signature in database
GPG Key ID: 6850B6345C862176
22 changed files with 2580 additions and 1 deletions

View File

@ -59,7 +59,9 @@ luci-app-koolproxyR source: [Ameykyl/luci-app-koolproxyR](https://github.com/Ame
openwrt-chinadns-ng source: [pexcn/openwrt-chinadns-ng](https://github.com/pexcn/openwrt-chinadns-ng).<br/>
luci-app-ssr-plus-jo source: [brokeld/luci-app-ssr-plus-jo](https://github.com/brokeld/luci-app-ssr-plus-jo).<br/>
openwrt-udpspeeder source: [pexcn/openwrt-udpspeeder](https://github.com/pexcn/openwrt-udpspeeder).<br/>
luci-app-onliner source: [rufengsuixing/luci-app-onliner](https://github.com/rufengsuixing/luci-app-onliner).
luci-app-onliner source: [rufengsuixing/luci-app-onliner](https://github.com/rufengsuixing/luci-app-onliner).<br/>
luci-app-dockerman source: [lisaac/luci-app-dockerman](https://github.com/lisaac/luci-app-dockerman).<br/>
luci-lib-docker source: [luci-lib-docker](https://github.com/lisaac/luci-lib-docker).
# License
### [GPL v3](https://www.gnu.org/licenses/gpl-3.0.html).

View File

@ -0,0 +1,43 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-dockerman
PKG_VERSION:=v0.1.0
PKG_RELEASE:=beta
PKG_MAINTAINER:=lisaac <https://github.com/lisaac/luci-app-dockerman>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
PKG_LICENSE:=Apache-2.0
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=Docker Manager interface for LuCI
PKGARCH:=all
DEPENDS:=+luci-lib-docker +luci-app-docker
endef
define Package/$(PKG_NAME)/description
Docker Manager interface for LuCI
endef
define Build/Prepare
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
rm -fr /tmp/luci-indexcache /tmp/luci-modulecache
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/lib/lua/luci
cp -pR ./luasrc/* $(1)/usr/lib/lua/luci
$(INSTALL_DIR) $(1)/
cp -pR ./root/* $(1)/
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

View File

@ -0,0 +1,172 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <lisaac.cn@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
]]--
require "luci.util"
local docker = require "luci.model.docker"
local uci = require "luci.model.uci"
module("luci.controller.dockerman",package.seeall)
function index()
entry({"admin", "docker"}, firstchild(), "Docker", 40).dependent = false
entry({"admin","docker","overview"},cbi("docker/overview"),_("Overview"),0).leaf=true
socket = luci.model.uci.cursor().get("docker","local", "socket_path")
if not nixio.fs.access(socket) then
return
end
if (require "luci.model.docker").new():version().code ~= 200 then return end
entry({"admin","docker","containers"},form("docker/containers"),_("Containers"),1).leaf=true
entry({"admin","docker","networks"},form("docker/networks"),_("Networks"),3).leaf=true
entry({"admin","docker","images"},form("docker/images"),_("Images"),2).leaf=true
entry({"admin","docker","events"},call("action_events"),_("Events"),4)
entry({"admin","docker","newcontainer"},form("docker/newcontainer")).leaf=true
entry({"admin","docker","newnetwork"},form("docker/newnetwork")).leaf=true
entry({"admin","docker","container"},form("docker/container")).leaf=true
entry({"admin","docker","container_stats"},call("action_get_container_stats")).leaf=true
entry({"admin","docker","confirm"},call("action_confirm")).leaf=true
-- for openwrt docker-ce by lean
if nixio.fs.access("/etc/config/dockerd") then
entry({"admin","services","docker"},alias("admin", "docker", "overview"))
entry({"admin","services","docker","status"},call("act_status")).leaf=true
end
end
function action_events()
local logs = ""
local dk = docker.new()
local query ={}
query["until"] = os.time()
local events = dk:events(nil, query)
for _, v in ipairs(events.body) do
if v.Type == "container" then
logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. (v.Action or "null") .. " Container ID:".. (v.Actor.ID or "null") .. " Container Name:" .. (v.Actor.Attributes.name or "null")
elseif v.Type == "network" then
logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. v.Action .. " Container ID:"..( v.Actor.Attributes.container or "null" ) .. " Network Name:" .. (v.Actor.Attributes.name or "null") .. " Network type:".. v.Actor.Attributes.type or ""
elseif v.Type == "image" then
logs = (logs ~= "" and (logs .. "\n") or logs) .. "[" .. os.date("%Y-%m-%d %H:%M:%S", v.time) .."] "..v.Type.. " " .. v.Action .. " Image:".. (v.Actor.ID or "null").. " Image Name:" .. (v.Actor.Attributes.name or "null")
end
end
luci.template.render("docker/logs", {self={syslog = logs, title="Docker Events"}})
end
local calculate_cpu_percent = function(d)
if type(d) ~= "table" then return end
cpu_count = tonumber(d["cpu_stats"]["online_cpus"])
cpu_percent = 0.0
cpu_delta = tonumber(d["cpu_stats"]["cpu_usage"]["total_usage"]) - tonumber(d["precpu_stats"]["cpu_usage"]["total_usage"])
system_delta = tonumber(d["cpu_stats"]["system_cpu_usage"]) - tonumber(d["precpu_stats"]["system_cpu_usage"])
if system_delta > 0.0 then
cpu_percent = string.format("%.2f", cpu_delta / system_delta * 100.0 * cpu_count)
end
-- return cpu_percent .. "%"
return cpu_percent
end
local get_memory = function(d)
if type(d) ~= "table" then return end
-- local limit = string.format("%.2f", tonumber(d["memory_stats"]["limit"]) / 1024 / 1024)
-- local usage = string.format("%.2f", (tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"])) / 1024 / 1024)
-- return usage .. "MB / " .. limit.. "MB"
local limit =tonumber(d["memory_stats"]["limit"])
local usage = tonumber(d["memory_stats"]["usage"]) - tonumber(d["memory_stats"]["stats"]["total_cache"])
return usage, limit
end
local get_rx_tx = function(d)
if type(d) ~="table" then return end
-- local data
-- if type(d["networks"]) == "table" then
-- for e, v in pairs(d["networks"]) do
-- data = (data and (data .. "<br>") or "") .. e .. " Total Tx:" .. string.format("%.2f",(tonumber(v.tx_bytes)/1024/1024)) .. "MB Total Rx: ".. string.format("%.2f",(tonumber(v.rx_bytes)/1024/1024)) .. "MB"
-- end
-- end
local data = {}
if type(d["networks"]) == "table" then
for e, v in pairs(d["networks"]) do
data[e] = {
bw_tx = tonumber(v.tx_bytes),
bw_rx = tonumber(v.rx_bytes)
}
end
end
return data
end
function action_get_container_stats(container_id)
if container_id then
local dk = docker.new()
local response = dk.containers:inspect(container_id)
if response.code == 200 and response.body.State.Running then
response = dk.containers:stats(container_id, {stream=false})
if response.code == 200 then
local container_stats = response.body
local cpu_percent = calculate_cpu_percent(container_stats)
local mem_useage, mem_limit = get_memory(container_stats)
local bw_rxtx = get_rx_tx(container_stats)
luci.http.status(response.code, response.body.message)
luci.http.prepare_content("application/json")
luci.http.write_json({
cpu_percent = cpu_percent,
memory = {
mem_useage = mem_useage,
mem_limit = mem_limit
},
bw_rxtx = bw_rxtx
})
else
luci.http.status(response.code, response.body.message)
luci.http.prepare_content("text/plain")
luci.http.write(response.body.message)
end
else
if response.code == 200 then
luci.http.status(500, "container "..container_id.." not running")
luci.http.prepare_content("text/plain")
luci.http.write("Container "..container_id.." not running")
else
luci.http.status(response.code, response.body.message)
luci.http.prepare_content("text/plain")
luci.http.write(response.body.message)
end
end
else
luci.http.status(404, "No container name or id")
luci.http.prepare_content("text/plain")
luci.http.write("No container name or id")
end
end
function action_confirm()
local status_path=luci.model.uci.cursor().get("docker","local", "status_path")
local data = nixio.fs.readfile(status_path)
if data then
code = 202
msg = data
else
code = 200
msg = "finish"
data = "finish"
end
-- luci.util.perror(data)
luci.http.status(code, msg)
luci.http.prepare_content("application/json")
luci.http.write_json({info = data})
end
function act_status()
local e={}
e.running=luci.sys.call("pgrep /usr/bin/dockerd >/dev/null")==0
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end

View File

@ -0,0 +1,437 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <lisaac.cn@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
]]--
require "luci.util"
local uci = luci.model.uci.cursor()
local docker = require "luci.model.docker"
local dk = docker.new()
container_id = arg[1]
local action = arg[2] or "info"
local images, networks, containers_info
if not container_id then return end
local res = dk.containers:inspect(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.NetworkSettings and d.NetworkSettings.Ports then
for inter, out in pairs(d.NetworkSettings.Ports) 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
data = (data and (data .. "<br>") or "") .. v["Source"] .. ":" .. v["Destination"] .. (v["Mode"] ~= "" and (":" .. v["Mode"]) 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_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 = dk.containers[cmd](dk, container_id)
if res and res.code >= 300 then
docker:append_status("fail code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
else
docker:clear_status()
end
if cmd ~= "remove" 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
m=SimpleForm("docker", container_info.Name:sub(2), translate("Docker Contaienr") )
m.tempalte = "cbi/xsimpleform"
m.redirect = luci.dispatcher.build_url("admin/docker/containers")
-- m:append(Template("docker/container"))
docker_status = m:section(SimpleSection)
docker_status.template="docker/apply_widget"
docker_status.err=nixio.fs.readfile(dk.options.status_path)
-- luci.util.perror(docker_status.err)
if docker_status 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="cbi/inlinebutton"
btnstart.inputtitle=translate("Start")
btnstart.inputstyle = "apply"
btnstart.forcewrite = true
btnrestart=action_section:option(Button, "_restart")
btnrestart.template="cbi/inlinebutton"
btnrestart.inputtitle=translate("Restart")
btnrestart.inputstyle = "reload"
btnrestart.forcewrite = true
btnstop=action_section:option(Button, "_stop")
btnstop.template="cbi/inlinebutton"
btnstop.inputtitle=translate("Stop")
btnstop.inputstyle = "reset"
btnstop.forcewrite = true
btnremove=action_section:option(Button, "_remove")
btnremove.template="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
btnremove.write = function(self, section)
start_stop_remove(m,"remove")
end
btnstop.write = function(self, section)
start_stop_remove(m,"stop")
end
tab_section = m:section(SimpleSection)
tab_section.template="docker/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["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 "-"}
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
self:value(k,v)
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"
Button.render(self, section, scope)
else
self.template = "cbi/dummyvalue"
self.default=""
DummyValue.render(self, section, scope)
end
end
btn_update.write = function(self, section, value)
-- luci.util.perror(section)
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(container_id,{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(container_id, nil, {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(leave_network, nil, {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
-- luci.util.perror(table_info[section]._opts)
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(connect_network, nil, {Container = container_id, EndpointConfig= network_opiton})
end
if res and res.code > 300 then
docker:append_status("fail 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 == "edit" then
editsection= m:section(SimpleSection)
d = editsection: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 = editsection: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 = editsection: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 = editsection: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:clear_status()
docker:append_status("Containers: update " .. container_id .. "...")
local res = dk.containers:update(container_id, nil, request_body)
if res and res.code >= 300 then
docker:append_status("fail 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.."/edit"))
end
end
elseif action == "logs" then
logsection= m:section(SimpleSection)
local logs = ""
local query ={
stdout = 1,
stderr = 1,
tail = 1000
}
local logs = dk.containers:logs(container_id, 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="docker/logs"
m.submit = false
m.reset = false
elseif action == "stats" then
local response = dk.containers:top(container_id, {ps_args="-aux"})
local container_top
if response.code == 200 then
container_top=response.body
else
response = dk.containers:top(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="docker/stats"
table_stats = {cpu={key="CPU Useage",value='-'},memory={key="Memory Useage",value='-'}}
stat_section = m:section(Table, table_stats, translate("Stats"))
stat_section:option(DummyValue, "key", translate("Stats")).width="33%"
stat_section:option(DummyValue, "value")
top_section= m:section(Table, container_top.Processes, translate("TOP"))
for i, v in ipairs(container_top.Titles) do
top_section:option(DummyValue, i, translate(v))
end
end
m.submit = false
m.reset = false
end
return m

View File

@ -0,0 +1,205 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <lisaac.cn@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
]]--
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(nil, {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]["_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 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.PublicPort .. ":") or "") .. (v2.PrivatePort and (v2.PrivatePort .."/") or "") .. (v2.Type and v2.Type 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.tempalte = "cbi/xsimpleform"
m.submit=false
m.reset=false
docker_status = m:section(SimpleSection)
docker_status.template="docker/apply_widget"
docker_status.err=nixio.fs.readfile(dk.options.status_path)
-- luci.util.perror(docker_status.err)
if docker_status 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("Name"))
container_name.width="20%"
container_name.template="cbi/dummyvalue"
container_name.href = function (self, section)
return luci.dispatcher.build_url("admin/docker/container/" .. urlencode(container_id:cfgvalue(section)))
end
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_image = c_table:option(DummyValue, "_image", translate("Image"))
container_image.template="cbi/dummyvalue"
container_image.width="10%"
-- container_image.href = function (self, section)
-- return luci.dispatcher.build_url("admin/docker/image/" .. urlencode(c_lists[section]._image_id))
-- end
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)
-- luci.template.render("admin_uci/apply", {
-- changes = next(changes) and changes,
-- configs = reload
-- })
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] = container_name:cfgvalue(c_table_sid)
end
end
if #c_selected >0 then
-- luci.util.perror(dk.options.status_path)
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, cont)
if res and res.code >= 300 then
success = false
docker:append_status("fail code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "<br>")
else
docker:append_status("done<br>")
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="cbi/inlinebutton"
btnnew.inputstyle = "add"
btnnew.forcewrite = true
btnstart=action_section:option(Button, "_start")
btnstart.template="cbi/inlinebutton"
btnstart.inputtitle=translate("Start")
btnstart.inputstyle = "apply"
btnstart.forcewrite = true
btnrestart=action_section:option(Button, "_restart")
btnrestart.template="cbi/inlinebutton"
btnrestart.inputtitle=translate("Restart")
btnrestart.inputstyle = "reload"
btnrestart.forcewrite = true
btnstop=action_section:option(Button, "_stop")
btnstop.template="cbi/inlinebutton"
btnstop.inputtitle=translate("Stop")
btnstop.inputstyle = "reset"
btnstop.forcewrite = true
btnremove=action_section:option(Button, "_remove")
btnremove.template="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
return m

View File

@ -0,0 +1,157 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <lisaac.cn@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
]]--
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(nil, {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,20)
if v.RepoTags then
data[index]["_tags"] = v.RepoTags[1]
else
_,_, data[index]["_tags"] = v.RepoDigests[1]:find("^(.-)@.+")
data[index]["_tags"]=data[index]["_tags"]..":none"
end
for ci,cv in ipairs(containers) do
if v.Id == cv.ImageID then
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..cv.Names[1]:sub(2)
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.tempalte = "cbi/xsimpleform"
m.submit=false
m.reset=false
local pull_value={{_image_tag_name="", _registry="index.docker.io"}}
local pull_section = m:section(Table,pull_value, "Pull Image")
pull_section.template="cbi/nullsection"
local tag_name = pull_section:option(Value, "_image_tag_name")
tag_name.template="cbi/inlinevalue"
tag_name.placeholder="lisaac/luci:latest"
local registry = pull_section:option(Value, "_registry")
registry.template="cbi/inlinevalue"
registry:value("index.docker.io", "DockerHub")
local action_pull = pull_section:option(Button, "_pull")
action_pull.inputtitle= translate("Pull")
action_pull.template="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[section]["_image_tag_name"] = value
end
registry.write = function(self, section,value)
pull_value[section]["_registry"] = value
end
action_pull.write = function(self, section)
local tag = pull_value[section]["_image_tag_name"]
local server = pull_value[section]["_registry"]
--去掉协议前缀和后缀
local _,_,tmp = server:find(".-://([%.%w%-%_]+)")
if not tmp then
_,_,server = server:find("([%.%w%-%_]+)")
end
local json_stringify = luci.json and luci.json.encode or luci.jsonc.stringify
if tag then
docker:clear_status()
docker:append_status("Images: " .. "pulling" .. " " .. tag .. "...")
local x_auth = nixio.bin.b64encode(json_stringify({serveraddress= server}))
local res = dk.images:create(nil, {fromImage=tag,_header={["X-Registry-Auth"]=x_auth}})
if res and res.code >=300 then
docker:append_status("fail code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "<br>")
else
docker:append_status("done<br>")
end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
end
end
image_table = m:section(Table, image_list, translate("Images"))
image_selecter = image_table:option(Flag, "_selected","")
image_selecter.disabled = 0
image_selecter.enabled = 1
image_selecter.default = 0
image_id = image_table:option(DummyValue, "_id", translate("ID"))
image_table:option(DummyValue, "_tags", translate("RepoTags"))
image_table:option(DummyValue, "_containers", translate("Containers"))
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
docker_status = m:section(SimpleSection)
docker_status.template="docker/apply_widget"
docker_status.err=nixio.fs.readfile(dk.options.status_path)
if docker_status 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="cbi/inlinebutton"
btnremove.inputstyle = "remove"
btnremove.forcewrite = true
btnremove.write = function(self, section)
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_id:cfgvalue(image_table_sid)
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 msg = dk.images["remove"](dk, img)
if msg.code ~= 200 then
docker:append_status("fail code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "<br>")
success = false
else
docker:append_status("done<br>")
end
end
if success then docker:clear_status() end
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
end
end
return m

View File

@ -0,0 +1,125 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <lisaac.cn@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
]]--
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.tempalte = "cbi/xsimpleform"
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_id = network_table:option(DummyValue, "_id", translate("ID"))
network_selecter.disabled = 0
network_selecter.enabled = 1
network_selecter.default = 0
for k, v in pairs(network_list) do
if v["_name"] ~= "bridge" and v["_name"] ~= "none" and v["_name"] ~= "host" then
network_selecter:depends("_name", v["_name"])
end
end
network_name = network_table:option(DummyValue, "_name", translate("Name"))
network_driver = network_table:option(DummyValue, "_driver", translate("Driver"))
network_interface = network_table:option(DummyValue, "_interface", translate("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="docker/apply_widget"
docker_status.err=nixio.fs.readfile(dk.options.status_path)
if docker_status 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="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="cbi/inlinebutton"
btnremove.inputstyle = "remove"
btnremove.forcewrite = true
btnremove.write = function(self, section)
local network_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_name:cfgvalue(network_table_sid)
end
end
if next(network_selected) ~= nil then
local success = true
docker:clear_status()
for _,net in ipairs(network_selected) do
docker:append_status("Networks: " .. "remove" .. " " .. net .. "...")
local res = dk.networks["remove"](dk, net)
if res and res.code >= 300 then
docker:append_status("fail code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "<br>")
success = false
else
docker:append_status("done<br>")
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,281 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <lisaac.cn@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
]]--
require "luci.util"
local uci = luci.model.uci.cursor()
local docker = require "luci.model.docker"
local dk = docker.new()
local container_name = arg[1] or ""
local image_name = arg[2] or ""
local images = dk.images:list().body
local networks = dk.networks:list().body
local containers = dk.containers:list(nil, {all=true}).body
local m = SimpleForm("docker", translate("Docker"))
m.tempalte = "cbi/xsimpleform"
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="docker/apply_widget"
docker_status.err=nixio.fs.readfile(dk.options.status_path)
if docker_status then docker:clear_status() end
local s = m:section(SimpleSection, translate("New Container"))
s.addremove = true
s.anonymous = true
local d = s:option(Value, "name", translate("Container Name"))
d.rmempty = true
d.default = container_name
d = s:option(Value, "image", translate("Docker Image"))
d.rmempty = true
d.default = image_name
for _, v in ipairs (images) do
if v.RepoTags then
d:value(v.RepoTags[1], v.RepoTags[1])
end
end
d = s:option(Flag, "privileged", translate("Privileged"))
d.rmempty = true
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 = "unless-stopped"
d = s:option(ListValue, "network", translate("Networks"))
d.rmempty = true
d.default = "bridge"
local dip = s:option(Value, "ip", translate("IPv4 Address"))
dip.datatype="ip4addr"
dip:depends("network", "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:value(v.Name, network_name)
if v.Name ~= "none" and v.Name ~= "bridge" and v.Name ~= "host" then
dip:depends("network", v.Name)
end
end
end
d = s:option(DynamicList, "links", translate("Links with other containers"))
d.placeholder = "container_name:alias"
d.rmempty = true
d:depends("network", "bridge")
d = s:option(DynamicList, "env", translate("Environmental Variable"))
d.placeholder = "TZ=Asia/Shanghai"
d.rmempty = true
d = s:option(DynamicList, "mount", translate("Bind Mount"))
d.placeholder = "/media:/media:slave"
d.rmempty = true
d = s:option(DynamicList, "port", translate("Exposed Ports"))
d.placeholder = "2200:22/tcp"
d.rmempty = true
d = s:option(Value, "command", translate("Run command"))
d.placeholder = "/bin/sh init.sh"
d.rmempty = true
d = s:option(Flag, "advance", translate("Advance"))
d.rmempty = true
d.disabled = 0
d.enabled = 1
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 = 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."))
d.placeholder = "1024"
d.rmempty = true
d:depends("advance", 1)
d.datatype="uinteger"
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 = s: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:depends("advance", 1)
d.datatype="uinteger"
d = s:option(DynamicList, "tmpfs", translate("Tmpfs"), translate("Mount tmpfs filesystems"))
d.placeholder = "/run:rw,noexec,nosuid,size=65536k"
d.rmempty = true
d:depends("advance", 1)
m.handle = function(self, state, data)
if state == FORM_VALID then
local tmp
local name = data.name
local image = data.image
if not image:match(".-:.+") then
image = image .. ":latest"
end
local privileged = data.privileged
local restart = data.restart
local env = data.env
local network = data.network
local ip = (network ~= "bridge" and network ~= "host" and network ~= "none") and data.ip or nil
local mount = data.mount
local memory = data.memory or 0
local cpushares = data.cpushares or 0
local cpus = data.cpus or 0
local blkioweight = data.blkioweight 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,v1 = v:find("(.-):(.+)")
if k and v1 then
tmpfs[k]=v1
end
end
end
tmp = data.port
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 links = data.links
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
local create_body={
Hostname = name,
Domainname = "",
Cmd = (#command ~= 0) and command or nil,
Env = env,
Image = image,
Volumes = nil,
ExposedPorts = (next(exposedports) ~= nil) and exposedports or nil,
HostConfig = {
Binds = (#mount ~= 0) and mount or nil,
NetworkMode = network,
RestartPolicy ={
Name = restart,
MaximumRetryCount = 0
},
Privileged = privileged and true or false,
PortBindings = (next(portbindings) ~= nil) and portbindings or nil,
Memory = memory,
CpuShares = tonumber(cpushares),
NanoCPUs = tonumber(cpus) * 10 ^ 9,
BlkioWeight = tonumber(blkioweight)
},
NetworkingConfig = ip and {
EndpointsConfig = {
[network] = {
IPAMConfig = {
IPv4Address = ip
}
}
}
} or nil
}
if next(tmpfs) ~= nil then
create_body["HostConfig"]["Tmpfs"] = tmpfs
end
if network == "bridge" and next(links) ~= nil then
create_body["HostConfig"]["Links"] = links
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
local server = "index.docker.io"
local json_stringify = luci.json and luci.json.encode or luci.jsonc.stringify
docker:append_status("Images: " .. "pulling" .. " " .. image .. "...")
local x_auth = nixio.bin.b64encode(json_stringify({serveraddress= server}))
local res = dk.images:create(nil, {fromImage=image,_header={["X-Registry-Auth"]=x_auth}})
if res and res.code < 300 then
docker:append_status("done<br>")
else
docker:append_status("fail code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "<br>")
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
end
end
end
docker:append_status("Container: " .. "create" .. " " .. name .. "...")
local res = dk.containers:create(name, nil, 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("fail 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
end
return m

View File

@ -0,0 +1,202 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <lisaac.cn@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
]]--
require "luci.util"
local uci = luci.model.uci.cursor()
local docker = require "luci.model.docker"
local dk = docker.new()
m = SimpleForm("docker", translate("Docker"))
m.tempalte = "cbi/xsimpleform"
m.redirect = luci.dispatcher.build_url("admin", "docker", "networks")
docker_status = m:section(SimpleSection)
docker_status.template="docker/apply_widget"
docker_status.err=nixio.fs.readfile(dk.options.status_path)
if docker_status 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")
d.placeholder="eth0"
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.disabled = 0
d.enabled = 1
d.default = 0
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 or next(aux_address)~=nil 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["IPAM"]["Options"] = {
macvlan_mode = data.macvlan_mode,
parent = data.parent
}
elseif driver == "ipvlan" then
create_body["IPAM"]["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"] = options
end
docker:append_status("Network: " .. "create" .. " " .. create_body.Name .. "...")
local res = dk.networks:create(nil, nil, create_body)
if res and res.code == 201 then
docker:clear_status()
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
else
docker:append_status("fail code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "<br>")
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
end
end
end
return m

View File

@ -0,0 +1,48 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <lisaac.cn@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
]]--
if nixio.fs.access("/etc/config/dockerd") then
local running = (luci.sys.call("pidof portainer >/dev/null") == 0)
local button = ""
if running then
button = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br /><br /><input type=\"button\" value=\" " .. translate("Open Portainer Docker Admin") .. " \" onclick=\"window.open('http://'+window.location.hostname+':" .. 9999 .. "')\"/><br />"
end
m = Map("dockerd", "Docker CE", translate("Docker is a set of platform-as-a-service (PaaS) products that use OS-level virtualization to deliver software in packages called containers.") .. button)
m:section(SimpleSection).template = "docker/docker_status"
s = m:section(TypedSection, "docker")
s.anonymous = true
wan_mode = s:option(Flag, "wan_mode", translate("Enable WAN access Dokcer"), translate("Enable WAN access docker mapped ports"))
wan_mode.default = 0
wan_mode.rmempty = false
o=s:option(DummyValue,"readme",translate(" "))
o.description=translate("<a href=\"../../../../DockerReadme.pdf\" target=\"_blank\" />"..translate("Download DockerReadme.pdf").."</a>")
end
m2 = Map("docker", translate("Docker"))
s = m2:section(NamedSection, "local", "section", translate("Setting"))
socket_path = s:option(Value, "socket_path", translate("Socket Path"))
status_path = s:option(Value, "status_path", translate("Action Status Tempfile Path"), translate("Where you want to save the docker status file"))
debug = s:option(Flag, "debug", translate("Enable Debug"), translate("For debug, It shows all docker API actions of luci-app-docker in Debug Tempfile Path"))
debug.enabled="true"
debug.disabled="false"
debug_path = s:option(Value, "debug_path", translate("Debug Tempfile Path"), translate("Where you want to save the debug tempfile"))
if m then
return m, m2
else
return m2
end

View File

@ -0,0 +1,40 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <lisaac.cn@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
]]--
require "luci.util"
local docker = require "luci.docker"
local uci = (require "luci.model.uci").cursor()
local _docker = {}
_docker.new = function(option)
local option = option or {}
options = {
socket_path = option.socket_path or uci.get("docker","local", "socket_path"),
debug = option.debug or uci.get("docker","local", "debug") == 'true' and true or false,
debug_path = option.debug_path or uci.get("docker","local", "debug_path")
}
local _new = docker.new(options)
_new.options.status_path = uci.get("docker","local", "status_path")
return _new
end
_docker.options={}
_docker.options.status_path = uci.get("docker","local", "status_path")
_docker.append_status=function(self,val)
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.clear_status=function(self)
nixio.fs.remove(self.options.status_path)
end
return _docker

View File

@ -0,0 +1,13 @@
<%+cbi/valueheader%>
<% if self.href then %><a href="<%=self:href(section)%>"><% end -%>
<%
local val = self:cfgvalue(section) or self.default or ""
if not self.rawhtml then
write(pcdata(val))
else
write(val)
end
%>
<%- if self.href then %></a><%end%>
<input type="hidden" id="<%=cbid%>" value="<%=pcdata(self:cfgvalue(section) or self.default or "")%>" />
<%+cbi/valuefooter%>

View File

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

View File

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

View File

@ -0,0 +1,88 @@
<%
if not self.embedded then
%><form method="post" enctype="multipart/form-data" action="<%=REQUEST_URI%>"<%=
attr("data-strings", luci.util.serialize_json({
label = {
choose = translate('-- Please choose --'),
custom = translate('-- custom --'),
},
path = {
resource = resource,
browser = url("admin/filebrowser")
}
}))
%>>
<input type="hidden" name="token" value="<%=token%>" />
<input type="hidden" name="cbi.submit" value="1" /><%
end
%><div class="cbi-map" id="cbi-<%=self.config%>"><%
if self.title and #self.title > 0 then
%><h2 name="content"><%=self.title%></h2><%
end
if self.description and #self.description > 0 then
%><div class="cbi-map-descr"><%=self.description%></div><%
end
self:render_children()
%></div><%
if self.message then
%><div class="alert-message notice"><%=self.message%></div><%
end
if self.errmessage then
%><div class="alert-message warning"><%=self.errmessage%></div><%
end
if not self.embedded then
if type(self.hidden) == "table" then
local k, v
for k, v in pairs(self.hidden) do
%><input type="hidden" id="<%=k%>" name="<%=k%>" value="<%=pcdata(v)%>" /><%
end
end
local display_back = (self.redirect)
local display_cancel = (self.cancel ~= false and self.on_cancel)
local display_skip = (self.flow and self.flow.skip)
local display_submit = (self.submit ~= false)
local display_reset = (self.reset ~= false)
if display_back or display_cancel or display_skip or display_submit or display_reset then
%><div class="cbi-page-actions"><%
if display_back then
%><input class="cbi-button cbi-button-link" type="button" value="<%:Back to Overview%>" onclick="location.href='<%=pcdata(self.redirect)%>'" /> <%
end
if display_cancel then
local label = pcdata(self.cancel or translate("Cancel"))
%><input class="cbi-button cbi-button-link" type="button" value="<%=label%>" onclick="cbi_submit(this, 'cbi.cancel')" /> <%
end
if display_skip then
%><input class="cbi-button cbi-button-neutral" type="button" value="<%:Skip%>" onclick="cbi_submit(this, 'cbi.skip')" /> <%
end
if display_submit then
local label = pcdata(self.submit or translate("Submit"))
%><input class="cbi-button cbi-button-save" type="submit" value="<%=label%>" /> <%
end
if display_reset then
local label = pcdata(self.reset or translate("Reset"))
%><input class="cbi-button cbi-button-reset" type="reset" value="<%=label%>" /> <%
end
%></div><%
end
%></form><%
end
%>
<script type="text/javascript">cbi_init();</script>

View File

@ -0,0 +1,144 @@
<style type="text/css">
#docker_apply_overlay {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
display: none;
z-index: 20000;
}
#docker_apply_overlay .alert-message {
position: relative;
top: 10%;
width: 60%;
margin: auto;
display: flex;
flex-wrap: wrap;
min-height: 32px;
align-items: center;
}
#docker_apply_overlay .alert-message > h4,
#docker_apply_overlay .alert-message > p,
#docker_apply_overlay .alert-message > div {
flex-basis: 100%;
}
#docker_apply_overlay .alert-message > img {
margin-right: 1em;
flex-basis: 32px;
}
body.apply-overlay-active {
overflow: hidden;
height: 100vh;
}
body.apply-overlay-active #docker_apply_overlay {
display: block;
}
</style>
<script type="text/javascript">//<![CDATA[
var xhr = new XHR(),
uci_apply_rollback = <%=math.max(luci.config and luci.config.apply and luci.config.apply.rollback or 30, 30)%>,
uci_apply_holdoff = <%=math.max(luci.config and luci.config.apply and luci.config.apply.holdoff or 4, 1)%>,
uci_apply_timeout = <%=math.max(luci.config and luci.config.apply and luci.config.apply.timeout or 5, 1)%>,
uci_apply_display = <%=math.max(luci.config and luci.config.apply and luci.config.apply.display or 1.5, 1)%>,
was_xhr_poll_running = false;
function docker_status_message(type, content) {
document.getElementById('docker_apply_overlay') || document.body.insertAdjacentHTML("beforeend",'<div id="docker_apply_overlay"><div class="alert-message"></div></div>')
var overlay = document.getElementById('docker_apply_overlay')
message = overlay.querySelector('.alert-message');
if (message && type) {
if (!message.classList.contains(type)) {
message.classList.remove('notice');
message.classList.remove('warning');
message.classList.add(type);
}
if (content)
message.innerHTML = content;
document.body.classList.add('apply-overlay-active');
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="Loadding.."
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.%>');
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" /> ' +
loading_msg);
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', "<%=self.err%>");
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{
docker_status_message()
})
<%- end %>
window.onload= function (){
var buttons = document.querySelectorAll('input[type="submit"]');
[].slice.call(buttons).forEach(function (el) {
el.onclick = fnSubmitForm.bind(this, el);
});
}
//]]></script>

View File

@ -0,0 +1,24 @@
<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_edit"><a id="a-cbi-tab-container_edit" href=""><%:Edit%></a></li>
<li id="cbi-tab-container_stats"><a id="a-cbi-tab-container_stats" href=""><%:Stats%></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","edit","stats","logs"]
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-disable"
}
})
</script>

View File

@ -0,0 +1,24 @@
<%#
LuCI - Lua Configuration Interface
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
-%>
<% if self.title == translate("Docker Events") then %>
<%+header%>
<% end %>
<h2><a id="content" name="content"><%=self.title%></a></h2>
<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 == translate("Docker Events") then %>
<%+footer%>
<% end %>

View File

@ -0,0 +1,62 @@
<script type="text/javascript">//<![CDATA[
function progressbar(v, m, pc, np)
{
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 / %s '+ (np == true ? "" :'(%d%%)')+'</small>' +
'</div>' +
'</div>' +
'</div>', pc, v, m, pc
);
}
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(5, '/cgi-bin/luci/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))
);
let num = 1
for (var key in info.bw_rxtx){
if (! document.getElementById("cbi-table-network_"+key+"-value")){
let tab = document.getElementById("cbi-table-cpu").parentNode
num ++
let div=document.getElementById('cbi-table-cpu').cloneNode(true);
div.id = "cbi-table-network_"+key;
div.children[0].innerHTML = "<%:Network TX/RX%>: " +key
div.children[1].id="cbi-table-network_"+key+"-value"
tab.appendChild(div)
}
e=document.getElementById("cbi-table-network_"+key+"-value")
e.innerHTML = progressbar(
niceBytes(info.bw_rxtx[key].bw_tx),
niceBytes(info.bw_rxtx[key].bw_rx),
((100 / (info.bw_rxtx[key].bw_rx?info.bw_rxtx[key].bw_rx:100)) * (info.bw_rxtx[key].bw_tx?info.bw_rxtx[key].bw_tx:0))>100 ? 100 :Math.floor((100 / (info.bw_rxtx[key].bw_rx?info.bw_rxtx[key].bw_rx:100)) * (info.bw_rxtx[key].bw_tx?info.bw_rxtx[key].bw_tx:0)) ,
true
);
}
});
//]]></script>

View File

@ -0,0 +1,5 @@
config section 'local'
option socket_path '/var/run/docker.sock'
option status_path '/tmp/.docker_action_status'
option debug_path '/tmp/.docker_debug'
option debug 'false'

View File

@ -0,0 +1,46 @@
#
# Copyright (C) 2019 lisaac <lisaac.cn@gmail.com>
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-lib-docker
PKG_VERSION:=v0.0.7-beta
PKG_RELEASE:=beta
PKG_MAINTAINER:=lisaac <https://github.com/lisaac/luci-lib-docker>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
PKG_LICENSE:=Apache-2.0
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=6. Libraries
TITLE:=Docker Engine API for LuCI
PKGARCH:=all
DEPENDS:=+luci-lib-json
endef
define Package/$(PKG_NAME)/description
Docker Engine API for LuCI
endef
define Build/Prepare
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
rm -fr /tmp/luci-indexcache /tmp/luci-modulecache
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/lib/lua/luci
cp -pR ./luasrc/* $(1)/usr/lib/lua/luci
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

View File

@ -0,0 +1,421 @@
require "nixio.util"
require "luci.util"
local json = require "luci.json"
local nixio = require "nixio"
local ltn12 = require "luci.ltn12"
local fs = require "nixio.fs"
local urlencode = luci.http and (luci.http.protocol and luci.http.protocol.urlencode or luci.util.urlencode)
local json_stringify = json.encode --luci.json and luci.json.encode or luci.jsonc.stringify
local json_parse = json.decode --luci.json and luci.json.decode or luci.jsonc.parse
local chunksource = function(sock, buffer)
buffer = buffer or ""
return function()
local output
local _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
if not count then -- lua ^ only match start of stirngnot start of line
_, endp, count = buffer:find("\r\n([0-9a-fA-F]+);?.-\r\n")
end
while not count do
local newblock, code = sock:recv(1024)
if not newblock then
return nil, code
end
buffer = buffer .. newblock
_, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
if not count then
_, endp, count = buffer:find("\r\n([0-9a-fA-F]+);?.-\r\n")
end
end
count = tonumber(count, 16)
if not count then
return nil, -1, "invalid encoding"
elseif count == 0 then -- finial
return nil
elseif count + 2 <= #buffer - endp then
output = buffer:sub(endp + 1, endp + count)
buffer = buffer:sub(endp + count + 3) -- don't forget handle buffer
return output
else
output = buffer:sub(endp + 1, endp + count)
buffer = ""
if count > #output then
local remain, code = sock:recvall(count - #output) --need read remaining
if not remain then
return nil, code
end
output = output .. remain
count, code = sock:recvall(2) --read \r\n
else
count, code = sock:recvall(count + 2 - #buffer + endp)
end
if not count then
return nil, code
end
return output
end
end
end
local docker_stream_filter = function(buffer)
buffer = buffer or ""
if #buffer < 8 then
return ""
end
local stream_type = ((string.byte(buffer, 1) == 1) and "stdout") or ((string.byte(buffer, 1) == 2) and "stderr") or ((string.byte(buffer, 1) == 0) and "stdin") or "stream_err"
local valid_length =
tonumber(string.byte(buffer, 5)) * 256 * 256 * 256 + tonumber(string.byte(buffer, 6)) * 256 * 256 + tonumber(string.byte(buffer, 7)) * 256 + tonumber(string.byte(buffer, 8))
if valid_length > #buffer + 8 then
return ""
end
return stream_type .. ": " .. string.sub(buffer, 9, valid_length + 8)
-- return string.sub(buffer, 9, valid_length + 8)
end
local gen_http_req = function(options)
local req
options = options or {}
options.protocol = options.protocol or "HTTP/1.1"
req = (options.method or "GET") .. " " .. options.path .. " " .. options.protocol .. "\r\n"
req = req .. "Host: " .. options.host .. "\r\n"
req = req .. "User-Agent: " .. options.user_agent .. "\r\n"
req = req .. "Connection: close\r\n"
if type(options.header) == "table" then
for k, v in pairs(options.header) do
req = req .. k .. ": " .. v .."\r\n"
end
end
if options.method == "POST" and type(options.conetnt) == "table" then
local conetnt_json = json_stringify(options.conetnt)
req = req .. "Content-Type: application/json\r\n"
req = req .. "Content-Length: " .. #conetnt_json .. "\r\n"
req = req .. "\r\n" .. conetnt_json
elseif options.method == "PUT" and options.conetnt then
req = req .. "Content-Type: application/x-tar\r\n"
req = req .. "Content-Length: " .. #options.conetnt .. "\r\n"
req = req .. "\r\n" .. options.conetnt
else
req = req .. "\r\n"
end
if options.debug then io.popen("echo '".. req .. "' >> " .. options.debug_path) end
return req
end
local send_http_socket = function(socket_path, req)
local docker_socket = nixio.socket("unix", "stream")
if docker_socket:connect(socket_path) ~= true then
return {
headers={
code=497,
message="bad socket path",
protocol="HTTP/1.1"
},
body={
message="can\'t connect to unix socket"
}
}
end
if docker_socket:send(req) == 0 then
return {
headers={
code=498,
message="bad socket path",
protocol="HTTP/1.1"
},
body={
message="can\'t send data to unix socket"
}
}
end
-- local data, err_code, err_msg, data_f = docker_socket:readall()
-- docker_socket:close()
local linesrc = docker_socket:linesource()
-- read socket using source http://w3.impa.br/~diego/software/luasocket/ltn12.html
--http://lua-users.org/wiki/FiltersSourcesAndSinks
-- handle response header
local line = linesrc()
if not line then
docker_socket:close()
return {
headers={
code=499,
message="bad socket path",
protocol="HTTP/1.1"
},
body={
message="no data receive from socket"
}
}
end
local response = {code = 0, headers = {}, body = {}}
local p, code, msg = line:match("^([%w./]+) ([0-9]+) (.*)")
response.protocol = p
response.code = tonumber(code)
response.message = msg
line = linesrc()
while line and line ~= "" do
local key, val = line:match("^([%w-]+)%s?:%s?(.*)")
if key and key ~= "Status" then
if type(response.headers[key]) == "string" then
response.headers[key] = {response.headers[key], val}
elseif type(response.headers[key]) == "table" then
response.headers[key][#response.headers[key] + 1] = val
else
response.headers[key] = val
end
end
line = linesrc()
end
-- handle response body
local body_buffer = linesrc(true)
response.body = {}
if response.headers["Transfer-Encoding"] == "chunked" then
local source = chunksource(docker_socket, body_buffer)
code = ltn12.pump.all(source, (ltn12.sink.table(response.body))) and response.code or 555
response.code = code
else
local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource())
code = ltn12.pump.all(body_source, (ltn12.sink.table(response.body))) and response.code or 555
response.code = code
end
docker_socket:close()
return response
end
local send_http_require = function(options, method, api_group, api_action, name_or_id, request_qurey, request_body)
local qurey
local req_options = setmetatable({}, {__index = options})
-- for docker action status
-- if options.status_enabled then
-- fs.writefile(options.status_path, api_group or "" .. " " .. api_action or "" .. " " .. name_or_id or "")
-- end
-- request_qurey = request_qurey or {}
-- request_body = request_body or {}
if name_or_id == "" then
name_or_id = nil
end
req_options.method = method
req_options.path = (api_group and ("/" .. api_group) or "") .. (name_or_id and ("/" .. name_or_id) or "") .. (api_action and ("/" .. api_action) or "")
req_options.header = {}
if type(request_qurey) == "table" then
for k, v in pairs(request_qurey) do
if type(v) == "table" then
if k ~= "_header" then
qurey = (qurey and qurey .. "&" or "?") .. k .. "=" .. urlencode(json_stringify(v))
else
-- for http header
for k1, v1 in pairs(v) do
req_options.header[k1] = v1
end
end
elseif type(v) == "boolean" then
qurey = (qurey and qurey .. "&" or "?") .. k .. "=" .. (v and "true" or "false")
elseif type(v) == "number" or type(v) == "string" then
qurey = (qurey and qurey .. "&" or "?") .. k .. "=" .. v
end
end
end
req_options.path = req_options.path .. (qurey or "")
-- if type(request_body) == "table" then
req_options.conetnt = request_body
-- end
local response = send_http_socket(req_options.socket_path, gen_http_req(req_options))
-- for docker action status
-- if options.status_enabled then
-- fs.remove(options.status_path)
-- end
return response
end
local gen_api = function(_table, http_method, api_group, api_action)
local _api_action
if api_action == "get_archive" or api_action == "put_archive" then
_api_action = "archive"
elseif api_action == "df" then
_api_action = "system/df"
elseif api_action ~= "list" and api_action ~= "inspect" and api_action ~= "remove" then
_api_action = api_action
elseif (api_group == "containers" or api_group == "images" or api_group == "exec") and (api_action == "list" or api_action == "inspect") then
_api_action = "json"
end
local fp = function(self, name_or_id, request_qurey, request_body)
if api_action == "list" then
if (name_or_id ~= "" and name_or_id ~= nil) then
if api_group == "images" then
name_or_id = nil
else
request_qurey = request_qurey or {}
request_qurey.filters = request_qurey.filters or {}
request_qurey.filters.name = request_qurey.filters.name or {}
request_qurey.filters.name[#request_qurey.filters.name + 1] = name_or_id
name_or_id = nil
end
end
elseif api_action == "create" then
if (name_or_id ~= "" and name_or_id ~= nil) then
request_qurey = request_qurey or {}
request_qurey.name = request_qurey.name or name_or_id
name_or_id = nil
end
elseif api_action == "logs" then
local body_buffer = ""
local response = send_http_require(self.options, http_method, api_group, _api_action, name_or_id, request_qurey, request_body)
if response.code >= 200 and response.code < 300 then
for i, v in ipairs(response.body) do
body_buffer = body_buffer .. docker_stream_filter(response.body[i])
end
response.body = body_buffer
end
return response
end
local response = send_http_require(self.options, http_method, api_group, _api_action, name_or_id, request_qurey, request_body)
if response.headers and response.headers["Content-Type"] == "application/json" then
if #response.body == 1 then
response.body = json_parse(response.body[1])
else
local tmp = {}
for _, v in ipairs(response.body) do
tmp[#tmp+1] = json_parse(v)
end
response.body = tmp
end
end
return response
end
if api_group then
_table[api_group][api_action] = fp
else
_table[api_action] = fp
end
end
local _docker = {containers = {}, exec = {}, images = {}, networks = {}, volumes = {}}
gen_api(_docker, "GET", "containers", "list")
gen_api(_docker, "POST", "containers", "create")
gen_api(_docker, "GET", "containers", "inspect")
gen_api(_docker, "GET", "containers", "top")
gen_api(_docker, "GET", "containers", "logs")
gen_api(_docker, "GET", "containers", "changes")
gen_api(_docker, "GET", "containers", "stats")
gen_api(_docker, "POST", "containers", "resize")
gen_api(_docker, "POST", "containers", "start")
gen_api(_docker, "POST", "containers", "stop")
gen_api(_docker, "POST", "containers", "restart")
gen_api(_docker, "POST", "containers", "kill")
gen_api(_docker, "POST", "containers", "update")
gen_api(_docker, "POST", "containers", "rename")
gen_api(_docker, "POST", "containers", "pause")
gen_api(_docker, "POST", "containers", "unpause")
gen_api(_docker, "POST", "containers", "update")
gen_api(_docker, "DELETE", "containers", "remove")
gen_api(_docker, "POST", "containers", "prune")
gen_api(_docker, "POST", "containers", "exec")
gen_api(_docker, "POST", "exec", "start")
gen_api(_docker, "POST", "exec", "resize")
gen_api(_docker, "GET", "exec", "inspect")
gen_api(_docker, "GET", "containers", "get_archive")
gen_api(_docker, "PUT", "containers", "put_archive")
-- TODO: export,attch
gen_api(_docker, "GET", "images", "list")
gen_api(_docker, "POST", "images", "create")
gen_api(_docker, "GET", "images", "inspect")
gen_api(_docker, "GET", "images", "history")
gen_api(_docker, "POST", "images", "tag")
gen_api(_docker, "DELETE", "images", "remove")
gen_api(_docker, "GET", "images", "search")
gen_api(_docker, "POST", "images", "prune")
-- TODO: build clear push commit export import
gen_api(_docker, "GET", "networks", "list")
gen_api(_docker, "GET", "networks", "inspect")
gen_api(_docker, "DELETE", "networks", "remove")
gen_api(_docker, "POST", "networks", "create")
gen_api(_docker, "POST", "networks", "connect")
gen_api(_docker, "POST", "networks", "disconnect")
gen_api(_docker, "POST", "networks", "prune")
gen_api(_docker, "GET", nil, "events")
gen_api(_docker, "GET", nil, "version")
gen_api(_docker, "GET", nil, "info")
gen_api(_docker, "GET", nil, "_ping")
gen_api(_docker, "GET", nil, "df")
function _docker.new(options)
local docker = {}
local _options = options or {}
docker.options = {
socket_path = _options.socket_path or "/var/run/docker.sock",
host = _options.host or "localhost",
version = _options.version or "v1.40",
user_agent = _options.user_agent or "LuCI",
protocol = _options.protocol or "HTTP/1.1",
-- status_enabled = _options.status_enabled or true,
-- status_path = _options.status_path or "/tmp/.docker_action_status",
debug = _options.debug or false,
debug_path = _options.debug_path or "/tmp/.docker_debug"
}
setmetatable(
docker,
{
__index = function(t, key)
if _docker[key] ~= nil then
return _docker[key]
else
return _docker.containers[key]
end
end
}
)
setmetatable(
docker.containers,
{
__index = function(t, key)
if key == "options" then
return docker.options
end
end
}
)
setmetatable(
docker.networks,
{
__index = function(t, key)
if key == "options" then
return docker.options
end
end
}
)
setmetatable(
docker.images,
{
__index = function(t, key)
if key == "options" then
return docker.options
end
end
}
)
setmetatable(
docker.exec,
{
__index = function(t, key)
if key == "options" then
return docker.options
end
end
}
)
return docker
end
return _docker