diff --git a/package/lean/lisaac/luci-app-diskman/Makefile b/package/lean/lisaac/luci-app-diskman/Makefile new file mode 100644 index 000000000..e05d122d5 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/Makefile @@ -0,0 +1,88 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-diskman +PKG_VERSION:=v0.2.11 +PKG_RELEASE:=beta +PKG_MAINTAINER:=lisaac +PKG_LICENSE:=AGPL-3.0 + +PKG_SOURCE_PROTO:=git +PKG_SOURCE_URL:=https://github.com/lisaac/luci-app-diskman.git +PKG_SOURCE_VERSION:=$(PKG_VERSION) + +PKG_SOURCE_SUBDIR:=$(PKG_NAME) +PKG_SOURCE:=$(PKG_SOURCE_SUBDIR)-$(PKG_SOURCE_VERSION).tar.gz +PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR)/applications/luci-app-diskman + +include $(INCLUDE_DIR)/package.mk + +define Package/$(PKG_NAME)/config +config PACKAGE_$(PKG_NAME)_INCLUDE_btrfs_progs + depends on PACKAGE_$(PKG_NAME) + bool "Include btrfs-progs" + default y +config PACKAGE_$(PKG_NAME)_INCLUDE_lsblk + depends on PACKAGE_$(PKG_NAME) + bool "Include lsblk" + default y +config PACKAGE_$(PKG_NAME)_INCLUDE_mdadm + depends on PACKAGE_$(PKG_NAME) + 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) + SECTION:=luci + CATEGORY:=LuCI + SUBMENU:=3. Applications + TITLE:=Disk Manager interface for LuCI + PKGARCH:=all + DEPENDS:=+e2fsprogs +parted +smartmontools +blkid \ + +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 +endef + +define Package/$(PKG_NAME)/description + Disk Manager interface for LuCI +endef + +define Build/Prepare + tar -xzvf $(DL_DIR)/$(PKG_SOURCE) -C $(BUILD_DIR) +endef + +define Build/Compile +endef + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +rm -fr /tmp/luci-indexcache /tmp/luci-modulecache +endef + +define Package/$(PKG_NAME)/install + # $(INSTALL_DIR) $(1)/ + # cp -pR $(PKG_BUILD_DIR)/root/* $(1)/ + # $(INSTALL_DIR) $(1)/www + # cp -pR $(PKG_BUILD_DIR)/htdoc/* $(1)/www + $(INSTALL_DIR) $(1)/usr/lib/lua/luci + cp -pR $(PKG_BUILD_DIR)/luasrc/* $(1)/usr/lib/lua/luci/ + $(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n + $$(foreach po,$$(shell find $(PKG_BUILD_DIR)/po/*/*.po), \ + po2lmo $$(po) \ + $(1)/usr/lib/lua/luci/i18n/diskman.$$(shell echo $$(po) | awk -F'/' '{print $$$$(NF-1)}').lmo;) + #po2lmo $(PKG_BUILD_DIR)/po/zh-cn/diskman.po $(1)/usr/lib/lua/luci/i18n/diskman.zh-cn.lmo +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/package/lean/lisaac/luci-app-diskman/depends.lst b/package/lean/lisaac/luci-app-diskman/depends.lst new file mode 100644 index 000000000..6227107ba --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/depends.lst @@ -0,0 +1 @@ +e2fsprogs parted smartmontools blkid mdadm btrfs-progs util-linux \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/controller/diskman.lua b/package/lean/lisaac/luci-app-diskman/luasrc/controller/diskman.lua new file mode 100644 index 000000000..1cf9bd03e --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/controller/diskman.lua @@ -0,0 +1,155 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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 \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua b/package/lean/lisaac/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua new file mode 100644 index 000000000..006007853 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua @@ -0,0 +1,210 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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 diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/model/cbi/diskman/disks.lua b/package/lean/lisaac/luci-app-diskman/luasrc/model/cbi/diskman/disks.lua new file mode 100644 index 000000000..c209df0aa --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/model/cbi/diskman/disks.lua @@ -0,0 +1,327 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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.. "
" + 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"] = ''..new_mp..'' + 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 diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua b/package/lean/lisaac/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua new file mode 100644 index 000000000..348a617a6 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua @@ -0,0 +1,366 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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 = '' ..new_mp ..'' .. "
" + 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 \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/model/diskman.lua b/package/lean/lisaac/luci-app-diskman/luasrc/model/diskman.lua new file mode 100644 index 000000000..98c1f7739 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/model/diskman.lua @@ -0,0 +1,738 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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 diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/disabled_button.htm b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/disabled_button.htm new file mode 100644 index 000000000..1ad4eca3b --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/disabled_button.htm @@ -0,0 +1,7 @@ +<%+cbi/valueheader%> + <% if self:cfgvalue(section) ~= false then %> + " 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%> \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/format_button.htm b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/format_button.htm new file mode 100644 index 000000000..18e306e27 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/format_button.htm @@ -0,0 +1,7 @@ +<%+cbi/valueheader%> + <% if self:cfgvalue(section) ~= false then %> + " 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%> diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/inlinebutton.htm b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/inlinebutton.htm new file mode 100644 index 000000000..b1b193257 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/inlinebutton.htm @@ -0,0 +1,7 @@ +
+ <% if self:cfgvalue(section) ~= false then %> + " type="submit"" <% if self.disable then %>disabled <% end %><%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> /> + <% else %> + - + <% end %> +
diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/xnullsection.htm b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/xnullsection.htm new file mode 100644 index 000000000..69aa65e00 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/xnullsection.htm @@ -0,0 +1,37 @@ +
+ <% if self.title and #self.title > 0 then -%> + <%=self.title%> + <%- end %> + <% if self.description and #self.description > 0 then -%> +
<%=self.description%>
+ <%- end %> +
+
+ <% self:render_children(1, scope or {}) %> +
+ <% if self.error and self.error[1] then -%> +
+
    <% for _, e in ipairs(self.error[1]) do -%> +
  • + <%- 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 -%> +
  • + <%- end %>
+
+ <%- end %> +
+
+<%- + if type(self.hidden) == "table" then + for k, v in pairs(self.hidden) do +-%> + +<%- + end + end +%> \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/xsimpleform.htm b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/xsimpleform.htm new file mode 100644 index 000000000..a831bfc77 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/cbi/xsimpleform.htm @@ -0,0 +1,88 @@ +<% if not self.embedded then %> +
> + + + <% + end + + %>
<% + + if self.title and #self.title > 0 then + %>

<%=self.title%>

<% + end + + if self.description and #self.description > 0 then + %>
<%=self.description%>
<% + end + + self:render_children() + + %>
<% + + if self.message then + %>
<%=self.message%>
<% + end + + if self.errmessage then + %>
<%=self.errmessage%>
<% + end + + if not self.embedded then + if type(self.hidden) == "table" then + local k, v + for k, v in pairs(self.hidden) do + %><% + end + end + + local display_back = (self.redirect) + local display_cancel = (self.cancel ~= false and self.on_cancel) + local display_skip = (self.flow and self.flow.skip) + local display_submit = (self.submit ~= false) + local display_reset = (self.reset ~= false) + + if display_back or display_cancel or display_skip or display_submit or display_reset then + %>
<% + + if display_back then + %> <% + end + + if display_cancel then + local label = pcdata(self.cancel or translate("Cancel")) + %> <% + end + + if display_skip then + %> <% + end + + if display_submit then + local label = pcdata(self.submit or translate("Submit")) + %> <% + end + + if display_reset then + local label = pcdata(self.reset or translate("Reset")) + %> <% + end + + %>
<% + end + + %>
<% + end +%> + + diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/disk_info.htm b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/disk_info.htm new file mode 100644 index 000000000..118acd50d --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/disk_info.htm @@ -0,0 +1,108 @@ + diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/partition_info.htm b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/partition_info.htm new file mode 100644 index 000000000..78f5c1bd7 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/partition_info.htm @@ -0,0 +1,129 @@ + + \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/smart_detail.htm b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/smart_detail.htm new file mode 100644 index 000000000..56a9139f0 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/luasrc/view/diskman/smart_detail.htm @@ -0,0 +1,79 @@ + + + S.M.A.R.T detail of <%=dev%> + + + + +
+
+ <%:S.M.A.R.T Attrbutes%>: /dev/<%=dev%> + + + <% if dev:match("nvme") then %> + + <% else %> + + + + + + + + + + <% end %> + + + + +
<%:ID%><%:Attrbute%><%:Flag%><%:Value%><%:Worst%><%:Thresh%><%:Type%><%:Updated%><%:Raw%>

