luci-app-dockerman/luci-lib-docker: add packages
This commit is contained in:
parent
75432f3cfd
commit
dca83141e6
@ -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).
|
||||
|
||||
43
package/ctcgfw/luci-app-dockerman/Makefile
Normal file
43
package/ctcgfw/luci-app-dockerman/Makefile
Normal 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)))
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 = " <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
|
||||
40
package/ctcgfw/luci-app-dockerman/luasrc/model/docker.lua
Normal file
40
package/ctcgfw/luci-app-dockerman/luasrc/model/docker.lua
Normal 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
|
||||
@ -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%>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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 %>
|
||||
@ -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>
|
||||
5
package/ctcgfw/luci-app-dockerman/root/etc/config/docker
Normal file
5
package/ctcgfw/luci-app-dockerman/root/etc/config/docker
Normal 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'
|
||||
46
package/ctcgfw/luci-lib-docker/Makefile
Normal file
46
package/ctcgfw/luci-lib-docker/Makefile
Normal 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)))
|
||||
421
package/ctcgfw/luci-lib-docker/luasrc/docker.lua
Normal file
421
package/ctcgfw/luci-lib-docker/luasrc/docker.lua
Normal 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 stirng,not 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
|
||||
Loading…
Reference in New Issue
Block a user