diff --git a/luci-app-cpufreq/Makefile b/luci-app-cpufreq/Makefile new file mode 100644 index 00000000..e812f2af --- /dev/null +++ b/luci-app-cpufreq/Makefile @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-3.0-only +# +# Copyright (C) 2021-2022 ImmortalWrt.org + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI for CPU Freq Setting +LUCI_DEPENDS:=+cpufreq + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js b/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js new file mode 100644 index 00000000..c0cdf2e7 --- /dev/null +++ b/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js @@ -0,0 +1,95 @@ +/* SPDX-License-Identifier: GPL-3.0-only + * + * Copyright (C) 2022 ImmortalWrt.org + */ + +'use strict'; +'require form'; +'require fs'; +'require uci'; +'require view'; + +return view.extend({ + load: function() { + return Promise.all([ + uci.load('cpufreq'), + L.resolveDefault(fs.exec_direct('/etc/init.d/cpufreq', [ 'get_policies' ], 'json'), {}) + ]); + }, + + render: function(data) { + var m, s, o; + + m = new form.Map('cpufreq', _('CPU Freq Settings'), + _('Set CPU Scaling Governor to Max Performance or Balance Mode')); + + s = m.section(form.NamedSection, 'cpufreq', 'settings'); + + if (Object.keys(data[1]).length === 0) { + s.render = () => { + this.handleSaveApply = null; + this.handleSave = null; + this.handleReset = null; + + return E('div', { 'class': 'cbi-section warning' }, [ + E('h3', {}, _('Unsupported device!')), + E('p', {}, _('Your device/kernel does not support CPU frequency scaling.')) + ]); + } + } else { + /* Mark user edited */ + var ss = m.section(form.NamedSection, 'global', 'settings'); + var so = ss.option(form.HiddenValue, 'set'); + so.load = (/* ... */) => { return 1 }; + so.readonly = true; + so.rmempty = false; + + for (var i in data[1]) { + var index = data[1][i].index; + s.tab(index, i, _('

Apply for CPU %s.

').format(data[1][i].cpus)); + + o = s.taboption(index, form.ListValue, 'governor' + index, _('CPU Scaling Governor')); + for (var gov of data[1][i].governors) + o.value(gov); + o.rmempty = false; + + o = s.taboption(index, form.ListValue, 'minfreq' + index, _('Min Idle CPU Freq')); + for (var freq of data[1][i].freqs) + o.value(freq); + o.rmempty = false; + + o = s.taboption(index, form.ListValue, 'maxfreq' + index, _('Max Turbo Boost CPU Freq')); + for (var freq of data[1][i].freqs) + o.value(freq); + o.validate = function(section_id, value) { + if (!section_id) + return true; + else if (value === null || value === '') + return _('Expecting: %s').format('non-empty value'); + + var minfreq = this.map.lookupOption('minfreq' + index, section_id)[0].formvalue(section_id); + if (parseInt(value) < parseInt(minfreq)) + return _('Max CPU Freq cannot be lower than Min CPU Freq.'); + + return true; + } + + o = s.taboption(index, form.Value, 'sdfactor' + index, _('CPU Switching Sampling rate'), + _('The sampling rate determines how frequently the governor checks to tune the CPU (ms)')); + o.datatype = 'range(1,100000)'; + o.default = '10'; + o.depends('governor' + index, 'ondemand'); + o.rmempty = false; + + o = s.taboption(index, form.Value, 'upthreshold' + index, _('CPU Switching Threshold'), + _('Kernel make a decision on whether it should increase the frequency (%)')); + o.datatype = 'range(1,99)'; + o.default = '50'; + o.depends('governor' + index, 'ondemand'); + o.rmempty = false; + } + } + + return m.render(); + } +}); diff --git a/luci-app-cpufreq/po/templates/cpufreq.pot b/luci-app-cpufreq/po/templates/cpufreq.pot new file mode 100644 index 00000000..9bc1c1a0 --- /dev/null +++ b/luci-app-cpufreq/po/templates/cpufreq.pot @@ -0,0 +1,68 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:49 +msgid "

Apply for CPU %s.

" +msgstr "" + +#: applications/luci-app-cpufreq/root/usr/share/luci/menu.d/luci-app-cpufreq.json:3 +msgid "CPU Freq" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:23 +msgid "CPU Freq Settings" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:51 +msgid "CPU Scaling Governor" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:77 +msgid "CPU Switching Sampling rate" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:84 +msgid "CPU Switching Threshold" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:68 +msgid "Expecting: %s" +msgstr "" + +#: applications/luci-app-cpufreq/root/usr/share/rpcd/acl.d/luci-app-cpufreq.json:3 +msgid "Grant access to CPUFreq configuration" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:85 +msgid "Kernel make a decision on whether it should increase the frequency (%)" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:72 +msgid "Max CPU Freq cannot be lower than Min CPU Freq." +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:61 +msgid "Max Turbo Boost CPU Freq" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:56 +msgid "Min Idle CPU Freq" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:24 +msgid "Set CPU Scaling Governor to Max Performance or Balance Mode" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:78 +msgid "" +"The sampling rate determines how frequently the governor checks to tune the " +"CPU (ms)" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:35 +msgid "Unsupported device!" +msgstr "" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:36 +msgid "Your device/kernel does not support CPU frequency scaling." +msgstr "" diff --git a/luci-app-cpufreq/po/zh-cn b/luci-app-cpufreq/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-app-cpufreq/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-cpufreq/po/zh_Hans/cpufreq.po b/luci-app-cpufreq/po/zh_Hans/cpufreq.po new file mode 100644 index 00000000..1d1c3e7d --- /dev/null +++ b/luci-app-cpufreq/po/zh_Hans/cpufreq.po @@ -0,0 +1,75 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Project-Id-Version: PACKAGE VERSION\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh-Hans\n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:49 +msgid "

Apply for CPU %s.

" +msgstr "

应用于 CPU %s。

" + +#: applications/luci-app-cpufreq/root/usr/share/luci/menu.d/luci-app-cpufreq.json:3 +msgid "CPU Freq" +msgstr "CPU 性能优化调节" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:23 +msgid "CPU Freq Settings" +msgstr "CPU 性能优化调节设置" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:51 +msgid "CPU Scaling Governor" +msgstr "CPU 工作模式" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:77 +msgid "CPU Switching Sampling rate" +msgstr "CPU 切换周期" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:84 +msgid "CPU Switching Threshold" +msgstr "CPU 切换频率触发阈值" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:68 +msgid "Expecting: %s" +msgstr "请输入:%s" + +#: applications/luci-app-cpufreq/root/usr/share/rpcd/acl.d/luci-app-cpufreq.json:3 +msgid "Grant access to CPUFreq configuration" +msgstr "授予访问 CPUFreq 配置的权限" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:85 +msgid "Kernel make a decision on whether it should increase the frequency (%)" +msgstr "当 CPU 占用率超过 (%) 的情况下触发内核切换频率" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:72 +msgid "Max CPU Freq cannot be lower than Min CPU Freq." +msgstr "最大 CPU 频率不能低于最小 CPU 频率。" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:61 +msgid "Max Turbo Boost CPU Freq" +msgstr "最大 Turbo Boost CPU 频率" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:56 +msgid "Min Idle CPU Freq" +msgstr "待机 CPU 最小频率" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:24 +msgid "Set CPU Scaling Governor to Max Performance or Balance Mode" +msgstr "设置路由器的 CPU 性能模式(高性能/均衡省电)" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:78 +msgid "" +"The sampling rate determines how frequently the governor checks to tune the " +"CPU (ms)" +msgstr "CPU 检查切换的周期 (ms)。注意:过于频繁的切换频率会引起网络延迟抖动" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:35 +msgid "Unsupported device!" +msgstr "不支持的设备!" + +#: applications/luci-app-cpufreq/htdocs/luci-static/resources/view/cpufreq.js:36 +msgid "Your device/kernel does not support CPU frequency scaling." +msgstr "您的 设备/内核 不支持 CPU 动态调频。" diff --git a/luci-app-cpufreq/root/usr/share/luci/menu.d/luci-app-cpufreq.json b/luci-app-cpufreq/root/usr/share/luci/menu.d/luci-app-cpufreq.json new file mode 100644 index 00000000..83bfa6d7 --- /dev/null +++ b/luci-app-cpufreq/root/usr/share/luci/menu.d/luci-app-cpufreq.json @@ -0,0 +1,14 @@ +{ + "admin/system/cpufreq": { + "title": "CPU Freq", + "order": 90, + "action": { + "type": "view", + "path": "cpufreq" + }, + "depends": { + "acl": [ "luci-app-cpufreq" ], + "uci": { "cpufreq": true } + } + } +} diff --git a/luci-app-cpufreq/root/usr/share/rpcd/acl.d/luci-app-cpufreq.json b/luci-app-cpufreq/root/usr/share/rpcd/acl.d/luci-app-cpufreq.json new file mode 100644 index 00000000..5e92194b --- /dev/null +++ b/luci-app-cpufreq/root/usr/share/rpcd/acl.d/luci-app-cpufreq.json @@ -0,0 +1,14 @@ +{ + "luci-app-cpufreq": { + "description": "Grant access to CPUFreq configuration", + "read": { + "file": { + "/etc/init.d/cpufreq get_policies": [ "exec" ] + }, + "uci": [ "cpufreq" ] + }, + "write": { + "uci": [ "cpufreq" ] + } + } +} diff --git a/luci-app-diskman/Makefile b/luci-app-diskman/Makefile new file mode 100644 index 00000000..d8b101d8 --- /dev/null +++ b/luci-app-diskman/Makefile @@ -0,0 +1,53 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-diskman +PKG_VERSION:=0.2.11 +PKG_RELEASE:=2 + +PKG_MAINTAINER:=lisaac +PKG_LICENSE:=AGPL-3.0 + +LUCI_TITLE:=Disk Manager interface for LuCI +LUCI_DEPENDS:=+blkid +e2fsprogs +parted +smartmontools \ + +PACKAGE_$(PKG_NAME)_INCLUDE_btrfs_progs:btrfs-progs \ + +PACKAGE_$(PKG_NAME)_INCLUDE_lsblk:lsblk \ + +PACKAGE_$(PKG_NAME)_INCLUDE_mdadm:mdadm \ + +PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_raid456:mdadm \ + +PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_raid456:kmod-md-raid456 \ + +PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_linears:mdadm \ + +PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_linears:kmod-md-linear + +include $(INCLUDE_DIR)/package.mk + +define Package/$(PKG_NAME)/config +config PACKAGE_$(PKG_NAME)_INCLUDE_btrfs_progs + bool "Include btrfs-progs" + default y + +config PACKAGE_$(PKG_NAME)_INCLUDE_lsblk + bool "Include lsblk" + default y + +config PACKAGE_$(PKG_NAME)_INCLUDE_mdadm + bool "Include mdadm" + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_raid456 + depends on PACKAGE_$(PKG_NAME)_INCLUDE_mdadm + bool "Include kmod-md-raid456" + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_linear + depends on PACKAGE_$(PKG_NAME)_INCLUDE_mdadm + bool "Include kmod-md-linear" + default n +endef + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +rm -fr /tmp/luci-indexcache /tmp/luci-modulecache +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-diskman/luasrc/controller/diskman.lua b/luci-app-diskman/luasrc/controller/diskman.lua new file mode 100644 index 00000000..98b2e646 --- /dev/null +++ b/luci-app-diskman/luasrc/controller/diskman.lua @@ -0,0 +1,151 @@ +--[[ +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 attr = { } + local dm = require "luci.model.diskman" + local cmd = io.popen(dm.command.smartctl .. " -H -A -i /dev/%s" % dev) + if cmd then + local content = cmd:read("*all") + local ln + cmd:close() + if content:match("NVMe Version:")then + for ln in string.gmatch(content,'[^\r\n]+') do + if ln:match("^(.-):%s+(.+)") then + local key, value = ln:match("^(.-):%s+(.+)") + attr[#attr+1]= { + key = key, + value = value + } + end + end + else + for ln in string.gmatch(content,'[^\r\n]+') do + if 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 + end + luci.http.prepare_content("application/json") + luci.http.write_json(attr) +end \ No newline at end of file diff --git a/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua b/luci-app-diskman/luasrc/model/cbi/diskman/btrfs.lua new file mode 100644 index 00000000..00600785 --- /dev/null +++ b/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/luci-app-diskman/luasrc/model/cbi/diskman/disks.lua b/luci-app-diskman/luasrc/model/cbi/diskman/disks.lua new file mode 100644 index 00000000..f49e89fa --- /dev/null +++ b/luci-app-diskman/luasrc/model/cbi/diskman/disks.lua @@ -0,0 +1,360 @@ +--[[ +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_status", translate("Health") .. "
" .. translate("Status")) +-- d:option(DummyValue, "status", translate("Status")) + +local btn_eject = d:option(Button, "_eject") +btn_eject.template = "diskman/cbi/disabled_button" +btn_eject.inputstyle = "remove" +btn_eject.inputtitle = translate("Eject") +btn_eject.forcewrite = true +btn_eject.write = function(self, section, value) + local dev = section + local disk_info = dm.get_disk_info(dev, true) + if disk_info.p_table:match("Raid") then + m.errmessage = translate("Unsupported raid reject!") + return + end + 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 + +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 = dm.find_free_md_device() + 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("grep -m1 'Personalities :' /proc/mdstat") + if valid_raid:match("%[linear%]") then + r_level:value("linear", "Linear") + end + if valid_raid:match("%[raid5%]") then + r_level:value("5", "Raid 5") + end + if valid_raid:match("%[raid6%]") then + 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/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua b/luci-app-diskman/luasrc/model/cbi/diskman/partition.lua new file mode 100644 index 00000000..348a617a --- /dev/null +++ b/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/luci-app-diskman/luasrc/model/diskman.lua b/luci-app-diskman/luasrc/model/diskman.lua new file mode 100644 index 00000000..96e41d6c --- /dev/null +++ b/luci-app-diskman/luasrc/model/diskman.lua @@ -0,0 +1,743 @@ +--[[ +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 + if val ~= "-" then + smart_info.temp = (val:match("(%d+)") or "?") .. "°C" + end + 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") or device:match("vd") 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.logic_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 or result + 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+$") + or dev:match("^vd[a-z]$") + 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.find_free_md_device = function() + for num=0,127 do + local md = io.open("/dev/md"..tostring(num), "r") + if md == nil then + return "/dev/md"..tostring(num) + else + io.close(md) + end + end + return nil +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 + rname = d.find_free_md_device() + if rname == nil then return "ERR: Cannot find free md device" end + 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/luci-app-diskman/luasrc/view/diskman/cbi/disabled_button.htm b/luci-app-diskman/luasrc/view/diskman/cbi/disabled_button.htm new file mode 100644 index 00000000..1ad4eca3 --- /dev/null +++ b/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/luci-app-diskman/luasrc/view/diskman/cbi/format_button.htm b/luci-app-diskman/luasrc/view/diskman/cbi/format_button.htm new file mode 100644 index 00000000..18e306e2 --- /dev/null +++ b/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/luci-app-diskman/luasrc/view/diskman/cbi/inlinebutton.htm b/luci-app-diskman/luasrc/view/diskman/cbi/inlinebutton.htm new file mode 100644 index 00000000..b1b19325 --- /dev/null +++ b/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/luci-app-diskman/luasrc/view/diskman/cbi/xnullsection.htm b/luci-app-diskman/luasrc/view/diskman/cbi/xnullsection.htm new file mode 100644 index 00000000..69aa65e0 --- /dev/null +++ b/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/luci-app-diskman/luasrc/view/diskman/cbi/xsimpleform.htm b/luci-app-diskman/luasrc/view/diskman/cbi/xsimpleform.htm new file mode 100644 index 00000000..72a21f50 --- /dev/null +++ b/luci-app-diskman/luasrc/view/diskman/cbi/xsimpleform.htm @@ -0,0 +1,87 @@ +<% 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 + + if self.message then + %>
<%=self.message%>
<% + end + + if self.errmessage then + %>
<%=self.errmessage%>
<% + end + + self:render_children() + + %>
<% + + 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/luci-app-diskman/luasrc/view/diskman/disk_info.htm b/luci-app-diskman/luasrc/view/diskman/disk_info.htm new file mode 100644 index 00000000..10ba3e7c --- /dev/null +++ b/luci-app-diskman/luasrc/view/diskman/disk_info.htm @@ -0,0 +1,110 @@ + diff --git a/luci-app-diskman/luasrc/view/diskman/partition_info.htm b/luci-app-diskman/luasrc/view/diskman/partition_info.htm new file mode 100644 index 00000000..16133f9e --- /dev/null +++ b/luci-app-diskman/luasrc/view/diskman/partition_info.htm @@ -0,0 +1,138 @@ + + \ No newline at end of file diff --git a/luci-app-diskman/luasrc/view/diskman/smart_detail.htm b/luci-app-diskman/luasrc/view/diskman/smart_detail.htm new file mode 100644 index 00000000..ffb4f7d9 --- /dev/null +++ b/luci-app-diskman/luasrc/view/diskman/smart_detail.htm @@ -0,0 +1,78 @@ + + + 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/luci-app-diskman/po/pl/diskman.po b/luci-app-diskman/po/pl/diskman.po new file mode 100644 index 00000000..16965563 --- /dev/null +++ b/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/luci-app-diskman/po/zh-cn b/luci-app-diskman/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-app-diskman/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-diskman/po/zh_Hans/diskman.po b/luci-app-diskman/po/zh_Hans/diskman.po new file mode 100644 index 00000000..e67f8637 --- /dev/null +++ b/luci-app-diskman/po/zh_Hans/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/luci-app-diskman/po/zh_Hant/diskman.po b/luci-app-diskman/po/zh_Hant/diskman.po new file mode 100644 index 00000000..fb1dadc6 --- /dev/null +++ b/luci-app-diskman/po/zh_Hant/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/luci-app-netdata/Makefile b/luci-app-netdata/Makefile new file mode 100644 index 00000000..398918ef --- /dev/null +++ b/luci-app-netdata/Makefile @@ -0,0 +1,16 @@ +# Copyright (C) 2016 Openwrt.org +# +# This is free software, licensed under the Apache License, Version 2.0 . +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI support for netdata +LUCI_DEPENDS:=+netdata +LUCI_PKGARCH:=all +PKG_VERSION:=1.0 +PKG_RELEASE:=1 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature \ No newline at end of file diff --git a/luci-app-netdata/luasrc/controller/netdata.lua b/luci-app-netdata/luasrc/controller/netdata.lua new file mode 100644 index 00000000..ff2d6eae --- /dev/null +++ b/luci-app-netdata/luasrc/controller/netdata.lua @@ -0,0 +1,9 @@ +module("luci.controller.netdata", package.seeall) + +function index() + if not (luci.sys.call("pidof netdata > /dev/null") == 0) then + return + end + + entry({"admin", "system", "netdata"}, template("netdata"), _("NetData"), 10).leaf = true +end \ No newline at end of file diff --git a/luci-app-netdata/luasrc/view/netdata.htm b/luci-app-netdata/luasrc/view/netdata.htm new file mode 100644 index 00000000..8b84e6e2 --- /dev/null +++ b/luci-app-netdata/luasrc/view/netdata.htm @@ -0,0 +1,9 @@ +<%+header%> +
+

