luci-app-dockerman: sync upsteam source

This commit is contained in:
CN_SZTL 2019-12-21 19:04:33 +08:00
parent 1d40d1ef59
commit 5e3bae59a5
No known key found for this signature in database
GPG Key ID: 6850B6345C862176
12 changed files with 1090 additions and 163 deletions

View File

@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-dockerman
PKG_VERSION:=v0.1.0
PKG_VERSION:=v0.1.4
PKG_RELEASE:=beta
PKG_MAINTAINER:=lisaac <https://github.com/lisaac/luci-app-dockerman>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)

View File

@ -14,31 +14,24 @@ 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
local 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():_ping().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","networks"},form("docker/networks"),_("Networks"),3).leaf=true
entry({"admin","docker","volumes"},form("docker/volumes"),_("Volumes"),4).leaf=true
entry({"admin","docker","events"},call("action_events"),_("Events"),5)
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
@ -148,7 +141,7 @@ function action_get_container_stats(container_id)
end
function action_confirm()
local status_path=luci.model.uci.cursor().get("docker","local", "status_path")
local status_path=luci.model.uci.cursor():get("docker", "local", "status_path")
local data = nixio.fs.readfile(status_path)
if data then
code = 202
@ -163,10 +156,3 @@ function action_confirm()
luci.http.prepare_content("application/json")
luci.http.write_json({info = data})
end
function act_status()
local e={}
e.running=luci.sys.call("pgrep /usr/bin/dockerd >/dev/null")==0
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end

View File

@ -24,8 +24,8 @@ 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
if d.HostConfig and d.HostConfig.PortBindings then
for inter, out in pairs(d.HostConfig.PortBindings) do
data = (data and (data .. "<br>") or "") .. out[1]["HostPort"] .. ":" .. inter
end
end
@ -62,6 +62,15 @@ local get_mounts = function(d)
return data
end
local get_device = function(d)
local data
if d.HostConfig and d.HostConfig.Devices then
for _,v in ipairs(d.HostConfig.Devices) do
data = (data and (data .. "<br>") or "") .. v["PathOnHost"] .. ":" .. v["PathInContainer"] .. (v["CgroupPermissions"] ~= "" and (":" .. v["CgroupPermissions"]) or "")
end
end
return data
end
local get_links = function(d)
local data
@ -84,19 +93,25 @@ local get_networks = function(d)
end
local start_stop_remove = function(m,cmd)
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)
local res
if cmd ~= "upgrade" then
res = dk.containers[cmd](dk, container_id)
else
res = dk.containers_upgrade(dk, container_id)
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
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"))
docker:clear_status()
if cmd ~= "remove" and cmd ~= "upgrade" then
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
else
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
end
end
end
@ -131,6 +146,16 @@ btnstop.template="cbi/inlinebutton"
btnstop.inputtitle=translate("Stop")
btnstop.inputstyle = "reset"
btnstop.forcewrite = true
btnupgrade=action_section:option(Button, "_upgrade")
btnupgrade.template="cbi/inlinebutton"
btnupgrade.inputtitle=translate("Upgrade")
btnupgrade.inputstyle = "reload"
btnstop.forcewrite = true
btnduplicate=action_section:option(Button, "_duplicate")
btnduplicate.template="cbi/inlinebutton"
btnduplicate.inputtitle=translate("Duplicate")
btnduplicate.inputstyle = "add"
btnstop.forcewrite = true
btnremove=action_section:option(Button, "_remove")
btnremove.template="cbi/inlinebutton"
btnremove.inputtitle=translate("Remove")
@ -143,12 +168,18 @@ end
btnrestart.write = function(self, section)
start_stop_remove(m,"restart")
end
btnupgrade.write = function(self, section)
start_stop_remove(m,"upgrade")
end
btnremove.write = function(self, section)
start_stop_remove(m,"remove")
end
btnstop.write = function(self, section)
start_stop_remove(m,"stop")
end
btnduplicate.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer/duplicate/"..container_id))
end
tab_section = m:section(SimpleSection)
tab_section.template="docker/container"
@ -166,7 +197,9 @@ if action == "info" then
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["09device"] = {_key = translate("Device"), _value = get_device(container_info) or "-"}
table_info["09mount"] = {_key = translate("Mount/Volume"), _value = get_mounts(container_info) or "-"}
table_info["10cmd"] = {_key = translate("Command"), _value = get_command(container_info) or "-"}
table_info["11env"] = {_key = translate("Env"), _value = get_env(container_info) or "-"}
table_info["12ports"] = {_key = translate("Ports"), _value = get_ports(container_info) or "-"}

View File

@ -27,14 +27,17 @@ function get_images()
data[index]["_selected"] = 0
data[index]["_id"] = v.Id:sub(8,20)
if v.RepoTags then
data[index]["_tags"] = v.RepoTags[1]
for i, v1 in ipairs(v.RepoTags) do
data[index]["_tags"] =(data[index]["_tags"] and ( data[index]["_tags"] .. "<br\>" )or "") .. v1
end
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)
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
"<a href=/cgi-bin/luci/admin/docker/container/"..cv.Id.." >".. cv.Names[1]:sub(2).."</a>"
end
end
data[index]["_size"] = string.format("%.2f", tostring(v.Size/1024/1024)).."MB"
@ -105,29 +108,15 @@ 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, "_tags", translate("RepoTags")).rawhtml = true
image_table:option(DummyValue, "_containers", translate("Containers")).rawhtml = true
image_table:option(DummyValue, "_size", translate("Size"))
image_table:option(DummyValue, "_created", translate("Created"))
image_selecter.write = function(self, section, value)
image_list[section]._selected = value
end
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 remove_action = function(force)
local image_selected = {}
-- 遍历table中sectionid
local image_table_sids = image_table:cfgsections()
@ -142,7 +131,11 @@ btnremove.write = function(self, section)
docker:clear_status()
for _,img in ipairs(image_selected) do
docker:append_status("Images: " .. "remove" .. " " .. img .. "...")
local msg = dk.images["remove"](dk, img)
local query_body ={}
if force then
query_body.force = true
end
local msg = dk.images["remove"](dk, img, query_body)
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
@ -154,4 +147,32 @@ btnremove.write = function(self, section)
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
end
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)
remove_action()
end
btnforceremove = action:option(Button, "forceremove")
btnforceremove.inputtitle= translate("Force Remove")
btnforceremove.template="cbi/inlinebutton"
btnforceremove.inputstyle = "remove"
btnforceremove.forcewrite = true
btnforceremove.write = function(self, section)
remove_action(true)
end
return m

View File

