luci-app-diskman: add package

This commit is contained in:
CN_SZTL 2020-01-15 02:58:54 +08:00
parent 365809fafb
commit 47a46f2d2d
No known key found for this signature in database
GPG Key ID: 6850B6345C862176
16 changed files with 1974 additions and 1 deletions

View File

@ -67,7 +67,8 @@ luci-app-autoipsetadder source: [rufengsuixing/luci-app-autoipsetadder](https://
luci-app-adguardhome source: [rufengsuixing/luci-app-adguardhome](https://github.com/rufengsuixing/luci-app-adguardhome).<br/>
Rclone-OpenWrt source: [ElonH/Rclone-OpenWrt](https://github.com/ElonH/Rclone-OpenWrt).<br/>
luci-app-usb3disable source: [rufengsuixing/luci-app-usb3disable](https://github.com/rufengsuixing/luci-app-usb3disable).<br/>
luci-app-vssr source: [jerrykuku/luci-app-vssr](https://github.com/jerrykuku/luci-app-vssr).
luci-app-vssr source: [jerrykuku/luci-app-vssr](https://github.com/jerrykuku/luci-app-vssr).<br/>
luci-app-diskman source: [lisaac/luci-app-diskman](https://github.com/lisaac/luci-app-diskman).
# License
### [GPL v3](https://www.gnu.org/licenses/gpl-3.0.html).

View File

@ -0,0 +1,46 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-diskman
PKG_VERSION:=v0.1.5
PKG_RELEASE:=beta
PKG_MAINTAINER:=lisaac <https://github.com/lisaac/luci-app-diskman>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
PKG_LICENSE:=Apache-2.0
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=Disk Manager interface for LuCI
PKGARCH:=all
DEPENDS:=+e2fsprogs +parted +smartmontools +blkid +btrfs-progs
endef
define Package/$(PKG_NAME)/description
Disk Manager interface for LuCI
endef
define Build/Prepare
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
rm -fr /tmp/luci-indexcache /tmp/luci-modulecache
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/lib/lua/luci
cp -pR ./luasrc/* $(1)/usr/lib/lua/luci
# $(INSTALL_DIR) $(1)/
# cp -pR ./root/* $(1)/
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
po2lmo ./po/zh-cn/diskman.po $(1)/usr/lib/lua/luci/i18n/diskman.zh-cn.lmo
po2lmo ./po/pl/diskman.po $(1)/usr/lib/lua/luci/i18n/diskman.pl.lmo
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

View File

@ -0,0 +1,49 @@
#
# Copyright (C) 2019 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=parted
PKG_VERSION:=3.3
PKG_RELEASE:=1
PKG_BUILD_DIR:=$(BUILD_DIR)/parted-$(PKG_VERSION)
PKG_SOURCE:=parted-$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=@GNU/parted
PKG_MD5SUM:=090655d05f3c471aa8e15a27536889ec
include $(INCLUDE_DIR)/package.mk
define Package/parted
SECTION:=utils
CATEGORY:=Utilities
TITLE:=parted Partition editor
URL:=http://www.gnu.org/software/parted/index.shtml
DEPENDS:= +libuuid +libreadline +libncurses +libblkid
endef
define Package/parted/description
parted Partition editor
http://www.gnu.org/software/parted/index.shtml
endef
define Build/Configure
$(call Build/Configure/Default, \
--without-readline \
--disable-device-mapper \
--disable-nls \
)
endef
define Package/parted/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_DIR) $(1)/usr/lib
$(INSTALL_BIN) $(PKG_BUILD_DIR)/libparted/.libs/*.so* $(1)/usr/lib/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/libparted/fs/.libs/*.so* $(1)/usr/lib/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/parted/.libs/parted $(1)/usr/sbin/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/partprobe/.libs/partprobe $(1)/usr/sbin/
endef
$(eval $(call BuildPackage,parted))

View File

@ -0,0 +1,122 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-diskman>
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"
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", "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 get_disk_info(dev)
if not dev then
luci.http.status(500, "no device")
luci.http.write_json("no device")
return
elseif not nixio.fs.access("/dev/"..dev) then
luci.http.status(500, "no device")
luci.http.write_json("no device")
return
end
local dm = require "luci.model.diskman"
local device_info = dm.get_disk_info(dev)
luci.http.status(200, "ok")
luci.http.prepare_content("application/json")
luci.http.write_json(device_info)
end
function mk_p_table()
local p_table = luci.http.formvalue("p_table")
local dev = luci.http.formvalue("dev")
if not dev then
luci.http.status(500, "no device")
luci.http.write_json("no device")
return
elseif not nixio.fs.access("/dev/"..dev) then
luci.http.status(500, "no device")
luci.http.write_json("no device")
return
end
local dm = require "luci.model.diskman"
if p_table == "GPT" or p_table == "MBR" then
p_table = p_table == "MBR" and "msdos" or "gpt"
local res = luci.sys.call(dm.command.parted .. " -s /dev/" .. dev .. " mktable ".. p_table)
if res == 0 then
luci.http.status(200, "ok")
else
luci.http.status(500, "command exec error")
end
luci.http.prepare_content("application/json")
luci.http.write_json({code=res})
else
luci.http.status(404, "not support")
luci.http.prepare_content("application/json")
luci.http.write_json({code="1"})
end
end
function smart_detail(dev)
luci.template.render("diskman/smart_detail", {dev=dev})
end
function smart_attr(dev)
local dm = require "luci.model.diskman"
local cmd = io.popen(dm.command.smartctl .. " --attributes -d sat /dev/%s" % dev)
if cmd then
local attr = { }
while true do
local ln = cmd:read("*l")
if not ln then
break
elseif ln:match("^.*%d+%s+.+%s+.+%s+.+%s+.+%s+.+%s+.+%s+.+%s+.+%s+.+") then
local id,attrbute,flag,value,worst,thresh,type,updated,raw = ln:match("^%s*(%d+)%s+([%a%p]+)%s+(%w+)%s+(%d+)%s+(%d+)%s+(%d+)%s+([%a%p]+)%s+(%a+)%s+[%w%p]+%s+(.+)")
id= "%x" % id
if not id:match("^%w%w") then
id = "0%s" % id
end
attr[#attr+1]= {
id = id:upper(),
attrbute = attrbute,
flag = flag,
value = value,
worst = worst,
thresh = thresh,
type = type,
updated = updated,
raw = raw
}
else
end
end
cmd:close()
luci.http.prepare_content("application/json")
luci.http.write_json(attr)
end
end

View File

@ -0,0 +1,257 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-diskman>
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"
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(d.command.mdadm .. " --assemble --scan")
end
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman"))
end
-- disks
local disks = dm.list_devices()
d = m:section(Table, disks, translate("Disks"))
d.config = "disk"
-- option(type, id(key of table), text)
d:option(DummyValue, "path", translate("Path"))
d:option(DummyValue, "model", translate("Model"))
d:option(DummyValue, "sn", translate("Serial Number"))
d:option(DummyValue, "size_formated", translate("Size"))
d:option(DummyValue, "temp", translate("Temp"))
-- d:option(DummyValue, "sec_size", translate("Sector Size "))
d:option(DummyValue, "p_table", translate("Partition Table"))
d:option(DummyValue, "sata_ver", translate("SATA Version"))
-- d:option(DummyValue, "rota_rate", translate("Rotation Rate"))
d:option(DummyValue, "health", translate("Health"))
d:option(DummyValue, "status", translate("Status"))
d.extedit = luci.dispatcher.build_url("admin/system/diskman/partition/%s")
-- raid devices
if dm.command.mdadm then
local raid_devices = dm.list_raid_devices()
-- raid_devices = diskmanager.getRAIDdevices()
if next(raid_devices) ~= nil then
local r = m:section(Table, raid_devices, translate("RAID Devices"))
r.config = "_raid"
r:option(DummyValue, "path", translate("Path"))
r:option(DummyValue, "level", translate("RAID mode"))
r:option(DummyValue, "size_formated", translate("Size"))
r:option(DummyValue, "p_table", translate("Partition Table"))
r:option(DummyValue, "status", translate("Status"))
r:option(DummyValue, "members_str", translate("Members"))
r:option(DummyValue, "active", translate("Active"))
-- local redit = r:option(Button, "rpartition")
-- redit.inputstyle = "edit"
-- redit.inputtitle = "Edit"
-- redit.write = function(self, section)
-- local url = luci.dispatcher.build_url("admin/system/diskman/partition")
-- url = url .. "/" .. raid_devices[section].path:match("/dev/(.+)")
-- luci.http.redirect(url)
-- end
r.extedit = luci.dispatcher.build_url("admin/system/diskman/partition/%s")
end
end
--tabs
tab_section = m:section(SimpleSection)
tab_section.tabs = {
mount_point = translate("Mount Point")
}
tab_section.default_tab = "mount_point"
tab_section.template="diskman/disk_info_tab"
-- raid functions
if dm.command.mdadm then
local rname, rmembers, rlevel
tab_section.tabs.raid = translate("Raid")
local r = m:section(SimpleSection, translate("RAID Creation"))
r.template="diskman/cbi/xnullsection"
r.config = "raid"
local r_name = r:option(Value, "_name", translate("Raid Name"))
r_name.placeholder = "/dev/md0"
r_name.write = function(self, section, value)
rname = value
end
local r_level = r:option(ListValue, "_level", translate("Raid Level"))
local valid_raid = luci.util.exec("lsmod | grep md_mod")
if valid_raid:match("linear") then
r_level:value("linear", "Linear")
end
if valid_raid:match("raid456") then
r_level:value("5", "Raid 5")
r_level:value("6", "Raid 6")
end
if valid_raid:match("raid1") then
r_level:value("1", "Raid 1")
end
if valid_raid:match("raid0") then
r_level:value("0", "Raid 0")
end
if valid_raid:match("raid10") then
r_level:value("10", "Raid 10")
end
r_level.write = function(self, section, value)
rlevel = value
end
local r_member = r:option(DynamicList, "_member", 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 = r:option(Button, "_create")
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:match("^ERR") then
m.errmessage = translate(res)
return
end
dm.gen_mdadm_config()
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman"))
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"))
table_mp.config = "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
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
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
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"
DummyValue.render(self, section, scope)
end
end
v_mount_option.write = function(self, section, value)
_mount_point.mount_options = value
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"
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 == " Mount " then
luci.util.exec("mkdir -p ".. _mount_point.mount_point)
res = luci.util.exec("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")
else
res = luci.util.exec("umount "..mount_point[section].mount_point .. " 2>&1")
end
if res:match("^mount:") then
m.errmessage = luci.util.pcdata(res)
else
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman"))
end
end
-- -- btrfs
if dm.command.btrfs then
tab_section.tabs.btrfs = translate("Btrfs")
local btrfs_devices = {}
local table_btrfs = m:section(Table, btrfs_devices, translate("Btrfs"))
table_btrfs.config = "btrfs"
end
return m

View File

@ -0,0 +1,332 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-diskman>
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"
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)
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)) 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 .. "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, "useage", translate("Useage"))
s_partition_table:option(DummyValue, "mount_point", translate("Mount Point"))
local val_fs = s_partition_table:option(Value, "fs", translate("File System"))
val_fs.forcewrite = true
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 = "cbi/value"
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"
end
DummyValue.render(self, section, scope)
end
btn_format = s_partition_table:option(Button, "_format")
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 = "Partition NOT found!"
return
end
local fs = disk_info.partitions[section]._fs
if not format_cmd[fs] then
m.errmessage = "Filesystem NOT support!"
return
end
local cmd = format_cmd[fs].cmd .. " " .. format_cmd[fs].option .. " " .. partition_name
-- luci.util.perror(cmd)
local res = luci.sys.exec(cmd)
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman/partition/" .. dev))
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)
-- luci.util.perror(value)
if value == "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 = "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 = "Invalid End Sector!"
return
end
local part_type = "primary"
-- create partition table if no partition table
if disk_info.p_table == "UNKNOWN" then
local cmd = dm.command.parted .. " -s /dev/" .. dev .. " mktable gpt"
elseif 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
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)
if res:match("Error.+") then
m.errmessage = res
else
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman/partition/" .. dev))
end
elseif value == "Remove" then
-- remove partition
local number = tostring(disk_info.partitions[section].number)
if (not number) or (number == "") then
m.errmessage = "Partition not exists!"
return
end
local cmd = dm.command.parted .. " -s /dev/" .. dev .. " rm " .. number
local res = luci.util.exec(cmd)
if res:match("Error.+") then
m.errmessage = res
else
luci.http.redirect(luci.dispatcher.build_url("admin/system/diskman/partition/" .. dev))
end
end
end
end
return m

View File

@ -0,0 +1,527 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2019 lisaac <https://github.com/lisaac/luci-app-diskman>
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 ver = require "luci.version"
local CMD = {"parted", "mdadm", "blkid", "smartctl", "df", "sgdisk", "btrfs"}
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
local mounts = nixio.fs.readfile("/proc/mounts") or ""
local swaps = nixio.fs.readfile("/proc/swaps") or ""
local df = luci.sys.exec(d.command.df) 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 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 == "INFORMATION" then
section = 1
elseif attrib == "READ 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" then
smart_info.temp = val .. "°C"
elseif attrib == "Rotation Rate" then
smart_info.rota_rate = val
elseif attrib == "SATA Version is" then
smart_info.sata_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, dk_root_dir
-- if use luci-in-dokcer, exclude the docker overlay mounts
if ver.distname == "LuCI in Docker" then
local _o, dk = pcall(require,"luci.docker")
if _o and dk then
dk_root_dir = dk.new():info().body.DockerRootDir
end
end
for m in mounts:gmatch("/dev/"..partition.." ([^ ]*)") do
if dk_root_dir and m:match("/host" .. dk_root_dir) then
else
mount_point = (mount_point and (mount_point .. " ") or "") .. m
end
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
local get_partition_useage = function(partition)
if not nixio.fs.access("/dev/"..partition) then return false end
local useage = df:match("\n/dev/" .. partition .. "%s+%d+%s+%d+%s+%d+%s+([0-9]+)%%%s")
useage = useage and (useage .. "%") or false
return useage
end
local get_parted_info = function(device)
if not device then return end
local result = {partitions={}}
local DEVICE_INFO_KEYS = { "path", "size", "type", "logic_sec", "phy_sec", "p_table", "model", "flags" }
local PARTITION_INFO_KEYS = { "number", "sec_start", "sec_end", "size", "fs", "tag_name", "flags" }
local partition_temp
local partitions_temp = {}
local disk_temp
for line in luci.util.execi(d.command.parted .. " -s -m /dev/" .. device .. " unit s print free", "r") do
if line:find("^/dev/"..device..":.+") then
disk_temp = parse_parted_info(DEVICE_INFO_KEYS, line)
disk_temp.partitions = {}
if disk_temp["size"] then
local length = disk_temp["size"]:gsub("^(%d+)s$", "%1")
local newsize = tostring(tonumber(length)*tonumber(disk_temp["logic_sec"]))
disk_temp["size"] = newsize
end
if disk_temp["p_table"] == "msdos" then
disk_temp["p_table"] = "MBR"
else
disk_temp["p_table"] = disk_temp["p_table"]:upper()
end
elseif line:find("^%d-:.+") then
partition_temp = parse_parted_info(PARTITION_INFO_KEYS, line)
-- use human-readable form instead of sector number
if partition_temp["size"] then
local length = partition_temp["size"]:gsub("^(%d+)s$", "%1")
local newsize = (tonumber(length) * tonumber(disk_temp["logic_sec"]))
partition_temp["size"] = newsize
partition_temp["size_formated"] = byte_format(newsize)
end
partition_temp["number"] = tonumber(partition_temp["number"]) or -1
if partition_temp["fs"] == "free" then
partition_temp["number"] = -1
partition_temp["fs"] = "Free Space"
partition_temp["name"] = "-"
elseif device:match("sd") or device:match("sata") then
partition_temp["name"] = device..partition_temp["number"]
elseif device:match("mmcblk") or device:match("md") then
partition_temp["name"] = device.."p"..partition_temp["number"]
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 "-"
partition_temp["useage"] = partition_temp["mount_point"]~="-" and get_partition_useage(partition_temp["name"]) or "-"
-- 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["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 real_size_sec = tonumber(nixio.fs.readfile("/sys/block/"..device.."/"..p["name"].."/size")) * tonumber(disk_temp.phy_sec)
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
end
end
end
result = disk_temp
result.partitions = partitions_temp
return result
end
local mddetail = function(mdpath)
local detail = {}
local path = mdpath:match("^/dev/md%d+$")
if path then
local mdadm = io.popen(d.command.mdadm .. " --detail "..path, "r")
for line in mdadm:lines() do
local key, value = line:match("^%s*(.+) : (.+)")
if key then
detail[key] = value
end
end
mdadm:close()
end
return detail
end
-- return {{device="", mount_points="", fs="", mount_options="", dump="", pass=""}..}
d.get_mount_points = function()
local mount
local res = {}
local h ={"device", "mount_point", "fs", "mount_options", "dump", "pass"}
for mount in mounts:gmatch("[^\n]+") do
local device = mount:match("^([^%s]+)%s+.+")
if "overlay" ~= device and "cgroup" ~= device and "sysfs" ~= device and "proc" ~= device and "udev" ~= device and "devpts" ~= device and "hugetlbfs" ~= device and "mqueue" ~= device 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, useage },
2 = { number, name, sec_start, sec_end, size, size_mounted, fs, tag_name, type, flags, mount_point, useage },
...
}
--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, then get smart_info again
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]$")
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)))
local ss = tonumber(fs.readfile(string.format("/sys/class/block/%s/queue/logical_block_size", bname)))
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.fat", option = "-F 32 -I" },
exfat = { cmd = "mkexfat", option = "-f" },
hfsplus = { cmd = "mkhfs", option = "-f" },
ntfs = { cmd = "mkntfs", option = "-f" },
swap = { cmd = "mkswap", option = "-f" },
btrfs = { cmd = "mkfs.btrfs", option = "-f" }
}
result = {}
for fmt, obj in pairs(AVAILABLE_FMTS) do
local cmd = luci.sys.exec("/usr/bin/which " .. obj["cmd"])
if cmd:match(obj["cmd"]) then
result[fmt] = { cmd = cmd:match("^.+"..obj["cmd"]) ,option = obj["option"] }
end
end
return result
end
d.create_raid = function(rname, rlevel, rmembers)
local mb = {}
for _, v in ipairs(rmembers) do
mb[v]=v
end
rmembers = {}
for _, v in pairs(mb) do
table.insert(rmembers, v)
end
if type(rname) == "string" then
if rname:match("^md%d-%s+") then
rname = "/dev/"..rname:match("^(md%d-)%s+")
elseif rname:match("^/dev/md%d-%s+") then
rname = "/dev/"..rname:match("^(/dev/md%d-)%s+")
elseif not rname:match("/") then
rname = "/dev/md/".. rname
else
return "ERR: Invalid raid name"
end
else
local mdnum = 0
for num=1,127 do
local md = io.open("/dev/md"..tostring(num), "r")
if md == nil then
mdnum = num
break
else
io.close(md)
end
end
if mdnum == 0 then return "ERR: Cannot find proper md number" end
rname = "/dev/md"..mdnum
end
if rlevel == "5" or rlevel == "6" then
if #rmembers < 3 then return "ERR: Not enough members" end
end
if rlevel == "10" then
if #rmembers < 4 then return "ERR: Not enough members" end
end
if #rmembers < 2 then return "ERR: Not enough members" end
local cmd = d.command.mdadm .. " --create "..rname.." --run --assume-clean --homehost=any --level=" .. rlevel .. " --raid-devices=" .. #rmembers .. " " .. table.concat(rmembers, " ")
local res = luci.util.exec(cmd)
return res
end
d.gen_mdadm_config = function()
if not nixio.fs.access("/etc/config/mdadm") then return end
local uci = require "luci.model.uci"
local x = uci.cursor()
-- delete all array sections
x:foreach("mdadm", "array", function(s) x:delete("mdadm",s[".name"]) end)
local cmd = d.command.mdadm .. " -D -s"
--ARRAY /dev/md1 metadata=1.2 name=any:1 UUID=f998ae14:37621b27:5c49e850:051f6813
--ARRAY /dev/md3 metadata=1.2 name=any:3 UUID=c068c141:4b4232ca:f48cbf96:67d42feb
for _, v in ipairs(luci.util.execl(cmd)) do
local device, uuid = v:match("^ARRAY%s-([^%s]+)%s-[^%s]-%s-[^%s]-%s-UUID=([^%s]+)%s-")
if device and uuid then
local section_name = x:add("mdadm", "array")
x:set("mdadm", section_name, "device", device)
x:set("mdadm", section_name, "uuid", uuid)
end
end
x:commit("mdadm")
-- enable mdadm
luci.util.exec("/etc/init.d/mdadm enable")
end
return d

View File

@ -0,0 +1,7 @@
<%+cbi/valueheader%>
<% if self:cfgvalue(section) ~= false then %>
<input class="cbi-button cbi-button-<%=self.inputstyle or "button" %>" type="submit"<%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> <% if self.view_disabled then %> disabled <% end %>/>
<% else %>
-
<% end %>
<%+cbi/valuefooter%>

View File

@ -0,0 +1,7 @@
<div style="display: inline-block;">
<% if self:cfgvalue(section) ~= false then %>
<input class="cbi-button cbi-button-<%=self.inputstyle or "button" %>" type="submit"" <% if self.disable then %>disabled <% end %><%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> />
<% else %>
-
<% end %>
</div>

View File

@ -0,0 +1,37 @@
<div class="cbi-section" id="cbi-<%=self.config%>-section">
<% if self.title and #self.title > 0 then -%>
<legend><%=self.title%></legend>
<%- end %>
<% if self.description and #self.description > 0 then -%>
<div class="cbi-section-descr"><%=self.description%></div>
<%- end %>
<div class="cbi-section-node">
<div id="cbi-<%=self.config%>-<%=tostring(self):sub(8)%>">
<% self:render_children(1, scope or {}) %>
</div>
<% if self.error and self.error[1] then -%>
<div class="cbi-section-error">
<ul><% for _, e in ipairs(self.error[1]) do -%>
<li>
<%- if e == "invalid" then -%>
<%:One or more fields contain invalid values!%>
<%- elseif e == "missing" then -%>
<%:One or more required fields have no value!%>
<%- else -%>
<%=pcdata(e)%>
<%- end -%>
</li>
<%- end %></ul>
</div>
<%- end %>
</div>
</div>
<%-
if type(self.hidden) == "table" then
for k, v in pairs(self.hidden) do
-%>
<input type="hidden" id="<%=k%>" name="<%=k%>" value="<%=pcdata(v)%>" />
<%-
end
end
%>

View File

@ -0,0 +1,88 @@
<% if not self.embedded then %>
<form method="post" enctype="multipart/form-data" action="<%=REQUEST_URI%>"<%=
attr("data-strings", luci.util.serialize_json({
label = {
choose = translate('-- Please choose --'),
custom = translate('-- custom --'),
},
path = {
resource = resource,
browser = url("admin/filebrowser")
}
}))
%>>
<script type="text/javascript" src="<%=resource%>/cbi.js"></script>
<input type="hidden" name="token" value="<%=token%>" />
<input type="hidden" name="cbi.submit" value="1" /><%
end
%><div class="cbi-map" id="cbi-<%=self.config%>"><%
if self.title and #self.title > 0 then
%><h2 name="content"><%=self.title%></h2><%
end
if self.description and #self.description > 0 then
%><div class="cbi-map-descr"><%=self.description%></div><%
end
self:render_children()
%></div><%
if self.message then
%><div class="alert-message notice"><%=self.message%></div><%
end
if self.errmessage then
%><div class="alert-message warning"><%=self.errmessage%></div><%
end
if not self.embedded then
if type(self.hidden) == "table" then
local k, v
for k, v in pairs(self.hidden) do
%><input type="hidden" id="<%=k%>" name="<%=k%>" value="<%=pcdata(v)%>" /><%
end
end
local display_back = (self.redirect)
local display_cancel = (self.cancel ~= false and self.on_cancel)
local display_skip = (self.flow and self.flow.skip)
local display_submit = (self.submit ~= false)
local display_reset = (self.reset ~= false)
if display_back or display_cancel or display_skip or display_submit or display_reset then
%><div class="cbi-page-actions"><%
if display_back then
%><input class="cbi-button cbi-button-link" type="button" value="<%:Back to Overview%>" onclick="location.href='<%=pcdata(self.redirect)%>'" /> <%
end
if display_cancel then
local label = pcdata(self.cancel or translate("Cancel"))
%><input class="cbi-button cbi-button-link" type="button" value="<%=label%>" onclick="cbi_submit(this, 'cbi.cancel')" /> <%
end
if display_skip then
%><input class="cbi-button cbi-button-neutral" type="button" value="<%:Skip%>" onclick="cbi_submit(this, 'cbi.skip')" /> <%
end
if display_submit then
local label = pcdata(self.submit or translate("Submit"))
%><input class="cbi-button cbi-button-save" type="submit" value="<%=label%>" /> <%
end
if display_reset then
local label = pcdata(self.reset or translate("Reset"))
%><input class="cbi-button cbi-button-reset" type="reset" value="<%=label%>" /> <%
end
%></div><%
end
%></form><%
end
%>
<script type="text/javascript">cbi_init();</script>

View File

@ -0,0 +1,143 @@
<br>
<ul class="cbi-tabmenu">
<% for k, v in pairs(self.tabs) do
local class = k == self.default_tab and "cbi-tab" or "cbi-tab-disabled"
local id = "tab.raid-tab." .. k
%>
<li id=<%=id%> class=<%=class%> >
<a onclick="this.blur(); return cbi_t_switch('raid-tab', '<%=k%>')" href=""><%=v%></a>
</li>
<% end %>
<!-- <li id="tab.raid-tab.mount_point" class="cbi-tab">
<a onclick="this.blur(); return cbi_t_switch('raid-tab', 'mount_point')" href=""><%:Mount Point%></a>
</li> -->
</ul>
<script type="text/javascript">
window.onload = function () {
//disk partition info
let p_colors = ["#c0c0ff", "#fbbd00", "#e97c30", "#a0e0a0", "#e0c0ff"]
let lines = document.querySelectorAll('[id^=cbi-disk-]')
lines.forEach((item) => {
let dev = item.id.match(/cbi-disk-(.*)/)[1]
if (dev == "table") { return }
XHR.get('/cgi-bin/luci/admin/system/diskman/get_disk_info/' + dev, null, (x, disk_info) => {
// handle disk info
item.childNodes.forEach((cell) => {
if (cell && cell.attributes) {
if (cell.getAttribute("data-name") == "sn" || cell.childNodes[1] && cell.childNodes[1].id.match(/sn/)) {
cell.innerText = disk_info.sn || "-"
} else if (cell.getAttribute("data-name") == "temp" || cell.childNodes[1] && cell.childNodes[1].id.match(/temp/)) {
cell.innerText = disk_info.temp || "-"
} else if (cell.getAttribute("data-name") == "p_table" || cell.childNodes[1] && cell.childNodes[1].id.match(/p_table/)) {
cell.innerText = disk_info.p_table || "-"
} else if (cell.getAttribute("data-name") == "sata_ver" || cell.childNodes[1] && cell.childNodes[1].id.match(/sata_ver/)) {
cell.innerText = disk_info.sata_ver || "-"
} else if (cell.getAttribute("data-name") == "health" || cell.childNodes[1] && cell.childNodes[1].id.match(/health/)) {
cell.innerText = disk_info.health || "-"
} else if (cell.getAttribute("data-name") == "status" || cell.childNodes[1] && cell.childNodes[1].id.match(/status/)) {
cell.innerText = disk_info.status || "-"
}
}
})
// handle partitons info
if (disk_info.partitions && disk_info.partitions.length > 0) {
let partitons_div
if (item.nodeName == "TR") {
partitons_div = '<tr width="100%" style="white-space:nowrap;"><td style="margin:0px; padding:0px; border:0px; white-space:nowrap;" colspan="15">'
} else if (item.nodeName == "DIV") {
partitons_div = '<div class="tr cbi-section-table-row cbi-rowstyle-1"><div style="white-space:nowrap; position:absolute; width:100%">'
}
let expand = 0
let need_expand = 0
disk_info.partitions.forEach((part) => {
let p = part.size / disk_info.size * 100
if (p <= 8) {
expand += 8
need_expand += p
part.part_percent = 8
}
})
let n = 0
disk_info.partitions.forEach((part) => {
let p = part.size / disk_info.size * 100
if (p > 8) {
part.part_percent = p * (100 - expand) / (100 - need_expand)
}
let part_percent = part.part_percent + '%'
let p_color = p_colors[n++]
if (n > 4) { n = 0 }
let inline_txt = (part.name != '-' && part.name || '') + ' ' + (part.fs != 'Free Space' && part.fs || '') + ' ' + part.size_formated + ' ' + (part.useage != '-' && part.useage || '')
let partiton_div = '<div title="' + inline_txt + '" style="display:inline-block; text-align:center;background-color:' + p_color + '; width:' + part_percent + '">' + inline_txt + '</div>'
partitons_div += partiton_div
})
if (item.nodeName == "TR") {
partitons_div += '</td></tr>'
} else if (item.nodeName == "DIV") {
partitons_div += '</div><div>&nbsp</div></div>'
}
item.insertAdjacentHTML('afterend', partitons_div);
}
})
})
//raid table
lines = document.querySelectorAll('[id^=cbi-_raid-]')
lines.forEach((item) => {
let dev = item.id.match(/cbi-_raid-(.*)/)[1]
if (dev == "table") { return }
console.log(dev)
XHR.get('/cgi-bin/luci/admin/system/diskman/get_disk_info/' + dev, null, (x, disk_info) => {
// handle raid info
item.childNodes.forEach((cell) => {
if (cell && cell.attributes) {
if (cell.getAttribute("data-name") == "p_table" || cell.childNodes[1] && cell.childNodes[1].id.match(/p_table/)) {
cell.innerText = disk_info.p_table || "-"
}
}
})
// handle partitons info
let partitons_div
if (item.nodeName == "TR") {
partitons_div = '<tr width="100%" style="white-space:nowrap;"><td style="margin:0px; padding:0px; border:0px; white-space:nowrap;" colspan="15">'
} else if (item.nodeName == "DIV") {
partitons_div = '<div class="tr cbi-section-table-row cbi-rowstyle-1"><div style="white-space:nowrap; position:absolute; width:100%">'
}
let n = 0
disk_info.partitions.forEach((part) => {
let part_percent = part.size / disk_info.size * 100 + '%'
let p_color = p_colors[n++]
if (n > 4) { n = 0 }
let inline_txt = (part.name != '-' && part.name || '') + ' ' + (part.fs != 'Free Space' && part.fs || '') + ' ' + part.size_formated + ' ' + (part.useage != '-' && part.useage || '')
let partiton_div = '<div title="' + inline_txt + '" style="display:inline-block; text-align:center;background-color:' + p_color + '; width:' + part_percent + '">' + inline_txt + '</div>'
partitons_div += partiton_div
})
if (item.nodeName == "TR") {
partitons_div += '</td></tr>'
} else if (item.nodeName == "DIV") {
partitons_div += '</div><div>&nbsp</div></div>'
}
item.insertAdjacentHTML('afterend', partitons_div);
})
})
// tab
<% for k, v in pairs(self.tabs) do
local display = k == self.default_tab and "block" or "none"
local tid = "cbi-"..k.."-table"
local sid = "cbi-"..k.."-section"
local cid = "container.raid-tab." .. k
%>
mount_point_table = document.getElementById("<%=tid%>") || document.getElementById("<%=sid%>") || null
if (mount_point_table) {
mount_point_table.setAttribute("style", "display: <%=display%>;")
mount_point_table.setAttribute("id", "<%=cid%>")
cbi_t_add('raid-tab', '<%=k%>')
}
<%end%>
// const mount_point_table = document.getElementById("cbi-mount_point-table")
// if (mount_point_table) {
// mount_point_table.setAttribute("style", "display: block;")
// mount_point_table.setAttribute("id", "container.raid-tab.mount_point")
// cbi_t_add('raid-tab', 'mount_point')
// }
}
</script>

View File

@ -0,0 +1,34 @@
<script type="text/javascript">//<![CDATA[
function show_detail(dev, e) {
e.preventDefault()
window.open('<%=luci.dispatcher.build_url("admin", "system", "diskman", "smartdetail")%>/' + dev,
'newwindow', 'height=480,width=800,top=100,left=200,toolbar=no,menubar=no,scrollbars=yes, resizable=no,location=no, status=no')
}
window.onload = function () {
// handle partition table
const btn_p_table = document.getElementById("cbid.table.1.p_table")
const btn_p_table_raw_index = btn_p_table.selectedIndex
const val_name = document.getElementById("cbi-table-1-path").innerText.split('/').pop()
btn_p_table.onchange = function () {
let btn_p_table_index = btn_p_table.selectedIndex
if (btn_p_table_index != btn_p_table_raw_index) {
if (confirm("Warnning !! \nTHIS WILL OVERWRITE EXISTING PARTITIONS!! \nModify the partition table?")) {
let p_table = btn_p_table.options[btn_p_table_index].value
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "diskman", "mk_p_table")%>', { dev: val_name, p_table: p_table }, (x, res) => {
if (res.code == 0) {
location.reload();
}
}
);
}
else {
}
}
}
// handle smartinfo
const url = location.href.split('/')
const dev = url[url.length - 1]
const btn_smart_detail = document.getElementById("cbi-table-1-health")
btn_smart_detail.children[0].onclick = show_detail.bind(this, dev)
}
</script>

View File

@ -0,0 +1,67 @@
<html>
<head>
<title>S.M.A.R.T detail of <%=dev%></title>
<link rel="stylesheet" type="text/css" media="screen" href="<%=media%>/cascade.css" />
<script type="text/javascript" src="<%=resource%>/xhr.js"></script>
<script type="text/javascript">//<![CDATA[
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "diskman", "smartattr", dev)%>', null,
function (x, st) {
var tb = document.getElementById('smart_attr_table');
if (st && tb) {
/* clear all rows */
while (tb.rows.length > 1)
tb.deleteRow(1);
for (var i = 0; i < st.length; i++) {
var tr = tb.insertRow(-1);
tr.className = 'cbi-section-table-row cbi-rowstyle-' + ((i % 2) + 1);
var td = null
tr.insertCell(-1).innerHTML = st[i].id;
tr.insertCell(-1).innerHTML = st[i].attrbute;
tr.insertCell(-1).innerHTML = st[i].flag;
tr.insertCell(-1).innerHTML = st[i].value;
tr.insertCell(-1).innerHTML = st[i].worst;
tr.insertCell(-1).innerHTML = st[i].thresh;
tr.insertCell(-1).innerHTML = st[i].type;
tr.insertCell(-1).innerHTML = st[i].updated;
tr.insertCell(-1).innerHTML = st[i].raw;
if ((st[i].id == '05' || st[i].id == 'C5') && st[i].raw != '0') {
tr.style.cssText = "background-color:red !important;";
}
}
if (tb.rows.length == 1) {
var tr = tb.insertRow(-1);
tr.className = 'cbi-section-table-row';
var td = tr.insertCell(-1);
td.colSpan = 4;
td.innerHTML = '<em><br /><%:No Attrbute to display.%></em>';
}
}
}
);
//]]></script>
</head>
<body>
<div id="maincontainer">
<fieldset class="cbi-section">
<legend><%:S.M.A.R.T Attrbutes%>: /dev/<%=dev%></legend>
<table class="cbi-section-table" id="smart_attr_table">
<tr class="cbi-section-table-titles">
<th class="cbi-section-table-cell"><%:ID%></th>
<th class="cbi-section-table-cell"><%:Attrbute%></th>
<th class="cbi-section-table-cell"><%:Flag%></th>
<th class="cbi-section-table-cell"><%:Value%></th>
<th class="cbi-section-table-cell"><%:Worst%></th>
<th class="cbi-section-table-cell"><%:Thresh%></th>
<th class="cbi-section-table-cell"><%:Type%></th>
<th class="cbi-section-table-cell"><%:Updated%></th>
<th class="cbi-section-table-cell"><%:Raw%></th>
</tr>
<tr class="cbi-section-table-row">
<td colspan="4"><em><br /><%:Collecting data...%></em></td>
</tr>
</table>
</fieldset>
</div>
</body>
</html>

View File

@ -0,0 +1,128 @@
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 z poziomu LuCI."
msgid "Rescan Disks"
msgstr "Reskan Dysków"
msgid "Disks"
msgstr "Dyski"
msgid "Path"
msgstr "Ścieżka"
msgid "Serial Number"
msgstr "Nr Seryjny"
msgid "Temp"
msgstr "Temperatura"
msgid "Partition Table"
msgstr "Tabela 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 "Nowy(a)"
msgid "Remove"
msgstr "Usuń"
msgid "Format"
msgstr "Format"
msgid "Start Sector"
msgstr "Sektor Początku"
msgid "End Sector"
msgstr "Sektor Końca"
msgid "Useage"
msgstr "Użycie"
msgid "Model"
msgstr "Model"
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 z poziomu 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 "Domyślnie 2048 ustawienie sektora, obsługiwane +rozmiar{b,k,m,g,t} w Sektorze Końcowym"

View File

@ -0,0 +1,128 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8\n"
msgid "DiskMan"
msgstr "磁盘管理"
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 "Useage"
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"