<%=translate("NetData")%>

+ +
+ +<%+footer%> \ No newline at end of file diff --git a/luci-app-netdata/po/zh-cn b/luci-app-netdata/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-app-netdata/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-netdata/po/zh_Hans/netdata.po b/luci-app-netdata/po/zh_Hans/netdata.po new file mode 100644 index 00000000..e8966b81 --- /dev/null +++ b/luci-app-netdata/po/zh_Hans/netdata.po @@ -0,0 +1,5 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +msgid "NetData" +msgstr "实时监控" diff --git a/luci-app-ramfree/Makefile b/luci-app-ramfree/Makefile new file mode 100644 index 00000000..cddacda3 --- /dev/null +++ b/luci-app-ramfree/Makefile @@ -0,0 +1,17 @@ +# Copyright (C) 2016 Openwrt.org +# +# This is free software, licensed under the Apache License, Version 2.0 . +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=luci-app-ramfree +LUCI_PKGARCH:=all +PKG_VERSION:=1.0 +PKG_RELEASE:=1 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature + + diff --git a/luci-app-ramfree/luasrc/controller/release_ram.lua b/luci-app-ramfree/luasrc/controller/release_ram.lua new file mode 100644 index 00000000..78723518 --- /dev/null +++ b/luci-app-ramfree/luasrc/controller/release_ram.lua @@ -0,0 +1,9 @@ +module("luci.controller.release_ram",package.seeall) + +function index() + entry({"admin","status","release_ram"}, call("release_ram"), _("Release Ram"), 9999).acl_depends = { "luci-app-ramfree" } +end +function release_ram() + luci.sys.call("sync && echo 3 > /proc/sys/vm/drop_caches") + luci.http.redirect(luci.dispatcher.build_url("admin/status")) +end diff --git a/luci-app-ramfree/po/zh-cn b/luci-app-ramfree/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-app-ramfree/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-ramfree/po/zh_Hans/release_ram.po b/luci-app-ramfree/po/zh_Hans/release_ram.po new file mode 100644 index 00000000..cff6f028 --- /dev/null +++ b/luci-app-ramfree/po/zh_Hans/release_ram.po @@ -0,0 +1,2 @@ +msgid "Release Ram" +msgstr "释放内存" diff --git a/luci-app-ramfree/root/usr/share/rpcd/acl.d/luci-app-ramfree.json b/luci-app-ramfree/root/usr/share/rpcd/acl.d/luci-app-ramfree.json new file mode 100644 index 00000000..f326496e --- /dev/null +++ b/luci-app-ramfree/root/usr/share/rpcd/acl.d/luci-app-ramfree.json @@ -0,0 +1,11 @@ +{ + "luci-app-ramfree": { + "description": "Free Your Boom", + "read": { + "uci": ["release_ram"] + }, + "write": { + "uci": ["release_ram"] + } + } +} \ No newline at end of file diff --git a/luci-app-socat/Makefile b/luci-app-socat/Makefile new file mode 100644 index 00000000..7fbcf2fd --- /dev/null +++ b/luci-app-socat/Makefile @@ -0,0 +1,16 @@ +# Copyright (C) 2020 Lienol +# +# This is free software, licensed under the GNU General Public License v3. +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI support for Socat +LUCI_DEPENDS:=+socat +LUCI_PKGARCH:=all +PKG_RELEASE:=3 +PKG_DATE:=20200824 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-socat/luasrc/controller/socat.lua b/luci-app-socat/luasrc/controller/socat.lua new file mode 100644 index 00000000..88498e99 --- /dev/null +++ b/luci-app-socat/luasrc/controller/socat.lua @@ -0,0 +1,19 @@ +-- Copyright 2020 Lienol +module("luci.controller.socat", package.seeall) +local http = require "luci.http" + +function index() + if not nixio.fs.access("/etc/config/socat") then return end + + entry({"admin", "network", "socat"}, cbi("socat/index"), _("Socat"), 100).acl_depends = { "luci-app-socat" } + entry({"admin", "network", "socat", "config"}, cbi("socat/config")).leaf = true + entry({"admin", "network", "socat", "status"}, call("status")).leaf = true +end + +function status() + local e = {} + e.index = luci.http.formvalue("index") + e.status = luci.sys.call(string.format("ps -w | grep -v 'grep' | grep '/var/etc/socat/%s' >/dev/null", luci.http.formvalue("id"))) == 0 + http.prepare_content("application/json") + http.write_json(e) +end diff --git a/luci-app-socat/luasrc/model/cbi/socat/config.lua b/luci-app-socat/luasrc/model/cbi/socat/config.lua new file mode 100644 index 00000000..afbc0849 --- /dev/null +++ b/luci-app-socat/luasrc/model/cbi/socat/config.lua @@ -0,0 +1,82 @@ +local d = require "luci.dispatcher" + +m = Map("socat", translate("Socat Config")) +m.redirect = d.build_url("admin", "network", "socat") + +s = m:section(NamedSection, arg[1], "config", "") +s.addremove = false +s.dynamic = false + +o = s:option(Flag, "enable", translate("Enable")) +o.default = "1" +o.rmempty = false + +o = s:option(Value, "remarks", translate("Remarks")) +o.default = translate("Remarks") +o.rmempty = false + +o = s:option(ListValue, "protocol", translate("Protocol")) +o:value("port_forwards", translate("Port Forwards")) + +o = s:option(ListValue, "family", translate("Restrict to address family")) +o:value("", translate("IPv4 and IPv6")) +o:value("4", translate("IPv4 only")) +o:value("6", translate("IPv6 only")) +o:depends("protocol", "port_forwards") + +o = s:option(ListValue, "proto", translate("Protocol")) +o:value("tcp", "TCP") +o:value("udp", "UDP") +o:depends("protocol", "port_forwards") + +o = s:option(Value, "listen_port", translate("Listen port")) +o.datatype = "portrange" +o.rmempty = false +o:depends("protocol", "port_forwards") + +o = s:option(Flag, "reuseaddr", "reuseaddr", translate("Bind to a port local")) +o.default = "1" +o.rmempty = false + +o = s:option(ListValue, "dest_proto", translate("Destination Protocol")) +o:value("tcp4", "IPv4-TCP") +o:value("udp4", "IPv4-UDP") +o:value("tcp6", "IPv6-TCP") +o:value("udp6", "IPv6-UDP") +o:depends("protocol", "port_forwards") + +dest_ipv4 = s:option(Value, "dest_ipv4", translate("Destination address")) +luci.sys.net.ipv4_hints(function(ip, name) + dest_ipv4:value(ip, "%s (%s)" %{ ip, name }) +end) +function dest_ipv4.cfgvalue(self, section) + return m:get(section, "dest_ip") +end +function dest_ipv4.write(self, section, value) + m:set(section, "dest_ip", value) +end +dest_ipv4:depends("dest_proto", "tcp4") +dest_ipv4:depends("dest_proto", "udp4") + +dest_ipv6 = s:option(Value, "dest_ipv6", translate("Destination address")) +luci.sys.net.ipv6_hints(function(ip, name) + dest_ipv6:value(ip, "%s (%s)" %{ ip, name }) +end) +function dest_ipv6.cfgvalue(self, section) + return m:get(section, "dest_ip") +end +function dest_ipv6.write(self, section, value) + m:set(section, "dest_ip", value) +end +dest_ipv6:depends("dest_proto", "tcp6") +dest_ipv6:depends("dest_proto", "udp6") + +o = s:option(Value, "dest_port", translate("Destination port")) +o.datatype = "portrange" +o.rmempty = false + +o = s:option(Flag, "firewall_accept", translate("Open firewall port")) +o.default = "1" +o.rmempty = false + +return m diff --git a/luci-app-socat/luasrc/model/cbi/socat/index.lua b/luci-app-socat/luasrc/model/cbi/socat/index.lua new file mode 100644 index 00000000..0bc520e4 --- /dev/null +++ b/luci-app-socat/luasrc/model/cbi/socat/index.lua @@ -0,0 +1,76 @@ +local d = require "luci.dispatcher" +local e = luci.model.uci.cursor() + +m = Map("socat", translate("Socat"), translate("Socat is a versatile networking tool named after 'Socket CAT', which can be regarded as an N-fold enhanced version of NetCat")) + +s = m:section(NamedSection, "global", "global") +s.anonymous = true +s.addremove = false + +o = s:option(Flag, "enable", translate("Enable")) +o.rmempty = false + +s = m:section(TypedSection, "config", translate("Port Forwards")) +s.anonymous = true +s.addremove = true +s.template = "cbi/tblsection" +s.extedit = d.build_url("admin", "network", "socat", "config", "%s") +function s.filter(e, t) + if m:get(t, "protocol") == "port_forwards" then + return true + end +end +function s.create(e, t) + local uuid = string.gsub(luci.sys.exec("echo -n $(cat /proc/sys/kernel/random/uuid)"), "-", "") + t = uuid + TypedSection.create(e, t) + luci.http.redirect(e.extedit:format(t)) +end +function s.remove(e, t) + e.map.proceed = true + e.map:del(t) + luci.http.redirect(d.build_url("admin", "network", "socat")) +end + +o = s:option(Flag, "enable", translate("Enable")) +o.width = "5%" +o.rmempty = false + +o = s:option(DummyValue, "status", translate("Status")) +o.template = "socat/status" +o.value = translate("Collecting data...") + +o = s:option(DummyValue, "remarks", translate("Remarks")) + +o = s:option(DummyValue, "family", translate("Listen Protocol")) +o.cfgvalue = function(t, n) + local listen = Value.cfgvalue(t, n) or "" + local protocol = (m:get(n, "proto") or ""):upper() + if listen == "" then + return protocol + else + return "IPv" .. listen .. "-" .. protocol + end +end + +o = s:option(DummyValue, "listen_port", translate("Listen port")) + +o = s:option(DummyValue, "dest_proto", translate("Destination Protocol")) +o.cfgvalue = function(t, n) + local listen = Value.cfgvalue(t, n) + local protocol = listen:sub(0, #listen - 1):upper() + local ip_type = "IPv" .. listen:sub(#listen) + return ip_type .. "-" .. protocol +end + +o = s:option(DummyValue, "dest_ip", translate("Destination address")) + +o = s:option(DummyValue, "dest_port", translate("Destination port")) + +o = s:option(Flag, "firewall_accept", translate("Open firewall port")) +o.default = "1" +o.rmempty = false + +m:append(Template("socat/list_status")) +return m + diff --git a/luci-app-socat/luasrc/view/socat/list_status.htm b/luci-app-socat/luasrc/view/socat/list_status.htm new file mode 100644 index 00000000..8ffac828 --- /dev/null +++ b/luci-app-socat/luasrc/view/socat/list_status.htm @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/luci-app-socat/luasrc/view/socat/status.htm b/luci-app-socat/luasrc/view/socat/status.htm new file mode 100644 index 00000000..04193e89 --- /dev/null +++ b/luci-app-socat/luasrc/view/socat/status.htm @@ -0,0 +1,3 @@ +<%+cbi/valueheader%> +-- +<%+cbi/valuefooter%> \ No newline at end of file diff --git a/luci-app-socat/po/zh-cn b/luci-app-socat/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-app-socat/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-socat/po/zh_Hans/socat.po b/luci-app-socat/po/zh_Hans/socat.po new file mode 100644 index 00000000..9068b611 --- /dev/null +++ b/luci-app-socat/po/zh_Hans/socat.po @@ -0,0 +1,50 @@ +msgid "Socat" +msgstr "Socat" + +msgid "Socat is a versatile networking tool named after 'Socket CAT', which can be regarded as an N-fold enhanced version of NetCat" +msgstr "Socat 是 Linux 下的一个多功能的网络工具,名字来由是「Socket CAT」。其功能与有瑞士军刀之称的 Netcat 类似,可以看做是 Netcat 的加强版。" + +msgid "Socat Config" +msgstr "Socat 配置" + +msgid "Status" +msgstr "状态" + +msgid "Enabled" +msgstr "启用" + +msgid "Remarks" +msgstr "备注" + +msgid "Protocol" +msgstr "协议" + +msgid "IPv6 Only" +msgstr "仅IPv6" + +msgid "When checked, only IPv6 ports are listen for, otherwise IPv4 will also be listened for." +msgstr "当勾选时,仅监听IPv6,否则将会同时监听IPv4。" + +msgid "Port Forwards" +msgstr "端口转发" + +msgid "Listen Protocol" +msgstr "监听协议" + +msgid "Listen port" +msgstr "监听端口" + +msgid "Bind to a port local" +msgstr "绑定到本地端口" + +msgid "Destination Protocol" +msgstr "目标协议" + +msgid "Destination address" +msgstr "目标地址" + +msgid "Destination port" +msgstr "目标端口" + +msgid "Open firewall port" +msgstr "打开防火墙端口" diff --git a/luci-app-socat/root/etc/config/socat b/luci-app-socat/root/etc/config/socat new file mode 100644 index 00000000..04e4f872 --- /dev/null +++ b/luci-app-socat/root/etc/config/socat @@ -0,0 +1,3 @@ + +config global 'global' + option enable '0' diff --git a/luci-app-socat/root/etc/init.d/socat b/luci-app-socat/root/etc/init.d/socat new file mode 100755 index 00000000..579db57f --- /dev/null +++ b/luci-app-socat/root/etc/init.d/socat @@ -0,0 +1,119 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2020 Lienol + +START=99 + +CONFIG=socat +CONFIG_PATH=/var/etc/$CONFIG + +add_rule() { + iptables -N SOCAT + iptables -I INPUT -j SOCAT + ip6tables -N SOCAT + ip6tables -I INPUT -j SOCAT +} + +del_rule() { + iptables -D INPUT -j SOCAT 2>/dev/null + iptables -F SOCAT 2>/dev/null + iptables -X SOCAT 2>/dev/null + ip6tables -D INPUT -j SOCAT 2>/dev/null + ip6tables -F SOCAT 2>/dev/null + ip6tables -X SOCAT 2>/dev/null +} + +gen_include() { + echo '#!/bin/sh' > /var/etc/$CONFIG.include + extract_rules() { + local _ipt="iptables" + [ "$1" == "6" ] && _ipt="ip6tables" + + echo "*$2" + ${_ipt}-save -t $2 | grep "SOCAT" | \ + sed -e "s/^-A \(INPUT\)/-I \1 1/" + echo 'COMMIT' + } + cat <<-EOF >> /var/etc/$CONFIG.include + iptables-save -c | grep -v "SOCAT" | iptables-restore -c + iptables-restore -n <<-EOT + $(extract_rules 4 filter) + EOT + ip6tables-save -c | grep -v "SOCAT" | ip6tables-restore -c + ip6tables-restore -n <<-EOT + $(extract_rules 6 filter) + EOT + EOF + return 0 +} + +run_service() { + config_get enable $1 enable + [ "$enable" = "0" ] && return 0 + config_get remarks $1 remarks + config_get protocol $1 protocol + config_get family $1 family + config_get proto $1 proto + config_get listen_port $1 listen_port + config_get reuseaddr $1 reuseaddr + config_get dest_proto $1 dest_proto + config_get dest_ip $1 dest_ip + config_get dest_port $1 dest_port + config_get firewall_accept $1 firewall_accept + ln -s /usr/bin/socat ${CONFIG_PATH}/$1 + + if [ "$reuseaddr" == "1" ]; then + reuseaddr=",reuseaddr" + else + reuseaddr="" + fi + + if [ "$family" == "6" ]; then + ipv6only_params=",ipv6-v6only" + else + ipv6only_params="" + fi + + # 端口转发 + if [ "$protocol" == "port_forwards" ]; then + listen=${proto}${family} + [ "$family" == "" ] && listen=${proto}6 + ${CONFIG_PATH}/$1 ${listen}-listen:${listen_port}${ipv6only_params}${reuseaddr},fork ${dest_proto}:${dest_ip}:${dest_port} >/dev/null 2>&1 & + fi + + [ "$firewall_accept" == "1" ] && { + if [ -z "$family" ] || [ "$family" == "6" ]; then + ip6tables -A SOCAT -p $proto --dport $listen_port -m comment --comment "$remarks" -j ACCEPT + fi + if [ -z "$family" ] || [ "$family" == "4" ]; then + iptables -A SOCAT -p $proto --dport $listen_port -m comment --comment "$remarks" -j ACCEPT + fi + } +} + +stop_service() { + ps -w | grep "$CONFIG_PATH/" | grep -v "grep" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 & + del_rule + rm -rf $CONFIG_PATH /var/etc/$CONFIG.include +} + +start() { + enable=$(uci -q get $CONFIG.@global[0].enable) + if [ "$enable" = "0" ];then + stop_service + else + mkdir -p $CONFIG_PATH + add_rule + config_load $CONFIG + config_foreach run_service "config" + gen_include + fi +} + +stop() { + stop_service +} + +restart() { + stop + start +} \ No newline at end of file diff --git a/luci-app-socat/root/etc/uci-defaults/luci-app-socat b/luci-app-socat/root/etc/uci-defaults/luci-app-socat new file mode 100755 index 00000000..95e3ec8e --- /dev/null +++ b/luci-app-socat/root/etc/uci-defaults/luci-app-socat @@ -0,0 +1,12 @@ +#!/bin/sh + +uci -q batch <<-EOF >/dev/null + delete firewall.socat + set firewall.socat=include + set firewall.socat.type=script + set firewall.socat.path=/var/etc/socat.include + set firewall.socat.reload=1 +EOF + +rm -rf /tmp/luci-*cache +exit 0 diff --git a/luci-app-socat/root/usr/share/rpcd/acl.d/luci-app-socat.json b/luci-app-socat/root/usr/share/rpcd/acl.d/luci-app-socat.json new file mode 100644 index 00000000..a79e14a5 --- /dev/null +++ b/luci-app-socat/root/usr/share/rpcd/acl.d/luci-app-socat.json @@ -0,0 +1,11 @@ +{ + "luci-app-socat": { + "description": "Grant UCI access for luci-app-socat", + "read": { + "uci": [ "socat" ] + }, + "write": { + "uci": [ "socat" ] + } + } +} diff --git a/luci-app-socat/root/usr/share/ucitrack/luci-app-socat.json b/luci-app-socat/root/usr/share/ucitrack/luci-app-socat.json new file mode 100644 index 00000000..3e75688d --- /dev/null +++ b/luci-app-socat/root/usr/share/ucitrack/luci-app-socat.json @@ -0,0 +1,4 @@ +{ + "config": "socat", + "init": [ "socat" ] +} diff --git a/luci-app-syncdial/Makefile b/luci-app-syncdial/Makefile new file mode 100644 index 00000000..70da3386 --- /dev/null +++ b/luci-app-syncdial/Makefile @@ -0,0 +1,17 @@ +# +# Copyright (C) 2008-2014 The LuCI Team +# +# This is free software, licensed under the Apache License, Version 2.0 . +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=Virtual WAN config generator +LUCI_DEPENDS:=+kmod-macvlan +luci-app-mwan3 +PKG_VERSION:=2.0 +PKG_RELEASE:=25 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature +#Makefile for syncdial diff --git a/luci-app-syncdial/luasrc/controller/syncdial.lua b/luci-app-syncdial/luasrc/controller/syncdial.lua new file mode 100644 index 00000000..03ef5d17 --- /dev/null +++ b/luci-app-syncdial/luasrc/controller/syncdial.lua @@ -0,0 +1,32 @@ +module("luci.controller.syncdial", package.seeall) + +function index() + if not nixio.fs.access("/etc/config/syncdial") then + return + end + + local page + page = entry({"admin", "network", "syncdial"}, cbi("syncdial"), _("多线多拨"),103) + page.dependent = true + page.acl_depends = { "luci-app-syncdial" } + + page = entry({"admin", "network", "syncdial", "status"}, call("act_status")) + page.leaf = true + page = entry({"admin", "network", "macvlan_redial"}, call("redial"), nil) + page.leaf = true +end + +function act_status() + local e = {} + local mwan3_status = luci.util.exec("mwan3 status") + e.num_online = 0 + for _ in mwan3_status:gmatch("tracking is active") do + e.num_online = e.num_online + 1 + end + luci.http.prepare_content("application/json") + luci.http.write_json(e) +end + +function redial() + os.execute("killall -9 pppd") +end diff --git a/luci-app-syncdial/luasrc/model/cbi/syncdial.lua b/luci-app-syncdial/luasrc/model/cbi/syncdial.lua new file mode 100644 index 00000000..89212b03 --- /dev/null +++ b/luci-app-syncdial/luasrc/model/cbi/syncdial.lua @@ -0,0 +1,88 @@ +local e = require"nixio.fs" +require("luci.tools.webadmin") + +m = Map("syncdial") +m.title = translate("多线多拨") +m.description = translate("使用macvlan驱动创建多个虚拟WAN口,支持并发多拨。") + +m:section(SimpleSection).template = "syncdial/syncdial_status" + +s = m:section(NamedSection, "config", "syncdial", translate("配置")) +s.anonymous = true + +o = s:option(Flag, "enabled", translate("启用")) +o.rmempty = false + +o = s:option(Flag, "syncon", translate("启用并发多拨")) +o.rmempty = false + +o = s:option(ListValue, "dial_type", translate("多拨类型")) +o:value("1", translate("单线多拨")) +o:value("2", translate("双线多拨")) +o.rmempty = false + +o = s:option(Value, "wanselect", translate("选择外网接口")) +o.description = translate("指定要多拨的外网接口,如wan") +luci.tools.webadmin.cbi_add_networks(o) +o.optional = false +o.rmempty = false + +o = s:option(Value, "wannum", translate("虚拟WAN接口数量")) +o.datatype = "and(uinteger,range(0,249))" +o.optional = false +o.default = 1 + +o = s:option(Flag, "bindwan", translate("绑定物理接口")) +o.rmempty = false + +o = s:option(Value, "wanselect2", translate("选择第二个外网接口")) +o.description = translate("指定要多拨的第二个外网接口,如wan2") +luci.tools.webadmin.cbi_add_networks(o) +o.optional=false +o:depends("dial_type","2") + +o = s:option(Value, "wannum2", translate("第二条线虚拟WAN接口数量")) +o.description = translate("设置第二条线的拨号数") +o.datatype = "and(uinteger,range(0,249))" +o.optional = false +o.default = 1 +o:depends("dial_type","2") + +o = s:option(Flag, "bindwan2", translate("绑定物理接口")) +o.description = translate("第二条线生成的虚拟接口绑定当前物理接口") +o.rmempty = false +o:depends("dial_type","2") + +o = s:option(Flag, "dialchk", translate("启用掉线检测")) +o.rmempty = false + +o = s:option(Value, "dialnum", translate("最低在线接口数量")) +o.description = translate("如果在线接口数量小于这个值则重拨。") +o.datatype = "and(uinteger,range(0,248))" +o.optional = false +o.default = 2 + +o = s:option(Value, "dialnum2", translate("第二条线最低在线接口数量")) +o.description = translate("如果第二条线在线接口数量小于这个值则重拨。") +o.datatype = "range(0,248)" +o.optional = false +o.default = 2 +o:depends("dial_type","2") + +o = s:option(Value, "dialwait", translate("重拨等待时间")) +o.description = translate("重拨时,接口全部下线后下一次拨号前的等待时间。单位:秒 最小值:5秒") +o.datatype = "and(uinteger,min(5))" +o.optional = false + +o = s:option(Flag, "old_frame", translate("使用旧的macvlan创建方式")) +o.rmempty = false + +o = s:option(Flag, "nomwan", translate("不自动配置MWAN3负载均衡")) +o.description = translate("需要自定义负载均衡设置或者要使用策略路由的用户选择") +o.rmempty = false + +o = s:option(DummyValue, "_redial", translate("重新并发拨号")) +o.template = "syncdial/redial_button" +o.width = "10%" + +return m diff --git a/luci-app-syncdial/luasrc/view/syncdial/redial_button.htm b/luci-app-syncdial/luasrc/view/syncdial/redial_button.htm new file mode 100644 index 00000000..2a810418 --- /dev/null +++ b/luci-app-syncdial/luasrc/view/syncdial/redial_button.htm @@ -0,0 +1,17 @@ +<%+cbi/valueheader%> + + + + +<%+cbi/valuefooter%> diff --git a/luci-app-syncdial/luasrc/view/syncdial/syncdial_status.htm b/luci-app-syncdial/luasrc/view/syncdial/syncdial_status.htm new file mode 100644 index 00000000..840c2814 --- /dev/null +++ b/luci-app-syncdial/luasrc/view/syncdial/syncdial_status.htm @@ -0,0 +1,17 @@ + + +
+