<%:Collecting data...%>
+
+
+ + \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-diskman/po/pl/diskman.po b/package/lean/lisaac/luci-app-diskman/po/pl/diskman.po new file mode 100644 index 000000000..169655633 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/po/pl/diskman.po @@ -0,0 +1,239 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8\n" + +msgid "DiskMan" +msgstr "Menadżer dysków" + +msgid "Manage Disks over LuCI." +msgstr "Zarządzaj dyskami przez LuCI." + +msgid "Rescan Disks" +msgstr "Skanuj dyski" + +msgid "Disks" +msgstr "Dyski" + +msgid "Path" +msgstr "Ścieżka" + +msgid "Serial Number" +msgstr "Numer seryjny" + +msgid "Temp" +msgstr "Temperatura" + +msgid "Partition Table" +msgstr "Tablica partycji" + +msgid "SATA Version" +msgstr "Wersja SATA" + +msgid "Health" +msgstr "Kondycja" + +msgid "File System" +msgstr "System plików" + +msgid "Mount Options" +msgstr "Opcje montowania" + +msgid "Mount" +msgstr "Montuj" + +msgid "Umount" +msgstr "Odmontuj" + +msgid "Eject" +msgstr "Wysuń" + +msgid "New" +msgstr "Nowa" + +msgid "Remove" +msgstr "Wysuń" + +msgid "Format" +msgstr "Formatuj" + +msgid "Start Sector" +msgstr "Sektor początkowy" + +msgid "End Sector" +msgstr "Sektor końcowy" + +msgid "Usage" +msgstr "Wykorzystane" + +msgid "Used" +msgstr "Zajęte" + +msgid "Free Space" +msgstr "Wolna przestrzeń" + +msgid "Model" +msgstr "Model urządzenia" + +msgid "Size" +msgstr "Rozmiar" + +msgid "Status" +msgstr "Status" + +msgid "Mount Point" +msgstr "Punkt montowania" + +msgid "Sector Size" +msgstr "Rozmiar sektora" + +msgid "Rotation Rate" +msgstr "Obroty" + +msgid "RAID Devices" +msgstr "Urządzenia RAID" + +msgid "RAID mode" +msgstr "Tryb RAID" + +msgid "Members" +msgstr "Członkowie" + +msgid "Active" +msgstr "Aktywny" + +msgid "RAID Creation" +msgstr "Kreator RAID" + +msgid "Raid Name" +msgstr "Nazwa RAID" + +msgid "Raid Level" +msgstr "Poziom RAID" + +msgid "Raid Member" +msgstr "Członek RAID" + +msgid "Create Raid" +msgstr "Utwórz RAID" + +msgid "Partition Management" +msgstr "Menadżer partycji" + +msgid "Partition Disk over LuCI." +msgstr "Zarządzaj partycjami przez LuCI." + +msgid "Device Info" +msgstr "Informacja o urządzeniu" + +msgid "Disk Man" +msgstr "Menadżer dysków" + +msgid "Partitions Info" +msgstr "Informacja o partycjach" + +msgid "Default 2048 sector alignment, support +size{b,k,m,g,t} in End Sector" +msgstr "Sektory wyrównywane są do wielokrotności 2048, sektor końcowy można podać jako rozmiar w postaci +size{b,k,m,g,t}" + +msgid "Multiple Devices Btrfs Creation" +msgstr "Kreator urządzeń Btrfs" + +msgid "Label" +msgstr "Etykieta" + +msgid "Btrfs Label" +msgstr "Etykieta Btrfs" + +msgid "Btrfs Raid Level" +msgstr "Poziom Raid Btrfs" + +msgid "Btrfs Member" +msgstr "Członek Btrfs" + +msgid "Create Btrfs" +msgstr "Utwórz Btrfs" + +msgid "New Snapshot" +msgstr "Nowy obraz" + +msgid "SubVolumes" +msgstr "Sub-wolumeny" + +msgid "Top Level" +msgstr "Top Level" + +msgid "Manage Btrfs" +msgstr "Zarządzaj Btrfs" + +msgid "Otime" +msgstr "Otime" + +msgid "Snapshots" +msgstr "Obrazy" + +msgid "Set Default" +msgstr "Ustaw domyślnie" + +msgid "Source Path" +msgstr "Ścieżka źródłowa" + +msgid "Readonly" +msgstr "Tylko do odczytu" + +msgid "Delete" +msgstr "Usuń" + +msgid "Create" +msgstr "Utwórz" + +msgid "Destination Path (optional)" +msgstr "Ścieżka docelowa (opcjonalnie)" + +msgid "Metadata" +msgstr "Metadata" + +msgid "Data" +msgstr "Data" + +msgid "Btrfs Info" +msgstr "Informacja o Btrfs" + +msgid "The source path for create the snapshot" +msgstr "Ścieżka źródłowa do utworzenia obrazu" + +msgid "The path where you want to store the snapshot" +msgstr "Ścieżka w której chcesz przechowywać obraz" + +msgid "Please input Source Path of snapshot, Source Path must start with '/'" +msgstr "Proszę podać ścieżkę źródłową dla obrazu, ścieżka musi zaczynać się od '/'" + +msgid "Please input Subvolume Path, Subvolume must start with '/'" +msgstr "Proszę podać ścieżkę dla sub-wolumenu, sub-wolumen musi zaczynać się od '/'" + +msgid "is in use! please unmount it first!" +msgstr "aktualnie jest w użyciu, proszę najpierw odmontować!" + +msgid "Partition NOT found!" +msgstr "Nie znaleziono partycji!" + +msgid "Filesystem NOT support!" +msgstr "System plików nie jest obsługiwany!" + +msgid "Invalid Start Sector!" +msgstr "Nieprawidłowy sektor początkowy!" + +msgid "Invalid End Sector" +msgstr "Nieprawidłowy sektor końcowy!" + +msgid "Partition not exists!" +msgstr "Partycja nie istnieje!" + +msgid "Creation" +msgstr "Kreator" + +msgid "Please select file system!" +msgstr "Proszę wybrać system plików!" + +msgid "Format partation:" +msgstr "Format partycji:" + +msgid "Warnning !! \nTHIS WILL OVERWRITE EXISTING PARTITIONS!! \nModify the partition table?" +msgstr "Ostrzeżenie !! \nISTNIEJĄCE PARTYCJE ZOSTANĄ NADPISANE!! \nZmodyfikować tablicę partycji?" diff --git a/package/lean/lisaac/luci-app-diskman/po/zh-cn/diskman.po b/package/lean/lisaac/luci-app-diskman/po/zh-cn/diskman.po new file mode 100644 index 000000000..f380fc586 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/po/zh-cn/diskman.po @@ -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确定修改分区表?" diff --git a/package/lean/lisaac/luci-app-diskman/po/zh_Hans b/package/lean/lisaac/luci-app-diskman/po/zh_Hans new file mode 120000 index 000000000..41451e4a1 --- /dev/null +++ b/package/lean/lisaac/luci-app-diskman/po/zh_Hans @@ -0,0 +1 @@ +zh-cn \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-dockerman/Makefile b/package/lean/lisaac/luci-app-dockerman/Makefile new file mode 100644 index 000000000..51dfa5c09 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/Makefile @@ -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 \ + Florian Eckert + +PKG_VERSION:=v0.5.25 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/package/lean/lisaac/luci-app-dockerman/depends.lst b/package/lean/lisaac/luci-app-dockerman/depends.lst new file mode 100644 index 000000000..8a62f6a74 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/depends.lst @@ -0,0 +1 @@ +ttyd docker-cli \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/containers.svg b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/containers.svg new file mode 100644 index 000000000..4165f90bd --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/containers.svg @@ -0,0 +1,7 @@ + + + + + Docker icon + + diff --git a/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/file-icon.png b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/file-icon.png new file mode 100644 index 000000000..f156dc1c7 Binary files /dev/null and b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/file-icon.png differ diff --git a/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/file-manager.css b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/file-manager.css new file mode 100644 index 000000000..911693b62 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/file-manager.css @@ -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; +} \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/folder-icon.png b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/folder-icon.png new file mode 100644 index 000000000..1370df3ad Binary files /dev/null and b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/folder-icon.png differ diff --git a/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/images.svg b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/images.svg new file mode 100644 index 000000000..90ca5a1c7 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/images.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/link-icon.png b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/link-icon.png new file mode 100644 index 000000000..03cc82cdf Binary files /dev/null and b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/link-icon.png differ diff --git a/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/networks.svg b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/networks.svg new file mode 100644 index 000000000..3eb12a393 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/networks.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/tar.min.js b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/tar.min.js new file mode 100644 index 000000000..d9c06667f --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/htdocs/luci-static/resources/dockerman/tar.min.js @@ -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;ia+=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;imaxLength) +{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{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 + + + + + diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/controller/dockerman.lua b/package/lean/lisaac/luci-app-dockerman/luasrc/controller/dockerman.lua new file mode 100644 index 000000000..50c8a9c15 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/controller/dockerman.lua @@ -0,0 +1,614 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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","
"):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 diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua new file mode 100644 index 000000000..f62650fe5 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua @@ -0,0 +1,152 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2021 Florian Eckert +Copyright 2021 lisaac +]]-- + +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 diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua new file mode 100644 index 000000000..5cf42100a --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua @@ -0,0 +1,802 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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 .. "
") or "") .. out[1]["HostPort"] .. ":" .. inter + end + end + + return data +end + +local get_env = function(d) + local data + + if d.Config and d.Config.Env then + for _,v in ipairs(d.Config.Env) do + data = (data and (data .. "
") or "") .. v + end + end + + return data +end + +local get_command = function(d) + local data + + if d.Config and d.Config.Cmd then + for _,v in ipairs(d.Config.Cmd) do + data = (data and (data .. " ") or "") .. v + end + end + + return data +end + +local get_mounts = function(d) + local data + + if d.Mounts then + for _,v in ipairs(d.Mounts) do + local v_sorce_d, v_dest_d + local v_sorce = "" + local v_dest = "" + for v_sorce_d in v["Source"]:gmatch('[^/]+') do + if v_sorce_d and #v_sorce_d > 12 then + v_sorce = v_sorce .. "/" .. v_sorce_d:sub(1,12) .. "..." + else + v_sorce = v_sorce .."/".. v_sorce_d + end + end + for v_dest_d in v["Destination"]:gmatch('[^/]+') do + if v_dest_d and #v_dest_d > 12 then + v_dest = v_dest .. "/" .. v_dest_d:sub(1,12) .. "..." + else + v_dest = v_dest .."/".. v_dest_d + end + end + data = (data and (data .. "
") or "") .. v_sorce .. ":" .. v["Destination"] .. (v["Mode"] ~= "" and (":" .. v["Mode"]) or "") + end + end + + return data +end + +local get_device = function(d) + local data + + if d.HostConfig and d.HostConfig.Devices then + for _,v in ipairs(d.HostConfig.Devices) do + data = (data and (data .. "
") or "") .. v["PathOnHost"] .. ":" .. v["PathInContainer"] .. (v["CgroupPermissions"] ~= "" and (":" .. v["CgroupPermissions"]) or "") + end + end + + return data +end + +local get_links = function(d) + local data + + if d.HostConfig and d.HostConfig.Links then + for _,v in ipairs(d.HostConfig.Links) do + data = (data and (data .. "
") or "") .. v + end + end + + return data +end + +local get_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 .. "
") 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 .. "
") 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 .. "
") 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","
"):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 .. "
" .. 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 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 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 uci = (require "luci.model.uci").cursor() + 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 diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua new file mode 100644 index 000000000..f660f1847 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua @@ -0,0 +1,284 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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"] = ""..v.Names[1]:sub(2).."" + data[index]["_status"] = "".. data[index]["_status"] .. "" .. "


" + else + data[index]["_name"] = ""..v.Names[1]:sub(2).."" + data[index]["_status"] = ''.. data[index]["_status"] .. "" + end + + if (type(v.NetworkSettings) == "table" and type(v.NetworkSettings.Networks) == "table") then + for networkname, netconfig in pairs(v.NetworkSettings.Networks) do + data[index]["_network"] = (data[index]["_network"] ~= nil and (data[index]["_network"] .." | ") or "").. networkname .. (netconfig.IPAddress ~= "" and (": " .. netconfig.IPAddress) or "") + end + end + + -- networkmode = v.HostConfig.NetworkMode ~= "default" and v.HostConfig.NetworkMode or "bridge" + -- data[index]["_network"] = v.NetworkSettings.Networks[networkmode].IPAddress or nil + -- local _, _, image = v.Image:find("^sha256:(.+)") + -- if image ~= nil then + -- image=image:sub(1,12) + -- end + + if v.Ports 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 ('') 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 "" 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"] = ''.. data[index]["_name"] .. "
ID: " .. data[index]["_id"] + .. "

