diff --git a/package/lean/luci-app-docker/Makefile b/package/lean/luci-app-docker/Makefile deleted file mode 100755 index 686e4d627..000000000 --- a/package/lean/luci-app-docker/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright (C) 2008-2014 The LuCI Team -# -# This is free software, licensed under the Apache License, Version 2.0 . -# - -include $(TOPDIR)/rules.mk - -LUCI_TITLE:=Luci for Docker-CE -LUCI_DEPENDS:=+docker-ce +e2fsprogs +fdisk -LUCI_PKGARCH:=all -PKG_VERSION:=1 -PKG_RELEASE:=9 - -include $(TOPDIR)/feeds/luci/luci.mk - -# call BuildPackage - OpenWrt buildroot signature - diff --git a/package/lean/luci-app-docker/luasrc/controller/docker.lua b/package/lean/luci-app-docker/luasrc/controller/docker.lua deleted file mode 100644 index 93b8f6618..000000000 --- a/package/lean/luci-app-docker/luasrc/controller/docker.lua +++ /dev/null @@ -1,17 +0,0 @@ -module("luci.controller.docker", package.seeall) - -function index() - if not nixio.fs.access("/etc/config/dockerd") then - return - end - - entry({"admin", "services", "docker"}, cbi("docker"), _("Docker CE Container"), 199).dependent = true - entry({"admin","services","docker","status"},call("act_status")).leaf=true -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/lean/luci-app-docker/luasrc/model/cbi/docker.lua b/package/lean/luci-app-docker/luasrc/model/cbi/docker.lua deleted file mode 100644 index 270fecbbc..000000000 --- a/package/lean/luci-app-docker/luasrc/model/cbi/docker.lua +++ /dev/null @@ -1,23 +0,0 @@ -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").."") - -return m \ No newline at end of file diff --git a/package/lean/luci-app-docker/luasrc/view/docker/docker_status.htm b/package/lean/luci-app-docker/luasrc/view/docker/docker_status.htm deleted file mode 100644 index 4d2c71468..000000000 --- a/package/lean/luci-app-docker/luasrc/view/docker/docker_status.htm +++ /dev/null @@ -1,22 +0,0 @@ - - -
-

- <%:Collecting data...%> -

-
\ No newline at end of file diff --git a/package/lean/luci-app-docker/po/zh-cn/docker.po b/package/lean/luci-app-docker/po/zh-cn/docker.po deleted file mode 100644 index b95369aac..000000000 --- a/package/lean/luci-app-docker/po/zh-cn/docker.po +++ /dev/null @@ -1,39 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: Luci ARP Bind\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-06-23 20:16+0800\n" -"PO-Revision-Date: 2015-06-23 20:17+0800\n" -"Last-Translator: coolsnowwolf \n" -"Language-Team: PandoraBox Team\n" -"Language: zh_CN\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Poedit 1.8.1\n" -"X-Poedit-SourceCharset: UTF-8\n" - -msgid "Docker CE Container" -msgstr "Docker CE 容器" - -msgid "Open Portainer Docker Admin" -msgstr "打开 Portainer Docker 管理页面" - -msgid "Docker is a set of platform-as-a-service (PaaS) products that use OS-level virtualization to deliver software in packages called containers." -msgstr "Docker是一组平台即服务(platform-as-a-service,PaaS)产品,它使用操作系统级容器虚拟化来交付软件包。" - -msgid "Enable WAN access Dokcer" -msgstr "允许 WAN 访问 Dokcer" - -msgid "Enable WAN access docker mapped ports" -msgstr "允许 WAN 访问 Dokcer 映射后的端口(易受攻击!)。