+ <%:Collecting data...%> +

+
diff --git a/luci-app-syncdial/root/bin/genwancfg b/luci-app-syncdial/root/bin/genwancfg new file mode 100755 index 00000000..6ecd6069 --- /dev/null +++ b/luci-app-syncdial/root/bin/genwancfg @@ -0,0 +1,278 @@ +#!/bin/sh +#macvlan及PPPoE拨号接口配置批量自动生成脚本 +#Copyright (C) 2016 +. /lib/functions.sh + +#检测IP列表 +chk_ip_list="www.baidu.com 114.114.114.114 119.29.29.29" +origfirewall=$(uci get firewall.@zone[1].network) +backupdev=$(uci get syncdial.config.devbackup) +norun=$(echo $origfirewall|grep vwan) +nomwan=$(uci get syncdial.config.nomwan) +wanselect=$(uci get syncdial.config.wanselect) +wannum=$(uci get syncdial.config.wannum) +oldframe=$(uci get syncdial.config.old_frame) +bindwan=$(uci -q get syncdial.config.bindwan) + +dial_type=$(uci get syncdial.config.dial_type) +[ "$dial_type" -eq 2 ] && { + wanselect2=$(uci get syncdial.config.wanselect2) + [ $? -ne 0 ] && { + logger -t Syncppp "You must select another pppoe interface ! 启用双线多拨必须同时选择第二个外网接口!" + return 0 + } + + wannum2=$(uci get syncdial.config.wannum2) + [ $? -ne 0 ] && { + logger -t Syncppp "When dualdial is enabled, the number of the second virtual WAN cannot be blank! 启用双线多拨时第二个虚拟WAN接口数不能为空!" + return 0 + } + bindwan2=$(uci -q get syncdial.config.bindwan2) +} + +#添加MWAN负载均衡相关配置 +#$1:接口名称 +mwan_cfg_add() { + #gen mwan3_interface + uci set mwan3.${1}=interface + uci set mwan3.${1}.enabled=1 + uci set mwan3.${1}.count=2 + uci set mwan3.${1}.timeout=2 + uci set mwan3.${1}.interval=5 + uci set mwan3.${1}.down=4 + uci set mwan3.${1}.up=1 + for i in $chk_ip_list + do + uci add_list mwan3.${1}.track_ip="$i" + done + uci set mwan3.${1}.reliability=1 + uci set mwan3.${1}.initial_state=online + uci set mwan3.${1}.family=ipv4 + uci set mwan3.${1}.track_method=ping + uci set mwan3.${1}.size=56 + uci set mwan3.${1}.failure_interval=5 + uci set mwan3.${1}.recovery_interval=5 + uci set mwan3.${1}.flush_conntrack=never + #gen mwan3_member + uci set mwan3.${1}_m1_w1=member + uci set mwan3.${1}_m1_w1.interface=${1} + uci set mwan3.${1}_m1_w1.metric=1 + uci set mwan3.${1}_m1_w1.weight=1 + #gen mwan3_policy + uci add_list mwan3.balanced.use_member=${1}_m1_w1 +} + +#删除MWAN负载均衡相关配置 +#$1:接口名称 +mwan_cfg_del() { + uci del mwan3.${1} + uci del mwan3.${1}_m1_w1 + uci del_list mwan3.balanced.use_member=${1}_m1_w1 +} + +#添加macvlan设备 +#$1:设虚拟备名称 $2:原始设备名称 +macvlan_dev_add() { + uci set network.macvlandev_${1}=device + uci set network.macvlandev_${1}.name=${1} + uci set network.macvlandev_${1}.device=${2} + uci set network.macvlandev_${1}.type=macvlan +} + +#添加PPPoE接口 +#$1:接口名称 $2:设备名称 $3:账户 $4:密码 $5:网关跃点 +pppoe_if_add() { + #gen vwan macaddr + NEW_MACADDR=$(openssl rand -hex 6 | sed 's/\(..\)/\1:/g; s/.$//') + #gen wan if + uci set network.${1}=interface + uci set network.${1}.device=${3} + uci set network.${1}.proto=pppoe + uci set network.${1}.username=${4} + uci set network.${1}.password=${5} + uci set network.${1}.metric=${2} + uci set network.${1}.macaddr=$NEW_MACADDR + #gen firewall + uci add_list firewall.@zone[1].network=${1} +} + + +orig_firewall_add() { + need_del_rule=`uci -q get firewall.@zone[1].network | awk -F"'" '{print $2}'` + uci del_list firewall.@zone[1].network="$need_del_rule" + for k in $( seq 1 250 ) + do + origdev=$(echo $origfirewall | cut -d " " -f$k) + if [ -z "$origdev" ]; then + break + fi + [ -z "$(uci get firewall.@zone[1].network | grep -w $origdev)" ] && uci add_list firewall.@zone[1].network=$origdev + done +} + +apply_cfg() { + uci commit + #/etc/init.d/network restart & + logger -t Syncppp "Apply syncdial configuaration." + ifup wan & + killall pppconnectcheck +} + +general_config_load() { + config_load 'syncdial' + config_get_bool enabled 'config' 'enabled' + config_get_bool old_frame 'config' 'old_frame' + congig_get_bool dial_type 'config' 'dial_type' + + if [ "$enabled" -eq 0 ]; then + if [ "$old_frame" -eq 1 ]; then + mwan_cfg_add $wanselect + if [ "$dial_type" -eq 2 ]; then + mwan_cfg_add $wanselect2 + fi + fi + echo "Disabled.Exit now." + apply_cfg + exit 1 + fi + + config_load 'network' + config_get pppoe_user $wanselect 'username' + config_get pppoe_password $wanselect 'password' + pppoe_device=$(uci get network.$wanselect.device) + [ "$dial_type" -eq 2 ] && { + config_get pppoe_user2 $wanselect2 'username' + config_get pppoe_password2 $wanselect2 'password' + pppoe_device2=$(uci get network.$wanselect2.device) + } + +} + +check_remove_device() { + local devcfg=${1} + [ ${devcfg::11} == 'macvlandev_' ] && uci del network.${devcfg} +} + +check_remove_interface() { + local ifcfg=${1} + [ ${ifcfg::4} == 'vwan' ] && { + uci del network.${ifcfg} + uci del_list firewall.@zone[1].network=${ifcfg} + [ "$nomwan" -ne 1 ] && mwan_cfg_del ${ifcfg} + } + uci set firewall.@zone[1].network="$backupdev" +} + +general_config_remove() { + config_load network + config_foreach check_remove_device 'device' + config_foreach check_remove_interface 'interface' + all_macvlans=`ip link show |grep macvlan | awk -F":" '{print $2}' | awk -F"@" '{print $1}'` + [ -n "$all_macvlans" ] && { + for macvlan in $all_macvlans + do + ip link delete $macvlan + done + } + [ "$(uci get network.$wanselect.proto)" == "none" ] && { + uci set network.$wanselect.proto=pppoe + } + + if [ "$oldframe" -eq 0 ]; then + [ "$wanselect" != "$(echo $(uci get syncdial.config.devbackup)| cut -d " " -f1)" ] && \ + [ "$wanselect" != "$(echo $(uci get syncdial.config.devbackup)| cut -d " " -f2)" ] && \ + [ "$nomwan" -ne 1 ] && mwan_cfg_del $wanselect + else + [ "$nomwan" -ne 1 ] && mwan_cfg_del $wanselect + fi + + [ "$dial_type" -eq 2 ] && { + [ $(uci get network.$wanselect2.proto) == "none" ] && { + uci set network.$wanselect2.proto=pppoe + } + + if [ "$oldframe" -eq 0 ]; then + [ "$wanselect2" != "$(echo $(uci get syncdial.config.devbackup)| cut -d " " -f1)" ] && \ + [ "$wanselect2" != "$(echo $(uci get syncdial.config.devbackup)| cut -d " " -f2)" ] && \ + [ "$nomwan" -ne 1 ] && mwan_cfg_del $wanselect2 + else + [ "$nomwan" -ne 1 ] && mwan_cfg_del $wanselect2 + fi + } +} + + +[ -z "$norun" ] && uci set syncdial.config.devbackup="$origfirewall" && uci commit syncdial +general_config_remove +general_config_load + +uci set network.$wanselect.metric=40 +if [ "$wannum" -gt 0 ]; then + [ "$old_frame" -eq 1 ] && { + uci set network.$wanselect.proto=none + device=$(uci get network.$wanselect.device) + for i in $(seq 1 $wannum) + do + ip link add link $device name macvlan$i type macvlan + ifconfig macvlan$i hw ether $(echo $(cat /sys/class/net/$device/address|awk -F ":" '{print $1":"$2":"$3":"$4":"$5":" }')$(echo "" | awk -F ":" '{printf("%X\n", 16+i);}' i=$i)) + ifconfig macvlan$i up + done + } + [ "$wanselect" != "$(echo $(uci get syncdial.config.devbackup)| cut -d " " -f1)" ] && \ + [ "$wanselect" != "$(echo $(uci get syncdial.config.devbackup)| cut -d " " -f2)" ] && \ + [ "$old_frame" -eq 0 -a "$nomwan" -ne 1 ] && mwan_cfg_add $wanselect + + + for i in $(seq 1 $wannum) + do + [ "$old_frame" -eq 0 ] && macvlan_dev_add macvlan$i $pppoe_device + if [ "$bindwan" != "" -a "$bindwan" == "1" ]; then + pppoe_if_add vwan$i $((40+$i)) $pppoe_device $pppoe_user $pppoe_password + else + pppoe_if_add vwan$i $((40+$i)) macvlan$i $pppoe_user $pppoe_password + fi + [ "$nomwan" -ne 1 ] && mwan_cfg_add vwan$i $((40+$i)) + done +else + [ "$nomwan" -ne 1 ] && mwan_cfg_add $wanselect +fi + +###dualdial configuration +[ "$(uci -q get syncdial.config.dial_type)" = "2" ] && { + + uci set network.$wanselect2.metric=60 + if [ "$wannum2" -gt 0 ]; then + [ "$old_frame" -eq 1 ] && { + uci set network.$wanselect2.proto=none + device2=$(uci get network.$wanselect2.device) + for i in $(seq 1 $wannum2) + do + ip link add link $device2 name macvlan$(($wannum+$i)) type macvlan + ifconfig macvlan$(($wannum+$i)) hw ether $(echo $(cat /sys/class/net/$device2/address|awk -F ":" '{print $1":"$2":"$3":"$4":"$5":" }')$(echo "" | awk -F ":" '{printf("%X\n", 16+i);}' i=$i)) + ifconfig macvlan$(($wannum+$i)) up + done + } + [ "$wanselect2" != "$(echo $(uci get syncdial.config.devbackup)| cut -d " " -f1)" ] && \ + [ "$wanselect2" != "$(echo $(uci get syncdial.config.devbackup)| cut -d " " -f2)" ] && \ + [ "$old_frame" -eq 0 -a "$nomwan" -ne 1 ] && mwan_cfg_add $wanselect2 + + + for i in $(seq 1 $wannum2) + do + [ "$old_frame" -eq 0 ] && macvlan_dev_add macvlan$(($wannum+$i)) $pppoe_device2 + if [ "$bindwan2" != "" -a "$bindwan2" == "1" ]; then + pppoe_if_add vwan$(($wannum+$i)) $((60+$i)) $pppoe_device2 $pppoe_user2 $pppoe_password2 + else + pppoe_if_add vwan$(($wannum+$i)) $((60+$i)) macvlan$(($wannum+$i)) $pppoe_user2 $pppoe_password2 + fi + [ "$nomwan" -ne 1 ] && mwan_cfg_add vwan$(($wannum+$i)) $((60+$i)) + done + else + [ "$nomwan" -ne 1 ] && mwan_cfg_add $wanselect2 + fi +} + +orig_firewall_add +apply_cfg + +return 0 diff --git a/luci-app-syncdial/root/etc/config/syncdial b/luci-app-syncdial/root/etc/config/syncdial new file mode 100644 index 00000000..5a39e586 --- /dev/null +++ b/luci-app-syncdial/root/etc/config/syncdial @@ -0,0 +1,19 @@ + +config syncdial 'config' + option syncon '1' + option dialwait '25' + option dialchk '1' + option nomwan '0' + option wanselect 'wan' + option dial_type '2' + option wannum '3' + option wanselect2 'wan2' + option wannum2 '2' + option dialnum '3' + option dialnum2 '2' + option old_frame '1' + option devbackup 'wan wan6 wan2' + option bindwan '0' + option bindwan2 '0' + option enabled '0' + diff --git a/luci-app-syncdial/root/etc/hotplug.d/iface/01-dialcheck b/luci-app-syncdial/root/etc/hotplug.d/iface/01-dialcheck new file mode 100755 index 00000000..a1f5a34d --- /dev/null +++ b/luci-app-syncdial/root/etc/hotplug.d/iface/01-dialcheck @@ -0,0 +1,36 @@ +#!/bin/sh +[ "$ACTION" = "ifdown" ] && pppconnectcheck & + +wanselect=$(uci get syncdial.config.wanselect) +[ "$(uci get syncdial.config.dial_type)" = "2" ] && { + wanselect2=$(uci get syncdial.config.wanselect2) +} + +[ "$(uci get syncdial.config.enabled)" = "1" ] && \ + [ "$(uci get syncdial.config.old_frame)" = "1" ] && \ + [ "$DEVICE" = "$(uci get network.$wanselect.device)" ] && \ + [ "$ACTION" = "ifup" ] && { + device=$(uci get network.$wanselect.device) + wannum=$(uci get syncdial.config.wannum) + for i in $(seq 1 $wannum) + do + [ -d /sys/class/net/macvlan$i ] || { + ip link add link $device name macvlan$i type macvlan + ifconfig macvlan$i hw ether $(echo $(cat /sys/class/net/$device/address|awk -F ":" '{print $1":"$2":"$3":"$4":"$5":" }')$(echo "" | awk -F ":" '{printf("%X\n", 16+i);}' i=$i)) + ifconfig macvlan$i up + } + done + + [ "$(uci get syncdial.config.dial_type)" = "2" ] && { + device2=$(uci get network.$wanselect2.device) + wannum2=$(uci get syncdial.config.wannum2) + for i in $(seq 1 $wannum2) + do + [ -d /sys/class/net/macvlan$(($wannum+$i)) ] || { + ip link add link $device2 name macvlan$(($wannum+$i)) type macvlan + ifconfig macvlan$(($wannum+$i)) hw ether $(echo $(cat /sys/class/net/$device2/address|awk -F ":" '{print $1":"$2":"$3":"$4":"$5":" }')$(echo "" | awk -F ":" '{printf("%X\n", 16+i);}' i=$i)) + ifconfig macvlan$(($wannum+$i)) up + } + done + } +} diff --git a/luci-app-syncdial/root/etc/hotplug.d/iface/01-mvifcreate b/luci-app-syncdial/root/etc/hotplug.d/iface/01-mvifcreate new file mode 100755 index 00000000..bcb69249 --- /dev/null +++ b/luci-app-syncdial/root/etc/hotplug.d/iface/01-mvifcreate @@ -0,0 +1,16 @@ +#!/bin/sh +[ "$(uci get syncdial.config.enabled)" = "1" ] && \ + [ "$(uci get syncdial.config.old_frame)" = "1" ] && \ + [ "$DEVICE" = "$(uci get network.wan.device)" ] && \ + [ "$ACTION" = "ifup" ] && { + device=$(uci get network.wan.device) + wannum=$(uci get syncdial.config.wannum) + for i in $(seq 1 $wannum) + do + [ -d /sys/class/net/macvlan$i ] || { + ip link add link $device name macvlan$i type macvlan + ifconfig macvlan$i hw ether $(echo $(cat /sys/class/net/$device/address|awk -F ":" '{print $1":"$2":"$3":"$4":"$5":" }')$(echo "" | awk -F ":" '{printf("%X\n", 16+i);}' i=$i)) + ifconfig macvlan$i up + } + done +} diff --git a/luci-app-syncdial/root/etc/uci-defaults/luci-syncdial b/luci-app-syncdial/root/etc/uci-defaults/luci-syncdial new file mode 100755 index 00000000..5394164c --- /dev/null +++ b/luci-app-syncdial/root/etc/uci-defaults/luci-syncdial @@ -0,0 +1,12 @@ +#!/bin/sh +touch /etc/config/syncdial + +uci -q batch <<-EOF >/dev/null + delete ucitrack.@syncdial[-1] + add ucitrack syncdial + set ucitrack.@syncdial[-1].exec='/bin/genwancfg' + commit ucitrack +EOF + +rm -f /tmp/luci-indexcache +exit 0 diff --git a/luci-app-syncdial/root/usr/share/rpcd/acl.d/luci-app-syncdial.json b/luci-app-syncdial/root/usr/share/rpcd/acl.d/luci-app-syncdial.json new file mode 100644 index 00000000..52495472 --- /dev/null +++ b/luci-app-syncdial/root/usr/share/rpcd/acl.d/luci-app-syncdial.json @@ -0,0 +1,11 @@ +{ + "luci-app-syncdial": { + "description": "Grant UCI access for luci-app-syncdial", + "read": { + "uci": [ "syncdial" ] + }, + "write": { + "uci": [ "syncdial" ] + } + } +} diff --git a/luci-app-usb-printer/Makefile b/luci-app-usb-printer/Makefile new file mode 100644 index 00000000..b2133768 --- /dev/null +++ b/luci-app-usb-printer/Makefile @@ -0,0 +1,18 @@ +# +# Copyright (C) 2008-2014 The LuCI Team +# +# This is free software, licensed under the Apache License, Version 2.0 . +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=USB Printer Share via TCP/IP +LUCI_DEPENDS:=+p910nd +kmod-usb-printer +PKG_VERSION:=1.0 +PKG_RELEASE:=2 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature +#applications/luci-app-usb-printer/ +#applications/luci-app-usb-printer/ diff --git a/luci-app-usb-printer/ipkg/postinst b/luci-app-usb-printer/ipkg/postinst new file mode 100755 index 00000000..97a343c7 --- /dev/null +++ b/luci-app-usb-printer/ipkg/postinst @@ -0,0 +1,6 @@ +#!/bin/sh +[ -n "${IPKG_INSTROOT}" ] || { + ( . /etc/uci-defaults/luci-usb-printer ) && rm -f /etc/uci-defaults/luci-usb-printer + exit 0 +} + diff --git a/luci-app-usb-printer/luasrc/controller/usb_printer.lua b/luci-app-usb-printer/luasrc/controller/usb_printer.lua new file mode 100644 index 00000000..9afc1091 --- /dev/null +++ b/luci-app-usb-printer/luasrc/controller/usb_printer.lua @@ -0,0 +1,30 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2008 Steven Barth + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +require("luci.sys") + +module("luci.controller.usb_printer", package.seeall) + +function index() + if not nixio.fs.access("/etc/config/usb_printer") then + return + end + + entry({"admin", "nas"}, firstchild(), "NAS", 44).dependent = false + + local page + + page = entry({"admin", "nas", "usb_printer"}, cbi("usb_printer"), _("USB Printer Server"), 50) + page.acl_depends = { "luci-app-usb-printer" } +end diff --git a/luci-app-usb-printer/luasrc/model/cbi/usb_printer.lua b/luci-app-usb-printer/luasrc/model/cbi/usb_printer.lua new file mode 100644 index 00000000..3cc7526e --- /dev/null +++ b/luci-app-usb-printer/luasrc/model/cbi/usb_printer.lua @@ -0,0 +1,130 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2008 Steven Barth +Copyright 2005-2013 hackpascal + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +require "luci.util" +local uci = luci.model.uci.cursor_state() +local net = require "luci.model.network" + +m = Map("usb_printer", translate("USB Printer Server"), + translate("Shares multiple USB printers via TCP/IP.
When modified bingings, re-plug usb connectors to take effect.
This module requires kmod-usb-printer.")) + +function hex_align(hex, num) + local len = num - string.len(hex) + + return string.rep("0", len) .. hex +end + +function detect_usb_printers() + local data = {} + + local lps = luci.util.execi("/usr/bin/detectlp") + + for value in lps do + local row = {} + + --[[ + detectlp 的输出格式: + 设备名,VID/PID/?,描述,型号 + ]]-- + + local pos = string.find(value, ",") + + local devname = string.sub(value, 1, pos - 1) + + local value = string.sub(value, pos + 1, string.len(value)) + + pos = string.find(value, ",") + local product = string.sub(value, 1, pos - 1) + + value = string.sub(value, pos + 1, string.len(value)) + + pos = string.find(value, ",") + local model = string.sub(value, 1, pos - 1) + + local name = string.sub(value, pos + 1, string.len(value)) + + pos = string.find(product, "/"); + + local vid = string.sub(product, 1, pos - 1) + + local pid = string.sub(product, pos + 1, string.len(product)) + + pos = string.find(pid, "/") + pid = string.sub(pid, 1, pos - 1) + + row["description"] = name + row["model"] = model + row["id"] = hex_align(vid, 4) .. ":" .. hex_align(pid, 4) + row["name"] = devname + row["product"] = product + + table.insert(data, row) + end + + return data +end + +local printers = detect_usb_printers() + +v = m:section(Table, printers, translate("Detected printers")) + +v:option(DummyValue, "description", translate("Description")) +v:option(DummyValue, "model", translate("Printer Model")) +v:option(DummyValue, "id", translate("VID/PID")) +v:option(DummyValue, "name", translate("Device Name")) + +net = net.init(m.uci) + +s = m:section(TypedSection, "printer", translate("Bindings")) +s.addremove = true +s.anonymous = true + +s:option(Flag, "enabled", translate("enable")) + +d = s:option(Value, "device", translate("Device")) +d.rmempty = true + +for key, item in ipairs(printers) do + d:value(item["product"], item["description"] .. " [" .. item["id"] .. "]") +end + +b = s:option(Value, "bind", translate("Interface"), translate("Specifies the interface to listen on.")) +b.template = "cbi/network_netlist" +b.nocreate = true +b.unspecified = true + +function b.cfgvalue(...) + local v = Value.cfgvalue(...) + if v then + return (net:get_status_by_address(v)) + end +end + +function b.write(self, section, value) + local n = net:get_network(value) + if n and n:ipaddr() then + Value.write(self, section, n:ipaddr()) + end +end + +p = s:option(ListValue, "port", translate("Port"), translate("TCP listener port.")) +p.rmempty = true +for i = 0, 9 do + p:value(i, 9100 + i) +end + +s:option(Flag, "bidirectional", translate("Bidirectional mode")) + +return m diff --git a/luci-app-usb-printer/po/zh-cn b/luci-app-usb-printer/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-app-usb-printer/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-usb-printer/po/zh_Hans/luci-app-usb-printer.po b/luci-app-usb-printer/po/zh_Hans/luci-app-usb-printer.po new file mode 100644 index 00000000..1e2575db --- /dev/null +++ b/luci-app-usb-printer/po/zh_Hans/luci-app-usb-printer.po @@ -0,0 +1,83 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Project-Id-Version: PACKAGE VERSION\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh-Hans\n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" + +#: luasrc/model/cbi/usb_printer.lua:128 +msgid "Bidirectional mode" +msgstr "双向模式" + +#: luasrc/model/cbi/usb_printer.lua:90 +msgid "Bindings" +msgstr "绑定" + +#: luasrc/model/cbi/usb_printer.lua:83 +msgid "Description" +msgstr "" + +#: luasrc/model/cbi/usb_printer.lua:81 +msgid "Detected printers" +msgstr "检测到的打印机" + +#: luasrc/model/cbi/usb_printer.lua:96 +msgid "Device" +msgstr "设备" + +#: luasrc/model/cbi/usb_printer.lua:86 +msgid "Device Name" +msgstr "设备名" + +#: luasrc/model/cbi/usb_printer.lua:103 +msgid "Interface" +msgstr "" + +#: luasrc/model/cbi/usb_printer.lua:122 +msgid "Port" +msgstr "端口" + +#: luasrc/model/cbi/usb_printer.lua:84 +msgid "Printer Model" +msgstr "打印机型号" + +#: luasrc/model/cbi/usb_printer.lua:21 +msgid "" +"Shares multiple USB printers via TCP/IP.
When modified bingings, re-" +"plug usb connectors to take effect.
This module requires kmod-usb-" +"printer." +msgstr "" +"通过 TCP/IP 共享 USB 打印机。
修改设置后,请重新连接打印机以使设置生效。" +"
此模块需要 kmod-usb-printer 支持。" + +#: luasrc/model/cbi/usb_printer.lua:103 +msgid "Specifies the interface to listen on." +msgstr "指定要监听的接口。" + +#: luasrc/model/cbi/usb_printer.lua:122 +msgid "TCP listener port." +msgstr "TCP 监听端口。" + +#: luasrc/controller/usb_printer.lua:28 luasrc/model/cbi/usb_printer.lua:20 +msgid "USB Printer Server" +msgstr "USB 打印服务器" + +#: luasrc/model/cbi/usb_printer.lua:85 +msgid "VID/PID" +msgstr "" + +#: luasrc/model/cbi/usb_printer.lua:94 +msgid "enable" +msgstr "启用" + +#~ msgid "Settings" +#~ msgstr "设置" + +#~ msgid "NAS" +#~ msgstr "网络存储" + +#~ msgid "Architecture" +#~ msgstr "架构" diff --git a/luci-app-usb-printer/po/zh_Hans/usb-printer.po b/luci-app-usb-printer/po/zh_Hans/usb-printer.po new file mode 100644 index 00000000..d1544e4a --- /dev/null +++ b/luci-app-usb-printer/po/zh_Hans/usb-printer.po @@ -0,0 +1,65 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-05-18 01:34+0800\n" +"PO-Revision-Date: 2014-05-18 01:34+0800\n" +"Last-Translator: hackpascal \n" +"Language-Team: \n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Pootle 2.0.6\n" + +msgid "Bidirectional mode" +msgstr "双向模式" + +msgid "Bindings" +msgstr "绑定" + +msgid "Device" +msgstr "设备" + +msgid "Device Name" +msgstr "设备名" + +msgid "Detected printers" +msgstr "检测到的打印机" + +msgid "" +"Shares multiple USB printers via TCP/IP.
" +"When modified bingings, re-plug usb connectors to take effect.
" +"This module requires kmod-usb-printer." +msgstr "" +"通过 TCP/IP 共享 USB 打印机。
修改设置后,请重新连接打印机以使设置生效。
" +"此模块需要 kmod-usb-printer 支持。" + +msgid "Port" +msgstr "端口" + +msgid "Printer Model" +msgstr "打印机型号" + +msgid "Settings" +msgstr "设置" + +msgid "TCP listener port." +msgstr "TCP 监听端口。" + +msgid "enable" +msgstr "启用" + +msgid "USB Printer Server" +msgstr "USB 打印服务器" + +msgid "Specifies the interface to listen on." +msgstr "指定要监听的接口。" + +msgid "NAS" +msgstr "网络存储" + +msgid "Architecture" +msgstr "架构" + diff --git a/luci-app-usb-printer/root/etc/config/usb_printer b/luci-app-usb-printer/root/etc/config/usb_printer new file mode 100644 index 00000000..e69de29b diff --git a/luci-app-usb-printer/root/etc/hotplug.d/usb/10-usb_printer b/luci-app-usb-printer/root/etc/hotplug.d/usb/10-usb_printer new file mode 100755 index 00000000..c250fe78 --- /dev/null +++ b/luci-app-usb-printer/root/etc/hotplug.d/usb/10-usb_printer @@ -0,0 +1,7 @@ +#!/bin/sh +# Copyright (C) 2005-2014 NowRush Studio +# Author: hackpascal + +if [ x"$INTERFACE" = x"7/1/1" ] || [ x"$INTERFACE" = x"7/1/2" ]; then + /usr/bin/usb_printer_hotplug "$PRODUCT" "$ACTION" +fi diff --git a/luci-app-usb-printer/root/etc/init.d/usb_printer b/luci-app-usb-printer/root/etc/init.d/usb_printer new file mode 100755 index 00000000..4da5e344 --- /dev/null +++ b/luci-app-usb-printer/root/etc/init.d/usb_printer @@ -0,0 +1,22 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2005-2013 NowRush Studio +# Author: hackpascal + +START=70 + +stop() { + killall p910nd 2>/dev/null +} + +start() { + for lps in `/usr/bin/detectlp`; do + product=`echo $lps | cut -d , -f 2` + + /usr/bin/usb_printer_hotplug "$product" add + done +} + +restart() { + stop + start +} diff --git a/luci-app-usb-printer/root/etc/uci-defaults/luci-usb-printer b/luci-app-usb-printer/root/etc/uci-defaults/luci-usb-printer new file mode 100755 index 00000000..a1beba72 --- /dev/null +++ b/luci-app-usb-printer/root/etc/uci-defaults/luci-usb-printer @@ -0,0 +1,5 @@ +#!/bin/sh + +[ -f /etc/init.d/p910nd ] && /etc/init.d/p910nd disable + +exit 0 diff --git a/luci-app-usb-printer/root/usr/bin/detectlp b/luci-app-usb-printer/root/usr/bin/detectlp new file mode 100755 index 00000000..b69385bb --- /dev/null +++ b/luci-app-usb-printer/root/usr/bin/detectlp @@ -0,0 +1,20 @@ +#!/bin/sh + +lp_path=/sys/class/usbmisc + +if ! [ -d "$lp_path" ]; then + exit +fi + +cd $lp_path + +for lps in `ls`; do + desc_file=$lp_path/$lps/device/ieee1284_id + uevent_file=$lp_path/$lps/device/uevent + + name=`cat $desc_file | sed 's/.*DES:\(.*\);.*/\1/' | cut -d ';' -f 1` + model=`cat $desc_file | sed 's/.*MDL:\(.*\);.*/\1/' | cut -d ';' -f 1` + product=`cat $uevent_file | grep PRODUCT= | sed 's/PRODUCT=\(.*\)/\1/'` + + echo $lps,$product,$model,$name; +done diff --git a/luci-app-usb-printer/root/usr/bin/usb_printer_hotplug b/luci-app-usb-printer/root/usr/bin/usb_printer_hotplug new file mode 100755 index 00000000..16b4ccc4 --- /dev/null +++ b/luci-app-usb-printer/root/usr/bin/usb_printer_hotplug @@ -0,0 +1,72 @@ +#!/bin/sh +# Copyright (C) 2005-2014 NowRush Studio +# Author: hackpascal + +. $IPKG_INSTROOT/lib/functions.sh + +PRODUCT=$1 +ACTION=$2 + +DEVICES= + +check_printer() { + local cfg=$1 + local enabled + local device_id + local bind_ip + local port + local bidirect + local device_file + local args="" + local pid_file + + config_get_bool enabled "$cfg" enabled 0 + [ "$enabled" -eq 0 ] && return 0 + + config_get device_id "$cfg" device "" + config_get bind_ip "$cfg" bind "0.0.0.0" + config_get port "$cfg" port "" + config_get_bool bidirect "$cfg" bidirectional "0" + + if [ -z "$device_id" ] || [ -z "$port" ]; then + return + fi + + if [ x"$PRODUCT" != x"$device_id" ]; then + return + fi + + device_file=`echo "$DEVICES" | grep $device_id | cut -d , -f 1` + + if [ "$ACTION" = "add" ] && [ -z "$device_file" ]; then + return + fi + + pid_file=/var/run/p910${port}d.pid + [ -f $pid_file ] && kill `cat $pid_file` 2>/dev/null + + if [ "$ACTION" = "add" ]; then + if [ "$bidirect" != 0 ]; then + args='-b' + fi + + logger "usb_printer: start p910nd on $bind_ip:$port for /dev/usb/$device_file" + /usr/sbin/p910nd $args -f /dev/usb/$device_file -i $bind_ip $port + fi +} + +if [ -z "$PRODUCT" ] || [ -z "$ACTION" ]; then + echo "Arguements required" + exit 1 +fi + +if [ "$ACTION" != "add" ] && [ "$ACTION" != "remove" ]; then + echo "Invalid action arguement" + exit 1 +fi + +DEVICES=`/usr/bin/detectlp` + +config_load usb_printer + +config_foreach check_printer printer diff --git a/luci-app-usb-printer/root/usr/share/rpcd/acl.d/luci-app-usb-printer.json b/luci-app-usb-printer/root/usr/share/rpcd/acl.d/luci-app-usb-printer.json new file mode 100644 index 00000000..4a43d9e0 --- /dev/null +++ b/luci-app-usb-printer/root/usr/share/rpcd/acl.d/luci-app-usb-printer.json @@ -0,0 +1,11 @@ +{ + "luci-app-usb-printer": { + "description": "Grant UCI access for luci-app-usb-printer", + "read": { + "uci": [ "usb_printer" ] + }, + "write": { + "uci": [ "usb_printer" ] + } + } +} diff --git a/luci-app-usb-printer/root/usr/share/ucitrack/luci-app-usb-printer.json b/luci-app-usb-printer/root/usr/share/ucitrack/luci-app-usb-printer.json new file mode 100644 index 00000000..7f1c1539 --- /dev/null +++ b/luci-app-usb-printer/root/usr/share/ucitrack/luci-app-usb-printer.json @@ -0,0 +1,4 @@ +{ + "config": "usb_printer", + "init": [ "usb_printer" ] +} diff --git a/luci-app-zerotier/Makefile b/luci-app-zerotier/Makefile new file mode 100644 index 00000000..8ef1c0c0 --- /dev/null +++ b/luci-app-zerotier/Makefile @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: GPL-3.0-only +# +# Copyright (C) 2022 ImmortalWrt.org + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI for Zerotier +LUCI_DEPENDS:=+zerotier +jsonfilter +ucode +LUCI_PKGARCH:=all + +define Package/luci-app-zerotier/conffiles +/etc/config/zero/ +/etc/config/zerotier +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature + + diff --git a/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js b/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js new file mode 100644 index 00000000..a26d437d --- /dev/null +++ b/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js @@ -0,0 +1,94 @@ +/* SPDX-License-Identifier: GPL-3.0-only + * + * Copyright (C) 2022 ImmortalWrt.org + */ + +'use strict'; +'require form'; +'require poll'; +'require rpc'; +'require uci'; +'require view'; + +var callServiceList = rpc.declare({ + object: 'service', + method: 'list', + params: ['name'], + expect: { '': {} } +}); + +function getServiceStatus() { + return L.resolveDefault(callServiceList('zerotier'), {}).then(function (res) { + var isRunning = false; + try { + isRunning = res['zerotier']['instances']['instance1']['running']; + } catch (e) { } + return isRunning; + }); +} + +function renderStatus(isRunning) { + var spanTemp = '%s %s'; + var renderHTML; + if (isRunning) { + renderHTML = String.format(spanTemp, 'green', _('ZeroTier'), _('RUNNING')); + } else { + renderHTML = String.format(spanTemp, 'red', _('ZeroTier'), _('NOT RUNNING')); + } + + return renderHTML; +} + +return view.extend({ + load: function() { + return Promise.all([ + uci.load('zerotier') + ]); + }, + + render: function(data) { + var m, s, o; + + m = new form.Map('zerotier', _('ZeroTier'), + _('ZeroTier is an open source, cross-platform and easy to use virtual LAN.')); + + s = m.section(form.TypedSection); + s.anonymous = true; + s.render = function () { + poll.add(function () { + return L.resolveDefault(getServiceStatus()).then(function (res) { + var view = document.getElementById("service_status"); + view.innerHTML = renderStatus(res); + }); + }); + + return E('div', { class: 'cbi-section', id: 'status_bar' }, [ + E('p', { id: 'service_status' }, _('Collecting data ...')) + ]); + } + + s = m.section(form.NamedSection, 'sample_config', 'config'); + + o = s.option(form.Flag, 'enabled', _('Enable')); + o.default = o.disabled; + o.rmempty = false; + + o = s.option(form.DynamicList, 'join', _('Network ID')); + o.rmempty = false; + + o = s.option(form.Flag, 'nat', _('Auto NAT clients'), + _('Allow ZeroTier clients access your LAN network.')); + o.default = o.disabled; + o.rmempty = false; + + o = s.option(form.Button, '_panel', _('ZeroTier Central'), + _('Create or manage your ZeroTier network, and auth clients who could access.')); + o.inputtitle = _('Open website'); + o.inputstyle = 'apply'; + o.onclick = function () { + window.open("https://my.zerotier.com/network", '_blank'); + } + + return m.render(); + } +}); diff --git a/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js b/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js new file mode 100644 index 00000000..de726662 --- /dev/null +++ b/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js @@ -0,0 +1,119 @@ +/* SPDX-License-Identifier: GPL-3.0-only + * + * Copyright (C) 2022 ImmortalWrt.org + */ + +'use strict'; +'require fs'; +'require ui'; +'require view'; + +return view.extend({ + load: function() { + return fs.exec('/sbin/ifconfig').then(function(res) { + if (res.code !== 0 || !res.stdout || res.stdout.trim() === '') { + ui.addNotification(null, E('p', {}, _('Unable to get interface info: %s.').format(res.message))); + return ''; + } + + var interfaces = res.stdout.match(/zt[a-z0-9]+/g); + if (!interfaces || interfaces.length === 0) + return 'No interface online.'; + + var promises = interfaces.map(function(name) { + return fs.exec('/sbin/ifconfig', [name]); + }); + + return Promise.all(promises).then(function(results) { + var data = results.map(function(res, index) { + if (res.code !== 0 || !res.stdout || res.stdout.trim() === '') { + ui.addNotification(null, E('p', {}, _('Unable to get interface %s info: %s.').format(interfaces[index], res.message))); + return null; + } + return { + name: interfaces[index], + stdout: res.stdout.trim() + }; + }).filter(Boolean); + + return data.map(function(info) { + var lines = info.stdout.split('\n'); + var parsedInfo = { + name: info.name + }; + + lines.forEach(function(line) { + if (line.includes('HWaddr')) { + parsedInfo.mac = line.split('HWaddr')[1].trim().split(' ')[0]; + } else if (line.includes('inet addr:')) { + parsedInfo.ipv4 = line.split('inet addr:')[1].trim().split(' ')[0]; + } else if (line.includes('inet6 addr:')) { + parsedInfo.ipv6 = line.split('inet6 addr:')[1].trim().split('/')[0]; + } else if (line.includes('MTU:')) { + parsedInfo.mtu = line.split('MTU:')[1].trim().split(' ')[0]; + } else if (line.includes('RX bytes:')) { + var rxMatch = line.match(/RX bytes:\d+ \(([\d.]+\s*[a-zA-Z]+)\)/); + if (rxMatch && rxMatch[1]) { + parsedInfo.rxBytes = rxMatch[1]; + } + var txMatch = line.match(/TX bytes:\d+ \(([\d.]+\s*[a-zA-Z]+)\)/); + if (txMatch && txMatch[1]) { + parsedInfo.txBytes = txMatch[1]; + } + } + }); + + return parsedInfo; + }); + }); + }); + }, + + render: function(data) { + var title = E('h2', {class: 'content'}, _('ZeroTier')); + var desc = E('div', {class: 'cbi-map-descr'}, _('ZeroTier is an open source, cross-platform and easy to use virtual LAN.')); + + if (!Array.isArray(data)) { + return E('div', {}, [title, desc, E('div', {}, _('No interface online.'))]); + } + var rows = data.flatMap(function(interfaceData) { + return [ + E('th', {class: 'th', colspan: '2'}, _('Network Interface Information')), + E('tr', {class: 'tr'}, [ + E('td', {class: 'td left', width: '25%'}, _('Interface Name')), + E('td', {class: 'td left', width: '25%'}, interfaceData.name) + ]), + E('tr', {class: 'tr'}, [ + E('td', {class: 'td left', width: '25%'}, _('MAC Address')), + E('td', {class: 'td left', width: '25%'}, interfaceData.mac) + ]), + E('tr', {class: 'tr'}, [ + E('td', {class: 'td left', width: '25%'}, _('IPv4 Address')), + E('td', {class: 'td left', width: '25%'}, interfaceData.ipv4) + ]), + E('tr', {class: 'tr'}, [ + E('td', {class: 'td left', width: '25%'}, _('IPv6 Address')), + E('td', {class: 'td left', width: '25%'}, interfaceData.ipv6) + ]), + E('tr', {class: 'tr'}, [ + E('td', {class: 'td left', width: '25%'}, _('MTU')), + E('td', {class: 'td left', width: '25%'}, interfaceData.mtu) + ]), + E('tr', {class: 'tr'}, [ + E('td', {class: 'td left', width: '25%'}, _('Total Download')), + E('td', {class: 'td left', width: '25%'}, interfaceData.rxBytes) + ]), + E('tr', {class: 'tr'}, [ + E('td', {class: 'td left', width: '25%'}, _('Total Upload')), + E('td', {class: 'td left', width: '25%'}, interfaceData.txBytes) + ]) + ]; + }); + + return E('div', {}, [title, desc, E('table', { 'class': 'table' }, rows)]); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-zerotier/po/templates/zerotier.pot b/luci-app-zerotier/po/templates/zerotier.pot new file mode 100644 index 00000000..b8619f45 --- /dev/null +++ b/luci-app-zerotier/po/templates/zerotier.pot @@ -0,0 +1,112 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:80 +msgid "Allow ZeroTier clients access your LAN network." +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:79 +msgid "Auto NAT clients" +msgstr "" + +#: applications/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json:14 +msgid "Base settings" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:66 +msgid "Collecting data ..." +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:85 +msgid "" +"Create or manage your ZeroTier network, and auth clients who could access." +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:72 +msgid "Enable" +msgstr "" + +#: applications/luci-app-zerotier/root/usr/share/rpcd/acl.d/luci-app-zerotier.json:3 +msgid "Grant access to ZeroTier configuration" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:91 +msgid "IPv4 Address" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:95 +msgid "IPv6 Address" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:83 +msgid "Interface Name" +msgstr "" + +#: applications/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json:22 +msgid "Interface info" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:87 +msgid "MAC Address" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:99 +msgid "MTU" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:36 +msgid "NOT RUNNING" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:76 +msgid "Network ID" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:81 +msgid "Network Interface Information" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:77 +msgid "No interface online." +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:86 +msgid "Open website" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:34 +msgid "RUNNING" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:103 +msgid "Total Download" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:107 +msgid "Total Upload" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:30 +msgid "Unable to get interface %s info: %s." +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:15 +msgid "Unable to get interface info: %s." +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:34 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:36 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:52 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:73 +#: applications/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json:3 +msgid "ZeroTier" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:84 +msgid "ZeroTier Central" +msgstr "" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:53 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:74 +msgid "ZeroTier is an open source, cross-platform and easy to use virtual LAN." +msgstr "" diff --git a/luci-app-zerotier/po/zh-cn b/luci-app-zerotier/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-app-zerotier/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-zerotier/po/zh_Hans/zerotier.po b/luci-app-zerotier/po/zh_Hans/zerotier.po new file mode 100644 index 00000000..76f51b53 --- /dev/null +++ b/luci-app-zerotier/po/zh_Hans/zerotier.po @@ -0,0 +1,118 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:80 +msgid "Allow ZeroTier clients access your LAN network." +msgstr "允许 ZeroTier 客户端访问您的局域网。" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:79 +msgid "Auto NAT clients" +msgstr "自动客户端 NAT" + +#: applications/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json:14 +msgid "Base settings" +msgstr "基本设置" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:66 +msgid "Collecting data ..." +msgstr "收集数据中 ..." + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:85 +msgid "" +"Create or manage your ZeroTier network, and auth clients who could access." +msgstr "创建或管理您的 ZeroTier 网络,并允许客户端接入。" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:72 +msgid "Enable" +msgstr "启用" + +#: applications/luci-app-zerotier/root/usr/share/rpcd/acl.d/luci-app-zerotier.json:3 +msgid "Grant access to ZeroTier configuration" +msgstr "授予访问 ZeroTier 配置的权限" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:91 +msgid "IPv4 Address" +msgstr "IPv4 地址" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:95 +msgid "IPv6 Address" +msgstr "IPv6 地址" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:83 +msgid "Interface Name" +msgstr "接口名称" + +#: applications/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json:22 +msgid "Interface info" +msgstr "接口信息" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:87 +msgid "MAC Address" +msgstr "MAC 地址" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:99 +msgid "MTU" +msgstr "MTU" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:36 +msgid "NOT RUNNING" +msgstr "未运行" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:76 +msgid "Network ID" +msgstr "网络 ID" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:81 +msgid "Network Interface Information" +msgstr "网络接口信息" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:77 +msgid "No interface online." +msgstr "没有在线的接口。" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:86 +msgid "Open website" +msgstr "打开网站" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:34 +msgid "RUNNING" +msgstr "运行中" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:103 +msgid "Total Download" +msgstr "总下载" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:107 +msgid "Total Upload" +msgstr "总上传" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:30 +msgid "Unable to get interface %s info: %s." +msgstr "无法获取接口 %s 的信息:%s。" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:15 +msgid "Unable to get interface info: %s." +msgstr "无法获取接口信息:%s。" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:34 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:36 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:52 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:73 +#: applications/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json:3 +msgid "ZeroTier" +msgstr "ZeroTier" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:84 +msgid "ZeroTier Central" +msgstr "ZeroTier 管理中心" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:53 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:74 +msgid "ZeroTier is an open source, cross-platform and easy to use virtual LAN." +msgstr "ZeroTier 是一个开源、跨平台且易于使用的虚拟局域网 VPN。" diff --git a/luci-app-zerotier/po/zh_Hant/zerotier.po b/luci-app-zerotier/po/zh_Hant/zerotier.po new file mode 100644 index 00000000..9a6f0b3e --- /dev/null +++ b/luci-app-zerotier/po/zh_Hant/zerotier.po @@ -0,0 +1,118 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh_Hant\n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:80 +msgid "Allow ZeroTier clients access your LAN network." +msgstr "允許 ZeroTier 用戶端訪問您的區域網路" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:79 +msgid "Auto NAT clients" +msgstr "自動 NAT 用戶端" + +#: applications/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json:14 +msgid "Base settings" +msgstr "基礎設定" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:66 +msgid "Collecting data ..." +msgstr "收集資料中..." + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:85 +msgid "" +"Create or manage your ZeroTier network, and auth clients who could access." +msgstr "新建或管理您的 ZeroTier 網路,並授權用戶端訪問" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:72 +msgid "Enable" +msgstr "啟用" + +#: applications/luci-app-zerotier/root/usr/share/rpcd/acl.d/luci-app-zerotier.json:3 +msgid "Grant access to ZeroTier configuration" +msgstr "允許訪問 ZeroTier 的配置" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:91 +msgid "IPv4 Address" +msgstr "IPv4 位址" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:95 +msgid "IPv6 Address" +msgstr "IPv6 位址" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:83 +msgid "Interface Name" +msgstr "介面名稱" + +#: applications/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json:22 +msgid "Interface info" +msgstr "介面資訊" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:87 +msgid "MAC Address" +msgstr "MAC 位址" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:99 +msgid "MTU" +msgstr "MTU" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:36 +msgid "NOT RUNNING" +msgstr "尚未執行" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:76 +msgid "Network ID" +msgstr "網路 ID" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:81 +msgid "Network Interface Information" +msgstr "網路介面資訊" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:77 +msgid "No interface online." +msgstr "沒有介面在線上。" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:86 +msgid "Open website" +msgstr "開啟網站" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:34 +msgid "RUNNING" +msgstr "正在執行" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:103 +msgid "Total Download" +msgstr "總下載量" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:107 +msgid "Total Upload" +msgstr "總上傳量" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:30 +msgid "Unable to get interface %s info: %s." +msgstr "無法取得介面 %s 的資訊:%s" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:15 +msgid "Unable to get interface info: %s." +msgstr "無法取得介面資訊:%s" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:34 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:36 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:52 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:73 +#: applications/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json:3 +msgid "ZeroTier" +msgstr "ZeroTier" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:84 +msgid "ZeroTier Central" +msgstr "ZeroTier 管理中心" + +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/base.js:53 +#: applications/luci-app-zerotier/htdocs/luci-static/resources/view/zerotier/interface.js:74 +msgid "ZeroTier is an open source, cross-platform and easy to use virtual LAN." +msgstr "ZeroTier 是一個開源、跨平台且易於使用的虛擬區域網路 VPN" diff --git a/luci-app-zerotier/root/etc/config/zerotier b/luci-app-zerotier/root/etc/config/zerotier new file mode 100644 index 00000000..34b1ad34 --- /dev/null +++ b/luci-app-zerotier/root/etc/config/zerotier @@ -0,0 +1,20 @@ + +config zerotier sample_config + option enabled 0 + + # persistent configuration folder (for ZT controller mode) + #option config_path '/etc/zerotier' + # copy to RAM to prevent writing to flash (for ZT controller mode) + #option copy_config_path '1' + + #option port '9993' + + # path to the local.conf + #option local_conf '/etc/zerotier.conf' + + # Generate secret on first start + option secret '' + + # Join a public network called Earth + list join '8056c2e21c000001' + #list join '' diff --git a/luci-app-zerotier/root/etc/hotplug.d/iface/40-zerotier b/luci-app-zerotier/root/etc/hotplug.d/iface/40-zerotier new file mode 100644 index 00000000..6e977a11 --- /dev/null +++ b/luci-app-zerotier/root/etc/hotplug.d/iface/40-zerotier @@ -0,0 +1,9 @@ +#!/bin/sh + +zero_enable="$(uci get zerotier.sample_config.enabled)" +nat_enable="$(uci get zerotier.sample_config.nat)" + +[ "$ACTION" = ifup -o "$ACTION" = ifupdate ] || exit 0 +[ "$ACTION" = ifupdate -a -z "$IFUPDATE_ADDRESSES" -a -z "$IFUPDATE_DATA" ] && exit 0 +[ "$zero_enable" -eq "1" -a "${nat_enable}" -eq "1" ] || exit 0 +/etc/zerotier.start > /tmp/zero.log 2>&1 & diff --git a/luci-app-zerotier/root/etc/init.d/zerotier b/luci-app-zerotier/root/etc/init.d/zerotier new file mode 100755 index 00000000..715df14d --- /dev/null +++ b/luci-app-zerotier/root/etc/init.d/zerotier @@ -0,0 +1,115 @@ +#!/bin/sh /etc/rc.common + +START=99 + +USE_PROCD=1 + +PROG=/usr/bin/zerotier-one +CONFIG_PATH=/var/lib/zerotier-one + +service_triggers() { + procd_add_reload_trigger "zerotier" + procd_add_interface_trigger "interface.*.up" wan /etc/init.d/zerotier restart +} + +section_enabled() { + config_get_bool enabled "$1" 'enabled' 0 + [ $enabled -gt 0 ] +} + +start_instance() { + local cfg="$1" + local port secret config_path + local ARGS="" + + if ! section_enabled "$cfg"; then + echo "disabled in config" + return 1 + fi + + [ -d /etc/config/zero ] || mkdir -p /etc/config/zero + config_path=/etc/config/zero + + config_get_bool port $cfg 'port' + config_get secret $cfg 'secret' + + # Remove existing link or folder + rm -rf $CONFIG_PATH + + # Create link from CONFIG_PATH to config_path + if [ -n "$config_path" -a "$config_path" != $CONFIG_PATH ]; then + if [ ! -d "$config_path" ]; then + echo "ZeroTier config_path does not exist: $config_path" + return + fi + + ln -s $config_path $CONFIG_PATH + fi + + mkdir -p $CONFIG_PATH/networks.d + + if [ -n "$port" ]; then + ARGS="$ARGS -p$port" + fi + + if [ "$secret" = "generate" ]; then + echo "Generate secret - please wait..." + local sf="/tmp/zt.$cfg.secret" + + zerotier-idtool generate "$sf" > /dev/null + [ $? -ne 0 ] && return 1 + + secret="$(cat $sf)" + rm "$sf" + + uci set zerotier.$cfg.secret="$secret" + uci commit zerotier + fi + + if [ -n "$secret" ]; then + echo "$secret" > $CONFIG_PATH/identity.secret + # make sure there is not previous identity.public + rm -f $CONFIG_PATH/identity.public + fi + + add_join() { + # an (empty) config file will cause ZT to join a network + touch $CONFIG_PATH/networks.d/$1.conf + } + + config_list_foreach $cfg 'join' add_join + + procd_open_instance + procd_set_param command $PROG $ARGS $CONFIG_PATH + procd_set_param stderr 1 + procd_close_instance +} + +start_service() { + config_load 'zerotier' + config_foreach start_instance 'zerotier' + touch /tmp/zero.log && /etc/zerotier.start > /tmp/zero.log 2>&1 & +} + +stop_instance() { + rm -f /tmp/zero.log + local cfg="$1" + + rm -f "/usr/share/nftables.d/chain-pre/forward/zerotier.nft" "/usr/share/nftables.d/chain-pre/srcnat/zerotier.nft" + fw4 reload + echo "zt interface rules removed!" > /tmp/zero.log 2>&1 & + + # Remove existing link or folder + rm -f $CONFIG_PATH/networks.d/*.conf + rm -rf $CONFIG_PATH +} + +stop_service() { + config_load 'zerotier' + config_foreach stop_instance 'zerotier' +} + +reload_service() { + stop + start +} diff --git a/luci-app-zerotier/root/etc/uci-defaults/40_luci-zerotier b/luci-app-zerotier/root/etc/uci-defaults/40_luci-zerotier new file mode 100755 index 00000000..96639fb3 --- /dev/null +++ b/luci-app-zerotier/root/etc/uci-defaults/40_luci-zerotier @@ -0,0 +1,12 @@ +#!/bin/sh + +uci -q batch <<-EOF >/dev/null + delete ucitrack.@zerotier[-1] + commit ucitrack + + delete firewall.zerotier + commit firewall +EOF + +rm -f /tmp/luci-indexcache +exit 0 diff --git a/luci-app-zerotier/root/etc/zerotier.start b/luci-app-zerotier/root/etc/zerotier.start new file mode 100755 index 00000000..4eaa6c2a --- /dev/null +++ b/luci-app-zerotier/root/etc/zerotier.start @@ -0,0 +1,37 @@ +#!/bin/sh + +zero_enable="$(uci get zerotier.sample_config.enabled)" +[ "$zero_enable" -eq "1" ] || exit 1 + +count=0 +[ -f "/tmp/zero.log" ] && { + while [ -z "$(ifconfig | grep 'zt' | awk '{print $1}')" ] + do + sleep 2 + let count++ + [ "$count" -lt 5 ] || exit 19 + done +} + +nft_incdir="/usr/share/nftables.d/chain-pre" +rm -f "$nft_incdir/input/zerotier.nft" "$nft_incdir/forward/zerotier.nft" "$$nft_incdir/srcnat/zerotier.nft" + +nat_enable="$(uci get zerotier.sample_config.nat)" +[ "$nat_enable" -eq "1" ] && { + [ -d "$nft_incdir/input" ] || mkdir -p "$nft_incdir/input" + [ -d "$nft_incdir/forward" ] || mkdir -p "$nft_incdir/forward" + [ -d "$nft_incdir/srcnat" ] || mkdir -p "$nft_incdir/srcnat" + for i in $(ifconfig | grep 'zt' | awk '{print $1}') + do + ip_segment="$(ip route | grep "dev $i proto kernel" | awk '{print $1}')" + echo "iifname $i counter accept comment \"!fw4: Zerotier allow inbound $i\"" >> "$nft_incdir/input/zerotier.nft" + echo "iifname $i counter accept comment \"!fw4: Zerotier allow inbound forward $i\"" >> "$nft_incdir/forward/zerotier.nft" + echo "oifname $i counter accept comment \"!fw4: Zerotier allow outbound forward $i\"" >> "$nft_incdir/forward/zerotier.nft" + echo "oifname $i counter masquerade comment \"!fw4: Zerotier $i outbound postrouting masq\"" >> "$nft_incdir/srcnat/zerotier.nft" + [ -z "$ip_segment" ] || echo "ip saddr $ip_segment counter masquerade comment \"!fw4: Zerotier $ip_segment postrouting masq\"" >> "$nft_incdir/srcnat/zerotier.nft" + done + echo "zt interface rules added!" > "/tmp/zero.log" + uci -q set firewall.@defaults[0].auto_includes="1" + uci -q commit firewall + fw4 reload +} diff --git a/luci-app-zerotier/root/etc/zerotier/zerotier.log b/luci-app-zerotier/root/etc/zerotier/zerotier.log new file mode 100644 index 00000000..e69de29b diff --git a/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json b/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json new file mode 100644 index 00000000..956d9e47 --- /dev/null +++ b/luci-app-zerotier/root/usr/share/luci/menu.d/luci-app-zerotier.json @@ -0,0 +1,29 @@ +{ + "admin/vpn/zerotier": { + "title": "ZeroTier", + "order": 90, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": [ "luci-app-zerotier" ], + "uci": { "zerotier": true } + } + }, + "admin/vpn/zerotier/base": { + "title": "Base settings", + "order": 10, + "action": { + "type": "view", + "path": "zerotier/base" + } + }, + "admin/vpn/zerotier/interface": { + "title": "Interface info", + "order": 20, + "action": { + "type": "view", + "path": "zerotier/interface" + } + } +} diff --git a/luci-app-zerotier/root/usr/share/rpcd/acl.d/luci-app-zerotier.json b/luci-app-zerotier/root/usr/share/rpcd/acl.d/luci-app-zerotier.json new file mode 100644 index 00000000..656cb3c0 --- /dev/null +++ b/luci-app-zerotier/root/usr/share/rpcd/acl.d/luci-app-zerotier.json @@ -0,0 +1,17 @@ +{ + "luci-app-zerotier": { + "description": "Grant access to ZeroTier configuration", + "read": { + "file": { + "/sbin/ifconfig": [ "exec" ] + }, + "ubus": { + "service": [ "list" ] + }, + "uci": [ "zerotier" ] + }, + "write": { + "uci": [ "zerotier" ] + } + } +}