Image: " .. (data[index]["_image"] or "<none>") + .. "
" + + 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"] .. "
") or "") .. '' .. v_sorce .. "→" .. v_dest..'' + 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","
"):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 diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua new file mode 100644 index 000000000..4d086f16c --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua @@ -0,0 +1,284 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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"] = '' .. v.Id:sub(8,20) .. '' + + 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"] .. "
" )or "") .. ((v1:match("") or (#v.RepoTags == 1)) and v1 or ('' .. v1 .. '')) + + 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 "" ).. ":" + end + + data[index]["_tags"] = data[index]["_tags"]:gsub("","<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 "").. + ''.. cv.Names[1]:sub(2).."" + end + end + + data[index]["_size"] = string.format("%.2f", tostring(v.Size/1024/1024)).."MB" + data[index]["_created"] = os.date("%Y/%m/%d %H:%M:%S",v.Created) + end + + return data +end + +local image_list = 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("
") 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","
"):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 diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua new file mode 100644 index 000000000..8837dd564 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua @@ -0,0 +1,159 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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","
"):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 diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua new file mode 100644 index 000000000..04695a613 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua @@ -0,0 +1,923 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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","
"):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 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 diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua new file mode 100644 index 000000000..84234542f --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua @@ -0,0 +1,258 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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","
"):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 diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua new file mode 100644 index 000000000..1d2bb905b --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua @@ -0,0 +1,151 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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.") +.. + " " .. + [[]] .. + translate("Github") .. + [[]]) +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","
"):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 diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua new file mode 100644 index 000000000..0a1ab18f9 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua @@ -0,0 +1,142 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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 "").. + ''.. cv.Names[1]:sub(2)..'' + end + end + end + data[index]["_driver"] = v.Driver + data[index]["_mountpoint"] = nil + + for v1 in v.Mountpoint:gmatch('[^/]+') do + if v1 == index then + data[index]["_mountpoint"] = data[index]["_mountpoint"] .."/" .. v1:sub(1,12) .. "..." + else + data[index]["_mountpoint"] = (data[index]["_mountpoint"] and data[index]["_mountpoint"] or "").."/".. v1 + end + end + data[index]["_created"] = v.CreatedAt + data[index]["_size"] = "-" + 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","
"):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 diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/model/docker.lua b/package/lean/lisaac/luci-app-dockerman/luasrc/model/docker.lua new file mode 100644 index 000000000..2a902912a --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/model/docker.lua @@ -0,0 +1,507 @@ +--[[ +LuCI - Lua Configuration Interface +Copyright 2019 lisaac +]]-- + +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 diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/apply_widget.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/apply_widget.htm new file mode 100644 index 000000000..f96b2d72a --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/apply_widget.htm @@ -0,0 +1,147 @@ + + + diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinebutton.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinebutton.htm new file mode 100644 index 000000000..a061a6dba --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinebutton.htm @@ -0,0 +1,7 @@ +
+ <% if self:cfgvalue(section) ~= false then %> + " type="submit"" <% if self.disable then %>disabled <% end %><%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> /> + <% else %> + - + <% end %> +
diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinevalue.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinevalue.htm new file mode 100644 index 000000000..e4b0cf7a0 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinevalue.htm @@ -0,0 +1,33 @@ +
+ + <%- if self.password then -%> + /> + <%- end -%> + 0, "data-choices", { self.keylist, self.vallist }) + %> /> + <%- if self.password then -%> +
+ <% end %> +
diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/namedsection.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/namedsection.htm new file mode 100644 index 000000000..244d2c10a --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/namedsection.htm @@ -0,0 +1,9 @@ +<% if self:cfgvalue(self.section) then section = self.section %> +
+ <%+cbi/tabmenu%> +
+ <%+cbi/ucisection%> +
+
+<% end %> + diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/xfvalue.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/xfvalue.htm new file mode 100644 index 000000000..04f7bc2ee --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/cbi/xfvalue.htm @@ -0,0 +1,10 @@ +<%+cbi/valueheader%> + /> + disabled <% end %><%= + attr("id", cbid) .. attr("name", cbid) .. attr("value", self.enabled or 1) .. + ifattr((self:cfgvalue(section) or self.default) == self.enabled, "checked", "checked") + %> /> + > +<%+cbi/valuefooter%> diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container.htm new file mode 100644 index 000000000..3684b6447 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container.htm @@ -0,0 +1,28 @@ +
+ + + diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container_console.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container_console.htm new file mode 100644 index 000000000..7f626b3dc --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container_console.htm @@ -0,0 +1,6 @@ +
+ +
+ diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm new file mode 100644 index 000000000..2e0650d9d --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm @@ -0,0 +1,332 @@ + +
+ +
+ + +
+
+
+ + diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container_stats.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container_stats.htm new file mode 100644 index 000000000..bbcd633e7 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/container_stats.htm @@ -0,0 +1,81 @@ + diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/containers_running_stats.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/containers_running_stats.htm new file mode 100644 index 000000000..d88e28be9 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/containers_running_stats.htm @@ -0,0 +1,91 @@ + \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/images_import.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/images_import.htm new file mode 100644 index 000000000..e6c28f8c7 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/images_import.htm @@ -0,0 +1,104 @@ + + +
+ disabled <% end %> /> + +
+ + diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/images_load.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/images_load.htm new file mode 100644 index 000000000..5ccc1f4e3 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/images_load.htm @@ -0,0 +1,40 @@ +
+ disabled <% end %> /> + +
+ diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/logs.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/logs.htm new file mode 100644 index 000000000..6cd2cb095 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/logs.htm @@ -0,0 +1,13 @@ +<% if self.title == "Events" then %> +<%+header%> +