推荐禁用该选项后,用系统防火墙选择性映射 172.17.0.X:XX 端口到 WAN" - -msgid "Docker Readme First" -msgstr "Docker 初始化无脑配置教程" - -msgid "Download DockerReadme.pdf" -msgstr "下载 Docker 初始化无脑配置教程" - -msgid "Please download DockerReadme.pdf to read when first-running" -msgstr "初次在OpenWrt中运行Docker必读(只需执行一次流程)" \ No newline at end of file diff --git a/package/lean/luci-app-docker/root/etc/config/dockerd b/package/lean/luci-app-docker/root/etc/config/dockerd deleted file mode 100644 index b73c0a3e3..000000000 --- a/package/lean/luci-app-docker/root/etc/config/dockerd +++ /dev/null @@ -1,4 +0,0 @@ - -config docker - option wan_mode '0' - diff --git a/package/lean/luci-app-docker/root/etc/docker-web b/package/lean/luci-app-docker/root/etc/docker-web deleted file mode 100755 index 010ab1eac..000000000 --- a/package/lean/luci-app-docker/root/etc/docker-web +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -docker run -d --restart=always --name="portainer" -p 9999:9000 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer \ No newline at end of file diff --git a/package/lean/luci-app-docker/root/etc/init.d/dockerd b/package/lean/luci-app-docker/root/etc/init.d/dockerd deleted file mode 100755 index 5d6f36bf2..000000000 --- a/package/lean/luci-app-docker/root/etc/init.d/dockerd +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh /etc/rc.common - -USE_PROCD=1 -START=25 - -start_service() { - local nofile=$(cat /proc/sys/fs/nr_open) - local wanmode=$(uci get dockerd.@docker[0].wan_mode) - - if [ $wanmode = "1" ] ;then - dockerwan=" " - else - dockerwan="--iptables=false" - fi - - procd_open_instance - procd_set_param stderr 1 - procd_set_param command /usr/bin/dockerd $dockerwan - procd_set_param limits nofile="${nofile} ${nofile}" - procd_close_instance - -} diff --git a/package/lean/luci-app-docker/root/etc/uci-defaults/docker b/package/lean/luci-app-docker/root/etc/uci-defaults/docker deleted file mode 100755 index e03f47783..000000000 --- a/package/lean/luci-app-docker/root/etc/uci-defaults/docker +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -uci -q batch <<-EOF >/dev/null - delete ucitrack.@dockerd[-1] - add ucitrack dockerd - set ucitrack.@dockerd[-1].init=dockerd - commit ucitrack -EOF - -rm -f /tmp/luci-indexcache -exit 0 diff --git a/package/lean/luci-app-docker/root/www/DockerReadme.pdf b/package/lean/luci-app-docker/root/www/DockerReadme.pdf deleted file mode 100644 index 8a9a94b83..000000000 Binary files a/package/lean/luci-app-docker/root/www/DockerReadme.pdf and /dev/null differ diff --git a/package/lean/luci-app-dockerman/Makefile b/package/lean/luci-app-dockerman/Makefile new file mode 100644 index 000000000..75f17a547 --- /dev/null +++ b/package/lean/luci-app-dockerman/Makefile @@ -0,0 +1,45 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-dockerman +PKG_VERSION:=v0.2.2 +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 +docker-ce +e2fsprogs +fdisk +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)/ + $(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n + po2lmo ./po/zh-cn/dockerman.po $(1)/usr/lib/lua/luci/i18n/dockerman.zh-cn.lmo +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/package/lean/luci-app-dockerman/luasrc/controller/dockerman.lua b/package/lean/luci-app-dockerman/luasrc/controller/dockerman.lua new file mode 100644 index 000000000..794224f0a --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/controller/dockerman.lua @@ -0,0 +1,153 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- +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", "services","docker"}, firstchild(), "Docker", 40).dependent = false + entry({"admin","services","docker","overview"},cbi("docker/overview"),_("Overview"),0).leaf=true + + 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","services","docker","containers"},form("docker/containers"),_("Containers"),1).leaf=true + entry({"admin","services","docker","images"},form("docker/images"),_("Images"),2).leaf=true + entry({"admin","services","docker","networks"},form("docker/networks"),_("Networks"),3).leaf=true + entry({"admin","services","docker","volumes"},form("docker/volumes"),_("Volumes"),4).leaf=true + entry({"admin","services","docker","events"},call("action_events"),_("Events"),5) + entry({"admin","services","docker","newcontainer"},form("docker/newcontainer")).leaf=true + entry({"admin","services","docker","newnetwork"},form("docker/newnetwork")).leaf=true + entry({"admin","services","docker","container"},form("docker/container")).leaf=true + entry({"admin","services","docker","container_stats"},call("action_get_container_stats")).leaf=true + entry({"admin","services","docker","confirm"},call("action_confirm")).leaf=true + +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 diff --git a/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/container.lua b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/container.lua new file mode 100644 index 000000000..d27c52f95 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/container.lua @@ -0,0 +1,482 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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.HostConfig and d.HostConfig.PortBindings then + for inter, out in pairs(d.HostConfig.PortBindings) 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 + local v_sorce_d, v_dest_d + local v_sorce = "" + local v_dest = "" + for v_sorce_d in v["Source"]:gmatch('[^/]+') do + if v_sorce_d and #v_sorce_d > 12 then + v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,12) .. "..." + else + v_sorce = v_sorce .."/".. v_sorce_d + end + end + for v_dest_d in v["Destination"]:gmatch('[^/]+') do + if v_dest_d and #v_dest_d > 12 then + v_dest = v_dest .. "/" .. v_dest_d:sub(1,12) .. "..." + else + v_dest = v_dest .."/".. v_dest_d + end + end + data = (data and (data .. "
") or "") .. v_sorce .. ":" .. v["Destination"] .. (v["Mode"] ~= "" and (":" .. v["Mode"]) or "") + end + end + return data +end + +local get_device = function(d) + local data + if d.HostConfig and d.HostConfig.Devices then + for _,v in ipairs(d.HostConfig.Devices) do + data = (data and (data .. "
") or "") .. v["PathOnHost"] .. ":" .. v["PathInContainer"] .. (v["CgroupPermissions"] ~= "" and (":" .. v["CgroupPermissions"]) or "") + end + end + return data +end + +local get_links = function(d) + local data + if d.HostConfig and d.HostConfig.Links then + for _,v in ipairs(d.HostConfig.Links) do + data = (data and (data .. "
") 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 + 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)) + luci.http.redirect(luci.dispatcher.build_url("admin/services/docker/container/"..container_id)) + else + docker:clear_status() + if cmd ~= "remove" and cmd ~= "upgrade" then + luci.http.redirect(luci.dispatcher.build_url("admin/services/docker/container/"..container_id)) + else + luci.http.redirect(luci.dispatcher.build_url("admin/services/docker/containers")) + end + end +end + +m=SimpleForm("docker", container_info.Name:sub(2), translate("Docker Container") ) +m.template = "docker/cbi/xsimpleform" +m.redirect = luci.dispatcher.build_url("admin/services/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.err then docker:clear_status() end + + +action_section = m:section(Table,{{}}) +action_section.notitle=true +action_section.rowcolors=false +action_section.template="cbi/nullsection" + +btnstart=action_section:option(Button, "_start") +btnstart.template="docker/cbi/inlinebutton" +btnstart.inputtitle=translate("Start") +btnstart.inputstyle = "apply" +btnstart.forcewrite = true +btnrestart=action_section:option(Button, "_restart") +btnrestart.template="docker/cbi/inlinebutton" +btnrestart.inputtitle=translate("Restart") +btnrestart.inputstyle = "reload" +btnrestart.forcewrite = true +btnstop=action_section:option(Button, "_stop") +btnstop.template="docker/cbi/inlinebutton" +btnstop.inputtitle=translate("Stop") +btnstop.inputstyle = "reset" +btnstop.forcewrite = true +btnupgrade=action_section:option(Button, "_upgrade") +btnupgrade.template="docker/cbi/inlinebutton" +btnupgrade.inputtitle=translate("Upgrade") +btnupgrade.inputstyle = "reload" +btnstop.forcewrite = true +btnduplicate=action_section:option(Button, "_duplicate") +btnduplicate.template="docker/cbi/inlinebutton" +btnduplicate.inputtitle=translate("Duplicate") +btnduplicate.inputstyle = "add" +btnstop.forcewrite = true +btnremove=action_section:option(Button, "_remove") +btnremove.template="docker/cbi/inlinebutton" +btnremove.inputtitle=translate("Remove") +btnremove.inputstyle = "remove" +btnremove.forcewrite = true + +btnstart.write = function(self, section) + start_stop_remove(m,"start") +end +btnrestart.write = function(self, section) + start_stop_remove(m,"restart") +end +btnupgrade.write = function(self, section) + start_stop_remove(m,"upgrade") +end +btnremove.write = function(self, section) + start_stop_remove(m,"remove") +end +btnstop.write = function(self, section) + start_stop_remove(m,"stop") +end +btnduplicate.write = function(self, section) + luci.http.redirect(luci.dispatcher.build_url("admin/services/docker/newcontainer/duplicate/"..container_id)) +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["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 "-"} + 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 = "docker/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/services/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/services/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=translate("CPU Useage"),value='-'},memory={key=translate("Memory Useage"),value='-'}} + stat_section = m:section(Table, table_stats, translate("Stats")) + stat_section:option(DummyValue, "key", translate("Stats")).width="33%" + + stat_section:option(DummyValue, "value") + top_section= m:section(Table, container_top.Processes, translate("TOP")) + for i, v in ipairs(container_top.Titles) do + top_section:option(DummyValue, i, translate(v)) + end +end +m.submit = false +m.reset = false +end + + +return m diff --git a/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/containers.lua b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/containers.lua new file mode 100644 index 000000000..dc4742297 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/containers.lua @@ -0,0 +1,198 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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.template = "docker/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.err then docker:clear_status() end + +c_table = m:section(Table, c_lists, translate("Containers")) +c_table.nodescr=true +-- v.template = "cbi/tblsection" +-- v.sortable = true +container_selecter = c_table:option(Flag, "_selected","") +container_selecter.disabled = 0 +container_selecter.enabled = 1 +container_selecter.default = 0 + +container_id = c_table:option(DummyValue, "_id", translate("ID")) +container_id.width="10%" +container_name = c_table:option(DummyValue, "_name", translate("Container Name")) +container_name.width="20%" +container_name.template="docker/cbi/dummyvalue" +container_name.href = function (self, section) + return luci.dispatcher.build_url("admin/services/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="docker/cbi/dummyvalue" +container_image.width="10%" +-- container_image.href = function (self, section) +-- return luci.dispatcher.build_url("admin/services/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/services/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="docker/cbi/inlinebutton" +btnnew.inputstyle = "add" +btnnew.forcewrite = true +btnstart=action_section:option(Button, "_start") +btnstart.template="docker/cbi/inlinebutton" +btnstart.inputtitle=translate("Start") +btnstart.inputstyle = "apply" +btnstart.forcewrite = true +btnrestart=action_section:option(Button, "_restart") +btnrestart.template="docker/cbi/inlinebutton" +btnrestart.inputtitle=translate("Restart") +btnrestart.inputstyle = "reload" +btnrestart.forcewrite = true +btnstop=action_section:option(Button, "_stop") +btnstop.template="docker/cbi/inlinebutton" +btnstop.inputtitle=translate("Stop") +btnstop.inputstyle = "reset" +btnstop.forcewrite = true +btnremove=action_section:option(Button, "_remove") +btnremove.template="docker/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/services/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 diff --git a/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/images.lua b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/images.lua new file mode 100644 index 000000000..9935f3fa8 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/images.lua @@ -0,0 +1,173 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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 + for i, v1 in ipairs(v.RepoTags) do + data[index]["_tags"] =(data[index]["_tags"] and ( data[index]["_tags"] .. "" )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).."" + 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.template = "docker/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, translate("Pull Image")) +pull_section.template="cbi/nullsection" +local tag_name = pull_section:option(Value, "_image_tag_name") +tag_name.template="docker/cbi/inlinevalue" +tag_name.placeholder="hello-world:latest" +local registry = pull_section:option(Value, "_registry") +registry.template="docker/cbi/inlinevalue" +registry:value("index.docker.io", "DockerHub") +local action_pull = pull_section:option(Button, "_pull") +action_pull.inputtitle= translate("Pull") +action_pull.template="docker/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/services/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")).rawhtml = true +image_table:option(DummyValue, "_containers", translate("Containers")).rawhtml = true +image_table:option(DummyValue, "_size", translate("Size")) +image_table:option(DummyValue, "_created", translate("Created")) +image_selecter.write = function(self, section, value) + image_list[section]._selected = value +end + +local remove_action = function(force) + local image_selected = {} + -- 遍历table中sectionid + local image_table_sids = image_table:cfgsections() + for _, image_table_sid in ipairs(image_table_sids) do + -- 得到选中项的名字 + if image_list[image_table_sid]._selected == 1 then + image_selected[#image_selected+1] = image_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 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).. "
") + success = false + else + docker:append_status("done
") + end + end + if success then docker:clear_status() end + luci.http.redirect(luci.dispatcher.build_url("admin/services/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.err then docker:clear_status() end + +action = m:section(Table,{{}}) +action.notitle=true +action.rowcolors=false +action.template="cbi/nullsection" + +btnremove = action:option(Button, "remove") +btnremove.inputtitle= translate("Remove") +btnremove.template="docker/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="docker/cbi/inlinebutton" +btnforceremove.inputstyle = "remove" +btnforceremove.forcewrite = true +btnforceremove.write = function(self, section) + remove_action(true) +end +return m diff --git a/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/networks.lua b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/networks.lua new file mode 100644 index 000000000..9337e96b3 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/networks.lua @@ -0,0 +1,123 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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.template = "docker/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_selecter.template = "docker/cbi/xfvalue" +network_id = network_table:option(DummyValue, "_id", translate("ID")) +network_selecter.disabled = 0 +network_selecter.enabled = 1 +network_selecter.default = 0 +network_selecter.render = function(self, section, scope) + self.disable = 0 + if network_list[section]["_name"] == "bridge" or network_list[section]["_name"] == "none" or network_list[section]["_name"] == "host" then + self.disable = 1 + end + Flag.render(self, section, scope) +end + +network_name = network_table:option(DummyValue, "_name", translate("Network Name")) +network_driver = network_table:option(DummyValue, "_driver", translate("Driver")) +network_interface = network_table:option(DummyValue, "_interface", translate("Parent Interface")) +network_subnet = network_table:option(DummyValue, "_subnet", translate("Subnet")) +network_gateway = network_table:option(DummyValue, "_gateway", translate("Gateway")) + +network_selecter.write = function(self, section, value) + network_list[section]._selected = value +end + +docker_status = m:section(SimpleSection) +docker_status.template="docker/apply_widget" +docker_status.err=nixio.fs.readfile(dk.options.status_path) +if docker_status.err then docker:clear_status() end + +action = m:section(Table,{{}}) +action.notitle=true +action.rowcolors=false +action.template="cbi/nullsection" +btnnew=action:option(Button, "_new") +btnnew.inputtitle= translate("New") +btnnew.template="docker/cbi/inlinebutton" +btnnew.notitle=true +btnnew.inputstyle = "add" +btnnew.forcewrite = true +btnnew.write = function(self, section) + luci.http.redirect(luci.dispatcher.build_url("admin/services/docker/newnetwork")) +end +btnremove = action:option(Button, "_remove") +btnremove.inputtitle= translate("Remove") +btnremove.template="docker/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/services/docker/networks")) + end +end + +return m diff --git a/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/newcontainer.lua b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/newcontainer.lua new file mode 100644 index 000000000..6e2cc689d --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/newcontainer.lua @@ -0,0 +1,579 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +require "luci.util" +require "math" +local uci = luci.model.uci.cursor() +local docker = require "luci.model.docker" +local dk = docker.new() +local cmd_line = table.concat(arg, '/') +local create_body = {} + +local images = dk.images:list().body +local networks = dk.networks:list().body +local containers = dk.containers:list(nil, {all=true}).body + +local is_quot_complete = function(str) + if not str then return true end + local num = 0, w + for w in str:gmatch("[\"\']") do + num = num + 1 + end + if math.fmod(num, 2) ~= 0 then + return false + else + return true + end +end + +-- reslvo default config +local default_config = { } +if cmd_line and cmd_line:match("^docker.+") then + local key = nil, _key + --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 + elseif _key then + -- there is a value that unpair quotation marks: + -- "i was a ok man" + -- now we only get: "i + if _key == "mount" or _key == "link" or _key == "env" or _key == "dns" or _key == "port" or _key == "device" or _key == "tmpfs" then + default_config[_key][#default_config[_key]] = default_config[_key][#default_config[_key]] .. " " .. w + if is_quot_complete(default_config[_key][#default_config[_key]]) then + -- clear quotation marks + default_config[_key][#default_config[_key]] = default_config[_key][#default_config[_key]]:gsub("[\"\']", "") + _key = nil + end + else + default_config[_key] = default_config[_key] .. " ".. w + if is_quot_complete(default_config[_key]) then + -- clear quotation marks + default_config[_key] = default_config[_key]:gsub("[\"\']", "") + _key = nil + end + end + -- start with '-' + elseif w:match("^%-+.+") and cursor <= 1 then + --key=value + local val + key, val = w:match("^%-+(.-)=(.+)") + -- -dit + if not key then key = w:match("^%-+(.+)") end + + if not key then + key = w:match("^%-(.+)") + if key:match("i") or key:match("t") or key:match("d") then + if key:match("i") then default_config["interactive"] = true end + if key:match("t") then default_config["tty"] = true end + -- clear key + key = nil + end + end + + if key == "v" or key == "volume" then + key = "mount" + elseif key == "p" or key == "publish" then + key = "port" + elseif key == "e" then + key = "env" + elseif key == "dns" then + key = "dns" + elseif key == "net" then + key = "network" + elseif key == "h" or key == "hostname" then + key = "hostname" + 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"] = true + key = nil + elseif key == "cap-add" then + default_config["privileged"] = true + end + --key=value + if val then + if key == "mount" or key == "link" or key == "env" or key == "dns" 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], val ) + -- clear quotation marks + default_config[key][#default_config[key]] = default_config[key][#default_config[key]]:gsub("[\"\']", "") + else + default_config[key] = val + -- clear quotation marks + default_config[key] = default_config[key]:gsub("[\"\']", "") + end + -- if there are " or ' in val and separate by space, we need keep the _key to link with next w + if is_quot_complete(val) then + _key = nil + else + _key = key + end + -- clear key + key = nil + end + cursor = 1 + -- value + elseif key and type(key) == "string" and cursor == 1 then + if key == "mount" or key == "link" or key == "env" or key == "dns" 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 ) + -- clear quotation marks + default_config[key][#default_config[key]] = default_config[key][#default_config[key]]:gsub("[\"\']", "") + else + default_config[key] = w + -- clear quotation marks + default_config[key] = default_config[key]:gsub("[\"\']", "") + 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 + -- if there are " or ' in val and separate by space, we need keep the _key to link with next w + if is_quot_complete(w) then + _key = nil + else + _key = key + 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.hostname = create_body.Hostname + default_config.tty = create_body.Tty and true or false + default_config.interactive = create_body.OpenStdin and true or false + default_config.privileged = create_body.HostConfig.Privileged and true or false + default_config.restart = create_body.HostConfig.RestartPolicy and create_body.HostConfig.RestartPolicy.name or nil + -- default_config.network = create_body.HostConfig.NetworkMode == "default" and "bridge" or create_body.HostConfig.NetworkMode + -- if container has leave original network, and add new network, .HostConfig.NetworkMode is INcorrect, so using first child of .NetworkingConfig.EndpointsConfig + default_config.network = next(create_body.NetworkingConfig.EndpointsConfig) + default_config.ip = default_config.network and default_config.network ~= "bridge" and default_config.network ~= "host" and default_config.network ~= "null" and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig and create_body.NetworkingConfig.EndpointsConfig[default_config.network].IPAMConfig.IPv4Address or nil + default_config.link = create_body.HostConfig.Links + default_config.env = create_body.Env + default_config.dns = create_body.HostConfig.Dns + default_config.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.template = "docker/cbi/xsimpleform" +m.redirect = luci.dispatcher.build_url("admin", "services","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.err then docker:clear_status() end + +local s = m:section(SimpleSection, translate("New Container")) +s.addremove = true +s.anonymous = true + +local d = s:option(DummyValue,"cmd_line", translate("Resolv CLI")) +d.rawhtml = true +d.template = "docker/resolv_container" + +d = s:option(Value, "name", translate("Container Name")) +d.rmempty = true +d.default = default_config.name or nil + +d = s:option(Flag, "interactive", translate("Interactive (-i)")) +d.rmempty = true +d.disabled = 0 +d.enabled = 1 +d.default = default_config.interactive and 1 or 0 + +d = s:option(Flag, "tty", translate("TTY (-t)")) +d.rmempty = true +d.disabled = 0 +d.enabled = 1 +d.default = default_config.tty and 1 or 0 + +d = s:option(Value, "image", translate("Docker Image")) +d.rmempty = true +d.default = default_config.image or nil +for _, v in ipairs (images) do + if v.RepoTags then + d:value(v.RepoTags[1], v.RepoTags[1]) + end +end + +d = s:option(Flag, "_force_pull", translate("Always pull image first")) +d.rmempty = true +d.disabled = 0 +d.enabled = 1 +d.default = 0 + +d = s:option(Flag, "privileged", translate("Privileged")) +d.rmempty = true +d.disabled = 0 +d.enabled = 1 +d.default = default_config.privileged and 1 or 0 + +d = s:option(ListValue, "restart", translate("Restart Policy")) +d.rmempty = true + +d:value("no", "No") +d:value("unless-stopped", "Unless stopped") +d:value("always", "Always") +d:value("on-failure", "On failure") +d.default = default_config.restart or "unless-stopped" + +local d_network = s:option(ListValue, "network", translate("Networks")) +d_network.rmempty = true +d_network.default = default_config.network or "bridge" + +local d_ip = s:option(Value, "ip", translate("IPv4 Address")) +d_ip.datatype="ip4addr" +d_ip:depends("network", "nil") +d_ip.default = default_config.ip or nil + +d = s:option(DynamicList, "link", translate("Links with other containers")) +d.template = "docker/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, "dns", translate("Set custom DNS servers")) +d.template = "docker/cbi/xdynlist" +d.placeholder = "8.8.8.8" +d.rmempty = true +d.default = default_config.dns or nil + +d = s:option(Value, "user", translate("User(-u)"), translate("The user that commands are run as inside the container.(format: name|uid[:group|gid])")) +d.placeholder = "1000:1000" +d.rmempty = true +d.default = default_config.user or nil + +d = s:option(DynamicList, "env", translate("Environmental Variable(-e)"), translate("Set environment variables to inside the container")) +d.template = "docker/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(-v)"), translate("Bind mount a volume")) +d.template = "docker/cbi/xdynlist" +d.placeholder = "/media:/media:slave" +d.rmempty = true +d.default = default_config.mount or nil + +local d_ports = s:option(DynamicList, "port", translate("Exposed Ports(-p)"), translate("Publish container's port(s) to the host")) +d_ports.template = "docker/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, "hostname", translate("Host Name")) +d.rmempty = true +d.default = default_config.hostname or nil +d:depends("advance", 1) + +d = s:option(DynamicList, "device", translate("Device(--device)"), translate("Add host device to the container")) +d.template = "docker/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(--tmpfs)"), translate("Mount tmpfs directory")) +d.template = "docker/cbi/xdynlist" +d.placeholder = "/run:rw,noexec,nosuid,size=65536k" +d.rmempty = true +d:depends("advance", 1) +d.default = default_config.tmpfs or nil + +d = s:option(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 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 + + +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 return end + local tmp + local name = data.name or ("luci_" .. os.date("%Y%m%d%H%M%S")) + local hostname = data.hostname + local tty = type(data.tty) == "number" and (data.tty == 1 and true or false) or default_config.tty or false + local interactive = type(data.interactive) == "number" and (data.interactive == 1 and true or false) or default_config.interactive or false + local image = data.image + local user = data.user + if image and not image:match(".-:.+") then + image = image .. ":latest" + end + local privileged = type(data.privileged) == "number" and (data.privileged == 1 and true or false) or default_config.privileged or false + local restart = data.restart + local env = data.env + local dns = data.dns + local 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 + + local device = {} + tmp = data.device + if type(tmp) == "table" then + for i, v in ipairs(tmp) do + local t = {} + local _,_, h, c, p = v:find("(.-):(.-):(.+)") + if h and c then + t['PathOnHost'] = h + t['PathInContainer'] = c + t['CgroupPermissions'] = p or "rwm" + else + local _,_, h, c = v:find("(.-):(.+)") + if h and c then + t['PathOnHost'] = h + t['PathInContainer'] = c + t['CgroupPermissions'] = "rwm" + 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+)") + if p == nil then + v2=v2..'/tcp' + end + portbindings[v2] = {{HostPort=v1}} + exposedports[v2] = {HostPort=v1} + end + end + + local link = data.link + tmp = data.command + local command = {} + if tmp ~= nil then + for v in string.gmatch(tmp, "[^%s]+") do + command[#command+1] = v + end + end + if memory ~= 0 then + _,_,n,unit = memory:find("([%d%.]+)([%l%u]+)") + if n then + unit = unit and unit:sub(1,1):upper() or "B" + if unit == "M" then + memory = tonumber(n) * 1024 * 1024 + elseif unit == "G" then + memory = tonumber(n) * 1024 * 1024 * 1024 + elseif unit == "K" then + memory = tonumber(n) * 1024 + else + memory = tonumber(n) + end + end + end + + create_body.Hostname = network ~= "host" and (hostname or name) or nil + create_body.Tty = tty and true or false + create_body.OpenStdin = interactive and true or false + create_body.User = user + create_body.Cmd = (#command ~= 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.Dns = dns + create_body.HostConfig.Binds = (#mount ~= 0) and mount or nil + 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 create_body.HostConfig.NetworkMode ~= network then + -- network mode changed, need to clear duplicate config + create_body.NetworkingConfig = nil + end + create_body.HostConfig.NetworkMode = network + if ip then + if create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and type(create_body.NetworkingConfig.EndpointsConfig) == "table" then + -- ip + duplicate config + for k, v in pairs (create_body.NetworkingConfig.EndpointsConfig) do + if k == network and v.IPAMConfig and v.IPAMConfig.IPv4Address then + v.IPAMConfig.IPv4Address = ip + else + create_body.NetworkingConfig.EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } + end + break + end + else + -- ip + no duplicate config + create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } } + end + elseif not create_body.NetworkingConfig then + -- no ip + no duplicate config + create_body.NetworkingConfig = nil + end + + create_body["HostConfig"]["Tmpfs"] = (next(tmpfs) ~= nil) and tmpfs or nil + create_body["HostConfig"]["Devices"] = (next(device) ~= nil) and device or nil + + 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 == 200 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/services/docker/newcontainer")) + end + end + docker:clear_status() + local exist_image = false + if image then + for _, v in ipairs (images) do + if v.RepoTags and v.RepoTags[1] == image then + exist_image = true + break + end + end + if not exist_image then + pull_image(image) + elseif data._force_pull == 1 then + pull_image(image) + end + end + + 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/services/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/services/docker/newcontainer")) + end +end + +return m diff --git a/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/newnetwork.lua b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/newnetwork.lua new file mode 100644 index 000000000..c599f0a66 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/newnetwork.lua @@ -0,0 +1,205 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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.template = "docker/cbi/xsimpleform" +m.redirect = luci.dispatcher.build_url("admin", "services","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.err then docker:clear_status() end + +s = m:section(SimpleSection, translate("New Network")) +s.addremove = true +s.anonymous = true + +d = s:option(Value, "name", translate("Network Name")) +d.rmempty = true + +d = s:option(ListValue, "dirver", translate("Driver")) +d.rmempty = true +d:value("bridge", "bridge") +d:value("macvlan", "macvlan") +d:value("ipvlan", "ipvlan") +d:value("overlay", "overlay") + +d = s:option(Value, "parent", translate("Parent Interface")) +d.rmempty = true +d:depends("dirver", "macvlan") +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.template = "docker/cbi/xdynlist" +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.template = "docker/cbi/xdynlist" +d.rmempty = true +d.placeholder="my-route=10.1.1.1" + +d = s:option(Flag, "ipv6", translate("Enable IPv6")) +d.rmempty = true +d.disabled = 0 +d.enabled = 1 +d.default = 0 + +d = s:option(Value, "subnet6", translate("IPv6 Subnet")) +d.rmempty = true +d.placeholder="fe80::/10" +d.datatype="ip6addr" +d:depends("ipv6", 1) + +d = s:option(Value, "gateway6", translate("IPv6 Gateway")) +d.rmempty = true +d.placeholder="fe80::1" +d.datatype="ip6addr" +d:depends("ipv6", 1) + +m.handle = function(self, state, data) + if state == FORM_VALID then + local name = data.name + local driver = data.dirver + + local internal = data.internal == 1 and true or false + + local subnet = data.subnet + local gateway = data.gateway + local ip_range = data.ip_range + + local aux_address = {} + local tmp = data.aux_address or {} + for i,v in ipairs(tmp) do + _,_,k1,v1 = v:find("(.-)=(.+)") + aux_address[k1] = v1 + end + + local options = {} + tmp = data.options or {} + for i,v in ipairs(tmp) do + _,_,k1,v1 = v:find("(.-)=(.+)") + options[k1] = v1 + end + + local ipv6 = data.ipv6 == 1 and true or false + + local create_body={ + Name = name, + Driver = driver, + EnableIPv6 = ipv6, + IPAM = { + Driver= "default" + }, + Internal = internal + } + + if subnet or gateway or ip_range then + create_body["IPAM"]["Config"] = { + { + Subnet = subnet, + Gateway = gateway, + IPRange = ip_range, + -- AuxAddress = aux_address + -- AuxiliaryAddresses = aux_address + } + } + end + if next(aux_address)~=nil then + create_body["IPAM"]["Config"]["AuxiliaryAddresses"] = aux_address + end + if driver == "macvlan" then + create_body["Options"] = { + macvlan_mode = data.macvlan_mode, + parent = data.parent + } + elseif driver == "ipvlan" then + create_body["Options"] = { + ipvlan_mode = data.ipvlan_mode + } + elseif driver == "overlay" then + create_body["Ingress"] = data.ingerss == 1 and true or false + end + + if ipv6 and data.subnet6 and data.subnet6 then + if type(create_body["IPAM"]["Config"]) ~= "table" then + create_body["IPAM"]["Config"] = {} + end + local index = #create_body["IPAM"]["Config"] + create_body["IPAM"]["Config"][index+1] = { + Subnet = data.subnet6, + Gateway = data.gateway6 + } + end + + if next(options) ~= nil then + create_body["Options"] = create_body["Options"] or {} + for k, v in pairs(options) do + create_body["Options"][k] = v + end + end + + 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/services/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/services/docker/newnetwork")) + end + end +end + +return m diff --git a/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/overview.lua b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/overview.lua new file mode 100644 index 000000000..b24dc004b --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/overview.lua @@ -0,0 +1,90 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +require "luci.util" +local docker = require "luci.model.docker" +local uci = require "luci.model.uci" + +function byte_format(byte) + local suff = {"B", "KB", "MB", "GB", "TB"} + for i=1, 5 do + if byte > 1024 and i < 5 then + byte = byte / 1024 + else + return string.format("%.2f %s", byte, suff[i]) + end + end +end + +local 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='-'} + +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")) +debug = s:option(Flag, "debug", translate("Enable Debug"), translate("For debug, It shows all docker API actions of luci-app-dockerman in Debug Tempfile Path")) +debug.enabled="true" +debug.disabled="false" +debug_path = s:option(Value, "debug_path", translate("Debug Tempfile Path"), translate("Where you want to save the debug tempfile")) + +return m diff --git a/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/volumes.lua b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/volumes.lua new file mode 100644 index 000000000..06ac409a7 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/model/cbi/docker/volumes.lua @@ -0,0 +1,116 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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 "").. + "".. cv.Names[1]:sub(2).."" + 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.template = "docker/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.err then docker:clear_status() end + +action = m:section(Table,{{}}) +action.notitle=true +action.rowcolors=false +action.template="cbi/nullsection" +btnremove = action:option(Button, "remove") +btnremove.inputtitle= translate("Remove") +btnremove.template="docker/cbi/inlinebutton" +btnremove.inputstyle = "remove" +btnremove.forcewrite = true +btnremove.write = function(self, section) + local volume_selected = {} + -- 遍历table中sectionid + local volume_table_sids = volume_table:cfgsections() + for _, volume_table_sid in ipairs(volume_table_sids) do + -- 得到选中项的名字 + if volume_list[volume_table_sid]._selected == 1 then + -- volume_selected[#volume_selected+1] = volume_id:cfgvalue(volume_table_sid) + volume_selected[#volume_selected+1] = volume_table_sid + end + end + if next(volume_selected) ~= nil then + local success = true + docker:clear_status() + for _,vol in ipairs(volume_selected) do + docker:append_status("Volumes: " .. "remove" .. " " .. vol .. "...") + local msg = dk.volumes["remove"](dk, vol) + if msg.code ~= 204 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/services/docker/volumes")) + end +end +return m diff --git a/package/lean/luci-app-dockerman/luasrc/model/docker.lua b/package/lean/luci-app-dockerman/luasrc/model/docker.lua new file mode 100644 index 000000000..e61d8f95a --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/model/docker.lua @@ -0,0 +1,229 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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
") + else + _docker:append_status("fail code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "
") + 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 + if table_equal(config.ExposedPorts, image_config.ExposedPorts) then config.ExposedPorts = 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 + config.ExposedPorts = {} + 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
") + 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
") + 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
") + + -- 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
") + 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") + } + local _new = docker.new(options) + _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.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 diff --git a/package/lean/luci-app-dockerman/luasrc/view/docker/apply_widget.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/apply_widget.htm new file mode 100644 index 000000000..b9de0408f --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/apply_widget.htm @@ -0,0 +1,139 @@ + + diff --git a/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/dummyvalue.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/dummyvalue.htm new file mode 100644 index 000000000..ac8a48aba --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/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/lean/luci-app-dockerman/luasrc/view/docker/cbi/inlinebutton.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/inlinebutton.htm new file mode 100644 index 000000000..b1b193257 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/inlinebutton.htm @@ -0,0 +1,7 @@ +
+ <% if self:cfgvalue(section) ~= false then %> + " type="submit"" <% if self.disable then %>disabled <% end %><%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> /> + <% else %> + - + <% end %> +
diff --git a/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/inlinevalue.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/inlinevalue.htm new file mode 100644 index 000000000..cfbf44a21 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/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/lean/luci-app-dockerman/luasrc/view/docker/cbi/xdynlist.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/xdynlist.htm new file mode 100644 index 000000000..549c4ce31 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/xdynlist.htm @@ -0,0 +1,27 @@ +<%+cbi/valueheader%> +> +<% + 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 +%> + />
+<% end end %> + +<%+cbi/valuefooter%> diff --git a/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/xfvalue.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/xfvalue.htm new file mode 100644 index 000000000..04f7bc2ee --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/xfvalue.htm @@ -0,0 +1,10 @@ +<%+cbi/valueheader%> + /> + disabled <% end %><%= + attr("id", cbid) .. attr("name", cbid) .. attr("value", self.enabled or 1) .. + ifattr((self:cfgvalue(section) or self.default) == self.enabled, "checked", "checked") + %> /> + > +<%+cbi/valuefooter%> diff --git a/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/xsimpleform.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/xsimpleform.htm new file mode 100644 index 000000000..7cfe8bf21 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/cbi/xsimpleform.htm @@ -0,0 +1,89 @@ +<% + if not self.embedded then + %>
> + + + <% + end + + %>
<% + + if self.title and #self.title > 0 then + %>

