fix
57
luci-app-adguardhome/Makefile
Normal file
|
@ -0,0 +1,57 @@
|
|||
# Copyright (C) 2018-2019 Lienol
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-adguardhome
|
||||
PKG_MAINTAINER:=<https://github.com/rufengsuixing/luci-app-adguardhome>
|
||||
|
||||
LUCI_TITLE:=LuCI app for AdGuardHome
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:=+ca-certs +curl +wget-ssl +PACKAGE_$(PKG_NAME)_INCLUDE_binary:adguardhome
|
||||
LUCI_DESCRIPTION:=LuCI support for AdGuardHome
|
||||
|
||||
define Package/$(PKG_NAME)/config
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_binary
|
||||
bool "Include Binary File"
|
||||
default y
|
||||
endef
|
||||
|
||||
PKG_CONFIG_DEPENDS:= CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_binary
|
||||
|
||||
define Package/luci-app-adguardhome/conffiles
|
||||
/usr/share/AdGuardHome/links.txt
|
||||
/etc/config/AdGuardHome
|
||||
/etc/AdGuardHome.yaml
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome/postinst
|
||||
#!/bin/sh
|
||||
/etc/init.d/AdGuardHome enable >/dev/null 2>&1
|
||||
enable=$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)
|
||||
if [ "$enable" == "1" ]; then
|
||||
/etc/init.d/AdGuardHome reload
|
||||
fi
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -f /tmp/luci-modulecache/*
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome/prerm
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
/etc/init.d/AdGuardHome disable
|
||||
/etc/init.d/AdGuardHome stop
|
||||
uci -q batch <<-EOF >/dev/null 2>&1
|
||||
delete ucitrack.@AdGuardHome[-1]
|
||||
commit ucitrack
|
||||
EOF
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
130
luci-app-adguardhome/luasrc/controller/AdGuardHome.lua
Normal file
|
@ -0,0 +1,130 @@
|
|||
module("luci.controller.AdGuardHome",package.seeall)
|
||||
local fs=require"nixio.fs"
|
||||
local http=require"luci.http"
|
||||
local uci=require"luci.model.uci".cursor()
|
||||
function index()
|
||||
local page = entry({"admin", "services", "AdGuardHome"},alias("admin", "services", "AdGuardHome", "base"),_("AdGuard Home"))
|
||||
page.order = 10
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-adguardhome" }
|
||||
entry({"admin","services","AdGuardHome","base"},cbi("AdGuardHome/base"),_("Base Setting"),1).leaf = true
|
||||
entry({"admin","services","AdGuardHome","log"},form("AdGuardHome/log"),_("Log"),2).leaf = true
|
||||
entry({"admin","services","AdGuardHome","manual"},cbi("AdGuardHome/manual"),_("Manual Config"),3).leaf = true
|
||||
entry({"admin","services","AdGuardHome","status"},call("act_status")).leaf=true
|
||||
entry({"admin", "services", "AdGuardHome", "check"}, call("check_update"))
|
||||
entry({"admin", "services", "AdGuardHome", "doupdate"}, call("do_update"))
|
||||
entry({"admin", "services", "AdGuardHome", "getlog"}, call("get_log"))
|
||||
entry({"admin", "services", "AdGuardHome", "dodellog"}, call("do_dellog"))
|
||||
entry({"admin", "services", "AdGuardHome", "reloadconfig"}, call("reload_config"))
|
||||
entry({"admin", "services", "AdGuardHome", "gettemplateconfig"}, call("get_template_config"))
|
||||
end
|
||||
function get_template_config()
|
||||
local b
|
||||
local d=""
|
||||
for cnt in io.lines("/tmp/resolv.conf.d/resolv.conf.auto") do
|
||||
b=string.match (cnt,"^[^#]*nameserver%s+([^%s]+)$")
|
||||
if (b~=nil) then
|
||||
d=d.." - "..b.."\n"
|
||||
end
|
||||
end
|
||||
local f=io.open("/usr/share/AdGuardHome/AdGuardHome_template.yaml", "r+")
|
||||
local tbl = {}
|
||||
local a=""
|
||||
while (1) do
|
||||
a=f:read("*l")
|
||||
if (a=="#bootstrap_dns") then
|
||||
a=d
|
||||
elseif (a=="#upstream_dns") then
|
||||
a=d
|
||||
elseif (a==nil) then
|
||||
break
|
||||
end
|
||||
table.insert(tbl, a)
|
||||
end
|
||||
f:close()
|
||||
http.prepare_content("text/plain; charset=utf-8")
|
||||
http.write(table.concat(tbl, "\n"))
|
||||
end
|
||||
function reload_config()
|
||||
fs.remove("/tmp/AdGuardHometmpconfig.yaml")
|
||||
http.prepare_content("application/json")
|
||||
http.write('')
|
||||
end
|
||||
function act_status()
|
||||
local e={}
|
||||
local binpath=uci:get("AdGuardHome","AdGuardHome","binpath")
|
||||
e.running=luci.sys.call("pgrep "..binpath.." >/dev/null")==0
|
||||
e.redirect=(fs.readfile("/var/run/AdGredir")=="1")
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(e)
|
||||
end
|
||||
function do_update()
|
||||
fs.writefile("/var/run/lucilogpos","0")
|
||||
http.prepare_content("application/json")
|
||||
http.write('')
|
||||
local arg
|
||||
if luci.http.formvalue("force") == "1" then
|
||||
arg="force"
|
||||
else
|
||||
arg=""
|
||||
end
|
||||
if fs.access("/var/run/update_core") then
|
||||
if arg=="force" then
|
||||
luci.sys.exec("kill $(pgrep /usr/share/AdGuardHome/update_core.sh) ; sh /usr/share/AdGuardHome/update_core.sh "..arg.." >/tmp/AdGuardHome_update.log 2>&1 &")
|
||||
end
|
||||
else
|
||||
luci.sys.exec("sh /usr/share/AdGuardHome/update_core.sh "..arg.." >/tmp/AdGuardHome_update.log 2>&1 &")
|
||||
end
|
||||
end
|
||||
function get_log()
|
||||
local logfile=uci:get("AdGuardHome","AdGuardHome","logfile")
|
||||
if (logfile==nil) then
|
||||
http.write("no log available\n")
|
||||
return
|
||||
elseif (logfile=="syslog") then
|
||||
if not fs.access("/var/run/AdGuardHomesyslog") then
|
||||
luci.sys.exec("(/usr/share/AdGuardHome/getsyslog.sh &); sleep 1;")
|
||||
end
|
||||
logfile="/tmp/AdGuardHometmp.log"
|
||||
fs.writefile("/var/run/AdGuardHomesyslog","1")
|
||||
elseif not fs.access(logfile) then
|
||||
http.write("")
|
||||
return
|
||||
end
|
||||
http.prepare_content("text/plain; charset=utf-8")
|
||||
local fdp
|
||||
if fs.access("/var/run/lucilogreload") then
|
||||
fdp=0
|
||||
fs.remove("/var/run/lucilogreload")
|
||||
else
|
||||
fdp=tonumber(fs.readfile("/var/run/lucilogpos")) or 0
|
||||
end
|
||||
local f=io.open(logfile, "r+")
|
||||
f:seek("set",fdp)
|
||||
local a=f:read(2048000) or ""
|
||||
fdp=f:seek()
|
||||
fs.writefile("/var/run/lucilogpos",tostring(fdp))
|
||||
f:close()
|
||||
http.write(a)
|
||||
end
|
||||
function do_dellog()
|
||||
local logfile=uci:get("AdGuardHome","AdGuardHome","logfile")
|
||||
fs.writefile(logfile,"")
|
||||
http.prepare_content("application/json")
|
||||
http.write('')
|
||||
end
|
||||
function check_update()
|
||||
http.prepare_content("text/plain; charset=utf-8")
|
||||
local fdp=tonumber(fs.readfile("/var/run/lucilogpos")) or 0
|
||||
local f=io.open("/tmp/AdGuardHome_update.log", "r+")
|
||||
f:seek("set",fdp)
|
||||
local a=f:read(2048000) or ""
|
||||
fdp=f:seek()
|
||||
fs.writefile("/var/run/lucilogpos",tostring(fdp))
|
||||
f:close()
|
||||
if fs.access("/var/run/update_core") then
|
||||
http.write(a)
|
||||
else
|
||||
http.write(a.."\0")
|
||||
end
|
||||
end
|
304
luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua
Normal file
|
@ -0,0 +1,304 @@
|
|||
require("luci.sys")
|
||||
require("luci.util")
|
||||
require("io")
|
||||
local m,s,o,o1
|
||||
local fs=require"nixio.fs"
|
||||
local uci=require"luci.model.uci".cursor()
|
||||
local configpath=uci:get("AdGuardHome","AdGuardHome","configpath") or "/etc/AdGuardHome.yaml"
|
||||
local binpath=uci:get("AdGuardHome","AdGuardHome","binpath") or "/usr/bin/AdGuardHome"
|
||||
httpport=uci:get("AdGuardHome","AdGuardHome","httpport") or "3000"
|
||||
m = Map("AdGuardHome", "AdGuard Home")
|
||||
m.description = translate("Free and open source, powerful network-wide ads & trackers blocking DNS server.")
|
||||
m:section(SimpleSection).template = "AdGuardHome/AdGuardHome_status"
|
||||
|
||||
s = m:section(TypedSection, "AdGuardHome")
|
||||
s.anonymous=true
|
||||
s.addremove=false
|
||||
---- enable
|
||||
o = s:option(Flag, "enabled", translate("Enable"))
|
||||
o.default = 0
|
||||
o.optional = false
|
||||
---- httpport
|
||||
o =s:option(Value,"httpport",translate("Browser management port"))
|
||||
o.placeholder=3000
|
||||
o.default=3000
|
||||
o.datatype="port"
|
||||
o.optional = false
|
||||
o.description = translate("<input type=\"button\" style=\"width:210px;border-color:Teal; text-align:center;font-weight:bold;color:Green;\" value=\"AdGuardHome Web:"..httpport.."\" onclick=\"window.open('http://'+window.location.hostname+':"..httpport.."/')\"/>")
|
||||
---- update warning not safe
|
||||
local binmtime=uci:get("AdGuardHome","AdGuardHome","binmtime") or "0"
|
||||
local e=""
|
||||
if not fs.access(configpath) then
|
||||
e=e.." "..translate("no config")
|
||||
end
|
||||
if not fs.access(binpath) then
|
||||
e=e.." "..translate("no core")
|
||||
else
|
||||
local version=uci:get("AdGuardHome","AdGuardHome","version")
|
||||
local testtime=fs.stat(binpath,"mtime")
|
||||
if testtime~=tonumber(binmtime) or version==nil then
|
||||
local tmp=luci.sys.exec(binpath.." --version | grep -m 1 -E 'v[0-9.]+' -o ")
|
||||
version=string.sub(tmp, 1)
|
||||
if version=="" then version="core error" end
|
||||
uci:set("AdGuardHome","AdGuardHome","version",version)
|
||||
uci:set("AdGuardHome","AdGuardHome","binmtime",testtime)
|
||||
uci:save("AdGuardHome")
|
||||
end
|
||||
e=version..e
|
||||
end
|
||||
o=s:option(Button,"restart",translate("Update"))
|
||||
o.inputtitle=translate("Update core version")
|
||||
o.template = "AdGuardHome/AdGuardHome_check"
|
||||
o.showfastconfig=(not fs.access(configpath))
|
||||
o.description=string.format(translate("core version:").."<strong><font id=\"updateversion\" color=\"green\">%s </font></strong>",e)
|
||||
---- port warning not safe
|
||||
local port=luci.sys.exec("awk '/ port:/{printf($2);exit;}' "..configpath.." 2>nul")
|
||||
if (port=="") then port="?" end
|
||||
---- Redirect
|
||||
o = s:option(ListValue, "redirect", port..translate("Redirect"), translate("AdGuardHome redirect mode"))
|
||||
o.placeholder = "none"
|
||||
o:value("none", translate("none"))
|
||||
o:value("dnsmasq-upstream", translate("Run as dnsmasq upstream server"))
|
||||
o:value("redirect", translate("Redirect 53 port to AdGuardHome"))
|
||||
o:value("exchange", translate("Use port 53 replace dnsmasq"))
|
||||
o.default = "none"
|
||||
o.optional = true
|
||||
---- bin path
|
||||
o = s:option(Value, "binpath", translate("Bin Path"), translate("AdGuardHome Bin path if no bin will auto download"))
|
||||
o.default = "/usr/bin/AdGuardHome"
|
||||
o.datatype = "string"
|
||||
o.optional = false
|
||||
o.rmempty=false
|
||||
o.validate=function(self, value)
|
||||
if value=="" then return nil end
|
||||
if fs.stat(value,"type")=="dir" then
|
||||
fs.rmdir(value)
|
||||
end
|
||||
if fs.stat(value,"type")=="dir" then
|
||||
if (m.message) then
|
||||
m.message =m.message.."\nerror!bin path is a dir"
|
||||
else
|
||||
m.message ="error!bin path is a dir"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
return value
|
||||
end
|
||||
--- upx
|
||||
o = s:option(ListValue, "upxflag", translate("use upx to compress bin after download"))
|
||||
o:value("", translate("none"))
|
||||
o:value("-1", translate("compress faster"))
|
||||
o:value("-9", translate("compress better"))
|
||||
o:value("--best", translate("compress best(can be slow for big files)"))
|
||||
o:value("--brute", translate("try all available compression methods & filters [slow]"))
|
||||
o:value("--ultra-brute", translate("try even more compression variants [very slow]"))
|
||||
o.default = ""
|
||||
o.description=translate("bin use less space,but may have compatibility issues")
|
||||
o.rmempty = true
|
||||
---- config path
|
||||
o = s:option(Value, "configpath", translate("Config Path"), translate("AdGuardHome config path"))
|
||||
o.default = "/etc/AdGuardHome.yaml"
|
||||
o.datatype = "string"
|
||||
o.optional = false
|
||||
o.rmempty=false
|
||||
o.validate=function(self, value)
|
||||
if value==nil then return nil end
|
||||
if fs.stat(value,"type")=="dir" then
|
||||
fs.rmdir(value)
|
||||
end
|
||||
if fs.stat(value,"type")=="dir" then
|
||||
if m.message then
|
||||
m.message =m.message.."\nerror!config path is a dir"
|
||||
else
|
||||
m.message ="error!config path is a dir"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
return value
|
||||
end
|
||||
---- work dir
|
||||
o = s:option(Value, "workdir", translate("Work dir"), translate("AdGuardHome work dir include rules,audit log and database"))
|
||||
o.default = "/etc/AdGuardHome"
|
||||
o.datatype = "string"
|
||||
o.optional = false
|
||||
o.rmempty=false
|
||||
o.validate=function(self, value)
|
||||
if value=="" then return nil end
|
||||
if fs.stat(value,"type")=="reg" then
|
||||
if m.message then
|
||||
m.message =m.message.."\nerror!work dir is a file"
|
||||
else
|
||||
m.message ="error!work dir is a file"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
if string.sub(value, -1)=="/" then
|
||||
return string.sub(value, 1, -2)
|
||||
else
|
||||
return value
|
||||
end
|
||||
end
|
||||
---- log file
|
||||
o = s:option(Value, "logfile", translate("Runtime log file"), translate("AdGuardHome runtime Log file if 'syslog': write to system log;if empty no log"))
|
||||
o.datatype = "string"
|
||||
o.rmempty = true
|
||||
o.validate=function(self, value)
|
||||
if fs.stat(value,"type")=="dir" then
|
||||
fs.rmdir(value)
|
||||
end
|
||||
if fs.stat(value,"type")=="dir" then
|
||||
if m.message then
|
||||
m.message =m.message.."\nerror!log file is a dir"
|
||||
else
|
||||
m.message ="error!log file is a dir"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
return value
|
||||
end
|
||||
---- debug
|
||||
o = s:option(Flag, "verbose", translate("Verbose log"))
|
||||
o.default = 0
|
||||
o.optional = true
|
||||
---- gfwlist
|
||||
local a=luci.sys.call("grep -m 1 -q programadd "..configpath)
|
||||
if (a==0) then
|
||||
a="Added"
|
||||
else
|
||||
a="Not added"
|
||||
end
|
||||
o=s:option(Button,"gfwdel",translate("Del gfwlist"),translate(a))
|
||||
o.optional = true
|
||||
o.inputtitle=translate("Del")
|
||||
o.write=function()
|
||||
luci.sys.exec("sh /usr/share/AdGuardHome/gfw2adg.sh del 2>&1")
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin","services","AdGuardHome"))
|
||||
end
|
||||
o=s:option(Button,"gfwadd",translate("Add gfwlist"),translate(a))
|
||||
o.optional = true
|
||||
o.inputtitle=translate("Add")
|
||||
o.write=function()
|
||||
luci.sys.exec("sh /usr/share/AdGuardHome/gfw2adg.sh 2>&1")
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin","services","AdGuardHome"))
|
||||
end
|
||||
o = s:option(Value, "gfwupstream", translate("Gfwlist upstream dns server"), translate("Gfwlist domain upstream dns service")..translate(a))
|
||||
o.default = "tcp://208.67.220.220:5353"
|
||||
o.datatype = "string"
|
||||
o.optional = true
|
||||
---- chpass
|
||||
o = s:option(Value, "hashpass", translate("Change browser management password"), translate("Press load culculate model and culculate finally save/apply"))
|
||||
o.default = ""
|
||||
o.datatype = "string"
|
||||
o.template = "AdGuardHome/AdGuardHome_chpass"
|
||||
o.optional = true
|
||||
---- upgrade protect
|
||||
o = s:option(MultiValue, "upprotect", translate("Keep files when system upgrade"))
|
||||
o:value("$binpath",translate("core bin"))
|
||||
o:value("$configpath",translate("config file"))
|
||||
o:value("$logfile",translate("log file"))
|
||||
o:value("$workdir/data/sessions.db",translate("sessions.db"))
|
||||
o:value("$workdir/data/stats.db",translate("stats.db"))
|
||||
o:value("$workdir/data/querylog.json",translate("querylog.json"))
|
||||
o:value("$workdir/data/filters",translate("filters"))
|
||||
o.widget = "checkbox"
|
||||
o.default = nil
|
||||
o.optional=true
|
||||
---- wait net on boot
|
||||
o = s:option(Flag, "waitonboot", translate("On boot when network ok restart"))
|
||||
o.default = 1
|
||||
o.optional = true
|
||||
---- backup workdir on shutdown
|
||||
local workdir=uci:get("AdGuardHome","AdGuardHome","workdir") or "/etc/AdGuardHome"
|
||||
o = s:option(MultiValue, "backupfile", translate("Backup workdir files when shutdown"))
|
||||
o1 = s:option(Value, "backupwdpath", translate("Backup workdir path"))
|
||||
local name
|
||||
o:value("filters","filters")
|
||||
o:value("stats.db","stats.db")
|
||||
o:value("querylog.json","querylog.json")
|
||||
o:value("sessions.db","sessions.db")
|
||||
o1:depends ("backupfile", "filters")
|
||||
o1:depends ("backupfile", "stats.db")
|
||||
o1:depends ("backupfile", "querylog.json")
|
||||
o1:depends ("backupfile", "sessions.db")
|
||||
for name in fs.glob(workdir.."/data/*")
|
||||
do
|
||||
name=fs.basename (name)
|
||||
if name~="filters" and name~="stats.db" and name~="querylog.json" and name~="sessions.db" then
|
||||
o:value(name,name)
|
||||
o1:depends ("backupfile", name)
|
||||
end
|
||||
end
|
||||
o.widget = "checkbox"
|
||||
o.default = nil
|
||||
o.optional=false
|
||||
o.description=translate("Will be restore when workdir/data is empty")
|
||||
----backup workdir path
|
||||
|
||||
o1.default = "/etc/AdGuardHome"
|
||||
o1.datatype = "string"
|
||||
o1.optional = false
|
||||
o1.validate=function(self, value)
|
||||
if fs.stat(value,"type")=="reg" then
|
||||
if m.message then
|
||||
m.message =m.message.."\nerror!backup dir is a file"
|
||||
else
|
||||
m.message ="error!backup dir is a file"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
if string.sub(value,-1)=="/" then
|
||||
return string.sub(value, 1, -2)
|
||||
else
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
----Crontab
|
||||
o = s:option(MultiValue, "crontab", translate("Crontab task"),translate("Please change time and args in crontab"))
|
||||
o:value("autoupdate",translate("Auto update core"))
|
||||
o:value("cutquerylog",translate("Auto tail querylog"))
|
||||
o:value("cutruntimelog",translate("Auto tail runtime log"))
|
||||
o:value("autohost",translate("Auto update ipv6 hosts and restart adh"))
|
||||
o:value("autogfw",translate("Auto update gfwlist and restart adh"))
|
||||
o.widget = "checkbox"
|
||||
o.default = nil
|
||||
o.optional=true
|
||||
|
||||
----downloadpath
|
||||
o = s:option(TextValue, "downloadlinks",translate("Download links for update"))
|
||||
o.optional = false
|
||||
o.rows = 4
|
||||
o.wrap = "soft"
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile("/usr/share/AdGuardHome/links.txt")
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.writefile("/usr/share/AdGuardHome/links.txt", value:gsub("\r\n", "\n"))
|
||||
end
|
||||
fs.writefile("/var/run/lucilogpos","0")
|
||||
function m.on_commit(map)
|
||||
if (fs.access("/var/run/AdGserverdis")) then
|
||||
io.popen("/etc/init.d/AdGuardHome reload &")
|
||||
return
|
||||
end
|
||||
local ucitracktest=uci:get("AdGuardHome","AdGuardHome","ucitracktest")
|
||||
if ucitracktest=="1" then
|
||||
return
|
||||
elseif ucitracktest=="0" then
|
||||
io.popen("/etc/init.d/AdGuardHome reload &")
|
||||
else
|
||||
if (fs.access("/var/run/AdGlucitest")) then
|
||||
uci:set("AdGuardHome","AdGuardHome","ucitracktest","0")
|
||||
io.popen("/etc/init.d/AdGuardHome reload &")
|
||||
else
|
||||
fs.writefile("/var/run/AdGlucitest","")
|
||||
if (ucitracktest=="2") then
|
||||
uci:set("AdGuardHome","AdGuardHome","ucitracktest","1")
|
||||
else
|
||||
uci:set("AdGuardHome","AdGuardHome","ucitracktest","2")
|
||||
end
|
||||
end
|
||||
uci:save("AdGuardHome")
|
||||
end
|
||||
end
|
||||
return m
|
16
luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/log.lua
Normal file
|
@ -0,0 +1,16 @@
|
|||
local fs=require"nixio.fs"
|
||||
local uci=require"luci.model.uci".cursor()
|
||||
local f,t
|
||||
f=SimpleForm("logview")
|
||||
f.reset = false
|
||||
f.submit = false
|
||||
t=f:field(TextValue,"conf")
|
||||
t.rmempty=true
|
||||
t.rows=20
|
||||
t.template="AdGuardHome/log"
|
||||
t.readonly="readonly"
|
||||
local logfile=uci:get("AdGuardHome","AdGuardHome","logfile") or ""
|
||||
t.timereplace=(logfile~="syslog" and logfile~="" )
|
||||
t.pollcheck=logfile~=""
|
||||
fs.writefile("/var/run/lucilogreload","")
|
||||
return f
|
97
luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/manual.lua
Normal file
|
@ -0,0 +1,97 @@
|
|||
local m, s, o
|
||||
local fs = require "nixio.fs"
|
||||
local uci=require"luci.model.uci".cursor()
|
||||
local sys=require"luci.sys"
|
||||
require("string")
|
||||
require("io")
|
||||
require("table")
|
||||
function gen_template_config()
|
||||
local b
|
||||
local d=""
|
||||
for cnt in io.lines("/tmp/resolv.conf.d/resolv.conf.auto") do
|
||||
b=string.match (cnt,"^[^#]*nameserver%s+([^%s]+)$")
|
||||
if (b~=nil) then
|
||||
d=d.." - "..b.."\n"
|
||||
end
|
||||
end
|
||||
local f=io.open("/usr/share/AdGuardHome/AdGuardHome_template.yaml", "r+")
|
||||
local tbl = {}
|
||||
local a=""
|
||||
while (1) do
|
||||
a=f:read("*l")
|
||||
if (a=="#bootstrap_dns") then
|
||||
a=d
|
||||
elseif (a=="#upstream_dns") then
|
||||
a=d
|
||||
elseif (a==nil) then
|
||||
break
|
||||
end
|
||||
table.insert(tbl, a)
|
||||
end
|
||||
f:close()
|
||||
return table.concat(tbl, "\n")
|
||||
end
|
||||
m = Map("AdGuardHome")
|
||||
local configpath = uci:get("AdGuardHome","AdGuardHome","configpath")
|
||||
local binpath = uci:get("AdGuardHome","AdGuardHome","binpath")
|
||||
s = m:section(TypedSection, "AdGuardHome")
|
||||
s.anonymous=true
|
||||
s.addremove=false
|
||||
--- config
|
||||
o = s:option(TextValue, "escconf")
|
||||
o.rows = 66
|
||||
o.wrap = "off"
|
||||
o.rmempty = true
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile("/tmp/AdGuardHometmpconfig.yaml") or fs.readfile(configpath) or gen_template_config() or ""
|
||||
end
|
||||
o.validate=function(self, value)
|
||||
fs.writefile("/tmp/AdGuardHometmpconfig.yaml", value:gsub("\r\n", "\n"))
|
||||
if fs.access(binpath) then
|
||||
if (sys.call(binpath.." -c /tmp/AdGuardHometmpconfig.yaml --check-config 2> /tmp/AdGuardHometest.log")==0) then
|
||||
return value
|
||||
end
|
||||
else
|
||||
return value
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin","services","AdGuardHome","manual"))
|
||||
return nil
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.move("/tmp/AdGuardHometmpconfig.yaml",configpath)
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
fs.writefile(configpath, "")
|
||||
end
|
||||
--- js and reload button
|
||||
o = s:option(DummyValue, "")
|
||||
o.anonymous=true
|
||||
o.template = "AdGuardHome/yamleditor"
|
||||
if not fs.access(binpath) then
|
||||
o.description=translate("WARNING!!! no bin found apply config will not be test")
|
||||
end
|
||||
--- log
|
||||
if (fs.access("/tmp/AdGuardHometmpconfig.yaml")) then
|
||||
local c=fs.readfile("/tmp/AdGuardHometest.log")
|
||||
if (c~="") then
|
||||
o = s:option(TextValue, "")
|
||||
o.readonly=true
|
||||
o.rows = 5
|
||||
o.rmempty = true
|
||||
o.name=""
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile("/tmp/AdGuardHometest.log")
|
||||
end
|
||||
end
|
||||
end
|
||||
function m.on_commit(map)
|
||||
local ucitracktest=uci:get("AdGuardHome","AdGuardHome","ucitracktest")
|
||||
if ucitracktest=="1" then
|
||||
return
|
||||
elseif ucitracktest=="0" then
|
||||
io.popen("/etc/init.d/AdGuardHome reload &")
|
||||
else
|
||||
fs.writefile("/var/run/AdGlucitest","")
|
||||
end
|
||||
end
|
||||
return m
|
|
@ -0,0 +1,78 @@
|
|||
<%+cbi/valueheader%>
|
||||
<%local fs=require"nixio.fs"%>
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" id="apply_update_button" value="<%:Update core version%>" onclick=" return apply_update() "/>
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" id="apply_forceupdate_button" value="<%:Force update%>" onclick=" return apply_forceupdate()" style="display:none"/>
|
||||
<% if self.showfastconfig then %>
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" id="to_configpage" value="<%:Fast config%>" onclick="location.href='<%=url([[admin]], [[services]], [[AdGuardHome]], [[manual]])%>'"/>
|
||||
<%end%>
|
||||
<div id="logview" style="display:none">
|
||||
<input type="checkbox" id="reversetag" value="reverse" onclick=" return reverselog()" style="vertical-align:middle;height: auto;"><%:reverse%></input>
|
||||
<textarea id="cbid.logview.1.conf" class="cbi-input-textarea" style="width: 100%;display:block;" data-update="change" rows="5" cols="60" readonly="readonly" > </textarea>
|
||||
</div>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var updatebtn = document.getElementById('apply_update_button');
|
||||
var forceupdatebtn = document.getElementById('apply_forceupdate_button');
|
||||
var islogreverse = false;
|
||||
function apply_forceupdate(){
|
||||
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[doupdate]])%>',{ force: 1 },function(x, data){}
|
||||
);
|
||||
updatebtn.disabled = true;
|
||||
poll_check();
|
||||
return
|
||||
}
|
||||
function reverselog(){
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
lv.innerHTML=lv.innerHTML.split('\n').reverse().join('\n')
|
||||
if (islogreverse){
|
||||
islogreverse=false;
|
||||
}else{
|
||||
islogreverse=true;
|
||||
}
|
||||
return
|
||||
}
|
||||
function apply_update(){
|
||||
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[doupdate]])%>',null,function(x, data){}
|
||||
);
|
||||
updatebtn.disabled = true;
|
||||
updatebtn.value = '<%:Check...%>';
|
||||
forceupdatebtn.style.display="inline"
|
||||
poll_check();
|
||||
return
|
||||
}
|
||||
function poll_check(){
|
||||
var tag = document.getElementById('logview');
|
||||
tag.style.display="block"
|
||||
XHR.poll(3, '<%=url([[admin]], [[services]], [[AdGuardHome]], [[check]])%>', null,
|
||||
function(x, data) {
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
if (x.responseText && lv) {
|
||||
if (x.responseText=="\u0000"){
|
||||
for(j = 0,len=this.XHR._q.length; j < len; j++) {
|
||||
if (this.XHR._q[j].url == '<%=url([[admin]], [[services]], [[AdGuardHome]], [[check]])%>'){
|
||||
this.XHR._q.splice(j,1);
|
||||
updatebtn.disabled = false;
|
||||
updatebtn.value = '<%:Updated%>';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (islogreverse){
|
||||
lv.innerHTML = x.responseText.split('\n').reverse().join('\n')+lv.innerHTML;
|
||||
}else{
|
||||
lv.innerHTML += x.responseText;
|
||||
}
|
||||
}
|
||||
}
|
||||
);}
|
||||
<% if fs.access("/var/run/update_core") then %>
|
||||
updatebtn.disabled = true;
|
||||
updatebtn.value = '<%:Check...%>';
|
||||
forceupdatebtn.style.display="inline"
|
||||
poll_check();
|
||||
<%elseif fs.access("/var/run/update_core_error") then %>
|
||||
poll_check();
|
||||
<%end%>
|
||||
//]]>
|
||||
</script>
|
||||
<%+cbi/valuefooter%>
|
|
@ -0,0 +1,49 @@
|
|||
<%+cbi/valueheader%>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
function chpass(btn)
|
||||
{
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:loading...%>';
|
||||
if (typeof bcryptloaded == 'undefined' ){
|
||||
var theHead = document.getElementsByTagName('head').item(0);
|
||||
//创建脚本的dom对象实例
|
||||
var myScript = document.createElement('script');
|
||||
myScript.src = '<%=resource%>/twin-bcrypt.min.js'; //指定脚本路径
|
||||
myScript.type = 'text/javascript'; //指定脚本类型
|
||||
myScript.defer = true; //程序下载完后再解析和执行
|
||||
theHead.appendChild(myScript);
|
||||
bcryptloaded=1;
|
||||
btn.value = '<%:Culculate%>';
|
||||
btn.disabled = false;
|
||||
return
|
||||
}
|
||||
var lv = document.getElementById('cbid.AdGuardHome.AdGuardHome.hashpass');
|
||||
if (lv.value != ""){
|
||||
var hash = TwinBcrypt.hashSync(lv.value);
|
||||
lv.value=hash;
|
||||
btn.value = '<%:Please save/apply%>';
|
||||
}else{
|
||||
btn.value = '<%:is empty%>';
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
<input data-update="change"<%=
|
||||
attr("id", cbid) ..
|
||||
attr("name", cbid) ..
|
||||
attr("type", self.password and "password" or "text") ..
|
||||
attr("class", self.password and "cbi-input-password" or "cbi-input-text") ..
|
||||
attr("value", self:cfgvalue(section) or self.default) ..
|
||||
ifattr(self.size, "size") ..
|
||||
ifattr(self.placeholder, "placeholder") ..
|
||||
ifattr(self.readonly, "readonly") ..
|
||||
ifattr(self.maxlength, "maxlength") ..
|
||||
ifattr(self.datatype, "data-type", self.datatype) ..
|
||||
ifattr(self.datatype, "data-optional", self.optional or self.rmempty) ..
|
||||
ifattr(self.combobox_manual, "data-manual", self.combobox_manual) ..
|
||||
ifattr(#self.keylist > 0, "data-choices", { self.keylist, self.vallist })
|
||||
%> />
|
||||
<% if self.password then %><img src="<%=resource%>/cbi/reload.gif" style="vertical-align:middle" title="<%:Reveal/hide password%>" onclick="var e = document.getElementById('<%=cbid%>'); e.type = (e.type=='password') ? 'text' : 'password';" /><% end %>
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" id="cbid.AdGuardHome.AdGuardHome.applychpass" value="<%:Load culculate model%>" onclick="return chpass(this)"/>
|
||||
<%+cbi/valuefooter%>
|
|
@ -0,0 +1,27 @@
|
|||
<script type="text/javascript">//<![CDATA[
|
||||
XHR.poll(3, '<%=url([[admin]], [[services]], [[AdGuardHome]], [[status]])%>', null,
|
||||
function(x, data) {
|
||||
var tb = document.getElementById('AdGuardHome_status');
|
||||
if (data && tb) {
|
||||
if (data.running) {
|
||||
tb.innerHTML = '<em><b><font color=green>AdGuardHome <%:RUNNING%></font></b></em>';
|
||||
} else {
|
||||
tb.innerHTML = '<em><b><font color=red>AdGuardHome <%:NOT RUNNING%></font></b></em>';
|
||||
}
|
||||
if (data.redirect)
|
||||
{
|
||||
tb.innerHTML+='<em><b><font color=green><%:Redirected%></font></b></em>'
|
||||
} else {
|
||||
tb.innerHTML+='<em><b><font color=red><%:Not redirect%></font></b></em>'
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
//]]>
|
||||
</script>
|
||||
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
|
||||
<fieldset class="cbi-section">
|
||||
<p id="AdGuardHome_status">
|
||||
<em><%:Collecting data...%></em>
|
||||
</p>
|
||||
</fieldset>
|
111
luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm
Normal file
|
@ -0,0 +1,111 @@
|
|||
<%+cbi/valueheader%>
|
||||
<input type="checkbox" name="NAME" value="reverse" onclick=" return reverselog()" style="vertical-align:middle;height: auto;" checked><%:reverse%></input>
|
||||
<%if self.timereplace then%>
|
||||
<input type="checkbox" name="NAME" value="localtime" onclick=" return chlogtime()" style="vertical-align:middle;height: auto;" checked><%:localtime%></input><br>
|
||||
<%end%>
|
||||
<textarea id="cbid.logview.1.conf" class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="32" cols="60" readonly="readonly" > </textarea>
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" id="apply_update_button" value="<%:dellog%>" onclick=" return apply_del_log() "/>
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:download log%>" style=" display:inline;" onclick=" return download_log()" />
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var islogreverse = true;
|
||||
var isutc2local = <%=tostring(self.timereplace)%>;
|
||||
function createAndDownloadFile(fileName, content) {
|
||||
var aTag = document.createElement('a');
|
||||
var blob = new Blob([content]);
|
||||
aTag.download = fileName;
|
||||
aTag.href = URL.createObjectURL(blob);
|
||||
aTag.click();
|
||||
URL.revokeObjectURL(blob);
|
||||
}
|
||||
function download_log(){
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
var dt = new Date();
|
||||
var timestamp = (dt.getMonth()+1)+"-"+dt.getDate()+"-"+dt.getHours()+"_"+dt.getMinutes();
|
||||
createAndDownloadFile("AdGuardHome"+timestamp+".log",lv.innerHTML)
|
||||
return
|
||||
}
|
||||
function apply_del_log(){
|
||||
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[dodellog]])%>',null,function(x, data){
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
lv.innerHTML="";
|
||||
}
|
||||
);
|
||||
return
|
||||
}
|
||||
function chlogtime(){
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
if (isutc2local){
|
||||
lv.innerHTML=line_toUTC(lv.innerHTML).join('\n');
|
||||
isutc2local=false;
|
||||
}else{
|
||||
lv.innerHTML=line_tolocal(lv.innerHTML).join('\n');
|
||||
isutc2local=true;
|
||||
}
|
||||
return
|
||||
}
|
||||
function reverselog(){
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
lv.innerHTML=lv.innerHTML.split('\n').reverse().join('\n')
|
||||
if (islogreverse){
|
||||
islogreverse=false;
|
||||
}else{
|
||||
islogreverse=true;
|
||||
}
|
||||
return
|
||||
}
|
||||
function p(s) {
|
||||
return s < 10 ? '0' + s: s;
|
||||
}
|
||||
function line_tolocal(str){
|
||||
var strt=new Array();
|
||||
str.trim().split('\n').forEach(function(v, i) {
|
||||
var dt = new Date(v.substring(0,19)+" UTC");
|
||||
if (dt != "Invalid Date"){
|
||||
strt[i]=dt.getFullYear()+"/"+p(dt.getMonth()+1)+"/"+p(dt.getDate())+" "+p(dt.getHours())+":"+p(dt.getMinutes())+":"+p(dt.getSeconds())+v.substring(19);
|
||||
}else{
|
||||
strt[i]=v;}})
|
||||
return strt
|
||||
}
|
||||
function line_toUTC(str){
|
||||
var strt=new Array();
|
||||
str.trim().split('\n').forEach(function(v, i) {
|
||||
var dt = new Date(v.substring(0,19))
|
||||
if (dt != "Invalid Date"){
|
||||
strt[i]=dt.getUTCFullYear()+"/"+p(dt.getUTCMonth()+1)+"/"+p(dt.getUTCDate())+" "+p(dt.getUTCHours())+":"+p(dt.getUTCMinutes())+":"+p(dt.getUTCSeconds())+v.substring(19);
|
||||
}else{
|
||||
strt[i]=v;}})
|
||||
return strt
|
||||
}
|
||||
function poll_check(){
|
||||
XHR.poll(3, '<%=url([[admin]], [[services]], [[AdGuardHome]], [[getlog]])%>', null,
|
||||
function(x, data) {
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
if (x.responseText && lv) {
|
||||
if (isutc2local)
|
||||
{
|
||||
var lines=line_toUTC(x.responseText);
|
||||
if (islogreverse){
|
||||
lv.innerHTML = lines.reverse().join('\n')+lv.innerHTML;
|
||||
}else{
|
||||
lv.innerHTML += lines.join('\n');
|
||||
}
|
||||
lv.innerHTML=line_tolocal(lv.innerHTML).join('\n');
|
||||
}else{
|
||||
if (islogreverse){
|
||||
lv.innerHTML = x.responseText.split('\n').reverse().join('\n')+lv.innerHTML;
|
||||
}else{
|
||||
lv.innerHTML += x.responseText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);}
|
||||
<%if self.pollcheck then%>
|
||||
poll_check();
|
||||
<%else%>
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
lv.innerHTML="<%:Please add log path in config to enable log%>"
|
||||
<%end%>
|
||||
//]]>
|
||||
</script>
|
||||
<%+cbi/valuefooter%>
|
39
luci-app-adguardhome/luasrc/view/AdGuardHome/yamleditor.htm
Normal file
|
@ -0,0 +1,39 @@
|
|||
<%+cbi/valueheader%>
|
||||
<script src="/luci-static/resources/codemirror/lib/codemirror.js"></script>
|
||||
<link rel="stylesheet" href="/luci-static/resources/codemirror/lib/codemirror.css"/>
|
||||
<script src="/luci-static/resources/codemirror/mode/yaml/yaml.js"></script>
|
||||
<link rel="stylesheet" href="/luci-static/resources/codemirror/theme/dracula.css"/>
|
||||
<link rel="stylesheet" href="/luci-static/resources/codemirror/addon/fold/foldgutter.css"/>
|
||||
<script src="/luci-static/resources/codemirror/addon/fold/foldcode.js"></script>
|
||||
<script src="/luci-static/resources/codemirror/addon/fold/foldgutter.js"></script>
|
||||
<script src="/luci-static/resources/codemirror/addon/fold/indent-fold.js"></script>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var editor = CodeMirror.fromTextArea(document.getElementById("cbid.AdGuardHome.AdGuardHome.escconf"), {
|
||||
mode: "text/yaml", //实现groovy代码高亮
|
||||
styleActiveLine: true,
|
||||
lineNumbers: true, //显示行号
|
||||
theme: "dracula", //设置主题
|
||||
lineWrapping: true, //代码折叠
|
||||
foldGutter: true,
|
||||
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||
matchBrackets: true //括号匹配
|
||||
}
|
||||
);
|
||||
function reload_config(){
|
||||
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[reloadconfig]])%>', null,
|
||||
function(x, data) {
|
||||
location.reload();
|
||||
});}
|
||||
function use_template(){
|
||||
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[gettemplateconfig]])%>', null,
|
||||
function(x, data) {
|
||||
editor.setValue(x.responseText)
|
||||
});}
|
||||
//]]>
|
||||
</script>
|
||||
<%fs=require"nixio.fs"%>
|
||||
<%if fs.access("/tmp/AdGuardHometmpconfig.yaml") then%>
|
||||
<input type="button" id="apply_update_button" value="<%:Reload Config%>" onclick=" return reload_config() "/>
|
||||
<%end%>
|
||||
<input type="button" id="template_button" value="<%:Use template%>" onclick=" return use_template() "/>
|
||||
<%+cbi/valuefooter%>
|
1
luci-app-adguardhome/po/zh-cn
Normal file
|
@ -0,0 +1 @@
|
|||
zh_Hans
|
408
luci-app-adguardhome/po/zh_Hans/adguardhome.po
Normal file
|
@ -0,0 +1,408 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: zh_Hans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:27
|
||||
msgid ""
|
||||
"<input type=\"button\" style=\"width:210px;border-color:Teal; text-align:"
|
||||
"center;font-weight:bold;color:Green;\" value=\"AdGuardHome Web:"
|
||||
msgstr ""
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/controller/AdGuardHome.lua:6
|
||||
msgid "AdGuard Home"
|
||||
msgstr ""
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:67
|
||||
msgid "AdGuardHome Bin path if no bin will auto download"
|
||||
msgstr "AdGuardHome 执行文件路径 如果没有执行文件将自动下载"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:99
|
||||
msgid "AdGuardHome config path"
|
||||
msgstr "AdGuardHome 配置文件路径"
|
||||
|
||||
#
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:58
|
||||
msgid "AdGuardHome redirect mode"
|
||||
msgstr "AdGuardHome重定向模式"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:142
|
||||
msgid ""
|
||||
"AdGuardHome runtime Log file if 'syslog': write to system log;if empty no log"
|
||||
msgstr "AdGuardHome 运行日志 如果填syslog将写入系统日志;如果空则不记录日志"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:120
|
||||
msgid "AdGuardHome work dir include rules,audit log and database"
|
||||
msgstr "AdGuardHome 工作目录包含规则,审计日志和数据库"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:179
|
||||
msgid "Add"
|
||||
msgstr "添加"
|
||||
|
||||
# hide div
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:177
|
||||
msgid "Add gfwlist"
|
||||
msgstr "加入gfw列表"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:259
|
||||
msgid "Auto tail querylog"
|
||||
msgstr "自动截短查询日志"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:260
|
||||
msgid "Auto tail runtime log"
|
||||
msgstr "自动截短运行日志"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:258
|
||||
msgid "Auto update core"
|
||||
msgstr "自动升级核心"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:262
|
||||
msgid "Auto update gfwlist and restart adh"
|
||||
msgstr "自动更新gfw列表并重启adh"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:261
|
||||
msgid "Auto update ipv6 hosts and restart adh"
|
||||
msgstr "自动更新ipv6主机并重启adh"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:212
|
||||
msgid "Backup workdir files when shutdown"
|
||||
msgstr "在关机时备份工作目录文件"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:213
|
||||
msgid "Backup workdir path"
|
||||
msgstr "工作目录备份路径"
|
||||
|
||||
# /cgi-bin/luci/admin/services/AdGuardHome
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/controller/AdGuardHome.lua:7
|
||||
msgid "Base Setting"
|
||||
msgstr "基础设置"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:67
|
||||
msgid "Bin Path"
|
||||
msgstr "执行文件路径"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:22
|
||||
msgid "Browser management port"
|
||||
msgstr "网页管理端口"
|
||||
|
||||
# hide div
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:189
|
||||
msgid "Change browser management password"
|
||||
msgstr "改变网页登录密码"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm:37
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm:70
|
||||
msgid "Check..."
|
||||
msgstr "检查中..."
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm:25
|
||||
msgid "Collecting data..."
|
||||
msgstr "获取数据中..."
|
||||
|
||||
#
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:99
|
||||
msgid "Config Path"
|
||||
msgstr "配置文件路径"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:257
|
||||
msgid "Crontab task"
|
||||
msgstr "计划任务"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm:16
|
||||
msgid "Culculate"
|
||||
msgstr "计算"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:172
|
||||
msgid "Del"
|
||||
msgstr "删除"
|
||||
|
||||
# hide div
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:170
|
||||
msgid "Del gfwlist"
|
||||
msgstr "删除gfw列表"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:268
|
||||
msgid "Download links for update"
|
||||
msgstr "升级用的下载链接"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:18
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm:6
|
||||
msgid "Fast config"
|
||||
msgstr "快速配置"
|
||||
|
||||
# button hide
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm:4
|
||||
msgid "Force update"
|
||||
msgstr "强制更新"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:11
|
||||
msgid ""
|
||||
"Free and open source, powerful network-wide ads & trackers blocking DNS "
|
||||
"server."
|
||||
msgstr "免费开源,功能强大的全网络广告和跟踪程序拦截DNS服务器"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:184
|
||||
msgid "Gfwlist domain upstream dns service"
|
||||
msgstr "gfw列表域名上游服务器"
|
||||
|
||||
# hide div
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:184
|
||||
msgid "Gfwlist upstream dns server"
|
||||
msgstr "gfw列表上游服务器"
|
||||
|
||||
#
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:195
|
||||
msgid "Keep files when system upgrade"
|
||||
msgstr "系统升级时保留文件"
|
||||
|
||||
# #button change
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm:48
|
||||
msgid "Load culculate model"
|
||||
msgstr "载入计算模块"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/controller/AdGuardHome.lua:8
|
||||
msgid "Log"
|
||||
msgstr "日志"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/controller/AdGuardHome.lua:9
|
||||
msgid "Manual Config"
|
||||
msgstr "手动设置"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm:9
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm:15
|
||||
msgid "Not redirect"
|
||||
msgstr "未重定向"
|
||||
|
||||
#
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:207
|
||||
msgid "On boot when network ok restart"
|
||||
msgstr "开机后网络准备好时重启"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm:106
|
||||
msgid "Please add log path in config to enable log"
|
||||
msgstr "请在设置里填写日志路径以启用日志"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:257
|
||||
msgid "Please change time and args in crontab"
|
||||
msgstr "请在计划任务中修改时间和参数"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm:24
|
||||
msgid "Please save/apply"
|
||||
msgstr "请提交"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:189
|
||||
msgid "Press load culculate model and culculate finally save/apply"
|
||||
msgstr "按载入计算模块 然后计算 最后保存/提交"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm:7
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
#
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:58
|
||||
msgid "Redirect"
|
||||
msgstr "重定向"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:62
|
||||
msgid "Redirect 53 port to AdGuardHome"
|
||||
msgstr "重定向53端口到AdGuardHome"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm:13
|
||||
msgid "Redirected"
|
||||
msgstr "已重定向"
|
||||
|
||||
# hide button
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/yamleditor.htm:36
|
||||
msgid "Reload Config"
|
||||
msgstr "重新载入配置"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm:47
|
||||
msgid "Reveal/hide password"
|
||||
msgstr ""
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:61
|
||||
msgid "Run as dnsmasq upstream server"
|
||||
msgstr "作为dnsmasq的上游服务器"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:142
|
||||
msgid "Runtime log file"
|
||||
msgstr "运行日志"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:49
|
||||
msgid "Update"
|
||||
msgstr "更新"
|
||||
|
||||
# button change
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:50
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm:3
|
||||
msgid "Update core version"
|
||||
msgstr "更新核心版本"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm:54
|
||||
msgid "Updated"
|
||||
msgstr "已更新"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:63
|
||||
msgid "Use port 53 replace dnsmasq"
|
||||
msgstr "使用53端口替换dnsmasq"
|
||||
|
||||
# /cgi-bin/luci//admin/services/AdGuardHome/manual/
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/yamleditor.htm:38
|
||||
msgid "Use template"
|
||||
msgstr "使用模板"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:160
|
||||
msgid "Verbose log"
|
||||
msgstr "详细日志"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/manual.lua:71
|
||||
msgid "WARNING!!! no bin found apply config will not be test"
|
||||
msgstr "警告!!!未找到执行文件,提交配置将不会进行校验"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:234
|
||||
msgid "Will be restore when workdir/data is empty"
|
||||
msgstr "在工作目录/data为空的时候恢复"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:120
|
||||
msgid "Work dir"
|
||||
msgstr "工作目录"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:96
|
||||
msgid "bin use less space,but may have compatibility issues"
|
||||
msgstr "减小执行文件空间占用,但是可能压缩后有兼容性问题"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:92
|
||||
msgid "compress best(can be slow for big files)"
|
||||
msgstr "最好的压缩(大文件可能慢)"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:91
|
||||
msgid "compress better"
|
||||
msgstr "更好的压缩"
|
||||
|
||||
# inlist
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:90
|
||||
msgid "compress faster"
|
||||
msgstr "快速压缩"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:197
|
||||
msgid "config file"
|
||||
msgstr "配置文件"
|
||||
|
||||
# checkbox
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:196
|
||||
msgid "core bin"
|
||||
msgstr "核心执行文件"
|
||||
|
||||
#
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:53
|
||||
msgid "core version:"
|
||||
msgstr "核心版本:"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm:7
|
||||
msgid "dellog"
|
||||
msgstr "删除日志"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm:8
|
||||
msgid "download log"
|
||||
msgstr "下载日志"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:202
|
||||
msgid "filters"
|
||||
msgstr ""
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm:26
|
||||
msgid "is empty"
|
||||
msgstr "为空"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm:6
|
||||
msgid "loading..."
|
||||
msgstr "载入中"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm:4
|
||||
msgid "localtime"
|
||||
msgstr "本地时间"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:198
|
||||
msgid "log file"
|
||||
msgstr "日志文件"
|
||||
|
||||
# description change
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:32
|
||||
msgid "no config"
|
||||
msgstr "没有配置文件"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:35
|
||||
msgid "no core"
|
||||
msgstr "没有核心"
|
||||
|
||||
# inlist
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:60
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:89
|
||||
msgid "none"
|
||||
msgstr "无"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:201
|
||||
msgid "querylog.json"
|
||||
msgstr "审计日志.json"
|
||||
|
||||
# /cgi-bin/luci/admin/services/AdGuardHome/log/
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm:9
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm:2
|
||||
msgid "reverse"
|
||||
msgstr "逆序"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:199
|
||||
msgid "sessions.db"
|
||||
msgstr ""
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:200
|
||||
msgid "stats.db"
|
||||
msgstr ""
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:93
|
||||
msgid "try all available compression methods & filters [slow]"
|
||||
msgstr "尝试所有可能的压缩方法和过滤器[慢]"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:94
|
||||
msgid "try even more compression variants [very slow]"
|
||||
msgstr "尝试更多变体压缩手段[很慢]"
|
||||
|
||||
#: /mnt/A/openwrt-latest/package/ctcgfw/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua:88
|
||||
msgid "use upx to compress bin after download"
|
||||
msgstr "下载后使用upx压缩执行文件"
|
||||
|
||||
#~ msgid "Added"
|
||||
#~ msgstr "已添加"
|
||||
|
||||
#~ msgid "Not added"
|
||||
#~ msgstr "未添加"
|
||||
|
||||
# unused
|
||||
#~ msgid "Change browser management username"
|
||||
#~ msgstr "改变网页登录用户名"
|
||||
|
||||
#~ msgid "Username"
|
||||
#~ msgstr "用户名"
|
||||
|
||||
#~ msgid "Check Config"
|
||||
#~ msgstr "检查配置"
|
||||
|
||||
#~ msgid "unknown"
|
||||
#~ msgstr "未知"
|
||||
|
||||
#~ msgid "Keep database when system upgrade"
|
||||
#~ msgstr "系统升级时保留数据"
|
||||
|
||||
#~ msgid "Boot delay until network ok"
|
||||
#~ msgstr "开机时直到网络准备好再启动"
|
11
luci-app-adguardhome/root/etc/config/AdGuardHome
Normal file
|
@ -0,0 +1,11 @@
|
|||
config AdGuardHome 'AdGuardHome'
|
||||
option enabled '0'
|
||||
option httpport '3000'
|
||||
option redirect 'none'
|
||||
option configpath '/etc/AdGuardHome.yaml'
|
||||
option workdir '/etc/AdGuardHome'
|
||||
option logfile '/tmp/AdGuardHome.log'
|
||||
option verbose '0'
|
||||
option binpath '/usr/bin/AdGuardHome'
|
||||
option upxflag ''
|
||||
|
642
luci-app-adguardhome/root/etc/init.d/AdGuardHome
Normal file
|
@ -0,0 +1,642 @@
|
|||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
|
||||
START=95
|
||||
STOP=01
|
||||
|
||||
CONFIGURATION=AdGuardHome
|
||||
CRON_FILE=/etc/crontabs/root
|
||||
|
||||
extra_command "do_redirect" "0 or 1"
|
||||
extra_command "testbackup" "backup or restore"
|
||||
extra_command "test_crontab"
|
||||
extra_command "force_reload"
|
||||
extra_command "isrunning"
|
||||
|
||||
set_forward_dnsmasq()
|
||||
{
|
||||
local PORT="$1"
|
||||
addr="127.0.0.1#$PORT"
|
||||
OLD_SERVER="`uci get dhcp.@dnsmasq[0].server 2>/dev/null`"
|
||||
echo $OLD_SERVER | grep "^$addr" >/dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
return
|
||||
fi
|
||||
uci delete dhcp.@dnsmasq[0].server 2>/dev/null
|
||||
uci add_list dhcp.@dnsmasq[0].server=$addr
|
||||
for server in $OLD_SERVER; do
|
||||
if [ "$server" = "$addr" ]; then
|
||||
continue
|
||||
fi
|
||||
# uci add_list dhcp.@dnsmasq[0].server=$server
|
||||
done
|
||||
uci delete dhcp.@dnsmasq[0].resolvfile 2>/dev/null
|
||||
uci set dhcp.@dnsmasq[0].noresolv=1
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq restart
|
||||
}
|
||||
|
||||
stop_forward_dnsmasq()
|
||||
{
|
||||
local OLD_PORT="$1"
|
||||
addr="127.0.0.1#$OLD_PORT"
|
||||
OLD_SERVER="`uci get dhcp.@dnsmasq[0].server 2>/dev/null`"
|
||||
echo $OLD_SERVER | grep "^$addr" >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
uci del_list dhcp.@dnsmasq[0].server=$addr 2>/dev/null
|
||||
addrlist="`uci get dhcp.@dnsmasq[0].server 2>/dev/null`"
|
||||
if [ -z "$addrlist" ] ; then
|
||||
uci set dhcp.@dnsmasq[0].resolvfile=/tmp/resolv.conf.d/resolv.conf.auto 2>/dev/null
|
||||
uci delete dhcp.@dnsmasq[0].noresolv 2>/dev/null
|
||||
fi
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq restart
|
||||
}
|
||||
|
||||
set_iptable()
|
||||
{
|
||||
local ipv6_server=$1
|
||||
local tcp_server=$2
|
||||
uci -q batch <<-EOF >/dev/null 2>&1
|
||||
delete firewall.AdGuardHome
|
||||
set firewall.AdGuardHome=include
|
||||
set firewall.AdGuardHome.type=script
|
||||
set firewall.AdGuardHome.path=/usr/share/AdGuardHome/firewall.start
|
||||
set firewall.AdGuardHome.reload=1
|
||||
commit firewall
|
||||
EOF
|
||||
|
||||
IPS="`ifconfig | grep "inet addr" | grep -v ":127" | grep "Bcast" | awk '{print $2}' | awk -F : '{print $2}'`"
|
||||
for IP in $IPS
|
||||
do
|
||||
if [ "$tcp_server" == "1" ]; then
|
||||
iptables -t nat -A PREROUTING -p tcp -d $IP --dport 53 -j REDIRECT --to-ports $AdGuardHome_PORT >/dev/null 2>&1
|
||||
fi
|
||||
iptables -t nat -A PREROUTING -p udp -d $IP --dport 53 -j REDIRECT --to-ports $AdGuardHome_PORT >/dev/null 2>&1
|
||||
done
|
||||
|
||||
if [ "$ipv6_server" == 0 ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
IPS="`ifconfig | grep "inet6 addr" | grep -v " fe80::" | grep -v " ::1" | grep "Global" | awk '{print $3}'`"
|
||||
for IP in $IPS
|
||||
do
|
||||
if [ "$tcp_server" == "1" ]; then
|
||||
ip6tables -t nat -A PREROUTING -p tcp -d $IP --dport 53 -j REDIRECT --to-ports $AdGuardHome_PORT >/dev/null 2>&1
|
||||
fi
|
||||
ip6tables -t nat -A PREROUTING -p udp -d $IP --dport 53 -j REDIRECT --to-ports $AdGuardHome_PORT >/dev/null 2>&1
|
||||
done
|
||||
}
|
||||
|
||||
clear_iptable()
|
||||
{
|
||||
uci -q batch <<-EOF >/dev/null 2>&1
|
||||
delete firewall.AdGuardHome
|
||||
commit firewall
|
||||
EOF
|
||||
local OLD_PORT="$1"
|
||||
local ipv6_server=$2
|
||||
IPS="`ifconfig | grep "inet addr" | grep -v ":127" | grep "Bcast" | awk '{print $2}' | awk -F : '{print $2}'`"
|
||||
for IP in $IPS
|
||||
do
|
||||
iptables -t nat -D PREROUTING -p udp -d $IP --dport 53 -j REDIRECT --to-ports $OLD_PORT >/dev/null 2>&1
|
||||
iptables -t nat -D PREROUTING -p tcp -d $IP --dport 53 -j REDIRECT --to-ports $OLD_PORT >/dev/null 2>&1
|
||||
done
|
||||
|
||||
if [ "$ipv6_server" == 0 ]; then
|
||||
return
|
||||
fi
|
||||
echo "warn ip6tables nat mod is needed"
|
||||
IPS="`ifconfig | grep "inet6 addr" | grep -v " fe80::" | grep -v " ::1" | grep "Global" | awk '{print $3}'`"
|
||||
for IP in $IPS
|
||||
do
|
||||
ip6tables -t nat -D PREROUTING -p udp -d $IP --dport 53 -j REDIRECT --to-ports $OLD_PORT >/dev/null 2>&1
|
||||
ip6tables -t nat -D PREROUTING -p tcp -d $IP --dport 53 -j REDIRECT --to-ports $OLD_PORT >/dev/null 2>&1
|
||||
done
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONFIGURATION"
|
||||
[ "$(uci get AdGuardHome.AdGuardHome.redirect)" == "redirect" ] && procd_add_reload_trigger firewall
|
||||
}
|
||||
|
||||
isrunning(){
|
||||
config_load "${CONFIGURATION}"
|
||||
_isrunning
|
||||
local r=$?
|
||||
([ "$r" == "0" ] && echo "running") || ([ "$r" == "1" ] && echo "not run" ) || echo "no bin"
|
||||
return $r
|
||||
}
|
||||
|
||||
_isrunning(){
|
||||
config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome"
|
||||
[ ! -f "$binpath" ] && return 2
|
||||
pgrep $binpath 2>&1 >/dev/null && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
force_reload(){
|
||||
config_load "${CONFIGURATION}"
|
||||
_isrunning && procd_send_signal "$CONFIGURATION" || start
|
||||
}
|
||||
|
||||
get_tz()
|
||||
{
|
||||
SET_TZ=""
|
||||
|
||||
if [ -e "/etc/localtime" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
for tzfile in /etc/TZ /var/etc/TZ
|
||||
do
|
||||
if [ ! -e "$tzfile" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
tz="`cat $tzfile 2>/dev/null`"
|
||||
done
|
||||
|
||||
if [ -z "$tz" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
SET_TZ=$tz
|
||||
}
|
||||
|
||||
rm_port53()
|
||||
{
|
||||
local AdGuardHome_PORT=$(config_editor "dns.port" "" "$configpath" "1")
|
||||
dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)
|
||||
if [ -z "$dnsmasq_port" ]; then
|
||||
dnsmasq_port="53"
|
||||
fi
|
||||
if [ "$dnsmasq_port" == "$AdGuardHome_PORT" ]; then
|
||||
if [ "$dnsmasq_port" == "53" ]; then
|
||||
dnsmasq_port="1745"
|
||||
fi
|
||||
elif [ "$dnsmasq_port" == "53" ]; then
|
||||
return
|
||||
fi
|
||||
config_editor "dns.port" "$dnsmasq_port" "$configpath"
|
||||
uci set dhcp.@dnsmasq[0].port="53"
|
||||
uci commit dhcp
|
||||
config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome"
|
||||
killall -9 $binpath
|
||||
/etc/init.d/dnsmasq restart
|
||||
}
|
||||
|
||||
use_port53()
|
||||
{
|
||||
local AdGuardHome_PORT=$(config_editor "dns.port" "" "$configpath" "1")
|
||||
dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)
|
||||
if [ -z "$dnsmasq_port" ]; then
|
||||
dnsmasq_port="53"
|
||||
fi
|
||||
if [ "$dnsmasq_port" == "$AdGuardHome_PORT" ]; then
|
||||
if [ "$dnsmasq_port" == "53" ]; then
|
||||
AdGuardHome_PORT="1745"
|
||||
fi
|
||||
elif [ "$AdGuardHome_PORT" == "53" ]; then
|
||||
return
|
||||
fi
|
||||
config_editor "dns.port" "53" "$configpath"
|
||||
uci set dhcp.@dnsmasq[0].port="$AdGuardHome_PORT"
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq reload
|
||||
}
|
||||
|
||||
do_redirect()
|
||||
{
|
||||
config_load "${CONFIGURATION}"
|
||||
_do_redirect $1
|
||||
}
|
||||
|
||||
_do_redirect()
|
||||
{
|
||||
local section="$CONFIGURATION"
|
||||
args=""
|
||||
ipv6_server=1
|
||||
tcp_server=0
|
||||
enabled=$1
|
||||
if [ "$enabled" == "1" ]; then
|
||||
echo -n "1">/var/run/AdGredir
|
||||
else
|
||||
echo -n "0">/var/run/AdGredir
|
||||
fi
|
||||
config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
|
||||
AdGuardHome_PORT=$(config_editor "dns.port" "" "$configpath" "1")
|
||||
if [ ! -s "$configpath" ]; then
|
||||
cp -f /usr/share/AdGuardHome/AdGuardHome_template.yaml $configpath
|
||||
fi
|
||||
if [ -z "$AdGuardHome_PORT" ]; then
|
||||
AdGuardHome_PORT="0"
|
||||
fi
|
||||
config_get "redirect" "$section" "redirect" "none"
|
||||
config_get "old_redirect" "$section" "old_redirect" "none"
|
||||
config_get "old_port" "$section" "old_port" "0"
|
||||
config_get "old_enabled" "$section" "old_enabled" "0"
|
||||
uci get dhcp.@dnsmasq[0].port >/dev/null 2>&1 || uci set dhcp.@dnsmasq[0].port="53" >/dev/null 2>&1
|
||||
if [ "$old_enabled" = "1" -a "$old_redirect" == "exchange" ]; then
|
||||
AdGuardHome_PORT=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ "$old_redirect" != "$redirect" ] || [ "$old_port" != "$AdGuardHome_PORT" ] || [ "$old_enabled" = "1" -a "$enabled" = "0" ]; then
|
||||
if [ "$old_redirect" != "none" ]; then
|
||||
if [ "$old_redirect" == "redirect" -a "$old_port" != "0" ]; then
|
||||
clear_iptable "$old_port" "$ipv6_server"
|
||||
elif [ "$old_redirect" == "dnsmasq-upstream" ]; then
|
||||
stop_forward_dnsmasq "$old_port"
|
||||
elif [ "$old_redirect" == "exchange" ]; then
|
||||
rm_port53
|
||||
fi
|
||||
fi
|
||||
elif [ "$old_enabled" = "1" -a "$enabled" = "1" ]; then
|
||||
if [ "$old_redirect" == "redirect" -a "$old_port" != "0" ]; then
|
||||
clear_iptable "$old_port" "$ipv6_server"
|
||||
fi
|
||||
fi
|
||||
uci delete AdGuardHome.@AdGuardHome[0].old_redirect 2>/dev/null
|
||||
uci delete AdGuardHome.@AdGuardHome[0].old_port 2>/dev/null
|
||||
uci delete AdGuardHome.@AdGuardHome[0].old_enabled 2>/dev/null
|
||||
uci add_list AdGuardHome.@AdGuardHome[0].old_redirect="$redirect" 2>/dev/null
|
||||
uci add_list AdGuardHome.@AdGuardHome[0].old_port="$AdGuardHome_PORT" 2>/dev/null
|
||||
uci add_list AdGuardHome.@AdGuardHome[0].old_enabled="$enabled" 2>/dev/null
|
||||
uci commit AdGuardHome
|
||||
[ "$enabled" == "0" ] && return 1
|
||||
if [ "$AdGuardHome_PORT" == "0" ]; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$redirect" = "redirect" ]; then
|
||||
set_iptable $ipv6_server $tcp_server
|
||||
elif [ "$redirect" = "dnsmasq-upstream" ]; then
|
||||
set_forward_dnsmasq "$AdGuardHome_PORT"
|
||||
elif [ "$redirect" == "exchange" -a "$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" == "53" ]; then
|
||||
use_port53
|
||||
fi
|
||||
}
|
||||
|
||||
get_filesystem()
|
||||
{
|
||||
# print out path filesystem
|
||||
echo $1 | awk '
|
||||
BEGIN{
|
||||
while (("mount"| getline ret) > 0)
|
||||
{
|
||||
split(ret,d);
|
||||
fs[d[3]]=d[5];
|
||||
m=index(d[1],":")
|
||||
if (m==0)
|
||||
{
|
||||
pt[d[3]]=d[1]
|
||||
}else{
|
||||
pt[d[3]]=substr(d[1],m+1)
|
||||
}}}{
|
||||
split($0,d,"/");
|
||||
if ("/" in fs)
|
||||
{
|
||||
result1=fs["/"];
|
||||
}
|
||||
if ("/" in pt)
|
||||
{
|
||||
result2=pt["/"];
|
||||
}
|
||||
for (i=2;i<=length(d);i++)
|
||||
{
|
||||
p[i]=p[i-1]"/"d[i];
|
||||
if (p[i] in fs)
|
||||
{
|
||||
result1=fs[p[i]];
|
||||
result2=pt[p[i]];
|
||||
}
|
||||
}
|
||||
if (result2 in fs){
|
||||
result=fs[result2]}
|
||||
else{
|
||||
result=result1}
|
||||
print(result);}'
|
||||
}
|
||||
|
||||
config_editor()
|
||||
{
|
||||
awk -v yaml="$1" -v value="$2" -v file="$3" -v ro="$4" '
|
||||
BEGIN{split(yaml,part,"\.");s="";i=1;l=length(part);}
|
||||
{
|
||||
if (match($0,s""part[i]":"))
|
||||
{
|
||||
if (i==l)
|
||||
{
|
||||
split($0,t,": ");
|
||||
if (ro==""){
|
||||
system("sed -i '\''"FNR"c \\"t[1]": "value"'\'' "file);
|
||||
}else{
|
||||
print(t[2]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
s=s"[- ]{2}";
|
||||
i++;
|
||||
}
|
||||
}' $3
|
||||
}
|
||||
|
||||
boot_service() {
|
||||
rm /var/run/AdGserverdis >/dev/null 2>&1
|
||||
config_load "${CONFIGURATION}"
|
||||
config_get waitonboot $CONFIGURATION waitonboot "0"
|
||||
config_get_bool enabled $CONFIGURATION enabled 0
|
||||
config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome"
|
||||
[ -f "$binpath" ] && start_service
|
||||
if [ "$enabled" == "1" ] && [ "$waitonboot" == "1" ]; then
|
||||
procd_open_instance "waitnet"
|
||||
procd_set_param command "/usr/share/AdGuardHome/waitnet.sh"
|
||||
procd_close_instance
|
||||
echo "no net start pinging"
|
||||
fi
|
||||
}
|
||||
|
||||
testbackup(){
|
||||
config_load "${CONFIGURATION}"
|
||||
if [ "$1" == "backup" ]; then
|
||||
backup
|
||||
elif [ "$1" == "restore" ]; then
|
||||
restore
|
||||
fi
|
||||
}
|
||||
|
||||
restore()
|
||||
{
|
||||
config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome"
|
||||
config_get backupwdpath $CONFIGURATION backupwdpath "/etc/AdGuardHome"
|
||||
cp -u -r -f $backupwdpath/data $workdir
|
||||
}
|
||||
|
||||
backup() {
|
||||
config_get backupwdpath $CONFIGURATION backupwdpath "/etc/AdGuardHome"
|
||||
mkdir -p $backupwdpath/data
|
||||
config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome"
|
||||
config_get backupfile $CONFIGURATION backupfile ""
|
||||
for one in $backupfile;
|
||||
do
|
||||
while :
|
||||
do
|
||||
if [ -d "$backupwdpath/data/$one" ]; then
|
||||
cpret=$(cp -u -r -f $workdir/data/$one $backupwdpath/data 2>&1)
|
||||
else
|
||||
cpret=$(cp -u -r -f $workdir/data/$one $backupwdpath/data/$one 2>&1)
|
||||
fi
|
||||
echo "$cpret"
|
||||
echo "$cpret" | grep "no space left on device"
|
||||
if [ "$?" == "0" ]; then
|
||||
echo "磁盘已满,删除log重试中"
|
||||
del_querylog && continue
|
||||
rm -f -r $backupwdpath/data/filters
|
||||
rm -f -r $workdir/data/filters && continue
|
||||
echo "backup failed"
|
||||
fi
|
||||
break
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
start_service() {
|
||||
# Reading config
|
||||
rm /var/run/AdGserverdis >/dev/null 2>&1
|
||||
config_load "${CONFIGURATION}"
|
||||
# update password
|
||||
config_get hashpass $CONFIGURATION hashpass ""
|
||||
config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
|
||||
if [ -n "$hashpass" ]; then
|
||||
config_editor "users.password" "$hashpass" "$configpath"
|
||||
uci set $CONFIGURATION.$CONFIGURATION.hashpass=""
|
||||
fi
|
||||
local enabled
|
||||
config_get_bool enabled $CONFIGURATION enabled 0
|
||||
# update crontab
|
||||
do_crontab
|
||||
if [ "$enabled" == "0" ]; then
|
||||
_do_redirect 0
|
||||
return
|
||||
fi
|
||||
#what need to do before reload
|
||||
config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome"
|
||||
|
||||
config_get backupfile $CONFIGURATION backupfile ""
|
||||
mkdir -p $workdir/data
|
||||
if [ -n "$backupfile" ] && [ ! -d "$workdir/data" ]; then
|
||||
restore
|
||||
fi
|
||||
# for overlay data-stk-oo not suppport
|
||||
local cwdfs=$(get_filesystem $workdir)
|
||||
echo "workdir is a $cwdfs filesystem"
|
||||
if [ "$cwdfs" == "jffs2" ]; then
|
||||
echo "fs error ln db to tmp $workdir $cwdfs"
|
||||
logger "AdGuardHome" "warning db redirect to tmp"
|
||||
touch $workdir/data/stats.db
|
||||
if [ ! -L $workdir/data/stats.db ]; then
|
||||
mv -f $workdir/data/stats.db /tmp/stats.db 2>/dev/null
|
||||
ln -s /tmp/stats.db $workdir/data/stats.db 2>/dev/null
|
||||
fi
|
||||
touch $workdir/data/sessions.db
|
||||
if [ ! -L $workdir/data/sessions.db ]; then
|
||||
mv -f $workdir/data/sessions.db /tmp/sessions.db 2>/dev/null
|
||||
ln -s /tmp/sessions.db $workdir/data/sessions.db 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
local ADDITIONAL_ARGS=""
|
||||
config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome"
|
||||
|
||||
mkdir -p ${binpath%/*}
|
||||
ADDITIONAL_ARGS="$ADDITIONAL_ARGS -c $configpath"
|
||||
ADDITIONAL_ARGS="$ADDITIONAL_ARGS -w $workdir"
|
||||
config_get httpport $CONFIGURATION httpport 3000
|
||||
ADDITIONAL_ARGS="$ADDITIONAL_ARGS -p $httpport"
|
||||
|
||||
# hack to save config file when upgrade system
|
||||
config_get upprotect $CONFIGURATION upprotect ""
|
||||
eval upprotect=${upprotect// /\\\\n}
|
||||
echo -e "$upprotect">/lib/upgrade/keep.d/luci-app-adguardhome
|
||||
|
||||
config_get logfile $CONFIGURATION logfile ""
|
||||
if [ -n "$logfile" ]; then
|
||||
ADDITIONAL_ARGS="$ADDITIONAL_ARGS -l $logfile"
|
||||
fi
|
||||
|
||||
if [ ! -f "$binpath" ]; then
|
||||
_do_redirect 0
|
||||
/usr/share/AdGuardHome/update_core.sh 2>&1 >/tmp/AdGuardHome_update.log &
|
||||
exit 0
|
||||
fi
|
||||
|
||||
config_get_bool verbose $CONFIGURATION verbose 0
|
||||
if [ "$verbose" -eq 1 ]; then
|
||||
ADDITIONAL_ARGS="$ADDITIONAL_ARGS -v"
|
||||
fi
|
||||
|
||||
procd_open_instance
|
||||
get_tz
|
||||
if [ -n "$SET_TZ" ]; then
|
||||
procd_set_param env TZ="$SET_TZ"
|
||||
fi
|
||||
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
|
||||
procd_set_param limits core="unlimited" nofile="65535 65535"
|
||||
procd_set_param stderr 1
|
||||
procd_set_param command $binpath $ADDITIONAL_ARGS
|
||||
procd_set_param file "$configpath" "/etc/hosts" "/etc/config/AdGuardHome"
|
||||
procd_close_instance
|
||||
if [ -f "$configpath" ]; then
|
||||
_do_redirect 1
|
||||
else
|
||||
_do_redirect 0
|
||||
config_get "redirect" "AdGuardHome" "redirect" "none"
|
||||
if [ "$redirect" != "none" ]; then
|
||||
procd_open_instance "waitconfig"
|
||||
procd_set_param command "/usr/share/AdGuardHome/watchconfig.sh"
|
||||
procd_close_instance
|
||||
echo "no config start watching"
|
||||
fi
|
||||
fi
|
||||
echo "AdGuardHome service enabled"
|
||||
echo "luci enable switch=$enabled"
|
||||
(sleep 10 && [ -z "$(pgrep $binpath)" ] && logger "AdGuardHome" "no process in 10s cancel redirect" && _do_redirect 0 )&
|
||||
if [[ "`uci get bypass.@global[0].global_server 2>/dev/null`" && "`uci get bypass.@global[0].adguardhome 2>/dev/null`" == 1 && "$(uci get dhcp.@dnsmasq[0].port)" == "53" ]]; then
|
||||
uci -q set AdGuardHome.AdGuardHome.redirect='exchange'
|
||||
uci commit AdGuardHome
|
||||
do_redirect 1
|
||||
fi
|
||||
}
|
||||
|
||||
reload_service()
|
||||
{
|
||||
rm /var/run/AdGlucitest >/dev/null 2>&1
|
||||
echo "AdGuardHome reloading"
|
||||
start
|
||||
}
|
||||
|
||||
del_querylog(){
|
||||
local btarget=$(ls $backupwdpath/data | grep -F "querylog.json" | sort -r | head -n 1)
|
||||
local wtarget=$(ls $workdir/data | grep -F "querylog.json" | sort -r | head -n 1)
|
||||
if [ "$btarget"x == "$wtarget"x ]; then
|
||||
[ -z "$btarget" ] && return 1
|
||||
rm -f $workdir/data/$wtarget
|
||||
rm -f $backupwdpath/data/$btarget
|
||||
return 0
|
||||
fi
|
||||
if [ "$btarget" \> "$wtarget" ]; then
|
||||
rm -f $backupwdpath/data/$btarget
|
||||
return 0
|
||||
else
|
||||
rm -f $workdir/data/$wtarget
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
stop_service()
|
||||
{
|
||||
config_load "${CONFIGURATION}"
|
||||
_do_redirect 0
|
||||
do_crontab
|
||||
if [ "$1" != "nobackup" ]; then
|
||||
config_get backupfile $CONFIGURATION backupfile "0"
|
||||
if [ -n "$backupfile" ]; then
|
||||
backup
|
||||
fi
|
||||
fi
|
||||
echo "AdGuardHome service disabled"
|
||||
touch /var/run/AdGserverdis
|
||||
}
|
||||
|
||||
boot() {
|
||||
rc_procd boot_service "$@"
|
||||
if eval "type service_started" 2>/dev/null >/dev/null; then
|
||||
service_started
|
||||
fi
|
||||
}
|
||||
|
||||
test_crontab(){
|
||||
config_load "${CONFIGURATION}"
|
||||
do_crontab
|
||||
}
|
||||
|
||||
do_crontab(){
|
||||
config_get_bool enabled $CONFIGURATION enabled 0
|
||||
config_get crontab $CONFIGURATION crontab ""
|
||||
local findstr default cronenable replace commit
|
||||
local cronreload=0
|
||||
local commit=0
|
||||
findstr="/usr/share/AdGuardHome/update_core.sh"
|
||||
default="30 3 * * * /usr/share/AdGuardHome/update_core.sh 2>&1"
|
||||
[ "$enabled" == "0" ] || [ "${crontab//autoupdate/}" == "$crontab" ] && cronenable=0 || cronenable=1
|
||||
crontab_editor
|
||||
|
||||
config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome"
|
||||
config_get lastworkdir $CONFIGURATION lastworkdir "/etc/AdGuardHome"
|
||||
findstr="/usr/share/AdGuardHome/tailto.sh [0-9]* \$(uci get AdGuardHome.AdGuardHome.workdir)/data/querylog.json"
|
||||
#[ -n "$lastworkdir" ] && findstr="/usr/share/AdGuardHome/tailto.sh [0-9]* $lastworkdir/data/querylog.json" && [ "$lastworkdir" != "$workdir" ] && replace="${lastworkdir//\//\\/}/${workdir//\//\\/}"
|
||||
default="0 * * * * /usr/share/AdGuardHome/tailto.sh 2000 \$(uci get AdGuardHome.AdGuardHome.workdir)/data/querylog.json"
|
||||
[ "$enabled" == "0" ] || [ "${crontab//cutquerylog/}" == "$crontab" ] && cronenable=0 || cronenable=1
|
||||
crontab_editor
|
||||
#[ "$lastworkdir" != "$workdir" ] && uci set AdGuardHome.AdGuardHome.lastworkdir="$workdir" && commit=1
|
||||
|
||||
config_get logfile $CONFIGURATION logfile ""
|
||||
config_get lastlogfile $CONFIGURATION lastlogfile ""
|
||||
findstr="/usr/share/AdGuardHome/tailto.sh [0-9]* \$(uci get AdGuardHome.AdGuardHome.logfile)"
|
||||
default="30 3 * * * /usr/share/AdGuardHome/tailto.sh 2000 \$(uci get AdGuardHome.AdGuardHome.logfile)"
|
||||
#[ -n "$lastlogfile" ] && findstr="/usr/share/AdGuardHome/tailto.sh [0-9]* $lastlogfile" && [ -n "$logfile" ] && [ "$lastlogfile" != "$logfile" ] && replace="${lastlogfile//\//\\/}/${logfile//\//\\/}"
|
||||
[ "$logfile" == "syslog" ] || [ "$logfile" == "" ] || [ "$enabled" == "0" ] || [ "${crontab//cutruntimelog/}" == "$crontab" ] && cronenable=0 || cronenable=1
|
||||
crontab_editor
|
||||
#[ -n "$logfile" ] && [ "$lastlogfile" != "$logfile" ] && uci set AdGuardHome.AdGuardHome.lastlogfile="$logfile" && commit=1
|
||||
|
||||
findstr="/usr/share/AdGuardHome/addhost.sh"
|
||||
default="0 * * * * /usr/share/AdGuardHome/addhost.sh"
|
||||
[ "$enabled" == "0" ] || [ "${crontab//autohost/}" == "$crontab" ] && cronenable=0 || cronenable=1
|
||||
crontab_editor
|
||||
[ "$cronenable" == "0" ] && /usr/share/AdGuardHome/addhost.sh "del" "noreload" || /usr/share/AdGuardHome/addhost.sh "" "noreload"
|
||||
|
||||
findstr="/usr/share/AdGuardHome/gfw2adg.sh"
|
||||
default="30 3 * * * /usr/share/AdGuardHome/gfw2adg.sh"
|
||||
[ "$enabled" == "0" ] || [ "${crontab//autogfw/}" == "$crontab" ] && cronenable=0 || cronenable=1
|
||||
crontab_editor
|
||||
[ "$cronreload" -gt 0 ] && /etc/init.d/cron restart
|
||||
#[ "$commit" -gt 0 ] && uci commit AdGuardHome
|
||||
}
|
||||
|
||||
crontab_editor(){
|
||||
#usage input:
|
||||
#findstr=
|
||||
#default=
|
||||
#cronenable=
|
||||
#replace="${last//\//\\/}/${now//\//\\/}"
|
||||
#output:cronreload:if >1 please /etc/init.d/cron restart manual
|
||||
local testline reload
|
||||
local line="$(grep "$findstr" $CRON_FILE)"
|
||||
[ -n "$replace" ] && [ -n "$line" ] && eval testline="\${line//$replace}" && [ "$testline" != "$line" ] && line="$testline" && reload="1" && replace=""
|
||||
if [ "${line:0:1}" != "#" ]; then
|
||||
if [ $cronenable -eq 1 ]; then
|
||||
[ -z "$line" ] && line="$default" && reload="1"
|
||||
if [ -n "$reload" ]; then
|
||||
sed -i "\,$findstr,d" $CRON_FILE
|
||||
echo "$line" >> $CRON_FILE
|
||||
cronreload=$((cronreload+1))
|
||||
fi
|
||||
elif [ -n "$line" ]; then
|
||||
sed -i "\,$findstr,d" $CRON_FILE
|
||||
echo "#$line" >> $CRON_FILE
|
||||
cronreload=$((cronreload+1))
|
||||
fi
|
||||
else
|
||||
if [ $cronenable -eq 1 ]; then
|
||||
sed -i "\,$findstr,d" $CRON_FILE
|
||||
echo "${line:1}" >> $CRON_FILE
|
||||
cronreload=$((cronreload+1))
|
||||
elif [ -z "$reload" ]; then
|
||||
sed -i "\,$findstr,d" $CRON_FILE
|
||||
echo "$line" >> $CRON_FILE
|
||||
fi
|
||||
fi
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
|
||||
uci -q batch <<-EOF >/dev/null 2>&1
|
||||
delete ucitrack.@AdGuardHome[-1]
|
||||
add ucitrack AdGuardHome
|
||||
set ucitrack.@AdGuardHome[-1].init=AdGuardHome
|
||||
commit ucitrack
|
||||
delete AdGuardHome.AdGuardHome.ucitracktest
|
||||
/etc/init.d/AdGuardHome restart
|
||||
EOF
|
||||
|
||||
rm -f /tmp/luci-indexcache
|
||||
|
||||
chmod +x /etc/init.d/AdGuardHome /usr/share/AdGuardHome/*
|
||||
exit 0
|
|
@ -0,0 +1,131 @@
|
|||
bind_host: 0.0.0.0
|
||||
bind_port: 3000
|
||||
beta_bind_port: 0
|
||||
users:
|
||||
- name: root
|
||||
password: $2y$10$dwn0hTYoECQMZETBErGlzOId2VANOVsPHsuH13TM/8KnysM5Dh/ve
|
||||
auth_attempts: 5
|
||||
block_auth_min: 15
|
||||
http_proxy: ""
|
||||
language: zh-cn
|
||||
debug_pprof: false
|
||||
web_session_ttl: 720
|
||||
dns:
|
||||
bind_hosts:
|
||||
- 0.0.0.0
|
||||
port: 1745
|
||||
statistics_interval: 30
|
||||
querylog_enabled: true
|
||||
querylog_file_enabled: true
|
||||
querylog_interval: 6h
|
||||
querylog_size_memory: 1000
|
||||
anonymize_client_ip: false
|
||||
protection_enabled: true
|
||||
blocking_mode: default
|
||||
blocking_ipv4: ""
|
||||
blocking_ipv6: ""
|
||||
blocked_response_ttl: 60
|
||||
parental_block_host: family-block.dns.adguard.com
|
||||
safebrowsing_block_host: standard-block.dns.adguard.com
|
||||
ratelimit: 0
|
||||
ratelimit_whitelist: []
|
||||
refuse_any: false
|
||||
upstream_dns:
|
||||
- 223.5.5.5
|
||||
upstream_dns_file: ""
|
||||
bootstrap_dns:
|
||||
- 119.29.29.29
|
||||
- 223.5.5.5
|
||||
all_servers: false
|
||||
fastest_addr: false
|
||||
fastest_timeout: 1s
|
||||
allowed_clients: []
|
||||
disallowed_clients: []
|
||||
blocked_hosts:
|
||||
- version.bind
|
||||
- id.server
|
||||
- hostname.bind
|
||||
trusted_proxies:
|
||||
- 127.0.0.0/8
|
||||
- ::1/128
|
||||
cache_size: 4194304
|
||||
cache_ttl_min: 0
|
||||
cache_ttl_max: 0
|
||||
cache_optimistic: true
|
||||
bogus_nxdomain: []
|
||||
aaaa_disabled: false
|
||||
enable_dnssec: false
|
||||
edns_client_subnet: false
|
||||
max_goroutines: 300
|
||||
ipset: []
|
||||
filtering_enabled: true
|
||||
filters_update_interval: 24
|
||||
parental_enabled: false
|
||||
safesearch_enabled: false
|
||||
safebrowsing_enabled: false
|
||||
safebrowsing_cache_size: 1048576
|
||||
safesearch_cache_size: 1048576
|
||||
parental_cache_size: 1048576
|
||||
cache_time: 30
|
||||
rewrites: []
|
||||
blocked_services: []
|
||||
upstream_timeout: 10s
|
||||
local_domain_name: lan
|
||||
resolve_clients: true
|
||||
use_private_ptr_resolvers: true
|
||||
local_ptr_upstreams: []
|
||||
tls:
|
||||
enabled: false
|
||||
server_name: ""
|
||||
force_https: false
|
||||
port_https: 443
|
||||
port_dns_over_tls: 853
|
||||
port_dns_over_quic: 784
|
||||
port_dnscrypt: 0
|
||||
dnscrypt_config_file: ""
|
||||
allow_unencrypted_doh: false
|
||||
strict_sni_check: false
|
||||
certificate_chain: ""
|
||||
private_key: ""
|
||||
certificate_path: ""
|
||||
private_key_path: ""
|
||||
filters:
|
||||
- enabled: true
|
||||
url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt
|
||||
name: AdGuard DNS filter
|
||||
id: 1628750870
|
||||
- enabled: true
|
||||
url: https://anti-ad.net/easylist.txt
|
||||
name: 'CHN: anti-AD'
|
||||
id: 1628750871
|
||||
whitelist_filters: []
|
||||
user_rules: []
|
||||
dhcp:
|
||||
enabled: false
|
||||
interface_name: ""
|
||||
dhcpv4:
|
||||
gateway_ip: ""
|
||||
subnet_mask: ""
|
||||
range_start: ""
|
||||
range_end: ""
|
||||
lease_duration: 86400
|
||||
icmp_timeout_msec: 1000
|
||||
options: []
|
||||
dhcpv6:
|
||||
range_start: ""
|
||||
lease_duration: 86400
|
||||
ra_slaac_only: false
|
||||
ra_allow_slaac: false
|
||||
clients: []
|
||||
log_compress: false
|
||||
log_localtime: false
|
||||
log_max_backups: 0
|
||||
log_max_size: 100
|
||||
log_max_age: 3
|
||||
log_file: ""
|
||||
verbose: false
|
||||
os:
|
||||
group: ""
|
||||
user: ""
|
||||
rlimit_nofile: 0
|
||||
schema_version: 12
|
35
luci-app-adguardhome/root/usr/share/AdGuardHome/addhost.sh
Normal file
|
@ -0,0 +1,35 @@
|
|||
#!/bin/sh
|
||||
|
||||
checkmd5(){
|
||||
local nowmd5=$(md5sum /etc/hosts)
|
||||
nowmd5=${nowmd5%% *}
|
||||
local lastmd5=$(uci get AdGuardHome.AdGuardHome.hostsmd5 2>/dev/null)
|
||||
if [ "$nowmd5" != "$lastmd5" ]; then
|
||||
uci set AdGuardHome.AdGuardHome.hostsmd5="$nowmd5"
|
||||
uci commit AdGuardHome
|
||||
[ "$1" == "noreload" ] || /etc/init.d/AdGuardHome reload
|
||||
fi
|
||||
}
|
||||
|
||||
[ "$1" == "del" ] && sed -i '/programaddstart/,/programaddend/d' /etc/hosts && checkmd5 "$2" && exit 0
|
||||
/usr/bin/awk 'BEGIN{
|
||||
while ((getline < "/tmp/dhcp.leases") > 0)
|
||||
{
|
||||
a[$2]=$4;
|
||||
}
|
||||
while (("ip -6 neighbor show | grep -v fe80" | getline) > 0)
|
||||
{
|
||||
if (a[$5]) {print $1" "a[$5] >"/tmp/tmphost"; }
|
||||
}
|
||||
print "#programaddend" >"/tmp/tmphost";
|
||||
}'
|
||||
grep programaddstart /etc/hosts >/dev/null 2>&1
|
||||
if [ "$?" == "0" ]; then
|
||||
sed -i '/programaddstart/,/programaddend/c\#programaddstart' /etc/hosts
|
||||
sed -i '/programaddstart/'r/tmp/tmphost /etc/hosts
|
||||
else
|
||||
echo "#programaddstart" >>/etc/hosts
|
||||
cat /tmp/tmphost >> /etc/hosts
|
||||
fi
|
||||
rm /tmp/tmphost
|
||||
checkmd5 "$2"
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
|
||||
AdGuardHome_enable=$(uci get AdGuardHome.AdGuardHome.enabled)
|
||||
redirect=$(uci get AdGuardHome.AdGuardHome.redirect)
|
||||
|
||||
if [ $AdGuardHome_enable -eq 1 -a "$redirect" == "redirect" ]; then
|
||||
/etc/init.d/AdGuardHome do_redirect 1
|
||||
fi
|
20
luci-app-adguardhome/root/usr/share/AdGuardHome/getsyslog.sh
Normal file
|
@ -0,0 +1,20 @@
|
|||
#!/bin/sh
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
logread -e AdGuardHome > /tmp/AdGuardHometmp.log
|
||||
logread -e AdGuardHome -f >> /tmp/AdGuardHometmp.log &
|
||||
pid=$!
|
||||
echo "1">/var/run/AdGuardHomesyslog
|
||||
while true
|
||||
do
|
||||
sleep 12
|
||||
watchdog=$(cat /var/run/AdGuardHomesyslog)
|
||||
if [ "$watchdog"x == "0"x ]; then
|
||||
kill $pid
|
||||
rm /tmp/AdGuardHometmp.log
|
||||
rm /var/run/AdGuardHomesyslog
|
||||
exit 0
|
||||
else
|
||||
echo "0">/var/run/AdGuardHomesyslog
|
||||
fi
|
||||
done
|
89
luci-app-adguardhome/root/usr/share/AdGuardHome/gfw2adg.sh
Normal file
|
@ -0,0 +1,89 @@
|
|||
#!/bin/sh
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
checkmd5(){
|
||||
local nowmd5=$(md5sum /tmp/adguard.list 2>/dev/null)
|
||||
nowmd5=${nowmd5%% *}
|
||||
local lastmd5=$(uci get AdGuardHome.AdGuardHome.gfwlistmd5 2>/dev/null)
|
||||
if [ "$nowmd5" != "$lastmd5" ]; then
|
||||
uci set AdGuardHome.AdGuardHome.gfwlistmd5="$nowmd5"
|
||||
uci commit AdGuardHome
|
||||
[ "$1" == "noreload" ] || /etc/init.d/AdGuardHome reload
|
||||
fi
|
||||
}
|
||||
|
||||
configpath=$(uci get AdGuardHome.AdGuardHome.configpath 2>/dev/null)
|
||||
[ "$1" == "del" ] && sed -i '/programaddstart/,/programaddend/d' $configpath && checkmd5 "$2" && exit 0
|
||||
gfwupstream=$(uci get AdGuardHome.AdGuardHome.gfwupstream 2>/dev/null)
|
||||
if [ -z $gfwupstream ]; then
|
||||
gfwupstream="tcp://208.67.220.220:5353"
|
||||
fi
|
||||
if [ ! -f "$configpath" ]; then
|
||||
echo "please make a config first"
|
||||
exit 1
|
||||
fi
|
||||
wget-ssl --no-check-certificate https://cdn.jsdelivr.net/gh/gfwlist/gfwlist/gfwlist.txt -O- | base64 -d > /tmp/gfwlist.txt
|
||||
cat /tmp/gfwlist.txt | awk -v upst="$gfwupstream" 'BEGIN{getline;}{
|
||||
s1=substr($0,1,1);
|
||||
if (s1=="!")
|
||||
{next;}
|
||||
if (s1=="@"){
|
||||
$0=substr($0,3);
|
||||
s1=substr($0,1,1);
|
||||
white=1;}
|
||||
else{
|
||||
white=0;
|
||||
}
|
||||
|
||||
if (s1=="|")
|
||||
{s2=substr($0,2,1);
|
||||
if (s2=="|")
|
||||
{
|
||||
$0=substr($0,3);
|
||||
split($0,d,"/");
|
||||
$0=d[1];
|
||||
}else{
|
||||
split($0,d,"/");
|
||||
$0=d[3];
|
||||
}}
|
||||
else{
|
||||
split($0,d,"/");
|
||||
$0=d[1];
|
||||
}
|
||||
star=index($0,"*");
|
||||
if (star!=0)
|
||||
{
|
||||
$0=substr($0,star+1);
|
||||
dot=index($0,".");
|
||||
if (dot!=0)
|
||||
$0=substr($0,dot+1);
|
||||
else
|
||||
next;
|
||||
s1=substr($0,1,1);
|
||||
}
|
||||
if (s1==".")
|
||||
{fin=substr($0,2);}
|
||||
else{fin=$0;}
|
||||
if (index(fin,".")==0) next;
|
||||
if (index(fin,"%")!=0) next;
|
||||
if (index(fin,":")!=0) next;
|
||||
match(fin,"^[0-9\.]+")
|
||||
if (RSTART==1 && RLENGTH==length(fin)) {print "ipset add gfwlist "fin>"/tmp/doipset.sh";next;}
|
||||
if (fin=="" || finl==fin) next;
|
||||
finl=fin;
|
||||
if (white==0)
|
||||
{print(" - '\''[/"fin"/]"upst"'\''");}
|
||||
else{
|
||||
print(" - '\''[/"fin"/]#'\''");}
|
||||
}END{print(" - '\''[/programaddend/]#'\''")}' > /tmp/adguard.list
|
||||
grep programaddstart $configpath
|
||||
if [ "$?" == "0" ]; then
|
||||
sed -i '/programaddstart/,/programaddend/c\ - '\''\[\/programaddstart\/\]#'\''' $configpath
|
||||
sed -i '/programaddstart/'r/tmp/adguard.list $configpath
|
||||
else
|
||||
sed -i '1i\ - '\''[/programaddstart/]#'\''' /tmp/adguard.list
|
||||
sed -i '/upstream_dns:/'r/tmp/adguard.list $configpath
|
||||
fi
|
||||
checkmd5 "$2"
|
||||
rm -f /tmp/gfwlist.txt /tmp/adguard.list
|
|
@ -0,0 +1,3 @@
|
|||
https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_${Arch}.tar.gz
|
||||
https://github.com/AdguardTeam/AdGuardHome/releases/download/${latest_ver}/AdGuardHome_linux_${Arch}.tar.gz
|
||||
https://static.adguard.com/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
|
||||
tail -n $1 "$2" > /var/run/tailtmp
|
||||
cat /var/run/tailtmp > "$2"
|
||||
rm /var/run/tailtmp
|
236
luci-app-adguardhome/root/usr/share/AdGuardHome/update_core.sh
Normal file
|
@ -0,0 +1,236 @@
|
|||
#!/bin/bash
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
binpath=$(uci get AdGuardHome.AdGuardHome.binpath)
|
||||
if [ -z "$binpath" ]; then
|
||||
uci set AdGuardHome.AdGuardHome.binpath="/tmp/AdGuardHome/AdGuardHome"
|
||||
binpath="/tmp/AdGuardHome/AdGuardHome"
|
||||
fi
|
||||
mkdir -p ${binpath%/*}
|
||||
upxflag=$(uci get AdGuardHome.AdGuardHome.upxflag 2>/dev/null)
|
||||
|
||||
check_if_already_running(){
|
||||
running_tasks="$(ps |grep "AdGuardHome" |grep "update_core" |grep -v "grep" |awk '{print $1}' |wc -l)"
|
||||
[ "${running_tasks}" -gt "2" ] && echo -e "\nA task is already running." && EXIT 2
|
||||
}
|
||||
|
||||
check_wgetcurl(){
|
||||
which curl && downloader="curl -L -k --retry 2 --connect-timeout 20 -o" && return
|
||||
which wget-ssl && downloader="wget-ssl --no-check-certificate -t 2 -T 20 -O" && return
|
||||
[ -z "$1" ] && opkg update || (echo error opkg && EXIT 1)
|
||||
[ -z "$1" ] && (opkg remove wget wget-nossl --force-depends ; opkg install wget ; check_wgetcurl 1 ;return)
|
||||
[ "$1" == "1" ] && (opkg install curl ; check_wgetcurl 2 ; return)
|
||||
echo error curl and wget && EXIT 1
|
||||
}
|
||||
|
||||
check_latest_version(){
|
||||
check_wgetcurl
|
||||
latest_ver="$($downloader - https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest 2>/dev/null|grep -E 'tag_name' |grep -E 'v[0-9.]+' -o 2>/dev/null)"
|
||||
if [ -z "${latest_ver}" ]; then
|
||||
echo -e "\nFailed to check latest version, please try again later." && EXIT 1
|
||||
fi
|
||||
now_ver="$($binpath -c /dev/null --check-config 2>&1| grep -m 1 -E 'v[0-9.]+' -o)"
|
||||
if [ "${latest_ver}"x != "${now_ver}"x ] || [ "$1" == "force" ]; then
|
||||
echo -e "Local version: ${now_ver}., cloud version: ${latest_ver}."
|
||||
doupdate_core
|
||||
else
|
||||
echo -e "\nLocal version: ${now_ver}, cloud version: ${latest_ver}."
|
||||
echo -e "You're already using the latest version."
|
||||
if [ ! -z "$upxflag" ]; then
|
||||
filesize=$(ls -l $binpath | awk '{ print $5 }')
|
||||
if [ $filesize -gt 8000000 ]; then
|
||||
echo -e "start upx may take a long time"
|
||||
doupx
|
||||
mkdir -p "/tmp/AdGuardHomeupdate/AdGuardHome" >/dev/null 2>&1
|
||||
rm -fr /tmp/AdGuardHomeupdate/AdGuardHome/${binpath##*/}
|
||||
/tmp/upx-${upx_latest_ver}-${Arch}_linux/upx $upxflag $binpath -o /tmp/AdGuardHomeupdate/AdGuardHome/${binpath##*/}
|
||||
rm -rf /tmp/upx-${upx_latest_ver}-${Arch}_linux
|
||||
/etc/init.d/AdGuardHome stop nobackup
|
||||
rm $binpath
|
||||
mv -f /tmp/AdGuardHomeupdate/AdGuardHome/${binpath##*/} $binpath
|
||||
/etc/init.d/AdGuardHome start
|
||||
echo -e "finished"
|
||||
fi
|
||||
fi
|
||||
EXIT 0
|
||||
fi
|
||||
}
|
||||
|
||||
doupx(){
|
||||
Archt="$(opkg info kernel | grep Architecture | awk -F "[ _]" '{print($2)}')"
|
||||
case $Archt in
|
||||
"i386")
|
||||
Arch="i386"
|
||||
;;
|
||||
"i686")
|
||||
Arch="i386"
|
||||
echo -e "i686 use $Arch may have bug"
|
||||
;;
|
||||
"x86")
|
||||
Arch="amd64"
|
||||
;;
|
||||
"mipsel")
|
||||
Arch="mipsel"
|
||||
;;
|
||||
"mips64el")
|
||||
Arch="mips64el"
|
||||
Arch="mipsel"
|
||||
echo -e "mips64el use $Arch may have bug"
|
||||
;;
|
||||
"mips")
|
||||
Arch="mips"
|
||||
;;
|
||||
"mips64")
|
||||
Arch="mips64"
|
||||
Arch="mips"
|
||||
echo -e "mips64 use $Arch may have bug"
|
||||
;;
|
||||
"arm")
|
||||
Arch="arm"
|
||||
;;
|
||||
"armeb")
|
||||
Arch="armeb"
|
||||
;;
|
||||
"aarch64")
|
||||
Arch="arm64"
|
||||
;;
|
||||
"powerpc")
|
||||
Arch="powerpc"
|
||||
;;
|
||||
"powerpc64")
|
||||
Arch="powerpc64"
|
||||
;;
|
||||
*)
|
||||
echo -e "error not support $Archt if you can use offical release please issue a bug"
|
||||
EXIT 1
|
||||
;;
|
||||
esac
|
||||
upx_latest_ver="$($downloader - https://api.github.com/repos/upx/upx/releases/latest 2>/dev/null|grep -E 'tag_name' |grep -E '[0-9.]+' -o 2>/dev/null)"
|
||||
$downloader /tmp/upx-${upx_latest_ver}-${Arch}_linux.tar.xz "https://github.com/upx/upx/releases/download/v${upx_latest_ver}/upx-${upx_latest_ver}-${Arch}_linux.tar.xz" 2>&1
|
||||
#tar xvJf
|
||||
which xz || (opkg list | grep ^xz || opkg update && opkg install xz) || (echo "xz download fail" && EXIT 1)
|
||||
mkdir -p /tmp/upx-${upx_latest_ver}-${Arch}_linux
|
||||
xz -d -c /tmp/upx-${upx_latest_ver}-${Arch}_linux.tar.xz| tar -x -C "/tmp" >/dev/null 2>&1
|
||||
if [ ! -e "/tmp/upx-${upx_latest_ver}-${Arch}_linux/upx" ]; then
|
||||
echo -e "Failed to download upx."
|
||||
EXIT 1
|
||||
fi
|
||||
rm /tmp/upx-${upx_latest_ver}-${Arch}_linux.tar.xz
|
||||
}
|
||||
|
||||
doupdate_core(){
|
||||
echo -e "Updating core..."
|
||||
mkdir -p "/tmp/AdGuardHomeupdate"
|
||||
rm -rf /tmp/AdGuardHomeupdate/* >/dev/null 2>&1
|
||||
Archt="$(opkg info kernel | grep Architecture | awk -F "[ _]" '{print($2)}')"
|
||||
case $Archt in
|
||||
"i386")
|
||||
Arch="386"
|
||||
;;
|
||||
"i686")
|
||||
Arch="386"
|
||||
;;
|
||||
"x86")
|
||||
Arch="amd64"
|
||||
;;
|
||||
"mipsel")
|
||||
Arch="mipsle"
|
||||
;;
|
||||
"mips64el")
|
||||
Arch="mips64le"
|
||||
Arch="mipsle"
|
||||
echo -e "mips64el use $Arch may have bug"
|
||||
;;
|
||||
"mips")
|
||||
Arch="mips"
|
||||
;;
|
||||
"mips64")
|
||||
Arch="mips64"
|
||||
Arch="mips"
|
||||
echo -e "mips64 use $Arch may have bug"
|
||||
;;
|
||||
"arm")
|
||||
Arch="arm"
|
||||
;;
|
||||
"aarch64")
|
||||
Arch="arm64"
|
||||
;;
|
||||
"powerpc")
|
||||
Arch="ppc"
|
||||
echo -e "error not support $Archt"
|
||||
EXIT 1
|
||||
;;
|
||||
"powerpc64")
|
||||
Arch="ppc64"
|
||||
echo -e "error not support $Archt"
|
||||
EXIT 1
|
||||
;;
|
||||
*)
|
||||
echo -e "error not support $Archt if you can use offical release please issue a bug"
|
||||
EXIT 1
|
||||
;;
|
||||
esac
|
||||
echo -e "start download"
|
||||
grep -v "^#" /usr/share/AdGuardHome/links.txt >/tmp/run/AdHlinks.txt
|
||||
while read link
|
||||
do
|
||||
eval link="$link"
|
||||
$downloader /tmp/AdGuardHomeupdate/${link##*/} "$link" 2>&1
|
||||
if [ "$?" != "0" ]; then
|
||||
echo "download failed try another download"
|
||||
rm -f /tmp/AdGuardHomeupdate/${link##*/}
|
||||
else
|
||||
local success="1"
|
||||
break
|
||||
fi
|
||||
done < "/tmp/run/AdHlinks.txt"
|
||||
rm /tmp/run/AdHlinks.txt
|
||||
[ -z "$success" ] && echo "no download success" && EXIT 1
|
||||
if [ "${link##*.}" == "gz" ]; then
|
||||
tar -zxf "/tmp/AdGuardHomeupdate/${link##*/}" -C "/tmp/AdGuardHomeupdate/"
|
||||
if [ ! -e "/tmp/AdGuardHomeupdate/AdGuardHome" ]; then
|
||||
echo -e "Failed to download core."
|
||||
rm -rf "/tmp/AdGuardHomeupdate" >/dev/null 2>&1
|
||||
EXIT 1
|
||||
fi
|
||||
downloadbin="/tmp/AdGuardHomeupdate/AdGuardHome/AdGuardHome"
|
||||
else
|
||||
downloadbin="/tmp/AdGuardHomeupdate/${link##*/}"
|
||||
fi
|
||||
chmod 755 $downloadbin
|
||||
echo -e "download success start copy"
|
||||
if [ -n "$upxflag" ]; then
|
||||
echo -e "start upx may take a long time"
|
||||
doupx
|
||||
/tmp/upx-${upx_latest_ver}-${Arch}_linux/upx $upxflag $downloadbin
|
||||
rm -rf /tmp/upx-${upx_latest_ver}-${Arch}_linux
|
||||
fi
|
||||
echo -e "start copy"
|
||||
/etc/init.d/AdGuardHome stop nobackup
|
||||
rm "$binpath"
|
||||
mv -f "$downloadbin" "$binpath"
|
||||
if [ "$?" == "1" ]; then
|
||||
echo "mv failed maybe not enough space please use upx or change bin to /tmp/AdGuardHome"
|
||||
EXIT 1
|
||||
fi
|
||||
/etc/init.d/AdGuardHome start
|
||||
rm -rf "/tmp/AdGuardHomeupdate" >/dev/null 2>&1
|
||||
echo -e "Succeeded in updating core."
|
||||
echo -e "Local version: ${latest_ver}, cloud version: ${latest_ver}.\n"
|
||||
EXIT 0
|
||||
}
|
||||
|
||||
EXIT(){
|
||||
rm /var/run/update_core 2>/dev/null
|
||||
[ "$1" != "0" ] && touch /var/run/update_core_error
|
||||
exit $1
|
||||
}
|
||||
|
||||
main(){
|
||||
check_if_already_running
|
||||
check_latest_version $1
|
||||
}
|
||||
trap "EXIT 1" SIGTERM SIGINT
|
||||
touch /var/run/update_core
|
||||
rm /var/run/update_core_error 2>/dev/null
|
||||
main $1
|
35
luci-app-adguardhome/root/usr/share/AdGuardHome/waitnet.sh
Normal file
|
@ -0,0 +1,35 @@
|
|||
#!/bin/sh
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
count=0
|
||||
while :
|
||||
do
|
||||
ping -c 1 -W 1 -q www.baidu.com 1>/dev/null 2>&1
|
||||
if [ "$?" == "0" ]; then
|
||||
/etc/init.d/AdGuardHome force_reload
|
||||
break
|
||||
fi
|
||||
ping -c 1 -W 1 -q 202.108.22.5 1>/dev/null 2>&1
|
||||
if [ "$?" == "0" ]; then
|
||||
/etc/init.d/AdGuardHome force_reload
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
ping -c 1 -W 1 -q www.google.com 1>/dev/null 2>&1
|
||||
if [ "$?" == "0" ]; then
|
||||
/etc/init.d/AdGuardHome force_reload
|
||||
break
|
||||
fi
|
||||
ping -c 1 -W 1 -q 8.8.8.8 1>/dev/null 2>&1
|
||||
if [ "$?" == "0" ]; then
|
||||
/etc/init.d/AdGuardHome force_reload
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
count=$((count+1))
|
||||
if [ $count -gt 18 ]; then
|
||||
/etc/init.d/AdGuardHome force_reload
|
||||
break
|
||||
fi
|
||||
done
|
||||
return 0
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
configpath=$(uci get AdGuardHome.AdGuardHome.configpath)
|
||||
while :
|
||||
do
|
||||
sleep 10
|
||||
if [ -f "$configpath" ]; then
|
||||
/etc/init.d/AdGuardHome do_redirect 1
|
||||
break
|
||||
fi
|
||||
done
|
||||
return 0
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"luci-app-adguardhome": {
|
||||
"description": "Grant UCI access for luci-app-adguardhome",
|
||||
"read": {
|
||||
"uci": [ "AdGuardHome" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "AdGuardHome" ]
|
||||
}
|
||||
}
|
||||
}
|
1
luci-app-adguardhome/root/www/luci-static/resources/codemirror/addon/fold/foldcode.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
!function(n){"object"==typeof exports&&"object"==typeof module?n(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],n):n(CodeMirror)}(function(n){"use strict";function e(e,o,i,t){if(i&&i.call){var l=i;i=null}else l=r(e,i,"rangeFinder");"number"==typeof o&&(o=n.Pos(o,0));var f=r(e,i,"minFoldSize");function d(n){var r=l(e,o);if(!r||r.to.line-r.from.line<f)return null;for(var i=e.findMarksAt(r.from),d=0;d<i.length;++d)if(i[d].__isFold&&"fold"!==t){if(!n)return null;r.cleared=!0,i[d].clear()}return r}var u=d(!0);if(r(e,i,"scanUp"))for(;!u&&o.line>e.firstLine();)o=n.Pos(o.line-1,0),u=d(!1);if(u&&!u.cleared&&"unfold"!==t){var a=function(n,e){var o=r(n,e,"widget");if("string"==typeof o){var i=document.createTextNode(o);(o=document.createElement("span")).appendChild(i),o.className="CodeMirror-foldmarker"}else o&&(o=o.cloneNode(!0));return o}(e,i);n.on(a,"mousedown",function(e){c.clear(),n.e_preventDefault(e)});var c=e.markText(u.from,u.to,{replacedWith:a,clearOnEnter:r(e,i,"clearOnEnter"),__isFold:!0});c.on("clear",function(o,r){n.signal(e,"unfold",e,o,r)}),n.signal(e,"fold",e,u.from,u.to)}}n.newFoldFunction=function(n,o){return function(r,i){e(r,i,{rangeFinder:n,widget:o})}},n.defineExtension("foldCode",function(n,o,r){e(this,n,o,r)}),n.defineExtension("isFolded",function(n){for(var e=this.findMarksAt(n),o=0;o<e.length;++o)if(e[o].__isFold)return!0}),n.commands.toggleFold=function(n){n.foldCode(n.getCursor())},n.commands.fold=function(n){n.foldCode(n.getCursor(),null,"fold")},n.commands.unfold=function(n){n.foldCode(n.getCursor(),null,"unfold")},n.commands.foldAll=function(e){e.operation(function(){for(var o=e.firstLine(),r=e.lastLine();o<=r;o++)e.foldCode(n.Pos(o,0),null,"fold")})},n.commands.unfoldAll=function(e){e.operation(function(){for(var o=e.firstLine(),r=e.lastLine();o<=r;o++)e.foldCode(n.Pos(o,0),null,"unfold")})},n.registerHelper("fold","combine",function(){var n=Array.prototype.slice.call(arguments,0);return function(e,o){for(var r=0;r<n.length;++r){var i=n[r](e,o);if(i)return i}}}),n.registerHelper("fold","auto",function(n,e){for(var o=n.getHelpers(e,"fold"),r=0;r<o.length;r++){var i=o[r](n,e);if(i)return i}});var o={rangeFinder:n.fold.auto,widget:"↔",minFoldSize:0,scanUp:!1,clearOnEnter:!0};function r(n,e,r){if(e&&void 0!==e[r])return e[r];var i=n.options.foldOptions;return i&&void 0!==i[r]?i[r]:o[r]}n.defineOption("foldOptions",null),n.defineExtension("foldOption",function(n,e){return r(this,n,e)})});
|
1
luci-app-adguardhome/root/www/luci-static/resources/codemirror/addon/fold/foldgutter.css
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.CodeMirror-foldmarker{color:blue;text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.CodeMirror-foldgutter{width:.7em}.CodeMirror-foldgutter-open,.CodeMirror-foldgutter-folded{cursor:pointer}.CodeMirror-foldgutter-open:after{content:"\25BE"}.CodeMirror-foldgutter-folded:after{content:"\25B8"}
|
1
luci-app-adguardhome/root/www/luci-static/resources/codemirror/addon/fold/foldgutter.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("./foldcode")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","./foldcode"],t):t(CodeMirror)}(function(t){"use strict";t.defineOption("foldGutter",!1,function(o,e,r){r&&r!=t.Init&&(o.clearGutter(o.state.foldGutter.options.gutter),o.state.foldGutter=null,o.off("gutterClick",a),o.off("changes",d),o.off("viewportChange",u),o.off("fold",l),o.off("unfold",l),o.off("swapDoc",d)),e&&(o.state.foldGutter=new function(t){this.options=t,this.from=this.to=0}(function(t){!0===t&&(t={});null==t.gutter&&(t.gutter="CodeMirror-foldgutter");null==t.indicatorOpen&&(t.indicatorOpen="CodeMirror-foldgutter-open");null==t.indicatorFolded&&(t.indicatorFolded="CodeMirror-foldgutter-folded");return t}(e)),f(o),o.on("gutterClick",a),o.on("changes",d),o.on("viewportChange",u),o.on("fold",l),o.on("unfold",l),o.on("swapDoc",d))});var o=t.Pos;function e(t,e){for(var r=t.findMarks(o(e,0),o(e+1,0)),n=0;n<r.length;++n)if(r[n].__isFold){var i=r[n].find(-1);if(i&&i.line===e)return r[n]}}function r(t){if("string"==typeof t){var o=document.createElement("div");return o.className=t+" CodeMirror-guttermarker-subtle",o}return t.cloneNode(!0)}function n(t,n,f){var a=t.state.foldGutter.options,d=n-1,u=t.foldOption(a,"minFoldSize"),l=t.foldOption(a,"rangeFinder"),c="string"==typeof a.indicatorFolded&&i(a.indicatorFolded),s="string"==typeof a.indicatorOpen&&i(a.indicatorOpen);t.eachLine(n,f,function(n){++d;var i=null,f=n.gutterMarkers;if(f&&(f=f[a.gutter]),e(t,d)){if(c&&f&&c.test(f.className))return;i=r(a.indicatorFolded)}else{var p=o(d,0),m=l&&l(t,p);if(m&&m.to.line-m.from.line>=u){if(s&&f&&s.test(f.className))return;i=r(a.indicatorOpen)}}(i||f)&&t.setGutterMarker(n,a.gutter,i)})}function i(t){return new RegExp("(^|\\s)"+t+"(?:$|\\s)\\s*")}function f(t){var o=t.getViewport(),e=t.state.foldGutter;e&&(t.operation(function(){n(t,o.from,o.to)}),e.from=o.from,e.to=o.to)}function a(t,r,n){var i=t.state.foldGutter;if(i){var f=i.options;if(n==f.gutter){var a=e(t,r);a?a.clear():t.foldCode(o(r,0),f)}}}function d(t){var o=t.state.foldGutter;if(o){var e=o.options;o.from=o.to=0,clearTimeout(o.changeUpdate),o.changeUpdate=setTimeout(function(){f(t)},e.foldOnChangeTimeSpan||600)}}function u(t){var o=t.state.foldGutter;if(o){var e=o.options;clearTimeout(o.changeUpdate),o.changeUpdate=setTimeout(function(){var e=t.getViewport();o.from==o.to||e.from-o.to>20||o.from-e.to>20?f(t):t.operation(function(){e.from<o.from&&(n(t,e.from,o.from),o.from=e.from),e.to>o.to&&(n(t,o.to,e.to),o.to=e.to)})},e.updateViewportTimeSpan||400)}}function l(t,o){var e=t.state.foldGutter;if(e){var r=o.line;r>=e.from&&r<e.to&&n(t,r,r+1)}}});
|
1
luci-app-adguardhome/root/www/luci-static/resources/codemirror/addon/fold/indent-fold.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(e){"use strict";function n(n,t){var i=n.getLine(t),o=i.search(/\S/);return-1==o||/\bcomment\b/.test(n.getTokenTypeAt(e.Pos(t,o+1)))?-1:e.countColumn(i,null,n.getOption("tabSize"))}e.registerHelper("fold","indent",function(t,i){var o=n(t,i.line);if(!(o<0)){for(var r=null,l=i.line+1,f=t.lastLine();l<=f;++l){var u=n(t,l);if(-1==u);else{if(!(u>o))break;r=l}}return r?{from:e.Pos(i.line,t.getLine(i.line).length),to:e.Pos(r,t.getLine(r).length)}:void 0}})});
|
1
luci-app-adguardhome/root/www/luci-static/resources/codemirror/lib/codemirror.css
vendored
Normal file
1
luci-app-adguardhome/root/www/luci-static/resources/codemirror/lib/codemirror.js
vendored
Normal file
1
luci-app-adguardhome/root/www/luci-static/resources/codemirror/mode/yaml/yaml.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(e){"use strict";e.defineMode("yaml",function(){var e=new RegExp("\\b(("+["true","false","on","off","yes","no"].join(")|(")+"))$","i");return{token:function(i,t){var r=i.peek(),n=t.escaped;if(t.escaped=!1,"#"==r&&(0==i.pos||/\s/.test(i.string.charAt(i.pos-1))))return i.skipToEnd(),"comment";if(i.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/))return"string";if(t.literal&&i.indentation()>t.keyCol)return i.skipToEnd(),"string";if(t.literal&&(t.literal=!1),i.sol()){if(t.keyCol=0,t.pair=!1,t.pairStart=!1,i.match(/---/))return"def";if(i.match(/\.\.\./))return"def";if(i.match(/\s*-\s+/))return"meta"}if(i.match(/^(\{|\}|\[|\])/))return"{"==r?t.inlinePairs++:"}"==r?t.inlinePairs--:"["==r?t.inlineList++:t.inlineList--,"meta";if(t.inlineList>0&&!n&&","==r)return i.next(),"meta";if(t.inlinePairs>0&&!n&&","==r)return t.keyCol=0,t.pair=!1,t.pairStart=!1,i.next(),"meta";if(t.pairStart){if(i.match(/^\s*(\||\>)\s*/))return t.literal=!0,"meta";if(i.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i))return"variable-2";if(0==t.inlinePairs&&i.match(/^\s*-?[0-9\.\,]+\s?$/))return"number";if(t.inlinePairs>0&&i.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/))return"number";if(i.match(e))return"keyword"}return!t.pair&&i.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)?(t.pair=!0,t.keyCol=i.indentation(),"atom"):t.pair&&i.match(/^:\s*/)?(t.pairStart=!0,"meta"):(t.pairStart=!1,t.escaped="\\"==r,i.next(),null)},startState:function(){return{pair:!1,pairStart:!1,keyCol:0,inlinePairs:0,inlineList:0,literal:!1,escaped:!1}},lineComment:"#",fold:"indent"}}),e.defineMIME("text/x-yaml","yaml"),e.defineMIME("text/yaml","yaml")});
|
1
luci-app-adguardhome/root/www/luci-static/resources/codemirror/theme/dracula.css
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.cm-s-dracula.CodeMirror,.cm-s-dracula .CodeMirror-gutters{background-color:#282a36 !important;color:#f8f8f2 !important;border:0}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:solid thin #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:rgba(255,255,255,0.10)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:rgba(255,255,255,0.10)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:rgba(255,255,255,0.10)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:white}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-keyword{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute{color:#50fa7b}.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-variable-3,.cm-s-dracula span.cm-type{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:rgba(255,255,255,0.1)}.cm-s-dracula .CodeMirror-matchingbracket{text-decoration:underline;color:white !important}
|
7
luci-app-adguardhome/root/www/luci-static/resources/twin-bcrypt.min.js
vendored
Normal file
51
luci-app-diskman/Makefile
Normal file
|
@ -0,0 +1,51 @@
|
|||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-diskman
|
||||
|
||||
PKG_MAINTAINER:=lisaac <lisaac.cn@gmail.com>
|
||||
PKG_LICENSE:=AGPL-3.0
|
||||
|
||||
LUCI_TITLE:=Disk Manager interface for LuCI
|
||||
LUCI_DEPENDS:=+blkid +e2fsprogs +parted +smartmontools \
|
||||
+PACKAGE_$(PKG_NAME)_INCLUDE_btrfs_progs:btrfs-progs \
|
||||
+PACKAGE_$(PKG_NAME)_INCLUDE_lsblk:lsblk \
|
||||
+PACKAGE_$(PKG_NAME)_INCLUDE_mdadm:mdadm \
|
||||
+PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_raid456:mdadm \
|
||||
+PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_raid456:kmod-md-raid456 \
|
||||
+PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_linears:mdadm \
|
||||
+PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_linears:kmod-md-linear
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)/config
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_btrfs_progs
|
||||
bool "Include btrfs-progs"
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_lsblk
|
||||
bool "Include lsblk"
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_mdadm
|
||||
bool "Include mdadm"
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_raid456
|
||||
depends on PACKAGE_$(PKG_NAME)_INCLUDE_mdadm
|
||||
bool "Include kmod-md-raid456"
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_linear
|
||||
depends on PACKAGE_$(PKG_NAME)_INCLUDE_mdadm
|
||||
bool "Include kmod-md-linear"
|
||||
default n
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
rm -fr /tmp/luci-indexcache /tmp/luci-modulecache
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
155
luci-app-diskman/luasrc/controller/diskman.lua
Normal file
|
@ -0,0 +1,155 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-diskman>
|
||||
]]--
|
||||
|
||||
require "luci.util"
|
||||
module("luci.controller.diskman",package.seeall)
|
||||
|
||||
function index()
|
||||
-- check all used executables in disk management are existed
|
||||
local CMD = {"parted", "blkid", "smartctl"}
|
||||
local executables_all_existed = true
|
||||
for _, cmd in ipairs(CMD) do
|
||||
local command = luci.sys.exec("/usr/bin/which " .. cmd)
|
||||
if not command:match(cmd) then
|
||||
executables_all_existed = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not executables_all_existed then return end
|
||||
-- entry(path, target, title, order)
|
||||
-- set leaf attr to true to pass argument throughe url (e.g. admin/system/disk/partition/sda)
|
||||
entry({"admin", "system", "diskman"}, alias("admin", "system", "diskman", "disks"), _("Disk Man"), 55)
|
||||
entry({"admin", "system", "diskman", "disks"}, form("diskman/disks"), nil).leaf = true
|
||||
entry({"admin", "system", "diskman", "partition"}, form("diskman/partition"), nil).leaf = true
|
||||
entry({"admin", "system", "diskman", "btrfs"}, form("diskman/btrfs"), nil).leaf = true
|
||||
entry({"admin", "system", "diskman", "format_partition"}, call("format_partition"), nil).leaf = true
|
||||
entry({"admin", "system", "diskman", "get_disk_info"}, call("get_disk_info"), nil).leaf = true
|
||||
entry({"admin", "system", "diskman", "mk_p_table"}, call("mk_p_table"), nil).leaf = true
|
||||
entry({"admin", "system", "diskman", "smartdetail"}, call("smart_detail"), nil).leaf = true
|
||||
entry({"admin", "system", "diskman", "smartattr"}, call("smart_attr"), nil).leaf = true
|
||||
end
|
||||
|
||||
function format_partition()
|
||||
local partation_name = luci.http.formvalue("partation_name")
|
||||
local fs = luci.http.formvalue("file_system")
|
||||
if not partation_name then
|
||||
luci.http.status(500, "Partition NOT found!")
|
||||
luci.http.write_json("Partition NOT found!")
|
||||
return
|
||||
elseif not nixio.fs.access("/dev/"..partation_name) then
|
||||
luci.http.status(500, "Partition NOT found!")
|
||||
luci.http.write_json("Partition NOT found!")
|
||||
return
|
||||
elseif not fs then
|
||||
luci.http.status(500, "no file system")
|
||||
luci.http.write_json("no file system")
|
||||
return
|
||||
end
|
||||
local dm = require "luci.model.diskman"
|
||||
code, msg = dm.format_partition(partation_name, fs)
|
||||
luci.http.status(code, msg)
|
||||
luci.http.write_json(msg)
|
||||
end
|
||||
|
||||
function get_disk_info(dev)
|
||||
if not dev then
|
||||
luci.http.status(500, "no device")
|
||||
luci.http.write_json("no device")
|
||||
return
|
||||
elseif not nixio.fs.access("/dev/"..dev) then
|
||||
luci.http.status(500, "no device")
|
||||
luci.http.write_json("no device")
|
||||
return
|
||||
end
|
||||
local dm = require "luci.model.diskman"
|
||||
local device_info = dm.get_disk_info(dev)
|
||||
luci.http.status(200, "ok")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(device_info)
|
||||
end
|
||||
|
||||
function mk_p_table()
|
||||
local p_table = luci.http.formvalue("p_table")
|
||||
local dev = luci.http.formvalue("dev")
|
||||
if not dev then
|
||||
luci.http.status(500, "no device")
|
||||
luci.http.write_json("no device")
|
||||
return
|
||||
elseif not nixio.fs.access("/dev/"..dev) then
|
||||
luci.http.status(500, "no device")
|
||||
luci.http.write_json("no device")
|
||||
return
|
||||
end
|
||||
local dm = require "luci.model.diskman"
|
||||
if p_table == "GPT" or p_table == "MBR" then
|
||||
p_table = p_table == "MBR" and "msdos" or "gpt"
|
||||
local res = luci.sys.call(dm.command.parted .. " -s /dev/" .. dev .. " mktable ".. p_table)
|
||||
if res == 0 then
|
||||
luci.http.status(200, "ok")
|
||||
else
|
||||
luci.http.status(500, "command exec error")
|
||||
end
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({code=res})
|
||||
else
|
||||
luci.http.status(404, "not support")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({code="1"})
|
||||
end
|
||||
end
|
||||
|
||||
function smart_detail(dev)
|
||||
luci.template.render("diskman/smart_detail", {dev=dev})
|
||||
end
|
||||
|
||||
function smart_attr(dev)
|
||||
local dm = require "luci.model.diskman"
|
||||
local cmd = io.popen(dm.command.smartctl .. " -H -A -i /dev/%s" % dev)
|
||||
if cmd then
|
||||
local attr = { }
|
||||
if cmd:match("NVMe Version:")then
|
||||
while true do
|
||||
local ln = cmd:read("*l")
|
||||
if not ln then
|
||||
break
|
||||
elseif ln:match("^(.-):%s+(.+)") then
|
||||
local key, value = ln:match("^(.-):%s+(.+)")
|
||||
attr[#attr+1]= {
|
||||
key = key,
|
||||
value = value
|
||||
}
|
||||
end
|
||||
end
|
||||
else
|
||||
while true do
|
||||
local ln = cmd:read("*l")
|
||||
if not ln then
|
||||
break
|
||||
elseif ln:match("^.*%d+%s+.+%s+.+%s+.+%s+.+%s+.+%s+.+%s+.+%s+.+%s+.+") then
|
||||
local id,attrbute,flag,value,worst,thresh,type,updated,raw = ln:match("^%s*(%d+)%s+([%a%p]+)%s+(%w+)%s+(%d+)%s+(%d+)%s+(%d+)%s+([%a%p]+)%s+(%a+)%s+[%w%p]+%s+(.+)")
|
||||
id= "%x" % id
|
||||
if not id:match("^%w%w") then
|
||||
id = "0%s" % id
|
||||
end
|
||||
attr[#attr+1]= {
|
||||
id = id:upper(),
|
||||
attrbute = attrbute,
|
||||
flag = flag,
|
||||
value = value,
|
||||
worst = worst,
|
||||
thresh = thresh,
|
||||
type = type,
|
||||
updated = updated,
|
||||
raw = raw
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
cmd:close()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(attr)
|
||||
end
|
||||
end
|
210
luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua
Normal file
|
@ -0,0 +1,210 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-diskman>
|
||||
]]--
|
||||
|
||||
require "luci.util"
|
||||
require("luci.tools.webadmin")
|
||||
local dm = require "luci.model.diskman"
|
||||
local uuid = arg[1]
|
||||
|
||||
if not uuid then luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman")) end
|
||||
|
||||
-- mount subv=/ to tempfs
|
||||
mount_point = "/tmp/.btrfs_tmp"
|
||||
nixio.fs.mkdirr(mount_point)
|
||||
luci.util.exec(dm.command.umount .. " "..mount_point .. " >/dev/null 2>&1")
|
||||
luci.util.exec(dm.command.mount .. " -t btrfs -o subvol=/ UUID="..uuid.." "..mount_point)
|
||||
|
||||
m = SimpleForm("btrfs", translate("Btrfs"), translate("Manage Btrfs"))
|
||||
m.template = "diskman/cbi/xsimpleform"
|
||||
m.redirect = luci.dispatcher.build_url("admin/system/diskman")
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
|
||||
-- info
|
||||
local btrfs_info = dm.get_btrfs_info(mount_point)
|
||||
local table_btrfs_info = m:section(Table, {btrfs_info}, translate("Btrfs Info"))
|
||||
table_btrfs_info:option(DummyValue, "uuid", translate("UUID"))
|
||||
table_btrfs_info:option(DummyValue, "members", translate("Members"))
|
||||
table_btrfs_info:option(DummyValue, "data_raid_level", translate("Data"))
|
||||
table_btrfs_info:option(DummyValue, "metadata_raid_lavel", translate("Metadata"))
|
||||
table_btrfs_info:option(DummyValue, "size_formated", translate("Size"))
|
||||
table_btrfs_info:option(DummyValue, "used_formated", translate("Used"))
|
||||
table_btrfs_info:option(DummyValue, "free_formated", translate("Free Space"))
|
||||
table_btrfs_info:option(DummyValue, "usage", translate("Usage"))
|
||||
local v_btrfs_label = table_btrfs_info:option(Value, "label", translate("Label"))
|
||||
local value_btrfs_label = ""
|
||||
v_btrfs_label.write = function(self, section, value)
|
||||
value_btrfs_label = value or ""
|
||||
end
|
||||
local btn_update_label = table_btrfs_info:option(Button, "_update_label")
|
||||
btn_update_label.inputtitle = translate("Update")
|
||||
btn_update_label.inputstyle = "edit"
|
||||
btn_update_label.write = function(self, section, value)
|
||||
local cmd = dm.command.btrfs .. " filesystem label " .. mount_point .. " " .. value_btrfs_label
|
||||
local res = luci.util.exec(cmd)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman/btrfs/" .. uuid))
|
||||
end
|
||||
-- subvolume
|
||||
local subvolume_list = dm.get_btrfs_subv(mount_point)
|
||||
subvolume_list["_"] = { ID = 0 }
|
||||
table_subvolume = m:section(Table, subvolume_list, translate("SubVolumes"))
|
||||
table_subvolume:option(DummyValue, "id", translate("ID"))
|
||||
table_subvolume:option(DummyValue, "top_level", translate("Top Level"))
|
||||
table_subvolume:option(DummyValue, "uuid", translate("UUID"))
|
||||
table_subvolume:option(DummyValue, "otime", translate("Otime"))
|
||||
table_subvolume:option(DummyValue, "snapshots", translate("Snapshots"))
|
||||
local v_path = table_subvolume:option(Value, "path", translate("Path"))
|
||||
v_path.forcewrite = true
|
||||
v_path.render = function(self, section, scope)
|
||||
if subvolume_list[section].ID == 0 then
|
||||
self.template = "cbi/value"
|
||||
self.placeholder = "/my_subvolume"
|
||||
self.forcewrite = true
|
||||
Value.render(self, section, scope)
|
||||
else
|
||||
self.template = "cbi/dvalue"
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
local value_path
|
||||
v_path.write = function(self, section, value)
|
||||
value_path = value
|
||||
end
|
||||
local btn_set_default = table_subvolume:option(Button, "_subv_set_default", translate("Set Default"))
|
||||
btn_set_default.forcewrite = true
|
||||
btn_set_default.inputstyle = "edit"
|
||||
btn_set_default.template = "diskman/cbi/disabled_button"
|
||||
btn_set_default.render = function(self, section, scope)
|
||||
if subvolume_list[section].default_subvolume then
|
||||
self.view_disabled = true
|
||||
self.inputtitle = translate("Set Default")
|
||||
elseif subvolume_list[section].ID == 0 then
|
||||
self.template = "cbi/dvalue"
|
||||
else
|
||||
self.inputtitle = translate("Set Default")
|
||||
self.view_disabled = false
|
||||
end
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
btn_set_default.write = function(self, section, value)
|
||||
local cmd
|
||||
if value == translate("Set Default") then
|
||||
cmd = dm.command.btrfs .. " subvolume set-default " .. mount_point..subvolume_list[section].path
|
||||
else
|
||||
cmd = dm.command.btrfs .. " subvolume set-default " .. mount_point.."/"
|
||||
end
|
||||
local res = luci.util.exec(cmd.. " 2>&1")
|
||||
if res and (res:match("ERR") or res:match("not enough arguments")) then
|
||||
m.errmessage = res
|
||||
else
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman/btrfs/" .. uuid))
|
||||
end
|
||||
end
|
||||
local btn_remove = table_subvolume:option(Button, "_subv_remove")
|
||||
btn_remove.template = "diskman/cbi/disabled_button"
|
||||
btn_remove.forcewrite = true
|
||||
btn_remove.render = function(self, section, scope)
|
||||
if subvolume_list[section].ID == 0 then
|
||||
btn_remove.inputtitle = translate("Create")
|
||||
btn_remove.inputstyle = "add"
|
||||
self.view_disabled = false
|
||||
elseif subvolume_list[section].path == "/" or subvolume_list[section].default_subvolume then
|
||||
btn_remove.inputtitle = translate("Delete")
|
||||
btn_remove.inputstyle = "remove"
|
||||
self.view_disabled = true
|
||||
else
|
||||
btn_remove.inputtitle = translate("Delete")
|
||||
btn_remove.inputstyle = "remove"
|
||||
self.view_disabled = false
|
||||
end
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
|
||||
btn_remove.write = function(self, section, value)
|
||||
local cmd
|
||||
if value == translate("Delete") then
|
||||
cmd = dm.command.btrfs .. " subvolume delete " .. mount_point .. subvolume_list[section].path
|
||||
elseif value == translate("Create") then
|
||||
if value_path and value_path:match("^/") then
|
||||
cmd = dm.command.btrfs .. " subvolume create " .. mount_point .. value_path
|
||||
else
|
||||
m.errmessage = translate("Please input Subvolume Path, Subvolume must start with '/'")
|
||||
return
|
||||
end
|
||||
end
|
||||
local res = luci.util.exec(cmd.. " 2>&1")
|
||||
if res and (res:match("ERR") or res:match("not enough arguments")) then
|
||||
m.errmessage = luci.util.pcdata(res)
|
||||
else
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman/btrfs/" .. uuid))
|
||||
end
|
||||
end
|
||||
-- snapshot
|
||||
-- local snapshot_list = dm.get_btrfs_subv(mount_point, 1)
|
||||
-- table_snapshot = m:section(Table, snapshot_list, translate("Snapshots"))
|
||||
-- table_snapshot:option(DummyValue, "id", translate("ID"))
|
||||
-- table_snapshot:option(DummyValue, "top_level", translate("Top Level"))
|
||||
-- table_snapshot:option(DummyValue, "uuid", translate("UUID"))
|
||||
-- table_snapshot:option(DummyValue, "otime", translate("Otime"))
|
||||
-- table_snapshot:option(DummyValue, "path", translate("Path"))
|
||||
-- local snp_remove = table_snapshot:option(Button, "_snp_remove")
|
||||
-- snp_remove.inputtitle = translate("Delete")
|
||||
-- snp_remove.inputstyle = "remove"
|
||||
-- snp_remove.write = function(self, section, value)
|
||||
-- local cmd = dm.command.btrfs .. " subvolume delete " .. mount_point .. snapshot_list[section].path
|
||||
-- local res = luci.util.exec(cmd.. " 2>&1")
|
||||
-- if res and (res:match("ERR") or res:match("not enough arguments")) then
|
||||
-- m.errmessage = luci.util.pcdata(res)
|
||||
-- else
|
||||
-- luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman/btrfs/" .. uuid))
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- new snapshots
|
||||
local s_snapshot = m:section(SimpleSection, translate("New Snapshot"))
|
||||
local value_sorce, value_dest, value_readonly
|
||||
local v_sorce = s_snapshot:option(Value, "_source", translate("Source Path"), translate("The source path for create the snapshot"))
|
||||
v_sorce.placeholder = "/data"
|
||||
v_sorce.forcewrite = true
|
||||
v_sorce.write = function(self, section, value)
|
||||
value_sorce = value
|
||||
end
|
||||
|
||||
local v_readonly = s_snapshot:option(Flag, "_readonly", translate("Readonly"), translate("The path where you want to store the snapshot"))
|
||||
v_readonly.forcewrite = true
|
||||
v_readonly.rmempty = false
|
||||
v_readonly.disabled = 0
|
||||
v_readonly.enabled = 1
|
||||
v_readonly.default = 1
|
||||
v_readonly.write = function(self, section, value)
|
||||
value_readonly = value
|
||||
end
|
||||
local v_dest = s_snapshot:option(Value, "_dest", translate("Destination Path (optional)"))
|
||||
v_dest.forcewrite = true
|
||||
v_dest.placeholder = "/.snapshot/202002051538"
|
||||
v_dest.write = function(self, section, value)
|
||||
value_dest = value
|
||||
end
|
||||
local btn_snp_create = s_snapshot:option(Button, "_snp_create")
|
||||
btn_snp_create.title = " "
|
||||
btn_snp_create.inputtitle = translate("New Snapshot")
|
||||
btn_snp_create.inputstyle = "add"
|
||||
btn_snp_create.write = function(self, section, value)
|
||||
if value_sorce and value_sorce:match("^/") then
|
||||
if not value_dest then value_dest = "/.snapshot"..value_sorce.."/"..os.date("%Y%m%d%H%M%S") end
|
||||
nixio.fs.mkdirr(mount_point..value_dest:match("(.-)[^/]+$"))
|
||||
local cmd = dm.command.btrfs .. " subvolume snapshot" .. (value_readonly == 1 and " -r " or " ") .. mount_point..value_sorce .. " " .. mount_point..value_dest
|
||||
local res = luci.util.exec(cmd .. " 2>&1")
|
||||
if res and (res:match("ERR") or res:match("not enough arguments")) then
|
||||
m.errmessage = luci.util.pcdata(res)
|
||||
else
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman/btrfs/" .. uuid))
|
||||
end
|
||||
else
|
||||
m.errmessage = translate("Please input Source Path of snapshot, Source Path must start with '/'")
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
327
luci-app-diskman/luasrc/model/cbi/diskman/disks.lua
Normal file
|
@ -0,0 +1,327 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-diskman>
|
||||
]]--
|
||||
|
||||
require "luci.util"
|
||||
require("luci.tools.webadmin")
|
||||
local dm = require "luci.model.diskman"
|
||||
|
||||
-- Use (non-UCI) SimpleForm since we have no related config file
|
||||
m = SimpleForm("diskman", translate("DiskMan"), translate("Manage Disks over LuCI."))
|
||||
m.template = "diskman/cbi/xsimpleform"
|
||||
m:append(Template("diskman/disk_info"))
|
||||
-- disable submit and reset button
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
-- rescan disks
|
||||
rescan = m:section(SimpleSection)
|
||||
rescan_button = rescan:option(Button, "_rescan")
|
||||
rescan_button.inputtitle= translate("Rescan Disks")
|
||||
rescan_button.template = "diskman/cbi/inlinebutton"
|
||||
rescan_button.inputstyle = "add"
|
||||
rescan_button.forcewrite = true
|
||||
rescan_button.write = function(self, section, value)
|
||||
luci.util.exec("echo '- - -' | tee /sys/class/scsi_host/host*/scan > /dev/null")
|
||||
if dm.command.mdadm then
|
||||
luci.util.exec(dm.command.mdadm .. " --assemble --scan")
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman"))
|
||||
end
|
||||
|
||||
-- disks
|
||||
local disks = dm.list_devices()
|
||||
d = m:section(Table, disks, translate("Disks"))
|
||||
d.config = "disk"
|
||||
-- option(type, id(key of table), text)
|
||||
d:option(DummyValue, "path", translate("Path"))
|
||||
d:option(DummyValue, "model", translate("Model"))
|
||||
d:option(DummyValue, "sn", translate("Serial Number"))
|
||||
d:option(DummyValue, "size_formated", translate("Size"))
|
||||
d:option(DummyValue, "temp", translate("Temp"))
|
||||
-- d:option(DummyValue, "sec_size", translate("Sector Size "))
|
||||
d:option(DummyValue, "p_table", translate("Partition Table"))
|
||||
d:option(DummyValue, "sata_ver", translate("SATA Version"))
|
||||
-- d:option(DummyValue, "rota_rate", translate("Rotation Rate"))
|
||||
d:option(DummyValue, "health", translate("Health"))
|
||||
d:option(DummyValue, "status", translate("Status"))
|
||||
|
||||
d.extedit = luci.dispatcher.build_url("admin/system/diskman/partition/%s")
|
||||
|
||||
-- raid devices
|
||||
if dm.command.mdadm then
|
||||
local raid_devices = dm.list_raid_devices()
|
||||
-- raid_devices = diskmanager.getRAIDdevices()
|
||||
if next(raid_devices) ~= nil then
|
||||
local r = m:section(Table, raid_devices, translate("RAID Devices"))
|
||||
r.config = "_raid"
|
||||
r:option(DummyValue, "path", translate("Path"))
|
||||
r:option(DummyValue, "level", translate("RAID mode"))
|
||||
r:option(DummyValue, "size_formated", translate("Size"))
|
||||
r:option(DummyValue, "p_table", translate("Partition Table"))
|
||||
r:option(DummyValue, "status", translate("Status"))
|
||||
r:option(DummyValue, "members_str", translate("Members"))
|
||||
r:option(DummyValue, "active", translate("Active"))
|
||||
r.extedit = luci.dispatcher.build_url("admin/system/diskman/partition/%s")
|
||||
end
|
||||
end
|
||||
|
||||
-- btrfs devices
|
||||
if dm.command.btrfs then
|
||||
btrfs_devices = dm.list_btrfs_devices()
|
||||
if next(btrfs_devices) ~= nil then
|
||||
local table_btrfs = m:section(Table, btrfs_devices, translate("Btrfs"))
|
||||
table_btrfs:option(DummyValue, "uuid", translate("UUID"))
|
||||
table_btrfs:option(DummyValue, "label", translate("Label"))
|
||||
table_btrfs:option(DummyValue, "members", translate("Members"))
|
||||
-- sieze is error, since there is RAID
|
||||
-- table_btrfs:option(DummyValue, "size_formated", translate("Size"))
|
||||
table_btrfs:option(DummyValue, "used_formated", translate("Usage"))
|
||||
table_btrfs.extedit = luci.dispatcher.build_url("admin/system/diskman/btrfs/%s")
|
||||
end
|
||||
end
|
||||
|
||||
-- mount point
|
||||
local mount_point = dm.get_mount_points()
|
||||
local _mount_point = {}
|
||||
table.insert( mount_point, { device = 0 } )
|
||||
local table_mp = m:section(Table, mount_point, translate("Mount Point"))
|
||||
local v_device = table_mp:option(Value, "device", translate("Device"))
|
||||
v_device.render = function(self, section, scope)
|
||||
if mount_point[section].device == 0 then
|
||||
self.template = "cbi/value"
|
||||
self.forcewrite = true
|
||||
for dev, info in pairs(disks) do
|
||||
for i, v in ipairs(info.partitions) do
|
||||
self:value("/dev/".. v.name, "/dev/".. v.name .. " ".. v.size_formated)
|
||||
end
|
||||
end
|
||||
Value.render(self, section, scope)
|
||||
else
|
||||
self.template = "cbi/dvalue"
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
v_device.write = function(self, section, value)
|
||||
_mount_point.device = value and value:gsub("%s+", "") or ""
|
||||
end
|
||||
local v_fs = table_mp:option(Value, "fs", translate("File System"))
|
||||
v_fs.render = function(self, section, scope)
|
||||
if mount_point[section].device == 0 then
|
||||
self.template = "cbi/value"
|
||||
self:value("auto", "auto")
|
||||
self.default = "auto"
|
||||
self.forcewrite = true
|
||||
Value.render(self, section, scope)
|
||||
else
|
||||
self.template = "cbi/dvalue"
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
v_fs.write = function(self, section, value)
|
||||
_mount_point.fs = value and value:gsub("%s+", "") or ""
|
||||
end
|
||||
local v_mount_option = table_mp:option(Value, "mount_options", translate("Mount Options"))
|
||||
v_mount_option.render = function(self, section, scope)
|
||||
if mount_point[section].device == 0 then
|
||||
self.template = "cbi/value"
|
||||
self.placeholder = "rw,noauto"
|
||||
self.forcewrite = true
|
||||
Value.render(self, section, scope)
|
||||
else
|
||||
self.template = "cbi/dvalue"
|
||||
local mp = mount_point[section].mount_options
|
||||
mount_point[section].mount_options = nil
|
||||
local length = 0
|
||||
for k in mp:gmatch("([^,]+)") do
|
||||
mount_point[section].mount_options = mount_point[section].mount_options and (mount_point[section].mount_options .. ",") or ""
|
||||
if length > 20 then
|
||||
mount_point[section].mount_options = mount_point[section].mount_options.. " <br>"
|
||||
length = 0
|
||||
end
|
||||
mount_point[section].mount_options = mount_point[section].mount_options .. k
|
||||
length = length + #k
|
||||
end
|
||||
self.rawhtml = true
|
||||
-- mount_point[section].mount_options = #mount_point[section].mount_options > 50 and mount_point[section].mount_options:sub(1,50) .. "..." or mount_point[section].mount_options
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
v_mount_option.write = function(self, section, value)
|
||||
_mount_point.mount_options = value and value:gsub("%s+", "") or ""
|
||||
end
|
||||
local v_mount_point = table_mp:option(Value, "mount_point", translate("Mount Point"))
|
||||
v_mount_point.render = function(self, section, scope)
|
||||
if mount_point[section].device == 0 then
|
||||
self.template = "cbi/value"
|
||||
self.placeholder = "/media/diskX"
|
||||
self.forcewrite = true
|
||||
Value.render(self, section, scope)
|
||||
else
|
||||
self.template = "cbi/dvalue"
|
||||
local new_mp = ""
|
||||
local v_mp_d
|
||||
for v_mp_d in self["section"]["data"][section]["mount_point"]:gmatch('[^/]+') do
|
||||
if #v_mp_d > 12 then
|
||||
new_mp = new_mp .. "/" .. v_mp_d:sub(1,7) .. ".." .. v_mp_d:sub(-4)
|
||||
else
|
||||
new_mp = new_mp .."/".. v_mp_d
|
||||
end
|
||||
end
|
||||
self["section"]["data"][section]["mount_point"] = '<span title="'..self["section"]["data"][section]["mount_point"] .. '" >'..new_mp..'</span>'
|
||||
self.rawhtml = true
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
v_mount_point.write = function(self, section, value)
|
||||
_mount_point.mount_point = value
|
||||
end
|
||||
local btn_umount = table_mp:option(Button, "_mount", translate("Mount"))
|
||||
btn_umount.forcewrite = true
|
||||
btn_umount.render = function(self, section, scope)
|
||||
if mount_point[section].device == 0 then
|
||||
self.inputtitle = translate("Mount")
|
||||
btn_umount.inputstyle = "add"
|
||||
else
|
||||
self.inputtitle = translate("Umount")
|
||||
btn_umount.inputstyle = "remove"
|
||||
end
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
btn_umount.write = function(self, section, value)
|
||||
local res
|
||||
if value == translate("Mount") then
|
||||
if not _mount_point.mount_point or not _mount_point.device then return end
|
||||
luci.util.exec("mkdir -p ".. _mount_point.mount_point)
|
||||
res = luci.util.exec(dm.command.mount .. " ".. _mount_point.device .. (_mount_point.fs and (" -t ".. _mount_point.fs )or "") .. (_mount_point.mount_options and (" -o " .. _mount_point.mount_options.. " ") or " ").._mount_point.mount_point .. " 2>&1")
|
||||
elseif value == translate("Umount") then
|
||||
res = luci.util.exec(dm.command.umount .. " "..mount_point[section].mount_point .. " 2>&1")
|
||||
end
|
||||
if res:match("^mount:") or res:match("^umount:") then
|
||||
m.errmessage = luci.util.pcdata(res)
|
||||
else
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman"))
|
||||
end
|
||||
end
|
||||
|
||||
if dm.command.mdadm or dm.command.btrfs then
|
||||
local creation_section = m:section(TypedSection, "_creation")
|
||||
creation_section.cfgsections=function()
|
||||
return {translate("Creation")}
|
||||
end
|
||||
creation_section:tab("raid", translate("RAID"), translate("RAID Creation"))
|
||||
creation_section:tab("btrfs", translate("Btrfs"), translate("Multiple Devices Btrfs Creation"))
|
||||
|
||||
-- raid functions
|
||||
if dm.command.mdadm then
|
||||
|
||||
local rname, rmembers, rlevel
|
||||
local r_name = creation_section:taboption("raid", Value, "_rname", translate("Raid Name"))
|
||||
r_name.placeholder = "/dev/md0"
|
||||
r_name.write = function(self, section, value)
|
||||
rname = value
|
||||
end
|
||||
local r_level = creation_section:taboption("raid", ListValue, "_rlevel", translate("Raid Level"))
|
||||
local valid_raid = luci.util.exec("lsmod | grep md_mod")
|
||||
if valid_raid:match("linear") then
|
||||
r_level:value("linear", "Linear")
|
||||
end
|
||||
if valid_raid:match("raid456") then
|
||||
r_level:value("5", "Raid 5")
|
||||
r_level:value("6", "Raid 6")
|
||||
end
|
||||
if valid_raid:match("raid1") then
|
||||
r_level:value("1", "Raid 1")
|
||||
end
|
||||
if valid_raid:match("raid0") then
|
||||
r_level:value("0", "Raid 0")
|
||||
end
|
||||
if valid_raid:match("raid10") then
|
||||
r_level:value("10", "Raid 10")
|
||||
end
|
||||
r_level.write = function(self, section, value)
|
||||
rlevel = value
|
||||
end
|
||||
local r_member = creation_section:taboption("raid", DynamicList, "_rmember", translate("Raid Member"))
|
||||
for dev, info in pairs(disks) do
|
||||
if not info.inuse and #info.partitions == 0 then
|
||||
r_member:value(info.path, info.path.. " ".. info.size_formated)
|
||||
end
|
||||
for i, v in ipairs(info.partitions) do
|
||||
if not v.inuse then
|
||||
r_member:value("/dev/".. v.name, "/dev/".. v.name .. " ".. v.size_formated)
|
||||
end
|
||||
end
|
||||
end
|
||||
r_member.write = function(self, section, value)
|
||||
rmembers = value
|
||||
end
|
||||
local r_create = creation_section:taboption("raid", Button, "_rcreate")
|
||||
r_create.render = function(self, section, scope)
|
||||
self.title = " "
|
||||
self.inputtitle = translate("Create Raid")
|
||||
self.inputstyle = "add"
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
r_create.write = function(self, section, value)
|
||||
-- mdadm --create --verbose /dev/md0 --level=stripe --raid-devices=2 /dev/sdb6 /dev/sdc5
|
||||
local res = dm.create_raid(rname, rlevel, rmembers)
|
||||
if res and res:match("^ERR") then
|
||||
m.errmessage = luci.util.pcdata(res)
|
||||
return
|
||||
end
|
||||
dm.gen_mdadm_config()
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman"))
|
||||
end
|
||||
end
|
||||
|
||||
-- btrfs
|
||||
if dm.command.btrfs then
|
||||
local blabel, bmembers, blevel
|
||||
local btrfs_label = creation_section:taboption("btrfs", Value, "_blabel", translate("Btrfs Label"))
|
||||
btrfs_label.write = function(self, section, value)
|
||||
blabel = value
|
||||
end
|
||||
local btrfs_level = creation_section:taboption("btrfs", ListValue, "_blevel", translate("Btrfs Raid Level"))
|
||||
btrfs_level:value("single", "Single")
|
||||
btrfs_level:value("raid0", "Raid 0")
|
||||
btrfs_level:value("raid1", "Raid 1")
|
||||
btrfs_level:value("raid10", "Raid 10")
|
||||
btrfs_level.write = function(self, section, value)
|
||||
blevel = value
|
||||
end
|
||||
|
||||
local btrfs_member = creation_section:taboption("btrfs", DynamicList, "_bmember", translate("Btrfs Member"))
|
||||
for dev, info in pairs(disks) do
|
||||
if not info.inuse and #info.partitions == 0 then
|
||||
btrfs_member:value(info.path, info.path.. " ".. info.size_formated)
|
||||
end
|
||||
for i, v in ipairs(info.partitions) do
|
||||
if not v.inuse then
|
||||
btrfs_member:value("/dev/".. v.name, "/dev/".. v.name .. " ".. v.size_formated)
|
||||
end
|
||||
end
|
||||
end
|
||||
btrfs_member.write = function(self, section, value)
|
||||
bmembers = value
|
||||
end
|
||||
local btrfs_create = creation_section:taboption("btrfs", Button, "_bcreate")
|
||||
btrfs_create.render = function(self, section, scope)
|
||||
self.title = " "
|
||||
self.inputtitle = translate("Create Btrfs")
|
||||
self.inputstyle = "add"
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
btrfs_create.write = function(self, section, value)
|
||||
-- mkfs.btrfs -L label -d blevel /dev/sda /dev/sdb
|
||||
local res = dm.create_btrfs(blabel, blevel, bmembers)
|
||||
if res and res:match("^ERR") then
|
||||
m.errmessage = luci.util.pcdata(res)
|
||||
return
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
366
luci-app-diskman/luasrc/model/cbi/diskman/partition.lua
Normal file
|
@ -0,0 +1,366 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-diskman>
|
||||
]]--
|
||||
|
||||
require "luci.util"
|
||||
require("luci.tools.webadmin")
|
||||
local dm = require "luci.model.diskman"
|
||||
local dev = arg[1]
|
||||
|
||||
if not dev then
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman"))
|
||||
elseif not nixio.fs.access("/dev/"..dev) then
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman"))
|
||||
end
|
||||
|
||||
m = SimpleForm("partition", translate("Partition Management"), translate("Partition Disk over LuCI."))
|
||||
m.template = "diskman/cbi/xsimpleform"
|
||||
m.redirect = luci.dispatcher.build_url("admin/system/diskman")
|
||||
m:append(Template("diskman/partition_info"))
|
||||
-- disable submit and reset button
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
|
||||
local disk_info = dm.get_disk_info(dev, true)
|
||||
local format_cmd = dm.get_format_cmd()
|
||||
|
||||
s = m:section(Table, {disk_info}, translate("Device Info"))
|
||||
-- s:option(DummyValue, "key")
|
||||
-- s:option(DummyValue, "value")
|
||||
s:option(DummyValue, "path", translate("Path"))
|
||||
s:option(DummyValue, "model", translate("Model"))
|
||||
s:option(DummyValue, "sn", translate("Serial Number"))
|
||||
s:option(DummyValue, "size_formated", translate("Size"))
|
||||
s:option(DummyValue, "sec_size", translate("Sector Size"))
|
||||
local dv_p_table = s:option(ListValue, "p_table", translate("Partition Table"))
|
||||
dv_p_table.render = function(self, section, scope)
|
||||
-- create table only if not used by raid and no partitions on disk
|
||||
if not disk_info.p_table:match("Raid") and (#disk_info.partitions == 0 or (#disk_info.partitions == 1 and disk_info.partitions[1].number == -1) or (disk_info.p_table:match("LOOP") and not disk_info.partitions[1].inuse)) then
|
||||
self:value(disk_info.p_table, disk_info.p_table)
|
||||
self:value("GPT", "GPT")
|
||||
self:value("MBR", "MBR")
|
||||
self.default = disk_info.p_table
|
||||
ListValue.render(self, section, scope)
|
||||
else
|
||||
self.template = "cbi/dvalue"
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
if disk_info.type:match("md") then
|
||||
s:option(DummyValue, "level", translate("Level"))
|
||||
s:option(DummyValue, "members_str", translate("Members"))
|
||||
else
|
||||
s:option(DummyValue, "temp", translate("Temp"))
|
||||
s:option(DummyValue, "sata_ver", translate("SATA Version"))
|
||||
s:option(DummyValue, "rota_rate", translate("Rotation Rate"))
|
||||
end
|
||||
s:option(DummyValue, "status", translate("Status"))
|
||||
local btn_health = s:option(Button, "health", translate("Health"))
|
||||
btn_health.render = function(self, section, scope)
|
||||
if disk_info.health then
|
||||
self.inputtitle = disk_info.health
|
||||
if disk_info.health == "PASSED" then
|
||||
self.inputstyle = "add"
|
||||
else
|
||||
self.inputstyle = "remove"
|
||||
end
|
||||
Button.render(self, section, scope)
|
||||
else
|
||||
self.template = "cbi/dvalue"
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
|
||||
local btn_eject = s:option(Button, "_eject")
|
||||
btn_eject.template = "diskman/cbi/disabled_button"
|
||||
btn_eject.inputstyle = "remove"
|
||||
btn_eject.render = function(self, section, scope)
|
||||
for i, p in ipairs(disk_info.partitions) do
|
||||
if p.mount_point ~= "-" then
|
||||
self.view_disabled = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if disk_info.p_table:match("Raid") then
|
||||
self.view_disabled = true
|
||||
end
|
||||
if disk_info.type:match("md") then
|
||||
btn_eject.inputtitle = translate("Remove")
|
||||
else
|
||||
btn_eject.inputtitle = translate("Eject")
|
||||
end
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
btn_eject.forcewrite = true
|
||||
btn_eject.write = function(self, section, value)
|
||||
for i, p in ipairs(disk_info.partitions) do
|
||||
if p.mount_point ~= "-" then
|
||||
m.errmessage = p.name .. translate("is in use! please unmount it first!")
|
||||
return
|
||||
end
|
||||
end
|
||||
if disk_info.type:match("md") then
|
||||
luci.util.exec(dm.command.mdadm .. " --stop /dev/" .. dev)
|
||||
luci.util.exec(dm.command.mdadm .. " --remove /dev/" .. dev)
|
||||
for _, disk in ipairs(disk_info.members) do
|
||||
luci.util.exec(dm.command.mdadm .. " --zero-superblock " .. disk)
|
||||
end
|
||||
dm.gen_mdadm_config()
|
||||
else
|
||||
luci.util.exec("echo 1 > /sys/block/" .. dev .. "/device/delete")
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman"))
|
||||
end
|
||||
-- eject: echo 1 > /sys/block/(device)/device/delete
|
||||
-- rescan: echo '- - -' | tee /sys/class/scsi_host/host*/scan > /dev/null
|
||||
|
||||
|
||||
-- partitions info
|
||||
if not disk_info.p_table:match("Raid") then
|
||||
s_partition_table = m:section(Table, disk_info.partitions, translate("Partitions Info"), translate("Default 2048 sector alignment, support +size{b,k,m,g,t} in End Sector"))
|
||||
|
||||
-- s_partition_table:option(DummyValue, "number", translate("Number"))
|
||||
s_partition_table:option(DummyValue, "name", translate("Name"))
|
||||
local val_sec_start = s_partition_table:option(Value, "sec_start", translate("Start Sector"))
|
||||
val_sec_start.render = function(self, section, scope)
|
||||
-- could create new partition
|
||||
if disk_info.partitions[section].number == -1 and disk_info.partitions[section].size > 1 * 1024 * 1024 then
|
||||
self.template = "cbi/value"
|
||||
Value.render(self, section, scope)
|
||||
else
|
||||
self.template = "cbi/dvalue"
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
local val_sec_end = s_partition_table:option(Value, "sec_end", translate("End Sector"))
|
||||
val_sec_end.render = function(self, section, scope)
|
||||
-- could create new partition
|
||||
if disk_info.partitions[section].number == -1 and disk_info.partitions[section].size > 1 * 1024 * 1024 then
|
||||
self.template = "cbi/value"
|
||||
Value.render(self, section, scope)
|
||||
else
|
||||
self.template = "cbi/dvalue"
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
val_sec_start.forcewrite = true
|
||||
val_sec_start.write = function(self, section, value)
|
||||
disk_info.partitions[section]._sec_start = value
|
||||
end
|
||||
val_sec_end.forcewrite = true
|
||||
val_sec_end.write = function(self, section, value)
|
||||
disk_info.partitions[section]._sec_end = value
|
||||
end
|
||||
s_partition_table:option(DummyValue, "size_formated", translate("Size"))
|
||||
if disk_info.p_table == "MBR" then
|
||||
s_partition_table:option(DummyValue, "type", translate("Type"))
|
||||
end
|
||||
s_partition_table:option(DummyValue, "used_formated", translate("Used"))
|
||||
s_partition_table:option(DummyValue, "free_formated", translate("Free Space"))
|
||||
s_partition_table:option(DummyValue, "usage", translate("Usage"))
|
||||
local dv_mount_point = s_partition_table:option(DummyValue, "mount_point", translate("Mount Point"))
|
||||
dv_mount_point.rawhtml = true
|
||||
dv_mount_point.render = function(self, section, scope)
|
||||
local new_mp = ""
|
||||
local v_mp_d
|
||||
for line in self["section"]["data"][section]["mount_point"]:gmatch("[^%s]+") do
|
||||
if line == '-' then
|
||||
new_mp = line
|
||||
break
|
||||
end
|
||||
for v_mp_d in line:gmatch('[^/]+') do
|
||||
if #v_mp_d > 12 then
|
||||
new_mp = new_mp .. "/" .. v_mp_d:sub(1,7) .. ".." .. v_mp_d:sub(-4)
|
||||
else
|
||||
new_mp = new_mp .."/".. v_mp_d
|
||||
end
|
||||
end
|
||||
new_mp = '<span title="'.. line .. '" >' ..new_mp ..'</span>' .. "<br/>"
|
||||
end
|
||||
self["section"]["data"][section]["mount_point"] = new_mp
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
local val_fs = s_partition_table:option(Value, "fs", translate("File System"))
|
||||
val_fs.forcewrite = true
|
||||
val_fs.partitions = disk_info.partitions
|
||||
for k, v in pairs(format_cmd) do
|
||||
val_fs.format_cmd = val_fs.format_cmd and (val_fs.format_cmd .. "," .. k) or k
|
||||
end
|
||||
|
||||
val_fs.write = function(self, section, value)
|
||||
disk_info.partitions[section]._fs = value
|
||||
end
|
||||
val_fs.render = function(self, section, scope)
|
||||
-- use listvalue when partition not mounted
|
||||
if disk_info.partitions[section].mount_point == "-" and disk_info.partitions[section].number ~= -1 and disk_info.partitions[section].type ~= "extended" then
|
||||
self.template = "diskman/cbi/format_button"
|
||||
self.inputstyle = "reset"
|
||||
self.inputtitle = disk_info.partitions[section].fs == "raw" and translate("Format") or disk_info.partitions[section].fs
|
||||
Button.render(self, section, scope)
|
||||
-- self:reset_values()
|
||||
-- self.keylist = {}
|
||||
-- self.vallist = {}
|
||||
-- for k, v in pairs(format_cmd) do
|
||||
-- self:value(k,k)
|
||||
-- end
|
||||
-- self.default = disk_info.partitions[section].fs
|
||||
else
|
||||
-- self:reset_values()
|
||||
-- self.keylist = {}
|
||||
-- self.vallist = {}
|
||||
self.template = "cbi/dvalue"
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
-- btn_format = s_partition_table:option(Button, "_format")
|
||||
-- btn_format.template = "diskman/cbi/format_button"
|
||||
-- btn_format.partitions = disk_info.partitions
|
||||
-- btn_format.render = function(self, section, scope)
|
||||
-- if disk_info.partitions[section].mount_point == "-" and disk_info.partitions[section].number ~= -1 and disk_info.partitions[section].type ~= "extended" then
|
||||
-- self.inputtitle = translate("Format")
|
||||
-- self.template = "diskman/cbi/disabled_button"
|
||||
-- self.view_disabled = false
|
||||
-- self.inputstyle = "reset"
|
||||
-- for k, v in pairs(format_cmd) do
|
||||
-- self:depends("val_fs", "k")
|
||||
-- end
|
||||
-- -- elseif disk_info.partitions[section].mount_point ~= "-" and disk_info.partitions[section].number ~= -1 then
|
||||
-- -- self.inputtitle = "Format"
|
||||
-- -- self.template = "diskman/cbi/disabled_button"
|
||||
-- -- self.view_disabled = true
|
||||
-- -- self.inputstyle = "reset"
|
||||
-- else
|
||||
-- self.inputtitle = ""
|
||||
-- self.template = "cbi/dvalue"
|
||||
-- end
|
||||
-- Button.render(self, section, scope)
|
||||
-- end
|
||||
-- btn_format.forcewrite = true
|
||||
-- btn_format.write = function(self, section, value)
|
||||
-- local partition_name = "/dev/".. disk_info.partitions[section].name
|
||||
-- if not nixio.fs.access(partition_name) then
|
||||
-- m.errmessage = translate("Partition NOT found!")
|
||||
-- return
|
||||
-- end
|
||||
-- local fs = disk_info.partitions[section]._fs
|
||||
-- if not format_cmd[fs] then
|
||||
-- m.errmessage = translate("Filesystem NOT support!")
|
||||
-- return
|
||||
-- end
|
||||
-- local cmd = format_cmd[fs].cmd .. " " .. format_cmd[fs].option .. " " .. partition_name
|
||||
-- local res = luci.util.exec(cmd .. " 2>&1")
|
||||
-- if res and res:lower():match("error+") then
|
||||
-- m.errmessage = luci.util.pcdata(res)
|
||||
-- else
|
||||
-- luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman/partition/" .. dev))
|
||||
-- end
|
||||
-- end
|
||||
|
||||
local btn_action = s_partition_table:option(Button, "_action")
|
||||
btn_action.forcewrite = true
|
||||
btn_action.template = "diskman/cbi/disabled_button"
|
||||
btn_action.render = function(self, section, scope)
|
||||
-- if partition is mounted or the size < 1mb, then disable the add action
|
||||
if disk_info.partitions[section].mount_point ~= "-" or (disk_info.partitions[section].type ~= "extended" and disk_info.partitions[section].number == -1 and disk_info.partitions[section].size <= 1 * 1024 * 1024) then
|
||||
self.view_disabled = true
|
||||
-- self.inputtitle = ""
|
||||
-- self.template = "cbi/dvalue"
|
||||
elseif disk_info.partitions[section].type == "extended" and next(disk_info.partitions[section]["logicals"]) ~= nil then
|
||||
self.view_disabled = true
|
||||
else
|
||||
-- self.template = "diskman/cbi/disabled_button"
|
||||
self.view_disabled = false
|
||||
end
|
||||
if disk_info.partitions[section].number ~= -1 then
|
||||
self.inputtitle = translate("Remove")
|
||||
self.inputstyle = "remove"
|
||||
else
|
||||
self.inputtitle = translate("New")
|
||||
self.inputstyle = "add"
|
||||
end
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
btn_action.write = function(self, section, value)
|
||||
if value == translate("New") then
|
||||
local start_sec = disk_info.partitions[section]._sec_start and tonumber(disk_info.partitions[section]._sec_start) or tonumber(disk_info.partitions[section].sec_start)
|
||||
local end_sec = disk_info.partitions[section]._sec_end
|
||||
|
||||
if start_sec then
|
||||
-- for sector alignment
|
||||
local align = tonumber(disk_info.phy_sec) / tonumber(disk_info.logic_sec)
|
||||
align = (align < 2048) and 2048
|
||||
if start_sec < 2048 then
|
||||
start_sec = "2048" .. "s"
|
||||
elseif math.fmod( start_sec, align ) ~= 0 then
|
||||
start_sec = tostring(start_sec + align - math.fmod( start_sec, align )) .. "s"
|
||||
else
|
||||
start_sec = start_sec .. "s"
|
||||
end
|
||||
else
|
||||
m.errmessage = translate("Invalid Start Sector!")
|
||||
return
|
||||
end
|
||||
-- support +size format for End sector
|
||||
local end_size, end_unit = end_sec:match("^+(%d-)([bkmgtsBKMGTS])$")
|
||||
if tonumber(end_size) and end_unit then
|
||||
local unit ={
|
||||
B=1,
|
||||
S=512,
|
||||
K=1024,
|
||||
M=1048576,
|
||||
G=1073741824,
|
||||
T=1099511627776
|
||||
}
|
||||
end_unit = end_unit:upper()
|
||||
end_sec = tostring(tonumber(end_size) * unit[end_unit] / unit["S"] + tonumber(start_sec:sub(1,-2)) - 1 ) .. "s"
|
||||
elseif tonumber(end_sec) then
|
||||
end_sec = end_sec .. "s"
|
||||
else
|
||||
m.errmessage = translate("Invalid End Sector!")
|
||||
return
|
||||
end
|
||||
local part_type = "primary"
|
||||
|
||||
if disk_info.p_table == "MBR" and disk_info["extended_partition_index"] then
|
||||
if tonumber(disk_info.partitions[disk_info["extended_partition_index"]].sec_start) <= tonumber(start_sec:sub(1,-2)) and tonumber(disk_info.partitions[disk_info["extended_partition_index"]].sec_end) >= tonumber(end_sec:sub(1,-2)) then
|
||||
part_type = "logical"
|
||||
if tonumber(start_sec:sub(1,-2)) - tonumber(disk_info.partitions[section].sec_start) < 2048 then
|
||||
start_sec = tonumber(start_sec:sub(1,-2)) + 2048
|
||||
start_sec = start_sec .."s"
|
||||
end
|
||||
end
|
||||
elseif disk_info.p_table == "GPT" then
|
||||
-- AUTOMATIC FIX GPT PARTITION TABLE
|
||||
-- Not all of the space available to /dev/sdb appears to be used, you can fix the GPT to use all of the space (an extra 16123870 blocks) or continue with the current setting?
|
||||
local cmd = ' printf "ok\nfix\n" | parted ---pretend-input-tty /dev/'.. dev ..' print'
|
||||
luci.util.exec(cmd .. " 2>&1")
|
||||
end
|
||||
|
||||
-- partiton
|
||||
local cmd = dm.command.parted .. " -s -a optimal /dev/" .. dev .. " mkpart " .. part_type .." " .. start_sec .. " " .. end_sec
|
||||
local res = luci.util.exec(cmd .. " 2>&1")
|
||||
if res and res:lower():match("error+") then
|
||||
m.errmessage = luci.util.pcdata(res)
|
||||
else
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman/partition/" .. dev))
|
||||
end
|
||||
elseif value == translate("Remove") then
|
||||
-- remove partition
|
||||
local number = tostring(disk_info.partitions[section].number)
|
||||
if (not number) or (number == "") then
|
||||
m.errmessage = translate("Partition not exists!")
|
||||
return
|
||||
end
|
||||
local cmd = dm.command.parted .. " -s /dev/" .. dev .. " rm " .. number
|
||||
local res = luci.util.exec(cmd .. " 2>&1")
|
||||
if res and res:lower():match("error+") then
|
||||
m.errmessage = luci.util.pcdata(res)
|
||||
else
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman/partition/" .. dev))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
738
luci-app-diskman/luasrc/model/diskman.lua
Normal file
|
@ -0,0 +1,738 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-diskman>
|
||||
]]--
|
||||
|
||||
require "luci.util"
|
||||
local ver = require "luci.version"
|
||||
|
||||
local CMD = {"parted", "mdadm", "blkid", "smartctl", "df", "btrfs", "lsblk"}
|
||||
|
||||
local d = {command ={}}
|
||||
for _, cmd in ipairs(CMD) do
|
||||
local command = luci.sys.exec("/usr/bin/which " .. cmd)
|
||||
d.command[cmd] = command:match("^.+"..cmd) or nil
|
||||
end
|
||||
|
||||
d.command.mount = nixio.fs.access("/usr/bin/mount") and "/usr/bin/mount" or "/bin/mount"
|
||||
d.command.umount = nixio.fs.access("/usr/bin/umount") and "/usr/bin/umount" or "/bin/umount"
|
||||
|
||||
local proc_mounts = nixio.fs.readfile("/proc/mounts") or ""
|
||||
local mounts = luci.util.exec(d.command.mount .. " 2>/dev/null") or ""
|
||||
local swaps = nixio.fs.readfile("/proc/swaps") or ""
|
||||
local df = luci.sys.exec(d.command.df .. " 2>/dev/null") or ""
|
||||
|
||||
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 get_smart_info = function(device)
|
||||
local section
|
||||
local smart_info = {}
|
||||
for _, line in ipairs(luci.util.execl(d.command.smartctl .. " -H -A -i -n standby -f brief /dev/" .. device)) do
|
||||
local attrib, val
|
||||
if section == 1 then
|
||||
attrib, val = line:match "^(.-):%s+(.+)"
|
||||
elseif section == 2 and smart_info.nvme_ver then
|
||||
attrib, val = line:match("^(.-):%s+(.+)")
|
||||
if not smart_info.health then smart_info.health = line:match(".-overall%-health.-: (.+)") end
|
||||
elseif section == 2 then
|
||||
attrib, val = line:match("^([0-9 ]+)%s+[^ ]+%s+[POSRCK-]+%s+[0-9-]+%s+[0-9-]+%s+[0-9-]+%s+[0-9-]+%s+([0-9-]+)")
|
||||
if not smart_info.health then smart_info.health = line:match(".-overall%-health.-: (.+)") end
|
||||
else
|
||||
attrib = line:match "^=== START OF (.*) SECTION ==="
|
||||
if attrib and attrib:match("INFORMATION") then
|
||||
section = 1
|
||||
elseif attrib and attrib:match("SMART DATA") then
|
||||
section = 2
|
||||
elseif not smart_info.status then
|
||||
val = line:match "^Device is in (.*) mode"
|
||||
if val then smart_info.status = val end
|
||||
end
|
||||
end
|
||||
|
||||
if not attrib then
|
||||
if section ~= 2 then section = 0 end
|
||||
elseif (attrib == "Power mode is") or
|
||||
(attrib == "Power mode was") then
|
||||
smart_info.status = val:match("(%S+)")
|
||||
-- elseif attrib == "Sector Sizes" then
|
||||
-- -- 512 bytes logical, 4096 bytes physical
|
||||
-- smart_info.phy_sec = val:match "([0-9]*) bytes physical"
|
||||
-- smart_info.logic_sec = val:match "([0-9]*) bytes logical"
|
||||
-- elseif attrib == "Sector Size" then
|
||||
-- -- 512 bytes logical/physical
|
||||
-- smart_info.phy_sec = val:match "([0-9]*)"
|
||||
-- smart_info.logic_sec = smart_info.phy_sec
|
||||
elseif attrib == "Serial Number" then
|
||||
smart_info.sn = val
|
||||
elseif attrib == "194" or attrib == "Temperature" then
|
||||
smart_info.temp = val:match("(%d+)") .. "°C"
|
||||
elseif attrib == "Rotation Rate" then
|
||||
smart_info.rota_rate = val
|
||||
elseif attrib == "SATA Version is" then
|
||||
smart_info.sata_ver = val
|
||||
elseif attrib == "NVMe Version" then
|
||||
smart_info.nvme_ver = val
|
||||
end
|
||||
end
|
||||
return smart_info
|
||||
end
|
||||
|
||||
local parse_parted_info = function(keys, line)
|
||||
-- parse the output of parted command (machine parseable format)
|
||||
-- /dev/sda:5860533168s:scsi:512:4096:gpt:ATA ST3000DM001-1ER1:;
|
||||
-- 1:34s:2047s:2014s:free;
|
||||
-- 1:2048s:1073743872s:1073741825s:ext4:primary:;
|
||||
local result = {}
|
||||
local values = {}
|
||||
|
||||
for value in line:gmatch("(.-)[:;]") do table.insert(values, value) end
|
||||
for i = 1,#keys do
|
||||
result[keys[i]] = values[i] or ""
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local is_raid_member = function(partition)
|
||||
-- check if inuse as raid member
|
||||
if nixio.fs.access("/proc/mdstat") then
|
||||
for _, result in ipairs(luci.util.execl("grep md /proc/mdstat | sed 's/[][]//g'")) do
|
||||
local md, buf
|
||||
md, buf = result:match("(md.-):(.+)")
|
||||
if buf:match(partition) then
|
||||
return "Raid Member: ".. md
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local get_mount_point = function(partition)
|
||||
local mount_point
|
||||
for m in mounts:gmatch("/dev/"..partition.." on ([^ ]*)") do
|
||||
mount_point = (mount_point and (mount_point .. " ") or "") .. m
|
||||
end
|
||||
if mount_point then return mount_point end
|
||||
-- result = luci.sys.exec('cat /proc/mounts | awk \'{if($1=="/dev/'.. partition ..'") print $2}\'')
|
||||
-- if result ~= "" then return result end
|
||||
|
||||
if swaps:match("\n/dev/" .. partition .."%s") then return "swap" end
|
||||
-- result = luci.sys.exec("cat /proc/swaps | grep /dev/" .. partition)
|
||||
-- if result ~= "" then return "swap" end
|
||||
|
||||
return is_raid_member(partition)
|
||||
|
||||
end
|
||||
|
||||
-- return used, free, usage
|
||||
local get_partition_usage = function(partition)
|
||||
if not nixio.fs.access("/dev/"..partition) then return false end
|
||||
local used, free, usage = df:match("\n/dev/" .. partition .. "%s+%d+%s+(%d+)%s+(%d+)%s+(%d+)%%%s-")
|
||||
|
||||
usage = usage and (usage .. "%") or "-"
|
||||
used = used and (tonumber(used) * 1024) or 0
|
||||
free = free and (tonumber(free) * 1024) or 0
|
||||
|
||||
return used, free, usage
|
||||
end
|
||||
|
||||
local get_parted_info = function(device)
|
||||
if not device then return end
|
||||
local result = {partitions={}}
|
||||
local DEVICE_INFO_KEYS = { "path", "size", "type", "logic_sec", "phy_sec", "p_table", "model", "flags" }
|
||||
local PARTITION_INFO_KEYS = { "number", "sec_start", "sec_end", "size", "fs", "tag_name", "flags" }
|
||||
local partition_temp
|
||||
local partitions_temp = {}
|
||||
local disk_temp
|
||||
|
||||
for line in luci.util.execi(d.command.parted .. " -s -m /dev/" .. device .. " unit s print free", "r") do
|
||||
if line:find("^/dev/"..device..":.+") then
|
||||
disk_temp = parse_parted_info(DEVICE_INFO_KEYS, line)
|
||||
disk_temp.partitions = {}
|
||||
if disk_temp["size"] then
|
||||
local length = disk_temp["size"]:gsub("^(%d+)s$", "%1")
|
||||
local newsize = tostring(tonumber(length)*tonumber(disk_temp["logic_sec"]))
|
||||
disk_temp["size"] = newsize
|
||||
end
|
||||
if disk_temp["p_table"] == "msdos" then
|
||||
disk_temp["p_table"] = "MBR"
|
||||
else
|
||||
disk_temp["p_table"] = disk_temp["p_table"]:upper()
|
||||
end
|
||||
elseif line:find("^%d-:.+") then
|
||||
partition_temp = parse_parted_info(PARTITION_INFO_KEYS, line)
|
||||
-- use human-readable form instead of sector number
|
||||
if partition_temp["size"] then
|
||||
local length = partition_temp["size"]:gsub("^(%d+)s$", "%1")
|
||||
local newsize = (tonumber(length) * tonumber(disk_temp["logic_sec"]))
|
||||
partition_temp["size"] = newsize
|
||||
partition_temp["size_formated"] = byte_format(newsize)
|
||||
end
|
||||
partition_temp["number"] = tonumber(partition_temp["number"]) or -1
|
||||
if partition_temp["fs"] == "free" then
|
||||
partition_temp["number"] = -1
|
||||
partition_temp["fs"] = "Free Space"
|
||||
partition_temp["name"] = "-"
|
||||
elseif device:match("sd") or device:match("sata") then
|
||||
partition_temp["name"] = device..partition_temp["number"]
|
||||
elseif device:match("mmcblk") or device:match("md") or device:match("nvme") then
|
||||
partition_temp["name"] = device.."p"..partition_temp["number"]
|
||||
end
|
||||
if partition_temp["number"] > 0 and partition_temp["fs"] == "" and d.command.lsblk then
|
||||
partition_temp["fs"] = luci.util.exec(d.command.lsblk .. " /dev/"..device.. tostring(partition_temp["number"]) .. " -no fstype"):match("([^%s]+)") or ""
|
||||
end
|
||||
partition_temp["fs"] = partition_temp["fs"] == "" and "raw" or partition_temp["fs"]
|
||||
partition_temp["sec_start"] = partition_temp["sec_start"] and partition_temp["sec_start"]:sub(1,-2)
|
||||
partition_temp["sec_end"] = partition_temp["sec_end"] and partition_temp["sec_end"]:sub(1,-2)
|
||||
partition_temp["mount_point"] = partition_temp["name"]~="-" and get_mount_point(partition_temp["name"]) or "-"
|
||||
if partition_temp["mount_point"]~="-" then
|
||||
partition_temp["used"], partition_temp["free"], partition_temp["usage"] = get_partition_usage(partition_temp["name"])
|
||||
partition_temp["used_formated"] = partition_temp["used"] and byte_format(partition_temp["used"]) or "-"
|
||||
partition_temp["free_formated"] = partition_temp["free"] and byte_format(partition_temp["free"]) or "-"
|
||||
else
|
||||
partition_temp["used"], partition_temp["free"], partition_temp["usage"] = 0,0,"-"
|
||||
partition_temp["used_formated"] = "-"
|
||||
partition_temp["free_formated"] = "-"
|
||||
end
|
||||
-- if disk_temp["p_table"] == "MBR" and (partition_temp["number"] < 4) and (partition_temp["number"] > 0) then
|
||||
-- local real_size_sec = tonumber(nixio.fs.readfile("/sys/block/"..device.."/"..partition_temp["name"].."/size")) * tonumber(disk_temp.phy_sec)
|
||||
-- if real_size_sec ~= partition_temp["size"] then
|
||||
-- disk_temp["extended_partition_index"] = partition_temp["number"]
|
||||
-- partition_temp["type"] = "extended"
|
||||
-- partition_temp["size"] = real_size_sec
|
||||
-- partition_temp["fs"] = "-"
|
||||
-- partition_temp["logicals"] = {}
|
||||
-- else
|
||||
-- partition_temp["type"] = "primary"
|
||||
-- end
|
||||
-- end
|
||||
|
||||
table.insert(partitions_temp, partition_temp)
|
||||
end
|
||||
end
|
||||
if disk_temp and disk_temp["p_table"] == "MBR" then
|
||||
for i, p in ipairs(partitions_temp) do
|
||||
if disk_temp["extended_partition_index"] and p["number"] > 4 then
|
||||
if tonumber(p["sec_end"]) <= tonumber(partitions_temp[disk_temp["extended_partition_index"]]["sec_end"]) and tonumber(p["sec_start"]) >= tonumber(partitions_temp[disk_temp["extended_partition_index"]]["sec_start"]) then
|
||||
p["type"] = "logical"
|
||||
table.insert(partitions_temp[disk_temp["extended_partition_index"]]["logicals"], i)
|
||||
end
|
||||
elseif (p["number"] < 4) and (p["number"] > 0) then
|
||||
local s = nixio.fs.readfile("/sys/block/"..device.."/"..p["name"].."/size")
|
||||
if s then
|
||||
local real_size_sec = tonumber(s) * tonumber(disk_temp.phy_sec)
|
||||
-- if size not equal, it's an extended
|
||||
if real_size_sec ~= p["size"] then
|
||||
disk_temp["extended_partition_index"] = i
|
||||
p["type"] = "extended"
|
||||
p["size"] = real_size_sec
|
||||
p["fs"] = "-"
|
||||
p["logicals"] = {}
|
||||
else
|
||||
p["type"] = "primary"
|
||||
end
|
||||
else
|
||||
-- if not found in "/sys/block"
|
||||
p["type"] = "primary"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
result = disk_temp
|
||||
result.partitions = partitions_temp
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local mddetail = function(mdpath)
|
||||
local detail = {}
|
||||
local path = mdpath:match("^/dev/md%d+$")
|
||||
if path then
|
||||
local mdadm = io.popen(d.command.mdadm .. " --detail "..path, "r")
|
||||
for line in mdadm:lines() do
|
||||
local key, value = line:match("^%s*(.+) : (.+)")
|
||||
if key then
|
||||
detail[key] = value
|
||||
end
|
||||
end
|
||||
mdadm:close()
|
||||
end
|
||||
return detail
|
||||
end
|
||||
|
||||
-- return {{device="", mount_points="", fs="", mount_options="", dump="", pass=""}..}
|
||||
d.get_mount_points = function()
|
||||
local mount
|
||||
local res = {}
|
||||
local h ={"device", "mount_point", "fs", "mount_options", "dump", "pass"}
|
||||
for mount in proc_mounts:gmatch("[^\n]+") do
|
||||
local device = mount:match("^([^%s]+)%s+.+")
|
||||
-- only show /dev/xxx device
|
||||
if device and device:match("/dev/") then
|
||||
res[#res+1] = {}
|
||||
local i = 0
|
||||
for v in mount:gmatch("[^%s]+") do
|
||||
i = i + 1
|
||||
res[#res][h[i]] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
d.get_disk_info = function(device, wakeup)
|
||||
--[[ return:
|
||||
{
|
||||
path, model, sn, size, size_mounted, flags, type, temp, p_table, logic_sec, phy_sec, sec_size, sata_ver, rota_rate, status, health,
|
||||
partitions = {
|
||||
1 = { number, name, sec_start, sec_end, size, size_mounted, fs, tag_name, type, flags, mount_point, usage, used, free, used_formated, free_formated},
|
||||
2 = { number, name, sec_start, sec_end, size, size_mounted, fs, tag_name, type, flags, mount_point, usage, used, free, used_formated, free_formated},
|
||||
...
|
||||
}
|
||||
--raid devices only
|
||||
level, members, members_str
|
||||
}
|
||||
--]]
|
||||
if not device then return end
|
||||
local disk_info
|
||||
local smart_info = get_smart_info(device)
|
||||
|
||||
-- check if divice is the member of raid
|
||||
smart_info["p_table"] = is_raid_member(device..'0')
|
||||
-- if status is not active(standby), only check smart_info.
|
||||
-- if only weakup == true, weakup the disk and check parted_info.
|
||||
if smart_info.status ~= "STANDBY" or wakeup or (smart_info["p_table"] and not smart_info["p_table"]:match("Raid")) or device:match("^md") then
|
||||
disk_info = get_parted_info(device)
|
||||
disk_info["sec_size"] = disk_info["logic_sec"] .. "/" .. disk_info["phy_sec"]
|
||||
disk_info["size_formated"] = byte_format(tonumber(disk_info["size"]))
|
||||
-- if status is standby, after get part info, the disk is weakuped, then get smart_info again for more informations
|
||||
if smart_info.status ~= "ACTIVE" then smart_info = get_smart_info(device) end
|
||||
else
|
||||
disk_info = {}
|
||||
end
|
||||
|
||||
for k, v in pairs(smart_info) do
|
||||
disk_info[k] = v
|
||||
end
|
||||
|
||||
if disk_info.type and disk_info.type:match("md") then
|
||||
local raid_info = d.list_raid_devices()[disk_info["path"]:match("/dev/(.+)")]
|
||||
for k, v in pairs(raid_info) do
|
||||
disk_info[k] = v
|
||||
end
|
||||
end
|
||||
return disk_info
|
||||
end
|
||||
|
||||
d.list_raid_devices = function()
|
||||
local fs = require "nixio.fs"
|
||||
|
||||
local raid_devices = {}
|
||||
if not fs.access("/proc/mdstat") then return raid_devices end
|
||||
local mdstat = io.open("/proc/mdstat", "r")
|
||||
for line in mdstat:lines() do
|
||||
|
||||
-- md1 : active raid1 sdb2[1] sda2[0]
|
||||
-- md127 : active raid5 sdh1[6] sdg1[4] sdf1[3] sde1[2] sdd1[1] sdc1[0]
|
||||
local device_info = {}
|
||||
local mdpath, list = line:match("^(md%d+) : (.+)")
|
||||
if mdpath then
|
||||
local members = {}
|
||||
for member in string.gmatch(list, "%S+") do
|
||||
member_path = member:match("^(%S+)%[%d+%]")
|
||||
if member_path then
|
||||
member = '/dev/'..member_path
|
||||
end
|
||||
table.insert(members, member)
|
||||
end
|
||||
local active = table.remove(members, 1)
|
||||
local level = "-"
|
||||
if active == "active" then
|
||||
level = table.remove(members, 1)
|
||||
end
|
||||
|
||||
local size = tonumber(fs.readfile(string.format("/sys/class/block/%s/size", mdpath)))
|
||||
local ss = tonumber(fs.readfile(string.format("/sys/class/block/%s/queue/logical_block_size", mdpath)))
|
||||
|
||||
device_info["path"] = "/dev/"..mdpath
|
||||
device_info["size"] = size*ss
|
||||
device_info["size_formated"] = byte_format(size*ss)
|
||||
device_info["active"] = active:upper()
|
||||
device_info["level"] = level
|
||||
device_info["members"] = members
|
||||
device_info["members_str"] = table.concat(members, ", ")
|
||||
|
||||
-- Get more info from output of mdadm --detail
|
||||
local detail = mddetail(device_info["path"])
|
||||
device_info["status"] = detail["State"]:upper()
|
||||
|
||||
raid_devices[mdpath] = device_info
|
||||
end
|
||||
end
|
||||
mdstat:close()
|
||||
|
||||
return raid_devices
|
||||
end
|
||||
|
||||
-- Collect Devices information
|
||||
--[[ return:
|
||||
{
|
||||
sda={
|
||||
path, model, inuse, size_formated,
|
||||
partitions={
|
||||
{ name, inuse, size_formated }
|
||||
...
|
||||
}
|
||||
}
|
||||
..
|
||||
}
|
||||
--]]
|
||||
d.list_devices = function()
|
||||
local fs = require "nixio.fs"
|
||||
|
||||
-- get all device names (sdX and mmcblkX)
|
||||
local target_devnames = {}
|
||||
for dev in fs.dir("/dev") do
|
||||
if dev:match("^sd[a-z]$")
|
||||
or dev:match("^mmcblk%d+$")
|
||||
or dev:match("^sata[a-z]$")
|
||||
or dev:match("^nvme%d+n%d+$")
|
||||
then
|
||||
table.insert(target_devnames, dev)
|
||||
end
|
||||
end
|
||||
|
||||
local devices = {}
|
||||
for i, bname in pairs(target_devnames) do
|
||||
local device_info = {}
|
||||
local device = "/dev/" .. bname
|
||||
local size = tonumber(fs.readfile(string.format("/sys/class/block/%s/size", bname)) or "0")
|
||||
local ss = tonumber(fs.readfile(string.format("/sys/class/block/%s/queue/logical_block_size", bname)) or "0")
|
||||
local model = fs.readfile(string.format("/sys/class/block/%s/device/model", bname))
|
||||
local partitions = {}
|
||||
for part in nixio.fs.glob("/sys/block/" .. bname .."/" .. bname .. "*") do
|
||||
local pname = nixio.fs.basename(part)
|
||||
local psize = byte_format(tonumber(nixio.fs.readfile(part .. "/size"))*ss)
|
||||
local mount_point = get_mount_point(pname)
|
||||
if mount_point then device_info["inuse"] = true end
|
||||
table.insert(partitions, {name = pname, size_formated = psize, inuse = mount_point})
|
||||
end
|
||||
|
||||
device_info["path"] = device
|
||||
device_info["size_formated"] = byte_format(size*ss)
|
||||
device_info["model"] = model
|
||||
device_info["partitions"] = partitions
|
||||
-- true or false
|
||||
device_info["inuse"] = device_info["inuse"] or get_mount_point(bname)
|
||||
|
||||
local udevinfo = {}
|
||||
if luci.sys.exec("which udevadm") ~= "" then
|
||||
local udevadm = io.popen("udevadm info --query=property --name="..device)
|
||||
for attr in udevadm:lines() do
|
||||
local k, v = attr:match("(%S+)=(%S+)")
|
||||
udevinfo[k] = v
|
||||
end
|
||||
udevadm:close()
|
||||
|
||||
device_info["info"] = udevinfo
|
||||
if udevinfo["ID_MODEL"] then device_info["model"] = udevinfo["ID_MODEL"] end
|
||||
end
|
||||
devices[bname] = device_info
|
||||
end
|
||||
-- luci.util.perror(luci.util.serialize_json(devices))
|
||||
return devices
|
||||
end
|
||||
|
||||
-- get formart cmd
|
||||
d.get_format_cmd = function()
|
||||
local AVAILABLE_FMTS = {
|
||||
ext2 = { cmd = "mkfs.ext2", option = "-F -E lazy_itable_init=1" },
|
||||
ext3 = { cmd = "mkfs.ext3", option = "-F -E lazy_itable_init=1" },
|
||||
ext4 = { cmd = "mkfs.ext4", option = "-F -E lazy_itable_init=1" },
|
||||
fat32 = { cmd = "mkfs.vfat", option = "-F" },
|
||||
exfat = { cmd = "mkexfat", option = "-f" },
|
||||
hfsplus = { cmd = "mkhfs", option = "-f" },
|
||||
ntfs = { cmd = "mkntfs", option = "-f" },
|
||||
swap = { cmd = "mkswap", option = "" },
|
||||
btrfs = { cmd = "mkfs.btrfs", option = "-f" }
|
||||
}
|
||||
result = {}
|
||||
for fmt, obj in pairs(AVAILABLE_FMTS) do
|
||||
local cmd = luci.sys.exec("/usr/bin/which " .. obj["cmd"])
|
||||
if cmd:match(obj["cmd"]) then
|
||||
result[fmt] = { cmd = cmd:match("^.+"..obj["cmd"]) ,option = obj["option"] }
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
d.create_raid = function(rname, rlevel, rmembers)
|
||||
local mb = {}
|
||||
for _, v in ipairs(rmembers) do
|
||||
mb[v]=v
|
||||
end
|
||||
rmembers = {}
|
||||
for _, v in pairs(mb) do
|
||||
table.insert(rmembers, v)
|
||||
end
|
||||
if type(rname) == "string" then
|
||||
if rname:match("^md%d-%s+") then
|
||||
rname = "/dev/"..rname:match("^(md%d-)%s+")
|
||||
elseif rname:match("^/dev/md%d-%s+") then
|
||||
rname = "/dev/"..rname:match("^(/dev/md%d-)%s+")
|
||||
elseif not rname:match("/") then
|
||||
rname = "/dev/md/".. rname
|
||||
else
|
||||
return "ERR: Invalid raid name"
|
||||
end
|
||||
else
|
||||
local mdnum = 0
|
||||
for num=1,127 do
|
||||
local md = io.open("/dev/md"..tostring(num), "r")
|
||||
if md == nil then
|
||||
mdnum = num
|
||||
break
|
||||
else
|
||||
io.close(md)
|
||||
end
|
||||
end
|
||||
if mdnum == 0 then return "ERR: Cannot find proper md number" end
|
||||
rname = "/dev/md"..mdnum
|
||||
end
|
||||
|
||||
if rlevel == "5" or rlevel == "6" then
|
||||
if #rmembers < 3 then return "ERR: Not enough members" end
|
||||
end
|
||||
if rlevel == "10" then
|
||||
if #rmembers < 4 then return "ERR: Not enough members" end
|
||||
end
|
||||
if #rmembers < 2 then return "ERR: Not enough members" end
|
||||
local cmd = d.command.mdadm .. " --create "..rname.." --run --assume-clean --homehost=any --level=" .. rlevel .. " --raid-devices=" .. #rmembers .. " " .. table.concat(rmembers, " ")
|
||||
local res = luci.util.exec(cmd)
|
||||
return res
|
||||
end
|
||||
|
||||
d.gen_mdadm_config = function()
|
||||
if not nixio.fs.access("/etc/config/mdadm") then return end
|
||||
local uci = require "luci.model.uci"
|
||||
local x = uci.cursor()
|
||||
-- delete all array sections
|
||||
x:foreach("mdadm", "array", function(s) x:delete("mdadm",s[".name"]) end)
|
||||
local cmd = d.command.mdadm .. " -D -s"
|
||||
--ARRAY /dev/md1 metadata=1.2 name=any:1 UUID=f998ae14:37621b27:5c49e850:051f6813
|
||||
--ARRAY /dev/md3 metadata=1.2 name=any:3 UUID=c068c141:4b4232ca:f48cbf96:67d42feb
|
||||
for _, v in ipairs(luci.util.execl(cmd)) do
|
||||
local device, uuid = v:match("^ARRAY%s-([^%s]+)%s-[^%s]-%s-[^%s]-%s-UUID=([^%s]+)%s-")
|
||||
if device and uuid then
|
||||
local section_name = x:add("mdadm", "array")
|
||||
x:set("mdadm", section_name, "device", device)
|
||||
x:set("mdadm", section_name, "uuid", uuid)
|
||||
end
|
||||
end
|
||||
x:commit("mdadm")
|
||||
-- enable mdadm
|
||||
luci.util.exec("/etc/init.d/mdadm enable")
|
||||
end
|
||||
|
||||
-- list btrfs filesystem device
|
||||
-- {uuid={uuid, label, members, size, used}...}
|
||||
d.list_btrfs_devices = function()
|
||||
local btrfs_device = {}
|
||||
if not d.command.btrfs then return btrfs_device end
|
||||
local line, _uuid
|
||||
for _, line in ipairs(luci.util.execl(d.command.btrfs .. " filesystem show -d --raw"))
|
||||
do
|
||||
local label, uuid = line:match("^Label:%s+([^%s]+)%s+uuid:%s+([^%s]+)")
|
||||
if label and uuid then
|
||||
_uuid = uuid
|
||||
local _label = label:match("^'([^']+)'")
|
||||
btrfs_device[_uuid] = {label = _label or label, uuid = uuid}
|
||||
-- table.insert(btrfs_device, {label = label, uuid = uuid})
|
||||
end
|
||||
local used = line:match("Total devices[%w%s]+used%s+(%d+)$")
|
||||
if used then
|
||||
btrfs_device[_uuid]["used"] = tonumber(used)
|
||||
btrfs_device[_uuid]["used_formated"] = byte_format(tonumber(used))
|
||||
end
|
||||
local size, device = line:match("devid[%w.%s]+size%s+(%d+)[%w.%s]+path%s+([^%s]+)$")
|
||||
if size and device then
|
||||
btrfs_device[_uuid]["size"] = btrfs_device[_uuid]["size"] and btrfs_device[_uuid]["size"] + tonumber(size) or tonumber(size)
|
||||
btrfs_device[_uuid]["size_formated"] = byte_format(btrfs_device[_uuid]["size"])
|
||||
btrfs_device[_uuid]["members"] = btrfs_device[_uuid]["members"] and btrfs_device[_uuid]["members"]..", "..device or device
|
||||
end
|
||||
end
|
||||
return btrfs_device
|
||||
end
|
||||
|
||||
d.create_btrfs = function(blabel, blevel, bmembers)
|
||||
-- mkfs.btrfs -L label -d blevel /dev/sda /dev/sdb
|
||||
if not d.command.btrfs or type(bmembers) ~= "table" or next(bmembers) == nil then return "ERR no btrfs support or no members" end
|
||||
local label = blabel and " -L " .. blabel or ""
|
||||
local cmd = "mkfs.btrfs -f " .. label .. " -d " .. blevel .. " " .. table.concat(bmembers, " ")
|
||||
return luci.util.exec(cmd)
|
||||
end
|
||||
|
||||
-- get btrfs info
|
||||
-- {uuid, label, members, data_raid_level,metadata_raid_lavel, size, used, size_formated, used_formated, free, free_formated, usage}
|
||||
d.get_btrfs_info = function(m_point)
|
||||
local btrfs_info = {}
|
||||
if not m_point or not d.command.btrfs then return btrfs_info end
|
||||
local cmd = d.command.btrfs .. " filesystem show --raw " .. m_point
|
||||
local _, line, uuid, _label, members
|
||||
for _, line in ipairs(luci.util.execl(cmd)) do
|
||||
if not uuid and not _label then
|
||||
_label, uuid = line:match("^Label:%s+([^%s]+)%s+uuid:%s+([^s]+)")
|
||||
else
|
||||
local mb = line:match("%s+devid.+path%s+([^%s]+)")
|
||||
if mb then
|
||||
members = members and (members .. ", ".. mb) or mb
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not _label or not uuid then return btrfs_info end
|
||||
local label = _label:match("^'([^']+)'")
|
||||
cmd = d.command.btrfs .. " filesystem usage -b " .. m_point
|
||||
local used, free, data_raid_level, metadata_raid_lavel
|
||||
for _, line in ipairs(luci.util.execl(cmd)) do
|
||||
if not used then
|
||||
used = line:match("^%s+Used:%s+(%d+)")
|
||||
elseif not free then
|
||||
free = line:match("^%s+Free %(estimated%):%s+(%d+)")
|
||||
elseif not data_raid_level then
|
||||
data_raid_level = line:match("^Data,%s-(%w+)")
|
||||
elseif not metadata_raid_lavel then
|
||||
metadata_raid_lavel = line:match("^Metadata,%s-(%w+)")
|
||||
end
|
||||
end
|
||||
if used and free and data_raid_level and metadata_raid_lavel then
|
||||
used = tonumber(used)
|
||||
free = tonumber(free)
|
||||
btrfs_info = {
|
||||
uuid = uuid,
|
||||
label = label,
|
||||
data_raid_level = data_raid_level,
|
||||
metadata_raid_lavel = metadata_raid_lavel,
|
||||
used = used,
|
||||
free = free,
|
||||
size = used + free,
|
||||
size_formated = byte_format(used + free),
|
||||
used_formated = byte_format(used),
|
||||
free_formated = byte_format(free),
|
||||
members = members,
|
||||
usage = string.format("%.2f",(used / (free+used) * 100)) .. "%"
|
||||
}
|
||||
end
|
||||
return btrfs_info
|
||||
end
|
||||
|
||||
-- get btrfs subvolume
|
||||
-- {id={id, gen, top_level, path, snapshots, otime, default_subvolume}...}
|
||||
d.get_btrfs_subv = function(m_point, snapshot)
|
||||
local subvolume = {}
|
||||
if not m_point or not d.command.btrfs then return subvolume end
|
||||
|
||||
-- get default subvolume
|
||||
local cmd = d.command.btrfs .. " subvolume get-default " .. m_point
|
||||
local res = luci.util.exec(cmd)
|
||||
local default_subvolume_id = res:match("^ID%s+([^%s]+)")
|
||||
|
||||
-- get the root subvolume
|
||||
if not snapshot then
|
||||
local _, line, section_snap, _uuid, _otime, _id, _snap
|
||||
cmd = d.command.btrfs .. " subvolume show ".. m_point
|
||||
for _, line in ipairs(luci.util.execl(cmd)) do
|
||||
if not section_snap then
|
||||
if not _uuid then
|
||||
_uuid = line:match("^%s-UUID:%s+([^%s]+)")
|
||||
elseif not _otime then
|
||||
_otime = line:match("^%s+Creation time:%s+(.+)")
|
||||
elseif not _id then
|
||||
_id = line:match("^%s+Subvolume ID:%s+([^%s]+)")
|
||||
elseif line:match("^%s+(Snapshot%(s%):)") then
|
||||
section_snap = true
|
||||
end
|
||||
else
|
||||
local snapshot = line:match("^%s+(.+)")
|
||||
if snapshot then
|
||||
_snap = _snap and (_snap ..", /".. snapshot) or ("/"..snapshot)
|
||||
end
|
||||
end
|
||||
end
|
||||
if _uuid and _otime and _id then
|
||||
subvolume["0".._id] = {id = _id , uuid = _uuid, otime = _otime, snapshots = _snap, path = "/"}
|
||||
if default_subvolume_id == _id then
|
||||
subvolume["0".._id].default_subvolume = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- get subvolume of btrfs
|
||||
cmd = d.command.btrfs .. " subvolume list -gcu" .. (snapshot and "s " or " ") .. m_point
|
||||
for _, line in ipairs(luci.util.execl(cmd)) do
|
||||
-- ID 259 gen 11 top level 258 uuid 26ae0c59-199a-cc4d-bd58-644eb4f65d33 path 1a/2b'
|
||||
local id, gen, top_level, uuid, path, otime, otime2
|
||||
if snapshot then
|
||||
id, gen, top_level, otime, otime2, uuid, path = line:match("^ID%s+([^%s]+)%s+gen%s+([^%s]+)%s+cgen.-top level%s+([^%s]+)%s+otime%s+([^%s]+)%s+([^%s]+)%s+uuid%s+([^%s]+)%s+path%s+([^%s]+)%s-$")
|
||||
else
|
||||
id, gen, top_level, uuid, path = line:match("^ID%s+([^%s]+)%s+gen%s+([^%s]+)%s+cgen.-top level%s+([^%s]+)%s+uuid%s+([^%s]+)%s+path%s+([^%s]+)%s-$")
|
||||
end
|
||||
if id and gen and top_level and uuid and path then
|
||||
subvolume[id] = {id = id, gen = gen, top_level = top_level, otime = (otime and otime or "") .." ".. (otime2 and otime2 or ""), uuid = uuid, path = '/'.. path}
|
||||
if not snapshot then
|
||||
-- use btrfs subv show to get snapshots
|
||||
local show_cmd = d.command.btrfs .. " subvolume show "..m_point.."/"..path
|
||||
local __, line_show, section_snap
|
||||
for __, line_show in ipairs(luci.util.execl(show_cmd)) do
|
||||
if not section_snap then
|
||||
local create_time = line_show:match("^%s+Creation time:%s+(.+)")
|
||||
if create_time then
|
||||
subvolume[id]["otime"] = create_time
|
||||
elseif line_show:match("^%s+(Snapshot%(s%):)") then
|
||||
section_snap = "true"
|
||||
end
|
||||
else
|
||||
local snapshot = line_show:match("^%s+(.+)")
|
||||
subvolume[id]["snapshots"] = subvolume[id]["snapshots"] and (subvolume[id]["snapshots"] .. ", /".. snapshot) or ("/"..snapshot)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if subvolume[default_subvolume_id] then
|
||||
subvolume[default_subvolume_id].default_subvolume = 1
|
||||
end
|
||||
-- if m_point == "/tmp/.btrfs_tmp" then
|
||||
-- luci.util.exec("umount " .. m_point)
|
||||
-- end
|
||||
return subvolume
|
||||
end
|
||||
|
||||
d.format_partition = function(partition, fs)
|
||||
local partition_name = "/dev/".. partition
|
||||
if not nixio.fs.access(partition_name) then
|
||||
return 500, "Partition NOT found!"
|
||||
end
|
||||
|
||||
local format_cmd = d.get_format_cmd()
|
||||
if not format_cmd[fs] then
|
||||
return 500, "Filesystem NOT support!"
|
||||
end
|
||||
local cmd = format_cmd[fs].cmd .. " " .. format_cmd[fs].option .. " " .. partition_name
|
||||
local res = luci.util.exec(cmd .. " 2>&1")
|
||||
if res and res:lower():match("error+") then
|
||||
return 500, res
|
||||
else
|
||||
return 200, "OK"
|
||||
end
|
||||
end
|
||||
|
||||
return d
|
|
@ -0,0 +1,7 @@
|
|||
<%+cbi/valueheader%>
|
||||
<% if self:cfgvalue(section) ~= false then %>
|
||||
<input class="cbi-button cbi-button-<%=self.inputstyle or "button" %>" type="submit"<%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> <% if self.view_disabled then %> disabled <% end %>/>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
<%+cbi/valuefooter%>
|
|
@ -0,0 +1,7 @@
|
|||
<%+cbi/valueheader%>
|
||||
<% if self:cfgvalue(section) ~= false then %>
|
||||
<input class="cbi-button cbi-button-<%=self.inputstyle or "button" %>" onclick="event.preventDefault();partition_format('<%=self.partitions[section].name%>', '<%=self.format_cmd%>', '<%=self.inputtitle%>');" type="submit"<%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> <% if self.view_disabled then %> disabled <% end %>/>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
<%+cbi/valuefooter%>
|
|
@ -0,0 +1,7 @@
|
|||
<div style="display: inline-block;">
|
||||
<% if self:cfgvalue(section) ~= false then %>
|
||||
<input class="cbi-button cbi-button-<%=self.inputstyle or "button" %>" type="submit"" <% if self.disable then %>disabled <% end %><%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> />
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</div>
|
37
luci-app-diskman/luasrc/view/diskman/cbi/xnullsection.htm
Normal file
|
@ -0,0 +1,37 @@
|
|||
<div class="cbi-section" id="cbi-<%=self.config%>-section">
|
||||
<% if self.title and #self.title > 0 then -%>
|
||||
<legend><%=self.title%></legend>
|
||||
<%- end %>
|
||||
<% if self.description and #self.description > 0 then -%>
|
||||
<div class="cbi-section-descr"><%=self.description%></div>
|
||||
<%- end %>
|
||||
<div class="cbi-section-node">
|
||||
<div id="cbi-<%=self.config%>-<%=tostring(self):sub(8)%>">
|
||||
<% self:render_children(1, scope or {}) %>
|
||||
</div>
|
||||
<% if self.error and self.error[1] then -%>
|
||||
<div class="cbi-section-error">
|
||||
<ul><% for _, e in ipairs(self.error[1]) do -%>
|
||||
<li>
|
||||
<%- if e == "invalid" then -%>
|
||||
<%:One or more fields contain invalid values!%>
|
||||
<%- elseif e == "missing" then -%>
|
||||
<%:One or more required fields have no value!%>
|
||||
<%- else -%>
|
||||
<%=pcdata(e)%>
|
||||
<%- end -%>
|
||||
</li>
|
||||
<%- end %></ul>
|
||||
</div>
|
||||
<%- end %>
|
||||
</div>
|
||||
</div>
|
||||
<%-
|
||||
if type(self.hidden) == "table" then
|
||||
for k, v in pairs(self.hidden) do
|
||||
-%>
|
||||
<input type="hidden" id="<%=k%>" name="<%=k%>" value="<%=pcdata(v)%>" />
|
||||
<%-
|
||||
end
|
||||
end
|
||||
%>
|
88
luci-app-diskman/luasrc/view/diskman/cbi/xsimpleform.htm
Normal file
|
@ -0,0 +1,88 @@
|
|||
<% if not self.embedded then %>
|
||||
<form method="post" enctype="multipart/form-data" action="<%=REQUEST_URI%>"<%=
|
||||
attr("data-strings", luci.util.serialize_json({
|
||||
label = {
|
||||
choose = translate('-- Please choose --'),
|
||||
custom = translate('-- custom --'),
|
||||
},
|
||||
path = {
|
||||
resource = resource,
|
||||
browser = url("admin/filebrowser")
|
||||
}
|
||||
}))
|
||||
%>>
|
||||
<script type="text/javascript" src="<%=resource%>/cbi.js"></script>
|
||||
<input type="hidden" name="token" value="<%=token%>" />
|
||||
<input type="hidden" name="cbi.submit" value="1" /><%
|
||||
end
|
||||
|
||||
%><div class="cbi-map" id="cbi-<%=self.config%>"><%
|
||||
|
||||
if self.title and #self.title > 0 then
|
||||
%><h2 name="content"><%=self.title%></h2><%
|
||||
end
|
||||
|
||||
if self.description and #self.description > 0 then
|
||||
%><div class="cbi-map-descr"><%=self.description%></div><%
|
||||
end
|
||||
|
||||
self:render_children()
|
||||
|
||||
%></div><%
|
||||
|
||||
if self.message then
|
||||
%><div class="alert-message notice"><%=self.message%></div><%
|
||||
end
|
||||
|
||||
if self.errmessage then
|
||||
%><div class="alert-message warning"><%=self.errmessage%></div><%
|
||||
end
|
||||
|
||||
if not self.embedded then
|
||||
if type(self.hidden) == "table" then
|
||||
local k, v
|
||||
for k, v in pairs(self.hidden) do
|
||||
%><input type="hidden" id="<%=k%>" name="<%=k%>" value="<%=pcdata(v)%>" /><%
|
||||
end
|
||||
end
|
||||
|
||||
local display_back = (self.redirect)
|
||||
local display_cancel = (self.cancel ~= false and self.on_cancel)
|
||||
local display_skip = (self.flow and self.flow.skip)
|
||||
local display_submit = (self.submit ~= false)
|
||||
local display_reset = (self.reset ~= false)
|
||||
|
||||
if display_back or display_cancel or display_skip or display_submit or display_reset then
|
||||
%><div class="cbi-page-actions"><%
|
||||
|
||||
if display_back then
|
||||
%><input class="cbi-button cbi-button-link" type="button" value="<%:Back to Overview%>" onclick="location.href='<%=pcdata(self.redirect)%>'" /> <%
|
||||
end
|
||||
|
||||
if display_cancel then
|
||||
local label = pcdata(self.cancel or translate("Cancel"))
|
||||
%><input class="cbi-button cbi-button-link" type="button" value="<%=label%>" onclick="cbi_submit(this, 'cbi.cancel')" /> <%
|
||||
end
|
||||
|
||||
if display_skip then
|
||||
%><input class="cbi-button cbi-button-neutral" type="button" value="<%:Skip%>" onclick="cbi_submit(this, 'cbi.skip')" /> <%
|
||||
end
|
||||
|
||||
if display_submit then
|
||||
local label = pcdata(self.submit or translate("Submit"))
|
||||
%><input class="cbi-button cbi-button-save" type="submit" value="<%=label%>" /> <%
|
||||
end
|
||||
|
||||
if display_reset then
|
||||
local label = pcdata(self.reset or translate("Reset"))
|
||||
%><input class="cbi-button cbi-button-reset" type="reset" value="<%=label%>" /> <%
|
||||
end
|
||||
|
||||
%></div><%
|
||||
end
|
||||
|
||||
%></form><%
|
||||
end
|
||||
%>
|
||||
|
||||
<script type="text/javascript">cbi_init();</script>
|
108
luci-app-diskman/luasrc/view/diskman/disk_info.htm
Normal file
|
@ -0,0 +1,108 @@
|
|||
<script type="text/javascript">
|
||||
window.onload = function () {
|
||||
//disk partition info
|
||||
let p_colors = ["#c0c0ff", "#fbbd00", "#e97c30", "#a0e0a0", "#e0c0ff"]
|
||||
let lines = document.querySelectorAll('[id^=cbi-disk-]')
|
||||
lines.forEach((item) => {
|
||||
let dev = item.id.match(/cbi-disk-(.*)/)[1]
|
||||
if (dev == "table") { return }
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin/system/diskman/get_disk_info")%>/' + dev, null, (x, disk_info) => {
|
||||
// handle disk info
|
||||
item.childNodes.forEach((cell) => {
|
||||
if (cell && cell.attributes) {
|
||||
if (cell.getAttribute("data-name") == "sn" || cell.childNodes[1] && cell.childNodes[1].id.match(/sn/)) {
|
||||
cell.innerText = disk_info.sn || "-"
|
||||
} else if (cell.getAttribute("data-name") == "temp" || cell.childNodes[1] && cell.childNodes[1].id.match(/temp/)) {
|
||||
cell.innerText = disk_info.temp || "-"
|
||||
} else if (cell.getAttribute("data-name") == "p_table" || cell.childNodes[1] && cell.childNodes[1].id.match(/p_table/)) {
|
||||
cell.innerText = disk_info.p_table || "-"
|
||||
} else if (cell.getAttribute("data-name") == "sata_ver" || cell.childNodes[1] && cell.childNodes[1].id.match(/sata_ver/)) {
|
||||
cell.innerText = disk_info.sata_ver || "-"
|
||||
} else if (cell.getAttribute("data-name") == "health" || cell.childNodes[1] && cell.childNodes[1].id.match(/health/)) {
|
||||
cell.innerText = disk_info.health || "-"
|
||||
} else if (cell.getAttribute("data-name") == "status" || cell.childNodes[1] && cell.childNodes[1].id.match(/status/)) {
|
||||
cell.innerText = disk_info.status || "-"
|
||||
}
|
||||
}
|
||||
})
|
||||
// handle partitons info
|
||||
if (disk_info.partitions && disk_info.partitions.length > 0) {
|
||||
let partitons_div
|
||||
if (item.nodeName == "TR") {
|
||||
partitons_div = '<tr width="100%" style="white-space:nowrap;"><td style="margin:0px; padding:0px; border:0px; white-space:nowrap;" colspan="15">'
|
||||
} else if (item.nodeName == "DIV") {
|
||||
partitons_div = '<div class="tr cbi-section-table-row cbi-rowstyle-1"><div style="white-space:nowrap; position:absolute; width:100%">'
|
||||
}
|
||||
let expand = 0
|
||||
let need_expand = 0
|
||||
disk_info.partitions.forEach((part) => {
|
||||
let p = part.size / disk_info.size * 100
|
||||
if (p <= 8) {
|
||||
expand += 8
|
||||
need_expand += p
|
||||
part.part_percent = 8
|
||||
}
|
||||
})
|
||||
let n = 0
|
||||
disk_info.partitions.forEach((part) => {
|
||||
let p = part.size / disk_info.size * 100
|
||||
if (p > 8) {
|
||||
part.part_percent = p * (100 - expand) / (100 - need_expand)
|
||||
}
|
||||
let part_percent = part.part_percent + '%'
|
||||
let p_color = p_colors[n++]
|
||||
if (n > 4) { n = 0 }
|
||||
let inline_txt = (part.name != '-' && part.name || '') + ' ' + (part.fs != 'Free Space' && part.fs || '') + ' ' + part.size_formated + ' ' + (part.useage != '-' && part.useage || '')
|
||||
let partiton_div = '<div title="' + inline_txt + '" style="color: #525F7F; display:inline-block; text-align:center;background-color:' + p_color + '; width:' + part_percent + '">' + inline_txt + '</div>'
|
||||
partitons_div += partiton_div
|
||||
})
|
||||
if (item.nodeName == "TR") {
|
||||
partitons_div += '</td></tr>'
|
||||
} else if (item.nodeName == "DIV") {
|
||||
partitons_div += '</div><div> </div></div>'
|
||||
}
|
||||
item.insertAdjacentHTML('afterend', partitons_div);
|
||||
}
|
||||
})
|
||||
})
|
||||
//raid table
|
||||
lines = document.querySelectorAll('[id^=cbi-_raid-]')
|
||||
lines.forEach((item) => {
|
||||
let dev = item.id.match(/cbi-_raid-(.*)/)[1]
|
||||
if (dev == "table") { return }
|
||||
console.log(dev)
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin/system/diskman/get_disk_info")%>/' + dev, null, (x, disk_info) => {
|
||||
// handle raid info
|
||||
item.childNodes.forEach((cell) => {
|
||||
if (cell && cell.attributes) {
|
||||
if (cell.getAttribute("data-name") == "p_table" || cell.childNodes[1] && cell.childNodes[1].id.match(/p_table/)) {
|
||||
cell.innerText = disk_info.p_table || "-"
|
||||
}
|
||||
}
|
||||
})
|
||||
// handle partitons info
|
||||
let partitons_div
|
||||
if (item.nodeName == "TR") {
|
||||
partitons_div = '<tr width="100%" style="white-space:nowrap;"><td style="margin:0px; padding:0px; border:0px; white-space:nowrap;" colspan="15">'
|
||||
} else if (item.nodeName == "DIV") {
|
||||
partitons_div = '<div class="tr cbi-section-table-row cbi-rowstyle-1"><div style="white-space:nowrap; position:absolute; width:100%">'
|
||||
}
|
||||
let n = 0
|
||||
disk_info.partitions.forEach((part) => {
|
||||
let part_percent = part.size / disk_info.size * 100 + '%'
|
||||
let p_color = p_colors[n++]
|
||||
if (n > 4) { n = 0 }
|
||||
let inline_txt = (part.name != '-' && part.name || '') + ' ' + (part.fs != 'Free Space' && part.fs || '') + ' ' + part.size_formated + ' ' + (part.useage != '-' && part.useage || '')
|
||||
let partiton_div = '<div title="' + inline_txt + '" style="display:inline-block; text-align:center;background-color:' + p_color + '; width:' + part_percent + '">' + inline_txt + '</div>'
|
||||
partitons_div += partiton_div
|
||||
})
|
||||
if (item.nodeName == "TR") {
|
||||
partitons_div += '</td></tr>'
|
||||
} else if (item.nodeName == "DIV") {
|
||||
partitons_div += '</div><div> </div></div>'
|
||||
}
|
||||
item.insertAdjacentHTML('afterend', partitons_div);
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
129
luci-app-diskman/luasrc/view/diskman/partition_info.htm
Normal file
|
@ -0,0 +1,129 @@
|
|||
<style type="text/css">
|
||||
#dialog_format {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: none;
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
#dialog_format .dialog_box {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255);
|
||||
top: 35%;
|
||||
width: 40%;
|
||||
min-width: 20em;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height:auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#dialog_format .dialog_line {
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
margin-left: 2em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
#dialog_format .dialog_box>h4,
|
||||
#dialog_format .dialog_box>p,
|
||||
#dialog_format .dialog_box>div {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
#dialog_format .dialog_box>img {
|
||||
margin-right: 1em;
|
||||
flex-basis: 32px;
|
||||
}
|
||||
|
||||
body.dialog-format-active {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body.dialog-format-active #dialog_format {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
function show_detail(dev, e) {
|
||||
e.preventDefault()
|
||||
window.open('<%=luci.dispatcher.build_url("admin/system/diskman/smartdetail")%>/' + dev,
|
||||
'newwindow', 'height=480,width=800,top=100,left=200,toolbar=no,menubar=no,scrollbars=yes, resizable=no,location=no, status=no')
|
||||
}
|
||||
window.onload = function () {
|
||||
// handle partition table
|
||||
const btn_p_table = document.getElementById("widget.cbid.table.1.p_table") || document.getElementById("cbid.table.1.p_table")
|
||||
const btn_p_table_raw_index = btn_p_table.selectedIndex
|
||||
const val_name = document.getElementById("cbi-table-1-path").innerText.split('/').pop()
|
||||
btn_p_table.onchange = function () {
|
||||
let btn_p_table_index = btn_p_table.selectedIndex
|
||||
if (btn_p_table_index != btn_p_table_raw_index) {
|
||||
if (confirm("<%:Warnning !! \nTHIS WILL OVERWRITE EXISTING PARTITIONS!! \nModify the partition table?%>")) {
|
||||
let p_table = btn_p_table.options[btn_p_table_index].value
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin/system/diskman/mk_p_table")%>', { dev: val_name, p_table: p_table }, (x, res) => {
|
||||
if (res.code == 0) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
}
|
||||
}
|
||||
}
|
||||
// handle smartinfo
|
||||
const url = location.href.split('/')
|
||||
const dev = url[url.length - 1]
|
||||
const btn_smart_detail = document.getElementById("cbi-table-1-health")
|
||||
btn_smart_detail.children[0].onclick = show_detail.bind(this, dev)
|
||||
}
|
||||
function close_dialog() {
|
||||
document.body.classList.remove('dialog-format-active')
|
||||
document.documentElement.style.overflowY = 'scroll'
|
||||
}
|
||||
function do_format(partation_name){
|
||||
let fs = document.getElementById("filesystem_list").value
|
||||
let status = document.getElementById("format-status")
|
||||
if(!fs) {
|
||||
status.innerHTML = "<%:Please select file system!%>"
|
||||
return
|
||||
}
|
||||
status.innerHTML = "<%:Formatting..%>"
|
||||
let b = document.getElementById('btn_format')
|
||||
b.disabled = true
|
||||
let xhr = new XHR()
|
||||
xhr.post('<%=luci.dispatcher.build_url("admin/system/diskman/format_partition")%>', { partation_name: partation_name, file_system: fs }, (x, res) => {
|
||||
if (x.status == 200) {
|
||||
status.innerHTML = x.statusText
|
||||
location.reload();
|
||||
}else{
|
||||
status.innerHTML = x.statusText
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function clear_text(){
|
||||
let s = document.getElementById('format-status')
|
||||
s.innerHTML = ""
|
||||
let b = document.getElementById('btn_format')
|
||||
b.disabled = false
|
||||
}
|
||||
|
||||
function partition_format(partition_name, format_cmd, current_fs){
|
||||
let list = ''
|
||||
format_cmd.split(",").forEach(e => {
|
||||
list = list + '<option value="'+e+'">'+e+'</option>'
|
||||
});
|
||||
document.getElementById('dialog_format') || document.body.insertAdjacentHTML("beforeend", '<div id="dialog_format"><div class="dialog_box"><div class="dialog_line"></div><div class="dialog_line"><span><%:Format partation:%> <b>'+partition_name+'</b></span><br><span id="format-status" style="color: red;"></span></div><div class="dialog_line"><select id="filesystem_list" class="cbi-input-select" onchange="clear_text()">'+list+'</select></div><div class="dialog_line" style="text-align: right;"><input type="button" class="cbi-button cbi-button-apply" id="btn_format" type="submit" value="<%:Format%>" onclick="do_format(`'+partition_name+'`)" /> <input type="button"class="cbi-button cbi-button-reset" type="reset" value="<%:Cancel%>" onclick="close_dialog()" /></div><div class="dialog_line"></div></div></div>>')
|
||||
document.body.classList.add('dialog-format-active')
|
||||
document.documentElement.style.overflowY = 'hidden'
|
||||
let fs_list = document.getElementById("filesystem_list")
|
||||
fs_list.value = current_fs
|
||||
}
|
||||
</script>
|
79
luci-app-diskman/luasrc/view/diskman/smart_detail.htm
Normal file
|
@ -0,0 +1,79 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>S.M.A.R.T detail of <%=dev%></title>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="<%=media%>/cascade.css" />
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
let formData = new FormData()
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open("GET", '<%=luci.dispatcher.build_url("admin", "system", "diskman", "smartattr", dev)%>', true)
|
||||
xhr.onload = function () {
|
||||
let st = JSON.parse(xhr.responseText)
|
||||
let tb = document.getElementById('smart_attr_table');
|
||||
if (st && tb) {
|
||||
/* clear all rows */
|
||||
while (tb.rows.length > 1)
|
||||
tb.deleteRow(1);
|
||||
|
||||
for (var i = 0; i < st.length; i++) {
|
||||
var tr = tb.insertRow(-1);
|
||||
tr.className = 'cbi-section-table-row cbi-rowstyle-' + ((i % 2) + 1);
|
||||
var td = null
|
||||
<% if dev: match("nvme") then %>
|
||||
tr.insertCell(-1).innerHTML = st[i].key;
|
||||
tr.insertCell(-1).innerHTML = st[i].value;
|
||||
<% else %>
|
||||
tr.insertCell(-1).innerHTML = st[i].id;
|
||||
tr.insertCell(-1).innerHTML = st[i].attrbute;
|
||||
tr.insertCell(-1).innerHTML = st[i].flag;
|
||||
tr.insertCell(-1).innerHTML = st[i].value;
|
||||
tr.insertCell(-1).innerHTML = st[i].worst;
|
||||
tr.insertCell(-1).innerHTML = st[i].thresh;
|
||||
tr.insertCell(-1).innerHTML = st[i].type;
|
||||
tr.insertCell(-1).innerHTML = st[i].updated;
|
||||
tr.insertCell(-1).innerHTML = st[i].raw;
|
||||
if ((st[i].id == '05' || st[i].id == 'C5') && st[i].raw != '0') {
|
||||
tr.style.cssText = "background-color:red !important;";
|
||||
}
|
||||
<% end %>
|
||||
}
|
||||
if (tb.rows.length == 1) {
|
||||
var tr = tb.insertRow(-1);
|
||||
tr.className = 'cbi-section-table-row';
|
||||
var td = tr.insertCell(-1);
|
||||
td.colSpan = 4;
|
||||
td.innerHTML = '<em><br /><%:No Attrbute to display.%></em>';
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.send(formData)
|
||||
//]]></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="maincontainer">
|
||||
<fieldset class="cbi-section">
|
||||
<legend><%:S.M.A.R.T Attrbutes%>: /dev/<%=dev%></legend>
|
||||
<table class="cbi-section-table" id="smart_attr_table">
|
||||
<tr class="cbi-section-table-titles">
|
||||
<% if dev:match("nvme") then %>
|
||||
<!-- <th class="cbi-section-table-cell"><%:KEY%></th>
|
||||
<th class="cbi-section-table-cell"><%:VALUE%></th> -->
|
||||
<% else %>
|
||||
<th class="cbi-section-table-cell"><%:ID%></th>
|
||||
<th class="cbi-section-table-cell"><%:Attrbute%></th>
|
||||
<th class="cbi-section-table-cell"><%:Flag%></th>
|
||||
<th class="cbi-section-table-cell"><%:Value%></th>
|
||||
<th class="cbi-section-table-cell"><%:Worst%></th>
|
||||
<th class="cbi-section-table-cell"><%:Thresh%></th>
|
||||
<th class="cbi-section-table-cell"><%:Type%></th>
|
||||
<th class="cbi-section-table-cell"><%:Updated%></th>
|
||||
<th class="cbi-section-table-cell"><%:Raw%></th>
|
||||
<% end %>
|
||||
</tr>
|
||||
<tr class="cbi-section-table-row">
|
||||
<td colspan="4"><em><br /><%:Collecting data...%></em></td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
239
luci-app-diskman/po/zh-cn/diskman.po
Normal file
|
@ -0,0 +1,239 @@
|
|||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
msgid "DiskMan"
|
||||
msgstr "DiskMan 磁盘管理"
|
||||
|
||||
msgid "Manage Disks over LuCI."
|
||||
msgstr "通过 LuCI 管理磁盘"
|
||||
|
||||
msgid "Rescan Disks"
|
||||
msgstr "重新扫描磁盘"
|
||||
|
||||
msgid "Disks"
|
||||
msgstr "磁盘"
|
||||
|
||||
msgid "Path"
|
||||
msgstr "路径"
|
||||
|
||||
msgid "Serial Number"
|
||||
msgstr "序列号"
|
||||
|
||||
msgid "Temp"
|
||||
msgstr "温度"
|
||||
|
||||
msgid "Partition Table"
|
||||
msgstr "分区表"
|
||||
|
||||
msgid "SATA Version"
|
||||
msgstr "SATA 版本"
|
||||
|
||||
msgid "Health"
|
||||
msgstr "健康"
|
||||
|
||||
msgid "File System"
|
||||
msgstr "文件系统"
|
||||
|
||||
msgid "Mount Options"
|
||||
msgstr "挂载选项"
|
||||
|
||||
msgid "Mount"
|
||||
msgstr "挂载"
|
||||
|
||||
msgid "Umount"
|
||||
msgstr "卸载"
|
||||
|
||||
msgid "Eject"
|
||||
msgstr "弹出"
|
||||
|
||||
msgid "New"
|
||||
msgstr "创建"
|
||||
|
||||
msgid "Remove"
|
||||
msgstr "移除"
|
||||
|
||||
msgid "Format"
|
||||
msgstr "格式化"
|
||||
|
||||
msgid "Start Sector"
|
||||
msgstr "起始扇区"
|
||||
|
||||
msgid "End Sector"
|
||||
msgstr "中止扇区"
|
||||
|
||||
msgid "Usage"
|
||||
msgstr "用量"
|
||||
|
||||
msgid "Used"
|
||||
msgstr "已使用"
|
||||
|
||||
msgid "Free Space"
|
||||
msgstr "空闲空间"
|
||||
|
||||
msgid "Model"
|
||||
msgstr "型号"
|
||||
|
||||
msgid "Size"
|
||||
msgstr "容量"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
msgid "Mount Point"
|
||||
msgstr "挂载点"
|
||||
|
||||
msgid "Sector Size"
|
||||
msgstr "扇区/物理扇区大小"
|
||||
|
||||
msgid "Rotation Rate"
|
||||
msgstr "转速"
|
||||
|
||||
msgid "RAID Devices"
|
||||
msgstr "RAID 设备"
|
||||
|
||||
msgid "RAID mode"
|
||||
msgstr "RAID 模式"
|
||||
|
||||
msgid "Members"
|
||||
msgstr "成员"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "活动"
|
||||
|
||||
msgid "RAID Creation"
|
||||
msgstr "RAID 创建"
|
||||
|
||||
msgid "Raid Name"
|
||||
msgstr "RAID 名称"
|
||||
|
||||
msgid "Raid Level"
|
||||
msgstr "RAID 级别"
|
||||
|
||||
msgid "Raid Member"
|
||||
msgstr "磁盘阵列成员"
|
||||
|
||||
msgid "Create Raid"
|
||||
msgstr "创建 RAID"
|
||||
|
||||
msgid "Partition Management"
|
||||
msgstr "分区管理"
|
||||
|
||||
msgid "Partition Disk over LuCI."
|
||||
msgstr "通过LuCI分区磁盘。"
|
||||
|
||||
msgid "Device Info"
|
||||
msgstr "设备信息"
|
||||
|
||||
msgid "Disk Man"
|
||||
msgstr "磁盘管理"
|
||||
|
||||
msgid "Partitions Info"
|
||||
msgstr "分区信息"
|
||||
|
||||
msgid "Default 2048 sector alignment, support +size{b,k,m,g,t} in End Sector"
|
||||
msgstr "默认2048扇区对齐,【中止扇区】支持 +容量{b,k,m,g,t} 格式,例:+500m +10g +1t"
|
||||
|
||||
msgid "Multiple Devices Btrfs Creation"
|
||||
msgstr "Btrfs 阵列创建"
|
||||
|
||||
msgid "Label"
|
||||
msgstr "卷标"
|
||||
|
||||
msgid "Btrfs Label"
|
||||
msgstr "Btrfs 卷标"
|
||||
|
||||
msgid "Btrfs Raid Level"
|
||||
msgstr "Btrfs Raid 级别"
|
||||
|
||||
msgid "Btrfs Member"
|
||||
msgstr "Btrfs 整列成员"
|
||||
|
||||
msgid "Create Btrfs"
|
||||
msgstr "创建 Btrfs"
|
||||
|
||||
msgid "New Snapshot"
|
||||
msgstr "新建快照"
|
||||
|
||||
msgid "SubVolumes"
|
||||
msgstr "子卷"
|
||||
|
||||
msgid "Top Level"
|
||||
msgstr "父ID"
|
||||
|
||||
msgid "Manage Btrfs"
|
||||
msgstr "Btrfs 管理"
|
||||
|
||||
msgid "Otime"
|
||||
msgstr "创建时间"
|
||||
|
||||
msgid "Snapshots"
|
||||
msgstr "快照"
|
||||
|
||||
msgid "Set Default"
|
||||
msgstr "默认子卷"
|
||||
|
||||
msgid "Source Path"
|
||||
msgstr "源目录"
|
||||
|
||||
msgid "Readonly"
|
||||
msgstr "只读"
|
||||
|
||||
msgid "Delete"
|
||||
msgstr "删除"
|
||||
|
||||
msgid "Create"
|
||||
msgstr "创建"
|
||||
|
||||
msgid "Destination Path (optional)"
|
||||
msgstr "目标目录(可选)"
|
||||
|
||||
msgid "Metadata"
|
||||
msgstr "元数据"
|
||||
|
||||
msgid "Data"
|
||||
msgstr "数据"
|
||||
|
||||
msgid "Btrfs Info"
|
||||
msgstr "Btrfs 信息"
|
||||
|
||||
msgid "The source path for create the snapshot"
|
||||
msgstr "创建快照的源数据目录"
|
||||
|
||||
msgid "The path where you want to store the snapshot"
|
||||
msgstr "存放快照数据目录"
|
||||
|
||||
msgid "Please input Source Path of snapshot, Source Path must start with '/'"
|
||||
msgstr "请输入快照源路径,源路径必须以'/'开头"
|
||||
|
||||
msgid "Please input Subvolume Path, Subvolume must start with '/'"
|
||||
msgstr "请输入子卷路径,子卷路径必须以'/'开头"
|
||||
|
||||
msgid "is in use! please unmount it first!"
|
||||
msgstr "正在被使用!请先卸载!"
|
||||
|
||||
msgid "Partition NOT found!"
|
||||
msgstr "分区未找到!"
|
||||
|
||||
msgid "Filesystem NOT support!"
|
||||
msgstr "文件系统不支持!"
|
||||
|
||||
msgid "Invalid Start Sector!"
|
||||
msgstr "无效的起始扇区!"
|
||||
|
||||
msgid "Invalid End Sector"
|
||||
msgstr "无效的终止扇区!"
|
||||
|
||||
msgid "Partition not exists!"
|
||||
msgstr "分区不存在!"
|
||||
|
||||
msgid "Creation"
|
||||
msgstr "创建"
|
||||
|
||||
msgid "Please select file system!"
|
||||
msgstr "请选择文件系统!"
|
||||
|
||||
msgid "Format partation:"
|
||||
msgstr "格式化分区:"
|
||||
|
||||
msgid "Warnning !! \nTHIS WILL OVERWRITE EXISTING PARTITIONS!! \nModify the partition table?"
|
||||
msgstr "警告!!\n此操作会覆盖现有分区\n确定修改分区表?"
|
1
luci-app-diskman/po/zh_Hans
Normal file
|
@ -0,0 +1 @@
|
|||
zh-cn
|
21
luci-app-dockerman/Makefile
Normal file
|
@ -0,0 +1,21 @@
|
|||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI Support for docker
|
||||
LUCI_DEPENDS:=@(aarch64||arm||x86_64) \
|
||||
+luci-compat \
|
||||
+luci-lib-docker \
|
||||
+luci-lib-ip \
|
||||
+docker \
|
||||
+dockerd \
|
||||
+ttyd
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0
|
||||
PKG_MAINTAINER:=lisaac <lisaac.cn@gmail.com> \
|
||||
Florian Eckert <fe@dev.tdt.de>
|
||||
|
||||
PKG_VERSION:=v0.5.25
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
1
luci-app-dockerman/depends.lst
Normal file
|
@ -0,0 +1 @@
|
|||
ttyd docker-cli
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<title>Docker icon</title>
|
||||
<path d="M4.82 17.275c-.684 0-1.304-.56-1.304-1.24s.56-1.243 1.305-1.243c.748 0 1.31.56 1.31 1.242s-.622 1.24-1.305 1.24zm16.012-6.763c-.135-.992-.75-1.8-1.56-2.42l-.315-.25-.254.31c-.494.56-.69 1.553-.63 2.295.06.562.24 1.12.554 1.554-.254.13-.568.25-.81.377-.57.187-1.124.25-1.68.25H.097l-.06.37c-.12 1.182.06 2.42.562 3.54l.244.435v.06c1.5 2.483 4.17 3.6 7.078 3.6 5.594 0 10.182-2.42 12.357-7.633 1.425.062 2.864-.31 3.54-1.676l.18-.31-.3-.187c-.81-.494-1.92-.56-2.85-.31l-.018.002zm-8.008-.992h-2.428v2.42h2.43V9.518l-.002.003zm0-3.043h-2.428v2.42h2.43V6.48l-.002-.003zm0-3.104h-2.428v2.42h2.43v-2.42h-.002zm2.97 6.147H13.38v2.42h2.42V9.518l-.007.003zm-8.998 0H4.383v2.42h2.422V9.518l-.01.003zm3.03 0h-2.4v2.42H9.84V9.518l-.015.003zm-6.03 0H1.4v2.42h2.428V9.518l-.03.003zm6.03-3.043h-2.4v2.42H9.84V6.48l-.015-.003zm-3.045 0H4.387v2.42H6.8V6.48l-.016-.003z" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,91 @@
|
|||
.fb-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.fb-container .cbi-button {
|
||||
height: 1.8rem;
|
||||
}
|
||||
.fb-container .cbi-input-text {
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
.fb-container .panel-title {
|
||||
padding-bottom: 0;
|
||||
width: 50%;
|
||||
border-bottom: none;
|
||||
}
|
||||
.fb-container .panel-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.fb-container .upload-container {
|
||||
display: none;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.fb-container .upload-file {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
.fb-container .cbi-value-field {
|
||||
text-align: left;
|
||||
}
|
||||
.fb-container .parent-icon strong {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.fb-container td[class$="-icon"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.fb-container .file-icon, .fb-container .folder-icon, .fb-container .link-icon {
|
||||
position: relative;
|
||||
}
|
||||
.fb-container .file-icon:before, .fb-container .folder-icon:before, .fb-container .link-icon:before {
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
content: '';
|
||||
background-size: contain;
|
||||
margin: 0 0.5rem 0 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.fb-container .file-icon:before {
|
||||
background-image: url(file-icon.png);
|
||||
}
|
||||
.fb-container .folder-icon:before {
|
||||
background-image: url(folder-icon.png);
|
||||
}
|
||||
.fb-container .link-icon:before {
|
||||
background-image: url(link-icon.png);
|
||||
}
|
||||
@media screen and (max-width: 480px) {
|
||||
.fb-container .upload-file {
|
||||
width: 14.6rem;
|
||||
}
|
||||
.fb-container .cbi-value-owner,
|
||||
.fb-container .cbi-value-perm {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cbi-section-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cbi-section-table-cell {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cbi-button-install {
|
||||
border-color: #c44;
|
||||
color: #c44;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.cbi-value-field {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.parent-icon {
|
||||
height: 1.8rem;
|
||||
padding: 10px 0;
|
||||
}
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="icon-hub" viewBox="0 -4 42 50" stroke-width="2" fill-rule="nonzero" width="100%" height="100%">
|
||||
<path d="M37.176371,36.2324812 C37.1920117,36.8041095 36.7372743,37.270685 36.1684891,37.270685 L3.74335204,37.2703476 C3.17827583,37.2703476 2.72400056,36.8091818 2.72400056,36.2397767 L2.72400056,19.6131383 C1.4312007,18.4881431 0.662551336,16.8884326 0.662551336,15.1618249 L0.664207893,14.69503 C0.63774183,14.4532127 0.650524255,14.2942438 0.711604827,14.1238231 L5.10793246,1.20935468 C5.24853286,0.797020623 5.63848594,0.511627907 6.06681069,0.511627907 L34.0728364,0.511627907 C34.5091607,0.511627907 34.889927,0.793578201 35.0316653,1.20921034 L39.4428567,14.1234095 C39.4871296,14.273204 39.5020782,14.4249444 39.4884726,14.5493649 L39.4884726,15.1505835 C39.4884726,16.9959517 38.6190601,18.6883031 37.1764746,19.7563084 L37.176371,36.2324812 Z M35.1376208,35.209311 L35.1376208,20.7057152 C34.7023924,20.8097593 34.271333,20.8633641 33.8336069,20.8633641 C32.0046019,20.8633641 30.3013756,19.9547008 29.2437221,18.4771538 C28.1860473,19.954695 26.4828515,20.8633641 24.6538444,20.8633641 C22.824803,20.8633641 21.1216155,19.9547157 20.0639591,18.4771544 C19.0062842,19.9546953 17.3030887,20.8633641 15.4740818,20.8633641 C13.6450404,20.8633641 11.9418529,19.9547157 10.8841965,18.4771544 C9.82652161,19.9546953 8.12332608,20.8633641 6.29431919,20.8633641 C5.76735555,20.8633641 5.24095778,20.7883418 4.73973398,20.644674 L4.73973398,35.209311 L35.1376208,35.209311 Z M30.2720226,15.6557626 C30.5154632,17.4501192 32.0503909,18.8018554 33.845083,18.8018554 C35.7286794,18.8018554 37.285413,17.3395134 37.4474599,15.4751932 L30.2280765,15.4751932 C30.2470638,15.532987 30.2617919,15.5932958 30.2720226,15.6557626 Z M21.0484306,15.4751932 C21.0674179,15.532987 21.0821459,15.5932958 21.0923767,15.6557626 C21.3358173,17.4501192 22.8707449,18.8018554 24.665437,18.8018554 C26.4601001,18.8018554 27.9950169,17.4501481 28.2378191,15.6611556 C28.2451225,15.5981318 28.2590045,15.5358056 28.2787375,15.4751932 L21.0484306,15.4751932 Z M11.9238102,15.6557626 C12.1672508,17.4501192 13.7021785,18.8018554 15.4968705,18.8018554 C17.2915336,18.8018554 18.8264505,17.4501481 19.0692526,15.6611556 C19.0765561,15.5981318 19.0904381,15.5358056 19.110171,15.4751932 L11.8798641,15.4751932 C11.8988514,15.532987 11.9135795,15.5932958 11.9238102,15.6557626 Z M6.31682805,18.8018317 C8.11149114,18.8018317 9.64640798,17.4501244 9.88921012,15.6611319 C9.89651357,15.5981081 9.91039559,15.5357819 9.93012856,15.4751696 L2.70318796,15.4751696 C2.86612006,17.3346852 4.42809696,18.8018317 6.31682805,18.8018317 Z M3.09670082,13.4139924 L37.04257,13.4139924 L33.3489482,2.57204736 L6.80119239,2.57204736 L3.09670082,13.4139924 Z"
|
||||
id="Fill-1"></path>
|
||||
<rect id="Rectangle-3" x="14" y="26" width="6" height="10"></rect>
|
||||
<path d="M20,26 L20,36 L26,36 L26,26 L20,26 Z" id="Rectangle-3"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 3 KiB |
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="100%" height="100%" viewBox="0 0 48.723 48.723" xml:space="preserve">
|
||||
<path d="M7.452,24.152h3.435v5.701h0.633c0.001,0,0.001,0,0.002,0h0.636v-5.701h3.51v-1.059h17.124v1.104h3.178v5.656h0.619 c0,0,0,0,0.002,0h0.619v-5.656h3.736v-0.856c0-0.012,0.006-0.021,0.006-0.032c0-0.072,0-0.143,0-0.215h5.721v-1.316h-5.721 c0-0.054,0-0.108,0-0.164c0-0.011-0.006-0.021-0.006-0.032v-0.832h-8.154v1.028h-7.911v-2.652h-0.689c-0.001,0-0.001,0-0.002,0 h-0.678v2.652h-7.846v-1.104H7.452v1.104H1.114v1.316h6.338V24.152z" />
|
||||
<path d="M21.484,16.849h5.204v-2.611h7.133V1.555H14.588v12.683h6.896V16.849z M16.537,12.288V3.505h15.335v8.783H16.537z" />
|
||||
<rect x="18.682" y="16.898" width="10.809" height="0.537" />
|
||||
<path d="M0,43.971h6.896v2.611H12.1v-2.611h7.134V31.287H0V43.971z M1.95,33.236h15.334v8.785H1.95V33.236z" />
|
||||
<rect x="4.095" y="46.631" width="10.808" height="0.537" />
|
||||
<path d="M29.491,30.994v12.684h6.895v2.611h5.205v-2.611h7.133V30.994H29.491z M46.774,41.729H31.44v-8.783h15.334V41.729z" />
|
||||
<rect x="33.584" y="46.338" width="10.809" height="0.537" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
185
luci-app-dockerman/htdocs/luci-static/resources/dockerman/tar.min.js
vendored
Normal file
|
@ -0,0 +1,185 @@
|
|||
// https://github.com/thiscouldbebetter/TarFileExplorer
|
||||
class TarFileTypeFlag
|
||||
{constructor(value,name)
|
||||
{this.value=value;this.id="_"+this.value;this.name=name;}
|
||||
static _instances;static Instances()
|
||||
{if(TarFileTypeFlag._instances==null)
|
||||
{TarFileTypeFlag._instances=new TarFileTypeFlag_Instances();}
|
||||
return TarFileTypeFlag._instances;}}
|
||||
class TarFileTypeFlag_Instances
|
||||
{constructor()
|
||||
{this.Normal=new TarFileTypeFlag("0","Normal");this.HardLink=new TarFileTypeFlag("1","Hard Link");this.SymbolicLink=new TarFileTypeFlag("2","Symbolic Link");this.CharacterSpecial=new TarFileTypeFlag("3","Character Special");this.BlockSpecial=new TarFileTypeFlag("4","Block Special");this.Directory=new TarFileTypeFlag("5","Directory");this.FIFO=new TarFileTypeFlag("6","FIFO");this.ContiguousFile=new TarFileTypeFlag("7","Contiguous File");this.LongFilePath=new TarFileTypeFlag("L","././@LongLink");this._All=[this.Normal,this.HardLink,this.SymbolicLink,this.CharacterSpecial,this.BlockSpecial,this.Directory,this.FIFO,this.ContiguousFile,this.LongFilePath,];for(var i=0;i<this._All.length;i++)
|
||||
{var item=this._All[i];this._All[item.id]=item;}}}
|
||||
class TarFileEntryHeader
|
||||
{constructor
|
||||
(fileName,fileMode,userIDOfOwner,userIDOfGroup,fileSizeInBytes,timeModifiedInUnixFormat,checksum,typeFlag,nameOfLinkedFile,uStarIndicator,uStarVersion,userNameOfOwner,groupNameOfOwner,deviceNumberMajor,deviceNumberMinor,filenamePrefix)
|
||||
{this.fileName=fileName;this.fileMode=fileMode;this.userIDOfOwner=userIDOfOwner;this.userIDOfGroup=userIDOfGroup;this.fileSizeInBytes=fileSizeInBytes;this.timeModifiedInUnixFormat=timeModifiedInUnixFormat;this.checksum=checksum;this.typeFlag=typeFlag;this.nameOfLinkedFile=nameOfLinkedFile;this.uStarIndicator=uStarIndicator;this.uStarVersion=uStarVersion;this.userNameOfOwner=userNameOfOwner;this.groupNameOfOwner=groupNameOfOwner;this.deviceNumberMajor=deviceNumberMajor;this.deviceNumberMinor=deviceNumberMinor;this.filenamePrefix=filenamePrefix;}
|
||||
static FileNameMaxLength=99;static SizeInBytes=500;static default()
|
||||
{var now=new Date();var unixEpoch=new Date(1970,1,1);var millisecondsSinceUnixEpoch=now-unixEpoch;var secondsSinceUnixEpoch=Math.floor
|
||||
(millisecondsSinceUnixEpoch/1000);var secondsSinceUnixEpochAsStringOctal=secondsSinceUnixEpoch.toString(8).padRight(12,"\0");var timeModifiedInUnixFormat=[];for(var i=0;i<secondsSinceUnixEpochAsStringOctal.length;i++)
|
||||
{var digitAsASCIICode=secondsSinceUnixEpochAsStringOctal.charCodeAt(i);timeModifiedInUnixFormat.push(digitAsASCIICode);}
|
||||
var returnValue=new TarFileEntryHeader
|
||||
("".padRight(100,"\0"),"0100777","0000000","0000000",0,timeModifiedInUnixFormat,0,TarFileTypeFlag.Instances().Normal,"","ustar","00","","","","","");return returnValue;};static directoryNew(directoryName)
|
||||
{var header=TarFileEntryHeader.default();header.fileName=directoryName;header.typeFlag=TarFileTypeFlag.Instances().Directory;header.fileSizeInBytes=0;header.checksumCalculate();return header;};static fileNew(fileName,fileContentsAsBytes)
|
||||
{var header=TarFileEntryHeader.default();header.fileName=fileName;header.typeFlag=TarFileTypeFlag.Instances().Normal;header.fileSizeInBytes=fileContentsAsBytes.length;header.checksumCalculate();return header;};static fromBytes(bytes)
|
||||
{var reader=new ByteStream(bytes);var fileName=reader.readString(100).trim();var fileMode=reader.readString(8);var userIDOfOwner=reader.readString(8);var userIDOfGroup=reader.readString(8);var fileSizeInBytesAsStringOctal=reader.readString(12);var timeModifiedInUnixFormat=reader.readBytes(12);var checksumAsStringOctal=reader.readString(8);var typeFlagValue=reader.readString(1);var nameOfLinkedFile=reader.readString(100);var uStarIndicator=reader.readString(6);var uStarVersion=reader.readString(2);var userNameOfOwner=reader.readString(32);var groupNameOfOwner=reader.readString(32);var deviceNumberMajor=reader.readString(8);var deviceNumberMinor=reader.readString(8);var filenamePrefix=reader.readString(155);var reserved=reader.readBytes(12);var fileSizeInBytes=parseInt
|
||||
(fileSizeInBytesAsStringOctal.trim(),8);var checksum=parseInt
|
||||
(checksumAsStringOctal,8);var typeFlags=TarFileTypeFlag.Instances()._All;var typeFlagID="_"+typeFlagValue;var typeFlag=typeFlags[typeFlagID];var returnValue=new TarFileEntryHeader
|
||||
(fileName,fileMode,userIDOfOwner,userIDOfGroup,fileSizeInBytes,timeModifiedInUnixFormat,checksum,typeFlag,nameOfLinkedFile,uStarIndicator,uStarVersion,userNameOfOwner,groupNameOfOwner,deviceNumberMajor,deviceNumberMinor,filenamePrefix);return returnValue;};checksumCalculate()
|
||||
{var thisAsBytes=this.toBytes();var offsetOfChecksumInBytes=148;var numberOfBytesInChecksum=8;var presumedValueOfEachChecksumByte=" ".charCodeAt(0);for(var i=0;i<numberOfBytesInChecksum;i++)
|
||||
{var offsetOfByte=offsetOfChecksumInBytes+i;thisAsBytes[offsetOfByte]=presumedValueOfEachChecksumByte;}
|
||||
var checksumSoFar=0;for(var i=0;i<thisAsBytes.length;i++)
|
||||
{var byteToAdd=thisAsBytes[i];checksumSoFar+=byteToAdd;}
|
||||
this.checksum=checksumSoFar;return this.checksum;};toBytes()
|
||||
{var headerAsBytes=[];var writer=new ByteStream(headerAsBytes);var fileSizeInBytesAsStringOctal=(this.fileSizeInBytes.toString(8)+"\0").padLeft(12,"0")
|
||||
var checksumAsStringOctal=(this.checksum.toString(8)+"\0 ").padLeft(8,"0");writer.writeString(this.fileName,100);writer.writeString(this.fileMode,8);writer.writeString(this.userIDOfOwner,8);writer.writeString(this.userIDOfGroup,8);writer.writeString(fileSizeInBytesAsStringOctal,12);writer.writeBytes(this.timeModifiedInUnixFormat);writer.writeString(checksumAsStringOctal,8);writer.writeString(this.typeFlag.value,1);writer.writeString(this.nameOfLinkedFile,100);writer.writeString(this.uStarIndicator,6);writer.writeString(this.uStarVersion,2);writer.writeString(this.userNameOfOwner,32);writer.writeString(this.groupNameOfOwner,32);writer.writeString(this.deviceNumberMajor,8);writer.writeString(this.deviceNumberMinor,8);writer.writeString(this.filenamePrefix,155);writer.writeString("".padRight(12,"\0"));return headerAsBytes;};toString()
|
||||
{var newline="\n";var returnValue="[TarFileEntryHeader "
|
||||
+"fileName='"+this.fileName+"' "
|
||||
+"typeFlag='"+(this.typeFlag==null?"err":this.typeFlag.name)+"' "
|
||||
+"fileSizeInBytes='"+this.fileSizeInBytes+"' "
|
||||
+"]"
|
||||
+newline;return returnValue;};}
|
||||
class TarFileEntry
|
||||
{constructor(header,dataAsBytes)
|
||||
{this.header=header;this.dataAsBytes=dataAsBytes;}
|
||||
static directoryNew(directoryName)
|
||||
{var header=TarFileEntryHeader.directoryNew(directoryName);var entry=new TarFileEntry(header,[]);return entry;};static fileNew(fileName,fileContentsAsBytes)
|
||||
{var header=TarFileEntryHeader.fileNew(fileName,fileContentsAsBytes);var entry=new TarFileEntry(header,fileContentsAsBytes);return entry;};static fromBytes(chunkAsBytes,reader)
|
||||
{var chunkSize=TarFile.ChunkSize;var header=TarFileEntryHeader.fromBytes
|
||||
(chunkAsBytes);var sizeOfDataEntryInBytesUnpadded=header.fileSizeInBytes;var numberOfChunksOccupiedByDataEntry=Math.ceil
|
||||
(sizeOfDataEntryInBytesUnpadded/chunkSize)
|
||||
var sizeOfDataEntryInBytesPadded=numberOfChunksOccupiedByDataEntry*chunkSize;var dataAsBytes=reader.readBytes
|
||||
(sizeOfDataEntryInBytesPadded).slice
|
||||
(0,sizeOfDataEntryInBytesUnpadded);var entry=new TarFileEntry(header,dataAsBytes);return entry;};static manyFromByteArrays
|
||||
(fileNamePrefix,fileNameSuffix,entriesAsByteArrays)
|
||||
{var returnValues=[];for(var i=0;i<entriesAsByteArrays.length;i++)
|
||||
{var entryAsBytes=entriesAsByteArrays[i];var entry=TarFileEntry.fileNew
|
||||
(fileNamePrefix+i+fileNameSuffix,entryAsBytes);returnValues.push(entry);}
|
||||
return returnValues;};download(event)
|
||||
{FileHelper.saveBytesAsFile
|
||||
(this.dataAsBytes,this.header.fileName);};remove(event)
|
||||
{alert("Not yet implemented!");};toBytes()
|
||||
{var entryAsBytes=[];var chunkSize=TarFile.ChunkSize;var headerAsBytes=this.header.toBytes();entryAsBytes=entryAsBytes.concat(headerAsBytes);entryAsBytes=entryAsBytes.concat(this.dataAsBytes);var sizeOfDataEntryInBytesUnpadded=this.header.fileSizeInBytes;var numberOfChunksOccupiedByDataEntry=Math.ceil
|
||||
(sizeOfDataEntryInBytesUnpadded/chunkSize)
|
||||
var sizeOfDataEntryInBytesPadded=numberOfChunksOccupiedByDataEntry*chunkSize;var numberOfBytesOfPadding=sizeOfDataEntryInBytesPadded-sizeOfDataEntryInBytesUnpadded;for(var i=0;i<numberOfBytesOfPadding;i++)
|
||||
{entryAsBytes.push(0);}
|
||||
return entryAsBytes;};toString()
|
||||
{var newline="\n";headerAsString=this.header.toString();var dataAsHexadecimalString=ByteHelper.bytesToStringHexadecimal
|
||||
(this.dataAsBytes);var returnValue="[TarFileEntry]"+newline
|
||||
+headerAsString
|
||||
+"[Data]"
|
||||
+dataAsHexadecimalString
|
||||
+"[/Data]"+newline
|
||||
+"[/TarFileEntry]"
|
||||
+newline;return returnValue}}
|
||||
class TarFile
|
||||
{constructor(fileName,entries)
|
||||
{this.fileName=fileName;this.entries=entries;}
|
||||
static ChunkSize=512;static fromBytes(fileName,bytes)
|
||||
{var reader=new ByteStream(bytes);var entries=[];var chunkSize=TarFile.ChunkSize;var numberOfConsecutiveZeroChunks=0;while(reader.hasMoreBytes()==true)
|
||||
{var chunkAsBytes=reader.readBytes(chunkSize);var areAllBytesInChunkZeroes=true;for(var b=0;b<chunkAsBytes.length;b++)
|
||||
{if(chunkAsBytes[b]!=0)
|
||||
{areAllBytesInChunkZeroes=false;break;}}
|
||||
if(areAllBytesInChunkZeroes==true)
|
||||
{numberOfConsecutiveZeroChunks++;if(numberOfConsecutiveZeroChunks==2)
|
||||
{break;}}
|
||||
else
|
||||
{numberOfConsecutiveZeroChunks=0;var entry=TarFileEntry.fromBytes(chunkAsBytes,reader);entries.push(entry);}}
|
||||
var returnValue=new TarFile(fileName,entries);returnValue.consolidateLongPathEntries();return returnValue;}
|
||||
static create(fileName)
|
||||
{return new TarFile
|
||||
(fileName,[]);}
|
||||
consolidateLongPathEntries()
|
||||
{var typeFlagLongPathName=TarFileTypeFlag.Instances().LongFilePath.name;var entries=this.entries;for(var i=0;i<entries.length;i++)
|
||||
{var entry=entries[i];if(entry.header.typeFlag.name==typeFlagLongPathName)
|
||||
{var entryNext=entries[i+1];entryNext.header.fileName=entry.dataAsBytes.reduce
|
||||
((a,b)=>a+=String.fromCharCode(b),"");entryNext.header.fileName=entryNext.header.fileName.replace(/\0/g,"");entries.splice(i,1);i--;}}}
|
||||
downloadAs(fileNameToSaveAs)
|
||||
{return FileHelper.saveBytesAsFile
|
||||
(this.toBytes(),fileNameToSaveAs)}
|
||||
entriesForDirectories()
|
||||
{return this.entries.filter(x=>x.header.typeFlag.name==TarFileTypeFlag.Instances().Directory);}
|
||||
toBytes()
|
||||
{this.toBytes_PrependLongPathEntriesAsNeeded();var fileAsBytes=[];var entriesAsByteArrays=this.entries.map(x=>x.toBytes());this.consolidateLongPathEntries();for(var i=0;i<entriesAsByteArrays.length;i++)
|
||||
{var entryAsBytes=entriesAsByteArrays[i];fileAsBytes=fileAsBytes.concat(entryAsBytes);}
|
||||
var chunkSize=TarFile.ChunkSize;var numberOfZeroChunksToWrite=2;for(var i=0;i<numberOfZeroChunksToWrite;i++)
|
||||
{for(var b=0;b<chunkSize;b++)
|
||||
{fileAsBytes.push(0);}}
|
||||
return fileAsBytes;}
|
||||
toBytes_PrependLongPathEntriesAsNeeded()
|
||||
{var typeFlagLongPath=TarFileTypeFlag.Instances().LongFilePath;var maxLength=TarFileEntryHeader.FileNameMaxLength;var entries=this.entries;for(var i=0;i<entries.length;i++)
|
||||
{var entry=entries[i];var entryHeader=entry.header;var entryFileName=entryHeader.fileName;if(entryFileName.length>maxLength)
|
||||
{var entryFileNameAsBytes=entryFileName.split("").map(x=>x.charCodeAt(0));var entryContainingLongPathToPrepend=TarFileEntry.fileNew
|
||||
(typeFlagLongPath.name,entryFileNameAsBytes);entryContainingLongPathToPrepend.header.typeFlag=typeFlagLongPath;entryContainingLongPathToPrepend.header.timeModifiedInUnixFormat=entryHeader.timeModifiedInUnixFormat;entryContainingLongPathToPrepend.header.checksumCalculate();entryHeader.fileName=entryFileName.substr(0,maxLength)+String.fromCharCode(0);entries.splice(i,0,entryContainingLongPathToPrepend);i++;}}}
|
||||
toString()
|
||||
{var newline="\n";var returnValue="[TarFile]"+newline;for(var i=0;i<this.entries.length;i++)
|
||||
{var entry=this.entries[i];var entryAsString=entry.toString();returnValue+=entryAsString;}
|
||||
returnValue+="[/TarFile]"+newline;return returnValue;}}
|
||||
function StringExtensions()
|
||||
{}
|
||||
{String.prototype.padLeft=function(lengthToPadTo,charToPadWith)
|
||||
{var returnValue=this;while(returnValue.length<lengthToPadTo)
|
||||
{returnValue=charToPadWith+returnValue;}
|
||||
return returnValue;}
|
||||
String.prototype.padRight=function(lengthToPadTo,charToPadWith)
|
||||
{var returnValue=this;while(returnValue.length<lengthToPadTo)
|
||||
{returnValue+=charToPadWith;}
|
||||
return returnValue;}}
|
||||
class Globals
|
||||
{static Instance=new Globals();}
|
||||
class FileHelper
|
||||
{static loadFileAsBytes(fileToLoad,callback)
|
||||
{var fileReader=new FileReader();fileReader.onload=(fileLoadedEvent)=>{var fileLoadedAsBinaryString=fileLoadedEvent.target.result;var fileLoadedAsBytes=ByteHelper.stringUTF8ToBytes(fileLoadedAsBinaryString);callback(fileToLoad.name,fileLoadedAsBytes);}
|
||||
fileReader.readAsBinaryString(fileToLoad);}
|
||||
static loadFileAsText(fileToLoad,callback)
|
||||
{var fileReader=new FileReader();fileReader.onload=(fileLoadedEvent)=>{var textFromFileLoaded=fileLoadedEvent.target.result;callback(fileToLoad.name,textFromFileLoaded);};fileReader.readAsText(fileToLoad);}
|
||||
static saveBytesAsFile(bytesToWrite,fileNameToSaveAs)
|
||||
{var bytesToWriteAsArrayBuffer=new ArrayBuffer(bytesToWrite.length);var bytesToWriteAsUIntArray=new Uint8Array(bytesToWriteAsArrayBuffer);for(var i=0;i<bytesToWrite.length;i++)
|
||||
{bytesToWriteAsUIntArray[i]=bytesToWrite[i];}
|
||||
var bytesToWriteAsBlob=new Blob
|
||||
([bytesToWriteAsArrayBuffer],{type:"application/type"});
|
||||
return bytesToWriteAsBlob
|
||||
// var downloadLink=document.createElement("a");downloadLink.download=fileNameToSaveAs;downloadLink.href=window.URL.createObjectURL(bytesToWriteAsBlob);downloadLink.click();
|
||||
}
|
||||
static saveTextAsFile(textToSave,fileNameToSaveAs)
|
||||
{var textToSaveAsBlob=new Blob([textToSave],{type:"text/plain"});var textToSaveAsURL=window.URL.createObjectURL(textToSaveAsBlob);var downloadLink=document.createElement("a");downloadLink.download=fileNameToSaveAs;downloadLink.href=textToSaveAsURL;downloadLink.click();}}
|
||||
class ByteStream
|
||||
{constructor(bytes)
|
||||
{this.bytes=bytes;this.byteIndexCurrent=0;}
|
||||
static BitsPerByte=8;static BitsPerByteTimesTwo=ByteStream.BitsPerByte*2;static BitsPerByteTimesThree=ByteStream.BitsPerByte*3;hasMoreBytes()
|
||||
{return(this.byteIndexCurrent<this.bytes.length);}
|
||||
readBytes(numberOfBytesToRead)
|
||||
{var returnValue=new Array(numberOfBytesToRead);for(var b=0;b<numberOfBytesToRead;b++)
|
||||
{returnValue[b]=this.readByte();}
|
||||
return returnValue;}
|
||||
readByte()
|
||||
{var returnValue=this.bytes[this.byteIndexCurrent];this.byteIndexCurrent++;return returnValue;}
|
||||
readString(lengthOfString)
|
||||
{var returnValue="";for(var i=0;i<lengthOfString;i++)
|
||||
{var byte=this.readByte();if(byte!=0)
|
||||
{var byteAsChar=String.fromCharCode(byte);returnValue+=byteAsChar;}}
|
||||
return returnValue;}
|
||||
writeBytes(bytesToWrite)
|
||||
{for(var b=0;b<bytesToWrite.length;b++)
|
||||
{this.bytes.push(bytesToWrite[b]);}
|
||||
this.byteIndexCurrent=this.bytes.length;}
|
||||
writeByte(byteToWrite)
|
||||
{this.bytes.push(byteToWrite);this.byteIndexCurrent++;}
|
||||
writeString(stringToWrite,lengthPadded)
|
||||
{for(var i=0;i<stringToWrite.length;i++)
|
||||
{var charAsByte=stringToWrite.charCodeAt(i);this.writeByte(charAsByte);}
|
||||
var numberOfPaddingChars=lengthPadded-stringToWrite.length;for(var i=0;i<numberOfPaddingChars;i++)
|
||||
{this.writeByte(0);}}}
|
||||
class ByteHelper
|
||||
{static stringUTF8ToBytes(stringToConvert)
|
||||
{var bytes=[];for(var i=0;i<stringToConvert.length;i++)
|
||||
{var byte=stringToConvert.charCodeAt(i);bytes.push(byte);}
|
||||
return bytes;}
|
||||
static bytesToStringUTF8(bytesToConvert)
|
||||
{var returnValue="";for(var i=0;i<bytesToConvert.length;i++)
|
||||
{var byte=bytesToConvert[i];var byteAsChar=String.fromCharCode(byte);returnValue+=byteAsChar}
|
||||
return returnValue;}}
|
||||
function ArrayExtensions()
|
||||
{}
|
||||
{Array.prototype.remove=function(elementToRemove)
|
||||
{this.splice(this.indexOf(elementToRemove),1);}}
|
After Width: | Height: | Size: 6.4 KiB |
614
luci-app-dockerman/luasrc/controller/dockerman.lua
Normal file
|
@ -0,0 +1,614 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
-- local uci = (require "luci.model.uci").cursor()
|
||||
|
||||
module("luci.controller.dockerman",package.seeall)
|
||||
|
||||
function index()
|
||||
entry({"admin", "docker"},
|
||||
alias("admin", "docker", "config"),
|
||||
_("Docker"),
|
||||
40).acl_depends = { "luci-app-dockerman" }
|
||||
|
||||
entry({"admin", "docker", "config"},cbi("dockerman/configuration"),_("Configuration"), 8).leaf=true
|
||||
|
||||
-- local uci = (require "luci.model.uci").cursor()
|
||||
-- if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
-- local host = uci:get("dockerd", "dockerman", "remote_host")
|
||||
-- local port = uci:get("dockerd", "dockerman", "remote_port")
|
||||
-- if not host or not port then
|
||||
-- return
|
||||
-- end
|
||||
-- else
|
||||
-- local socket = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
|
||||
-- if socket and not nixio.fs.access(socket) then
|
||||
-- return
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- if (require "luci.model.docker").new():_ping().code ~= 200 then
|
||||
-- return
|
||||
-- end
|
||||
|
||||
entry({"admin", "docker", "overview"}, form("dockerman/overview"),_("Overview"), 2).leaf=true
|
||||
entry({"admin", "docker", "containers"}, form("dockerman/containers"), _("Containers"), 3).leaf=true
|
||||
entry({"admin", "docker", "images"}, form("dockerman/images"), _("Images"), 4).leaf=true
|
||||
entry({"admin", "docker", "networks"}, form("dockerman/networks"), _("Networks"), 5).leaf=true
|
||||
entry({"admin", "docker", "volumes"}, form("dockerman/volumes"), _("Volumes"), 6).leaf=true
|
||||
entry({"admin", "docker", "events"}, call("action_events"), _("Events"), 7)
|
||||
|
||||
entry({"admin", "docker", "newcontainer"}, form("dockerman/newcontainer")).leaf=true
|
||||
entry({"admin", "docker", "newnetwork"}, form("dockerman/newnetwork")).leaf=true
|
||||
entry({"admin", "docker", "container"}, form("dockerman/container")).leaf=true
|
||||
|
||||
entry({"admin", "docker", "container_stats"}, call("action_get_container_stats")).leaf=true
|
||||
entry({"admin", "docker", "containers_stats"}, call("action_get_containers_stats")).leaf=true
|
||||
entry({"admin", "docker", "get_system_df"}, call("action_get_system_df")).leaf=true
|
||||
entry({"admin", "docker", "container_get_archive"}, call("download_archive")).leaf=true
|
||||
entry({"admin", "docker", "container_put_archive"}, call("upload_archive")).leaf=true
|
||||
entry({"admin", "docker", "container_list_file"}, call("list_file")).leaf=true
|
||||
entry({"admin", "docker", "container_remove_file"}, call("remove_file")).leaf=true
|
||||
entry({"admin", "docker", "container_rename_file"}, call("rename_file")).leaf=true
|
||||
entry({"admin", "docker", "container_export"}, call("export_container")).leaf=true
|
||||
entry({"admin", "docker", "images_save"}, call("save_images")).leaf=true
|
||||
entry({"admin", "docker", "images_load"}, call("load_images")).leaf=true
|
||||
entry({"admin", "docker", "images_import"}, call("import_images")).leaf=true
|
||||
entry({"admin", "docker", "images_get_tags"}, call("get_image_tags")).leaf=true
|
||||
entry({"admin", "docker", "images_tag"}, call("tag_image")).leaf=true
|
||||
entry({"admin", "docker", "images_untag"}, call("untag_image")).leaf=true
|
||||
entry({"admin", "docker", "confirm"}, call("action_confirm")).leaf=true
|
||||
end
|
||||
|
||||
function action_get_system_df()
|
||||
local res = docker.new():df()
|
||||
luci.http.status(res.code, res.message)
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(res.body)
|
||||
end
|
||||
|
||||
function scandir(id, directory)
|
||||
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
|
||||
if not cmd_docker or cmd_docker:match("^%s+$") then
|
||||
return
|
||||
end
|
||||
local i, t, popen = 0, {}, io.popen
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
|
||||
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
|
||||
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
|
||||
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
|
||||
if remote and host and port then
|
||||
hosts = "tcp://" .. host .. ':'.. port
|
||||
elseif socket_path then
|
||||
hosts = "unix://" .. socket_path
|
||||
else
|
||||
return
|
||||
end
|
||||
local pfile = popen(cmd_docker .. ' -H "'.. hosts ..'" exec ' ..id .." ls -lh \""..directory.."\" | egrep -v '^total'")
|
||||
for fileinfo in pfile:lines() do
|
||||
i = i + 1
|
||||
t[i] = fileinfo
|
||||
end
|
||||
pfile:close()
|
||||
return t
|
||||
end
|
||||
|
||||
function list_response(id, path, success)
|
||||
luci.http.prepare_content("application/json")
|
||||
local result
|
||||
if success then
|
||||
local rv = scandir(id, path)
|
||||
result = {
|
||||
ec = 0,
|
||||
data = rv
|
||||
}
|
||||
else
|
||||
result = {
|
||||
ec = 1
|
||||
}
|
||||
end
|
||||
luci.http.write_json(result)
|
||||
end
|
||||
|
||||
function list_file(id)
|
||||
local path = luci.http.formvalue("path")
|
||||
list_response(id, path, true)
|
||||
end
|
||||
|
||||
function rename_file(id)
|
||||
local filepath = luci.http.formvalue("filepath")
|
||||
local newpath = luci.http.formvalue("newpath")
|
||||
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
|
||||
if not cmd_docker or cmd_docker:match("^%s+$") then
|
||||
return
|
||||
end
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
|
||||
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
|
||||
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
|
||||
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
|
||||
if remote and host and port then
|
||||
hosts = "tcp://" .. host .. ':'.. port
|
||||
elseif socket_path then
|
||||
hosts = "unix://" .. socket_path
|
||||
else
|
||||
return
|
||||
end
|
||||
local success = os.execute(cmd_docker .. ' -H "'.. hosts ..'" exec '.. id ..' mv "'..filepath..'" "'..newpath..'"')
|
||||
list_response(nixio.fs.dirname(filepath), success)
|
||||
end
|
||||
|
||||
function remove_file(id)
|
||||
local path = luci.http.formvalue("path")
|
||||
local isdir = luci.http.formvalue("isdir")
|
||||
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
|
||||
if not cmd_docker or cmd_docker:match("^%s+$") then
|
||||
return
|
||||
end
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint")
|
||||
local socket_path = not remote and uci:get("dockerd", "dockerman", "socket_path") or nil
|
||||
local host = remote and uci:get("dockerd", "dockerman", "remote_host") or nil
|
||||
local port = remote and uci:get("dockerd", "dockerman", "remote_port") or nil
|
||||
if remote and host and port then
|
||||
hosts = "tcp://" .. host .. ':'.. port
|
||||
elseif socket_path then
|
||||
hosts = "unix://" .. socket_path
|
||||
else
|
||||
return
|
||||
end
|
||||
path = path:gsub("<>", "/")
|
||||
path = path:gsub(" ", "\ ")
|
||||
local success
|
||||
if isdir then
|
||||
success = os.execute(cmd_docker .. ' -H "'.. hosts ..'" exec '.. id ..' rm -r "'..path..'"')
|
||||
else
|
||||
success = os.remove(path)
|
||||
end
|
||||
list_response(nixio.fs.dirname(path), success)
|
||||
end
|
||||
|
||||
function action_events()
|
||||
local logs = ""
|
||||
local query ={}
|
||||
|
||||
local dk = docker.new()
|
||||
query["until"] = os.time()
|
||||
local events = dk:events({query = query})
|
||||
|
||||
if events.code == 200 then
|
||||
for _, v in ipairs(events.body) do
|
||||
local date = "unknown"
|
||||
if v and v.time then
|
||||
date = os.date("%Y-%m-%d %H:%M:%S", v.time)
|
||||
end
|
||||
|
||||
local name = v.Actor.Attributes.name or "unknown"
|
||||
local action = v.Action or "unknown"
|
||||
|
||||
if v and v.Type == "container" then
|
||||
local id = v.Actor.ID or "unknown"
|
||||
logs = logs .. string.format("[%s] %s %s Container ID: %s Container Name: %s\n", date, v.Type, action, id, name)
|
||||
elseif v.Type == "network" then
|
||||
local container = v.Actor.Attributes.container or "unknown"
|
||||
local network = v.Actor.Attributes.type or "unknown"
|
||||
logs = logs .. string.format("[%s] %s %s Container ID: %s Network Name: %s Network type: %s\n", date, v.Type, action, container, name, network)
|
||||
elseif v.Type == "image" then
|
||||
local id = v.Actor.ID or "unknown"
|
||||
logs = logs .. string.format("[%s] %s %s Image: %s Image name: %s\n", date, v.Type, action, id, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
luci.template.render("dockerman/logs", {self={syslog = logs, title="Events"}})
|
||||
end
|
||||
|
||||
local calculate_cpu_percent = function(d)
|
||||
if type(d) ~= "table" then
|
||||
return
|
||||
end
|
||||
|
||||
local cpu_count = tonumber(d["cpu_stats"]["online_cpus"])
|
||||
local cpu_percent = 0.0
|
||||
local cpu_delta = tonumber(d["cpu_stats"]["cpu_usage"]["total_usage"]) - tonumber(d["precpu_stats"]["cpu_usage"]["total_usage"])
|
||||
local 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
|
||||
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[e] = {
|
||||
bw_tx = tonumber(v.tx_bytes),
|
||||
bw_rx = tonumber(v.rx_bytes)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local function get_stat(container_id)
|
||||
if container_id then
|
||||
local dk = docker.new()
|
||||
local response = dk.containers:inspect({id = container_id})
|
||||
if response.code == 200 and response.body.State.Running then
|
||||
response = dk.containers:stats({id = container_id, query = {stream = false, ["one-shot"] = true}})
|
||||
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)
|
||||
return response.code, response.body.message, {
|
||||
cpu_percent = cpu_percent,
|
||||
memory = {
|
||||
mem_useage = mem_useage,
|
||||
mem_limit = mem_limit
|
||||
},
|
||||
bw_rxtx = bw_rxtx
|
||||
}
|
||||
else
|
||||
return response.code, response.body.message
|
||||
end
|
||||
else
|
||||
if response.code == 200 then
|
||||
return 500, "container "..container_id.." not running"
|
||||
else
|
||||
return response.code, response.body.message
|
||||
end
|
||||
end
|
||||
else
|
||||
return 404, "No container name or id"
|
||||
end
|
||||
end
|
||||
function action_get_container_stats(container_id)
|
||||
local code, msg, res = get_stat(container_id)
|
||||
luci.http.status(code, msg)
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(res)
|
||||
end
|
||||
|
||||
function action_get_containers_stats()
|
||||
local res = luci.http.formvalue(containers) or ""
|
||||
local stats = {}
|
||||
res = luci.jsonc.parse(res.containers)
|
||||
if res and type(res) == "table" then
|
||||
for i, v in ipairs(res) do
|
||||
_,_,stats[v] = get_stat(v)
|
||||
end
|
||||
end
|
||||
luci.http.status(200, "OK")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(stats)
|
||||
end
|
||||
|
||||
function action_confirm()
|
||||
local data = docker:read_status()
|
||||
if data then
|
||||
data = data:gsub("\n","<br>"):gsub(" "," ")
|
||||
code = 202
|
||||
msg = data
|
||||
else
|
||||
code = 200
|
||||
msg = "finish"
|
||||
data = "finish"
|
||||
end
|
||||
|
||||
luci.http.status(code, msg)
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({info = data})
|
||||
end
|
||||
|
||||
function export_container(id)
|
||||
local dk = docker.new()
|
||||
local first
|
||||
|
||||
local cb = function(res, chunk)
|
||||
if res.code == 200 then
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.header('Content-Disposition', 'inline; filename="'.. id ..'.tar"')
|
||||
luci.http.header('Content-Type', 'application\/x-tar')
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
else
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.prepare_content("text/plain")
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
end
|
||||
end
|
||||
|
||||
local res = dk.containers:export({id = id}, cb)
|
||||
end
|
||||
|
||||
function download_archive()
|
||||
local id = luci.http.formvalue("id")
|
||||
local path = luci.http.formvalue("path")
|
||||
local filename = luci.http.formvalue("filename") or "archive"
|
||||
local dk = docker.new()
|
||||
local first
|
||||
|
||||
local cb = function(res, chunk)
|
||||
if res and res.code and res.code == 200 then
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.header('Content-Disposition', 'inline; filename="'.. filename .. '.tar"')
|
||||
luci.http.header('Content-Type', 'application\/x-tar')
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
else
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("text/plain")
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
end
|
||||
end
|
||||
|
||||
local res = dk.containers:get_archive({
|
||||
id = id,
|
||||
query = {
|
||||
path = luci.http.urlencode(path)
|
||||
}
|
||||
}, cb)
|
||||
end
|
||||
|
||||
function upload_archive(container_id)
|
||||
local path = luci.http.formvalue("upload-path")
|
||||
local dk = docker.new()
|
||||
local ltn12 = require "luci.ltn12"
|
||||
|
||||
local rec_send = function(sinkout)
|
||||
luci.http.setfilehandler(function (meta, chunk, eof)
|
||||
if chunk then
|
||||
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local res = dk.containers:put_archive({
|
||||
id = container_id,
|
||||
query = {
|
||||
path = luci.http.urlencode(path)
|
||||
},
|
||||
body = rec_send
|
||||
})
|
||||
|
||||
local msg = res and res.message or res.body and res.body.message or nil
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
|
||||
-- function save_images()
|
||||
-- local names = luci.http.formvalue("names")
|
||||
-- local dk = docker.new()
|
||||
-- local first
|
||||
|
||||
-- local cb = function(res, chunk)
|
||||
-- if res.code == 200 then
|
||||
-- if not first then
|
||||
-- first = true
|
||||
-- luci.http.status(res.code, res.message)
|
||||
-- luci.http.header('Content-Disposition', 'inline; filename="'.. "images" ..'.tar"')
|
||||
-- luci.http.header('Content-Type', 'application\/x-tar')
|
||||
-- end
|
||||
-- luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
-- else
|
||||
-- if not first then
|
||||
-- first = true
|
||||
-- luci.http.prepare_content("text/plain")
|
||||
-- end
|
||||
-- luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- docker:write_status("Images: saving" .. " " .. names .. "...")
|
||||
-- local res = dk.images:get({
|
||||
-- query = {
|
||||
-- names = luci.http.urlencode(names)
|
||||
-- }
|
||||
-- }, cb)
|
||||
-- docker:clear_status()
|
||||
|
||||
-- local msg = res and res.body and res.body.message or nil
|
||||
-- luci.http.status(res.code, msg)
|
||||
-- luci.http.prepare_content("application/json")
|
||||
-- luci.http.write_json({message = msg})
|
||||
-- end
|
||||
|
||||
function load_images()
|
||||
local archive = luci.http.formvalue("upload-archive")
|
||||
local dk = docker.new()
|
||||
local ltn12 = require "luci.ltn12"
|
||||
|
||||
local rec_send = function(sinkout)
|
||||
luci.http.setfilehandler(function (meta, chunk, eof)
|
||||
if chunk then
|
||||
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
docker:write_status("Images: loading...")
|
||||
local res = dk.images:load({body = rec_send})
|
||||
local msg = res and res.body and ( res.body.message or res.body.stream or res.body.error ) or nil
|
||||
if res and res.code == 200 and msg and msg:match("Loaded image ID") then
|
||||
docker:clear_status()
|
||||
else
|
||||
docker:append_status("code:" .. (res and res.code or "500") .." ".. (msg or "unknow"))
|
||||
end
|
||||
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
|
||||
function import_images()
|
||||
local src = luci.http.formvalue("src")
|
||||
local itag = luci.http.formvalue("tag")
|
||||
local dk = docker.new()
|
||||
local ltn12 = require "luci.ltn12"
|
||||
|
||||
local rec_send = function(sinkout)
|
||||
luci.http.setfilehandler(function (meta, chunk, eof)
|
||||
if chunk then
|
||||
ltn12.pump.step(ltn12.source.string(chunk), sinkout)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
docker:write_status("Images: importing".. " ".. itag .."...\n")
|
||||
local repo = itag and itag:match("^([^:]+)")
|
||||
local tag = itag and itag:match("^[^:]-:([^:]+)")
|
||||
local res = dk.images:create({
|
||||
query = {
|
||||
fromSrc = luci.http.urlencode(src or "-"),
|
||||
repo = repo or nil,
|
||||
tag = tag or nil
|
||||
},
|
||||
body = not src and rec_send or nil
|
||||
}, docker.import_image_show_status_cb)
|
||||
|
||||
local msg = res and res.body and ( res.body.message )or nil
|
||||
if not msg and #res.body == 0 then
|
||||
msg = res.body.status or res.body.error
|
||||
elseif not msg and #res.body >= 1 then
|
||||
msg = res.body[#res.body].status or res.body[#res.body].error
|
||||
end
|
||||
|
||||
if res.code == 200 and msg and msg:match("sha256:") then
|
||||
docker:clear_status()
|
||||
else
|
||||
docker:append_status("code:" .. (res and res.code or "500") .." ".. (msg or "unknow"))
|
||||
end
|
||||
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
|
||||
function get_image_tags(image_id)
|
||||
if not image_id then
|
||||
luci.http.status(400, "no image id")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = "no image id"})
|
||||
return
|
||||
end
|
||||
|
||||
local dk = docker.new()
|
||||
local res = dk.images:inspect({
|
||||
id = image_id
|
||||
})
|
||||
local msg = res and res.body and res.body.message or nil
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
|
||||
if res.code == 200 then
|
||||
local tags = res.body.RepoTags
|
||||
luci.http.write_json({tags = tags})
|
||||
else
|
||||
local msg = res and res.body and res.body.message or nil
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
end
|
||||
|
||||
function tag_image(image_id)
|
||||
local src = luci.http.formvalue("tag")
|
||||
local image_id = image_id or luci.http.formvalue("id")
|
||||
|
||||
if type(src) ~= "string" or not image_id then
|
||||
luci.http.status(400, "no image id or tag")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = "no image id or tag"})
|
||||
return
|
||||
end
|
||||
|
||||
local repo = src:match("^([^:]+)")
|
||||
local tag = src:match("^[^:]-:([^:]+)")
|
||||
local dk = docker.new()
|
||||
local res = dk.images:tag({
|
||||
id = image_id,
|
||||
query={
|
||||
repo=repo,
|
||||
tag=tag
|
||||
}
|
||||
})
|
||||
local msg = res and res.body and res.body.message or nil
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
|
||||
if res.code == 201 then
|
||||
local tags = res.body.RepoTags
|
||||
luci.http.write_json({tags = tags})
|
||||
else
|
||||
local msg = res and res.body and res.body.message or nil
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
end
|
||||
|
||||
function untag_image(tag)
|
||||
local tag = tag or luci.http.formvalue("tag")
|
||||
|
||||
if not tag then
|
||||
luci.http.status(400, "no tag name")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = "no tag name"})
|
||||
return
|
||||
end
|
||||
|
||||
local dk = docker.new()
|
||||
local res = dk.images:inspect({name = tag})
|
||||
|
||||
if res.code == 200 then
|
||||
local tags = res.body.RepoTags
|
||||
if #tags > 1 then
|
||||
local r = dk.images:remove({name = tag})
|
||||
local msg = r and r.body and r.body.message or nil
|
||||
luci.http.status(r.code, msg)
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = msg})
|
||||
else
|
||||
luci.http.status(500, "Cannot remove the last tag")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = "Cannot remove the last tag"})
|
||||
end
|
||||
else
|
||||
local msg = res and res.body and res.body.message or nil
|
||||
luci.http.status(res and res.code or 500, msg or "unknow")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({message = msg or "unknow"})
|
||||
end
|
||||
end
|
152
luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua
Normal file
|
@ -0,0 +1,152 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2021 Florian Eckert <fe@dev.tdt.de>
|
||||
Copyright 2021 lisaac <lisaac.cn@gmail.com>
|
||||
]]--
|
||||
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
|
||||
local m, s, o
|
||||
|
||||
m = Map("dockerd",
|
||||
translate("Docker - Configuration"),
|
||||
translate("DockerMan is a simple docker manager client for LuCI"))
|
||||
|
||||
if nixio.fs.access("/usr/bin/dockerd") and not m.uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
s = m:section(NamedSection, "globals", "section", translate("Docker Daemon settings"))
|
||||
|
||||
o = s:option(Flag, "auto_start", translate("Auto start"))
|
||||
o.rmempty = false
|
||||
o.write = function(self, section, value)
|
||||
if value == "1" then
|
||||
luci.util.exec("/etc/init.d/dockerd enable")
|
||||
else
|
||||
luci.util.exec("/etc/init.d/dockerd disable")
|
||||
end
|
||||
m.uci:set("dockerd", "globals", "auto_start", value)
|
||||
end
|
||||
|
||||
o = s:option(Value, "data_root",
|
||||
translate("Docker Root Dir"))
|
||||
o.placeholder = "/opt/docker/"
|
||||
o:depends("remote_endpoint", 0)
|
||||
|
||||
o = s:option(Value, "bip",
|
||||
translate("Default bridge"),
|
||||
translate("Configure the default bridge network"))
|
||||
o.placeholder = "172.17.0.1/16"
|
||||
o.datatype = "ipaddr"
|
||||
o:depends("remote_endpoint", 0)
|
||||
|
||||
o = s:option(DynamicList, "registry_mirrors",
|
||||
translate("Registry Mirrors"),
|
||||
translate("It replaces the daemon registry mirrors with a new set of registry mirrors"))
|
||||
o:value("https://hub-mirror.c.163.com", "https://hub-mirror.c.163.com")
|
||||
o:depends("remote_endpoint", 0)
|
||||
o.forcewrite = true
|
||||
|
||||
o = s:option(ListValue, "log_level",
|
||||
translate("Log Level"),
|
||||
translate('Set the logging level'))
|
||||
o:value("debug", translate("Debug"))
|
||||
o:value("", translate("Info")) -- This is the default debug level from the deamon is optin is not set
|
||||
o:value("warn", translate("Warning"))
|
||||
o:value("error", translate("Error"))
|
||||
o:value("fatal", translate("Fatal"))
|
||||
o.rmempty = true
|
||||
o:depends("remote_endpoint", 0)
|
||||
|
||||
o = s:option(DynamicList, "hosts",
|
||||
translate("Client connection"),
|
||||
translate('Specifies where the Docker daemon will listen for client connections (default: unix:///var/run/docker.sock)'))
|
||||
o:value("unix:///var/run/docker.sock", "unix:///var/run/docker.sock")
|
||||
o:value("tcp://0.0.0.0:2375", "tcp://0.0.0.0:2375")
|
||||
o.rmempty = true
|
||||
o:depends("remote_endpoint", 0)
|
||||
end
|
||||
|
||||
s = m:section(NamedSection, "dockerman", "section", translate("DockerMan settings"))
|
||||
s:tab("ac", translate("Access Control"))
|
||||
s:tab("dockerman", translate("DockerMan"))
|
||||
|
||||
o = s:taboption("dockerman", Flag, "remote_endpoint",
|
||||
translate("Remote Endpoint"),
|
||||
translate("Connect to remote docker endpoint"))
|
||||
o.rmempty = false
|
||||
o.validate = function(self, value, sid)
|
||||
local res = luci.http.formvaluetable("cbid.dockerd")
|
||||
if res["dockerman.remote_endpoint"] == "1" then
|
||||
if res["dockerman.remote_port"] and res["dockerman.remote_port"] ~= "" and res["dockerman.remote_host"] and res["dockerman.remote_host"] ~= "" then
|
||||
return 1
|
||||
else
|
||||
return nil, translate("Please input the PORT or HOST IP of remote docker instance!")
|
||||
end
|
||||
else
|
||||
if not res["dockerman.socket_path"] then
|
||||
return nil, translate("Please input the SOCKET PATH of docker daemon!")
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
o = s:taboption("dockerman", Value, "socket_path",
|
||||
translate("Docker Socket Path"))
|
||||
o.default = "/var/run/docker.sock"
|
||||
o.placeholder = "/var/run/docker.sock"
|
||||
o:depends("remote_endpoint", 0)
|
||||
|
||||
o = s:taboption("dockerman", Value, "remote_host",
|
||||
translate("Remote Host"),
|
||||
translate("Host or IP Address for the connection to a remote docker instance"))
|
||||
o.datatype = "host"
|
||||
o.placeholder = "10.1.1.2"
|
||||
o:depends("remote_endpoint", 1)
|
||||
|
||||
o = s:taboption("dockerman", Value, "remote_port",
|
||||
translate("Remote Port"))
|
||||
o.placeholder = "2375"
|
||||
o.datatype = "port"
|
||||
o:depends("remote_endpoint", 1)
|
||||
|
||||
-- o = s:taboption("dockerman", Value, "status_path", translate("Action Status Tempfile Path"), translate("Where you want to save the docker status file"))
|
||||
-- o = s:taboption("dockerman", Flag, "debug", translate("Enable Debug"), translate("For debug, It shows all docker API actions of luci-app-dockerman in Debug Tempfile Path"))
|
||||
-- o.enabled="true"
|
||||
-- o.disabled="false"
|
||||
-- o = s:taboption("dockerman", Value, "debug_path", translate("Debug Tempfile Path"), translate("Where you want to save the debug tempfile"))
|
||||
|
||||
if nixio.fs.access("/usr/bin/dockerd") and not m.uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
o = s:taboption("ac", DynamicList, "ac_allowed_interface", translate("Allowed access interfaces"), translate("Which interface(s) can access containers under the bridge network, fill-in Interface Name"))
|
||||
local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {}
|
||||
for i, v in ipairs(interfaces) do
|
||||
o:value(v, v)
|
||||
end
|
||||
o = s:taboption("ac", DynamicList, "ac_allowed_ports", translate("Ports allowed to be accessed"), translate("Which Port(s) can be accessed, it's not restricted by the Allowed Access interfaces configuration. Use this configuration with caution!"))
|
||||
o.placeholder = "8080/tcp"
|
||||
local docker = require "luci.model.docker"
|
||||
local containers, res, lost_state
|
||||
local dk = docker.new()
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
else
|
||||
lost_state = false
|
||||
res = dk.containers:list()
|
||||
if res and res.code and res.code < 300 then
|
||||
containers = res.body
|
||||
end
|
||||
end
|
||||
|
||||
-- allowed_container.placeholder = "container name_or_id"
|
||||
if containers then
|
||||
for i, v in ipairs(containers) do
|
||||
if v.State == "running" and v.Ports then
|
||||
for _, port in ipairs(v.Ports) do
|
||||
if port.PublicPort and port.IP and not string.find(port.IP,":") then
|
||||
o:value(port.PublicPort.."/"..port.Type, v.Names[1]:sub(2) .. " | " .. port.PublicPort .. " | " .. port.Type)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
810
luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua
Normal file
|
@ -0,0 +1,810 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
require "luci.util"
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
local dk = docker.new()
|
||||
|
||||
container_id = arg[1]
|
||||
local action = arg[2] or "info"
|
||||
|
||||
local m, s, o
|
||||
local images, networks, container_info, res
|
||||
|
||||
if not container_id then
|
||||
return
|
||||
end
|
||||
|
||||
res = dk.containers:inspect({id = container_id})
|
||||
if res.code < 300 then
|
||||
container_info = 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 .. "<br>") or "") .. out[1]["HostPort"] .. ":" .. inter
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_env = function(d)
|
||||
local data
|
||||
|
||||
if d.Config and d.Config.Env then
|
||||
for _,v in ipairs(d.Config.Env) do
|
||||
data = (data and (data .. "<br>") or "") .. v
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_command = function(d)
|
||||
local data
|
||||
|
||||
if d.Config and d.Config.Cmd then
|
||||
for _,v in ipairs(d.Config.Cmd) do
|
||||
data = (data and (data .. " ") or "") .. v
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_mounts = function(d)
|
||||
local data
|
||||
|
||||
if d.Mounts then
|
||||
for _,v in ipairs(d.Mounts) do
|
||||
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 .. "<br>") 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 .. "<br>") 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 .. "<br>") or "") .. v
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_tmpfs = function(d)
|
||||
local data
|
||||
|
||||
if d.HostConfig and d.HostConfig.Tmpfs then
|
||||
for k, v in pairs(d.HostConfig.Tmpfs) do
|
||||
data = (data and (data .. "<br>") or "") .. k .. (v~="" and ":" or "")..v
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_dns = function(d)
|
||||
local data
|
||||
|
||||
if d.HostConfig and d.HostConfig.Dns then
|
||||
for _, v in ipairs(d.HostConfig.Dns) do
|
||||
data = (data and (data .. "<br>") or "") .. v
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local get_sysctl = function(d)
|
||||
local data
|
||||
|
||||
if d.HostConfig and d.HostConfig.Sysctls then
|
||||
for k, v in pairs(d.HostConfig.Sysctls) do
|
||||
data = (data and (data .. "<br>") or "") .. k..":"..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)
|
||||
local res
|
||||
|
||||
docker:clear_status()
|
||||
docker:append_status("Containers: " .. cmd .. " " .. container_id .. "...")
|
||||
|
||||
if cmd ~= "upgrade" then
|
||||
res = dk.containers[cmd](dk, {id = container_id})
|
||||
else
|
||||
res = dk.containers_upgrade(dk, {id = container_id})
|
||||
end
|
||||
|
||||
if res and res.code >= 300 then
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
|
||||
else
|
||||
docker:clear_status()
|
||||
if cmd ~= "remove" and cmd ~= "upgrade" then
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id))
|
||||
else
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
m=SimpleForm("docker",
|
||||
translatef("Docker - Container (%s)", container_info.Name:sub(2)),
|
||||
translate("On this page, the selected container can be managed."))
|
||||
m.redirect = luci.dispatcher.build_url("admin/docker/containers")
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template = "cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "_start")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Start")
|
||||
o.inputstyle = "apply"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"start")
|
||||
end
|
||||
|
||||
o = s:option(Button, "_restart")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Restart")
|
||||
o.inputstyle = "reload"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"restart")
|
||||
end
|
||||
|
||||
o = s:option(Button, "_stop")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Stop")
|
||||
o.inputstyle = "reset"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"stop")
|
||||
end
|
||||
|
||||
o = s:option(Button, "_kill")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Kill")
|
||||
o.inputstyle = "reset"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"kill")
|
||||
end
|
||||
|
||||
o = s:option(Button, "_export")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Export")
|
||||
o.inputstyle = "apply"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container_export/"..container_id))
|
||||
end
|
||||
|
||||
o = s:option(Button, "_upgrade")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Upgrade")
|
||||
o.inputstyle = "reload"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"upgrade")
|
||||
end
|
||||
|
||||
o = s:option(Button, "_duplicate")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Duplicate/Edit")
|
||||
o.inputstyle = "add"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer/duplicate/"..container_id))
|
||||
end
|
||||
|
||||
o = s:option(Button, "_remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle=translate("Remove")
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"remove")
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/container"
|
||||
|
||||
if action == "info" then
|
||||
res = dk.networks:list()
|
||||
if res.code < 300 then
|
||||
networks = res.body
|
||||
else
|
||||
return
|
||||
end
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
table_info = {
|
||||
["01name"] = {
|
||||
_key = translate("Name"),
|
||||
_value = container_info.Name:sub(2) or "-",
|
||||
_button=translate("Update")
|
||||
},
|
||||
["02id"] = {
|
||||
_key = translate("ID"),
|
||||
_value = container_info.Id or "-"
|
||||
},
|
||||
["03image"] = {
|
||||
_key = translate("Image"),
|
||||
_value = container_info.Config.Image .. "<br>" .. container_info.Image
|
||||
},
|
||||
["04status"] = {
|
||||
_key = translate("Status"),
|
||||
_value = container_info.State and container_info.State.Status or "-"
|
||||
},
|
||||
["05created"] = {
|
||||
_key = translate("Created"),
|
||||
_value = container_info.Created or "-"
|
||||
},
|
||||
}
|
||||
|
||||
if container_info.State.Status == "running" then
|
||||
table_info["06start"] = {
|
||||
_key = translate("Start Time"),
|
||||
_value = container_info.State and container_info.State.StartedAt or "-"
|
||||
}
|
||||
else
|
||||
table_info["06start"] = {
|
||||
_key = translate("Finish Time"),
|
||||
_value = container_info.State and container_info.State.FinishedAt or "-"
|
||||
}
|
||||
end
|
||||
|
||||
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["081user"] = {
|
||||
_key = translate("User"),
|
||||
_value = container_info.Config and (container_info.Config.User ~="" and container_info.Config.User or "-") 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 "-"
|
||||
}
|
||||
table_info["14device"] = {
|
||||
_key = translate("Device"),
|
||||
_value = get_device(container_info) or "-"
|
||||
}
|
||||
table_info["15tmpfs"] = {
|
||||
_key = translate("Tmpfs"),
|
||||
_value = get_tmpfs(container_info) or "-"
|
||||
}
|
||||
table_info["16dns"] = {
|
||||
_key = translate("DNS"),
|
||||
_value = get_dns(container_info) or "-"
|
||||
}
|
||||
table_info["17sysctl"] = {
|
||||
_key = translate("Sysctl"),
|
||||
_value = get_sysctl(container_info) or "-"
|
||||
}
|
||||
|
||||
info_networks = get_networks(container_info)
|
||||
list_networks = {}
|
||||
for _, v in ipairs (networks) do
|
||||
if v and 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")
|
||||
}
|
||||
|
||||
s = m:section(Table,table_info)
|
||||
s.nodescr=true
|
||||
s.formvalue=function(self, section)
|
||||
return table_info
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "_key", translate("Info"))
|
||||
o.width = "20%"
|
||||
|
||||
o = s:option(ListValue, "_value")
|
||||
o.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
|
||||
if k ~= "host" then
|
||||
self:value(k,v)
|
||||
end
|
||||
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
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section, value)
|
||||
table_info[section]._value=value
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:option(Value, "_opts")
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section, value)
|
||||
table_info[section]._opts=value
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
return value
|
||||
end
|
||||
o.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
|
||||
|
||||
o = s:option(Button, "_button")
|
||||
o.forcewrite = true
|
||||
o.render = function(self, section, scope)
|
||||
if table_info[section]._button and table_info[section]._value ~= nil then
|
||||
self.inputtitle=table_info[section]._button
|
||||
self.template = "cbi/button"
|
||||
self.inputstyle = "edit"
|
||||
Button.render(self, section, scope)
|
||||
else
|
||||
self.template = "cbi/dvalue"
|
||||
self.default=""
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
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({
|
||||
id = container_id,
|
||||
query = {
|
||||
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({
|
||||
id = container_id,
|
||||
body = {
|
||||
RestartPolicy = {
|
||||
Name = new_restart
|
||||
}
|
||||
}
|
||||
})
|
||||
elseif table_info[section]._key == translate("Network") then
|
||||
local _,_,leave_network
|
||||
|
||||
_, _, 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({
|
||||
name = leave_network,
|
||||
body = {
|
||||
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
|
||||
|
||||
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({
|
||||
name = connect_network,
|
||||
body = {
|
||||
Container = container_id,
|
||||
EndpointConfig= network_opiton
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
if res and res.code > 300 then
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
|
||||
else
|
||||
docker:clear_status()
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/info"))
|
||||
end
|
||||
elseif action == "resources" then
|
||||
s = m:section(SimpleSection)
|
||||
o = s:option( Value, "cpus",
|
||||
translate("CPUs"),
|
||||
translate("Number of CPUs. Number is a fractional number. 0.000 means no limit."))
|
||||
o.placeholder = "1.5"
|
||||
o.rmempty = true
|
||||
o.datatype="ufloat"
|
||||
o.default = container_info.HostConfig.NanoCpus / (10^9)
|
||||
|
||||
o = 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."))
|
||||
o.placeholder = "1024"
|
||||
o.rmempty = true
|
||||
o.datatype="uinteger"
|
||||
o.default = container_info.HostConfig.CpuShares
|
||||
|
||||
o = s:option(Value, "memory",
|
||||
translate("Memory"),
|
||||
translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M."))
|
||||
o.placeholder = "128m"
|
||||
o.rmempty = true
|
||||
o.default = container_info.HostConfig.Memory ~=0 and ((container_info.HostConfig.Memory / 1024 /1024) .. "M") or 0
|
||||
|
||||
o = s:option(Value, "blkioweight",
|
||||
translate("Block IO Weight"),
|
||||
translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000."))
|
||||
o.placeholder = "500"
|
||||
o.rmempty = true
|
||||
o.datatype="uinteger"
|
||||
o.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:write_status("Containers: update " .. container_id .. "...")
|
||||
local res = dk.containers:update({id = container_id, body = request_body})
|
||||
if res and res.code >= 300 then
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
|
||||
else
|
||||
docker:clear_status()
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/container/"..container_id.."/resources"))
|
||||
end
|
||||
end
|
||||
|
||||
elseif action == "file" then
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
s= m:section(SimpleSection)
|
||||
s.template = "dockerman/container_file_manager"
|
||||
s.container = container_id
|
||||
m.redirect = nil
|
||||
elseif action == "inspect" then
|
||||
s = m:section(SimpleSection)
|
||||
s.syslog = luci.jsonc.stringify(container_info, true)
|
||||
s.title = translate("Container Inspect")
|
||||
s.template = "dockerman/logs"
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
elseif action == "logs" then
|
||||
local logs = ""
|
||||
local query ={
|
||||
stdout = 1,
|
||||
stderr = 1,
|
||||
tail = 1000
|
||||
}
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
|
||||
logs = dk.containers:logs({id = container_id, query = query})
|
||||
if logs.code == 200 then
|
||||
s.syslog=logs.body
|
||||
else
|
||||
s.syslog="Get Logs ERROR\n"..logs.code..": "..logs.body
|
||||
end
|
||||
|
||||
s.title=translate("Container Logs")
|
||||
s.template = "dockerman/logs"
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
elseif action == "console" then
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
|
||||
local cmd_ttyd = luci.util.exec("command -v ttyd"):match("^.+ttyd") or nil
|
||||
|
||||
if cmd_docker and cmd_ttyd and container_info.State.Status == "running" then
|
||||
local cmd = "/bin/sh"
|
||||
local uid
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
|
||||
o = s:option(Value, "command", translate("Command"))
|
||||
o:value("/bin/sh", "/bin/sh")
|
||||
o:value("/bin/ash", "/bin/ash")
|
||||
o:value("/bin/bash", "/bin/bash")
|
||||
o.default = "/bin/sh"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section, value)
|
||||
cmd = value
|
||||
end
|
||||
|
||||
o = s:option(Value, "uid", translate("UID"))
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section, value)
|
||||
uid = value
|
||||
end
|
||||
|
||||
o = s:option(Button, "connect")
|
||||
o.render = function(self, section, scope)
|
||||
self.inputstyle = "add"
|
||||
self.title = " "
|
||||
self.inputtitle = translate("Connect")
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
o.write = function(self, section)
|
||||
local cmd_docker = luci.util.exec("command -v docker"):match("^.+docker") or nil
|
||||
local cmd_ttyd = luci.util.exec("command -v ttyd"):match("^.+ttyd") or nil
|
||||
|
||||
if not cmd_docker or not cmd_ttyd or cmd_docker:match("^%s+$") or cmd_ttyd:match("^%s+$") then
|
||||
return
|
||||
end
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
|
||||
local ttyd_ssl = uci:get("ttyd", "@ttyd[0]", "ssl")
|
||||
local ttyd_ssl_key = uci:get("ttyd", "@ttyd[0]", "ssl_key")
|
||||
local ttyd_ssl_cert = uci:get("ttyd", "@ttyd[0]", "ssl_cert")
|
||||
|
||||
if ttyd_ssl == "1" and ttyd_ssl_cert and ttyd_ssl_key then
|
||||
cmd_ttyd = string.format('%s -S -C %s -K %s', cmd_ttyd, ttyd_ssl_cert, ttyd_ssl_key)
|
||||
end
|
||||
|
||||
local pid = luci.util.trim(luci.util.exec("netstat -lnpt | grep :7682 | grep ttyd | tr -s ' ' | cut -d ' ' -f7 | cut -d'/' -f1"))
|
||||
if pid and pid ~= "" then
|
||||
luci.util.exec("kill -9 " .. pid)
|
||||
end
|
||||
|
||||
local hosts
|
||||
local remote = uci:get_bool("dockerd", "dockerman", "remote_endpoint") or false
|
||||
local host = nil
|
||||
local port = nil
|
||||
local socket = nil
|
||||
|
||||
if remote then
|
||||
host = uci:get("dockerd", "dockerman", "remote_host") or nil
|
||||
port = uci:get("dockerd", "dockerman", "remote_port") or nil
|
||||
else
|
||||
socket = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
|
||||
end
|
||||
|
||||
if remote and host and port then
|
||||
hosts = "tcp://" .. host .. ':'.. port
|
||||
elseif socket then
|
||||
hosts = "unix://" .. socket
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
if uid and uid ~= "" then
|
||||
uid = "-u " .. uid
|
||||
else
|
||||
uid = ""
|
||||
end
|
||||
|
||||
local start_cmd = string.format('%s -d 2 --once -p 7682 %s -H "%s" exec -it %s %s %s&', cmd_ttyd, cmd_docker, hosts, uid, container_id, cmd)
|
||||
|
||||
os.execute(start_cmd)
|
||||
|
||||
o = s:option(DummyValue, "console")
|
||||
o.container_id = container_id
|
||||
o.template = "dockerman/container_console"
|
||||
end
|
||||
end
|
||||
elseif action == "stats" then
|
||||
local response = dk.containers:top({id = container_id, query = {ps_args="-aux"}})
|
||||
local container_top
|
||||
|
||||
if response.code == 200 then
|
||||
container_top=response.body
|
||||
else
|
||||
response = dk.containers:top({id = container_id})
|
||||
if response.code == 200 then
|
||||
container_top=response.body
|
||||
end
|
||||
end
|
||||
|
||||
if type(container_top) == "table" then
|
||||
s = m:section(SimpleSection)
|
||||
s.container_id = container_id
|
||||
s.template = "dockerman/container_stats"
|
||||
table_stats = {
|
||||
cpu={
|
||||
key=translate("CPU Useage"),
|
||||
value='-'
|
||||
},
|
||||
memory={
|
||||
key=translate("Memory Useage"),
|
||||
value='-'
|
||||
}
|
||||
}
|
||||
|
||||
container_top = response.body
|
||||
s = m:section(Table, table_stats, translate("Stats"))
|
||||
s:option(DummyValue, "key", translate("Stats")).width="33%"
|
||||
s: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
|
284
luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua
Normal file
|
@ -0,0 +1,284 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local http = require "luci.http"
|
||||
local docker = require "luci.model.docker"
|
||||
|
||||
local m, s, o
|
||||
local images, networks, containers, res, lost_state
|
||||
local urlencode = luci.http.protocol and luci.http.protocol.urlencode or luci.util.urlencode
|
||||
local dk = docker.new()
|
||||
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
else
|
||||
res = dk.images:list()
|
||||
if res and res.code and res.code < 300 then
|
||||
images = res.body
|
||||
end
|
||||
|
||||
res = dk.networks:list()
|
||||
if res and res.code and res.code < 300 then
|
||||
networks = res.body
|
||||
end
|
||||
|
||||
res = dk.containers:list({
|
||||
query = {
|
||||
all = true
|
||||
}
|
||||
})
|
||||
if res and res.code and res.code < 300 then
|
||||
containers = res.body
|
||||
end
|
||||
end
|
||||
|
||||
function get_containers()
|
||||
local data = {}
|
||||
if type(containers) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i, v in ipairs(containers) do
|
||||
local index = (10^12 - v.Created) .. "_id_" .. 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]["_name"] = "<font color='green'>"..v.Names[1]:sub(2).."</font>"
|
||||
data[index]["_status"] = "<a href='"..luci.dispatcher.build_url("admin/docker/container/"..v.Id).."/stats'><font color='green'>".. data[index]["_status"] .. "</font>" .. "<br><font color='#9f9f9f' class='container_cpu_status'></font><br><font color='#9f9f9f' class='container_mem_status'></font><br><font color='#9f9f9f' class='container_network_status'></font></a>"
|
||||
else
|
||||
data[index]["_name"] = "<font color='red'>"..v.Names[1]:sub(2).."</font>"
|
||||
data[index]["_status"] = '<font class="container_not_running" color="red">'.. data[index]["_status"] .. "</font>"
|
||||
end
|
||||
|
||||
if (type(v.NetworkSettings) == "table" and type(v.NetworkSettings.Networks) == "table") then
|
||||
for networkname, netconfig in pairs(v.NetworkSettings.Networks) do
|
||||
data[index]["_network"] = (data[index]["_network"] ~= nil and (data[index]["_network"] .." | ") or "").. networkname .. (netconfig.IPAddress ~= "" and (": " .. netconfig.IPAddress) or "")
|
||||
end
|
||||
end
|
||||
|
||||
-- networkmode = v.HostConfig.NetworkMode ~= "default" and v.HostConfig.NetworkMode or "bridge"
|
||||
-- data[index]["_network"] = v.NetworkSettings.Networks[networkmode].IPAddress or nil
|
||||
-- local _, _, image = v.Image:find("^sha256:(.+)")
|
||||
-- if image ~= nil then
|
||||
-- image=image:sub(1,12)
|
||||
-- end
|
||||
|
||||
if v.Ports and next(v.Ports) ~= nil then
|
||||
data[index]["_ports"] = nil
|
||||
local ip = require "luci.ip"
|
||||
for _,v2 in ipairs(v.Ports) do
|
||||
-- display ipv4 only
|
||||
if ip.new(v2.IP or "0.0.0.0"):is4() then
|
||||
data[index]["_ports"] = (data[index]["_ports"] and (data[index]["_ports"] .. ", ") or "")
|
||||
.. ((v2.PublicPort and v2.Type and v2.Type == "tcp") and ('<a href="javascript:void(0);" onclick="window.open((window.location.origin.match(/^(.+):\\d+$/) && window.location.origin.match(/^(.+):\\d+$/)[1] || window.location.origin) + \':\' + '.. v2.PublicPort ..', \'_blank\');">') or "")
|
||||
.. (v2.PublicPort and (v2.PublicPort .. ":") or "") .. (v2.PrivatePort and (v2.PrivatePort .."/") or "") .. (v2.Type and v2.Type or "")
|
||||
.. ((v2.PublicPort and v2.Type and v2.Type == "tcp")and "</a>" or "")
|
||||
end
|
||||
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]["_id_name"] = '<a href='..luci.dispatcher.build_url("admin/docker/container/"..v.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. data[index]["_name"] .. "<br><font color='#9f9f9f'>ID: " .. data[index]["_id"]
|
||||
.. "</font></a><br>Image: " .. (data[index]["_image"] or "<none>")
|
||||
.. "<br><font color='#9f9f9f' class='container_size_".. v.Id .."'></font>"
|
||||
|
||||
if type(v.Mounts) == "table" and next(v.Mounts) then
|
||||
for _, v2 in pairs(v.Mounts) do
|
||||
if v2.Type ~= "volume" then
|
||||
local v_sorce_d, v_dest_d
|
||||
local v_sorce = ""
|
||||
local v_dest = ""
|
||||
for v_sorce_d in v2["Source"]:gmatch('[^/]+') do
|
||||
if v_sorce_d and #v_sorce_d > 12 then
|
||||
v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,8) .. ".."
|
||||
else
|
||||
v_sorce = v_sorce .."/".. v_sorce_d
|
||||
end
|
||||
end
|
||||
for v_dest_d in v2["Destination"]:gmatch('[^/]+') do
|
||||
if v_dest_d and #v_dest_d > 12 then
|
||||
v_dest = v_dest .. "/" .. v_dest_d:sub(1,8) .. ".."
|
||||
else
|
||||
v_dest = v_dest .."/".. v_dest_d
|
||||
end
|
||||
end
|
||||
data[index]["_mounts"] = (data[index]["_mounts"] and (data[index]["_mounts"] .. "<br>") or "") .. '<span title="'.. v2.Source.. "→" .. v2.Destination .. '" ><a href="'..luci.dispatcher.build_url("admin/docker/container/"..v.Id)..'/file?path='..v2["Destination"]..'">' .. v_sorce .. "→" .. v_dest..'</a></span>'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
data[index]["_image_id"] = v.ImageID:sub(8,20)
|
||||
data[index]["_command"] = v.Command
|
||||
end
|
||||
return data
|
||||
end
|
||||
|
||||
local container_list = not lost_state and get_containers() or {}
|
||||
|
||||
m = SimpleForm("docker",
|
||||
translate("Docker - Containers"),
|
||||
translate("This page displays all containers that have been created on the connected docker host."))
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
m:append(Template("dockerman/containers_running_stats"))
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(Table, container_list, translate("Containers"))
|
||||
s.nodescr=true
|
||||
s.config="containers"
|
||||
|
||||
o = s:option(Flag, "_selected","")
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
o.width = "1%"
|
||||
o.write=function(self, section, value)
|
||||
container_list[section]._selected = value
|
||||
end
|
||||
|
||||
-- o = s:option(DummyValue, "_id", translate("ID"))
|
||||
-- o.width="10%"
|
||||
|
||||
-- o = s:option(DummyValue, "_name", translate("Container Name"))
|
||||
-- o.rawhtml = true
|
||||
|
||||
o = s:option(DummyValue, "_id_name", translate("Container Info"))
|
||||
o.rawhtml = true
|
||||
o.width="15%"
|
||||
|
||||
o = s:option(DummyValue, "_status", translate("Status"))
|
||||
o.width="15%"
|
||||
o.rawhtml=true
|
||||
|
||||
o = s:option(DummyValue, "_network", translate("Network"))
|
||||
o.width="10%"
|
||||
|
||||
o = s:option(DummyValue, "_ports", translate("Ports"))
|
||||
o.width="5%"
|
||||
o.rawhtml = true
|
||||
o = s:option(DummyValue, "_mounts", translate("Mounts"))
|
||||
o.width="25%"
|
||||
o.rawhtml = true
|
||||
|
||||
-- o = s:option(DummyValue, "_image", translate("Image"))
|
||||
-- o.width="8%"
|
||||
|
||||
o = s:option(DummyValue, "_command", translate("Command"))
|
||||
o.width="15%"
|
||||
|
||||
local start_stop_remove = function(m, cmd)
|
||||
local container_selected = {}
|
||||
-- 遍历table中sectionid
|
||||
for k in pairs(container_list) do
|
||||
-- 得到选中项的名字
|
||||
if container_list[k]._selected == 1 then
|
||||
container_selected[#container_selected + 1] = container_list[k]["_id"]
|
||||
end
|
||||
end
|
||||
if #container_selected > 0 then
|
||||
local success = true
|
||||
|
||||
docker:clear_status()
|
||||
for _, cont in ipairs(container_selected) do
|
||||
docker:append_status("Containers: " .. cmd .. " " .. cont .. "...")
|
||||
local res = dk.containers[cmd](dk, {id = cont})
|
||||
if res and res.code and res.code >= 300 then
|
||||
success = false
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
|
||||
else
|
||||
docker:append_status("done\n")
|
||||
end
|
||||
end
|
||||
|
||||
if success then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
|
||||
end
|
||||
end
|
||||
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template="cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "_new")
|
||||
o.inputtitle = translate("Add")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "add"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "_start")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Start")
|
||||
o.inputstyle = "apply"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"start")
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "_restart")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Restart")
|
||||
o.inputstyle = "reload"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"restart")
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "_stop")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Stop")
|
||||
o.inputstyle = "reset"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"stop")
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "_kill")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Kill")
|
||||
o.inputstyle = "reset"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m,"kill")
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "_remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Remove")
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
start_stop_remove(m, "remove")
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
return m
|
284
luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua
Normal file
|
@ -0,0 +1,284 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
local dk = docker.new()
|
||||
|
||||
local containers, images, res, lost_state
|
||||
local m, s, o
|
||||
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
else
|
||||
res = dk.images:list()
|
||||
if res and res.code and res.code < 300 then
|
||||
images = res.body
|
||||
end
|
||||
|
||||
res = dk.containers:list({ query = { all = true } })
|
||||
if res and res.code and res.code < 300 then
|
||||
containers = res.body
|
||||
end
|
||||
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)
|
||||
data[index]["_id"] = '<a href="javascript:new_tag(\''..v.Id:sub(8,20)..'\')" class="dockerman-link" title="'..translate("New tag")..'">' .. v.Id:sub(8,20) .. '</a>'
|
||||
|
||||
if v.RepoTags and next(v.RepoTags)~=nil then
|
||||
for i, v1 in ipairs(v.RepoTags) do
|
||||
data[index]["_tags"] =(data[index]["_tags"] and ( data[index]["_tags"] .. "<br>" )or "") .. ((v1:match("<none>") or (#v.RepoTags == 1)) and v1 or ('<a href="javascript:un_tag(\''..v1..'\')" class="dockerman_link" title="'..translate("Remove tag")..'" >' .. v1 .. '</a>'))
|
||||
|
||||
if not data[index]["tag"] then
|
||||
data[index]["tag"] = v1
|
||||
end
|
||||
end
|
||||
else
|
||||
data[index]["_tags"] = v.RepoDigests[1] and v.RepoDigests[1]:match("^(.-)@.+")
|
||||
data[index]["_tags"] = (data[index]["_tags"] and data[index]["_tags"] or "<none>" ).. ":<none>"
|
||||
end
|
||||
|
||||
data[index]["_tags"] = data[index]["_tags"]:gsub("<none>","<none>")
|
||||
for ci,cv in ipairs(containers) do
|
||||
if v.Id == cv.ImageID then
|
||||
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
|
||||
'<a href='..luci.dispatcher.build_url("admin/docker/container/"..cv.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. cv.Names[1]:sub(2).."</a>"
|
||||
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 = not lost_state and get_images() or {}
|
||||
|
||||
m = SimpleForm("docker",
|
||||
translate("Docker - Images"),
|
||||
translate("On this page all images are displayed that are available on the system and with which a container can be created."))
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
|
||||
local pull_value={
|
||||
_image_tag_name="",
|
||||
_registry="index.docker.io"
|
||||
}
|
||||
|
||||
s = m:section(SimpleSection,
|
||||
translate("Pull Image"),
|
||||
translate("By entering a valid image name with the corresponding version, the docker image can be downloaded from the configured registry."))
|
||||
s.template="cbi/nullsection"
|
||||
|
||||
o = s:option(Value, "_image_tag_name")
|
||||
o.template = "dockerman/cbi/inlinevalue"
|
||||
o.placeholder="lisaac/luci:latest"
|
||||
o.write = function(self, section, value)
|
||||
local hastag = value:find(":")
|
||||
|
||||
if not hastag then
|
||||
value = value .. ":latest"
|
||||
end
|
||||
pull_value["_image_tag_name"] = value
|
||||
end
|
||||
|
||||
o = s:option(Button, "_pull")
|
||||
o.inputtitle= translate("Pull")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "add"
|
||||
o.disable = lost_state
|
||||
o.write = function(self, section)
|
||||
local tag = pull_value["_image_tag_name"]
|
||||
local json_stringify = luci.jsonc and luci.jsonc.stringify
|
||||
|
||||
if tag and tag ~= "" then
|
||||
docker:write_status("Images: " .. "pulling" .. " " .. tag .. "...\n")
|
||||
local res = dk.images:create({query = {fromImage=tag}}, docker.pull_image_show_status_cb)
|
||||
|
||||
if res and res.code and res.code == 200 and (res.body[#res.body] and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. tag)) then
|
||||
docker:clear_status()
|
||||
else
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
|
||||
end
|
||||
else
|
||||
docker:append_status("code: 400 please input the name of image name!")
|
||||
end
|
||||
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection,
|
||||
translate("Import Image"),
|
||||
translate("When pressing the Import button, both a local image can be loaded onto the system and a valid image tar can be downloaded from remote."))
|
||||
|
||||
o = s:option(DummyValue, "_image_import")
|
||||
o.template = "dockerman/images_import"
|
||||
o.disable = lost_state
|
||||
|
||||
s = m:section(Table, image_list, translate("Images overview"))
|
||||
|
||||
o = s:option(Flag, "_selected","")
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
o.write = function(self, section, value)
|
||||
image_list[section]._selected = value
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "_id", translate("ID"))
|
||||
o.rawhtml = true
|
||||
|
||||
o = s:option(DummyValue, "_tags", translate("RepoTags"))
|
||||
o.rawhtml = true
|
||||
|
||||
o = s:option(DummyValue, "_containers", translate("Containers"))
|
||||
o.rawhtml = true
|
||||
|
||||
o = s:option(DummyValue, "_size", translate("Size"))
|
||||
|
||||
o = s:option(DummyValue, "_created", translate("Created"))
|
||||
|
||||
local remove_action = function(force)
|
||||
local image_selected = {}
|
||||
|
||||
for k in pairs(image_list) do
|
||||
if image_list[k]._selected == 1 then
|
||||
image_selected[#image_selected+1] = (image_list[k]["_tags"]:match("<br>") or image_list[k]["_tags"]:match("<none>")) and image_list[k].id or image_list[k].tag
|
||||
end
|
||||
end
|
||||
|
||||
if next(image_selected) ~= nil then
|
||||
local success = true
|
||||
|
||||
docker:clear_status()
|
||||
for _, img in ipairs(image_selected) do
|
||||
local query
|
||||
docker:append_status("Images: " .. "remove" .. " " .. img .. "...")
|
||||
|
||||
if force then
|
||||
query = {force = true}
|
||||
end
|
||||
|
||||
local msg = dk.images:remove({
|
||||
id = img,
|
||||
query = query
|
||||
})
|
||||
if msg and msg.code ~= 200 then
|
||||
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
|
||||
success = false
|
||||
else
|
||||
docker:append_status("done\n")
|
||||
end
|
||||
end
|
||||
|
||||
if success then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/images"))
|
||||
end
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err = docker:read_status()
|
||||
s.err = s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template="cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "remove")
|
||||
o.inputtitle= translate("Remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
remove_action()
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "forceremove")
|
||||
o.inputtitle= translate("Force Remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
remove_action(true)
|
||||
end
|
||||
o.disable = lost_state
|
||||
|
||||
o = s:option(Button, "save")
|
||||
o.inputtitle= translate("Save")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "edit"
|
||||
o.disable = lost_state
|
||||
o.forcewrite = true
|
||||
o.write = function (self, section)
|
||||
local image_selected = {}
|
||||
|
||||
for k in pairs(image_list) do
|
||||
if image_list[k]._selected == 1 then
|
||||
image_selected[#image_selected + 1] = image_list[k].id
|
||||
end
|
||||
end
|
||||
|
||||
if next(image_selected) ~= nil then
|
||||
local names, first, show_name
|
||||
|
||||
for _, img in ipairs(image_selected) do
|
||||
names = names and (names .. "&names=".. img) or img
|
||||
end
|
||||
if #image_selected > 1 then
|
||||
show_name = "images"
|
||||
else
|
||||
show_name = image_selected[1]
|
||||
end
|
||||
local cb = function(res, chunk)
|
||||
if res and res.code and res.code == 200 then
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.header('Content-Disposition', 'inline; filename="'.. show_name .. '.tar"')
|
||||
luci.http.header('Content-Type', 'application\/x-tar')
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
else
|
||||
if not first then
|
||||
first = true
|
||||
luci.http.prepare_content("text/plain")
|
||||
end
|
||||
luci.ltn12.pump.all(chunk, luci.http.write)
|
||||
end
|
||||
end
|
||||
|
||||
docker:write_status("Images: " .. "save" .. " " .. table.concat(image_selected, "\n") .. "...")
|
||||
local msg = dk.images:get({query = {names = names}}, cb)
|
||||
if msg and msg.code and msg.code ~= 200 then
|
||||
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
|
||||
else
|
||||
docker:clear_status()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(Button, "load")
|
||||
o.inputtitle= translate("Load")
|
||||
o.template = "dockerman/images_load"
|
||||
o.inputstyle = "add"
|
||||
o.disable = lost_state
|
||||
|
||||
return m
|
159
luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua
Normal file
|
@ -0,0 +1,159 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
|
||||
local m, s, o
|
||||
local networks, dk, res, lost_state
|
||||
|
||||
dk = docker.new()
|
||||
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
else
|
||||
res = dk.networks:list()
|
||||
if res and res.code and res.code < 300 then
|
||||
networks = res.body
|
||||
end
|
||||
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 = not lost_state and get_networks() or {}
|
||||
|
||||
m = SimpleForm("docker",
|
||||
translate("Docker - Networks"),
|
||||
translate("This page displays all docker networks that have been created on the connected docker host."))
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
|
||||
s = m:section(Table, network_list, translate("Networks overview"))
|
||||
s.nodescr=true
|
||||
|
||||
o = s:option(Flag, "_selected","")
|
||||
o.template = "dockerman/cbi/xfvalue"
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
o.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
|
||||
o.write = function(self, section, value)
|
||||
network_list[section]._selected = value
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "_id", translate("ID"))
|
||||
|
||||
o = s:option(DummyValue, "_name", translate("Network Name"))
|
||||
|
||||
o = s:option(DummyValue, "_driver", translate("Driver"))
|
||||
|
||||
o = s:option(DummyValue, "_interface", translate("Parent Interface"))
|
||||
|
||||
o = s:option(DummyValue, "_subnet", translate("Subnet"))
|
||||
|
||||
o = s:option(DummyValue, "_gateway", translate("Gateway"))
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err = docker:read_status()
|
||||
s.err = s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template="cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "_new")
|
||||
o.inputtitle= translate("New")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.notitle=true
|
||||
o.inputstyle = "add"
|
||||
o.forcewrite = true
|
||||
o.disable = lost_state
|
||||
o.write = function(self, section)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
|
||||
end
|
||||
|
||||
o = s:option(Button, "_remove")
|
||||
o.inputtitle= translate("Remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.disable = lost_state
|
||||
o.write = function(self, section)
|
||||
local network_selected = {}
|
||||
local network_name_selected = {}
|
||||
local network_driver_selected = {}
|
||||
|
||||
for k in pairs(network_list) do
|
||||
if network_list[k]._selected == 1 then
|
||||
network_selected[#network_selected + 1] = network_list[k]._id
|
||||
network_name_selected[#network_name_selected + 1] = network_list[k]._name
|
||||
network_driver_selected[#network_driver_selected + 1] = network_list[k]._driver
|
||||
end
|
||||
end
|
||||
|
||||
if next(network_selected) ~= nil then
|
||||
local success = true
|
||||
docker:clear_status()
|
||||
|
||||
for ii, net in ipairs(network_selected) do
|
||||
docker:append_status("Networks: " .. "remove" .. " " .. net .. "...")
|
||||
local res = dk.networks["remove"](dk, {id = net})
|
||||
|
||||
if res and res.code and res.code >= 300 then
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
|
||||
success = false
|
||||
else
|
||||
docker:append_status("done\n")
|
||||
if network_driver_selected[ii] == "macvlan" then
|
||||
docker.remove_macvlan_interface(network_name_selected[ii])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if success then
|
||||
docker:clear_status()
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
923
luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua
Normal file
|
@ -0,0 +1,923 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
|
||||
local m, s, o
|
||||
|
||||
local dk = docker.new()
|
||||
|
||||
local cmd_line = table.concat(arg, '/')
|
||||
local images, networks
|
||||
local create_body = {}
|
||||
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
images = {}
|
||||
networks = {}
|
||||
else
|
||||
images = dk.images:list().body
|
||||
networks = dk.networks:list().body
|
||||
end
|
||||
|
||||
local is_quot_complete = function(str)
|
||||
local num = 0, w
|
||||
require "math"
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
num = 0
|
||||
for w in str:gmatch("\'") do
|
||||
num = num + 1
|
||||
end
|
||||
|
||||
if math.fmod(num, 2) ~= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function contains(list, x)
|
||||
for _, v in pairs(list) do
|
||||
if v == x then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local resolve_cli = function(cmd_line)
|
||||
local config = {
|
||||
advance = 1
|
||||
}
|
||||
|
||||
local key_no_val = {
|
||||
't',
|
||||
'd',
|
||||
'i',
|
||||
'tty',
|
||||
'rm',
|
||||
'read_only',
|
||||
'interactive',
|
||||
'init',
|
||||
'help',
|
||||
'detach',
|
||||
'privileged',
|
||||
'P',
|
||||
'publish_all',
|
||||
}
|
||||
|
||||
local key_with_val = {
|
||||
'sysctl',
|
||||
'add_host',
|
||||
'a',
|
||||
'attach',
|
||||
'blkio_weight_device',
|
||||
'cap_add',
|
||||
'cap_drop',
|
||||
'device',
|
||||
'device_cgroup_rule',
|
||||
'device_read_bps',
|
||||
'device_read_iops',
|
||||
'device_write_bps',
|
||||
'device_write_iops',
|
||||
'dns',
|
||||
'dns_option',
|
||||
'dns_search',
|
||||
'e',
|
||||
'env',
|
||||
'env_file',
|
||||
'expose',
|
||||
'group_add',
|
||||
'l',
|
||||
'label',
|
||||
'label_file',
|
||||
'link',
|
||||
'link_local_ip',
|
||||
'log_driver',
|
||||
'log_opt',
|
||||
'network_alias',
|
||||
'p',
|
||||
'publish',
|
||||
'security_opt',
|
||||
'storage_opt',
|
||||
'tmpfs',
|
||||
'v',
|
||||
'volume',
|
||||
'volumes_from',
|
||||
'blkio_weight',
|
||||
'cgroup_parent',
|
||||
'cidfile',
|
||||
'cpu_period',
|
||||
'cpu_quota',
|
||||
'cpu_rt_period',
|
||||
'cpu_rt_runtime',
|
||||
'c',
|
||||
'cpu_shares',
|
||||
'cpus',
|
||||
'cpuset_cpus',
|
||||
'cpuset_mems',
|
||||
'detach_keys',
|
||||
'disable_content_trust',
|
||||
'domainname',
|
||||
'entrypoint',
|
||||
'gpus',
|
||||
'health_cmd',
|
||||
'health_interval',
|
||||
'health_retries',
|
||||
'health_start_period',
|
||||
'health_timeout',
|
||||
'h',
|
||||
'hostname',
|
||||
'ip',
|
||||
'ip6',
|
||||
'ipc',
|
||||
'isolation',
|
||||
'kernel_memory',
|
||||
'mac_address',
|
||||
'm',
|
||||
'memory',
|
||||
'memory_reservation',
|
||||
'memory_swap',
|
||||
'memory_swappiness',
|
||||
'mount',
|
||||
'name',
|
||||
'network',
|
||||
'no_healthcheck',
|
||||
'oom_kill_disable',
|
||||
'oom_score_adj',
|
||||
'pid',
|
||||
'pids_limit',
|
||||
'restart',
|
||||
'runtime',
|
||||
'shm_size',
|
||||
'sig_proxy',
|
||||
'stop_signal',
|
||||
'stop_timeout',
|
||||
'ulimit',
|
||||
'u',
|
||||
'user',
|
||||
'userns',
|
||||
'uts',
|
||||
'volume_driver',
|
||||
'w',
|
||||
'workdir'
|
||||
}
|
||||
|
||||
local key_abb = {
|
||||
net='network',
|
||||
a='attach',
|
||||
c='cpu-shares',
|
||||
d='detach',
|
||||
e='env',
|
||||
h='hostname',
|
||||
i='interactive',
|
||||
l='label',
|
||||
m='memory',
|
||||
p='publish',
|
||||
P='publish_all',
|
||||
t='tty',
|
||||
u='user',
|
||||
v='volume',
|
||||
w='workdir'
|
||||
}
|
||||
|
||||
local key_with_list = {
|
||||
'sysctl',
|
||||
'add_host',
|
||||
'a',
|
||||
'attach',
|
||||
'blkio_weight_device',
|
||||
'cap_add',
|
||||
'cap_drop',
|
||||
'device',
|
||||
'device_cgroup_rule',
|
||||
'device_read_bps',
|
||||
'device_read_iops',
|
||||
'device_write_bps',
|
||||
'device_write_iops',
|
||||
'dns',
|
||||
'dns_optiondns_search',
|
||||
'e',
|
||||
'env',
|
||||
'env_file',
|
||||
'expose',
|
||||
'group_add',
|
||||
'l',
|
||||
'label',
|
||||
'label_file',
|
||||
'link',
|
||||
'link_local_ip',
|
||||
'log_opt',
|
||||
'network_alias',
|
||||
'p',
|
||||
'publish',
|
||||
'security_opt',
|
||||
'storage_opt',
|
||||
'tmpfs',
|
||||
'v',
|
||||
'volume',
|
||||
'volumes_from',
|
||||
}
|
||||
|
||||
local key = nil
|
||||
local _key = nil
|
||||
local val = nil
|
||||
local is_cmd = false
|
||||
|
||||
cmd_line = cmd_line:match("^DOCKERCLI%s+(.+)")
|
||||
for w in cmd_line:gmatch("[^%s]+") do
|
||||
if w =='\\' then
|
||||
elseif not key and not _key and not is_cmd then
|
||||
--key=val
|
||||
key, val = w:match("^%-%-([%lP%-]-)=(.+)")
|
||||
if not key then
|
||||
--key val
|
||||
key = w:match("^%-%-([%lP%-]+)")
|
||||
if not key then
|
||||
-- -v val
|
||||
key = w:match("^%-([%lP%-]+)")
|
||||
if key then
|
||||
-- for -dit
|
||||
if key:match("i") or key:match("t") or key:match("d") then
|
||||
if key:match("i") then
|
||||
config[key_abb["i"]] = true
|
||||
key:gsub("i", "")
|
||||
end
|
||||
if key:match("t") then
|
||||
config[key_abb["t"]] = true
|
||||
key:gsub("t", "")
|
||||
end
|
||||
if key:match("d") then
|
||||
config[key_abb["d"]] = true
|
||||
key:gsub("d", "")
|
||||
end
|
||||
if key:match("P") then
|
||||
config[key_abb["P"]] = true
|
||||
key:gsub("P", "")
|
||||
end
|
||||
if key == "" then
|
||||
key = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if key then
|
||||
key = key:gsub("-","_")
|
||||
key = key_abb[key] or key
|
||||
if contains(key_no_val, key) then
|
||||
config[key] = true
|
||||
val = nil
|
||||
key = nil
|
||||
elseif contains(key_with_val, key) then
|
||||
-- if key == "cap_add" then config.privileged = true end
|
||||
else
|
||||
key = nil
|
||||
val = nil
|
||||
end
|
||||
else
|
||||
config.image = w
|
||||
key = nil
|
||||
val = nil
|
||||
is_cmd = true
|
||||
end
|
||||
elseif (key or _key) and not is_cmd then
|
||||
if key == "mount" then
|
||||
-- we need resolve mount options here
|
||||
-- type=bind,source=/source,target=/app
|
||||
local _type = w:match("^type=([^,]+),") or "bind"
|
||||
local source = (_type ~= "tmpfs") and (w:match("source=([^,]+),") or w:match("src=([^,]+),")) or ""
|
||||
local target = w:match(",target=([^,]+)") or w:match(",dst=([^,]+)") or w:match(",destination=([^,]+)") or ""
|
||||
local ro = w:match(",readonly") and "ro" or nil
|
||||
|
||||
if source and target then
|
||||
if _type ~= "tmpfs" then
|
||||
local bind_propagation = (_type == "bind") and w:match(",bind%-propagation=([^,]+)") or nil
|
||||
val = source..":"..target .. ((ro or bind_propagation) and (":" .. (ro and ro or "") .. (((ro and bind_propagation) and "," or "") .. (bind_propagation and bind_propagation or ""))or ""))
|
||||
else
|
||||
local tmpfs_mode = w:match(",tmpfs%-mode=([^,]+)") or nil
|
||||
local tmpfs_size = w:match(",tmpfs%-size=([^,]+)") or nil
|
||||
key = "tmpfs"
|
||||
val = target .. ((tmpfs_mode or tmpfs_size) and (":" .. (tmpfs_mode and ("mode=" .. tmpfs_mode) or "") .. ((tmpfs_mode and tmpfs_size) and "," or "") .. (tmpfs_size and ("size=".. tmpfs_size) or "")) or "")
|
||||
if not config[key] then
|
||||
config[key] = {}
|
||||
end
|
||||
table.insert( config[key], val )
|
||||
key = nil
|
||||
val = nil
|
||||
end
|
||||
end
|
||||
else
|
||||
val = w
|
||||
end
|
||||
elseif is_cmd then
|
||||
config["command"] = (config["command"] and (config["command"] .. " " )or "") .. w
|
||||
end
|
||||
if (key or _key) and val then
|
||||
key = _key or key
|
||||
if contains(key_with_list, key) then
|
||||
if not config[key] then
|
||||
config[key] = {}
|
||||
end
|
||||
if _key then
|
||||
config[key][#config[key]] = config[key][#config[key]] .. " " .. w
|
||||
else
|
||||
table.insert( config[key], val )
|
||||
end
|
||||
if is_quot_complete(config[key][#config[key]]) then
|
||||
config[key][#config[key]] = config[key][#config[key]]:gsub("[\"\']", "")
|
||||
_key = nil
|
||||
else
|
||||
_key = key
|
||||
end
|
||||
else
|
||||
config[key] = (config[key] and (config[key] .. " ") or "") .. val
|
||||
if is_quot_complete(config[key]) then
|
||||
config[key] = config[key]:gsub("[\"\']", "")
|
||||
_key = nil
|
||||
else
|
||||
_key = key
|
||||
end
|
||||
end
|
||||
key = nil
|
||||
val = nil
|
||||
end
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
local default_config = {}
|
||||
|
||||
if cmd_line and cmd_line:match("^DOCKERCLI.+") then
|
||||
default_config = resolve_cli(cmd_line)
|
||||
elseif cmd_line and cmd_line:match("^duplicate/[^/]+$") then
|
||||
local container_id = cmd_line:match("^duplicate/(.+)")
|
||||
create_body = dk:containers_duplicate_config({id = container_id}) or {}
|
||||
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 = create_body.NetworkingConfig and create_body.NetworkingConfig.EndpointsConfig and next(create_body.NetworkingConfig.EndpointsConfig) or nil
|
||||
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.volume = create_body.HostConfig.Binds
|
||||
default_config.cap_add = create_body.HostConfig.CapAdd
|
||||
default_config.publish_all = create_body.HostConfig.PublishAllPorts
|
||||
|
||||
if create_body.HostConfig.Sysctls and type(create_body.HostConfig.Sysctls) == "table" then
|
||||
default_config.sysctl = {}
|
||||
for k, v in pairs(create_body.HostConfig.Sysctls) do
|
||||
table.insert( default_config.sysctl, k.."="..v )
|
||||
end
|
||||
end
|
||||
if create_body.HostConfig.LogConfig then
|
||||
if create_body.HostConfig.LogConfig.Config and type(create_body.HostConfig.LogConfig.Config) == "table" then
|
||||
default_config.log_opt = {}
|
||||
for k, v in pairs(create_body.HostConfig.LogConfig.Config) do
|
||||
table.insert( default_config.log_opt, k.."="..v )
|
||||
end
|
||||
end
|
||||
default_config.log_driver = create_body.HostConfig.LogConfig.Type or nil
|
||||
end
|
||||
|
||||
if create_body.HostConfig.PortBindings and type(create_body.HostConfig.PortBindings) == "table" then
|
||||
default_config.publish = {}
|
||||
for k, v in pairs(create_body.HostConfig.PortBindings) do
|
||||
for x, y in ipairs(v) do
|
||||
table.insert( default_config.publish, y.HostPort..":"..k:match("^(%d+)/.+").."/"..k:match("^%d+/(.+)") )
|
||||
end
|
||||
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.cpu_shares = create_body.HostConfig.CpuShares
|
||||
default_config.memory = create_body.HostConfig.Memory
|
||||
default_config.blkio_weight = 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
|
||||
|
||||
if create_body.HostConfig.Tmpfs and type(create_body.HostConfig.Tmpfs) == "table" then
|
||||
default_config.tmpfs = {}
|
||||
for k, v in pairs(create_body.HostConfig.Tmpfs) do
|
||||
table.insert( default_config.tmpfs, k .. (v~="" and ":" or "")..v )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
m = SimpleForm("docker", translate("Docker - Containers"))
|
||||
m.redirect = luci.dispatcher.build_url("admin", "docker", "containers")
|
||||
if lost_state then
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection, translate("Create new docker container"))
|
||||
s.addremove = true
|
||||
s.anonymous = true
|
||||
|
||||
o = s:option(DummyValue,"cmd_line", translate("Resolve CLI"))
|
||||
o.rawhtml = true
|
||||
o.template = "dockerman/newcontainer_resolve"
|
||||
|
||||
o = s:option(Value, "name", translate("Container Name"))
|
||||
o.rmempty = true
|
||||
o.default = default_config.name or nil
|
||||
|
||||
o = s:option(Flag, "interactive", translate("Interactive (-i)"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = default_config.interactive and 1 or 0
|
||||
|
||||
o = s:option(Flag, "tty", translate("TTY (-t)"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = default_config.tty and 1 or 0
|
||||
|
||||
o = s:option(Value, "image", translate("Docker Image"))
|
||||
o.rmempty = true
|
||||
o.default = default_config.image or nil
|
||||
for _, v in ipairs (images) do
|
||||
if v.RepoTags then
|
||||
o:value(v.RepoTags[1], v.RepoTags[1])
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(Flag, "_force_pull", translate("Always pull image first"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
|
||||
o = s:option(Flag, "privileged", translate("Privileged"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = default_config.privileged and 1 or 0
|
||||
|
||||
o = s:option(ListValue, "restart", translate("Restart Policy"))
|
||||
o.rmempty = true
|
||||
o:value("no", "No")
|
||||
o:value("unless-stopped", "Unless stopped")
|
||||
o:value("always", "Always")
|
||||
o:value("on-failure", "On failure")
|
||||
o.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
|
||||
|
||||
o = s:option(DynamicList, "link", translate("Links with other containers"))
|
||||
o.placeholder = "container_name:alias"
|
||||
o.rmempty = true
|
||||
o:depends("network", "bridge")
|
||||
o.default = default_config.link or nil
|
||||
|
||||
o = s:option(DynamicList, "dns", translate("Set custom DNS servers"))
|
||||
o.placeholder = "8.8.8.8"
|
||||
o.rmempty = true
|
||||
o.default = default_config.dns or nil
|
||||
|
||||
o = s:option(Value, "user",
|
||||
translate("User(-u)"),
|
||||
translate("The user that commands are run as inside the container.(format: name|uid[:group|gid])"))
|
||||
o.placeholder = "1000:1000"
|
||||
o.rmempty = true
|
||||
o.default = default_config.user or nil
|
||||
|
||||
o = s:option(DynamicList, "env",
|
||||
translate("Environmental Variable(-e)"),
|
||||
translate("Set environment variables to inside the container"))
|
||||
o.placeholder = "TZ=Asia/Shanghai"
|
||||
o.rmempty = true
|
||||
o.default = default_config.env or nil
|
||||
|
||||
o = s:option(DynamicList, "volume",
|
||||
translate("Bind Mount(-v)"),
|
||||
translate("Bind mount a volume"))
|
||||
o.placeholder = "/media:/media:slave"
|
||||
o.rmempty = true
|
||||
o.default = default_config.volume or nil
|
||||
|
||||
local d_publish = s:option(DynamicList, "publish",
|
||||
translate("Exposed Ports(-p)"),
|
||||
translate("Publish container's port(s) to the host"))
|
||||
d_publish.placeholder = "2200:22/tcp"
|
||||
d_publish.rmempty = true
|
||||
d_publish.default = default_config.publish or nil
|
||||
|
||||
o = s:option(Value, "command", translate("Run command"))
|
||||
o.placeholder = "/bin/sh init.sh"
|
||||
o.rmempty = true
|
||||
o.default = default_config.command or nil
|
||||
|
||||
o = s:option(Flag, "advance", translate("Advance"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = default_config.advance or 0
|
||||
|
||||
o = s:option(Value, "hostname",
|
||||
translate("Host Name"),
|
||||
translate("The hostname to use for the container"))
|
||||
o.rmempty = true
|
||||
o.default = default_config.hostname or nil
|
||||
o:depends("advance", 1)
|
||||
|
||||
o = s:option(Flag, "publish_all",
|
||||
translate("Exposed All Ports(-P)"),
|
||||
translate("Allocates an ephemeral host port for all of a container's exposed ports"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = default_config.publish_all and 1 or 0
|
||||
o:depends("advance", 1)
|
||||
|
||||
o = s:option(DynamicList, "device",
|
||||
translate("Device(--device)"),
|
||||
translate("Add host device to the container"))
|
||||
o.placeholder = "/dev/sda:/dev/xvdc:rwm"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.device or nil
|
||||
|
||||
o = s:option(DynamicList, "tmpfs",
|
||||
translate("Tmpfs(--tmpfs)"),
|
||||
translate("Mount tmpfs directory"))
|
||||
o.placeholder = "/run:rw,noexec,nosuid,size=65536k"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.tmpfs or nil
|
||||
|
||||
o = s:option(DynamicList, "sysctl",
|
||||
translate("Sysctl(--sysctl)"),
|
||||
translate("Sysctls (kernel parameters) options"))
|
||||
o.placeholder = "net.ipv4.ip_forward=1"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.sysctl or nil
|
||||
|
||||
o = s:option(DynamicList, "cap_add",
|
||||
translate("CAP-ADD(--cap-add)"),
|
||||
translate("A list of kernel capabilities to add to the container"))
|
||||
o.placeholder = "NET_ADMIN"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.cap_add or nil
|
||||
|
||||
o = s:option(Value, "cpus",
|
||||
translate("CPUs"),
|
||||
translate("Number of CPUs. Number is a fractional number. 0.000 means no limit"))
|
||||
o.placeholder = "1.5"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.datatype="ufloat"
|
||||
o.default = default_config.cpus or nil
|
||||
|
||||
o = s:option(Value, "cpu_shares",
|
||||
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"))
|
||||
o.placeholder = "1024"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.datatype="uinteger"
|
||||
o.default = default_config.cpu_shares or nil
|
||||
|
||||
o = s:option(Value, "memory",
|
||||
translate("Memory"),
|
||||
translate("Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M"))
|
||||
o.placeholder = "128m"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.memory or nil
|
||||
|
||||
o = s:option(Value, "blkio_weight",
|
||||
translate("Block IO Weight"),
|
||||
translate("Block IO weight (relative weight) accepts a weight value between 10 and 1000"))
|
||||
o.placeholder = "500"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.datatype="uinteger"
|
||||
o.default = default_config.blkio_weight or nil
|
||||
|
||||
o = s:option(Value, "log_driver",
|
||||
translate("Logging driver"),
|
||||
translate("The logging driver for the container"))
|
||||
o.placeholder = "json-file"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.log_driver or nil
|
||||
|
||||
o = s:option(DynamicList, "log_opt",
|
||||
translate("Log driver options"),
|
||||
translate("The logging configuration for this container"))
|
||||
o.placeholder = "max-size=1m"
|
||||
o.rmempty = true
|
||||
o:depends("advance", 1)
|
||||
o.default = default_config.log_opt 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_publish: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 publish_all = type(data.publish_all) == "number" and (data.publish_all == 1 and true or false) or default_config.publish_all 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 cap_add = data.cap_add
|
||||
local sysctl = {}
|
||||
local log_driver = data.log_driver
|
||||
|
||||
tmp = data.sysctl
|
||||
if type(tmp) == "table" then
|
||||
for i, v in ipairs(tmp) do
|
||||
local k,v1 = v:match("(.-)=(.+)")
|
||||
if k and v1 then
|
||||
sysctl[k]=v1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local log_opt = {}
|
||||
tmp = data.log_opt
|
||||
if type(tmp) == "table" then
|
||||
for i, v in ipairs(tmp) do
|
||||
local k,v1 = v:match("(.-)=(.+)")
|
||||
if k and v1 then
|
||||
log_opt[k]=v1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local network = data.network
|
||||
local ip = (network ~= "bridge" and network ~= "host" and network ~= "none") and data.ip or nil
|
||||
local volume = data.volume
|
||||
local memory = data.memory or nil
|
||||
local cpu_shares = data.cpu_shares or nil
|
||||
local cpus = data.cpus or nil
|
||||
local blkio_weight = data.blkio_weight or nil
|
||||
|
||||
local portbindings = {}
|
||||
local exposedports = {}
|
||||
|
||||
local tmpfs = {}
|
||||
tmp = data.tmpfs
|
||||
if type(tmp) == "table" then
|
||||
for i, v in ipairs(tmp)do
|
||||
local k= v:match("([^:]+)")
|
||||
local v1 = v:match(".-:([^:]+)") or ""
|
||||
if k 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"
|
||||
else
|
||||
t['PathOnHost'] = v
|
||||
t['PathInContainer'] = v
|
||||
t['CgroupPermissions'] = "rwm"
|
||||
end
|
||||
end
|
||||
|
||||
if next(t) ~= nil then
|
||||
table.insert( device, t )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
tmp = data.publish 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 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
|
||||
|
||||
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
|
||||
create_body.Env = env
|
||||
create_body.Image = image
|
||||
create_body.ExposedPorts = exposedports
|
||||
create_body.HostConfig = create_body.HostConfig or {}
|
||||
create_body.HostConfig.Dns = dns
|
||||
create_body.HostConfig.Binds = volume
|
||||
create_body.HostConfig.RestartPolicy = { Name = restart, MaximumRetryCount = 0 }
|
||||
create_body.HostConfig.Privileged = privileged and true or false
|
||||
create_body.HostConfig.PortBindings = portbindings
|
||||
create_body.HostConfig.Memory = memory and tonumber(memory)
|
||||
create_body.HostConfig.CpuShares = cpu_shares and tonumber(cpu_shares)
|
||||
create_body.HostConfig.NanoCPUs = cpus and tonumber(cpus) * 10 ^ 9
|
||||
create_body.HostConfig.BlkioWeight = blkio_weight and tonumber(blkio_weight)
|
||||
create_body.HostConfig.PublishAllPorts = publish_all
|
||||
|
||||
if create_body.HostConfig.NetworkMode ~= network then
|
||||
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
|
||||
for k, v in pairs (create_body.NetworkingConfig.EndpointsConfig) do
|
||||
if k == network and v.IPAMConfig and v.IPAMConfig.IPv4Address then
|
||||
v.IPAMConfig.IPv4Address = ip
|
||||
else
|
||||
create_body.NetworkingConfig.EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } }
|
||||
end
|
||||
break
|
||||
end
|
||||
else
|
||||
create_body.NetworkingConfig = { EndpointsConfig = { [network] = { IPAMConfig = { IPv4Address = ip } } } }
|
||||
end
|
||||
elseif not create_body.NetworkingConfig then
|
||||
create_body.NetworkingConfig = nil
|
||||
end
|
||||
|
||||
create_body["HostConfig"]["Tmpfs"] = tmpfs
|
||||
create_body["HostConfig"]["Devices"] = device
|
||||
create_body["HostConfig"]["Sysctls"] = sysctl
|
||||
create_body["HostConfig"]["CapAdd"] = cap_add
|
||||
create_body["HostConfig"]["LogConfig"] = {
|
||||
Config = log_opt,
|
||||
Type = log_driver
|
||||
}
|
||||
|
||||
if network == "bridge" then
|
||||
create_body["HostConfig"]["Links"] = link
|
||||
end
|
||||
|
||||
local pull_image = function(image)
|
||||
local json_stringify = luci.jsonc and luci.jsonc.stringify
|
||||
docker:append_status("Images: " .. "pulling" .. " " .. image .. "...\n")
|
||||
local res = dk.images:create({query = {fromImage=image}}, docker.pull_image_show_status_cb)
|
||||
if res and res.code and res.code == 200 and (res.body[#res.body] and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. image or res.body[#res.body].status == "Status: Image is up to date for ".. image)) then
|
||||
docker:append_status("done\n")
|
||||
else
|
||||
res.code = (res.code == 200) and 500 or res.code
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)).. "\n")
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/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
|
||||
|
||||
create_body = docker.clear_empty_tables(create_body)
|
||||
|
||||
docker:append_status("Container: " .. "create" .. " " .. name .. "...")
|
||||
local res = dk.containers:create({name = name, body = create_body})
|
||||
if res and res.code and res.code == 201 then
|
||||
docker:clear_status()
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/containers"))
|
||||
else
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message))
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newcontainer"))
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
258
luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua
Normal file
|
@ -0,0 +1,258 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
|
||||
local m, s, o
|
||||
|
||||
local dk = docker.new()
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
end
|
||||
|
||||
m = SimpleForm("docker", translate("Docker - Network"))
|
||||
m.redirect = luci.dispatcher.build_url("admin", "docker", "networks")
|
||||
if lost_state then
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
end
|
||||
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(SimpleSection, translate("Create new docker network"))
|
||||
s.addremove = true
|
||||
s.anonymous = true
|
||||
|
||||
o = s:option(Value, "name",
|
||||
translate("Network Name"),
|
||||
translate("Name of the network that can be selected during container creation"))
|
||||
o.rmempty = true
|
||||
|
||||
o = s:option(ListValue, "driver", translate("Driver"))
|
||||
o.rmempty = true
|
||||
o:value("bridge", translate("Bridge device"))
|
||||
o:value("macvlan", translate("MAC VLAN"))
|
||||
o:value("ipvlan", translate("IP VLAN"))
|
||||
o:value("overlay", translate("Overlay network"))
|
||||
|
||||
o = s:option(Value, "parent", translate("Base device"))
|
||||
o.rmempty = true
|
||||
o:depends("driver", "macvlan")
|
||||
local interfaces = luci.sys and luci.sys.net and luci.sys.net.devices() or {}
|
||||
for _, v in ipairs(interfaces) do
|
||||
o:value(v, v)
|
||||
end
|
||||
o.default="br-lan"
|
||||
o.placeholder="br-lan"
|
||||
|
||||
o = s:option(ListValue, "macvlan_mode", translate("Mode"))
|
||||
o.rmempty = true
|
||||
o:depends("driver", "macvlan")
|
||||
o.default="bridge"
|
||||
o:value("bridge", translate("Bridge (Support direct communication between MAC VLANs)"))
|
||||
o:value("private", translate("Private (Prevent communication between MAC VLANs)"))
|
||||
o:value("vepa", translate("VEPA (Virtual Ethernet Port Aggregator)"))
|
||||
o:value("passthru", translate("Pass-through (Mirror physical device to single MAC VLAN)"))
|
||||
|
||||
o = s:option(ListValue, "ipvlan_mode", translate("Ipvlan Mode"))
|
||||
o.rmempty = true
|
||||
o:depends("driver", "ipvlan")
|
||||
o.default="l3"
|
||||
o:value("l2", translate("L2 bridge"))
|
||||
o:value("l3", translate("L3 bridge"))
|
||||
|
||||
o = s:option(Flag, "ingress",
|
||||
translate("Ingress"),
|
||||
translate("Ingress network is the network which provides the routing-mesh in swarm mode"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
o:depends("driver", "overlay")
|
||||
|
||||
o = s:option(DynamicList, "options", translate("Options"))
|
||||
o.rmempty = true
|
||||
o.placeholder="com.docker.network.driver.mtu=1500"
|
||||
|
||||
o = s:option(Flag, "internal", translate("Internal"), translate("Restrict external access to the network"))
|
||||
o.rmempty = true
|
||||
o:depends("driver", "overlay")
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
|
||||
if nixio.fs.access("/etc/config/network") and nixio.fs.access("/etc/config/firewall")then
|
||||
o = s:option(Flag, "op_macvlan", translate("Create macvlan interface"), translate("Auto create macvlan interface in Openwrt"))
|
||||
o:depends("driver", "macvlan")
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 1
|
||||
end
|
||||
|
||||
o = s:option(Value, "subnet", translate("Subnet"))
|
||||
o.rmempty = true
|
||||
o.placeholder="10.1.0.0/16"
|
||||
o.datatype="ip4addr"
|
||||
|
||||
o = s:option(Value, "gateway", translate("Gateway"))
|
||||
o.rmempty = true
|
||||
o.placeholder="10.1.1.1"
|
||||
o.datatype="ip4addr"
|
||||
|
||||
o = s:option(Value, "ip_range", translate("IP range"))
|
||||
o.rmempty = true
|
||||
o.placeholder="10.1.1.0/24"
|
||||
o.datatype="ip4addr"
|
||||
|
||||
o = s:option(DynamicList, "aux_address", translate("Exclude IPs"))
|
||||
o.rmempty = true
|
||||
o.placeholder="my-route=10.1.1.1"
|
||||
|
||||
o = s:option(Flag, "ipv6", translate("Enable IPv6"))
|
||||
o.rmempty = true
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
|
||||
o = s:option(Value, "subnet6", translate("IPv6 Subnet"))
|
||||
o.rmempty = true
|
||||
o.placeholder="fe80::/10"
|
||||
o.datatype="ip6addr"
|
||||
o:depends("ipv6", 1)
|
||||
|
||||
o = s:option(Value, "gateway6", translate("IPv6 Gateway"))
|
||||
o.rmempty = true
|
||||
o.placeholder="fe80::1"
|
||||
o.datatype="ip6addr"
|
||||
o:depends("ipv6", 1)
|
||||
|
||||
m.handle = function(self, state, data)
|
||||
if state == FORM_VALID then
|
||||
local name = data.name
|
||||
local driver = data.driver
|
||||
|
||||
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 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
|
||||
|
||||
create_body = docker.clear_empty_tables(create_body)
|
||||
docker:write_status("Network: " .. "create" .. " " .. create_body.Name .. "...")
|
||||
|
||||
local res = dk.networks:create({
|
||||
body = create_body
|
||||
})
|
||||
|
||||
if res and res.code == 201 then
|
||||
docker:write_status("Network: " .. "create macvlan interface...")
|
||||
res = dk.networks:inspect({
|
||||
name = create_body.Name
|
||||
})
|
||||
|
||||
if driver == "macvlan" and
|
||||
data.op_macvlan ~= 0 and
|
||||
res and
|
||||
res.code and
|
||||
res.code == 200 and
|
||||
res.body and
|
||||
res.body.IPAM and
|
||||
res.body.IPAM.Config and
|
||||
res.body.IPAM.Config[1] and
|
||||
res.body.IPAM.Config[1].Gateway and
|
||||
res.body.IPAM.Config[1].Subnet then
|
||||
|
||||
docker.create_macvlan_interface(data.name,
|
||||
data.parent,
|
||||
res.body.IPAM.Config[1].Gateway,
|
||||
res.body.IPAM.Config[1].Subnet)
|
||||
end
|
||||
|
||||
docker:clear_status()
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/networks"))
|
||||
else
|
||||
docker:append_status("code:" .. res.code.." ".. (res.body.message and res.body.message or res.message).. "\n")
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/newnetwork"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
151
luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua
Normal file
|
@ -0,0 +1,151 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
|
||||
local m, s, o, lost_state
|
||||
local dk = docker.new()
|
||||
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
end
|
||||
|
||||
m = SimpleForm("dockerd",
|
||||
translate("Docker - Overview"),
|
||||
translate("An overview with the relevant data is displayed here with which the LuCI docker client is connected.")
|
||||
..
|
||||
" " ..
|
||||
[[<a href="https://github.com/lisaac/luci-app-dockerman" target="_blank">]] ..
|
||||
translate("Github") ..
|
||||
[[</a>]])
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
|
||||
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='-'}
|
||||
docker_info_table['9RegistryMirrors'] = {_key=translate("Registry Mirrors"),_value='-'}
|
||||
|
||||
if nixio.fs.access("/usr/bin/dockerd") and not uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template = "cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "_start")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = lost_state and translate("Start") or translate("Stop")
|
||||
o.inputstyle = lost_state and "add" or "remove"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
docker:clear_status()
|
||||
|
||||
if lost_state then
|
||||
docker:append_status("Docker daemon: starting")
|
||||
luci.util.exec("/etc/init.d/dockerd start")
|
||||
luci.util.exec("sleep 5")
|
||||
luci.util.exec("/etc/init.d/dockerman start")
|
||||
|
||||
else
|
||||
docker:append_status("Docker daemon: stopping")
|
||||
luci.util.exec("/etc/init.d/dockerd stop")
|
||||
end
|
||||
docker:clear_status()
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/overview"))
|
||||
end
|
||||
|
||||
o = s:option(Button, "_restart")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputtitle = translate("Restart")
|
||||
o.inputstyle = "reload"
|
||||
o.forcewrite = true
|
||||
o.write = function(self, section)
|
||||
docker:clear_status()
|
||||
docker:append_status("Docker daemon: restarting")
|
||||
luci.util.exec("/etc/init.d/dockerd restart")
|
||||
luci.util.exec("sleep 5")
|
||||
luci.util.exec("/etc/init.d/dockerman start")
|
||||
docker:clear_status()
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/overview"))
|
||||
end
|
||||
end
|
||||
|
||||
s = m:section(Table, docker_info_table)
|
||||
s:option(DummyValue, "_key", translate("Info"))
|
||||
s:option(DummyValue, "_value")
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/overview"
|
||||
|
||||
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("dockerd", "dockerman", "socket_path")
|
||||
if not lost_state then
|
||||
local containers_list = dk.containers:list({query = {all=true}}).body
|
||||
local images_list = dk.images:list().body
|
||||
local vol = dk.volumes:list()
|
||||
local volumes_list = vol and vol.body and vol.body.Volumes or {}
|
||||
local networks_list = dk.networks:list().body or {}
|
||||
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 = docker.byte_format(docker_info.body.MemTotal)
|
||||
if docker_info.body.DockerRootDir then
|
||||
local statvfs = nixio.fs.statvfs(docker_info.body.DockerRootDir)
|
||||
local size = statvfs and (statvfs.bavail * statvfs.bsize) or 0
|
||||
docker_info_table['7DockerRootDir']._value = docker_info.body.DockerRootDir .. " (" .. tostring(docker.byte_format(size)) .. " " .. translate("Available") .. ")"
|
||||
end
|
||||
|
||||
docker_info_table['8IndexServerAddress']._value = docker_info.body.IndexServerAddress
|
||||
for i, v in ipairs(docker_info.body.RegistryConfig.Mirrors) do
|
||||
docker_info_table['9RegistryMirrors']._value = docker_info_table['9RegistryMirrors']._value == "-" and v or (docker_info_table['9RegistryMirrors']._value .. ", " .. v)
|
||||
end
|
||||
|
||||
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(#images_list)
|
||||
s.networks_total = tostring(#networks_list)
|
||||
s.volumes_total = tostring(#volumes_list)
|
||||
else
|
||||
docker_info_table['3ServerVersion']._value = translate("Can NOT connect to docker daemon, please check!!")
|
||||
end
|
||||
|
||||
return m
|
142
luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua
Normal file
|
@ -0,0 +1,142 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.model.docker"
|
||||
local dk = docker.new()
|
||||
|
||||
local m, s, o
|
||||
|
||||
local res, containers, volumes, lost_state
|
||||
|
||||
function get_volumes()
|
||||
local data = {}
|
||||
for i, v in ipairs(volumes) do
|
||||
local index = v.Name
|
||||
data[index]={}
|
||||
data[index]["_selected"] = 0
|
||||
data[index]["_nameraw"] = v.Name
|
||||
data[index]["_name"] = v.Name:sub(1,12)
|
||||
|
||||
for ci,cv in ipairs(containers) do
|
||||
if cv.Mounts and type(cv.Mounts) ~= "table" then
|
||||
break
|
||||
end
|
||||
for vi, vv in ipairs(cv.Mounts) do
|
||||
if v.Name == vv.Name then
|
||||
data[index]["_containers"] = (data[index]["_containers"] and (data[index]["_containers"] .. " | ") or "")..
|
||||
'<a href='..luci.dispatcher.build_url("admin/docker/container/"..cv.Id)..' class="dockerman_link" title="'..translate("Container detail")..'">'.. cv.Names[1]:sub(2)..'</a>'
|
||||
end
|
||||
end
|
||||
end
|
||||
data[index]["_driver"] = v.Driver
|
||||
data[index]["_mountpoint"] = nil
|
||||
|
||||
for v1 in v.Mountpoint:gmatch('[^/]+') do
|
||||
if v1 == index then
|
||||
data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..."
|
||||
else
|
||||
data[index]["_mountpoint"] = (data[index]["_mountpoint"] and data[index]["_mountpoint"] or "").."/".. v1
|
||||
end
|
||||
end
|
||||
data[index]["_created"] = v.CreatedAt
|
||||
data[index]["_size"] = "<font class='volume_size_" .. v.Name .. "'>-</font>"
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
if dk:_ping().code ~= 200 then
|
||||
lost_state = true
|
||||
else
|
||||
res = dk.volumes:list()
|
||||
if res and res.code and res.code <300 then
|
||||
volumes = res.body.Volumes
|
||||
end
|
||||
|
||||
res = dk.containers:list({
|
||||
query = {
|
||||
all=true
|
||||
}
|
||||
})
|
||||
if res and res.code and res.code <300 then
|
||||
containers = res.body
|
||||
end
|
||||
end
|
||||
|
||||
local volume_list = not lost_state and get_volumes() or {}
|
||||
|
||||
m = SimpleForm("docker", translate("Docker - Volumes"))
|
||||
m.submit=false
|
||||
m.reset=false
|
||||
m:append(Template("dockerman/volume_size"))
|
||||
|
||||
s = m:section(Table, volume_list, translate("Volumes overview"))
|
||||
|
||||
o = s:option(Flag, "_selected","")
|
||||
o.disabled = 0
|
||||
o.enabled = 1
|
||||
o.default = 0
|
||||
o.write = function(self, section, value)
|
||||
volume_list[section]._selected = value
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "_name", translate("Name"))
|
||||
o = s:option(DummyValue, "_driver", translate("Driver"))
|
||||
o = s:option(DummyValue, "_containers", translate("Containers"))
|
||||
o.rawhtml = true
|
||||
o = s:option(DummyValue, "_mountpoint", translate("Mount Point"))
|
||||
o = s:option(DummyValue, "_size", translate("Size"))
|
||||
o.rawhtml = true
|
||||
o = s:option(DummyValue, "_created", translate("Created"))
|
||||
|
||||
s = m:section(SimpleSection)
|
||||
s.template = "dockerman/apply_widget"
|
||||
s.err=docker:read_status()
|
||||
s.err=s.err and s.err:gsub("\n","<br>"):gsub(" "," ")
|
||||
if s.err then
|
||||
docker:clear_status()
|
||||
end
|
||||
|
||||
s = m:section(Table,{{}})
|
||||
s.notitle=true
|
||||
s.rowcolors=false
|
||||
s.template="cbi/nullsection"
|
||||
|
||||
o = s:option(Button, "remove")
|
||||
o.inputtitle= translate("Remove")
|
||||
o.template = "dockerman/cbi/inlinebutton"
|
||||
o.inputstyle = "remove"
|
||||
o.forcewrite = true
|
||||
o.disable = lost_state
|
||||
o.write = function(self, section)
|
||||
local volume_selected = {}
|
||||
|
||||
for k in pairs(volume_list) do
|
||||
if volume_list[k]._selected == 1 then
|
||||
volume_selected[#volume_selected+1] = k
|
||||
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, {id = vol})
|
||||
if msg and msg.code and msg.code ~= 204 then
|
||||
docker:append_status("code:" .. msg.code.." ".. (msg.body.message and msg.body.message or msg.message).. "\n")
|
||||
success = false
|
||||
else
|
||||
docker:append_status("done\n")
|
||||
end
|
||||
end
|
||||
|
||||
if success then
|
||||
docker:clear_status()
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/docker/volumes"))
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
507
luci-app-dockerman/luasrc/model/docker.lua
Normal file
|
@ -0,0 +1,507 @@
|
|||
--[[
|
||||
LuCI - Lua Configuration Interface
|
||||
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-dockerman>
|
||||
]]--
|
||||
|
||||
local docker = require "luci.docker"
|
||||
local fs = require "nixio.fs"
|
||||
local uci = (require "luci.model.uci").cursor()
|
||||
|
||||
local _docker = {}
|
||||
_docker.options = {}
|
||||
|
||||
--pull image and return iamge id
|
||||
local update_image = function(self, image_name)
|
||||
local json_stringify = luci.jsonc and luci.jsonc.stringify
|
||||
_docker:append_status("Images: " .. "pulling" .. " " .. image_name .. "...\n")
|
||||
local res = self.images:create({query = {fromImage=image_name}}, _docker.pull_image_show_status_cb)
|
||||
|
||||
if res and res.code and res.code == 200 and (#res.body > 0 and not res.body[#res.body].error and res.body[#res.body].status and (res.body[#res.body].status == "Status: Downloaded newer image for ".. image_name)) then
|
||||
_docker:append_status("done\n")
|
||||
else
|
||||
res.body.message = res.body[#res.body] and res.body[#res.body].error or (res.body.message or res.message)
|
||||
end
|
||||
|
||||
new_image_id = self.images:inspect({name = 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
|
||||
res[k1] = v1
|
||||
end
|
||||
end
|
||||
|
||||
return next(res) ~= nil and res or nil
|
||||
end
|
||||
|
||||
_docker.clear_empty_tables = function ( t )
|
||||
local k, v
|
||||
|
||||
if next(t) == nil then
|
||||
t = nil
|
||||
else
|
||||
for k, v in pairs(t) do
|
||||
if type(v) == 'table' then
|
||||
t[k] = _docker.clear_empty_tables(v)
|
||||
if t[k] and next(t[k]) == nil then
|
||||
t[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
local get_config = function(container_config, image_config)
|
||||
local config = container_config.Config
|
||||
local old_host_config = container_config.HostConfig
|
||||
local old_network_setting = container_config.NetworkSettings.Networks or {}
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
local host_config = old_host_config
|
||||
host_config.Mounts = {}
|
||||
for i, v in ipairs(container_config.Mounts) do
|
||||
if v.Type == "volume" then
|
||||
table.insert(host_config.Mounts, {
|
||||
Type = v.Type,
|
||||
Target = v.Destination,
|
||||
Source = v.Source:match("([^/]+)\/_data"),
|
||||
BindOptions = (v.Type == "bind") and {Propagation = v.Propagation} or nil,
|
||||
ReadOnly = not v.RW
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local create_body = config
|
||||
create_body["HostConfig"] = host_config
|
||||
create_body["NetworkingConfig"] = {EndpointsConfig = network_setting}
|
||||
create_body = _docker.clear_empty_tables(create_body) or {}
|
||||
extra_network = _docker.clear_empty_tables(extra_network) or {}
|
||||
|
||||
return create_body, extra_network
|
||||
end
|
||||
|
||||
local upgrade = function(self, request)
|
||||
_docker:clear_status()
|
||||
|
||||
local container_info = self.containers:inspect({id = request.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 image_id, res = update_image(self, image_name)
|
||||
if res and res.code and res.code ~= 200 then
|
||||
return res
|
||||
end
|
||||
|
||||
if image_id == old_image_id then
|
||||
return {code = 305, body = {message = "Already up to date"}}
|
||||
end
|
||||
|
||||
local t = os.date("%Y%m%d%H%M%S")
|
||||
_docker:append_status("Container: rename" .. " " .. container_name .. " to ".. container_name .. "_old_".. t .. "...")
|
||||
res = self.containers:rename({name = container_name, query = { name = container_name .. "_old_" ..t }})
|
||||
if res and res.code and res.code < 300 then
|
||||
_docker:append_status("done\n")
|
||||
else
|
||||
return res
|
||||
end
|
||||
|
||||
local image_config = self.images:inspect({id = old_image_id}).body.Config
|
||||
local create_body, extra_network = get_config(container_info.body, image_config)
|
||||
|
||||
-- create new container
|
||||
_docker:append_status("Container: Create" .. " " .. container_name .. "...")
|
||||
create_body = _docker.clear_empty_tables(create_body)
|
||||
res = self.containers:create({name = container_name, body = create_body})
|
||||
if res and res.code and res.code > 300 then
|
||||
return res
|
||||
end
|
||||
_docker:append_status("done\n")
|
||||
|
||||
-- extra networks need to network connect action
|
||||
for k, v in pairs(extra_network) do
|
||||
_docker:append_status("Networks: Connect" .. " " .. container_name .. "...")
|
||||
res = self.networks:connect({id = k, body = {Container = container_name, EndpointConfig = v}})
|
||||
if res and res.code and res.code > 300 then
|
||||
return res
|
||||
end
|
||||
_docker:append_status("done\n")
|
||||
end
|
||||
|
||||
_docker:append_status("Container: " .. "Stop" .. " " .. container_name .. "_old_".. t .. "...")
|
||||
res = self.containers:stop({name = container_name .. "_old_" ..t })
|
||||
if res and res.code and res.code < 305 then
|
||||
_docker:append_status("done\n")
|
||||
else
|
||||
return res
|
||||
end
|
||||
|
||||
_docker:append_status("Container: " .. "Start" .. " " .. container_name .. "...")
|
||||
res = self.containers:start({name = container_name})
|
||||
if res and res.code and res.code < 305 then
|
||||
_docker:append_status("done\n")
|
||||
else
|
||||
return res
|
||||
end
|
||||
|
||||
_docker:clear_status()
|
||||
return res
|
||||
end
|
||||
|
||||
local duplicate_config = function (self, request)
|
||||
local container_info = self.containers:inspect({id = request.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 image_config = self.images:inspect({id = old_image_id}).body.Config
|
||||
|
||||
return get_config(container_info.body, image_config)
|
||||
end
|
||||
|
||||
_docker.new = function()
|
||||
local host = nil
|
||||
local port = nil
|
||||
local socket_path = nil
|
||||
local debug_path = nil
|
||||
|
||||
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
host = uci:get("dockerd", "dockerman", "remote_host") or nil
|
||||
port = uci:get("dockerd", "dockerman", "remote_port") or nil
|
||||
else
|
||||
socket_path = uci:get("dockerd", "dockerman", "socket_path") or "/var/run/docker.sock"
|
||||
end
|
||||
|
||||
local debug = uci:get_bool("dockerd", "dockerman", "debug")
|
||||
if debug then
|
||||
debug_path = uci:get("dockerd", "dockerman", "debug_path") or "/tmp/.docker_debug"
|
||||
end
|
||||
|
||||
local status_path = uci:get("dockerd", "dockerman", "status_path") or "/tmp/.docker_action_status"
|
||||
|
||||
_docker.options = {
|
||||
host = host,
|
||||
port = port,
|
||||
socket_path = socket_path,
|
||||
debug = debug,
|
||||
debug_path = debug_path,
|
||||
status_path = status_path
|
||||
}
|
||||
|
||||
local _new = docker.new(_docker.options)
|
||||
_new.containers_upgrade = upgrade
|
||||
_new.containers_duplicate_config = duplicate_config
|
||||
|
||||
return _new
|
||||
end
|
||||
|
||||
_docker.options.status_path = uci:get("dockerd", "dockerman", "status_path") or "/tmp/.docker_action_status"
|
||||
|
||||
_docker.append_status=function(self,val)
|
||||
if not val then
|
||||
return
|
||||
end
|
||||
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.write_status=function(self,val)
|
||||
if not val then
|
||||
return
|
||||
end
|
||||
local file_docker_action_status=io.open(self.options.status_path, "w+")
|
||||
file_docker_action_status:write(val)
|
||||
file_docker_action_status:close()
|
||||
end
|
||||
|
||||
_docker.read_status=function(self)
|
||||
return fs.readfile(self.options.status_path)
|
||||
end
|
||||
|
||||
_docker.clear_status=function(self)
|
||||
fs.remove(self.options.status_path)
|
||||
end
|
||||
|
||||
local status_cb = function(res, source, handler)
|
||||
res.body = res.body or {}
|
||||
while true do
|
||||
local chunk = source()
|
||||
if chunk then
|
||||
--standard output to res.body
|
||||
table.insert(res.body, chunk)
|
||||
handler(chunk)
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--{"status":"Pulling from library\/debian","id":"latest"}
|
||||
--{"status":"Pulling fs layer","progressDetail":[],"id":"50e431f79093"}
|
||||
--{"status":"Downloading","progressDetail":{"total":50381971,"current":2029978},"id":"50e431f79093","progress":"[==> ] 2.03MB\/50.38MB"}
|
||||
--{"status":"Download complete","progressDetail":[],"id":"50e431f79093"}
|
||||
--{"status":"Extracting","progressDetail":{"total":50381971,"current":17301504},"id":"50e431f79093","progress":"[=================> ] 17.3MB\/50.38MB"}
|
||||
--{"status":"Pull complete","progressDetail":[],"id":"50e431f79093"}
|
||||
--{"status":"Digest: sha256:a63d0b2ecbd723da612abf0a8bdb594ee78f18f691d7dc652ac305a490c9b71a"}
|
||||
--{"status":"Status: Downloaded newer image for debian:latest"}
|
||||
_docker.pull_image_show_status_cb = function(res, source)
|
||||
return status_cb(res, source, function(chunk)
|
||||
local json_parse = luci.jsonc.parse
|
||||
local step = json_parse(chunk)
|
||||
if type(step) == "table" then
|
||||
local buf = _docker:read_status()
|
||||
local num = 0
|
||||
local str = '\t' .. (step.id and (step.id .. ": ") or "") .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n"
|
||||
if step.id then
|
||||
buf, num = buf:gsub("\t"..step.id .. ": .-\n", str)
|
||||
end
|
||||
if num == 0 then
|
||||
buf = buf .. str
|
||||
end
|
||||
_docker:write_status(buf)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--{"status":"Downloading from https://downloads.openwrt.org/releases/19.07.0/targets/x86/64/openwrt-19.07.0-x86-64-generic-rootfs.tar.gz"}
|
||||
--{"status":"Importing","progressDetail":{"current":1572391,"total":3821714},"progress":"[====================\u003e ] 1.572MB/3.822MB"}
|
||||
--{"status":"sha256:d5304b58e2d8cc0a2fd640c05cec1bd4d1229a604ac0dd2909f13b2b47a29285"}
|
||||
_docker.import_image_show_status_cb = function(res, source)
|
||||
return status_cb(res, source, function(chunk)
|
||||
local json_parse = luci.jsonc.parse
|
||||
local step = json_parse(chunk)
|
||||
if type(step) == "table" then
|
||||
local buf = _docker:read_status()
|
||||
local num = 0
|
||||
local str = '\t' .. (step.status and step.status or "") .. (step.progress and (" " .. step.progress) or "").."\n"
|
||||
if step.status then
|
||||
buf, num = buf:gsub("\t"..step.status .. " .-\n", str)
|
||||
end
|
||||
if num == 0 then
|
||||
buf = buf .. str
|
||||
end
|
||||
_docker:write_status(buf)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
_docker.create_macvlan_interface = function(name, device, gateway, subnet)
|
||||
if not fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then
|
||||
return
|
||||
end
|
||||
|
||||
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
return
|
||||
end
|
||||
|
||||
local ip = require "luci.ip"
|
||||
local if_name = "docker_"..name
|
||||
local dev_name = "macvlan_"..name
|
||||
local net_mask = tostring(ip.new(subnet):mask())
|
||||
local lan_interfaces
|
||||
|
||||
-- add macvlan device
|
||||
uci:delete("network", dev_name)
|
||||
uci:set("network", dev_name, "device")
|
||||
uci:set("network", dev_name, "name", dev_name)
|
||||
uci:set("network", dev_name, "ifname", device)
|
||||
uci:set("network", dev_name, "type", "macvlan")
|
||||
uci:set("network", dev_name, "mode", "bridge")
|
||||
|
||||
-- add macvlan interface
|
||||
uci:delete("network", if_name)
|
||||
uci:set("network", if_name, "interface")
|
||||
uci:set("network", if_name, "proto", "static")
|
||||
uci:set("network", if_name, "ifname", dev_name)
|
||||
uci:set("network", if_name, "ipaddr", gateway)
|
||||
uci:set("network", if_name, "netmask", net_mask)
|
||||
uci:foreach("firewall", "zone", function(s)
|
||||
if s.name == "lan" then
|
||||
local interfaces
|
||||
if type(s.network) == "table" then
|
||||
interfaces = table.concat(s.network, " ")
|
||||
uci:delete("firewall", s[".name"], "network")
|
||||
else
|
||||
interfaces = s.network and s.network or ""
|
||||
end
|
||||
interfaces = interfaces .. " " .. if_name
|
||||
interfaces = interfaces:gsub("%s+", " ")
|
||||
uci:set("firewall", s[".name"], "network", interfaces)
|
||||
end
|
||||
end)
|
||||
|
||||
uci:commit("firewall")
|
||||
uci:commit("network")
|
||||
|
||||
os.execute("ifup " .. if_name)
|
||||
end
|
||||
|
||||
_docker.remove_macvlan_interface = function(name)
|
||||
if not fs.access("/etc/config/network") or not fs.access("/etc/config/firewall") then
|
||||
return
|
||||
end
|
||||
|
||||
if uci:get_bool("dockerd", "dockerman", "remote_endpoint") then
|
||||
return
|
||||
end
|
||||
|
||||
local if_name = "docker_"..name
|
||||
local dev_name = "macvlan_"..name
|
||||
uci:foreach("firewall", "zone", function(s)
|
||||
if s.name == "lan" then
|
||||
local interfaces
|
||||
if type(s.network) == "table" then
|
||||
interfaces = table.concat(s.network, " ")
|
||||
else
|
||||
interfaces = s.network and s.network or ""
|
||||
end
|
||||
interfaces = interfaces and interfaces:gsub(if_name, "")
|
||||
interfaces = interfaces and interfaces:gsub("%s+", " ")
|
||||
uci:set("firewall", s[".name"], "network", interfaces)
|
||||
end
|
||||
end)
|
||||
|
||||
uci:delete("network", dev_name)
|
||||
uci:delete("network", if_name)
|
||||
uci:commit("network")
|
||||
uci:commit("firewall")
|
||||
|
||||
os.execute("ip link del " .. if_name)
|
||||
end
|
||||
|
||||
_docker.byte_format = function (byte)
|
||||
if not byte then return 'NaN' end
|
||||
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
|
||||
|
||||
return _docker
|
147
luci-app-dockerman/luasrc/view/dockerman/apply_widget.htm
Normal file
|
@ -0,0 +1,147 @@
|
|||
<style type="text/css">
|
||||
#docker_apply_overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: none;
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
#docker_apply_overlay .alert-message {
|
||||
position: relative;
|
||||
top: 10%;
|
||||
width: 60%;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#docker_apply_overlay .alert-message > h4,
|
||||
#docker_apply_overlay .alert-message > p,
|
||||
#docker_apply_overlay .alert-message > div {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
#docker_apply_overlay .alert-message > img {
|
||||
margin-right: 1em;
|
||||
flex-basis: 32px;
|
||||
}
|
||||
|
||||
body.apply-overlay-active {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body.apply-overlay-active #docker_apply_overlay {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var xhr = new XHR(),
|
||||
uci_apply_rollback = <%=math.max(luci.config and luci.config.apply and luci.config.apply.rollback or 90, 90)%>,
|
||||
uci_apply_holdoff = <%=math.max(luci.config and luci.config.apply and luci.config.apply.holdoff or 4, 1)%>,
|
||||
uci_apply_timeout = <%=math.max(luci.config and luci.config.apply and luci.config.apply.timeout or 5, 1)%>,
|
||||
uci_apply_display = <%=math.max(luci.config and luci.config.apply and luci.config.apply.display or 1.5, 1)%>,
|
||||
was_xhr_poll_running = false;
|
||||
|
||||
function docker_status_message(type, content) {
|
||||
document.getElementById('docker_apply_overlay') || document.body.insertAdjacentHTML("beforeend",'<div id="docker_apply_overlay"><div class="alert-message"></div></div>')
|
||||
var overlay = document.getElementById('docker_apply_overlay')
|
||||
message = overlay.querySelector('.alert-message');
|
||||
|
||||
if (message && type) {
|
||||
if (!message.classList.contains(type)) {
|
||||
message.classList.remove('notice');
|
||||
message.classList.remove('warning');
|
||||
message.classList.add(type);
|
||||
}
|
||||
|
||||
if (content)
|
||||
message.innerHTML = content;
|
||||
|
||||
document.body.classList.add('apply-overlay-active');
|
||||
document.body.scrollTop = document.documentElement.scrollTop = 0;
|
||||
if (!was_xhr_poll_running) {
|
||||
was_xhr_poll_running = XHR.running();
|
||||
XHR.halt();
|
||||
}
|
||||
}
|
||||
else {
|
||||
document.body.classList.remove('apply-overlay-active');
|
||||
|
||||
if (was_xhr_poll_running)
|
||||
XHR.run();
|
||||
}
|
||||
}
|
||||
|
||||
var loading_msg="Loading.."
|
||||
function uci_confirm_docker() {
|
||||
var tt;
|
||||
docker_status_message('notice');
|
||||
var call = function(r, resjson, duration) {
|
||||
if (r && r.status === 200 ) {
|
||||
var indicator = document.querySelector('.uci_change_indicator');
|
||||
if (indicator) indicator.style.display = 'none';
|
||||
docker_status_message('notice', '<%:Docker actions done.%>');
|
||||
document.body.classList.remove('apply-overlay-active');
|
||||
window.clearTimeout(tt);
|
||||
return;
|
||||
}
|
||||
loading_msg = resjson?resjson.info:loading_msg
|
||||
// var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
|
||||
var delay =1000
|
||||
window.setTimeout(function() {
|
||||
xhr.get('<%=url("admin/docker/confirm")%>', null, call, uci_apply_timeout * 1000);
|
||||
}, delay);
|
||||
};
|
||||
|
||||
var tick = function() {
|
||||
var now = Date.now();
|
||||
|
||||
docker_status_message('notice',
|
||||
'<img src="<%=resource%>/icons/loading.gif" alt="" style="vertical-align:middle" /> <span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' +
|
||||
loading_msg + '</span>');
|
||||
|
||||
tt = window.setTimeout(tick, 200);
|
||||
ts = now;
|
||||
};
|
||||
|
||||
tick();
|
||||
/* wait a few seconds for the settings to become effective */
|
||||
window.setTimeout(call, Math.max(uci_apply_holdoff * 1000 , 1));
|
||||
}
|
||||
// document.getElementsByTagName("form")[0].addEventListener("submit", (e)=>{
|
||||
// uci_confirm_docker()
|
||||
// })
|
||||
|
||||
function fnSubmitForm(el){
|
||||
if (el.id != "cbid.table.1._new") {
|
||||
uci_confirm_docker()
|
||||
}
|
||||
}
|
||||
|
||||
<% if self.err then -%>
|
||||
docker_status_message('warning', '<span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">'+`<%=self.err%>`+'</span>');
|
||||
document.getElementById('docker_apply_overlay').addEventListener("click", (e)=>{
|
||||
docker_status_message()
|
||||
})
|
||||
<%- end %>
|
||||
|
||||
window.onload= function (){
|
||||
var buttons = document.querySelectorAll('input[type="submit"]');
|
||||
[].slice.call(buttons).forEach(function (el) {
|
||||
el.onclick = fnSubmitForm.bind(this, el);
|
||||
});
|
||||
|
||||
if(typeof(fnWindowLoad) == "function"){
|
||||
fnWindowLoad()
|
||||
}
|
||||
}
|
||||
|
||||
//]]></script>
|
|
@ -0,0 +1,7 @@
|
|||
<div style="display: inline-block;">
|
||||
<% if self:cfgvalue(section) ~= false then %>
|
||||
<input class="btn cbi-button cbi-button-<%=self.inputstyle or "button" %>" type="submit"" <% if self.disable then %>disabled <% end %><%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> />
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</div>
|
33
luci-app-dockerman/luasrc/view/dockerman/cbi/inlinevalue.htm
Normal file
|
@ -0,0 +1,33 @@
|
|||
<div style="display: inline-block;">
|
||||
<!-- <%- if self.title then -%>
|
||||
<label class="cbi-value-title"<%= attr("for", cbid) %>>
|
||||
<%- if self.titleref then -%><a title="<%=self.titledesc or translate('Go to relevant configuration page')%>" class="cbi-title-ref" href="<%=self.titleref%>"><%- end -%>
|
||||
<%-=self.title-%>
|
||||
<%- if self.titleref then -%></a><%- end -%>
|
||||
</label>
|
||||
<%- end -%> -->
|
||||
<%- if self.password then -%>
|
||||
<input type="password" style="position:absolute; left:-100000px" aria-hidden="true"<%=
|
||||
attr("name", "password." .. cbid)
|
||||
%> />
|
||||
<%- end -%>
|
||||
<input data-update="change"<%=
|
||||
attr("id", cbid) ..
|
||||
attr("name", cbid) ..
|
||||
attr("type", self.password and "password" or "text") ..
|
||||
attr("class", self.password and "cbi-input-password" or "cbi-input-text") ..
|
||||
attr("value", self:cfgvalue(section) or self.default) ..
|
||||
ifattr(self.password, "autocomplete", "new-password") ..
|
||||
ifattr(self.size, "size") ..
|
||||
ifattr(self.placeholder, "placeholder") ..
|
||||
ifattr(self.readonly, "readonly") ..
|
||||
ifattr(self.maxlength, "maxlength") ..
|
||||
ifattr(self.datatype, "data-type", self.datatype) ..
|
||||
ifattr(self.datatype, "data-optional", self.optional or self.rmempty) ..
|
||||
ifattr(self.combobox_manual, "data-manual", self.combobox_manual) ..
|
||||
ifattr(#self.keylist > 0, "data-choices", { self.keylist, self.vallist })
|
||||
%> />
|
||||
<%- if self.password then -%>
|
||||
<div class="btn cbi-button cbi-button-neutral" title="<%:Reveal/hide password%>" onclick="var e = this.previousElementSibling; e.type = (e.type === 'password') ? 'text' : 'password'">∗</div>
|
||||
<% end %>
|
||||
</div>
|
|
@ -0,0 +1,9 @@
|
|||
<% if self:cfgvalue(self.section) then section = self.section %>
|
||||
<div class="cbi-section" id="cbi-<%=self.config%>-<%=section%>">
|
||||
<%+cbi/tabmenu%>
|
||||
<div class="cbi-section-node<% if self.tabs then %> cbi-section-node-tabbed<% end %>" id="cbi-<%=self.config%>-<%=section%>">
|
||||
<%+cbi/ucisection%>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- /nsection -->
|
10
luci-app-dockerman/luasrc/view/dockerman/cbi/xfvalue.htm
Normal file
|
@ -0,0 +1,10 @@
|
|||
<%+cbi/valueheader%>
|
||||
<input type="hidden" value="1"<%=
|
||||
attr("name", "cbi.cbe." .. self.config .. "." .. section .. "." .. self.option)
|
||||
%> />
|
||||
<input class="cbi-input-checkbox" data-update="click change" type="checkbox" <% if self.disable == 1 then %>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")
|
||||
%> />
|
||||
<label<%= attr("for", cbid)%>></label>
|
||||
<%+cbi/valuefooter%>
|
28
luci-app-dockerman/luasrc/view/dockerman/container.htm
Normal file
|
@ -0,0 +1,28 @@
|
|||
<br>
|
||||
<ul class="cbi-tabmenu">
|
||||
<li id="cbi-tab-container_info"><a id="a-cbi-tab-container_info" href=""><%:Info%></a></li>
|
||||
<li id="cbi-tab-container_resources"><a id="a-cbi-tab-container_resources" href=""><%:Resources%></a></li>
|
||||
<li id="cbi-tab-container_stats"><a id="a-cbi-tab-container_stats" href=""><%:Stats%></a></li>
|
||||
<li id="cbi-tab-container_file"><a id="a-cbi-tab-container_file" href=""><%:File%></a></li>
|
||||
<li id="cbi-tab-container_console"><a id="a-cbi-tab-container_console" href=""><%:Console%></a></li>
|
||||
<li id="cbi-tab-container_inspect"><a id="a-cbi-tab-container_inspect" href=""><%:Inspect%></a></li>
|
||||
<li id="cbi-tab-container_logs"><a id="a-cbi-tab-container_logs" href=""><%:Logs%></a></li>
|
||||
</ul>
|
||||
|
||||
<script type="text/javascript">
|
||||
let re = /\/admin\/docker\/container\//
|
||||
let p = window.location.href
|
||||
let path = p.split(re)
|
||||
let container_id = path[1].split('/')[0] || path[1]
|
||||
let action = path[1].split('/')[1] || "info"
|
||||
let actions=["info","resources","stats","file","console","logs","inspect"]
|
||||
actions.forEach(function(item) {
|
||||
document.getElementById("a-cbi-tab-container_" + item).href= path[0]+"/admin/docker/container/"+container_id+'/'+item
|
||||
if (action === item) {
|
||||
document.getElementById("cbi-tab-container_" + item).className="cbi-tab"
|
||||
}
|
||||
else {
|
||||
document.getElementById("cbi-tab-container_" + item).className="cbi-tab-disabled"
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,6 @@
|
|||
<div class="cbi-map">
|
||||
<iframe id="terminal" style="width: 100%; min-height: 500px; border: none; border-radius: 3px;"></iframe>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
document.getElementById("terminal").src = window.location.protocol + "//" + window.location.hostname + ":7682";
|
||||
</script>
|
|
@ -0,0 +1,332 @@
|
|||
<link rel="stylesheet" href="/luci-static/resources/dockerman/file-manager.css?v=@ver">
|
||||
<fieldset class="cbi-section fb-container">
|
||||
<input id="current-path" type="text" class="current-path cbi-input-text" value="/" />
|
||||
<div class="panel-container">
|
||||
<input type="file" name="upload_archive" accept="*/*"
|
||||
style="visibility:hidden; position: absolute;top: 0px; left: 0px;" multiple="multiple" id="upload_archive" />
|
||||
<button id="upload-file" class="upload-toggle cbi-button cbi-button-edit"><%:Upload%></button>
|
||||
</div>
|
||||
<div id="list-content"></div>
|
||||
</fieldset>
|
||||
<script type="text/javascript" src="<%=resource%>/dockerman/tar.min.js"></script>
|
||||
<script>
|
||||
String.prototype.replaceAll = function (search, replacement) {
|
||||
var target = this;
|
||||
return target.replace(new RegExp(search, 'g'), replacement);
|
||||
};
|
||||
(function () {
|
||||
var iwxhr = new XHR();
|
||||
var listElem = document.getElementById("list-content");
|
||||
listElem.onclick = handleClick;
|
||||
var currentPath;
|
||||
var pathElem = document.getElementById("current-path");
|
||||
pathElem.onblur = function () {
|
||||
update_list(this.value.trim());
|
||||
};
|
||||
pathElem.onkeydown = function (evt) {
|
||||
if (evt.keyCode == 13) {
|
||||
this.blur()
|
||||
evt.preventDefault()
|
||||
}
|
||||
};
|
||||
function removePath(filename, isdir) {
|
||||
var c = confirm('!!!<%:DELETING%> ' + filename + ' ... <%:PLEASE CONFIRM%>!!!');
|
||||
if (c) {
|
||||
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_remove_file")%>/<%=self.container%>',
|
||||
{
|
||||
path: concatPath(currentPath, filename),
|
||||
isdir: isdir
|
||||
},
|
||||
function (x, res) {
|
||||
if (res.ec === 0) {
|
||||
refresh_list(res.data, currentPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renamePath(filename) {
|
||||
var newname = prompt('%:Please input new filename%>: ', filename);
|
||||
if (newname) {
|
||||
newname = newname.trim();
|
||||
if (newname != filename) {
|
||||
var newpath = concatPath(currentPath, newname);
|
||||
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_rename_file")%>/<%=self.container%>',
|
||||
{
|
||||
filepath: concatPath(currentPath, filename),
|
||||
newpath: newpath
|
||||
},
|
||||
function (x, res) {
|
||||
if (res.ec === 0) {
|
||||
refresh_list(res.data, currentPath);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openpath(filename, dirname) {
|
||||
dirname = dirname || currentPath;
|
||||
window.open('<%=luci.dispatcher.build_url("admin/docker/container_get_archive")%>?id=<%=self.container%>&path='
|
||||
+ encodeURIComponent(dirname + '/' + filename) + '&filename='
|
||||
+ encodeURIComponent(filename))
|
||||
}
|
||||
|
||||
function getFileElem(elem) {
|
||||
if (elem.className.indexOf('-icon') > -1) {
|
||||
return elem;
|
||||
}
|
||||
else if (elem.parentNode.className.indexOf('-icon') > -1) {
|
||||
return elem.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
function concatPath(path, filename) {
|
||||
if (path === '/') {
|
||||
return path + filename;
|
||||
}
|
||||
else {
|
||||
return path.replace(/\/$/, '') + '/' + filename;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(evt) {
|
||||
// evt.preventDefault();
|
||||
var targetElem = evt.target;
|
||||
var infoElem;
|
||||
if (targetElem.className.indexOf('cbi-button-remove') > -1) {
|
||||
infoElem = targetElem.parentNode.parentNode;
|
||||
removePath(infoElem.dataset['filename'], infoElem.dataset['isdir'])
|
||||
evt.preventDefault();
|
||||
location.reload()
|
||||
}
|
||||
else if (targetElem.className.indexOf('cbi-button-download') > -1) {
|
||||
infoElem = targetElem.parentNode.parentNode;
|
||||
openpath(targetElem.parentNode.parentNode.dataset['filename']);
|
||||
evt.preventDefault();
|
||||
}
|
||||
else if (targetElem.className.indexOf('cbi-button-rename') > -1) {
|
||||
renamePath(targetElem.parentNode.parentNode.dataset['filename']);
|
||||
evt.preventDefault();
|
||||
location.reload()
|
||||
}
|
||||
else if (targetElem = getFileElem(targetElem)) {
|
||||
if (targetElem.className.indexOf('parent-icon') > -1) {
|
||||
update_list(currentPath.replace(/\/[^/]+($|\/$)/, ''));
|
||||
}
|
||||
else if (targetElem.className.indexOf('file-icon') > -1) {
|
||||
openpath(targetElem.parentNode.dataset['filename']);
|
||||
}
|
||||
else if (targetElem.className.indexOf('link-icon') > -1) {
|
||||
infoElem = targetElem.parentNode;
|
||||
var filepath = infoElem.dataset['linktarget'];
|
||||
if (filepath) {
|
||||
if (infoElem.dataset['isdir'] === "1") {
|
||||
update_list(filepath);
|
||||
}
|
||||
else {
|
||||
var lastSlash = filepath.lastIndexOf('/')
|
||||
openpath(filepath.substring(lastSlash + 1), filepath.substring(0, lastSlash));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (targetElem.className.indexOf('folder-icon') > -1) {
|
||||
update_list(concatPath(currentPath, targetElem.parentNode.dataset['filename']))
|
||||
}
|
||||
}
|
||||
}
|
||||
function refresh_list(filenames, path) {
|
||||
var listHtml = '<table class="cbi-section-table"><tbody>';
|
||||
if (path !== '/') {
|
||||
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-2"><td class="parent-icon" colspan="6"><strong>..</strong></td></tr>';
|
||||
}
|
||||
if (filenames) {
|
||||
for (var i = 0; i < filenames.length; i++) {
|
||||
var line = filenames[i]
|
||||
if (line) {
|
||||
var f = line.match(/(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+([\S\s]+)/);
|
||||
var isLink = f[1][0] === 'z' || f[1][0] === 'l' || f[1][0] === 'x';
|
||||
var o = {
|
||||
displayname: f[9],
|
||||
filename: isLink ? f[9].split(' -> ')[0] : f[9],
|
||||
perms: f[1],
|
||||
date: f[7] + ' ' + f[6] + ' ' + f[8],
|
||||
size: f[5],
|
||||
owner: f[3],
|
||||
icon: (f[1][0] === 'd') ? "folder-icon" : (isLink ? "link-icon" : "file-icon")
|
||||
};
|
||||
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-' + (1 + i % 2)
|
||||
+ '" data-filename="' + o.filename + '" data-isdir="' + Number(f[1][0] === 'd' || f[1][0] === 'z') + '"'
|
||||
+ ((f[1][0] === 'z' || f[1][0] === 'l') ? (' data-linktarget="' + f[9].split(' -> ')[1]) : '')
|
||||
+ '">'
|
||||
+ '<td class="cbi-value-field ' + o.icon + '">'
|
||||
+ '<strong>' + o.displayname + '</strong>'
|
||||
+ '</td>'
|
||||
+ '<td class="cbi-value-field cbi-value-owner">' + o.owner + '</td>'
|
||||
+ '<td class="cbi-value-field cbi-value-date">' + o.date + '</td>'
|
||||
+ '<td class="cbi-value-field cbi-value-size">' + o.size + '</td>'
|
||||
+ '<td class="cbi-value-field cbi-value-perm">' + o.perms + '</td>'
|
||||
+ '<td class="cbi-section-table-cell">\
|
||||
<button class="btn cbi-button cbi-button-rename cbi-button-edit">'+ "<%:Rename%>" + '</button>\
|
||||
<button class="btn cbi-button cbi-button-download cbi-button-add">'+ "<%:Download%>" + '</button>\
|
||||
<button class="btn cbi-button cbi-button-remove">'+ "<%:Remove%>" + '</button>\
|
||||
</td>'
|
||||
+ '</tr>';
|
||||
}
|
||||
}
|
||||
}
|
||||
listHtml += "</table>";
|
||||
listElem.innerHTML = listHtml;
|
||||
}
|
||||
function update_list(path, opt) {
|
||||
opt = opt || {};
|
||||
path = concatPath(path, '');
|
||||
if (currentPath != path) {
|
||||
iwxhr.get('<%=luci.dispatcher.build_url("admin/docker/container_list_file")%>/<%=self.container%>',
|
||||
{ path: path },
|
||||
function (x, res) {
|
||||
if (res.ec === 0) {
|
||||
refresh_list(res.data, path);
|
||||
}
|
||||
else {
|
||||
refresh_list([], path);
|
||||
}
|
||||
}
|
||||
);
|
||||
if (!opt.popState) {
|
||||
history.pushState({ path: path }, null, '?path=' + path);
|
||||
}
|
||||
currentPath = path;
|
||||
pathElem.value = currentPath;
|
||||
}
|
||||
};
|
||||
|
||||
async function file2Tar(tarFile, fileToLoad) {
|
||||
if (! fileToLoad) return
|
||||
function file2Byte(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var fileReader = new FileReader();
|
||||
fileReader.onerror = () => {
|
||||
fileReader.abort();
|
||||
reject(new DOMException("Problem parsing input file."));
|
||||
};
|
||||
fileReader.onload = (fileLoadedEvent) => {
|
||||
resolve(ByteHelper.stringUTF8ToBytes(fileLoadedEvent.target.result));
|
||||
}
|
||||
fileReader.readAsBinaryString(file);
|
||||
})
|
||||
}
|
||||
const x = await file2Byte(fileToLoad)
|
||||
return fileByte2Tar(tarFile, fileToLoad.name, x).downloadAs(fileToLoad.name + ".tar")
|
||||
}
|
||||
|
||||
function fileByte2Tar(tarFile, fileName, fileBytes) {
|
||||
if (!tarFile) tarFile = TarFile.create(fileName)
|
||||
var tarHeader = TarFileEntryHeader.default();
|
||||
var tarFileEntryHeader = new TarFileEntryHeader
|
||||
(
|
||||
// ByteHelper.bytesToStringUTF8(fileName),
|
||||
fileName,
|
||||
tarHeader.fileMode,
|
||||
tarHeader.userIDOfOwner,
|
||||
tarHeader.userIDOfGroup,
|
||||
fileBytes.length, // fileSizeInBytes,
|
||||
tarHeader.timeModifiedInUnixFormat, // todo
|
||||
0, // checksum,
|
||||
TarFileTypeFlag.Instances().Normal,
|
||||
tarHeader.nameOfLinkedFile,
|
||||
tarHeader.uStarIndicator,
|
||||
tarHeader.uStarVersion,
|
||||
tarHeader.userNameOfOwner,
|
||||
tarHeader.groupNameOfOwner,
|
||||
tarHeader.deviceNumberMajor,
|
||||
tarHeader.deviceNumberMinor,
|
||||
tarHeader.filenamePrefix
|
||||
);
|
||||
|
||||
tarFileEntryHeader.checksumCalculate();
|
||||
var entryForFileToAdd = new TarFileEntry
|
||||
(
|
||||
tarFileEntryHeader,
|
||||
fileBytes
|
||||
);
|
||||
|
||||
tarFile.entries.push(entryForFileToAdd);
|
||||
return tarFile
|
||||
}
|
||||
|
||||
var btnUpload = document.getElementById('upload-file');
|
||||
|
||||
btnUpload.onclick = function (e) {
|
||||
document.getElementById("upload_archive").click()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
let fileLoad = document.getElementById('upload_archive')
|
||||
fileLoad.onchange = async function (e) {
|
||||
let uploadArchive = document.getElementById('upload_archive')
|
||||
// let uploadPath = document.getElementById('path').value
|
||||
if (!uploadArchive.value) {
|
||||
docker_status_message('warning', "<%:Please input the PATH and select the file !%>")
|
||||
document.getElementById('docker_apply_overlay').addEventListener("click", (e) => {
|
||||
docker_status_message()
|
||||
})
|
||||
return
|
||||
}
|
||||
docker_status_message('notice',
|
||||
'<img src="<%=resource%>/icons/loading.gif" style="vertical-align:middle" /> <span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' +
|
||||
'Uploading...' + '</span>');
|
||||
Globals.Instance.tarFile = TarFile.create("Archive.tar")
|
||||
let bytesToWriteAsBlob
|
||||
for (let i = 0; i < uploadArchive.files.length; i++) {
|
||||
let fileName = uploadArchive.files[i].name
|
||||
bytesToWriteAsBlob = await file2Tar(Globals.Instance.tarFile, uploadArchive.files[i])
|
||||
}
|
||||
let formData = new FormData()
|
||||
formData.append('upload-filename', "Archive.tar")
|
||||
formData.append('upload-path', concatPath(currentPath, ''))
|
||||
formData.append('upload-archive', bytesToWriteAsBlob)
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/container_put_archive")%>/<%=self.container%>', true)
|
||||
xhr.onload = function () {
|
||||
if (xhr.status == 200) {
|
||||
uploadArchive.value = ''
|
||||
docker_status_message('notice', "<%:Upload Success%>")
|
||||
function sleep(time) {
|
||||
return new Promise((resolve) => setTimeout(resolve, time))
|
||||
}
|
||||
sleep(800).then(() => {
|
||||
location.reload()
|
||||
})
|
||||
}
|
||||
else {
|
||||
// docker_status_message('warning', "<%:Upload Error%>:" + xhr.statusText)
|
||||
docker_status_message('warning', "<%:Upload Error%>:" + '<span style="white-space:pre-line; word-break:break-all; font-family: \'Courier New\', Courier, monospace;">' +
|
||||
JSON.parse(xhr.response).message + '</span>')
|
||||
}
|
||||
document.getElementById('docker_apply_overlay').addEventListener("click", (e) => {
|
||||
docker_status_message()
|
||||
})
|
||||
}
|
||||
xhr.send(formData)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function (evt) {
|
||||
var initPath = '/';
|
||||
if (/path=([/\w\.\-\_]+)/.test(location.search)) {
|
||||
initPath = RegExp.$1;
|
||||
}
|
||||
update_list(initPath, { popState: true });
|
||||
});
|
||||
window.addEventListener('popstate', function (evt) {
|
||||
var path = '/';
|
||||
if (evt.state && evt.state.path) {
|
||||
path = evt.state.path;
|
||||
}
|
||||
update_list(path, { popState: true });
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
</script>
|
81
luci-app-dockerman/luasrc/view/dockerman/container_stats.htm
Normal file
|
@ -0,0 +1,81 @@
|
|||
<script type="text/javascript">//<![CDATA[
|
||||
let last_bw_tx
|
||||
let last_bw_rx
|
||||
let interval = 3
|
||||
|
||||
function progressbar(v, m, pc, np, f) {
|
||||
m = m || 100
|
||||
|
||||
return String.format(
|
||||
'<div style="width:100%%; max-width:500px; position:relative; border:1px solid #999999">' +
|
||||
'<div style="background-color:#CCCCCC; width:%d%%; height:15px">' +
|
||||
'<div style="position:absolute; left:0; top:0; text-align:center; width:100%%; color:#000000">' +
|
||||
'<small>%s '+(f?f:'/')+' %s ' + (np ? "" : '(%d%%)') + '</small>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>', pc, v, m, pc, f
|
||||
);
|
||||
}
|
||||
|
||||
function niceBytes(bytes, decimals) {
|
||||
if (bytes == 0) return '0 Bytes';
|
||||
var k = 1000,
|
||||
dm = decimals + 1 || 3,
|
||||
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
||||
i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
XHR.poll(interval, '<%=luci.dispatcher.build_url("admin/docker/container_stats")%>/<%=self.container_id%>', { status: 1 },
|
||||
function (x, info) {
|
||||
var e;
|
||||
|
||||
if (e = document.getElementById('cbi-table-cpu-value'))
|
||||
e.innerHTML = progressbar(
|
||||
(info.cpu_percent), 100, (info.cpu_percent ? info.cpu_percent : 0));
|
||||
if (e = document.getElementById('cbi-table-memory-value'))
|
||||
e.innerHTML = progressbar(
|
||||
niceBytes(info.memory.mem_useage),
|
||||
niceBytes(info.memory.mem_limit),
|
||||
((100 / (info.memory.mem_limit ? info.memory.mem_limit : 100)) * (info.memory.mem_useage ? info.memory.mem_useage : 0))
|
||||
);
|
||||
|
||||
for (var eth in info.bw_rxtx) {
|
||||
if (!document.getElementById("cbi-table-speed_" + eth + "-value")) {
|
||||
let tab = document.getElementById("cbi-table-cpu").parentNode
|
||||
let div = document.getElementById('cbi-table-cpu').cloneNode(true);
|
||||
div.id = "cbi-table-speed_" + eth;
|
||||
div.children[0].innerHTML = "<%:Upload/Download%>: " + eth
|
||||
div.children[1].id = "cbi-table-speed_" + eth + "-value"
|
||||
tab.appendChild(div)
|
||||
}
|
||||
if (!document.getElementById("cbi-table-network_" + eth + "-value")) {
|
||||
let tab = document.getElementById("cbi-table-cpu").parentNode
|
||||
let div = document.getElementById('cbi-table-cpu').cloneNode(true);
|
||||
div.id = "cbi-table-network_" + eth;
|
||||
div.children[0].innerHTML = "<%:TX/RX%>: " + eth
|
||||
div.children[1].id = "cbi-table-network_" + eth + "-value"
|
||||
tab.appendChild(div)
|
||||
}
|
||||
e = document.getElementById("cbi-table-network_" + eth + "-value")
|
||||
e.innerHTML = progressbar(
|
||||
'↑'+niceBytes(info.bw_rxtx[eth].bw_tx),
|
||||
'↓'+niceBytes(info.bw_rxtx[eth].bw_rx),
|
||||
null,
|
||||
true, " "
|
||||
);
|
||||
e = document.getElementById("cbi-table-speed_" + eth + "-value")
|
||||
if (! last_bw_tx) last_bw_tx = info.bw_rxtx[eth].bw_tx
|
||||
if (! last_bw_rx) last_bw_rx = info.bw_rxtx[eth].bw_rx
|
||||
e.innerHTML = progressbar(
|
||||
'↑'+niceBytes((info.bw_rxtx[eth].bw_tx - last_bw_tx)/interval)+'/s',
|
||||
'↓'+niceBytes((info.bw_rxtx[eth].bw_rx - last_bw_rx)/interval)+'/s',
|
||||
null,
|
||||
true, " "
|
||||
);
|
||||
last_bw_tx = info.bw_rxtx[eth].bw_tx
|
||||
last_bw_rx = info.bw_rxtx[eth].bw_rx
|
||||
}
|
||||
|
||||
});
|
||||
//]]></script>
|
|
@ -0,0 +1,91 @@
|
|||
<script type="text/javascript">
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
function niceBytes(x) {
|
||||
let l = 0, n = parseInt(x, 10) || 0;
|
||||
while (n >= 1024 && ++l) {
|
||||
n = n / 1024;
|
||||
}
|
||||
return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
|
||||
}
|
||||
|
||||
fnWindowLoad = function () {
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin/docker/get_system_df")%>/', null, (x, info)=>{
|
||||
if(!info || !info.Containers || !info.Containers.forEach) return
|
||||
info.Containers.forEach(item=>{
|
||||
const size_c = document.getElementsByClassName("container_size_" + item.Id)
|
||||
size_c[0].title = "RW Size: " + niceBytes(item.SizeRw) + " / RootFS Size(Include Image): " + niceBytes(item.SizeRootFs)
|
||||
size_c[0].innerText = "Size: " + niceBytes(item.SizeRw) + "/" + niceBytes(item.SizeRootFs)
|
||||
})
|
||||
})
|
||||
let lines = document.querySelectorAll('[id^=cbi-containers-]')
|
||||
let last_bw_tx = {}
|
||||
let last_bw_rx = {}
|
||||
let interval = 30
|
||||
let containers = []
|
||||
lines.forEach((item) => {
|
||||
let containerId = item.id.match(/cbi-containers-.+_id_(.*)/)
|
||||
if (!containerId) { return }
|
||||
containerId = containerId[1]
|
||||
if (item.getElementsByClassName("container_not_running").length > 0) { return }
|
||||
XHR.poll(interval, '<%=luci.dispatcher.build_url("admin/docker/container_stats")%>/' + containerId, null, (x, info) => {
|
||||
// handle stats info
|
||||
if (!info) { return }
|
||||
item.childNodes.forEach((cell) => {
|
||||
if (cell && cell.attributes) {
|
||||
if (cell.getAttribute("data-name") == "_status" || cell.childNodes[1] && cell.childNodes[1].id.match(/_status/)) {
|
||||
let runningStats = cell.getElementsByClassName("container_cpu_status")
|
||||
runningStats[0].innerText = "CPU: " + info.cpu_percent + "%"
|
||||
runningStats = cell.getElementsByClassName("container_mem_status")
|
||||
runningStats[0].innerText = "MEM: " + niceBytes(info.memory.mem_useage)
|
||||
runningStats = cell.getElementsByClassName("container_network_status")
|
||||
for (var eth in info.bw_rxtx) {
|
||||
if (last_bw_tx[containerId] != undefined && last_bw_rx[containerId] != undefined) {
|
||||
runningStats[0].innerText = '↑' + niceBytes((info.bw_rxtx[eth].bw_tx - last_bw_tx[containerId]) / interval) + '/s ↓' + niceBytes((info.bw_rxtx[eth].bw_rx - last_bw_rx[containerId]) / interval) + '/s'
|
||||
}
|
||||
last_bw_rx[containerId] = info.bw_rxtx[eth].bw_rx
|
||||
last_bw_tx[containerId] = info.bw_rxtx[eth].bw_tx
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// containers.push(containerId)
|
||||
})
|
||||
// XHR.post('<%=luci.dispatcher.build_url("admin/docker/containers_stats")%>', {
|
||||
// containers: JSON.stringify(containers)
|
||||
// }, (x, info) => {
|
||||
// lines.forEach((item) => {
|
||||
// if (!info) { return }
|
||||
|
||||
// let containerId = item.id.match(/cbi-containers-.+_id_(.*)/)
|
||||
// if (!containerId) { return }
|
||||
// containerId = containerId[1]
|
||||
// if (!info[containerId]) { return }
|
||||
// infoC = info[containerId]
|
||||
// if (item.getElementsByClassName("container_not_running").length > 0) { return }
|
||||
// item.childNodes.forEach((cell) => {
|
||||
// if (cell && cell.attributes) {
|
||||
// if (cell.getAttribute("data-name") == "_status" || cell.childNodes[1] && cell.childNodes[1].id.match(/_status/)) {
|
||||
// let runningStats = cell.getElementsByClassName("container_cpu_status")
|
||||
// runningStats[0].innerText = "CPU: " + infoC.cpu_percent + "%"
|
||||
// runningStats = cell.getElementsByClassName("container_mem_status")
|
||||
// runningStats[0].innerText = "MEM: " + niceBytes(infoC.memory.mem_useage)
|
||||
// runningStats = cell.getElementsByClassName("container_network_status")
|
||||
// for (var eth in infoC.bw_rxtx) {
|
||||
// if (last_bw_tx[containerId] != undefined && last_bw_rx[containerId] != undefined) {
|
||||
// runningStats[0].innerText = '↑' + niceBytes((infoC.bw_rxtx[eth].bw_tx - last_bw_tx[containerId]) / interval) + '/s ↓' + niceBytes((infoC.bw_rxtx[eth].bw_rx - last_bw_rx[containerId]) / interval) + '/s'
|
||||
// }
|
||||
// last_bw_rx[containerId] = infoC.bw_rxtx[eth].bw_rx
|
||||
// last_bw_tx[containerId] = infoC.bw_rxtx[eth].bw_tx
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
|
||||
XHR.run()
|
||||
XHR.halt()
|
||||
}
|
||||
</script>
|
104
luci-app-dockerman/luasrc/view/dockerman/images_import.htm
Normal file
|
@ -0,0 +1,104 @@
|
|||
<input type="text" class="cbi-input-text" name="isrc" placeholder="http://host/image.tar" id="isrc" />
|
||||
<input type="text" class="cbi-input-text" name="itag" placeholder="repository:tag" id="itag" />
|
||||
<div style="display: inline-block;">
|
||||
<input type="button"" class="btn cbi-button cbi-button-add" id="btnimport" name="import" value="<%:Import%>" <% if self.disable then %>disabled <% end %>/>
|
||||
<input type="file" id="file_import" style="visibility:hidden; position: absolute;top: 0px; left: 0px;" />
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
let btnImport = document.getElementById('btnimport')
|
||||
let valISrc = document.getElementById('isrc')
|
||||
let valITag = document.getElementById('itag')
|
||||
btnImport.onclick = function (e) {
|
||||
if (valISrc.value == "") {
|
||||
document.getElementById("file_import").click()
|
||||
return
|
||||
}
|
||||
else {
|
||||
let formData = new FormData()
|
||||
formData.append('src', valISrc.value)
|
||||
formData.append('tag', valITag.value)
|
||||
let xhr = new XMLHttpRequest()
|
||||
uci_confirm_docker()
|
||||
xhr.open("POST", "<%=luci.dispatcher.build_url('admin/docker/images_import')%>", true)
|
||||
xhr.onload = function () {
|
||||
location.reload()
|
||||
}
|
||||
xhr.send(formData)
|
||||
}
|
||||
}
|
||||
|
||||
let fileimport = document.getElementById('file_import')
|
||||
fileimport.onchange = function (e) {
|
||||
let fileimport = document.getElementById('file_import')
|
||||
if (!fileimport.value) {
|
||||
return
|
||||
}
|
||||
let valITag = document.getElementById('itag')
|
||||
let fileName = fileimport.files[0].name
|
||||
let formData = new FormData()
|
||||
formData.append('upload-filename', fileName)
|
||||
formData.append('tag', valITag.value)
|
||||
formData.append('upload-archive', fileimport.files[0])
|
||||
let xhr = new XMLHttpRequest()
|
||||
uci_confirm_docker()
|
||||
xhr.open("POST", "<%=luci.dispatcher.build_url('admin/docker/images_import')%>", true)
|
||||
xhr.onload = function () {
|
||||
fileimport.value = ''
|
||||
location.reload()
|
||||
}
|
||||
xhr.send(formData)
|
||||
}
|
||||
|
||||
let new_tag = function (image_id) {
|
||||
let new_tag = prompt("<%:New tag%>\n<%:Image%>" + "ID: " + image_id + "\n<%:Please input new tag%>:", "")
|
||||
if (new_tag) {
|
||||
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_tag')%>",
|
||||
{
|
||||
id: image_id,
|
||||
tag: new_tag
|
||||
},
|
||||
function (r) {
|
||||
if (r.status == 201) {
|
||||
location.reload()
|
||||
}
|
||||
else {
|
||||
docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText);
|
||||
document.getElementById('docker_apply_overlay').addEventListener(
|
||||
"click",
|
||||
(e)=>{
|
||||
docker_status_message()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let un_tag = function (tag) {
|
||||
if (tag.match("<none>"))
|
||||
return
|
||||
if (confirm("<%:Remove tag%>: " + tag + " ?")) {
|
||||
(new XHR()).post("<%=luci.dispatcher.build_url('admin/docker/images_untag')%>",
|
||||
{
|
||||
tag: tag
|
||||
},
|
||||
function (r) {
|
||||
if (r.status == 200) {
|
||||
location.reload()
|
||||
}
|
||||
else {
|
||||
docker_status_message('warning', 'Image: untagging ' + tag + '...fail code:' + r.status + r.statusText);
|
||||
document.getElementById('docker_apply_overlay').addEventListener(
|
||||
"click",
|
||||
(e)=>{
|
||||
docker_status_message()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
40
luci-app-dockerman/luasrc/view/dockerman/images_load.htm
Normal file
|
@ -0,0 +1,40 @@
|
|||
<div style="display: inline-block;">
|
||||
<input type="button"" class="btn cbi-button cbi-button-add" id="btnload" name="load" value="<%:Load%>" <% if self.disable then %>disabled <% end %>/>
|
||||
<input type="file" id="file_load" style="visibility:hidden; position: absolute;top: 0px; left: 0px;" accept="application/x-tar" />
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
let btnLoad = document.getElementById('btnload')
|
||||
btnLoad.onclick = function (e) {
|
||||
document.getElementById("file_load").click()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
let fileLoad = document.getElementById('file_load')
|
||||
fileLoad.onchange = function(e){
|
||||
let fileLoad = document.getElementById('file_load')
|
||||
if (!fileLoad.value) {
|
||||
return
|
||||
}
|
||||
let fileName = fileLoad.files[0].name
|
||||
let formData = new FormData()
|
||||
formData.append('upload-filename', fileName)
|
||||
formData.append('upload-archive', fileLoad.files[0])
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open("POST", '<%=luci.dispatcher.build_url("admin/docker/images_load")%>', true)
|
||||
xhr.onload = function() {
|
||||
if (xhr.status == 200) {
|
||||
docker_status_message('notice', xhr.statusText)
|
||||
function sleep(time) {
|
||||
return new Promise((resolve) => setTimeout(resolve, time))
|
||||
}
|
||||
sleep(1500).then(() => {
|
||||
location.reload()
|
||||
})
|
||||
} else {
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
uci_confirm_docker()
|
||||
xhr.send(formData)
|
||||
}
|
||||
</script>
|
13
luci-app-dockerman/luasrc/view/dockerman/logs.htm
Normal file
|
@ -0,0 +1,13 @@
|
|||
<% if self.title == "Events" then %>
|
||||
<%+header%>
|
||||
<h2 name="content"><%:Docker - Events%></h2>
|
||||
<div class="cbi-section">
|
||||
<h3><%:Events%></h3>
|
||||
<% end %>
|
||||
<div id="content_syslog">
|
||||
<textarea readonly="readonly" wrap="off" rows="<%=self.syslog:cmatch('\n')+2%>" id="syslog"><%=self.syslog:pcdata()%></textarea>
|
||||
</div>
|
||||
<% if self.title == "Events" then %>
|
||||
</div>
|
||||
<%+footer%>
|
||||
<% end %>
|
|
@ -0,0 +1,102 @@
|
|||
<style type="text/css">
|
||||
#dialog_reslov {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: none;
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
#dialog_reslov .dialog_box {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255);
|
||||
top: 10%;
|
||||
width: 50%;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height:auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#dialog_reslov .dialog_line {
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
margin-left: 2em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
#dialog_reslov .dialog_box>h4,
|
||||
#dialog_reslov .dialog_box>p,
|
||||
#dialog_reslov .dialog_box>div {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
#dialog_reslov .dialog_box>img {
|
||||
margin-right: 1em;
|
||||
flex-basis: 32px;
|
||||
}
|
||||
|
||||
body.dialog-reslov-active {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body.dialog-reslov-active #dialog_reslov {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
function close_reslov_dialog() {
|
||||
document.body.classList.remove('dialog-reslov-active')
|
||||
document.documentElement.style.overflowY = 'scroll'
|
||||
}
|
||||
|
||||
function reslov_container() {
|
||||
let s = document.getElementById('cmd-line-status')
|
||||
|
||||
if (!s)
|
||||
return
|
||||
|
||||
let cmd_line = document.getElementById("dialog_reslov_text").value;
|
||||
if (cmd_line == null || cmd_line == "") {
|
||||
return
|
||||
}
|
||||
|
||||
cmd_line = cmd_line.replace(/(^\s*)/g,"")
|
||||
if (!cmd_line.match(/^docker\s+(run|create)/)) {
|
||||
s.innerHTML = "<font color='red'><%:Command line Error%></font>"
|
||||
return
|
||||
}
|
||||
|
||||
let reg_space = /\s+/g
|
||||
let reg_muti_line= /\\\s*\n/g
|
||||
// reg_rem =/(?<!\\)`#.+(?<!\\)`/g // the command has `# `
|
||||
let reg_rem =/`#.+`/g// the command has `# `
|
||||
cmd_line = cmd_line.replace(/^docker\s+(run|create)/,"DOCKERCLI").replace(reg_rem, " ").replace(reg_muti_line, " ").replace(reg_space, " ")
|
||||
console.log(cmd_line)
|
||||
window.location.href = '<%=luci.dispatcher.build_url("admin/docker/newcontainer")%>/' + encodeURI(cmd_line)
|
||||
}
|
||||
|
||||
function clear_text(){
|
||||
let s = document.getElementById('cmd-line-status')
|
||||
s.innerHTML = ""
|
||||
}
|
||||
|
||||
function show_reslov_dialog() {
|
||||
document.getElementById('dialog_reslov') || document.body.insertAdjacentHTML("beforeend", '<div id="dialog_reslov"><div class="dialog_box"><div class="dialog_line"></div><div class="dialog_line"><span><%:Plese input <docker create/run> command line:%></span><br><span id="cmd-line-status"></span></div><div class="dialog_line"><textarea class="cbi-input-textarea" id="dialog_reslov_text" style="width: 100%; height:100%;" rows="15" onkeyup="clear_text()" placeholder="docker run -d alpine sh"></textarea></div><div class="dialog_line" style="text-align: right;"><input type="button" class="btn cbi-button cbi-button-apply" type="submit" value="<%:Submit%>" onclick="reslov_container()"/> <input type="button" class="btn cbi-button cbi-button-reset" type="reset" value="<%:Cancel%>" onclick="close_reslov_dialog()" /></div><div class="dialog_line"></div></div></div>')
|
||||
document.body.classList.add('dialog-reslov-active')
|
||||
let s = document.getElementById('cmd-line-status')
|
||||
s.innerHTML = ""
|
||||
document.documentElement.style.overflowY = 'hidden'
|
||||
}
|
||||
</script>
|
||||
<%+cbi/valueheader%>
|
||||
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Command line%>" onclick="show_reslov_dialog()" />
|
||||
|
||||
<%+cbi/valuefooter%>
|
197
luci-app-dockerman/luasrc/view/dockerman/overview.htm
Normal file
|
@ -0,0 +1,197 @@
|
|||
<style>
|
||||
/*!
|
||||
Pure v1.0.1
|
||||
Copyright 2013 Yahoo!
|
||||
Licensed under the BSD License.
|
||||
https://github.com/pure-css/pure/blob/master/LICENSE.md
|
||||
*/
|
||||
.pure-g {
|
||||
letter-spacing: -.31em;
|
||||
text-rendering: optimizespeed;
|
||||
font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-flex-flow: row wrap;
|
||||
-ms-flex-flow: row wrap;
|
||||
flex-flow: row wrap;
|
||||
-webkit-align-content: flex-start;
|
||||
-ms-flex-line-pack: start;
|
||||
align-content: flex-start
|
||||
}
|
||||
|
||||
.pure-u {
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
vertical-align: top;
|
||||
text-rendering: auto
|
||||
}
|
||||
|
||||
.pure-g [class*=pure-u] {
|
||||
font-family: sans-serif
|
||||
}
|
||||
|
||||
.pure-u-1-4,
|
||||
.pure-u-2-5,
|
||||
.pure-u-3-5 {
|
||||
display: inline-block;
|
||||
zoom: 1;
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
vertical-align: top;
|
||||
text-rendering: auto
|
||||
}
|
||||
|
||||
.pure-u-1-4 {
|
||||
width: 25%
|
||||
}
|
||||
|
||||
.pure-u-2-5 {
|
||||
width: 40%
|
||||
}
|
||||
|
||||
.pure-u-3-5 {
|
||||
width: 60%
|
||||
}
|
||||
|
||||
.status {
|
||||
margin: 1rem -0.5rem 1rem -0.5rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
margin: 0.5rem 0.5rem;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
font-family: inherit;
|
||||
min-width: inherit;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border: 1px solid rgba(0, 0, 0, .05);
|
||||
border-radius: .375rem;
|
||||
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .15);
|
||||
}
|
||||
|
||||
.img-con {
|
||||
margin: 1rem;
|
||||
min-width: 4rem;
|
||||
max-width: 4rem;
|
||||
min-height: 4rem;
|
||||
max-height: 4rem;
|
||||
}
|
||||
|
||||
.block h4 {
|
||||
font-size: .8125rem;
|
||||
font-weight: 600;
|
||||
margin: 1rem;
|
||||
color: #8898aa !important;
|
||||
line-height: 1.8em;
|
||||
}
|
||||
|
||||
.cbi-section-table-cell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.pure-u-1-4 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.cbi-button-add {
|
||||
position: fixed;
|
||||
padding: 0.3rem 0.5rem;
|
||||
z-index: 1000;
|
||||
width: 50px !important;
|
||||
height: 50px;
|
||||
bottom: 90px;
|
||||
right: 5px;
|
||||
font-size: 16px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
background-color: #fb6340 !important;
|
||||
border-color: #fb6340 !important;
|
||||
box-shadow: 0 0 1rem 0 rgba(136, 152, 170, .75);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="pure-g status">
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-2-5">
|
||||
<div class="img-con">
|
||||
<img src="<%=resource%>/dockerman/containers.svg" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-3-5">
|
||||
<h4 style="text-align: right; font-size: 1rem"><%:Containers%></h4>
|
||||
<h4 style="text-align: right;">
|
||||
<%- if self.containers_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/containers")%>'><%- end -%>
|
||||
<span style="font-size: 2rem; color: #2dce89;"><%=self.containers_running%></span>
|
||||
<span style="font-size: 1rem; color: #8898aa !important;">/<%=self.containers_total%></span>
|
||||
<%- if self.containers_total ~= "-" then -%></a><%- end -%>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-2-5">
|
||||
<div class="img-con">
|
||||
<img src="<%=resource%>/dockerman/images.svg" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-3-5">
|
||||
<h4 style="text-align: right; font-size: 1rem"><%:Images%></h4>
|
||||
<h4 style="text-align: right;">
|
||||
<%- if self.images_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/images")%>'><%- end -%>
|
||||
<span style="font-size: 2rem; color: #2dce89;"><%=self.images_used%></span>
|
||||
<span style="font-size: 1rem; color: #8898aa !important;">/<%=self.images_total%></span>
|
||||
<%- if self.images_total ~= "-" then -%></a><%- end -%>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-2-5">
|
||||
<div class="img-con">
|
||||
<img src="<%=resource%>/dockerman/networks.svg" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-3-5">
|
||||
<h4 style="text-align: right; font-size: 1rem"><%:Networks%></h4>
|
||||
<h4 style="text-align: right;">
|
||||
<%- if self.networks_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/networks")%>'><%- end -%>
|
||||
<span style="font-size: 2rem; color: #2dce89;"><%=self.networks_total%></span>
|
||||
<!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
|
||||
<%- if self.networks_total ~= "-" then -%></a><%- end -%>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<div class="block pure-g">
|
||||
<div class="pure-u-2-5">
|
||||
<div class="img-con">
|
||||
<img src="<%=resource%>/dockerman/volumes.svg" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-3-5">
|
||||
<h4 style="text-align: right; font-size: 1rem"><%:Volumes%></h4>
|
||||
<h4 style="text-align: right;">
|
||||
<%- if self.volumes_total ~= "-" then -%><a href='<%=luci.dispatcher.build_url("admin/docker/volumes")%>'><%- end -%>
|
||||
<span style="font-size: 2rem; color: #2dce89;"><%=self.volumes_total%></span>
|
||||
<!-- <span style="font-size: 1rem; color: #8898aa !important;">/20</span> -->
|
||||
<%- if self.volumes_total ~= "-" then -%></a><%- end -%>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
21
luci-app-dockerman/luasrc/view/dockerman/volume_size.htm
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script type="text/javascript">
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
function niceBytes(x) {
|
||||
let l = 0, n = parseInt(x, 10) || 0;
|
||||
while (n >= 1024 && ++l) {
|
||||
n = n / 1024;
|
||||
}
|
||||
return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
|
||||
}
|
||||
|
||||
fnWindowLoad = function () {
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin/docker/get_system_df")%>/', null, (x, info)=>{
|
||||
if(!info || !info.Volumes || !info.Volumes.forEach) return
|
||||
info.Volumes.forEach(item=>{
|
||||
console.log(info)
|
||||
const size_c = document.getElementsByClassName("volume_size_" + item.Name)
|
||||
size_c[0].innerText = item.UsageData ? niceBytes(item.UsageData.Size) : '-'
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
1002
luci-app-dockerman/po/templates/dockerman.pot
Normal file
1094
luci-app-dockerman/po/zh-cn/dockerman.po
Normal file
1
luci-app-dockerman/po/zh_Hans
Normal file
|
@ -0,0 +1 @@
|
|||
zh-cn
|
14
luci-app-dockerman/postinst
Normal file
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
|
||||
/init.sh env
|
||||
touch /etc/config/dockerd
|
||||
uci set dockerd.dockerman=dockerman
|
||||
uci set dockerd.dockerman.socket_path=`uci get dockerd.dockerman.socket_path 2&> /dev/null || echo '/var/run/docker.sock'`
|
||||
uci set dockerd.dockerman.status_path=`uci get dockerd.dockerman.status_path 2&> /dev/null || echo '/tmp/.docker_action_status'`
|
||||
uci set dockerd.dockerman.debug=`uci get dockerd.dockerman.debug 2&> /dev/null || echo 'false'`
|
||||
uci set dockerd.dockerman.debug_path=`uci get dockerd.dockerman.debug_path 2&> /dev/null || echo '/tmp/.docker_debug'`
|
||||
uci set dockerd.dockerman.remote_port=`uci get dockerd.dockerman.remote_port 2&> /dev/null || echo '2375'`
|
||||
uci set dockerd.dockerman.remote_endpoint=`uci get dockerd.dockerman.remote_endpoint 2&> /dev/null || echo '0'`
|
||||
uci del_list dockerd.dockerman.ac_allowed_interface='br-lan'
|
||||
uci add_list dockerd.dockerman.ac_allowed_interface='br-lan'
|
||||
uci commit dockerd
|
131
luci-app-dockerman/root/etc/init.d/dockerman
Normal file
|
@ -0,0 +1,131 @@
|
|||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
# PROCD_DEBUG=1
|
||||
config_load 'dockerd'
|
||||
# config_get daemon_ea "dockerman" daemon_ea
|
||||
_DOCKERD=/etc/init.d/dockerd
|
||||
|
||||
docker_running(){
|
||||
docker version > /dev/null 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
add_ports() {
|
||||
[ $# -eq 0 ] && return
|
||||
$($_DOCKERD running) && docker_running || return 1
|
||||
ids=$@
|
||||
for id in $ids; do
|
||||
id=$(docker ps --filter "ID=$id" --quiet)
|
||||
[ -z "$id" ] && {
|
||||
echo "Docker containner not running";
|
||||
return 1;
|
||||
}
|
||||
ports=$(docker ps --filter "ID=$id" --format "{{.Ports}}")
|
||||
# echo "$ports"
|
||||
for port in $ports; do
|
||||
echo "$port" | grep -qE "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:.*$" || continue;
|
||||
[ "${port: -1}" == "," ] && port="${port:0:-1}"
|
||||
local protocol=""
|
||||
[ "${port%tcp}" != "$port" ] && protocol="/tcp"
|
||||
[ "${port%udp}" != "$port" ] && protocol="/udp"
|
||||
[ "$protocol" == "" ] && continue
|
||||
port="${port%%->*}"
|
||||
port="${port##*:}"
|
||||
uci_add_list dockerd dockerman ac_allowed_ports "${port}${protocol}"
|
||||
done
|
||||
done
|
||||
uci_commit dockerd
|
||||
}
|
||||
|
||||
|
||||
convert() {
|
||||
_convert() {
|
||||
_id=$1
|
||||
_id=$(docker ps --all --filter "ID=$_id" --quiet)
|
||||
if [ -z "$_id" ]; then
|
||||
uci_remove_list dockerd dockerman ac_allowed_container "$1"
|
||||
return
|
||||
fi
|
||||
if /etc/init.d/dockerman add_ports "$_id"; then
|
||||
uci_remove_list dockerd dockerman ac_allowed_container "$_id"
|
||||
fi
|
||||
}
|
||||
config_list_foreach dockerman ac_allowed_container _convert
|
||||
uci_commit dockerd
|
||||
}
|
||||
|
||||
iptables_append(){
|
||||
# Wait for a maximum of 10 second per command, retrying every millisecond
|
||||
local iptables_wait_args="--wait 10 --wait-interval 1000"
|
||||
if ! iptables ${iptables_wait_args} --check $@ 2>/dev/null; then
|
||||
iptables ${iptables_wait_args} -A $@ 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
init_dockerman_chain(){
|
||||
iptables -N DOCKER-MAN >/dev/null 2>&1
|
||||
iptables -F DOCKER-MAN >/dev/null 2>&1
|
||||
iptables -D DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1
|
||||
iptables -I DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1
|
||||
}
|
||||
|
||||
delete_dockerman_chain(){
|
||||
iptables -D DOCKER-USER -j DOCKER-MAN >/dev/null 2>&1
|
||||
iptables -F DOCKER-MAN >/dev/null 2>&1
|
||||
iptables -X DOCKER-MAN >/dev/null 2>&1
|
||||
}
|
||||
|
||||
add_allowed_interface(){
|
||||
iptables_append DOCKER-MAN -i $1 -o docker0 -j RETURN
|
||||
}
|
||||
|
||||
add_allowed_ports(){
|
||||
port=$1
|
||||
if [ "${port%/tcp}" != "$port" ]; then
|
||||
iptables_append DOCKER-MAN -p tcp -m conntrack --ctorigdstport ${port%/tcp} --ctdir ORIGINAL -j RETURN
|
||||
elif [ "${port%/udp}" != "$port" ]; then
|
||||
iptables_append DOCKER-MAN -p udp -m conntrack --ctorigdstport ${port%/udp} --ctdir ORIGINAL -j RETURN
|
||||
fi
|
||||
}
|
||||
|
||||
handle_allowed_ports(){
|
||||
config_list_foreach "dockerman" "ac_allowed_ports" add_allowed_ports
|
||||
}
|
||||
|
||||
handle_allowed_interface(){
|
||||
config_list_foreach "dockerman" "ac_allowed_interface" add_allowed_interface
|
||||
iptables_append DOCKER-MAN -m conntrack --ctstate ESTABLISHED,RELATED -o docker0 -j RETURN >/dev/null 2>&1
|
||||
iptables_append DOCKER-MAN -m conntrack --ctstate NEW,INVALID -o docker0 -j DROP >/dev/null 2>&1
|
||||
iptables_append DOCKER-MAN -j RETURN >/dev/null 2>&1
|
||||
}
|
||||
|
||||
start_service(){
|
||||
[ -x "$_DOCKERD" ] && $($_DOCKERD enabled) || return 0
|
||||
delete_dockerman_chain
|
||||
$($_DOCKERD running) && docker_running || return 0
|
||||
init_dockerman_chain
|
||||
handle_allowed_ports
|
||||
handle_allowed_interface
|
||||
}
|
||||
|
||||
stop_service(){
|
||||
delete_dockerman_chain
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger 'dockerd'
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
start
|
||||
}
|
||||
|
||||
boot() {
|
||||
sleep 5s
|
||||
start
|
||||
}
|
||||
|
||||
extra_command "add_ports" "Add allowed ports based on the container ID(s)"
|
||||
extra_command "convert" "Convert Ac allowed container to AC allowed ports"
|
36
luci-app-dockerman/root/etc/uci-defaults/luci-app-dockerman
Normal file
|
@ -0,0 +1,36 @@
|
|||
#!/bin/sh
|
||||
|
||||
. $IPKG_INSTROOT/lib/functions.sh
|
||||
|
||||
[ -x "$(command -v dockerd)" ] && chmod +x /etc/init.d/dockerman && /etc/init.d/dockerman enable >/dev/null 2>&1
|
||||
sed -i 's/self:cfgvalue(section) or {}/self:cfgvalue(section) or self.default or {}/' /usr/lib/lua/luci/view/cbi/dynlist.htm
|
||||
/etc/init.d/uhttpd restart >/dev/null 2>&1
|
||||
rm -fr /tmp/luci-indexcache /tmp/luci-modulecache >/dev/null 2>&1
|
||||
touch /etc/config/dockerd
|
||||
ls /etc/rc.d/*dockerd &> /dev/null && uci -q set dockerd.globals.auto_start="1" || uci -q set dockerd.globals.auto_start="0"
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set uhttpd.main.script_timeout="3600"
|
||||
commit uhttpd
|
||||
set dockerd.dockerman=dockerman
|
||||
set dockerd.dockerman.socket_path='/var/run/docker.sock'
|
||||
set dockerd.dockerman.status_path='/tmp/.docker_action_status'
|
||||
set dockerd.dockerman.debug='false'
|
||||
set dockerd.dockerman.debug_path='/tmp/.docker_debug'
|
||||
set dockerd.dockerman.remote_endpoint='0'
|
||||
|
||||
del_list dockerd.dockerman.ac_allowed_interface='br-lan'
|
||||
add_list dockerd.dockerman.ac_allowed_interface='br-lan'
|
||||
|
||||
commit dockerd
|
||||
EOF
|
||||
# remove dockerd firewall
|
||||
config_load dockerd
|
||||
remove_firewall(){
|
||||
cfg=${1}
|
||||
uci_remove dockerd ${1}
|
||||
}
|
||||
config_foreach remove_firewall firewall
|
||||
# Convert ac_allowed_container to ac_allowed_ports
|
||||
(sleep 30s && /etc/init.d/dockerman convert;/etc/init.d/dockerman restart) &
|
||||
|
||||
exit 0
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"luci-app-dockerman": {
|
||||
"description": "Grant UCI access for luci-app-dockerman",
|
||||
"read": {
|
||||
"uci": [ "dockerd" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "dockerd" ]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ LUCI_PKGARCH:=all
|
|||
|
||||
PKG_NAME:=luci-app-zerotier
|
||||
PKG_VERSION:=1.0
|
||||
PKG_RELEASE:=20
|
||||
PKG_RELEASE:=21
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
module("luci.controller.zerotier", package.seeall)
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/zerotier") then
|
||||
return
|
||||
end
|
||||
if not nixio.fs.access("/etc/config/zerotier") then
|
||||
return
|
||||
end
|
||||
|
||||
entry({"admin", "vpn"}, firstchild(), "VPN", 45).dependent = false
|
||||
entry({"admin", "vpn"}, firstchild(), "VPN", 45).dependent = false
|
||||
|
||||
entry({"admin", "vpn", "zerotier"}, alias("admin", "vpn", "zerotier", "general"), _("ZeroTier"), 99)
|
||||
entry({"admin", "vpn", "zerotier", "general"}, cbi("zerotier/settings"), _("Base Setting"), 1)
|
||||
entry({"admin", "vpn", "zerotier", "log"}, form("zerotier/info"), _("Interface Info"), 2)
|
||||
entry({"admin", "vpn", "zerotier"}, alias("admin", "vpn", "zerotier", "general"), _("ZeroTier"), 99)
|
||||
|
||||
entry({"admin", "vpn", "zerotier", "status"}, call("act_status"))
|
||||
entry({"admin", "vpn", "zerotier", "general"}, cbi("zerotier/settings"), _("Base Setting"), 1)
|
||||
entry({"admin", "vpn", "zerotier", "log"}, form("zerotier/info"), _("Interface Info"), 2)
|
||||
entry({"admin", "vpn", "zerotier", "manual"}, cbi("zerotier/manual"), _("Manual Config"), 3)
|
||||
|
||||
entry({"admin", "vpn", "zerotier", "status"}, call("act_status"))
|
||||
end
|
||||
|
||||
function act_status()
|
||||
local e = {}
|
||||
e.running = luci.sys.call("pgrep /usr/bin/zerotier-one >/dev/null") == 0
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
local e = {}
|
||||
e.running = luci.sys.call("pgrep /usr/bin/zerotier-one >/dev/null") == 0
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
|
|
|
@ -7,8 +7,8 @@ t = f:field(TextValue, "conf")
|
|||
t.rmempty = true
|
||||
t.rows = 19
|
||||
function t.cfgvalue()
|
||||
luci.sys.exec("for i in $(ifconfig | grep 'zt' | awk '{print $1}'); do ifconfig $i; done > /tmp/zero.info")
|
||||
return fs.readfile(conffile) or ""
|
||||
luci.sys.exec("for i in $(ifconfig | grep 'zt' | awk '{print $1}'); do ifconfig $i; done > /tmp/zero.info")
|
||||
return fs.readfile(conffile) or ""
|
||||
end
|
||||
t.readonly = "readonly"
|
||||
|
||||
|
|
26
luci-app-zerotier/luasrc/model/cbi/zerotier/manual.lua
Normal file
|
@ -0,0 +1,26 @@
|
|||
local m, s, o
|
||||
local fs = require "nixio.fs"
|
||||
local jsonc = require "luci.jsonc" or nil
|
||||
m = Map("zerotier")
|
||||
s = m:section(NamedSection, "sample_config", "zerotier")
|
||||
s.anonymous = true
|
||||
s.addremove = false
|
||||
o = s:option(TextValue, "manualconfig")
|
||||
o.rows = 20
|
||||
o.wrap = "soft"
|
||||
o.rmempty = true
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile("/etc/config/zero/local.conf")
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
fs.writefile("/etc/config/zero/local.conf", value:gsub("\r\n", "\n"))
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
if jsonc == nil or jsonc.parse(value) ~= nil then
|
||||
return value
|
||||
end
|
||||
return nil
|
||||
end
|
||||
o.description =
|
||||
'<a href="https://www.zerotier.com/manual/" target="_blank">https://www.zerotier.com/manual/</a><br><a href="https://github.com/zerotier/ZeroTierOne/blob/dev/service/README.md" target="_blank">https://github.com/zerotier/ZeroTierOne/blob/dev/service/README.md</a>'
|
||||
return m
|