<%:Docker - Events%>

+
+

<%:Events%>

+<% end %> +
+ +
+<% if self.title == "Events" then %> +
+<%+footer%> +<% end %> diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm new file mode 100644 index 000000000..ee7c151c4 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm @@ -0,0 +1,102 @@ + + + +<%+cbi/valueheader%> + + + +<%+cbi/valuefooter%> diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/overview.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/overview.htm new file mode 100644 index 000000000..e491fc512 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/overview.htm @@ -0,0 +1,197 @@ + + +
+
+
+
+
+ +
+
+
+

<%:Containers%>

+

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

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

<%:Images%>

+

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

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

<%:Networks%>

+

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

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

<%:Volumes%>

+

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

+
+
+
+
diff --git a/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/volume_size.htm b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/volume_size.htm new file mode 100644 index 000000000..dc024734b --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/luasrc/view/dockerman/volume_size.htm @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-dockerman/po/templates/dockerman.pot b/package/lean/lisaac/luci-app-dockerman/po/templates/dockerman.pot new file mode 100644 index 000000000..0d6a5de98 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/po/templates/dockerman.pot @@ -0,0 +1,1002 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:619 +msgid "A list of kernel capabilities to add to the container" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:69 +msgid "Access Control" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:223 +msgid "Add" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:595 +msgid "Add host device to the container" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:571 +msgid "Advance" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:586 +msgid "Allocates an ephemeral host port for all of a container's exposed ports" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:118 +msgid "Allowed access interfaces" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:498 +msgid "Always pull image first" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:29 +msgid "" +"An overview with the relevant data is displayed here with which the LuCI " +"docker client is connected." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:43 +msgid "Api Version" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:94 +msgid "Auto create macvlan interface in Openwrt" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:18 +msgid "Auto start" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:134 +msgid "Available" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:47 +msgid "Base device" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:553 +msgid "Bind Mount(-v)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:554 +msgid "Bind mount a volume" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:596 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:652 +msgid "Block IO Weight" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:653 +msgid "" +"Block IO weight (relative weight) accepts a weight value between 10 and 1000" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:597 +msgid "" +"Block IO weight (relative weight) accepts a weight value between 10 and 1000." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:61 +msgid "Bridge (Support direct communication between MAC VLANs)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:42 +msgid "Bridge device" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:84 +msgid "" +"By entering a valid image name with the corresponding version, the docker " +"image can be downloaded from the configured registry." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:618 +msgid "CAP-ADD(--cap-add)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:581 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:635 +msgid "CPU Shares Weight" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:779 +msgid "CPU Useage" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:636 +msgid "" +"CPU shares relative weight, if 0 is set, the system will ignore the value " +"and use the default of 1024" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:582 +msgid "" +"CPU shares relative weight, if 0 is set, the system will ignore the value " +"and use the default of 1024." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:573 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:626 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:44 +msgid "CPUs" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:159 +msgid "Can NOT connect to docker daemon, please check!!" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm:91 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:60 +msgid "Client connection" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:347 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:687 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:182 +msgid "Command" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm:100 +msgid "Command line" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm:72 +msgid "Command line Error" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:17 +msgid "Configuration" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:36 +msgid "Configure the default bridge network" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:405 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:707 +msgid "Connect" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:403 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:437 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:473 +msgid "Connect Network" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:74 +msgid "Connect to remote docker endpoint" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:7 +msgid "Console" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:161 +msgid "Container Info" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:650 +msgid "Container Inspect" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:671 +msgid "Container Logs" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:473 +msgid "Container Name" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:92 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:58 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:29 +msgid "Container detail" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:38 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:142 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:148 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:87 +#: applications/luci-app-dockerman/luasrc/view/dockerman/overview.htm:133 +msgid "Containers" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:94 +msgid "Create macvlan interface" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:465 +msgid "Create new docker container" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:31 +msgid "Create new docker network" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:312 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:153 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:92 +msgid "Created" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:33 +msgid "DELETING" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:371 +msgid "DNS" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:51 +msgid "Debug" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:35 +msgid "Default bridge" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:363 +msgid "Device" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:594 +msgid "Device(--device)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:396 +msgid "Disconnect" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:14 +msgid "Docker" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:12 +msgid "Docker - Configuration" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:192 +msgid "Docker - Container (%s)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:128 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:450 +msgid "Docker - Containers" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/logs.htm:3 +msgid "Docker - Events" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:72 +msgid "Docker - Images" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:15 +msgid "Docker - Network" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:54 +msgid "Docker - Networks" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:28 +msgid "Docker - Overview" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:69 +msgid "Docker - Volumes" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:16 +msgid "Docker Daemon settings" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:489 +msgid "Docker Image" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:30 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:46 +msgid "Docker Root Dir" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:93 +msgid "Docker Socket Path" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:42 +msgid "Docker Version" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/apply_widget.htm:91 +msgid "Docker actions done." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:70 +msgid "DockerMan" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:13 +msgid "DockerMan is a simple docker manager client for LuCI" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:68 +msgid "DockerMan settings" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:172 +msgid "Download" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:82 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:40 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:85 +msgid "Driver" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:265 +msgid "Duplicate/Edit" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:120 +msgid "Enable IPv6" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:351 +msgid "Env" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:546 +msgid "Environmental Variable(-e)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:54 +msgid "Error" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:42 +#: applications/luci-app-dockerman/luasrc/view/dockerman/logs.htm:5 +msgid "Events" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:116 +msgid "Exclude IPs" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:247 +msgid "Export" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:585 +msgid "Exposed All Ports(-P)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:560 +msgid "Exposed Ports(-p)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:55 +msgid "Fatal" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:6 +msgid "File" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:324 +msgid "Finish Time" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:220 +msgid "Force Remove" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:88 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:106 +msgid "Gateway" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:33 +msgid "Github" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinevalue.htm:4 +msgid "Go to relevant configuration page" +msgstr "" + +#: applications/luci-app-dockerman/root/usr/share/rpcd/acl.d/luci-app-dockerman.json:3 +msgid "Grant UCI access for luci-app-dockerman" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:330 +msgid "Healthy" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:578 +msgid "Host Name" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:100 +msgid "Host or IP Address for the connection to a remote docker instance" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:300 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:142 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:78 +msgid "ID" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:44 +msgid "IP VLAN" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:111 +msgid "IP range" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:522 +msgid "IPv4 Address" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:132 +msgid "IPv6 Gateway" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:126 +msgid "IPv6 Subnet" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:304 +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_import.htm:54 +msgid "Image" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:39 +#: applications/luci-app-dockerman/luasrc/view/dockerman/overview.htm:151 +msgid "Images" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:132 +msgid "Images overview" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_import.htm:4 +msgid "Import" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:125 +msgid "Import Image" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:47 +msgid "Index Server Address" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:52 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:414 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:102 +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:3 +msgid "Info" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:74 +msgid "Ingress" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:75 +msgid "" +"Ingress network is the network which provides the routing-mesh in swarm mode" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:8 +msgid "Inspect" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:477 +msgid "Interactive (-i)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:86 +msgid "Internal" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:66 +msgid "Ipvlan Mode" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:43 +msgid "" +"It replaces the daemon registry mirrors with a new set of registry mirrors" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:238 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:264 +msgid "Kill" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:70 +msgid "L2 bridge" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:71 +msgid "L3 bridge" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:359 +msgid "Links" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:527 +msgid "Links with other containers" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:283 +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_load.htm:2 +msgid "Load" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:49 +msgid "Log Level" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:661 +msgid "Log driver options" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:9 +msgid "Logs" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:43 +msgid "MAC VLAN" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:589 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:644 +msgid "Memory" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:783 +msgid "Memory Useage" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:645 +msgid "" +"Memory limit (format: []). Number is a positive integer. Unit " +"can be one of b, k, m, or g. Minimum is 4M" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:590 +msgid "" +"Memory limit (format: []). Number is a positive integer. Unit " +"can be one of b, k, m, or g. Minimum is 4M." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:57 +msgid "Mode" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:90 +msgid "Mount Point" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:603 +msgid "Mount tmpfs directory" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:343 +msgid "Mount/Volume" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:175 +msgid "Mounts" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:295 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:419 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:83 +msgid "Name" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:37 +msgid "Name of the network that can be selected during container creation" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:394 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:528 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:169 +msgid "Network" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:80 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:36 +msgid "Network Name" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:40 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:518 +#: applications/luci-app-dockerman/luasrc/view/dockerman/overview.htm:169 +msgid "Networks" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:59 +msgid "Networks overview" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:104 +msgid "New" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:39 +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_import.htm:54 +msgid "New tag" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:627 +msgid "Number of CPUs. Number is a fractional number. 0.000 means no limit" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:574 +msgid "Number of CPUs. Number is a fractional number. 0.000 means no limit." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:73 +msgid "" +"On this page all images are displayed that are available on the system and " +"with which a container can be created." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:193 +msgid "On this page, the selected container can be managed." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:82 +msgid "Options" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:45 +msgid "Overlay network" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:37 +msgid "Overview" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:33 +msgid "PLEASE CONFIRM" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:84 +msgid "Parent Interface" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:64 +msgid "Pass-through (Mirror physical device to single MAC VLAN)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_import.htm:54 +msgid "Please input new tag" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:270 +msgid "Please input the PATH and select the file !" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:82 +msgid "Please input the PORT or HOST IP of remote docker instance!" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:86 +msgid "Please input the SOCKET PATH of docker daemon!" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm:91 +msgid "Plese input command line:" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:355 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:172 +msgid "Ports" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:124 +msgid "Ports allowed to be accessed" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:62 +msgid "Private (Prevent communication between MAC VLANs)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:504 +msgid "Privileged" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:561 +msgid "Publish container's port(s) to the host" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:100 +msgid "Pull" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:83 +msgid "Pull Image" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:42 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:48 +msgid "Registry Mirrors" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:73 +msgid "Remote Endpoint" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:99 +msgid "Remote Host" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:106 +msgid "Remote Port" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:274 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:274 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:210 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:115 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:108 +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:173 +msgid "Remove" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:43 +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_import.htm:82 +msgid "Remove tag" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:171 +msgid "Rename" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:145 +msgid "RepoTags" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:469 +msgid "Resolve CLI" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:4 +msgid "Resources" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:220 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:244 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:87 +msgid "Restart" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:334 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:427 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:510 +msgid "Restart Policy" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:86 +msgid "Restrict external access to the network" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinevalue.htm:31 +msgid "Reveal/hide password" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:566 +msgid "Run command" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:230 +msgid "Save" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:533 +msgid "Set custom DNS servers" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:547 +msgid "Set environment variables to inside the container" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:50 +msgid "Set the logging level" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:151 +msgid "Size" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:61 +msgid "" +"Specifies where the Docker daemon will listen for client connections " +"(default: unix:///var/run/docker.sock)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:211 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:234 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:65 +msgid "Start" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:319 +msgid "Start Time" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:789 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:790 +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:5 +msgid "Stats" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:308 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:165 +msgid "Status" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:229 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:254 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:65 +msgid "Stop" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm:91 +msgid "Submit" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:86 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:101 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:375 +msgid "Sysctl" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:610 +msgid "Sysctl(--sysctl)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:611 +msgid "Sysctls (kernel parameters) options" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:792 +msgid "TOP" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:483 +msgid "TTY (-t)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_stats.htm:56 +msgid "TX/RX" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:579 +msgid "The hostname to use for the container" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:662 +msgid "The logging configuration for this container" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:540 +msgid "" +"The user that commands are run as inside the container.(format: name|uid[:" +"group|gid])" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:129 +msgid "" +"This page displays all containers that have been created on the connected " +"docker host." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:55 +msgid "" +"This page displays all docker networks that have been created on the " +"connected docker host." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:367 +msgid "Tmpfs" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:602 +msgid "Tmpfs(--tmpfs)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:45 +msgid "Total Memory" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:697 +msgid "UID" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:297 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:336 +msgid "Update" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:256 +msgid "Upgrade" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:7 +msgid "Upload" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:303 +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:304 +msgid "Upload Error" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:294 +msgid "Upload Success" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_stats.htm:48 +msgid "Upload/Download" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:339 +msgid "User" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:539 +msgid "User(-u)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:63 +msgid "VEPA (Virtual Ethernet Port Aggregator)" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:41 +#: applications/luci-app-dockerman/luasrc/view/dockerman/overview.htm:187 +msgid "Volumes" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:73 +msgid "Volumes overview" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:53 +msgid "Warning" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:126 +msgid "" +"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." +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:124 +msgid "" +"Which Port(s) can be accessed, it's not restricted by the Allowed Access " +"interfaces configuration. Use this configuration with caution!" +msgstr "" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:118 +msgid "" +"Which interface(s) can access containers under the bridge network, fill-in " +"Interface Name" +msgstr "" diff --git a/package/lean/lisaac/luci-app-dockerman/po/zh-cn/dockerman.po b/package/lean/lisaac/luci-app-dockerman/po/zh-cn/dockerman.po new file mode 100644 index 000000000..2bdc11b8d --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/po/zh-cn/dockerman.po @@ -0,0 +1,1094 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2021-03-19 04:16+0000\n" +"Last-Translator: Eric \n" +"Language-Team: Chinese (Simplified) \n" +"Language: zh_Hans\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.5.2-dev\n" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:619 +msgid "A list of kernel capabilities to add to the container" +msgstr "要添加到容器的内核功能列表" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:69 +msgid "Access Control" +msgstr "访问控制" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:223 +msgid "Add" +msgstr "新增" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:595 +msgid "Add host device to the container" +msgstr "将主机设备添加到容器" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:571 +msgid "Advance" +msgstr "高级选项" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:586 +msgid "Allocates an ephemeral host port for all of a container's exposed ports" +msgstr "为容器的所有暴露端口分配临时主机端口" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:118 +msgid "Allowed access interfaces" +msgstr "允许的访问接口" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:498 +msgid "Always pull image first" +msgstr "总是先拉取镜像" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:29 +msgid "" +"An overview with the relevant data is displayed here with which the LuCI " +"docker client is connected." +msgstr "在此展示与LuCI docker客户端相连接的相关数据的概览。" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:43 +msgid "Api Version" +msgstr "Api 版本" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:94 +msgid "Auto create macvlan interface in Openwrt" +msgstr "在 Openwrt 中自动创建 macvlan 界面" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:18 +msgid "Auto start" +msgstr "自动启动" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:134 +msgid "Available" +msgstr "可用" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:47 +msgid "Base device" +msgstr "基设备" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:553 +msgid "Bind Mount(-v)" +msgstr "绑定挂载(-v)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:554 +msgid "Bind mount a volume" +msgstr "绑定挂载卷" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:596 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:652 +msgid "Block IO Weight" +msgstr "块 IO 权重" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:653 +msgid "" +"Block IO weight (relative weight) accepts a weight value between 10 and 1000" +msgstr "块 IO 权重(相对权重)接受10到1000之间的数值" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:597 +msgid "" +"Block IO weight (relative weight) accepts a weight value between 10 and 1000." +msgstr "块 IO 权重(相对权重)接受10到1000之间的数值。" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:61 +msgid "Bridge (Support direct communication between MAC VLANs)" +msgstr "桥接(支持 MAC VLAN 之间的直接通信)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:42 +msgid "Bridge device" +msgstr "Bridge device" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:84 +msgid "" +"By entering a valid image name with the corresponding version, the docker " +"image can be downloaded from the configured registry." +msgstr "" +"通过输入具有相应版本的有效映像名称,可以从镜像存储中心(Registry)中下载" +"docker映像。" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:618 +msgid "CAP-ADD(--cap-add)" +msgstr "权限控制(--cap-add)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:581 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:635 +msgid "CPU Shares Weight" +msgstr "CPU 共享权重" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:779 +msgid "CPU Useage" +msgstr "CPU 使用率" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:636 +msgid "" +"CPU shares relative weight, if 0 is set, the system will ignore the value " +"and use the default of 1024" +msgstr "CPU 共享相对权重,如果设置为 0,则系统将忽略该值并使用默认值 1024" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:582 +msgid "" +"CPU shares relative weight, if 0 is set, the system will ignore the value " +"and use the default of 1024." +msgstr "CPU 共享相对权重,如果设置为 0,则系统将忽略该值并使用默认值 1024。" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:573 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:626 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:44 +msgid "CPUs" +msgstr "线程数量" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:159 +msgid "Can NOT connect to docker daemon, please check!!" +msgstr "无法连接到docker守护进程(docker daemon),请检查!!" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm:91 +msgid "Cancel" +msgstr "取消" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:60 +msgid "Client connection" +msgstr "客户端连接" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:347 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:687 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:182 +msgid "Command" +msgstr "命令" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm:100 +msgid "Command line" +msgstr "命令行" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm:72 +msgid "Command line Error" +msgstr "命令行错误" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:17 +msgid "Configuration" +msgstr "配置" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:36 +msgid "Configure the default bridge network" +msgstr "配置默认桥接网络" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:405 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:707 +msgid "Connect" +msgstr "连接" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:403 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:437 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:473 +msgid "Connect Network" +msgstr "连接网络" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:74 +msgid "Connect to remote docker endpoint" +msgstr "连接到远程docker" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:7 +msgid "Console" +msgstr "控制台" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:161 +msgid "Container Info" +msgstr "容器信息" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:650 +msgid "Container Inspect" +msgstr "检查容器" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:671 +msgid "Container Logs" +msgstr "容器日志" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:473 +msgid "Container Name" +msgstr "容器名称" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:92 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:58 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:29 +msgid "Container detail" +msgstr "容器详情" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:38 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:142 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:148 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:87 +#: applications/luci-app-dockerman/luasrc/view/dockerman/overview.htm:133 +msgid "Containers" +msgstr "容器" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:94 +msgid "Create macvlan interface" +msgstr "创建 macvlan 接口" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:465 +msgid "Create new docker container" +msgstr "创建 docker 容器" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:31 +msgid "Create new docker network" +msgstr "创建 docker 网络" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:312 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:153 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:92 +msgid "Created" +msgstr "创建时间" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:33 +msgid "DELETING" +msgstr "删除中" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:371 +msgid "DNS" +msgstr "DNS" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:51 +msgid "Debug" +msgstr "调试" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:35 +msgid "Default bridge" +msgstr "默认桥接" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:363 +msgid "Device" +msgstr "设备" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:594 +msgid "Device(--device)" +msgstr "设备(--device)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:396 +msgid "Disconnect" +msgstr "断开" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:14 +msgid "Docker" +msgstr "Docker" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:12 +msgid "Docker - Configuration" +msgstr "Docker - 配置" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:192 +msgid "Docker - Container (%s)" +msgstr "Docker - 容器 (%s)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:128 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:450 +msgid "Docker - Containers" +msgstr "Docker - 容器" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/logs.htm:3 +msgid "Docker - Events" +msgstr "Docker - 事件" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:72 +msgid "Docker - Images" +msgstr "Docker - 镜像" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:15 +msgid "Docker - Network" +msgstr "Docker - 网络" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:54 +msgid "Docker - Networks" +msgstr "Docker - 网络" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:28 +msgid "Docker - Overview" +msgstr "Docker - 概览" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:69 +msgid "Docker - Volumes" +msgstr "Docker - 存储卷" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:16 +msgid "Docker Daemon settings" +msgstr "Docker 服务端(Docker Daemon)设置" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:489 +msgid "Docker Image" +msgstr "Docker 镜像" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:30 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:46 +msgid "Docker Root Dir" +msgstr "Docker 根目录" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:93 +msgid "Docker Socket Path" +msgstr "Docker 套接字路径" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:42 +msgid "Docker Version" +msgstr "Docker 版本" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/apply_widget.htm:91 +msgid "Docker actions done." +msgstr "Docker 执行完成。" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:70 +msgid "DockerMan" +msgstr "DockerMan" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:13 +msgid "DockerMan is a simple docker manager client for LuCI" +msgstr "DockerMan是用于LuCI的简单docker管理器客户端" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:68 +msgid "DockerMan settings" +msgstr "DockerMan设置" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:172 +msgid "Download" +msgstr "下载" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:82 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:40 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:85 +msgid "Driver" +msgstr "驱动" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:265 +msgid "Duplicate/Edit" +msgstr "复制/编辑" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:120 +msgid "Enable IPv6" +msgstr "启用 IPv6" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:351 +msgid "Env" +msgstr "环境变量" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:546 +msgid "Environmental Variable(-e)" +msgstr "环境变量(-e)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:54 +msgid "Error" +msgstr "错误" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:42 +#: applications/luci-app-dockerman/luasrc/view/dockerman/logs.htm:5 +msgid "Events" +msgstr "事件" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:116 +msgid "Exclude IPs" +msgstr "排除 IP" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:247 +msgid "Export" +msgstr "导出" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:585 +msgid "Exposed All Ports(-P)" +msgstr "暴露所有端口(-P)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:560 +msgid "Exposed Ports(-p)" +msgstr "暴露端口(-p)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:55 +msgid "Fatal" +msgstr "致命的" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:6 +msgid "File" +msgstr "文件" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:324 +msgid "Finish Time" +msgstr "完成时间" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:220 +msgid "Force Remove" +msgstr "强制移除" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:88 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:106 +msgid "Gateway" +msgstr "网关" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:33 +msgid "Github" +msgstr "Github" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinevalue.htm:4 +msgid "Go to relevant configuration page" +msgstr "进入相关配置页面" + +#: applications/luci-app-dockerman/root/usr/share/rpcd/acl.d/luci-app-dockerman.json:3 +msgid "Grant UCI access for luci-app-dockerman" +msgstr "授予 UCI 访问 luci-app-dockerman 的权限" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:330 +msgid "Healthy" +msgstr "健康" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:578 +msgid "Host Name" +msgstr "主机名" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:100 +msgid "Host or IP Address for the connection to a remote docker instance" +msgstr "连接到远程Docker实例的主机名或IP地址" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:300 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:142 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:78 +msgid "ID" +msgstr "ID" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:44 +msgid "IP VLAN" +msgstr "IP VLAN" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:111 +msgid "IP range" +msgstr "IP 范围" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:522 +msgid "IPv4 Address" +msgstr "IPv4 地址" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:132 +msgid "IPv6 Gateway" +msgstr "IPv6 网关" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:126 +msgid "IPv6 Subnet" +msgstr "IPv6 子网" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:304 +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_import.htm:54 +msgid "Image" +msgstr "镜像" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:39 +#: applications/luci-app-dockerman/luasrc/view/dockerman/overview.htm:151 +msgid "Images" +msgstr "镜像" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:132 +msgid "Images overview" +msgstr "镜像概览" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_import.htm:4 +msgid "Import" +msgstr "导入" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:125 +msgid "Import Image" +msgstr "导入镜像" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:47 +msgid "Index Server Address" +msgstr "索引服务器地址" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:52 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:414 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:102 +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:3 +msgid "Info" +msgstr "信息" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:74 +msgid "Ingress" +msgstr "入口" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:75 +msgid "" +"Ingress network is the network which provides the routing-mesh in swarm mode" +msgstr "入口网络是以群模式提供路由网格的网络" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:8 +msgid "Inspect" +msgstr "检查" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:477 +msgid "Interactive (-i)" +msgstr "交互(-i)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:86 +msgid "Internal" +msgstr "内部" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:66 +msgid "Ipvlan Mode" +msgstr "Ipvlan 模式" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:43 +msgid "" +"It replaces the daemon registry mirrors with a new set of registry mirrors" +msgstr "" +"设置新的镜像存储中心(Registry)镜像源,这将取代服务端(daemon)配置的镜像存" +"储中心(Registry)的镜像源" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:238 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:264 +msgid "Kill" +msgstr "强制关闭" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:70 +msgid "L2 bridge" +msgstr "L2 桥接" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:71 +msgid "L3 bridge" +msgstr "L3 桥接" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:359 +msgid "Links" +msgstr "链接" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:527 +msgid "Links with other containers" +msgstr "与其他容器的链接" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:283 +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_load.htm:2 +msgid "Load" +msgstr "负载" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:49 +msgid "Log Level" +msgstr "日志等级" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:661 +msgid "Log driver options" +msgstr "日志驱动选项" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:9 +msgid "Logs" +msgstr "日志" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:43 +msgid "MAC VLAN" +msgstr "MAC VLAN" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:589 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:644 +msgid "Memory" +msgstr "内存" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:783 +msgid "Memory Useage" +msgstr "内存使用率" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:645 +msgid "" +"Memory limit (format: []). Number is a positive integer. Unit " +"can be one of b, k, m, or g. Minimum is 4M" +msgstr "" +"内存限制(格式:<数字>[<单位>])。数字是正整数。单位可以是 b、k、m 或 g 之一。" +"最小值为 4M" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:590 +msgid "" +"Memory limit (format: []). Number is a positive integer. Unit " +"can be one of b, k, m, or g. Minimum is 4M." +msgstr "" +"内存限制(格式:<数字>[<单位>])。数字是正整数。单位可以是 b、k、m 或 g 之一。" +"最小值为 4M。" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:57 +msgid "Mode" +msgstr "模式" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:90 +msgid "Mount Point" +msgstr "挂载点" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:603 +msgid "Mount tmpfs directory" +msgstr "挂载 tmpfs 目录" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:343 +msgid "Mount/Volume" +msgstr "挂载/卷" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:175 +msgid "Mounts" +msgstr "挂载点" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:295 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:419 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:83 +msgid "Name" +msgstr "名称" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:37 +msgid "Name of the network that can be selected during container creation" +msgstr "在容器创建时可以选择网络的名称" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:394 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:528 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:169 +msgid "Network" +msgstr "网络" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:80 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:36 +msgid "Network Name" +msgstr "网络名称" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:40 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:518 +#: applications/luci-app-dockerman/luasrc/view/dockerman/overview.htm:169 +msgid "Networks" +msgstr "网络" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:59 +msgid "Networks overview" +msgstr "网络概览" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:104 +msgid "New" +msgstr "新建" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:39 +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_import.htm:54 +msgid "New tag" +msgstr "新建标签" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:627 +msgid "Number of CPUs. Number is a fractional number. 0.000 means no limit" +msgstr "CPU 数量。数字是小数。0.000 表示没有限制" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:574 +msgid "Number of CPUs. Number is a fractional number. 0.000 means no limit." +msgstr "CPU 数量。数字是小数。0.000 表示没有限制。" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:73 +msgid "" +"On this page all images are displayed that are available on the system and " +"with which a container can be created." +msgstr "在此页面上,显示系统上可用的所有镜像文件,并可以用它们来创建容器" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:193 +msgid "On this page, the selected container can be managed." +msgstr "在此页面可以管理所选的容器。" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:82 +msgid "Options" +msgstr "选项" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:45 +msgid "Overlay network" +msgstr "Overlay network" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:37 +msgid "Overview" +msgstr "概览" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:33 +msgid "PLEASE CONFIRM" +msgstr "请确认" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:84 +msgid "Parent Interface" +msgstr "父接口" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:64 +msgid "Pass-through (Mirror physical device to single MAC VLAN)" +msgstr "直通(将物理设备镜像到单独的 MAC VLAN)" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_import.htm:54 +msgid "Please input new tag" +msgstr "请输入新的标签" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:270 +msgid "Please input the PATH and select the file !" +msgstr "请输入路径并选择文件!" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:82 +msgid "Please input the PORT or HOST IP of remote docker instance!" +msgstr "请输入合法的远程docker实例端口和主机IP" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:86 +msgid "Please input the SOCKET PATH of docker daemon!" +msgstr "请输入合法docker服务端(docker daemon)的SOCKET地址" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm:91 +msgid "Plese input command line:" +msgstr "请输入 的命令行:" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:355 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:172 +msgid "Ports" +msgstr "端口" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:124 +msgid "Ports allowed to be accessed" +msgstr "允许访问的端口" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:62 +msgid "Private (Prevent communication between MAC VLANs)" +msgstr "专用(阻止 MAC VLAN 之间的通信)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:504 +msgid "Privileged" +msgstr "特权模式" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:561 +msgid "Publish container's port(s) to the host" +msgstr "将容器的端口发布到主机" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:100 +msgid "Pull" +msgstr "拉取" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:83 +msgid "Pull Image" +msgstr "拉取镜像" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:42 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:48 +msgid "Registry Mirrors" +msgstr "镜像加速器" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:73 +msgid "Remote Endpoint" +msgstr "远程实例" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:99 +msgid "Remote Host" +msgstr "远程主机" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:106 +msgid "Remote Port" +msgstr "远程端口" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:274 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:274 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:210 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:115 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:108 +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:173 +msgid "Remove" +msgstr "移除" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:43 +#: applications/luci-app-dockerman/luasrc/view/dockerman/images_import.htm:82 +msgid "Remove tag" +msgstr "移除标签" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:171 +msgid "Rename" +msgstr "重命名" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:145 +msgid "RepoTags" +msgstr "仓库标签" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:469 +msgid "Resolve CLI" +msgstr "解析 CLI" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:4 +msgid "Resources" +msgstr "资源" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:220 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:244 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:87 +msgid "Restart" +msgstr "重新启动" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:334 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:427 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:510 +msgid "Restart Policy" +msgstr "重启策略" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:86 +msgid "Restrict external access to the network" +msgstr "限制外部网络访问" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/cbi/inlinevalue.htm:31 +msgid "Reveal/hide password" +msgstr "显示/隐藏 密码" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:566 +msgid "Run command" +msgstr "运行命令" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:230 +msgid "Save" +msgstr "保存" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:533 +msgid "Set custom DNS servers" +msgstr "设置自定义 DNS 服务器" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:547 +msgid "Set environment variables to inside the container" +msgstr "在容器内部设置环境变量" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:50 +msgid "Set the logging level" +msgstr "设置日志记录级别" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:151 +msgid "Size" +msgstr "大小" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:61 +msgid "" +"Specifies where the Docker daemon will listen for client connections " +"(default: unix:///var/run/docker.sock)" +msgstr "" +"指定Docker服务端(Docker daemon)将在何处侦听客户端连接(默认: unix:///var/" +"run/docker.sock)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:211 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:234 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:65 +msgid "Start" +msgstr "启动" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:319 +msgid "Start Time" +msgstr "开始时间" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:789 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:790 +#: applications/luci-app-dockerman/luasrc/view/dockerman/container.htm:5 +msgid "Stats" +msgstr "状态" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:308 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:165 +msgid "Status" +msgstr "状态" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:229 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:254 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:65 +msgid "Stop" +msgstr "停止" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/newcontainer_resolve.htm:91 +msgid "Submit" +msgstr "提交" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:86 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:101 +msgid "Subnet" +msgstr "子网" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:375 +msgid "Sysctl" +msgstr "系统控制" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:610 +msgid "Sysctl(--sysctl)" +msgstr "系统控制(--sysctl)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:611 +msgid "Sysctls (kernel parameters) options" +msgstr "系统控制(内核参数)选项" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:792 +msgid "TOP" +msgstr "TOP" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:483 +msgid "TTY (-t)" +msgstr "TTY(-t)" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_stats.htm:56 +msgid "TX/RX" +msgstr "发射/接收" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:579 +msgid "The hostname to use for the container" +msgstr "容器使用的主机名" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:662 +msgid "The logging configuration for this container" +msgstr "该容器的日志记录配置" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:540 +msgid "" +"The user that commands are run as inside the container.(format: name|uid[:" +"group|gid])" +msgstr "在容器中以用户运行命令。(格式:name|uid[:group|gid])" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/containers.lua:129 +msgid "" +"This page displays all containers that have been created on the connected " +"docker host." +msgstr "此页面显示在连接的Docker主机上已创建的所有容器。" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/networks.lua:55 +msgid "" +"This page displays all docker networks that have been created on the " +"connected docker host." +msgstr "此页面显示在已连接的Docker主机上创建的所有Docker网络。" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:367 +msgid "Tmpfs" +msgstr "Tmpfs" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:602 +msgid "Tmpfs(--tmpfs)" +msgstr "Tmpfs(--tmpfs)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/overview.lua:45 +msgid "Total Memory" +msgstr "总内存" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:697 +msgid "UID" +msgstr "UID" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:297 +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:336 +msgid "Update" +msgstr "更新" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:256 +msgid "Upgrade" +msgstr "升级" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:7 +msgid "Upload" +msgstr "上传" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:303 +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:304 +msgid "Upload Error" +msgstr "上传错误" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_file_manager.htm:294 +msgid "Upload Success" +msgstr "上传成功" + +#: applications/luci-app-dockerman/luasrc/view/dockerman/container_stats.htm:48 +msgid "Upload/Download" +msgstr "上传/下载" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/container.lua:339 +msgid "User" +msgstr "用户" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newcontainer.lua:539 +msgid "User(-u)" +msgstr "用户(-u)" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/newnetwork.lua:63 +msgid "VEPA (Virtual Ethernet Port Aggregator)" +msgstr "VEPA(虚拟以太网端口聚合器)" + +#: applications/luci-app-dockerman/luasrc/controller/dockerman.lua:41 +#: applications/luci-app-dockerman/luasrc/view/dockerman/overview.htm:187 +msgid "Volumes" +msgstr "存储卷" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/volumes.lua:73 +msgid "Volumes overview" +msgstr "卷概览" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:53 +msgid "Warning" +msgstr "警告" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/images.lua:126 +msgid "" +"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." +msgstr "" +"按下导入按钮时,既可以将本地镜像文件加载到系统上,也可以从远程下载有效的Tar格" +"式的镜像文件。" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:124 +msgid "" +"Which Port(s) can be accessed, it's not restricted by the Allowed Access " +"interfaces configuration. Use this configuration with caution!" +msgstr "设置可以被访问的端口,该配置不受“允许的访问接口”配置的限制。请谨慎使用该配置选项!" + +#: applications/luci-app-dockerman/luasrc/model/cbi/dockerman/configuration.lua:118 +msgid "" +"Which interface(s) can access containers under the bridge network, fill-in " +"Interface Name" +msgstr "哪些接口可以访问桥接网络下的容器,请填写接口名称" + +#~ msgid "Containers allowed to be accessed" +#~ msgstr "允许访问的容器" + +#~ msgid "" +#~ "Which container(s) under bridge network can be accessed, even from " +#~ "interfaces that are not allowed, fill-in Container Id or Name" +#~ msgstr "" +#~ "桥接网络下哪些容器可以访问,即使是不允许从接口访问,也要填写容器 ID 或名称" + +#~ msgid "Connect to remote endpoint" +#~ msgstr "连接到远程终端" + +#~ msgid "Global settings" +#~ msgstr "全局设定" + +#~ msgid "Path" +#~ msgstr "路径" + +#~ msgid "Please input the PATH !" +#~ msgstr "请输入合法路径!" + +#~ msgid "Setting" +#~ msgstr "设置" + +#~ msgid "Specifies where the Docker daemon will listen for client connections" +#~ msgstr "指定Docker服务端(Docker daemon)侦听客户端连接的位置" + +#~ msgid "Docker Container" +#~ msgstr "Docker 容器" + +#~ msgid "" +#~ "DockerMan is a Simple Docker manager client for LuCI, If you have any " +#~ "issue please visit:" +#~ msgstr "" +#~ "DockerMan 是一个简单的 LuCI 客户端 Docker 管理器,如果您有任何问题,请访" +#~ "问:" + +#~ msgid "Import Images" +#~ msgstr "导入镜像" + +#~ msgid "New Container" +#~ msgstr "新建容器" + +#~ msgid "New Network" +#~ msgstr "新建网络" + +#~ msgid "Macvlan Mode" +#~ msgstr "Macvlan 模式" + +#~ msgid "" +#~ "Daemon unix socket (unix:///var/run/docker.sock) or TCP Remote Hosts " +#~ "(tcp://0.0.0.0:2375), default: unix:///var/run/docker.sock" +#~ msgstr "" +#~ "守护进程 unix 套接字 (unix:///var/run/docker.sock) 或 TCP 远程主机 " +#~ "(tcp://0.0.0.0:2375),默认值:unix:///var/run/docker.sock" + +#~ msgid "Docker Daemon" +#~ msgstr "Docker 服务端" + +#~ msgid "Dockerman connect to remote endpoint" +#~ msgstr "Dockerman 连接到远程端点" + +#~ msgid "Enable" +#~ msgstr "启用" + +#~ msgid "Server Host" +#~ msgstr "服务器主机" + +#~ msgid "Contaienr Info" +#~ msgstr "容器信息" diff --git a/package/lean/lisaac/luci-app-dockerman/po/zh_Hans b/package/lean/lisaac/luci-app-dockerman/po/zh_Hans new file mode 120000 index 000000000..41451e4a1 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/po/zh_Hans @@ -0,0 +1 @@ +zh-cn \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-dockerman/postinst b/package/lean/lisaac/luci-app-dockerman/postinst new file mode 100644 index 000000000..b0db1cb89 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/postinst @@ -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 \ No newline at end of file diff --git a/package/lean/lisaac/luci-app-dockerman/root/etc/init.d/dockerman b/package/lean/lisaac/luci-app-dockerman/root/etc/init.d/dockerman new file mode 100755 index 000000000..80309aeab --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/root/etc/init.d/dockerman @@ -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" diff --git a/package/lean/lisaac/luci-app-dockerman/root/etc/uci-defaults/luci-app-dockerman b/package/lean/lisaac/luci-app-dockerman/root/etc/uci-defaults/luci-app-dockerman new file mode 100755 index 000000000..4358728a1 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/root/etc/uci-defaults/luci-app-dockerman @@ -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 diff --git a/package/lean/lisaac/luci-app-dockerman/root/usr/share/rpcd/acl.d/luci-app-dockerman.json b/package/lean/lisaac/luci-app-dockerman/root/usr/share/rpcd/acl.d/luci-app-dockerman.json new file mode 100644 index 000000000..78c2c6418 --- /dev/null +++ b/package/lean/lisaac/luci-app-dockerman/root/usr/share/rpcd/acl.d/luci-app-dockerman.json @@ -0,0 +1,11 @@ +{ + "luci-app-dockerman": { + "description": "Grant UCI access for luci-app-dockerman", + "read": { + "uci": [ "dockerd" ] + }, + "write": { + "uci": [ "dockerd" ] + } + } +}