<%=self.title%>

<% + end + + if self.description and #self.description > 0 then + %>
<%=self.description%>
<% + end + + self:render_children() + + %>
<% + + if self.message then + %>
<%=self.message%>
<% + end + + if self.errmessage then + %>
<%=self.errmessage%>
<% + end + + if not self.embedded then + if type(self.hidden) == "table" then + local k, v + for k, v in pairs(self.hidden) do + %><% + end + end + + local display_back = (self.redirect) + local display_cancel = (self.cancel ~= false and self.on_cancel) + local display_skip = (self.flow and self.flow.skip) + local display_submit = (self.submit ~= false) + local display_reset = (self.reset ~= false) + + if display_back or display_cancel or display_skip or display_submit or display_reset then + %>
<% + + if display_back then + %> <% + end + + if display_cancel then + local label = pcdata(self.cancel or translate("Cancel")) + %> <% + end + + if display_skip then + %> <% + end + + if display_submit then + local label = pcdata(self.submit or translate("Submit")) + %> <% + end + + if display_reset then + local label = pcdata(self.reset or translate("Reset")) + %> <% + end + + %>
<% + end + + %>
<% + end +%> + + diff --git a/package/lean/luci-app-dockerman/luasrc/view/docker/container.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/container.htm new file mode 100644 index 000000000..da53b75e5 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/container.htm @@ -0,0 +1,24 @@ +
+ + + diff --git a/package/lean/luci-app-dockerman/luasrc/view/docker/logs.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/logs.htm new file mode 100644 index 000000000..6e189c66a --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/logs.htm @@ -0,0 +1,10 @@ +<% if self.title == translate("Docker Events") then %> +<%+header%> +<% end %> +