@ -12,13 +12,122 @@ 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 cmd_line = table.concat(arg, '/')
local create_body = {}
local images = dk.images:list().body
local networks = dk.networks:list().body
local containers = dk.containers:list(nil, {all=true}).body
-- reslvo default config
local default_config = { }
if cmd_line and cmd_line:match("^docker.+") then
local key = nil
--cursor = 0: docker run
--cursor = 1: resloving para
--cursor = 2: resloving image
--cursor > 2: resloving command
local cursor = 0
for w in cmd_line:gmatch("[^%s]+") do
-- skip '\'
if w == '\\' then
-- start with '-'
elseif w:match("^%-+(.+)") and cursor <= 1 then
local v = w:match("^%-+.-=(.+)")
if v then
key = w:match("^%-+(.-)=.+"):lower()
else
key = w:match("^%-+(.+)"):lower()
end
if key == "v" or key == "volume" then
key = "mount"
elseif key == "p" then
key = "port"
elseif key == "e" then
key = "env"
elseif key == "net" then
key = "network"
elseif key == "cpu-shares" then
key = "cpushares"
elseif key == "m" then
key = "memory"
elseif key == "blkio-weight" then
key = "blkioweight"
elseif key == "privileged" then
default_config["privileged"] = 1
end
if v then
if key == "mount" or key == "link" or key == "env" or key == "port" or key == "device" or key == "tmpfs" then
if not default_config[key] then default_config[key] = {} end
table.insert( default_config[key], v )
else
default_config[key] = v
end
end
-- value
elseif key and type(key) == "string" then
if key == "mount" or key == "link" or key == "env" or key == "port" or key == "device" or key == "tmpfs" then
if not default_config[key] then default_config[key] = {} end
table.insert( default_config[key], w )
else
default_config[key] = w
end
if key == "cpus" or key == "cpushare" or key == "memory" or key == "blkioweight" or key == "device" or key == "tmpfs" then
default_config["advance"] = 1
end
key = nil
cursor = 1
--image and command
elseif cursor >= 1 and key == nil then
if cursor == 1 then
default_config["image"] = w
elseif cursor > 1 then
default_config["command"] = (default_config["command"] and (default_config["command"] .. " " )or "") .. w
end
cursor = cursor + 1
end
end
elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then
local container_id = cmd_line:match("^duplicate/(.+)")
create_body = dk:containers_duplicate_config(container_id)
if not create_body.HostConfig then create_body.HostConfig = {} end
if next(create_body) ~= nil then
default_config.name = nil
default_config.image = create_body.Image
default_config.privileged = create_body.HostConfig.Privileged and 1 or 0
default_config.restart = create_body.HostConfig.RestartPolicy and create_body.HostConfig.RestartPolicy.name or nil
default_config.network = create_body.HostConfig.NetworkMode == "default" and "bridge" or create_body.HostConfig.NetworkMode
default_config.ip = default_config.network and default_config.network ~= "bridge" and default_config.network ~= "host" and default_config.network ~= "null" and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig.IPv4Address or nil
default_config.link = create_body.HostConfig.Links
default_config.env = create_body.Env
default_config.mount = create_body.HostConfig.Binds
if create_body.HostConfig.PortBindings and type(create_body.HostConfig.PortBindings) == "table" then
default_config.port = {}
for k, v in pairs(create_body.HostConfig.PortBindings) do
table.insert( default_config.port, v[1].HostPort..":"..k:match("^(%d+)/.+").."/"..k:match("^%d+/(.+)") )
end
end
default_config.user = create_body.User or nil
default_config.command = create_body.Cmd and type(create_body.Cmd) == "table" and table.concat(create_body.Cmd, " ") or nil
default_config.advance = 1
default_config.cpus = create_body.HostConfig.NanoCPUs
default_config.cpushares = create_body.HostConfig.CpuShares
default_config.memory = create_body.HostConfig.Memory
default_config.blkioweight = create_body.HostConfig.BlkioWeight
if create_body.HostConfig.Devices and type(create_body.HostConfig.Devices) == "table" then
default_config.device = {}
for _, v in ipairs(create_body.HostConfig.Devices) do
table.insert( default_config.device, v.PathOnHost..":"..v.PathInContainer..(v.CgroupPermissions ~= "" and (":" .. v.CgroupPermissions) or "") )
end
end
default_config.tmpfs = create_body.HostConfig.Tmpfs
end
end
local m = SimpleForm("docker", translate("Docker"))
m.tempalte = "cbi/xsimpleform"
@ -36,20 +145,34 @@ local s = m:section(SimpleSection, translate("New Container"))
s.addremove = true
s.anonymous = true
local d = s:option(Value, "name", translate("Container Name"))
local d = s:option(DummyValue,"cmd_line","Resolv CLI")
d.rawhtml = true
d.template = "docker/resolv_container"
d = s:option(Value, "name", translate("Container Name"))
d.rmempty = true
d.default = container_name
d = s:option(Flag, "_force_pull", translate("Always pull image first"))
d.rmempty = true
d.disabled = 0
d.enabled = 1
d.default = 0
d.default = default_config.name or nil
d = s:option(Value, "image", translate("Docker Image"))
d.rmempty = true
d.default = image_name
d.default = default_config.image or nil
for _, v in ipairs (images) do
if v.RepoTags then
d:value(v.RepoTags[1], v.RepoTags[1])
end
end
d = s:option(Flag, "privileged", translate("Privileged"))
d.rmempty = true
d.disabled = 0
d.enabled = 1
d.default = default_config.privileged or 0
d = s:option(ListValue, "restart", translate("Restart policy"))
d.rmempty = true
@ -58,88 +181,123 @@ 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.default = default_config.restart or "unless-stopped"
d = s:option(ListValue, "network", translate("Networks"))
local d_network = s:option(ListValue, "network", translate("Networks"))
d_network.rmempty = true
d_network.default = default_config.network or "bridge"
local d_ip = s:option(Value, "ip", translate("IPv4 Address"))
d_ip.datatype="ip4addr"
d_ip:depends("network", "nil")
d_ip.default = default_config.ip or nil
d = s:option(Value, "user", translate("User"))
d.placeholder = "1000:1000"
d.rmempty = true
d.default = "bridge"
d.default = default_config.user or nil
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 = s:option(DynamicList, "link", translate("Links with other containers"))
d.template = "cbi/xdynlist"
d.placeholder = "container_name:alias"
d.rmempty = true
d:depends("network", "bridge")
d.default = default_config.link or nil
d = s:option(DynamicList, "env", translate("Environmental Variable"))
d.template = "cbi/xdynlist"
d.placeholder = "TZ=Asia/Shanghai"
d.rmempty = true
d.default = default_config.env or nil
d = s:option(DynamicList, "mount", translate("Bind Mount"))
d.template = "cbi/xdynlist"
d.placeholder = "/media:/media:slave"
d.rmempty = true
d.default = default_config.mount or nil
d = s:option(DynamicList, "port", translate("Exposed Ports"))
d.placeholder = "2200:22/tcp"
d.rmempty = true
local d_ports = s:option(DynamicList, "port", translate("Exposed Ports"))
d_ports.template = "cbi/xdynlist"
d_ports.placeholder = "2200:22/tcp"
d_ports.rmempty = true
d_ports.default = default_config.port or nil
d = s:option(Value, "command", translate("Run command"))
d.placeholder = "/bin/sh init.sh"
d.rmempty = true
d.default = default_config.command or nil
d = s:option(Flag, "advance", translate("Advance"))
d.rmempty = true
d.disabled = 0
d.enabled = 1
d.default = default_config.advance or 0
d = s:option(Value, "cpus", translate("CPUs"), translate("Number of CPUs. Number is a fractional number. 0.000 means no limit."))
d.placeholder = "1.5"
d.rmempty = true
d:depends("advance", 1)
d.datatype="ufloat"
d.default = default_config.cpus or nil
d = s:option(Value, "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.default = default_config.cpushares or nil
d = s:option(Value, "memory", translate("Memory"), translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M."))
d.placeholder = "128m"
d.rmempty = true
d:depends("advance", 1)
d.default = default_config.memory or nil
d = s:option(Value, "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.default = default_config.blkioweight or nil
d = s:option(DynamicList, "device", translate("Device"))
d.template = "cbi/xdynlist"
d.placeholder = "/dev/sda:/dev/xvdc:rwm"
d.rmempty = true
d:depends("advance", 1)
d.default = default_config.device or nil
d = s:option(DynamicList, "tmpfs", translate("Tmpfs"), translate("Mount tmpfs filesystems"))
d.template = "cbi/xdynlist"
d.placeholder = "/run:rw,noexec,nosuid,size=65536k"
d.rmempty = true
d:depends("advance", 1)
d.default = default_config.tmpfs or nil
for _, v in ipairs (networks) do
if v.Name then
local parent = v.Options and v.Options.parent or nil
local ip = v.IPAM and v.IPAM.Config and v.IPAM.Config[1] and v.IPAM.Config[1].Subnet or nil
ipv6 = v.IPAM and v.IPAM.Config and v.IPAM.Config[2] and v.IPAM.Config[2].Subnet or nil
local network_name = v.Name .. " | " .. v.Driver .. (parent and (" | " .. parent) or "") .. (ip and (" | " .. ip) or "").. (ipv6 and (" | " .. ipv6) or "")
d_network:value(v.Name, network_name)
if v.Name ~= "none" and v.Name ~= "bridge" and v.Name ~= "host" then
d_ip:depends("network", v.Name)
end
if v.Driver == "bridge" then
d_ports:depends("network", v.Name)
end
end
end
m.handle = function(self, state, data)
if state == FORM_VALID then
local tmp
local name = data.name
local image = data.image
local user = data.user
if not image:match(".-:.+") then
image = image .. ":latest"
end
@ -167,7 +325,30 @@ m.handle = function(self, state, data)
end
end
tmp = data.port
local device = {}
tmp = data.device
if type(tmp) == "table" then
for i, v in ipairs(tmp) do
local t = {}
local _,_, h, c, p = v:find("(.-):(.-):(.+)")
if h and c then
t['PathOnHost'] = h
t['PathInContainer'] = c
t['CgroupPermissions'] = p or nil
else
local _,_, h, c = v:find("(.-):(.+)")
if h and c then
t['PathOnHost'] = h
t['PathInContainer'] = c
end
end
if next(t) ~= nil then
table.insert( device, t )
end
end
end
tmp = data.port or {}
for i, v in ipairs(tmp) do
for v1 ,v2 in string.gmatch(v, "(%d+):([^%s]+)") do
local _,_,p= v2:find("^%d+/(%w+)")
@ -179,11 +360,11 @@ m.handle = function(self, state, data)
end
end
local links = data.links
local link = data.link
tmp = data.command
local command = {}
if tmp ~= nil then
for v in string.gmatch(tmp, "[^%s]+") do
for v in string.gmatch(tmp, "[^%s]+") do
command[#command+1] = v
end
end
@ -203,45 +384,62 @@ m.handle = function(self, state, data)
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
}
create_body.Hostname = name
create_body.User = user
create_body.Cmd = (#command ~= 0) and command or nil
create_body.Env = env
create_body.Image = image
create_body.ExposedPorts = (next(exposedports) ~= nil) and exposedports or nil
create_body.HostConfig = create_body.HostConfig or {}
create_body.HostConfig.Binds = (#mount ~= 0) and mount or nil
create_body.HostConfig.NetworkMode = network
create_body.HostConfig.RestartPolicy = { Name = restart, MaximumRetryCount = 0 }
create_body.HostConfig.Privileged = privileged and true or false
create_body.HostConfig.PortBindings = (next(portbindings) ~= nil) and portbindings or nil
create_body.HostConfig.Memory = tonumber(memory)
create_body.HostConfig.CpuShares = tonumber(cpushares)
create_body.HostConfig.NanoCPUs = tonumber(cpus) * 10 ^ 9
create_body.HostConfig.BlkioWeight = tonumber(blkioweight)
if ip then
if create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and type(create_body.NetworkingConfig.EndpointsConfig) == "table" then
for k, v in pairs (create_body.NetworkingConfig.EndpointsConfig) do
if k == network and v.IPAMConfig and v.IPAMConfig.IPv4Address then
v.IPAMConfig.IPv4Address = ip
else
create_body.NetworkingConfig.EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } }
end
break
end
else
create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } }
end
elseif not create_body.NetworkingConfig then
create_body.NetworkingConfig = nil
end
if next(tmpfs) ~= nil then
create_body["HostConfig"]["Tmpfs"] = tmpfs
end
if network == "bridge" and next(links) ~= nil then
create_body["HostConfig"]["Links"] = links
if next(device) ~= nil then
create_body["HostConfig"]["Devices"] = device
end
if network == "bridge" and next(link) ~= nil then
create_body["HostConfig"]["Links"] = link
end
local pull_image = function(image)
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
docker:clear_status()
local exist_image = false
if image then
@ -252,17 +450,9 @@ m.handle = function(self, state, data)
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
pull_image(image)
elseif data._force_pull == 1 then
pull_image(image)
end
end

View File

@ -65,6 +65,7 @@ d.default = 0
d:depends("dirver", "overlay")
d = s:option(DynamicList, "options", translate("Options"))
d.template = "cbi/xdynlist"
d.rmempty = true
d.placeholder="com.docker.network.driver.mtu=1500"
@ -90,6 +91,7 @@ d.placeholder="10.1.1.0/24"
d.datatype="ip4addr"
d = s:option(DynamicList, "aux_address", translate("Exclude IPs"))
d.template = "cbi/xdynlist"
d.rmempty = true
d.placeholder="my-route=10.1.1.1"

View File

@ -7,33 +7,83 @@ You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
$Id$
]]--
if nixio.fs.access("/etc/config/dockerd") then
local running = (luci.sys.call("pidof portainer >/dev/null") == 0)
local button = ""
if running then
button = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br /><br /><input type=\"button\" value=\" " .. translate("Open Portainer Docker Admin") .. " \" onclick=\"window.open('http://'+window.location.hostname+':" .. 9999 .. "')\"/><br />"
require "luci.util"
local docker = require "luci.model.docker"
local uci = require "luci.model.uci"
function byte_format(byte)
local suff = {"B", "KB", "MB", "GB", "TB"}
for i=1, 5 do
if byte > 1024 and i < 5 then
byte = byte / 1024
else
return string.format("%.2f %s", byte, suff[i])
end
end
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
local m = Map("docker", translate("Docker"))
local docker_info_table = {}
-- docker_info_table['0OperatingSystem'] = {_key=translate("Operating System"),_value='-'}
-- docker_info_table['1Architecture'] = {_key=translate("Architecture"),_value='-'}
-- docker_info_table['2KernelVersion'] = {_key=translate("Kernel Version"),_value='-'}
docker_info_table['3ServerVersion'] = {_key=translate("Docker Version"),_value='-'}
docker_info_table['4ApiVersion'] = {_key=translate("Api Version"),_value='-'}
docker_info_table['5NCPU'] = {_key=translate("CPUs"),_value='-'}
docker_info_table['6MemTotal'] = {_key=translate("Total Memory"),_value='-'}
docker_info_table['7DockerRootDir'] = {_key=translate("Docker Root Dir"),_value='-'}
docker_info_table['8IndexServerAddress'] = {_key=translate("Index Server Address"),_value='-'}
m2 = Map("docker", translate("Docker"))
s = m2:section(NamedSection, "local", "section", translate("Setting"))
s = m:section(Table, docker_info_table)
s:option(DummyValue, "_key", translate("Info"))
s:option(DummyValue, "_value")
s = m:section(SimpleSection)
s.containers_running = '-'
s.images_used = '-'
s.containers_total = '-'
s.images_total = '-'
s.networks_total = '-'
s.volumes_total = '-'
local socket = luci.model.uci.cursor():get("docker", "local", "socket_path")
if nixio.fs.access(socket) and (require "luci.model.docker").new():_ping().code == 200 then
local dk = docker.new()
-- local containers_list = dk.containers:list(nil, {all=true}).body
-- local images_list = dk.images:list().body
local volumes_list = dk.volumes:list().body.Volumes
local networks_list = dk.networks:list().body
local docker_info = dk:info()
-- docker_info_table['0OperatingSystem']._value = docker_info.body.OperatingSystem
-- docker_info_table['1Architecture']._value = docker_info.body.Architecture
-- docker_info_table['2KernelVersion']._value = docker_info.body.KernelVersion
docker_info_table['3ServerVersion']._value = docker_info.body.ServerVersion
docker_info_table['4ApiVersion']._value = docker_info.headers["Api-Version"]
docker_info_table['5NCPU']._value = tostring(docker_info.body.NCPU)
docker_info_table['6MemTotal']._value = tostring(byte_format(docker_info.body.MemTotal))
docker_info_table['7DockerRootDir']._value = docker_info.body.DockerRootDir
docker_info_table['8IndexServerAddress']._value = docker_info.body.IndexServerAddress
-- s.images_used = 0
-- for i, v in ipairs(images_list) do
-- for ci,cv in ipairs(containers_list) do
-- if v.Id == cv.ImageID then
-- s.images_used = s.images_used + 1
-- break
-- end
-- end
-- end
s.containers_running = tostring(docker_info.body.ContainersRunning)
-- s.images_used = tostring(s.images_used)
s.containers_total = tostring(docker_info.body.Containers)
s.images_total = tostring(docker_info.body.Images)
s.networks_total = tostring(#networks_list)
s.volumes_total = tostring(#volumes_list)
end
s.template = "docker/overview"
s = m: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"))
@ -41,8 +91,5 @@ debug = s:option(Flag, "debug", translate("Enable Debug"), translate("For debug,
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
return m

View File

@ -0,0 +1,122 @@
--[[
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, volumes
local res = dk.volumes:list()
if res.code <300 then volumes = res.body.Volumes else return end
res = dk.containers:list(nil, {all=true})
if res.code <300 then containers = res.body else return end
function get_volumes()
local data = {}
for i, v in ipairs(volumes) do
-- local index = v.CreatedAt .. v.Name
local index = v.Name
data[index]={}
data[index]["_selected"] = 0
data[index]["_nameraw"] = v.Name
data[index]["_name"] = v.Name:sub(1,12)
for ci,cv in ipairs(containers) do
if cv.Mounts and type(cv.Mounts) ~= "table" then break end
for vi, vv in ipairs(cv.Mounts) do
if v.Name == vv.Name then
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
"<a href=/cgi-bin/luci/admin/docker/container/"..cv.Id.." >".. cv.Names[1]:sub(2).."</a>"
end
end
end
data[index]["_driver"] = v.Driver
data[index]["_mountpoint"] = nil
for v1 in v.Mountpoint:gmatch('[^/]+') do
if v1 == index then
data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..."
else
data[index]["_mountpoint"] = (data[index]["_mountpoint"] and data[index]["_mountpoint"] or "").."/".. v1
end
end
data[index]["_created"] = v.CreatedAt
end
return data
end
local volume_list = get_volumes()
-- m = Map("docker", translate("Docker"))
m = SimpleForm("docker", translate("Docker"))
m.tempalte = "cbi/xsimpleform"
m.submit=false
m.reset=false
volume_table = m:section(Table, volume_list, translate("Volumes"))
volume_selecter = volume_table:option(Flag, "_selected","")
volume_selecter.disabled = 0
volume_selecter.enabled = 1
volume_selecter.default = 0
volume_id = volume_table:option(DummyValue, "_name", translate("Name"))
volume_table:option(DummyValue, "_driver", translate("Driver"))
volume_table:option(DummyValue, "_containers", translate("Containers")).rawhtml = true
volume_table:option(DummyValue, "_mountpoint", translate("Mount Point"))
volume_table:option(DummyValue, "_created", translate("Created"))
volume_selecter.write = function(self, section, value)
volume_list[section]._selected = value
end
docker_status = m:section(SimpleSection)
docker_status.template="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 volume_selected = {}
-- 遍历table中sectionid
local volume_table_sids = volume_table:cfgsections()
for _, volume_table_sid in ipairs(volume_table_sids) do
-- 得到选中项的名字
if volume_list[volume_table_sid]._selected == 1 then
-- volume_selected[#volume_selected+1] = volume_id:cfgvalue(volume_table_sid)
luci.util.perror(volume_table_sid)
volume_selected[#volume_selected+1] = volume_table_sid
end
end
if next(volume_selected) ~= nil then
local success = true
docker:clear_status()
for _,vol in ipairs(volume_selected) do
docker:append_status("Volumes: " .. "remove" .. " " .. vol .. "...")
local msg = dk.volumes["remove"](dk, vol)
if msg.code ~= 204 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/volumes"))
end
end
return m

View File

@ -7,28 +7,221 @@ 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 = {}
--pull image and return iamge id
local update_image = function(self, image_name)
local server = "index.docker.io"
local json_stringify = luci.json and luci.json.encode or luci.jsonc.stringify
_docker:append_status("Images: " .. "pulling" .. " " .. image_name .. "...")
local x_auth = nixio.bin.b64encode(json_stringify({serveraddress= server}))
local res = self.images:create(nil, {fromImage=image_name, _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>")
end
new_image_id = self.images:inspect(image_name).body.Id
return new_image_id, res
end
local table_equal = function(t1, t2)
if not t1 then return true end
if not t2 then return false end
if #t1 ~= #t2 then return false end
for i, v in ipairs(t1) do
if t1[i] ~= t2[i] then return false end
end
return true
end
local table_subtract = function(t1, t2)
if not t1 or next(t1) == nil then return nil end
if not t2 or next(t2) == nil then return t1 end
local res = {}
for _, v1 in ipairs(t1) do
local found = false
for _, v2 in ipairs(t2) do
if v1 == v2 then
found= true
break
end
end
if not found then
table.insert(res, v1)
end
end
return next(res) == nil and nil or res
end
local map_subtract = function(t1, t2)
if not t1 or next(t1) == nil then return nil end
if not t2 or next(t2) == nil then return t1 end
local res = {}
for k1, v1 in pairs(t1) do
local found = false
for k2, v2 in ipairs(t2) do
if k1 == k2 and luci.util.serialize_data(v1) == luci.util.serialize_data(v2) then
found= true
break
end
end
if not found then
if v1 and type(v1) == "table" then
if next(v1) == nil then
res[k1] = { k = 'v' }
else
res[k1] = v1
end
end
end
end
return next(res) ~= nil and res or nil
end
-- return create_body, extra_network
local get_config = function(old_config, old_host_config, old_network_setting, image_config)
local config = old_config
if config.WorkingDir == image_config.WorkingDir then config.WorkingDir = "" end
if config.User == image_config.User then config.User = "" end
if table_equal(config.Cmd, image_config.Cmd) then config.Cmd = nil end
if table_equal(config.Entrypoint, image_config.Entrypoint) then config.Entrypoint = nil end
config.Env = table_subtract(config.Env, image_config.Env)
config.Labels = table_subtract(config.Labels, image_config.Labels)
config.Volumes = map_subtract(config.Volumes, image_config.Volumes)
-- subtract ports exposed in image from container
if old_host_config.PortBindings and next(old_host_config.PortBindings) ~= nil then
for p, v in pairs(old_host_config.PortBindings) do
config.ExposedPorts[p] = { HostPort=v[1] and v[1].HostPort }
end
end
-- handle network config, we need only one network, extras need to network connect action
local network_setting = {}
local multi_network = false
local extra_network = {}
for k, v in pairs(old_network_setting) do
if multi_network then
extra_network[k] = v
else
network_setting[k] = v
end
multi_network = true
end
-- handle hostconfig
local host_config = old_host_config
if host_config.PortBindings and next(host_config.PortBindings) == nil then host_config.PortBindings = nil end
host_config.LogConfig = nil
-- merge configs
local create_body = config
create_body["HostConfig"] = host_config
create_body["NetworkingConfig"] = {EndpointsConfig = network_setting}
return create_body, extra_network
end
local upgrade = function(self, container_id)
_docker:clear_status()
-- get image name, image id, container name, configuration information
local container_info = self.containers:inspect(container_id)
if container_info.code > 300 and type(container_info.body) == "table" then
return container_info
end
local image_name = container_info.body.Config.Image
if not image_name:match(".-:.+") then image_name = image_name .. ":latest" end
local old_image_id = container_info.body.Image
local container_name = container_info.body.Name:sub(2)
local old_config = container_info.body.Config
local old_host_config = container_info.body.HostConfig
local old_network_setting = container_info.body.NetworkSettings.Networks or {}
local image_id, res = update_image(self, image_name)
if res and res.code > 300 then return res end
if image_id == old_image_id then
return {code = 305, body = {message = "Already up to date"}}
end
_docker:append_status("Container: " .. "Stop" .. " " .. container_name .. "...")
res = self.containers:stop(container_name)
if res and res.code < 305 then
_docker:append_status("done<br>")
else
return res
end
_docker:append_status("Container: rename" .. " " .. container_name .. " to ".. container_name .. "_old ...")
res = self.containers:rename(container_name,{ name = container_name .. "_old" })
if res and res.code < 300 then
_docker:append_status("done<br>")
else
return res
end
-- handle config
local image_config = self.images:inspect(old_image_id).body.Config
local create_body, extra_network = get_config(old_config, old_host_config, old_network_setting, image_config)
-- create new container
_docker:append_status("Container: Create" .. " " .. container_name .. "...")
res = self.containers:create(container_name, nil, create_body)
if res and res.code > 300 then return res end
_docker:append_status("done<br>")
-- extra networks need to network connect action
for k, v in pairs(extra_network) do
if v.IPAMConfig and next(v.IPAMConfig) == nil then v.IPAMConfig =nil end
if v.DriverOpts and next(v.DriverOpts) == nil then v.DriverOpts =nil end
if v.Aliases and next(v.Aliases) == nil then v.Aliases =nil end
_docker:append_status("Networks: Connect" .. " " .. container_name .. "...")
res = self.networks:connect(k, nil, {Container = container_name, EndpointConfig = v})
if res.code > 300 then return res end
_docker:append_status("done<br>")
end
_docker:clear_status()
return res
end
local duplicate_config = function (self, container_id)
local container_info = self.containers:inspect(container_id)
if container_info.code > 300 and type(container_info.body) == "table" then return nil end
local old_image_id = container_info.body.Image
local old_config = container_info.body.Config
local old_host_config = container_info.body.HostConfig
local old_network_setting = container_info.body.NetworkSettings.Networks or {}
local image_config = self.images:inspect(old_image_id).body.Config
return get_config(old_config, old_host_config, old_network_setting, image_config)
end
_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")
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")
_new.options.status_path = uci:get("docker", "local", "status_path")
_new.containers_upgrade = upgrade
_new.containers_duplicate_config = duplicate_config
return _new
end
_docker.options={}
_docker.options.status_path = uci.get("docker","local", "status_path")
_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+")
local file_docker_action_status=io.open(self.options.status_path, "a+")
file_docker_action_status:write(val)
file_docker_action_status:close()
end

View File

@ -0,0 +1,27 @@
<%+cbi/valueheader%>
<div<%=
attr("data-prefix", cbid) ..
attr("data-browser-path", self.default_path) ..
attr("data-dynlist", luci.util.serialize_json({
self.keylist, self.vallist,
self.datatype, self.optional or self.rmempty
})) ..
ifattr(self.size, "data-size", self.size) ..
ifattr(self.placeholder, "data-placeholder", self.placeholder)
%>>
<%
local vals = self:cfgvalue(section) or self.default or {}
for i=1, #vals + 1 do
local val = vals[i]
if (val and #val > 0) or (i == 1) then
%>
<input class="cbi-input-text" value="<%=pcdata(val)%>" data-update="change" type="text"<%=
attr("id", cbid .. "." .. i) ..
attr("name", cbid) ..
ifattr(self.size, "size") ..
ifattr(i == 1 and self.placeholder, "placeholder", self.placeholder)
%> /><br />
<% end end %>
</div>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,287 @@
<style>
/*!
Pure v1.0.1
Copyright 2013 Yahoo!
Licensed under the BSD License.
https://github.com/pure-css/pure/blob/master/LICENSE.md
*/
.pure-g {
letter-spacing: -.31em;
text-rendering: optimizespeed;
font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-webkit-flex-flow: row wrap;
-ms-flex-flow: row wrap;
flex-flow: row wrap;
-webkit-align-content: flex-start;
-ms-flex-line-pack: start;
align-content: flex-start
}
.pure-u {
display: inline-block;
zoom: 1;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto
}
.pure-g [class*=pure-u] {
font-family: sans-serif
}
.pure-u-1-4,
.pure-u-2-5,
.pure-u-3-5 {
display: inline-block;
zoom: 1;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto
}
.pure-u-1-4 {
width: 25%
}
.pure-u-2-5 {
width: 40%
}
.pure-u-3-5 {
width: 60%
}
.status {
margin: 1rem -0.5rem 1rem -0.5rem;
}
.block {
margin: 0.5rem 0.5rem;
padding: 0;
font-weight: normal;
font-style: normal;
line-height: 1;
font-family: inherit;
min-width: inherit;
overflow-x: auto;
overflow-y: hidden;
border: 1px solid rgba(0, 0, 0, .05);
border-radius: .375rem;
background-color: #fff;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .15);
}
.img-con {
margin: 1rem;
}
.block h4 {
font-size: .8125rem;
font-weight: 600;
margin: 1rem;
color: #8898aa !important;
line-height: 1.8em;
}
.cbi-section-table-cell {
position: relative;
}
@media screen and (max-width: 700px) {
.pure-u-1-4 {
width: 50%;
}
.cbi-button-add {
position: fixed;
padding: 0.3rem 0.5rem;
z-index: 1000;
width: 50px !important;
height: 50px;
bottom: 90px;
right: 5px;
font-size: 16px;
border-radius: 50%;
display: block;
background-color: #fb6340 !important;
border-color: #fb6340 !important;
box-shadow: 0 0 1rem 0 rgba(136, 152, 170, .75);
}
}
</style>
<div class="pure-g status">
<div class="pure-u-1-4">
<div class="block pure-g">
<div class="pure-u-2-5">
<div class="img-con">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Docker icon</title>
<path
d="M4.82 17.275c-.684 0-1.304-.56-1.304-1.24s.56-1.243 1.305-1.243c.748 0 1.31.56 1.31 1.242s-.622 1.24-1.305 1.24zm16.012-6.763c-.135-.992-.75-1.8-1.56-2.42l-.315-.25-.254.31c-.494.56-.69 1.553-.63 2.295.06.562.24 1.12.554 1.554-.254.13-.568.25-.81.377-.57.187-1.124.25-1.68.25H.097l-.06.37c-.12 1.182.06 2.42.562 3.54l.244.435v.06c1.5 2.483 4.17 3.6 7.078 3.6 5.594 0 10.182-2.42 12.357-7.633 1.425.062 2.864-.31 3.54-1.676l.18-.31-.3-.187c-.81-.494-1.92-.56-2.85-.31l-.018.002zm-8.008-.992h-2.428v2.42h2.43V9.518l-.002.003zm0-3.043h-2.428v2.42h2.43V6.48l-.002-.003zm0-3.104h-2.428v2.42h2.43v-2.42h-.002zm2.97 6.147H13.38v2.42h2.42V9.518l-.007.003zm-8.998 0H4.383v2.42h2.422V9.518l-.01.003zm3.03 0h-2.4v2.42H9.84V9.518l-.015.003zm-6.03 0H1.4v2.42h2.428V9.518l-.03.003zm6.03-3.043h-2.4v2.42H9.84V6.48l-.015-.003zm-3.045 0H4.387v2.42H6.8V6.48l-.016-.003z" />
</svg>
</div>
</div>
<div class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Containers%></h4>
<h4 style="text-align: right;">
<%- if self.containers_total ~= "-" then -%><a href="/cgi-bin/luci/admin/docker/containers"><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.containers_running%></span>
<span style="font-size: 1rem; color: #8898aa !important;">/<%=self.containers_total%></span>
<%- if self.containers_total ~= "-" then -%></a><%- end -%>
</h4>
</div>
</div>
</div>
<div class="pure-u-1-4">
<div class="block pure-g">
<div class="pure-u-2-5">
<div class="img-con">
<svg id="icon-hub" viewBox="0 0 42 38" stroke-width="2" fill-rule="nonzero" width="100%" height="100%">
<path
d="M37.176371,36.2324812 C37.1920117,36.8041095 36.7372743,37.270685 36.1684891,37.270685 L3.74335204,37.2703476 C3.17827583,37.2703476 2.72400056,36.8091818 2.72400056,36.2397767 L2.72400056,19.6131383 C1.4312007,18.4881431 0.662551336,16.8884326 0.662551336,15.1618249 L0.664207893,14.69503 C0.63774183,14.4532127 0.650524255,14.2942438 0.711604827,14.1238231 L5.10793246,1.20935468 C5.24853286,0.797020623 5.63848594,0.511627907 6.06681069,0.511627907 L34.0728364,0.511627907 C34.5091607,0.511627907 34.889927,0.793578201 35.0316653,1.20921034 L39.4428567,14.1234095 C39.4871296,14.273204 39.5020782,14.4249444 39.4884726,14.5493649 L39.4884726,15.1505835 C39.4884726,16.9959517 38.6190601,18.6883031 37.1764746,19.7563084 L37.176371,36.2324812 Z M35.1376208,35.209311 L35.1376208,20.7057152 C34.7023924,20.8097593 34.271333,20.8633641 33.8336069,20.8633641 C32.0046019,20.8633641 30.3013756,19.9547008 29.2437221,18.4771538 C28.1860473,19.954695 26.4828515,20.8633641 24.6538444,20.8633641 C22.824803,20.8633641 21.1216155,19.9547157 20.0639591,18.4771544 C19.0062842,19.9546953 17.3030887,20.8633641 15.4740818,20.8633641 C13.6450404,20.8633641 11.9418529,19.9547157 10.8841965,18.4771544 C9.82652161,19.9546953 8.12332608,20.8633641 6.29431919,20.8633641 C5.76735555,20.8633641 5.24095778,20.7883418 4.73973398,20.644674 L4.73973398,35.209311 L35.1376208,35.209311 Z M30.2720226,15.6557626 C30.5154632,17.4501192 32.0503909,18.8018554 33.845083,18.8018554 C35.7286794,18.8018554 37.285413,17.3395134 37.4474599,15.4751932 L30.2280765,15.4751932 C30.2470638,15.532987 30.2617919,15.5932958 30.2720226,15.6557626 Z M21.0484306,15.4751932 C21.0674179,15.532987 21.0821459,15.5932958 21.0923767,15.6557626 C21.3358173,17.4501192 22.8707449,18.8018554 24.665437,18.8018554 C26.4601001,18.8018554 27.9950169,17.4501481 28.2378191,15.6611556 C28.2451225,15.5981318 28.2590045,15.5358056 28.2787375,15.4751932 L21.0484306,15.4751932 Z M11.9238102,15.6557626 C12.1672508,17.4501192 13.7021785,18.8018554 15.4968705,18.8018554 C17.2915336,18.8018554 18.8264505,17.4501481 19.0692526,15.6611556 C19.0765561,15.5981318 19.0904381,15.5358056 19.110171,15.4751932 L11.8798641,15.4751932 C11.8988514,15.532987 11.9135795,15.5932958 11.9238102,15.6557626 Z M6.31682805,18.8018317 C8.11149114,18.8018317 9.64640798,17.4501244 9.88921012,15.6611319 C9.89651357,15.5981081 9.91039559,15.5357819 9.93012856,15.4751696 L2.70318796,15.4751696 C2.86612006,17.3346852 4.42809696,18.8018317 6.31682805,18.8018317 Z M3.09670082,13.4139924 L37.04257,13.4139924 L33.3489482,2.57204736 L6.80119239,2.57204736 L3.09670082,13.4139924 Z"
id="Fill-1"></path>
<rect id="Rectangle-3" x="14" y="26" width="6" height="10"></rect>
<path d="M20,26 L20,36 L26,36 L26,26 L20,26 Z" id="Rectangle-3"></path>
</svg>
</div>
</div>
<div class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Images%></h4>
<h4 style="text-align: right;">
<%- if self.images_total ~= "-" then -%><a href="/cgi-bin/luci/admin/docker/images"><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.images_total%></span>
<!-- <span style="font-size: 1rem; color: #8898aa !important;">/<%=self.images_total%></span> -->
<%- if self.images_total ~= "-" then -%></a><%- end -%>
</h4>
</div>
</div>
</div>
<div class="pure-u-1-4">
<div class="block pure-g">
<div class="pure-u-2-5">
<div class="img-con">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" width="48.723px" height="48.723px" viewBox="0 0 48.723 48.723"
style="enable-background:new 0 0 48.723 48.723;" xml:space="preserve">
<g>
<g id="_x33_8_89_">
<g>
<path d="M7.452,24.152h3.435v5.701h0.633c0.001,0,0.001,0,0.002,0h0.636v-5.701h3.51v-1.059h17.124v1.104h3.178v5.656h0.619
c0,0,0,0,0.002,0h0.619v-5.656h3.736v-0.856c0-0.012,0.006-0.021,0.006-0.032c0-0.072,0-0.143,0-0.215h5.721v-1.316h-5.721
c0-0.054,0-0.108,0-0.164c0-0.011-0.006-0.021-0.006-0.032v-0.832h-8.154v1.028h-7.911v-2.652h-0.689c-0.001,0-0.001,0-0.002,0
h-0.678v2.652h-7.846v-1.104H7.452v1.104H1.114v1.316h6.338V24.152z" />
<path
d="M21.484,16.849h5.204v-2.611h7.133V1.555H14.588v12.683h6.896V16.849z M16.537,12.288V3.505h15.335v8.783H16.537z" />
<rect x="18.682" y="16.898" width="10.809" height="0.537" />
<path
d="M0,43.971h6.896v2.611H12.1v-2.611h7.134V31.287H0V43.971z M1.95,33.236h15.334v8.785H1.95V33.236z" />
<rect x="4.095" y="46.631" width="10.808" height="0.537" />
<path
d="M29.491,30.994v12.684h6.895v2.611h5.205v-2.611h7.133V30.994H29.491z M46.774,41.729H31.44v-8.783h15.334V41.729z" />
<rect x="33.584" y="46.338" width="10.809" height="0.537" />
</g>
</g>
</g>
</svg>
</div>
</div>
<div class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Networks%></h4>
<h4 style="text-align: right;">
<%- if self.networks_total ~= "-" then -%><a href="/cgi-bin/luci/admin/docker/networks"><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.networks_total%></span>
<!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
<%- if self.networks_total ~= "-" then -%></a><%- end -%>
</h4>
</div>
</div>
</div>
<div class="pure-u-1-4">
<div class="block pure-g">
<div class="pure-u-2-5">
<div class="img-con">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" viewBox="0 0 55 55" style="enable-background:new 0 0 55 55;" xml:space="preserve">
<path
d="M52.354,8.51C51.196,4.22,42.577,0,27.5,0C12.423,0,3.803,4.22,2.646,8.51C2.562,8.657,2.5,8.818,2.5,9v0.5V21v0.5V22v11
v0.5V34v12c0,0.162,0.043,0.315,0.117,0.451C3.798,51.346,14.364,55,27.5,55c13.106,0,23.655-3.639,24.875-8.516
C52.455,46.341,52.5,46.176,52.5,46V34v-0.5V33V22v-0.5V21V9.5V9C52.5,8.818,52.438,8.657,52.354,8.51z M50.421,33.985
c-0.028,0.121-0.067,0.241-0.116,0.363c-0.04,0.099-0.089,0.198-0.143,0.297c-0.067,0.123-0.142,0.246-0.231,0.369
c-0.066,0.093-0.141,0.185-0.219,0.277c-0.111,0.131-0.229,0.262-0.363,0.392c-0.081,0.079-0.17,0.157-0.26,0.236
c-0.164,0.143-0.335,0.285-0.526,0.426c-0.082,0.061-0.17,0.12-0.257,0.18c-0.226,0.156-0.462,0.311-0.721,0.463
c-0.068,0.041-0.141,0.08-0.212,0.12c-0.298,0.168-0.609,0.335-0.945,0.497c-0.043,0.021-0.088,0.041-0.132,0.061
c-0.375,0.177-0.767,0.351-1.186,0.519c-0.012,0.005-0.024,0.009-0.036,0.014c-2.271,0.907-5.176,1.67-8.561,2.17
c-0.017,0.002-0.034,0.004-0.051,0.007c-0.658,0.097-1.333,0.183-2.026,0.259c-0.113,0.012-0.232,0.02-0.346,0.032
c-0.605,0.063-1.217,0.121-1.847,0.167c-0.288,0.021-0.59,0.031-0.883,0.049c-0.474,0.028-0.943,0.059-1.429,0.076
C29.137,40.984,28.327,41,27.5,41s-1.637-0.016-2.432-0.044c-0.486-0.017-0.955-0.049-1.429-0.076
c-0.293-0.017-0.595-0.028-0.883-0.049c-0.63-0.046-1.242-0.104-1.847-0.167c-0.114-0.012-0.233-0.02-0.346-0.032
c-0.693-0.076-1.368-0.163-2.026-0.259c-0.017-0.002-0.034-0.004-0.051-0.007c-3.385-0.5-6.29-1.263-8.561-2.17
c-0.012-0.004-0.024-0.009-0.036-0.014c-0.419-0.168-0.812-0.342-1.186-0.519c-0.043-0.021-0.089-0.041-0.132-0.061
c-0.336-0.162-0.647-0.328-0.945-0.497c-0.07-0.04-0.144-0.079-0.212-0.12c-0.259-0.152-0.495-0.307-0.721-0.463
c-0.086-0.06-0.175-0.119-0.257-0.18c-0.191-0.141-0.362-0.283-0.526-0.426c-0.089-0.078-0.179-0.156-0.26-0.236
c-0.134-0.13-0.252-0.26-0.363-0.392c-0.078-0.092-0.153-0.184-0.219-0.277c-0.088-0.123-0.163-0.246-0.231-0.369
c-0.054-0.099-0.102-0.198-0.143-0.297c-0.049-0.121-0.088-0.242-0.116-0.363C4.541,33.823,4.5,33.661,4.5,33.5
c0-0.113,0.013-0.226,0.031-0.338c0.025-0.151,0.011-0.302-0.031-0.445v-7.424c0.028,0.026,0.063,0.051,0.092,0.077
c0.218,0.192,0.44,0.383,0.69,0.567C9.049,28.786,16.582,31,27.5,31c10.872,0,18.386-2.196,22.169-5.028
c0.302-0.22,0.574-0.447,0.83-0.678l0.001-0.001v7.424c-0.042,0.143-0.056,0.294-0.031,0.445c0.019,0.112,0.031,0.225,0.031,0.338
C50.5,33.661,50.459,33.823,50.421,33.985z M50.5,13.293v7.424c-0.042,0.143-0.056,0.294-0.031,0.445
c0.019,0.112,0.031,0.225,0.031,0.338c0,0.161-0.041,0.323-0.079,0.485c-0.028,0.121-0.067,0.241-0.116,0.363
c-0.04,0.099-0.089,0.198-0.143,0.297c-0.067,0.123-0.142,0.246-0.231,0.369c-0.066,0.093-0.141,0.185-0.219,0.277
c-0.111,0.131-0.229,0.262-0.363,0.392c-0.081,0.079-0.17,0.157-0.26,0.236c-0.164,0.143-0.335,0.285-0.526,0.426
c-0.082,0.061-0.17,0.12-0.257,0.18c-0.226,0.156-0.462,0.311-0.721,0.463c-0.068,0.041-0.141,0.08-0.212,0.12
c-0.298,0.168-0.609,0.335-0.945,0.497c-0.043,0.021-0.088,0.041-0.132,0.061c-0.375,0.177-0.767,0.351-1.186,0.519
c-0.012,0.005-0.024,0.009-0.036,0.014c-2.271,0.907-5.176,1.67-8.561,2.17c-0.017,0.002-0.034,0.004-0.051,0.007
c-0.658,0.097-1.333,0.183-2.026,0.259c-0.113,0.012-0.232,0.02-0.346,0.032c-0.605,0.063-1.217,0.121-1.847,0.167
c-0.288,0.021-0.59,0.031-0.883,0.049c-0.474,0.028-0.943,0.059-1.429,0.076C29.137,28.984,28.327,29,27.5,29
s-1.637-0.016-2.432-0.044c-0.486-0.017-0.955-0.049-1.429-0.076c-0.293-0.017-0.595-0.028-0.883-0.049
c-0.63-0.046-1.242-0.104-1.847-0.167c-0.114-0.012-0.233-0.02-0.346-0.032c-0.693-0.076-1.368-0.163-2.026-0.259
c-0.017-0.002-0.034-0.004-0.051-0.007c-3.385-0.5-6.29-1.263-8.561-2.17c-0.012-0.004-0.024-0.009-0.036-0.014
c-0.419-0.168-0.812-0.342-1.186-0.519c-0.043-0.021-0.089-0.041-0.132-0.061c-0.336-0.162-0.647-0.328-0.945-0.497
c-0.07-0.04-0.144-0.079-0.212-0.12c-0.259-0.152-0.495-0.307-0.721-0.463c-0.086-0.06-0.175-0.119-0.257-0.18
c-0.191-0.141-0.362-0.283-0.526-0.426c-0.089-0.078-0.179-0.156-0.26-0.236c-0.134-0.13-0.252-0.26-0.363-0.392
c-0.078-0.092-0.153-0.184-0.219-0.277c-0.088-0.123-0.163-0.246-0.231-0.369c-0.054-0.099-0.102-0.198-0.143-0.297
c-0.049-0.121-0.088-0.242-0.116-0.363C4.541,21.823,4.5,21.661,4.5,21.5c0-0.113,0.013-0.226,0.031-0.338
c0.025-0.151,0.011-0.302-0.031-0.445v-7.424c0.12,0.109,0.257,0.216,0.387,0.324c0.072,0.06,0.139,0.12,0.215,0.18
c0.3,0.236,0.624,0.469,0.975,0.696c0.073,0.047,0.155,0.093,0.231,0.14c0.294,0.183,0.605,0.362,0.932,0.538
c0.121,0.065,0.242,0.129,0.367,0.193c0.365,0.186,0.748,0.367,1.151,0.542c0.066,0.029,0.126,0.059,0.193,0.087
c0.469,0.199,0.967,0.389,1.485,0.573c0.143,0.051,0.293,0.099,0.44,0.149c0.412,0.139,0.838,0.272,1.279,0.401
c0.159,0.046,0.315,0.094,0.478,0.138c0.585,0.162,1.189,0.316,1.823,0.458c0.087,0.02,0.181,0.036,0.269,0.055
c0.559,0.122,1.139,0.235,1.735,0.341c0.202,0.036,0.407,0.07,0.613,0.104c0.567,0.093,1.151,0.178,1.75,0.256
c0.154,0.02,0.301,0.043,0.457,0.062c0.744,0.09,1.514,0.167,2.305,0.233c0.195,0.016,0.398,0.028,0.596,0.042
c0.633,0.046,1.28,0.084,1.942,0.114c0.241,0.011,0.481,0.022,0.727,0.031C25.712,18.979,26.59,19,27.5,19s1.788-0.021,2.65-0.05
c0.245-0.009,0.485-0.02,0.727-0.031c0.662-0.03,1.309-0.068,1.942-0.114c0.198-0.015,0.4-0.026,0.596-0.042
c0.791-0.065,1.561-0.143,2.305-0.233c0.156-0.019,0.303-0.042,0.457-0.062c0.599-0.078,1.182-0.163,1.75-0.256
c0.206-0.034,0.411-0.068,0.613-0.104c0.596-0.106,1.176-0.219,1.735-0.341c0.088-0.019,0.182-0.036,0.269-0.055
c0.634-0.142,1.238-0.297,1.823-0.458c0.163-0.045,0.319-0.092,0.478-0.138c0.441-0.129,0.867-0.262,1.279-0.401
c0.147-0.05,0.297-0.098,0.44-0.149c0.518-0.184,1.017-0.374,1.485-0.573c0.067-0.028,0.127-0.058,0.193-0.087
c0.403-0.176,0.786-0.356,1.151-0.542c0.125-0.064,0.247-0.128,0.367-0.193c0.327-0.175,0.638-0.354,0.932-0.538
c0.076-0.047,0.158-0.093,0.231-0.14c0.351-0.227,0.675-0.459,0.975-0.696c0.075-0.06,0.142-0.12,0.215-0.18
C50.243,13.509,50.38,13.402,50.5,13.293z M27.5,2c13.555,0,23,3.952,23,7.5s-9.445,7.5-23,7.5s-23-3.952-23-7.5S13.945,2,27.5,2z
M50.5,45.703c-0.014,0.044-0.024,0.089-0.032,0.135C49.901,49.297,40.536,53,27.5,53S5.099,49.297,4.532,45.838
c-0.008-0.045-0.019-0.089-0.032-0.131v-8.414c0.028,0.026,0.063,0.051,0.092,0.077c0.218,0.192,0.44,0.383,0.69,0.567
C9.049,40.786,16.582,43,27.5,43c10.872,0,18.386-2.196,22.169-5.028c0.302-0.22,0.574-0.447,0.83-0.678l0.001-0.001V45.703z" />
</svg>
</div>
</div>
<div class="pure-u-3-5">
<h4 style="text-align: right; font-size: 1rem"><%:Volumes%></h4>
<h4 style="text-align: right;">
<%- if self.volumes_total ~= "-" then -%><a href="/cgi-bin/luci/admin/docker/volumes"><%- end -%>
<span style="font-size: 2rem; color: #2dce89;"><%=self.volumes_total%></span>
<!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
<%- if self.volumes_total ~= "-" then -%></a><%- end -%>
</h4>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,19 @@
<%+cbi/valueheader%>
<script type="text/javascript">
function resolv_container(op) {
let s = document.getElementById(op + '-status');
if (!s) return;
let cmd_line = prompt("Plese input <docker create> command line:", "");
if (cmd_line == null || cmd_line == "") {
s.innerHTML = "<font color='red'>Canceled</font>";
return;
}
let re = /\/admin\/docker\/newcontainer\//
let p = window.location.href
let path = p.split(re)
window.location = path[0] + "/admin/docker/newcontainer/" + encodeURI(cmd_line)
}
</script>
<input type="button" class="cbi-button cbi-button-apply" value="Command line" onclick="resolv_container('<%=self.option%>')" />
<span id="<%=self.option%>-status"></span>
<%+cbi/valuefooter%>