diff --git a/README.md b/README.md
index c975394af7..702f54445d 100644
--- a/README.md
+++ b/README.md
@@ -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).
luci-app-ssr-plus-jo source: [brokeld/luci-app-ssr-plus-jo](https://github.com/brokeld/luci-app-ssr-plus-jo).
openwrt-udpspeeder source: [pexcn/openwrt-udpspeeder](https://github.com/pexcn/openwrt-udpspeeder).
-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).
+luci-app-dockerman source: [lisaac/luci-app-dockerman](https://github.com/lisaac/luci-app-dockerman).
+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).
diff --git a/package/ctcgfw/luci-app-dockerman/Makefile b/package/ctcgfw/luci-app-dockerman/Makefile
new file mode 100644
index 0000000000..a9007be1f6
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/Makefile
@@ -0,0 +1,43 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=luci-app-dockerman
+PKG_VERSION:=v0.1.0
+PKG_RELEASE:=beta
+PKG_MAINTAINER:=lisaac
+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)))
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/controller/dockerman.lua b/package/ctcgfw/luci-app-dockerman/luasrc/controller/dockerman.lua
new file mode 100644
index 0000000000..91b81f2a68
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/controller/dockerman.lua
@@ -0,0 +1,172 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac
+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 .. " ") 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
\ No newline at end of file
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/container.lua b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/container.lua
new file mode 100644
index 0000000000..f43497dd9d
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/container.lua
@@ -0,0 +1,437 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac
+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 .. " ") 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 .. " ") 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 .. " ") 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 .. " ") 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 .. " " .. 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 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
\ No newline at end of file
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/containers.lua b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/containers.lua
new file mode 100644
index 0000000000..313e9b17d8
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/containers.lua
@@ -0,0 +1,205 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac
+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"] = ''.. data[index]["_status"] .. ""
+ else
+ data[index]["_status"] = ''.. data[index]["_status"] .. ""
+ 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).. " ")
+ else
+ docker:append_status("done ")
+ 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
\ No newline at end of file
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/images.lua b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/images.lua
new file mode 100644
index 0000000000..f14fb1fbb5
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/images.lua
@@ -0,0 +1,157 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac
+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).. " ")
+ else
+ docker:append_status("done ")
+ 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).. " ")
+ success = false
+ else
+ docker:append_status("done ")
+ end
+ end
+ if success then docker:clear_status() end
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
+ end
+end
+return m
\ No newline at end of file
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/networks.lua b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/networks.lua
new file mode 100644
index 0000000000..c37f8ff124
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/networks.lua
@@ -0,0 +1,125 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac
+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).. " ")
+ success = false
+ else
+ docker:append_status("done ")
+ end
+ end
+ if success then
+ docker:clear_status()
+ end
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
+ end
+end
+
+return m
\ No newline at end of file
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/newcontainer.lua b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/newcontainer.lua
new file mode 100644
index 0000000000..02046ccd54
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/newcontainer.lua
@@ -0,0 +1,281 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac
+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 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 ")
+ 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
+
+ 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
\ No newline at end of file
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/newnetwork.lua b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/newnetwork.lua
new file mode 100644
index 0000000000..5f104bdb96
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/newnetwork.lua
@@ -0,0 +1,202 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac
+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).. " ")
+ luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
+ end
+ end
+end
+
+return m
\ No newline at end of file
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/overview.lua b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/overview.lua
new file mode 100644
index 0000000000..2bce676403
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/model/cbi/docker/overview.lua
@@ -0,0 +1,48 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac
+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 = "
"
+ 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(""..translate("Download DockerReadme.pdf").."")
+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
\ No newline at end of file
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/model/docker.lua b/package/ctcgfw/luci-app-dockerman/luasrc/model/docker.lua
new file mode 100644
index 0000000000..74962ed6ce
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/model/docker.lua
@@ -0,0 +1,40 @@
+--[[
+LuCI - Lua Configuration Interface
+Copyright 2019 lisaac
+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
\ No newline at end of file
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/dummyvalue.htm b/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/dummyvalue.htm
new file mode 100644
index 0000000000..ac8a48aba4
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/dummyvalue.htm
@@ -0,0 +1,13 @@
+<%+cbi/valueheader%>
+<% if self.href then %><% 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 %><%end%>
+" />
+<%+cbi/valuefooter%>
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/inlinebutton.htm b/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/inlinebutton.htm
new file mode 100644
index 0000000000..68e9d20077
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/inlinebutton.htm
@@ -0,0 +1,7 @@
+
+ <% if self:cfgvalue(section) ~= false then %>
+ " type="submit"<%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> />
+ <% else %>
+ -
+ <% end %>
+
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/inlinevalue.htm b/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/inlinevalue.htm
new file mode 100644
index 0000000000..cfbf44a217
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/inlinevalue.htm
@@ -0,0 +1,33 @@
+
+ <%- if self.title then -%>
+
+ <%- end -%>
+ <%- if self.password then -%>
+ />
+ <%- end -%>
+ 0, "data-choices", { self.keylist, self.vallist })
+ %> />
+ <%- if self.password then -%>
+
∗
+ <% end %>
+
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/xsimpleform.htm b/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/xsimpleform.htm
new file mode 100644
index 0000000000..1ccbb3205c
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/view/cbi/xsimpleform.htm
@@ -0,0 +1,88 @@
+<%
+ if not self.embedded then
+ %><%
+ end
+%>
+
+
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/view/docker/apply_widget.htm b/package/ctcgfw/luci-app-dockerman/luasrc/view/docker/apply_widget.htm
new file mode 100644
index 0000000000..96f90b4c09
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/view/docker/apply_widget.htm
@@ -0,0 +1,144 @@
+
+
\ No newline at end of file
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/view/docker/container.htm b/package/ctcgfw/luci-app-dockerman/luasrc/view/docker/container.htm
new file mode 100644
index 0000000000..51c7f0acf4
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/view/docker/container.htm
@@ -0,0 +1,24 @@
+
+
+
+
\ No newline at end of file
diff --git a/package/ctcgfw/luci-app-dockerman/luasrc/view/docker/logs.htm b/package/ctcgfw/luci-app-dockerman/luasrc/view/docker/logs.htm
new file mode 100644
index 0000000000..6147e95290
--- /dev/null
+++ b/package/ctcgfw/luci-app-dockerman/luasrc/view/docker/logs.htm
@@ -0,0 +1,24 @@
+<%#
+LuCI - Lua Configuration Interface
+Copyright 2008 Steven Barth
+Copyright 2008 Jo-Philipp Wich
+
+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 %>
+