<%=self.title%>

+
+ +
+<% if self.title == translate("Docker Events") then %> +<%+footer%> +<% end %> diff --git a/package/lean/luci-app-dockerman/luasrc/view/docker/overview.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/overview.htm new file mode 100644 index 000000000..f8b67eee3 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/overview.htm @@ -0,0 +1,281 @@ + + +
+
+
+
+
+ + Docker icon + + +
+
+
+

<%:Containers%>

+

+ <%- if self.containers_total ~= "-" then -%><%- end -%> + <%=self.containers_running%> + /<%=self.containers_total%> + <%- if self.containers_total ~= "-" then -%><%- end -%> +

+
+
+
+
+
+
+
+ + + + + +
+
+
+

<%:Images%>

+

+ <%- if self.images_total ~= "-" then -%><%- end -%> + <%=self.images_total%> + + <%- if self.images_total ~= "-" then -%><%- end -%> +

+
+
+
+
+
+
+
+ + + + + + + + + +
+
+
+

<%:Networks%>

+

+ <%- if self.networks_total ~= "-" then -%><%- end -%> + <%=self.networks_total%> + + <%- if self.networks_total ~= "-" then -%><%- end -%> +

+
+
+
+
+
+
+
+ + + +
+
+
+

<%:Volumes%>

+

+ <%- if self.volumes_total ~= "-" then -%><%- end -%> + <%=self.volumes_total%> + + <%- if self.volumes_total ~= "-" then -%><%- end -%> +

+
+
+
+
diff --git a/package/lean/luci-app-dockerman/luasrc/view/docker/resolv_container.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/resolv_container.htm new file mode 100644 index 000000000..38abc914c --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/resolv_container.htm @@ -0,0 +1,94 @@ + + +<%+cbi/valueheader%> + + +<%+cbi/valuefooter%> diff --git a/package/lean/luci-app-dockerman/luasrc/view/docker/stats.htm b/package/lean/luci-app-dockerman/luasrc/view/docker/stats.htm new file mode 100644 index 000000000..2016181c5 --- /dev/null +++ b/package/lean/luci-app-dockerman/luasrc/view/docker/stats.htm @@ -0,0 +1,60 @@ + diff --git a/package/lean/luci-app-dockerman/po/zh-cn/dockerman.po b/package/lean/luci-app-dockerman/po/zh-cn/dockerman.po new file mode 100644 index 000000000..083a38bdf --- /dev/null +++ b/package/lean/luci-app-dockerman/po/zh-cn/dockerman.po @@ -0,0 +1,332 @@ +msgid "Containers" +msgstr "容器" + +msgid "Images" +msgstr "镜像" + +msgid "Networks" +msgstr "网络" + +msgid "Volumes" +msgstr "存储卷" + +msgid "Events" +msgstr "事件" + +msgid "Docker Contaienr" +msgstr "Docker 容器" + +msgid "Start" +msgstr "启动" + +msgid "Restart" +msgstr "重启" + +msgid "Stop" +msgstr "停止" + +msgid "Upgrade" +msgstr "升级容器" + +msgid "Duplicate" +msgstr "复制容器" + +msgid "Remove" +msgstr "移除" + +msgid "Name" +msgstr "名称" + +msgid "Image" +msgstr "镜像" + +msgid "Status" +msgstr "状态" + +msgid "Created" +msgstr "创建时间" + +msgid "Start Time" +msgstr "启动时间" + +msgid "Healthy" +msgstr "健康" + +msgid "Restart Policy" +msgstr "重启策略" + +msgid "Update" +msgstr "更新" + +msgid "Device(--device)" +msgstr "设备(--device)" + +msgid "Mount/Volume" +msgstr "挂载/存储卷" + +msgid "Command" +msgstr "启动命令" + +msgid "Setting" +msgstr "设置" + +msgid "Driver" +msgstr "驱动" + +msgid "Env" +msgstr "环境变量" + +msgid "Ports" +msgstr "端口" + +msgid "Links" +msgstr "链接" + +msgid "Disconnect" +msgstr "断开" + +msgid "Connect Network" +msgstr "连接网络" + +msgid "Connect" +msgstr "连接" + +msgid "Info" +msgstr "信息" + +msgid "CPUs" +msgstr "CPU数量" + +msgid "Number of CPUs. Number is a fractional number. 0.000 means no limit." +msgstr "CPU数量,数字是小数,0.000表示没有限制。" + +msgid "CPU Shares Weight" +msgstr "CPU份额权重" + +msgid "CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024." +msgstr "CPU份额相对权重,如果设置为0,则系统将忽略该值,并使用默认值1024。" + +msgid "Memory" +msgstr "内存" + +msgid "Memory limit (format: []). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M." +msgstr "内存限制 (格式: <容量>[<单位>]). 数字是一个正整数。单位可以是b,k,m或g之一。最小为4M。" + +msgid "Block IO Weight" +msgstr "IO 权重" + +msgid "Block IO weight (relative weight) accepts a weight value between 10 and 1000." +msgstr "IO 权重 (相对权重) 接受10到1000之间的权重值。" + +msgid "Container Logs" +msgstr "容器日志" + +msgid "RepoTags" +msgstr "标签" + +msgid "Size" +msgstr "大小" + +msgid "Force Remove" +msgstr "强制移除" + +msgid "Subnet" +msgstr "子网络" + +msgid "Gateway" +msgstr "网关" + +msgid "New" +msgstr "新建" + +msgid "Resolv CLI" +msgstr "解析命令行" + +msgid "Docker Image" +msgstr "Docker 镜像" + +msgid "User(-u)" +msgstr "用户(-u)" + +msgid "New Container" +msgstr "新容器" + +msgid "Container Name" +msgstr "容器名称" + +msgid "Interactive (-i)" +msgstr "互动式 (-i)" + +msgid "Always pull image first" +msgstr "始终先拉取镜像" + +msgid "Privileged" +msgstr "特权模式(--privileged)" + +msgid "IPv4 Address" +msgstr "IPv4 地址" + +msgid "Links with other containers" +msgstr "与其他容器的链接(--link)" + +msgid "Environmental Variable(-e)" +msgstr "环境变量(-e)" + +msgid "Bind Mount(-v)" +msgstr "挂载(-v)" + +msgid "Exposed Ports(-p)" +msgstr "暴露端口(-p)" + +msgid "Run command" +msgstr "运行命令" + +msgid "Advance" +msgstr "高级" + +msgid "Mount tmpfs directory" +msgstr "挂载tmpfs到容器内部目录" + +msgid "New Network" +msgstr "新网络" + +msgid "Network Name" +msgstr "网络名称" + +msgid "Parent Interface" +msgstr "父接口" + +msgid "Macvlan Mode" +msgstr "Macvlan模式" + +msgid "Ipvlan Mode" +msgstr "Ipvlan模式" + +msgid "Ingress network is the network which provides the routing-mesh in swarm mode." +msgstr "Ingress网络是在群集模式下提供路由网的网络。" + +msgid "Options" +msgstr "选项" + +msgid "Restrict external access to the network" +msgstr "限制外部访问网络" + +msgid "IP range" +msgstr "IP范围" + +msgid "Exclude IPs" +msgstr "排除IP" + +msgid "Enable IPv6" +msgstr "启用IPv6" + +msgid "IPv6 Subnet" +msgstr "IPv6子网" + +msgid "IPv6 Gateway" +msgstr "IPv6网关" + +msgid "Docker Version" +msgstr "Docker版本" + +msgid "Api Version" +msgstr "API版本" + +msgid "Total Memory" +msgstr "总内存" + +msgid "Docker Root Dir" +msgstr "Docker根目录" + +msgid "Index Server Address" +msgstr "默认服务器地址" + +msgid "Socket Path" +msgstr "Socket路径" + +msgid "Action Status Tempfile Path" +msgstr "docker 动作状态的临时文件路径" + +msgid "Where you want to save the docker status file" +msgstr "保存docker status文件的位置" + +msgid "Enable Debug" +msgstr "启用调试" + +msgid "For debug, It shows all docker API actions of luci-app-dockermab in Debug Tempfile Path" +msgstr "用于调试,它在调试临时文件路径中显示 luci-app-dockermab 的所有docker API操作" + +msgid "Debug Tempfile Path" +msgstr "调试临时文件路径" + +msgid "Where you want to save the debug tempfile" +msgstr "保存调试临时文件的位置" + +msgid "Edit" +msgstr "编辑" + +msgid "Stats" +msgstr "状态" + +msgid "Logs" +msgstr "日志" + +msgid "Network TX/RX" +msgstr "网络发送/接收" + +msgid "CPU Useage" +msgstr "CPU用量" + +msgid "Memory Useage" +msgstr "内存用量" + +msgid "Docker Container" +msgstr "Docker 容器" + +msgid "Overview" +msgstr "概况" + +msgid "Pull Image" +msgstr "拉取镜像" + +msgid "Pull" +msgstr "拉取" + +msgid "Command line" +msgstr "输入命令行" + +msgid "Plese input command line:" +msgstr "请输入 docker run/create ... 命令行:" + +msgid "Network Name" +msgstr "网络名" + +msgid "Set custom DNS servers" +msgstr "自定义 DNS 服务器" + +msgid "The user that commands are run as inside the container.(format: name|uid[:group|gid])" +msgstr "容器内部执行命令的用户(组), 格式: UID:GID" + +msgid "Set environment variables to inside the container" +msgstr "容器内部环境变量" + +msgid "Bind mount a volume" +msgstr "绑定挂载" + +msgid "Publish container's port(s) to the host" +msgstr "将容器的端口发布到宿主" + +msgid "Add host device to the container" +msgstr "添加宿主设备到容器内部" + +msgid "Device" +msgstr "设备" + +msgid "Finish Time" +msgstr "结束时间" + +msgid "Command line Error" +msgstr "命令行错误" + +msgid "Canceled" +msgstr "已取消" diff --git a/package/lean/luci-app-dockerman/root/etc/config/docker b/package/lean/luci-app-dockerman/root/etc/config/docker new file mode 100644 index 000000000..f13c5c205 --- /dev/null +++ b/package/lean/luci-app-dockerman/root/etc/config/docker @@ -0,0 +1,5 @@ +config section 'local' + option socket_path '/var/run/docker.sock' + option status_path '/tmp/.docker_action_status' + option debug_path '/tmp/.docker_debug' + option debug 'false' diff --git a/package/lean/luci-app-docker/root/etc/docker-init b/package/lean/luci-app-dockerman/root/etc/docker-init similarity index 99% rename from package/lean/luci-app-docker/root/etc/docker-init rename to package/lean/luci-app-dockerman/root/etc/docker-init index 9bade865f..45b9769a5 100755 --- a/package/lean/luci-app-docker/root/etc/docker-init +++ b/package/lean/luci-app-dockerman/root/etc/docker-init @@ -23,4 +23,3 @@ w fi echo "y" | mkfs.ext4 /dev/sda$partid - diff --git a/package/lean/luci-lib-docker/Makefile b/package/lean/luci-lib-docker/Makefile new file mode 100644 index 000000000..f8ff83eb9 --- /dev/null +++ b/package/lean/luci-lib-docker/Makefile @@ -0,0 +1,46 @@ +# +# Copyright (C) 2019 lisaac +# +# This is free software, licensed under the Apache License, Version 2.0 . +# +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-lib-docker +PKG_VERSION:=v0.1.1 +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:=6. Libraries + TITLE:=Docker Engine API for LuCI + PKGARCH:=all + DEPENDS:=+luci-lib-json +endef + +define Package/$(PKG_NAME)/description + Docker Engine API for LuCI +endef + +define Build/Prepare +endef + +define Build/Compile +endef + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +rm -fr /tmp/luci-indexcache /tmp/luci-modulecache +endef + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/usr/lib/lua/luci + cp -pR ./luasrc/* $(1)/usr/lib/lua/luci +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/package/lean/luci-lib-docker/luasrc/docker.lua b/package/lean/luci-lib-docker/luasrc/docker.lua new file mode 100644 index 000000000..0d7e27f14 --- /dev/null +++ b/package/lean/luci-lib-docker/luasrc/docker.lua @@ -0,0 +1,436 @@ +require "nixio.util" +require "luci.util" + local json = require "luci.json" +local nixio = require "nixio" +local ltn12 = require "luci.ltn12" +local fs = require "nixio.fs" + +local urlencode = luci.util.urlencode or luci.http and luci.http.protocol and luci.http.protocol.urlencode +local json_stringify = json.encode --luci.json and luci.json.encode or luci.jsonc.stringify +local json_parse = json.decode --luci.json and luci.json.decode or luci.jsonc.parse + +local chunksource = function(sock, buffer) + buffer = buffer or "" + return function() + local output + local _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n") + if not count then -- lua ^ only match start of stirng,not start of line + _, endp, count = buffer:find("\r\n([0-9a-fA-F]+);?.-\r\n") + end + while not count do + local newblock, code = sock:recv(1024) + if not newblock then + return nil, code + end + buffer = buffer .. newblock + _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n") + if not count then + _, endp, count = buffer:find("\r\n([0-9a-fA-F]+);?.-\r\n") + end + end + count = tonumber(count, 16) + if not count then + return nil, -1, "invalid encoding" + elseif count == 0 then -- finial + return nil + elseif count + 2 <= #buffer - endp then + output = buffer:sub(endp + 1, endp + count) + buffer = buffer:sub(endp + count + 3) -- don't forget handle buffer + return output + else + output = buffer:sub(endp + 1, endp + count) + buffer = "" + if count > #output then + local remain, code = sock:recvall(count - #output) --need read remaining + if not remain then + return nil, code + end + output = output .. remain + count, code = sock:recvall(2) --read \r\n + else + count, code = sock:recvall(count + 2 - #buffer + endp) + end + if not count then + return nil, code + end + return output + end + end +end + +local docker_stream_filter = function(buffer) + buffer = buffer or "" + if #buffer < 8 then + return "" + end + local stream_type = ((string.byte(buffer, 1) == 1) and "stdout") or ((string.byte(buffer, 1) == 2) and "stderr") or ((string.byte(buffer, 1) == 0) and "stdin") or "stream_err" + local valid_length = + tonumber(string.byte(buffer, 5)) * 256 * 256 * 256 + tonumber(string.byte(buffer, 6)) * 256 * 256 + tonumber(string.byte(buffer, 7)) * 256 + tonumber(string.byte(buffer, 8)) + if valid_length > #buffer + 8 then + return "" + end + return stream_type .. ": " .. string.sub(buffer, 9, valid_length + 8) + -- return string.sub(buffer, 9, valid_length + 8) +end + +local gen_http_req = function(options) + local req + options = options or {} + options.protocol = options.protocol or "HTTP/1.1" + req = (options.method or "GET") .. " " .. options.path .. " " .. options.protocol .. "\r\n" + req = req .. "Host: " .. options.host .. "\r\n" + req = req .. "User-Agent: " .. options.user_agent .. "\r\n" + req = req .. "Connection: close\r\n" + if type(options.header) == "table" then + for k, v in pairs(options.header) do + req = req .. k .. ": " .. v .."\r\n" + end + end + if options.method == "POST" and type(options.conetnt) == "table" then + local conetnt_json = json_stringify(options.conetnt) + req = req .. "Content-Type: application/json\r\n" + req = req .. "Content-Length: " .. #conetnt_json .. "\r\n" + req = req .. "\r\n" .. conetnt_json + elseif options.method == "PUT" and options.conetnt then + req = req .. "Content-Type: application/x-tar\r\n" + req = req .. "Content-Length: " .. #options.conetnt .. "\r\n" + req = req .. "\r\n" .. options.conetnt + else + req = req .. "\r\n" + end + if options.debug then io.popen("echo '".. req .. "' >> " .. options.debug_path) end + return req +end + +local send_http_socket = function(socket_path, req) + local docker_socket = nixio.socket("unix", "stream") + if docker_socket:connect(socket_path) ~= true then + return { + headers={ + code=497, + message="bad socket path", + protocol="HTTP/1.1" + }, + body={ + message="can\'t connect to unix socket" + } + } + end + if docker_socket:send(req) == 0 then + return { + headers={ + code=498, + message="bad socket path", + protocol="HTTP/1.1" + }, + body={ + message="can\'t send data to unix socket" + } + } + end + -- local data, err_code, err_msg, data_f = docker_socket:readall() + -- docker_socket:close() + local linesrc = docker_socket:linesource() + -- read socket using source http://w3.impa.br/~diego/software/luasocket/ltn12.html + --http://lua-users.org/wiki/FiltersSourcesAndSinks + + -- handle response header + local line = linesrc() + if not line then + docker_socket:close() + return { + headers={ + code=499, + message="bad socket path", + protocol="HTTP/1.1" + }, + body={ + message="no data receive from socket" + } + } + end + local response = {code = 0, headers = {}, body = {}} + + local p, code, msg = line:match("^([%w./]+) ([0-9]+) (.*)") + response.protocol = p + response.code = tonumber(code) + response.message = msg + line = linesrc() + while line and line ~= "" do + local key, val = line:match("^([%w-]+)%s?:%s?(.*)") + if key and key ~= "Status" then + if type(response.headers[key]) == "string" then + response.headers[key] = {response.headers[key], val} + elseif type(response.headers[key]) == "table" then + response.headers[key][#response.headers[key] + 1] = val + else + response.headers[key] = val + end + end + line = linesrc() + end + -- handle response body + local body_buffer = linesrc(true) + response.body = {} + if response.headers["Transfer-Encoding"] == "chunked" then + local source = chunksource(docker_socket, body_buffer) + code = ltn12.pump.all(source, (ltn12.sink.table(response.body))) and response.code or 555 + response.code = code + else + local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource()) + code = ltn12.pump.all(body_source, (ltn12.sink.table(response.body))) and response.code or 555 + response.code = code + end + docker_socket:close() + return response +end + +local send_http_require = function(options, method, api_group, api_action, name_or_id, request_qurey, request_body) + local qurey + local req_options = setmetatable({}, {__index = options}) + + -- for docker action status + -- if options.status_enabled then + -- fs.writefile(options.status_path, api_group or "" .. " " .. api_action or "" .. " " .. name_or_id or "") + -- end + + -- request_qurey = request_qurey or {} + -- request_body = request_body or {} + if name_or_id == "" then + name_or_id = nil + end + req_options.method = method + req_options.path = (api_group and ("/" .. api_group) or "") .. (name_or_id and ("/" .. name_or_id) or "") .. (api_action and ("/" .. api_action) or "") + req_options.header = {} + if type(request_qurey) == "table" then + for k, v in pairs(request_qurey) do + if type(v) == "table" then + if k ~= "_header" then + qurey = (qurey and qurey .. "&" or "?") .. k .. "=" .. urlencode(json_stringify(v)) + else + -- for http header + for k1, v1 in pairs(v) do + req_options.header[k1] = v1 + end + end + elseif type(v) == "boolean" then + qurey = (qurey and qurey .. "&" or "?") .. k .. "=" .. (v and "true" or "false") + elseif type(v) == "number" or type(v) == "string" then + qurey = (qurey and qurey .. "&" or "?") .. k .. "=" .. v + end + end + end + req_options.path = req_options.path .. (qurey or "") + -- if type(request_body) == "table" then + req_options.conetnt = request_body + -- end + local response = send_http_socket(req_options.socket_path, gen_http_req(req_options)) + -- for docker action status + -- if options.status_enabled then + -- fs.remove(options.status_path) + -- end + return response +end + +local gen_api = function(_table, http_method, api_group, api_action) + local _api_action + if api_action == "get_archive" or api_action == "put_archive" then + _api_action = "archive" + elseif api_action == "df" then + _api_action = "system/df" + elseif api_action ~= "list" and api_action ~= "inspect" and api_action ~= "remove" then + _api_action = api_action + elseif (api_group == "containers" or api_group == "images" or api_group == "exec") and (api_action == "list" or api_action == "inspect") then + _api_action = "json" + end + + local fp = function(self, name_or_id, request_qurey, request_body) + if api_action == "list" then + if (name_or_id ~= "" and name_or_id ~= nil) then + if api_group == "images" then + name_or_id = nil + else + request_qurey = request_qurey or {} + request_qurey.filters = request_qurey.filters or {} + request_qurey.filters.name = request_qurey.filters.name or {} + request_qurey.filters.name[#request_qurey.filters.name + 1] = name_or_id + name_or_id = nil + end + end + elseif api_action == "create" then + if (name_or_id ~= "" and name_or_id ~= nil) then + request_qurey = request_qurey or {} + request_qurey.name = request_qurey.name or name_or_id + name_or_id = nil + end + elseif api_action == "logs" then + local body_buffer = "" + local response = send_http_require(self.options, http_method, api_group, _api_action, name_or_id, request_qurey, request_body) + if response.code >= 200 and response.code < 300 then + for i, v in ipairs(response.body) do + body_buffer = body_buffer .. docker_stream_filter(response.body[i]) + end + response.body = body_buffer + end + return response + end + local response = send_http_require(self.options, http_method, api_group, _api_action, name_or_id, request_qurey, request_body) + if response.headers and response.headers["Content-Type"] == "application/json" then + if #response.body == 1 then + response.body = json_parse(response.body[1]) + else + local tmp = {} + for _, v in ipairs(response.body) do + tmp[#tmp+1] = json_parse(v) + end + response.body = tmp + end + end + return response + end + + if api_group then + _table[api_group][api_action] = fp + else + _table[api_action] = fp + end +end + +local _docker = {containers = {}, exec = {}, images = {}, networks = {}, volumes = {}} + +gen_api(_docker, "GET", "containers", "list") +gen_api(_docker, "POST", "containers", "create") +gen_api(_docker, "GET", "containers", "inspect") +gen_api(_docker, "GET", "containers", "top") +gen_api(_docker, "GET", "containers", "logs") +gen_api(_docker, "GET", "containers", "changes") +gen_api(_docker, "GET", "containers", "stats") +gen_api(_docker, "POST", "containers", "resize") +gen_api(_docker, "POST", "containers", "start") +gen_api(_docker, "POST", "containers", "stop") +gen_api(_docker, "POST", "containers", "restart") +gen_api(_docker, "POST", "containers", "kill") +gen_api(_docker, "POST", "containers", "update") +gen_api(_docker, "POST", "containers", "rename") +gen_api(_docker, "POST", "containers", "pause") +gen_api(_docker, "POST", "containers", "unpause") +gen_api(_docker, "POST", "containers", "update") +gen_api(_docker, "DELETE", "containers", "remove") +gen_api(_docker, "POST", "containers", "prune") +gen_api(_docker, "POST", "containers", "exec") +gen_api(_docker, "POST", "exec", "start") +gen_api(_docker, "POST", "exec", "resize") +gen_api(_docker, "GET", "exec", "inspect") +gen_api(_docker, "GET", "containers", "get_archive") +gen_api(_docker, "PUT", "containers", "put_archive") +-- TODO: export,attch + +gen_api(_docker, "GET", "images", "list") +gen_api(_docker, "POST", "images", "create") +gen_api(_docker, "GET", "images", "inspect") +gen_api(_docker, "GET", "images", "history") +gen_api(_docker, "POST", "images", "tag") +gen_api(_docker, "DELETE", "images", "remove") +gen_api(_docker, "GET", "images", "search") +gen_api(_docker, "POST", "images", "prune") +-- TODO: build clear push commit export import + +gen_api(_docker, "GET", "networks", "list") +gen_api(_docker, "GET", "networks", "inspect") +gen_api(_docker, "DELETE", "networks", "remove") +gen_api(_docker, "POST", "networks", "create") +gen_api(_docker, "POST", "networks", "connect") +gen_api(_docker, "POST", "networks", "disconnect") +gen_api(_docker, "POST", "networks", "prune") + +gen_api(_docker, "GET", "volumes", "list") +gen_api(_docker, "GET", "volumes", "inspect") +gen_api(_docker, "DELETE", "volumes", "remove") +gen_api(_docker, "POST", "volumes", "create") + +gen_api(_docker, "GET", nil, "events") +gen_api(_docker, "GET", nil, "version") +gen_api(_docker, "GET", nil, "info") +gen_api(_docker, "GET", nil, "_ping") +gen_api(_docker, "GET", nil, "df") + +function _docker.new(options) + local docker = {} + local _options = options or {} + docker.options = { + socket_path = _options.socket_path or "/var/run/docker.sock", + host = _options.host or "localhost", + version = _options.version or "v1.40", + user_agent = _options.user_agent or "LuCI", + protocol = _options.protocol or "HTTP/1.1", + -- status_enabled = _options.status_enabled or true, + -- status_path = _options.status_path or "/tmp/.docker_action_status", + debug = _options.debug or false, + debug_path = _options.debug_path or "/tmp/.docker_debug" + } + setmetatable( + docker, + { + __index = function(t, key) + if _docker[key] ~= nil then + return _docker[key] + else + return _docker.containers[key] + end + end + } + ) + setmetatable( + docker.containers, + { + __index = function(t, key) + if key == "options" then + return docker.options + end + end + } + ) + setmetatable( + docker.networks, + { + __index = function(t, key) + if key == "options" then + return docker.options + end + end + } + ) + setmetatable( + docker.images, + { + __index = function(t, key) + if key == "options" then + return docker.options + end + end + } + ) + setmetatable( + docker.volumes, + { + __index = function(t, key) + if key == "options" then + return docker.options + end + end + } + ) + setmetatable( + docker.exec, + { + __index = function(t, key) + if key == "options" then + return docker.options + end + end + } + ) + return docker +end + +return _docker