mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 15:52:52 +08:00
1737 lines
60 KiB
Bash
Executable File
1737 lines
60 KiB
Bash
Executable File
#!/bin/sh
|
|
# LuCI RPCD Backend for luci-app-diskbox
|
|
# Copyright (C) 2026 permails <https://github.com/permails/luci-app-diskbox>
|
|
# Licensed under the GNU General Public License v3.0
|
|
|
|
. /usr/share/libubox/jshn.sh
|
|
|
|
# --- Per-process caches -----------------------------------------------------
|
|
# rpcd runs every RPC in a fresh process, so a process-global cache is
|
|
# inherently per-request: built on first use, never spanning calls. We cache
|
|
# facts that are invariant within a single RPC (a device's fstype, its parent
|
|
# disk, whether it is external) so the listing loop and the guard chain do not
|
|
# each re-run blkid / get_parent_disk / is_external_device for the same device.
|
|
# A stale entry can only ever surface in a single in-flight call after a
|
|
# format, and the brick-critical guard decision is always made *before* any
|
|
# format - so the cache never changes a protection outcome.
|
|
# Entries are stored as |<key>=<value>|<key>=<value>... The "=" boundary keeps
|
|
# a key from colliding with a value that happens to name the same device.
|
|
_FS_CACHE="" # "|<dev>=<fstype>|<dev>=<fstype>..."
|
|
_PD_CACHE="" # "|<bname>=<parent>|<bname>=<parent>..."
|
|
_EXT_CACHE="" # "|<bname>=0|<bname>=1..."
|
|
|
|
# dev_fs_type <dev>: device filesystem type, cached per process (avoids a
|
|
# redundant blkid when both the listing loop and the guard chain probe it).
|
|
dev_fs_type() {
|
|
local d="$1"
|
|
d=${d#/dev/}
|
|
local found="" val=""
|
|
case "$_FS_CACHE" in
|
|
*"|$d="*) found=1; val=${_FS_CACHE#*|$d=}; val=${val%%|*} ;;
|
|
esac
|
|
[ -n "$found" ] && { printf '%s' "$val"; return; }
|
|
val=$(blkid -o value -s TYPE "/dev/$d" 2>/dev/null | head -n1)
|
|
_FS_CACHE="${_FS_CACHE}|${d}=${val}"
|
|
printf '%s' "$val"
|
|
}
|
|
|
|
# Helper: format bytes to human readable string (e.g. 1.23 GB)
|
|
format_bytes() {
|
|
local bytes="$1"
|
|
[ -z "$bytes" ] && echo "-" && return
|
|
awk -v b="$bytes" 'BEGIN {
|
|
split("B KB MB GB TB PB", u, " ");
|
|
i = 1;
|
|
while (b >= 1024 && i < 6) {
|
|
b = b / 1024;
|
|
i++;
|
|
}
|
|
if (i == 1)
|
|
printf "%.0f %s", b, u[i];
|
|
else
|
|
printf "%.2f %s", b, u[i];
|
|
}'
|
|
}
|
|
|
|
# Helper: check if device or partition is in use (mounted, swap, raid)
|
|
check_inuse() {
|
|
local dev="$1"
|
|
local bname=$(basename "$dev")
|
|
if grep -q "^/dev/${bname}[[:space:]]" /proc/mounts 2>/dev/null; then
|
|
echo "1"
|
|
return
|
|
fi
|
|
if grep -q "^/dev/${bname}[[:space:]]" /proc/swaps 2>/dev/null; then
|
|
echo "1"
|
|
return
|
|
fi
|
|
if [ -f /proc/mdstat ] && grep -q "${bname}" /proc/mdstat 2>/dev/null; then
|
|
echo "1"
|
|
return
|
|
fi
|
|
echo "0"
|
|
}
|
|
|
|
# Helper: get mount point for partition
|
|
get_mount_point() {
|
|
local dev="$1"
|
|
local bname=$(basename "$dev")
|
|
local mnt=$(awk -v d="/dev/$bname" '$1==d {printf "%s ", $2}' /proc/mounts 2>/dev/null)
|
|
if [ -n "$mnt" ]; then
|
|
echo "$mnt" | sed 's/ $//'
|
|
return
|
|
fi
|
|
if grep -q "^/dev/${bname}[[:space:]]" /proc/swaps 2>/dev/null; then
|
|
echo "swap"
|
|
return
|
|
fi
|
|
if [ -f /proc/mdstat ]; then
|
|
local md=$(awk -v d="$bname" '$0 ~ d {gsub(/[[\]:]/,""); print $1}' /proc/mdstat 2>/dev/null | head -n1)
|
|
if [ -n "$md" ]; then
|
|
echo "Raid Member: $md"
|
|
return
|
|
fi
|
|
fi
|
|
echo "-"
|
|
}
|
|
|
|
# Helper: run smartctl with fallback device types
|
|
run_smartctl() {
|
|
local dev="$1"
|
|
local opts="$2"
|
|
which smartctl >/dev/null 2>&1 || return
|
|
|
|
local devtype=""
|
|
case "$dev" in
|
|
nvme*) devtype="nvme" ;;
|
|
esac
|
|
|
|
if [ -n "$devtype" ]; then
|
|
smartctl -d "$devtype" $opts "/dev/$dev" 2>/dev/null && return
|
|
fi
|
|
|
|
for t in "" "sat" "scsi" "nvme"; do
|
|
local res=""
|
|
if [ -z "$t" ]; then
|
|
res=$(smartctl $opts "/dev/$dev" 2>&1)
|
|
else
|
|
res=$(smartctl -d "$t" $opts "/dev/$dev" 2>&1)
|
|
fi
|
|
if echo "$res" | grep -qE "START OF SMART DATA SECTION|START OF READ SMART DATA SECTION|NVMe Version:|SMART Health Status:|Device is in [A-Z]+ mode"; then
|
|
echo "$res"
|
|
return
|
|
fi
|
|
done
|
|
smartctl $opts "/dev/$dev" 2>&1
|
|
}
|
|
|
|
# Methods
|
|
# ===== BEGIN SYSTEM-DISK GUARD v5 (hotpatch 2026-09-03) =====
|
|
# v5.2: multi-signal system-partition classification (cross-SoC, FIT-robust)
|
|
# hard (code:2) : live base system (mounted at / or /rom, squashfs) and
|
|
# small firmware-label partitions on the root disk
|
|
# soft (code:3) : mounted or filesystem-bearing partitions on internal
|
|
# disks (incl. /overlay = factory-reset level warning);
|
|
# unmounted small partitions on the root disk (A/B slots)
|
|
# data space : ANY partition > 4GiB is user data, never "system"
|
|
# (128G eMMC: 2G system + 126G data area, data is often
|
|
# rootfs_data mounted at /overlay)
|
|
# system disk : disk holding the root= partition (fallback: block device
|
|
# backing / or /overlay*); mktable hard-blocked, create
|
|
# allowed (unallocated space only), remove per-partition
|
|
|
|
# is_external_device: returns 0 if external (USB), 1 if internal.
|
|
# USB is detected via the sysfs device link, so x86 internal SATA (sd*)
|
|
# and SoC internal NVMe are correctly treated as internal.
|
|
is_external_device() {
|
|
local bname=$(basename "$1")
|
|
# Memoize per process: a disk's bus type is invariant within a call, and
|
|
# every partition on it re-asks the same question from the guard chain.
|
|
local found="" ext=""
|
|
case "$_EXT_CACHE" in
|
|
*"|$bname="*) found=1; ext=${_EXT_CACHE#*|$bname=}; ext=${ext%%|*} ;;
|
|
esac
|
|
if [ -n "$found" ]; then
|
|
[ "$ext" = "1" ] && return 0
|
|
return 1
|
|
fi
|
|
local link=$(readlink -f "/sys/block/$bname/device" 2>/dev/null)
|
|
case "$link" in
|
|
*usb*) ext=1 ;;
|
|
*) ext=0 ;;
|
|
esac
|
|
_EXT_CACHE="${_EXT_CACHE}|${bname}=${ext}"
|
|
[ "$ext" = "1" ] && return 0
|
|
return 1
|
|
}
|
|
|
|
# get_parent_disk: whole-disk name for a partition; the name itself for a disk.
|
|
# A partition's sysfs dir contains a file named ".." holding the disk name -
|
|
# works whether or not partitions are listed in /sys/block (MTK FIT images
|
|
# list them only under the disk dir).
|
|
get_parent_disk() {
|
|
local bname=$(basename "$1")
|
|
# Memoize per process: a partition's parent disk is invariant within a call,
|
|
# and the guard chain asks for the same parent up to three times per part.
|
|
local found="" val=""
|
|
case "$_PD_CACHE" in
|
|
*"|$bname="*) found=1; val=${_PD_CACHE#*|$bname=}; val=${val%%|*} ;;
|
|
esac
|
|
[ -n "$found" ] && { printf '%s' "$val"; return; }
|
|
if [ -f "/sys/block/$bname/.." ]; then
|
|
val=$(head -n1 "/sys/block/$bname/.." 2>/dev/null)
|
|
_PD_CACHE="${_PD_CACHE}|${bname}=${val}"
|
|
printf '%s' "$val"
|
|
return
|
|
fi
|
|
[ -e "/sys/block/$bname" ] && { _PD_CACHE="${_PD_CACHE}|${bname}=${bname}"; printf '%s' "$bname"; return; }
|
|
for d in /sys/block/*/; do
|
|
if [ -e "${d%/}/$bname" ]; then
|
|
val=$(basename "$d")
|
|
_PD_CACHE="${_PD_CACHE}|${bname}=${val}"
|
|
printf '%s' "$val"
|
|
return
|
|
fi
|
|
done
|
|
val=$(lsblk -no PKNAME "/dev/$bname" 2>/dev/null | head -n1)
|
|
_PD_CACHE="${_PD_CACHE}|${bname}=${val}"
|
|
printf '%s' "$val"
|
|
}
|
|
|
|
# get_root_disk: the system disk. Resolution order:
|
|
# 1) root= cmdline device, if it is a real sd/mmcblk/nvme/vd disk
|
|
# 2) block device backing / or /overlay*, if a real disk
|
|
# 3) FIT fallback: the internal (non-usb) disk. On MTK FIT images
|
|
# root=/dev/fit0 is a virtual platform/fitblk device (not a block
|
|
# partition), so fall back to the physical internal disk it lives on.
|
|
get_root_disk() {
|
|
# Invariant per process (one rpcd invocation = one process); cache the answer
|
|
# so per-partition callers (partition_is_boot / disk_is_root_disk) don't
|
|
# re-derive it every time.
|
|
[ -n "${_ROOT_DISK+x}" ] && { printf '%s' "$_ROOT_DISK"; return; }
|
|
_ROOT_DISK=$(get_root_disk_impl)
|
|
printf '%s' "$_ROOT_DISK"
|
|
}
|
|
|
|
get_root_disk_impl() {
|
|
local rootdev=$(grep -o 'root=/dev/[^ ]*' /proc/cmdline 2>/dev/null | sed 's|root=/dev/||' | head -n1)
|
|
if [ -n "$rootdev" ]; then
|
|
local rd=$(get_parent_disk "$rootdev")
|
|
case "$rd" in sd*|mmcblk*|nvme*|vd*) echo "$rd"; return ;; esac
|
|
fi
|
|
local dev=$(awk '$2=="/" || index($2, "/overlay")==1 { if (index($1, "/dev/")==1) { s=$1; sub(/^\/dev\//, "", s); print s; exit } }' /proc/mounts 2>/dev/null)
|
|
if [ -n "$dev" ]; then
|
|
local rd=$(get_parent_disk "$dev")
|
|
case "$rd" in sd*|mmcblk*|nvme*|vd*) echo "$rd"; return ;; esac
|
|
fi
|
|
# FIT fallback: the internal (non-usb) disk
|
|
for d in /sys/block/sd* /sys/block/mmcblk* /sys/block/nvme* /sys/block/vd*; do
|
|
[ -e "$d/device" ] || continue
|
|
local bn=$(basename "$d")
|
|
case "$bn" in *boot*) continue ;; esac
|
|
is_external_device "$bn" || { echo "$bn"; return; }
|
|
done
|
|
}
|
|
|
|
# is_live_root_mount: returns 0 if the device is the live base system,
|
|
# i.e. mounted at / (real-fs root) or /rom (squashfs lower of overlayfs).
|
|
# Wiping these bricks the device - size-independent, checked first.
|
|
is_live_root_mount() {
|
|
local bname=$(basename "$1")
|
|
awk -v d="/dev/$bname" '$1==d && ($2=="/" || $2=="/rom") { found=1; exit } END { exit (found ? 0 : 1) }' /proc/mounts 2>/dev/null
|
|
}
|
|
|
|
# is_overlay_mount: returns 0 if the device is mounted at /overlay*
|
|
# (rootfs_data: user data + config; wiping = factory reset, not brick)
|
|
is_overlay_mount() {
|
|
local bname=$(basename "$1")
|
|
awk -v d="/dev/$bname" '$1==d && index($2, "/overlay")==1 { found=1; exit } END { exit (found ? 0 : 1) }' /proc/mounts 2>/dev/null
|
|
}
|
|
|
|
# part_large_enough: returns 0 if the block device exceeds 4 GiB (awk math, 32-bit safe)
|
|
# Size file: top-level /sys/block/<part>/size, or under the parent disk dir
|
|
# (/sys/block/<disk>/<part>/size) on layouts where partitions are not
|
|
# listed in /sys/block (e.g. MTK FIT eMMC). sysfs size is always 512-byte sectors.
|
|
part_large_enough() {
|
|
local bname=$(basename "$1")
|
|
local parent=$(get_parent_disk "$bname")
|
|
local szpath="/sys/block/$bname/size"
|
|
[ "$parent" != "$bname" ] && szpath="/sys/block/$parent/$bname/size"
|
|
[ -e "$szpath" ] || szpath="/sys/block/$bname/size"
|
|
local sec=$(cat "$szpath" 2>/dev/null || echo 0)
|
|
case "$sec" in ''|*[!0-9]*) sec=0 ;; esac
|
|
awk -v s="$sec" 'BEGIN { exit ((s*512 > 4294967296) ? 0 : 1) }'
|
|
}
|
|
|
|
# disk_is_root_disk: returns 0 if this is the system (root) disk
|
|
disk_is_root_disk() {
|
|
local bname=$(basename "$1")
|
|
local rootdisk=$(get_root_disk)
|
|
[ -n "$rootdisk" ] && [ "$bname" = "$rootdisk" ]
|
|
}
|
|
|
|
# partition_is_boot: returns 0 if a system/firmware partition (hard-protected)
|
|
# v5.2 ordering: brick-level signals first (size-independent), then the
|
|
# 4GiB data-space rule, then small-on-system-disk = firmware.
|
|
# Robust to absent GPT labels and MTK FIT images (root=/dev/fit0):
|
|
# on a router the small partitions on the internal system disk are
|
|
# firmware (kernel/rootfs/factory/A-B slots); the large one is user data.
|
|
partition_is_boot() {
|
|
# Memoize per device: guard state is invariant during a single RPC (rpcd
|
|
# runs each call in a fresh process, so the cache never spans requests).
|
|
# partition_is_data re-enters this, so without caching the whole guard
|
|
# chain (get_parent_disk / is_external_device / is_live_root_mount /
|
|
# blkid / part_large_enough / get_root_disk) ran twice per partition.
|
|
local _bk
|
|
_bk=$(basename "$1")
|
|
case "$_BOOT_CACHE" in
|
|
*"|$_bk=1|"*) return 0 ;;
|
|
*"|$_bk=0|"*) return 1 ;;
|
|
esac
|
|
if _partition_is_boot_impl "$@"; then
|
|
_BOOT_CACHE="$_BOOT_CACHE|$_bk=1|"
|
|
return 0
|
|
fi
|
|
_BOOT_CACHE="$_BOOT_CACHE|$_bk=0|"
|
|
return 1
|
|
}
|
|
|
|
_partition_is_boot_impl() {
|
|
local bname=$(basename "$1")
|
|
local parent=$(get_parent_disk "$bname")
|
|
[ -n "$parent" ] || return 1
|
|
# (1) external disks never hold system partitions
|
|
is_external_device "$parent" && return 1
|
|
# (2) brick-level, size-independent: the live base system
|
|
is_live_root_mount "$bname" && return 0
|
|
# (2b) squashfs is always read-only firmware
|
|
local fstype=$(dev_fs_type "$bname")
|
|
[ "$fstype" = "squashfs" ] && return 0
|
|
# (3) a partition above 4GiB is user data space, never firmware
|
|
# (e.g. 128G eMMC: 2G system + 126G data area; the data area may be
|
|
# mounted anywhere - /overlay, /mnt/... - still user data, not system)
|
|
part_large_enough "$bname" && return 1
|
|
# (4) small partition on the system disk = firmware (label-independent)
|
|
local rootdisk=$(get_root_disk)
|
|
[ -n "$rootdisk" ] && [ "$parent" = "$rootdisk" ] && return 0
|
|
return 1
|
|
}
|
|
|
|
# partition_is_data: returns 0 if a mounted non-firmware partition on an internal disk
|
|
partition_is_data() {
|
|
# Memoize per device (same reasoning as partition_is_boot above).
|
|
local _dk
|
|
_dk=$(basename "$1")
|
|
case "$_DATA_CACHE" in
|
|
*"|$_dk=1|"*) return 0 ;;
|
|
*"|$_dk=0|"*) return 1 ;;
|
|
esac
|
|
if _partition_is_data_impl "$@"; then
|
|
_DATA_CACHE="$_DATA_CACHE|$_dk=1|"
|
|
return 0
|
|
fi
|
|
_DATA_CACHE="$_DATA_CACHE|$_dk=0|"
|
|
return 1
|
|
}
|
|
|
|
_partition_is_data_impl() {
|
|
local bname=$(basename "$1")
|
|
local pm=$(get_mount_point "$bname")
|
|
[ "$pm" = "-" ] && return 1
|
|
local parent=$(get_parent_disk "$bname")
|
|
[ -n "$parent" ] || return 1
|
|
is_external_device "$parent" && return 1
|
|
partition_is_boot "$1" && return 1
|
|
return 0
|
|
}
|
|
|
|
# partition_needs_force: returns 0 if a format requires force=1
|
|
partition_needs_force() {
|
|
local bname=$(basename "$1")
|
|
local parent=$(get_parent_disk "$bname")
|
|
[ -n "$parent" ] || return 1
|
|
is_external_device "$parent" && return 1
|
|
# mounted (in use) or existing filesystem - confirm before wiping
|
|
[ "$(get_mount_point "$bname")" != "-" ] && return 0
|
|
local fstype=$(dev_fs_type "$bname")
|
|
[ -n "$fstype" ] && return 0
|
|
# safety net: unmounted, no-fs, small partition on the root disk
|
|
# (possible A/B inactive firmware slot) - require explicit force
|
|
local rootdisk=$(get_root_disk)
|
|
[ -n "$rootdisk" ] && [ "$parent" = "$rootdisk" ] && ! part_large_enough "$bname" && return 0
|
|
return 1
|
|
}
|
|
|
|
# require_not_boot: hard block, no force override
|
|
require_not_boot() {
|
|
local what="$1"
|
|
logger -t diskbox "BLOCKED(boot): $what"
|
|
echo '{"code": 2, "error": "Blocked: this is a boot/firmware partition."}'
|
|
}
|
|
|
|
# require_not_system_disk: hard block, no force override
|
|
require_not_system_disk() {
|
|
local what="$1"
|
|
logger -t diskbox "BLOCKED(system-disk): $what"
|
|
echo '{"code": 2, "error": "Blocked: this operation targets the system disk (it holds the running system)."}'
|
|
}
|
|
|
|
# rescan_partitions: refresh the kernel's partition list after a GPT change.
|
|
# On embedded eMMC (MTK FIT images) parted does not reliably trigger a kernel
|
|
# rescan: the kernel keeps a stale partition list, so removed partitions'
|
|
# device nodes survive and new partitions never appear (UI and kernel
|
|
# disagree). Never rescan while a partition of this disk is mounted - a
|
|
# rescan can drop kernel partitions, which would corrupt live I/O.
|
|
rescan_partitions() {
|
|
local dev="$1"
|
|
local mounted=$(awk -v d="/dev/$dev" '$1 == d || index($1, d "p") == 1 { print $2; exit }' /proc/mounts 2>/dev/null)
|
|
if [ -n "$mounted" ]; then
|
|
logger -t diskbox "WARN(rescan): skipping /dev/$dev - $mounted is mounted"
|
|
return 1
|
|
fi
|
|
if which partprobe >/dev/null 2>&1; then
|
|
partprobe "/dev/$dev" >/dev/null 2>&1
|
|
logger -t diskbox "rescan: partprobe /dev/$dev rc=$?"
|
|
elif which blockdev >/dev/null 2>&1; then
|
|
blockdev --rereadpt "/dev/$dev" >/dev/null 2>&1
|
|
logger -t diskbox "rescan: blockdev --rereadpt /dev/$dev rc=$?"
|
|
fi
|
|
return 0
|
|
}
|
|
# ===== END SYSTEM-DISK GUARD v5 =====
|
|
|
|
action_list_devices() {
|
|
json_init
|
|
json_add_object "devices"
|
|
|
|
for devpath in /sys/block/sd* /sys/block/mmcblk* /sys/block/nvme* /sys/block/vd* /sys/block/sata*; do
|
|
[ -e "$devpath" ] || continue
|
|
local bname=$(basename "$devpath")
|
|
case "$bname" in
|
|
*boot*|*rpmb*) continue ;;
|
|
esac
|
|
|
|
local size_sec=$(cat "$devpath/size" 2>/dev/null || echo 0)
|
|
local ss=$(cat "$devpath/queue/logical_block_size" 2>/dev/null || echo 512)
|
|
local size_bytes=$(awk -v s="$size_sec" -v b="$ss" 'BEGIN { printf "%.0f", s*b }')
|
|
local size_str=$(format_bytes "$size_bytes")
|
|
local model=$(cat "$devpath/device/model" 2>/dev/null | sed 's/^[ \t]*//;s/[ \t]*$//')
|
|
local sn=$(cat "$devpath/device/serial" 2>/dev/null | sed 's/^[ \t]*//;s/[ \t]*$//')
|
|
local temp="N/A"
|
|
local health="Normal"
|
|
local p_table="-"
|
|
local sata_ver="-"
|
|
local inuse=$(check_inuse "$bname")
|
|
local dmount=$(get_mount_point "$bname")
|
|
local dfstype=$(dev_fs_type "$bname")
|
|
|
|
# eMMC enhancement
|
|
if echo "$bname" | grep -q "^mmcblk"; then
|
|
local emmc_name=$(cat "$devpath/device/name" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
local emmc_type=$(cat "$devpath/device/type" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
if [ -n "$emmc_name" ]; then
|
|
model="${emmc_type:-eMMC} $emmc_name"
|
|
else
|
|
model="eMMC Flash Drive"
|
|
fi
|
|
|
|
if [ -f "/sys/class/thermal/thermal_zone0/temp" ]; then
|
|
local tz=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null)
|
|
if [ -n "$tz" ] && [ "$tz" -gt 0 ] 2>/dev/null; then
|
|
temp="$(awk -v t="$tz" 'BEGIN {printf "%.1f °C", t/1000}')"
|
|
fi
|
|
fi
|
|
|
|
local life=$(cat "$devpath/device/life_time" 2>/dev/null)
|
|
local eol=$(cat "$devpath/device/pre_eol_info" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
if [ "$eol" = "0x02" ] || [ "$eol" = "2" ]; then
|
|
health="Warning (Consuming)"
|
|
elif [ "$eol" = "0x03" ] || [ "$eol" = "3" ]; then
|
|
health="Urgent (Replace)"
|
|
fi
|
|
if [ -n "$life" ]; then
|
|
local ta=$(echo "$life" | awk '{print $1}')
|
|
if [ -n "$ta" ]; then
|
|
local a_pct=$((ta * 10))
|
|
[ "$a_pct" -gt 100 ] && a_pct=100
|
|
health="$health (Used A:${a_pct}%)"
|
|
fi
|
|
fi
|
|
else
|
|
if which smartctl >/dev/null 2>&1; then
|
|
local s_out=$(run_smartctl "$bname" "-H -A -i -n standby -f brief")
|
|
local s_temp=$(echo "$s_out" | grep -iE "(Temperature_Celsius|Airflow_Temperature_Cel|Temperature)" | awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+$/ && $i < 120 && $i > 0) val=$i} END {if(val) print val}')
|
|
[ -n "$s_temp" ] && temp="${s_temp} °C"
|
|
local s_health=$(echo "$s_out" | grep -iE "(overall-health|SMART Health Status)" | head -n1 | awk -F': ' '{print $2}')
|
|
[ -n "$s_health" ] && health="$s_health"
|
|
local s_sn=$(echo "$s_out" | grep -i "^Serial Number:" | head -n1 | awk -F': ' '{print $2}' | sed 's/^[ \t]*//;s/[ \t]*$//')
|
|
[ -n "$s_sn" ] && sn="$s_sn"
|
|
local s_sata=$(echo "$s_out" | grep -i "^SATA Version is:" | head -n1 | awk -F': ' '{print $2}')
|
|
[ -n "$s_sata" ] && sata_ver="$s_sata"
|
|
local s_model=$(echo "$s_out" | grep -iE "^(Device Model|Model Number):" | head -n1 | awk -F': ' '{print $2}' | sed 's/^[ \t]*//;s/[ \t]*$//')
|
|
[ -n "$s_model" ] && model="$s_model"
|
|
fi
|
|
fi
|
|
|
|
# Partition table type
|
|
if which parted >/dev/null 2>&1; then
|
|
local pt=$(parted "/dev/$bname" print 2>/dev/null | grep -i "^Partition Table:" | awk -F': ' '{print $2}' | tr 'a-z' 'A-Z')
|
|
[ "$pt" = "MSDOS" ] && pt="MBR"
|
|
[ -n "$pt" ] && p_table="$pt"
|
|
fi
|
|
|
|
json_add_object "$bname"
|
|
json_add_string "name" "$bname"
|
|
json_add_string "path" "/dev/$bname"
|
|
json_add_string "model" "${model:--}"
|
|
json_add_string "sn" "${sn:--}"
|
|
json_add_int "size" "$size_bytes"
|
|
json_add_string "size_formated" "$size_str"
|
|
json_add_string "temp" "$temp"
|
|
json_add_string "health_status" "$health"
|
|
json_add_string "p_table" "$p_table"
|
|
json_add_string "sata_ver" "$sata_ver"
|
|
json_add_boolean "inuse" "$inuse"
|
|
local ds=0
|
|
if disk_is_root_disk "$bname" >/dev/null 2>&1; then ds=1; fi
|
|
json_add_boolean "is_system" "$ds"
|
|
json_add_string "mount_point" "$dmount"
|
|
json_add_string "fstype" "${dfstype:-}"
|
|
|
|
# Partitions
|
|
json_add_array "partitions"
|
|
for partpath in /sys/block/$bname/$bname*; do
|
|
[ -e "$partpath" ] || continue
|
|
local pname=$(basename "$partpath")
|
|
local psize_sec=$(cat "$partpath/size" 2>/dev/null || echo 0)
|
|
local psize_bytes=$(awk -v s="$psize_sec" -v b="$ss" 'BEGIN { printf "%.0f", s*b }')
|
|
local psize_str=$(format_bytes "$psize_bytes")
|
|
local pmount=$(get_mount_point "$pname")
|
|
local pinuse=0
|
|
[ "$pmount" != "-" ] && pinuse=1
|
|
local pfstype=$(dev_fs_type "$pname")
|
|
|
|
json_add_object
|
|
json_add_string "name" "$pname"
|
|
json_add_string "path" "/dev/$pname"
|
|
json_add_int "size" "$psize_bytes"
|
|
json_add_string "size_formated" "$psize_str"
|
|
json_add_string "mount_point" "$pmount"
|
|
json_add_string "fstype" "${pfstype:-}"
|
|
json_add_boolean "inuse" "$pinuse"
|
|
local pb=0 pd=0
|
|
if partition_is_boot "$pname" >/dev/null 2>&1; then pb=1; fi
|
|
if partition_is_data "$pname" >/dev/null 2>&1; then pd=1; fi
|
|
json_add_boolean "is_boot" "$pb"
|
|
json_add_boolean "is_data" "$pd"
|
|
json_close_object
|
|
done
|
|
json_close_array
|
|
|
|
json_close_object
|
|
done
|
|
|
|
json_close_object
|
|
json_dump
|
|
}
|
|
|
|
action_get_disk_info() {
|
|
read -r input
|
|
local dev=$(echo "$input" | jsonfilter -e '@.device' 2>/dev/null | sed 's|^/dev/||')
|
|
if [ -z "$dev" ]; then
|
|
echo '{"error": "Device not specified"}'
|
|
return
|
|
fi
|
|
if [ ! -e "/sys/block/$dev" ] && [ ! -e "/sys/class/block/$dev" ]; then
|
|
echo '{"error": "Device not found"}'
|
|
return
|
|
fi
|
|
|
|
local devpath="/sys/class/block/$dev"
|
|
local size_sec=$(cat "$devpath/size" 2>/dev/null || echo 0)
|
|
local logic_sec=$(cat "$devpath/queue/logical_block_size" 2>/dev/null || echo 512)
|
|
local phy_sec=$(cat "$devpath/queue/physical_block_size" 2>/dev/null || echo "$logic_sec")
|
|
local size_bytes=$(awk -v s="$size_sec" -v b="$logic_sec" 'BEGIN { printf "%.0f", s*b }')
|
|
local size_str=$(format_bytes "$size_bytes")
|
|
local model=$(cat "$devpath/device/model" 2>/dev/null | sed 's/^[ \t]*//;s/[ \t]*$//')
|
|
local sn=$(cat "$devpath/device/serial" 2>/dev/null | sed 's/^[ \t]*//;s/[ \t]*$//')
|
|
local p_table="-"
|
|
local temp="N/A"
|
|
local health="Normal"
|
|
local sata_ver="-"
|
|
local rota_rate="-"
|
|
local status="ACTIVE"
|
|
|
|
if which smartctl >/dev/null 2>&1; then
|
|
local s_out=$(run_smartctl "$dev" "-H -A -i -n standby -f brief")
|
|
local s_temp=$(echo "$s_out" | grep -iE "(Temperature_Celsius|Airflow_Temperature_Cel|Temperature)" | awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+$/ && $i < 120 && $i > 0) val=$i} END {if(val) print val}')
|
|
[ -n "$s_temp" ] && temp="${s_temp} °C"
|
|
local s_health=$(echo "$s_out" | grep -iE "(overall-health|SMART Health Status)" | head -n1 | awk -F': ' '{print $2}')
|
|
[ -n "$s_health" ] && health="$s_health"
|
|
local s_sn=$(echo "$s_out" | grep -i "^Serial Number:" | head -n1 | awk -F': ' '{print $2}' | sed 's/^[ \t]*//;s/[ \t]*$//')
|
|
[ -n "$s_sn" ] && sn="$s_sn"
|
|
local s_sata=$(echo "$s_out" | grep -i "^SATA Version is:" | head -n1 | awk -F': ' '{print $2}')
|
|
[ -n "$s_sata" ] && sata_ver="$s_sata"
|
|
local s_model=$(echo "$s_out" | grep -iE "^(Device Model|Model Number):" | head -n1 | awk -F': ' '{print $2}' | sed 's/^[ \t]*//;s/[ \t]*$//')
|
|
[ -n "$s_model" ] && model="$s_model"
|
|
local s_rota=$(echo "$s_out" | grep -i "^Rotation Rate:" | head -n1 | awk -F': ' '{print $2}')
|
|
[ -n "$s_rota" ] && rota_rate="$s_rota"
|
|
local s_power=$(echo "$s_out" | grep -i "^Device is in" | head -n1 | awk '{print $4}')
|
|
[ -n "$s_power" ] && status="$s_power"
|
|
fi
|
|
|
|
if echo "$dev" | grep -q "^mmcblk"; then
|
|
local emmc_name=$(cat "$devpath/device/name" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
local emmc_type=$(cat "$devpath/device/type" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
model="${emmc_type:-eMMC} ${emmc_name:-Flash Drive}"
|
|
if [ -f "/sys/class/thermal/thermal_zone0/temp" ]; then
|
|
local tz=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null)
|
|
[ -n "$tz" ] && [ "$tz" -gt 0 ] 2>/dev/null && temp="$(awk -v t="$tz" 'BEGIN {printf "%.1f °C", t/1000}')"
|
|
fi
|
|
fi
|
|
|
|
json_init
|
|
json_add_string "name" "$dev"
|
|
json_add_string "path" "/dev/$dev"
|
|
json_add_string "model" "${model:--}"
|
|
json_add_string "sn" "${sn:--}"
|
|
json_add_int "size" "$size_bytes"
|
|
json_add_string "size_formated" "$size_str"
|
|
json_add_int "logic_sec" "$logic_sec"
|
|
json_add_int "phy_sec" "$phy_sec"
|
|
json_add_string "sec_size" "${logic_sec}/${phy_sec}"
|
|
json_add_string "temp" "$temp"
|
|
json_add_string "health" "$health"
|
|
json_add_string "health_status" "$health"
|
|
json_add_string "status" "$status"
|
|
json_add_string "sata_ver" "$sata_ver"
|
|
json_add_string "rota_rate" "$rota_rate"
|
|
json_add_string "type" "$dev"
|
|
|
|
# Parted parse
|
|
json_add_array "partitions"
|
|
if which parted >/dev/null 2>&1; then
|
|
local parted_out=$(parted -s -m "/dev/$dev" unit s print free 2>/dev/null)
|
|
local pt=$(echo "$parted_out" | grep "^/dev/$dev:" | awk -F: '{print $4}' | tr 'a-z' 'A-Z')
|
|
[ "$pt" = "MSDOS" ] && pt="MBR"
|
|
[ -n "$pt" ] && p_table="$pt"
|
|
|
|
# Kernel (sysfs) partition list, as "<name> <start> <end>" lines.
|
|
# Detects GPT/kernel desync: a region the GPT reports as "free" may
|
|
# still hold a live kernel partition (removed from the GPT but the
|
|
# kernel never rescanned - common on eMMC). Such a region must be
|
|
# presented as a real partition with its true mount/filesystem
|
|
# state, not as unallocated space the user could create over.
|
|
local kernel_parts=""
|
|
for kdev in "/sys/block/$dev/$dev"*; do
|
|
[ -d "$kdev" ] || continue
|
|
local kn=$(basename "$kdev")
|
|
[ "$kn" = "$dev" ] && continue
|
|
[ -e "$kdev/start" ] || continue
|
|
local kst=$(cat "$kdev/start" 2>/dev/null)
|
|
local ksz=$(cat "$kdev/size" 2>/dev/null)
|
|
case "$kst$ksz" in ''|*[!0-9]*) continue ;; esac
|
|
[ "$ksz" -gt 0 ] || continue
|
|
kernel_parts="${kernel_parts}${kn} ${kst} $(awk -v a="$kst" -v b="$ksz" 'BEGIN { printf "%.0f", a+b-1 }')
|
|
"
|
|
done
|
|
|
|
local old_ifs="$IFS"
|
|
IFS='
|
|
'
|
|
for line in $(echo "$parted_out" | grep "^[0-9-]*:"); do
|
|
IFS="$old_ifs"
|
|
# Split the machine-format row (num:start:end:size:fs:flags) with pure
|
|
# parameter expansion - no forked cut/sed/tr (was ~8 forks/row x N rows).
|
|
local _r
|
|
local pnum=${line%%:*}
|
|
_r=${line#*:}
|
|
local pstart=${_r%%:*}
|
|
pstart=${pstart%s}
|
|
_r=${_r#*:}
|
|
local pend=${_r%%:*}
|
|
pend=${pend%s}
|
|
_r=${_r#*:}
|
|
local psize=${_r%%:*}
|
|
psize=${psize%s}
|
|
_r=${_r#*:}
|
|
local pfs=${_r%%:*}
|
|
pfs=${pfs//;/}
|
|
local part_bytes=$(awk -v s="$psize" -v b="$logic_sec" 'BEGIN { printf "%.0f", s*b }')
|
|
local part_str=$(format_bytes "$part_bytes")
|
|
local part_name="-"
|
|
local part_mount="-"
|
|
local part_used=0
|
|
local part_free=0
|
|
local part_usage="-"
|
|
local part_type="primary"
|
|
|
|
if [ "$pfs" = "free" ]; then
|
|
# GPT says "free" here, but the kernel may still hold a live
|
|
# partition covering this region (GPT/kernel desync). If so,
|
|
# emit it as a real partition with its true state and skip the
|
|
# "free space" row so the user cannot create over live data.
|
|
local kmatch=$(printf '%s' "$kernel_parts" | awk -v fs="$pstart" -v fe="$pend" 'NF==3 && $2 >= fs+0 && $3 <= fe+0 { print; exit }')
|
|
if [ -n "$kmatch" ]; then
|
|
local kpn=$(echo "$kmatch" | awk '{print $1}')
|
|
local kps=$(echo "$kmatch" | awk '{print $2}')
|
|
local kpe=$(echo "$kmatch" | awk '{print $3}')
|
|
local kno="${kpn#$dev}"
|
|
kno="${kno#p}"
|
|
case "$kno" in ''|*[!0-9]*) kno=0 ;; esac
|
|
local kbytes=$(awk -v e="$kpe" -v s="$kps" -v b="$logic_sec" 'BEGIN { printf "%.0f", (e-s+1)*b }')
|
|
local kfs=$(dev_fs_type "$kpn")
|
|
[ -z "$kfs" ] && kfs=$(lsblk -no fstype "/dev/$kpn" 2>/dev/null)
|
|
[ -z "$kfs" ] && kfs="raw"
|
|
local kmount=$(get_mount_point "$kpn")
|
|
local kused=0 kfree=0 kusage="-"
|
|
if [ "$kmount" != "-" ]; then
|
|
local kdf=$(df -k "/dev/$kpn" 2>/dev/null | tail -n1)
|
|
if [ -n "$kdf" ]; then
|
|
kused=$(echo "$kdf" | awk '{printf "%.0f", $3*1024}')
|
|
kfree=$(echo "$kdf" | awk '{printf "%.0f", $4*1024}')
|
|
kusage=$(echo "$kdf" | awk '{print $5}')
|
|
fi
|
|
fi
|
|
local kpb=0 kpd=0
|
|
partition_is_boot "$kpn" >/dev/null 2>&1 && kpb=1
|
|
partition_is_data "$kpn" >/dev/null 2>&1 && kpd=1
|
|
json_add_object
|
|
json_add_int "number" "$kno"
|
|
json_add_string "name" "$kpn"
|
|
json_add_string "path" "/dev/$kpn"
|
|
json_add_string "sec_start" "$kps"
|
|
json_add_string "sec_end" "$kpe"
|
|
json_add_int "size" "$kbytes"
|
|
json_add_string "size_formated" "$(format_bytes $kbytes)"
|
|
json_add_string "fs" "$kfs"
|
|
json_add_string "type" "primary"
|
|
json_add_string "mount_point" "$kmount"
|
|
json_add_int "used" "$kused"
|
|
json_add_string "used_formated" "$(format_bytes $kused)"
|
|
json_add_int "free" "$kfree"
|
|
json_add_string "free_formated" "$(format_bytes $kfree)"
|
|
json_add_string "usage" "$kusage"
|
|
json_add_boolean "is_boot" "$kpb"
|
|
json_add_boolean "is_data" "$kpd"
|
|
json_close_object
|
|
continue
|
|
fi
|
|
pnum="-1"
|
|
pfs="Free Space"
|
|
part_type="free"
|
|
else
|
|
case "$dev" in
|
|
sd*|vd*|sata*) part_name="${dev}${pnum}" ;;
|
|
mmcblk*|nvme*|md*) part_name="${dev}p${pnum}" ;;
|
|
esac
|
|
|
|
if [ "$pt" = "MBR" ]; then
|
|
if [ "$pnum" -gt 4 ] 2>/dev/null; then
|
|
part_type="logical"
|
|
elif [ "$pnum" -gt 0 ] 2>/dev/null; then
|
|
local sys_sz=$(cat "/sys/block/$dev/$part_name/size" 2>/dev/null || echo 0)
|
|
local sys_bytes=$(awk -v s="$sys_sz" -v b="$logic_sec" 'BEGIN { printf "%.0f", s*b }')
|
|
if [ "$sys_bytes" -gt 0 ] && [ "$sys_bytes" -ne "$part_bytes" ]; then
|
|
part_type="extended"
|
|
pfs="-"
|
|
part_bytes="$sys_bytes"
|
|
part_str=$(format_bytes "$sys_bytes")
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ "$part_type" != "extended" ]; then
|
|
if [ -n "$part_name" ] && [ -z "$pfs" ] && which lsblk >/dev/null 2>&1; then
|
|
pfs=$(lsblk -no fstype "/dev/$part_name" 2>/dev/null)
|
|
fi
|
|
[ -z "$pfs" ] && pfs="raw"
|
|
fi
|
|
|
|
part_mount=$(get_mount_point "$part_name")
|
|
if [ "$part_mount" != "-" ]; then
|
|
local df_info=$(df -k "/dev/$part_name" 2>/dev/null | tail -n1)
|
|
if [ -n "$df_info" ]; then
|
|
local df_used=$(echo "$df_info" | awk '{print $3}')
|
|
local df_avail=$(echo "$df_info" | awk '{print $4}')
|
|
local df_pct=$(echo "$df_info" | awk '{print $5}')
|
|
part_used=$(awk -v u="$df_used" 'BEGIN { printf "%.0f", u*1024 }')
|
|
part_free=$(awk -v f="$df_avail" 'BEGIN { printf "%.0f", f*1024 }')
|
|
part_usage="$df_pct"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
local pb=0 pd=0
|
|
if [ "$pnum" != "-1" ] && [ -n "$part_name" ] && [ "$part_name" != "-" ]; then
|
|
partition_is_boot "$part_name" >/dev/null 2>&1 && pb=1
|
|
partition_is_data "$part_name" >/dev/null 2>&1 && pd=1
|
|
fi
|
|
json_add_object
|
|
json_add_int "number" "$pnum"
|
|
json_add_string "name" "$part_name"
|
|
json_add_string "path" "/dev/$part_name"
|
|
json_add_string "sec_start" "$pstart"
|
|
json_add_string "sec_end" "$pend"
|
|
json_add_int "size" "$part_bytes"
|
|
json_add_string "size_formated" "$part_str"
|
|
json_add_string "fs" "$pfs"
|
|
json_add_string "type" "$part_type"
|
|
json_add_string "mount_point" "$part_mount"
|
|
json_add_int "used" "$part_used"
|
|
json_add_string "used_formated" "$(format_bytes $part_used)"
|
|
json_add_int "free" "$part_free"
|
|
json_add_string "free_formated" "$(format_bytes $part_free)"
|
|
json_add_string "usage" "$part_usage"
|
|
json_add_boolean "is_boot" "$pb"
|
|
json_add_boolean "is_data" "$pd"
|
|
json_close_object
|
|
done
|
|
IFS="$old_ifs"
|
|
fi
|
|
json_close_array
|
|
|
|
json_add_string "p_table" "$p_table"
|
|
json_dump
|
|
}
|
|
|
|
action_get_smart_attr() {
|
|
read -r input
|
|
local dev=$(echo "$input" | jsonfilter -e '@.device' 2>/dev/null | sed 's|^/dev/||')
|
|
if [ -z "$dev" ]; then
|
|
echo '[]'
|
|
return
|
|
fi
|
|
|
|
json_init
|
|
json_add_array "attributes"
|
|
|
|
if echo "$dev" | grep -q "^mmcblk"; then
|
|
local devpath="/sys/class/block/$dev"
|
|
[ ! -d "$devpath" ] && devpath="/sys/block/$dev"
|
|
|
|
local m_name=$(cat "$devpath/device/name" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
local m_type=$(cat "$devpath/device/type" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
local m_cid=$(cat "$devpath/device/cid" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
local m_csd=$(cat "$devpath/device/csd" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
local m_rev=$(cat "$devpath/device/rev" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
local m_oemid=$(cat "$devpath/device/oemid" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
local m_manfid=$(cat "$devpath/device/manfid" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
local m_sn=$(cat "$devpath/device/serial" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
local m_date=$(cat "$devpath/device/date" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
local life=$(cat "$devpath/device/life_time" 2>/dev/null)
|
|
local eol=$(cat "$devpath/device/pre_eol_info" 2>/dev/null | sed 's/[[:space:]]//g')
|
|
|
|
local eol_str="Normal (0x01)"
|
|
[ "$eol" = "0x02" ] || [ "$eol" = "2" ] && eol_str="Warning (Consuming >80%)"
|
|
[ "$eol" = "0x03" ] || [ "$eol" = "3" ] && eol_str="Urgent (Replace)"
|
|
|
|
local life_a="Not Available"
|
|
local life_b="Not Available"
|
|
if [ -n "$life" ]; then
|
|
local ta=$(echo "$life" | awk '{print $1}')
|
|
local tb=$(echo "$life" | awk '{print $2}')
|
|
if [ -n "$ta" ]; then
|
|
local a_pct=$((ta * 10))
|
|
[ "$a_pct" -gt 100 ] && a_pct=100
|
|
life_a="Used ${a_pct}% (SLC Estimated)"
|
|
fi
|
|
if [ -n "$tb" ]; then
|
|
local b_pct=$((tb * 10))
|
|
[ "$b_pct" -gt 100 ] && b_pct=100
|
|
life_b="Used ${b_pct}% (MLC Estimated)"
|
|
fi
|
|
fi
|
|
|
|
json_add_object; json_add_string "key" "Storage Standard"; json_add_string "value" "JEDEC eMMC (EXT_CSD Protocol)"; json_close_object
|
|
json_add_object; json_add_string "key" "Device Type"; json_add_string "value" "${m_type:-eMMC}"; json_close_object
|
|
[ -n "$m_name" ] && { json_add_object; json_add_string "key" "Product Name"; json_add_string "value" "$m_name"; json_close_object; }
|
|
[ -n "$m_sn" ] && { json_add_object; json_add_string "key" "Serial Number"; json_add_string "value" "$m_sn"; json_close_object; }
|
|
[ -n "$m_date" ] && { json_add_object; json_add_string "key" "Manufacturing Date"; json_add_string "value" "$m_date"; json_close_object; }
|
|
[ -n "$m_rev" ] && { json_add_object; json_add_string "key" "Hardware Revision"; json_add_string "value" "$m_rev"; json_close_object; }
|
|
[ -n "$m_manfid" ] && { json_add_object; json_add_string "key" "Manufacturer ID"; json_add_string "value" "$m_manfid"; json_close_object; }
|
|
[ -n "$m_oemid" ] && { json_add_object; json_add_string "key" "OEM / Application ID"; json_add_string "value" "$m_oemid"; json_close_object; }
|
|
json_add_object; json_add_string "key" "Pre-EOL Health State"; json_add_string "value" "$eol_str"; json_close_object
|
|
json_add_object; json_add_string "key" "Device Life Time (Type A - SLC)"; json_add_string "value" "$life_a"; json_close_object
|
|
json_add_object; json_add_string "key" "Device Life Time (Type B - MLC)"; json_add_string "value" "$life_b"; json_close_object
|
|
if [ -f "/sys/class/thermal/thermal_zone0/temp" ]; then
|
|
local tz=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null)
|
|
[ -n "$tz" ] && [ "$tz" -gt 0 ] 2>/dev/null && {
|
|
local tz_str="$(awk -v t="$tz" 'BEGIN {printf "%.1f °C", t/1000}')"
|
|
json_add_object; json_add_string "key" "Current Temperature"; json_add_string "value" "$tz_str"; json_close_object
|
|
}
|
|
fi
|
|
[ -n "$m_cid" ] && { json_add_object; json_add_string "key" "CID Register"; json_add_string "value" "$m_cid"; json_close_object; }
|
|
[ -n "$m_csd" ] && { json_add_object; json_add_string "key" "CSD Register"; json_add_string "value" "$m_csd"; json_close_object; }
|
|
else
|
|
local output=$(run_smartctl "$dev" "-H -A -i")
|
|
local old_ifs="$IFS"
|
|
IFS='
|
|
'
|
|
if echo "$output" | grep -q "NVMe Version:"; then
|
|
for line in $output; do
|
|
IFS="$old_ifs"
|
|
local k=$(echo "$line" | awk -F': ' '{print $1}' | sed 's/^[ \t]*//;s/[ \t]*$//')
|
|
local v=$(echo "$line" | awk -F': ' '{print $2}' | sed 's/^[ \t]*//;s/[ \t]*$//')
|
|
if [ -n "$k" ] && [ -n "$v" ]; then
|
|
json_add_object
|
|
json_add_string "key" "$k"
|
|
json_add_string "value" "$v"
|
|
json_close_object
|
|
fi
|
|
done
|
|
else
|
|
for line in $output; do
|
|
IFS="$old_ifs"
|
|
if echo "$line" | grep -qE '^[[:space:]]*[0-9]+[[:space:]]+[A-Za-z0-9_-]+'; then
|
|
local id=$(echo "$line" | awk '{printf "%02X", $1}')
|
|
local attr=$(echo "$line" | awk '{print $2}')
|
|
local flag=$(echo "$line" | awk '{print $3}')
|
|
local val=$(echo "$line" | awk '{print $4}')
|
|
local worst=$(echo "$line" | awk '{print $5}')
|
|
local thresh=$(echo "$line" | awk '{print $6}')
|
|
local type=$(echo "$line" | awk '{print $7}')
|
|
local updated=$(echo "$line" | awk '{print $8}')
|
|
local raw=$(echo "$line" | awk '{for(i=10;i<=NF;i++) printf "%s ", $i; if(NF>=10) print ""; else print $9}' | sed 's/[ \t]*$//')
|
|
|
|
json_add_object
|
|
json_add_string "id" "$id"
|
|
json_add_string "attrbute" "$attr"
|
|
json_add_string "flag" "$flag"
|
|
json_add_string "value" "$val"
|
|
json_add_string "worst" "$worst"
|
|
json_add_string "thresh" "$thresh"
|
|
json_add_string "type" "$type"
|
|
json_add_string "updated" "$updated"
|
|
json_add_string "raw" "$raw"
|
|
json_close_object
|
|
fi
|
|
done
|
|
fi
|
|
IFS="$old_ifs"
|
|
fi
|
|
|
|
json_close_array
|
|
json_dump
|
|
}
|
|
|
|
action_get_format_cmd() {
|
|
json_init
|
|
json_add_object "formats"
|
|
|
|
which mkfs.ext2 >/dev/null 2>&1 && json_add_string "ext2" "mkfs.ext2 -F -E lazy_itable_init=1"
|
|
which mkfs.ext3 >/dev/null 2>&1 && json_add_string "ext3" "mkfs.ext3 -F -E lazy_itable_init=1"
|
|
which mkfs.ext4 >/dev/null 2>&1 && json_add_string "ext4" "mkfs.ext4 -F -E lazy_itable_init=1"
|
|
which mkfs.fat >/dev/null 2>&1 && json_add_string "fat32" "mkfs.fat -F 32"
|
|
which mkfs.vfat >/dev/null 2>&1 && [ ! $(which mkfs.fat) ] && json_add_string "fat32" "mkfs.vfat -F 32"
|
|
which mkfs.exfat >/dev/null 2>&1 && json_add_string "exfat" "mkfs.exfat"
|
|
which mkfs.ntfs >/dev/null 2>&1 && json_add_string "ntfs" "mkfs.ntfs -f"
|
|
which mkswap >/dev/null 2>&1 && json_add_string "swap" "mkswap"
|
|
which mkfs.btrfs >/dev/null 2>&1 && json_add_string "btrfs" "mkfs.btrfs -f"
|
|
|
|
json_close_object
|
|
json_dump
|
|
}
|
|
|
|
action_format_partition() {
|
|
read -r input
|
|
local part=$(echo "$input" | jsonfilter -e '@.partition' 2>/dev/null | sed 's|^/dev/||')
|
|
local fs=$(echo "$input" | jsonfilter -e '@.filesystem' 2>/dev/null)
|
|
|
|
if [ -z "$part" ] || [ ! -e "/dev/$part" ]; then
|
|
echo '{"code": 1, "error": "Partition not found"}'
|
|
return
|
|
fi
|
|
|
|
local cmd=""
|
|
# [guard v5] boot partition check
|
|
if partition_is_boot "$part" >/dev/null 2>&1; then
|
|
require_not_boot "format /dev/$part"; return
|
|
fi
|
|
# [guard v5.1] soft: in-use / filesystem-bearing / A/B-slot partition requires force=1
|
|
if partition_needs_force "$part" >/dev/null 2>&1; then
|
|
local force_fmt=$(echo "$input" | jsonfilter -e '@.force' 2>/dev/null)
|
|
if [ "$force_fmt" != "1" ]; then
|
|
local msg="Confirm required: this partition holds data. Re-send with force:1 to proceed."
|
|
if is_overlay_mount "$part" >/dev/null 2>&1; then
|
|
msg="Confirm required: this is the system data partition (overlay, config + user data). Formatting wipes it (factory reset). Re-send with force:1 to proceed."
|
|
fi
|
|
logger -t diskbox "CONFIRM-REQUIRED format /dev/$part (system data, no force flag)"
|
|
echo "{\"code\": 3, \"error\": \"$msg\"}"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
case "$fs" in
|
|
ext2) cmd="mkfs.ext2 -F -E lazy_itable_init=1 /dev/$part" ;;
|
|
ext3) cmd="mkfs.ext3 -F -E lazy_itable_init=1 /dev/$part" ;;
|
|
ext4) cmd="mkfs.ext4 -F -E lazy_itable_init=1 /dev/$part" ;;
|
|
fat32) cmd="mkfs.fat -F 32 /dev/$part" ;;
|
|
exfat) cmd="mkfs.exfat /dev/$part" ;;
|
|
ntfs) cmd="mkfs.ntfs -f /dev/$part" ;;
|
|
swap) cmd="mkswap /dev/$part" ;;
|
|
btrfs) cmd="mkfs.btrfs -f /dev/$part" ;;
|
|
*) echo '{"code": 1, "error": "Unsupported filesystem"}'; return ;;
|
|
esac
|
|
|
|
local out=$($cmd 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_mk_partition_table() {
|
|
read -r input
|
|
local dev=$(echo "$input" | jsonfilter -e '@.device' 2>/dev/null | sed 's|^/dev/||')
|
|
local tbl=$(echo "$input" | jsonfilter -e '@.table' 2>/dev/null | tr 'A-Z' 'a-z')
|
|
|
|
[ "$tbl" = "mbr" ] && tbl="msdos"
|
|
if [ "$tbl" != "gpt" ] && [ "$tbl" != "msdos" ]; then
|
|
echo '{"code": 1, "error": "Invalid table type, must be GPT or MBR"}'
|
|
return
|
|
fi
|
|
|
|
# [guard v5] system disk
|
|
if disk_is_root_disk "$dev" >/dev/null 2>&1; then
|
|
require_not_system_disk "mktable /dev/$dev"; return
|
|
fi
|
|
|
|
local out=$(parted -s "/dev/$dev" mktable "$tbl" 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
rescan_partitions "$dev"
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_create_partition() {
|
|
read -r input
|
|
local dev=$(echo "$input" | jsonfilter -e '@.device' 2>/dev/null | sed 's|^/dev/||')
|
|
local start_sec=$(echo "$input" | jsonfilter -e '@.start_sec' 2>/dev/null | sed 's/s$//')
|
|
local end_sec=$(echo "$input" | jsonfilter -e '@.end_sec' 2>/dev/null | sed 's/s$//')
|
|
local ptype=$(echo "$input" | jsonfilter -e '@.type' 2>/dev/null || echo "primary")
|
|
[ -z "$ptype" ] && ptype="primary"
|
|
|
|
if [ -z "$dev" ] || [ -z "$start_sec" ] || [ -z "$end_sec" ]; then
|
|
echo '{"code": 1, "error": "Missing parameters"}'
|
|
return
|
|
fi
|
|
|
|
# [guard v5.1] creating a partition only consumes unallocated space -
|
|
# safe on the system disk too (it cannot touch existing partitions),
|
|
# so no root-disk block here.
|
|
|
|
# Sector alignment: phy_sec / logic_sec, min 2048
|
|
local phy_sec=$(cat "/sys/class/block/$dev/queue/physical_block_size" 2>/dev/null || echo 512)
|
|
local logic_sec=$(cat "/sys/class/block/$dev/queue/logical_block_size" 2>/dev/null || echo 512)
|
|
local align=$(( phy_sec / logic_sec ))
|
|
[ "$align" -lt 2048 ] && align=2048
|
|
|
|
if [ "$start_sec" -lt 2048 ] 2>/dev/null; then
|
|
start_sec=2048
|
|
else
|
|
local rem=$(( start_sec % align ))
|
|
if [ "$rem" -ne 0 ]; then
|
|
start_sec=$(( start_sec + align - rem ))
|
|
fi
|
|
fi
|
|
|
|
# Check end_sec format (+size)
|
|
case "$end_sec" in
|
|
+*[bkmgtsBKMGTS])
|
|
local num=$(echo "$end_sec" | sed 's/^+//;s/[bkmgtsBKMGTS]$//')
|
|
local unit=$(echo "$end_sec" | grep -o '[bkmgtsBKMGTS]$' | tr 'a-z' 'A-Z')
|
|
local mult=1
|
|
case "$unit" in
|
|
B) mult=1 ;;
|
|
S) mult=512 ;;
|
|
K) mult=1024 ;;
|
|
M) mult=1048576 ;;
|
|
G) mult=1073741824 ;;
|
|
T) mult=1099511627776 ;;
|
|
esac
|
|
local added_sectors=$(awk -v n="$num" -v m="$mult" -v b="$logic_sec" 'BEGIN { printf "%.0f", (n*m)/b }')
|
|
end_sec=$(awk -v s="$start_sec" -v a="$added_sectors" 'BEGIN { printf "%.0f", s+a-1 }')
|
|
;;
|
|
esac
|
|
|
|
# GPT automatic fix
|
|
local pt_type=$(parted -s "/dev/$dev" print 2>/dev/null | grep -i "^Partition Table:" | awk -F': ' '{print $2}' | tr 'a-z' 'A-Z')
|
|
if [ "$pt_type" = "GPT" ]; then
|
|
printf "ok\nfix\n" | parted ---pretend-input-tty "/dev/$dev" print >/dev/null 2>&1
|
|
fi
|
|
|
|
local out=$(parted -s -a optimal "/dev/$dev" mkpart "$ptype" "${start_sec}s" "${end_sec}s" 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
rescan_partitions "$dev"
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_remove_partition() {
|
|
read -r input
|
|
local dev=$(echo "$input" | jsonfilter -e '@.device' 2>/dev/null | sed 's|^/dev/||')
|
|
local num=$(echo "$input" | jsonfilter -e '@.number' 2>/dev/null)
|
|
|
|
if [ -z "$dev" ] || [ -z "$num" ] || [ "$num" = "-1" ]; then
|
|
echo '{"code": 1, "error": "Invalid partition number"}'
|
|
return
|
|
fi
|
|
|
|
# [guard v5.1] on the system disk, only non-system partitions may be removed:
|
|
# resolve the partition by number and check it individually.
|
|
if disk_is_root_disk "$dev" >/dev/null 2>&1; then
|
|
# lsblk's PN column is missing on BusyBox variants
|
|
# ("lsblk: unknown column: PN"), which would leave the name empty
|
|
# and trip the conservative system-disk block below. Fall back to
|
|
# the conventional partition name ($dev p$num) verified against
|
|
# the device node / sysfs before trusting it.
|
|
local pname=$(lsblk -no NAME,PN "/dev/$dev" 2>/dev/null | awk -v n="$num" '$2==n { print $1; exit }')
|
|
if [ -z "$pname" ]; then
|
|
pname="${dev}p${num}"
|
|
fi
|
|
if [ ! -b "/dev/$pname" ] && [ ! -e "/sys/block/$dev/$pname" ]; then
|
|
require_not_system_disk "remove_partition /dev/$dev #$num (partition not resolved)"; return
|
|
fi
|
|
if partition_is_boot "$pname" >/dev/null 2>&1; then
|
|
require_not_boot "remove /dev/$pname"; return
|
|
fi
|
|
fi
|
|
|
|
local out=$(parted -s "/dev/$dev" rm "$num" 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
rescan_partitions "$dev"
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_mount() {
|
|
read -r input
|
|
local dev=$(echo "$input" | jsonfilter -e '@.device' 2>/dev/null)
|
|
local point=$(echo "$input" | jsonfilter -e '@.point' 2>/dev/null)
|
|
local fs=$(echo "$input" | jsonfilter -e '@.fs' 2>/dev/null)
|
|
local opts=$(echo "$input" | jsonfilter -e '@.options' 2>/dev/null)
|
|
|
|
if [ -z "$dev" ] || [ -z "$point" ]; then
|
|
echo '{"code": 1, "error": "Device and mount point required"}'
|
|
return
|
|
fi
|
|
|
|
mkdir -p "$point"
|
|
local cmd="mount"
|
|
[ -n "$fs" ] && [ "$fs" != "auto" ] && cmd="$cmd -t $fs"
|
|
[ -n "$opts" ] && cmd="$cmd -o $opts"
|
|
cmd="$cmd $dev $point"
|
|
|
|
local out=$($cmd 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_umount() {
|
|
read -r input
|
|
local point=$(echo "$input" | jsonfilter -e '@.point' 2>/dev/null)
|
|
|
|
if [ -z "$point" ]; then
|
|
echo '{"code": 1, "error": "Mount point required"}'
|
|
return
|
|
fi
|
|
|
|
# [guard] system mount points: unmounting /, /rom or /overlay* bricks or
|
|
# crashes the router (live base system / writable rootfs). Hard block.
|
|
case "$point" in
|
|
/|/rom|/overlay|/overlay/*)
|
|
logger -t diskbox "BLOCKED(system-mount): umount $point"
|
|
echo '{"code": 2, "error": "Blocked: this is a system mount point."}'
|
|
return
|
|
;;
|
|
esac
|
|
|
|
# [guard] is_boot partitions: unmounting firmware partitions (A/B slots,
|
|
# kernel, rootfs) can brick the device. Hard block. v5.2 classifies any
|
|
# partition > 4GiB as data space, so large data partitions (e.g. the
|
|
# 113G eMMC at /mnt/emmc0) are not affected.
|
|
local devmp=$(awk -v p="$point" '$2==p { print $1; exit }' /proc/mounts 2>/dev/null)
|
|
case "$devmp" in
|
|
/dev/sd*|/dev/mmcblk*|/dev/nvme*|/dev/vd*)
|
|
partition_is_boot "$devmp" && {
|
|
logger -t diskbox "BLOCKED(system-partition): umount $point on $devmp"
|
|
echo '{"code": 2, "error": "Blocked: this is a system partition."}'
|
|
return
|
|
}
|
|
;;
|
|
esac
|
|
|
|
local out=$(umount "$point" 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_get_mount_points() {
|
|
json_init
|
|
json_add_array "mount_points"
|
|
|
|
local old_ifs="$IFS"
|
|
IFS='
|
|
'
|
|
local m_lines=$(awk '$1 ~ /^\/dev\// {print $1 "|" $2 "|" $3 "|" $4 "|" $5 "|" $6}' /proc/mounts 2>/dev/null)
|
|
for line in $m_lines; do
|
|
IFS="|"
|
|
set -- $line
|
|
local dev="$1"
|
|
local mp="$2"
|
|
local fs="$3"
|
|
local opts="$4"
|
|
local dump="$5"
|
|
local pass="$6"
|
|
IFS="$old_ifs"
|
|
[ -n "$dev" ] || continue
|
|
json_add_object
|
|
json_add_string "device" "$dev"
|
|
json_add_string "mount_point" "$mp"
|
|
json_add_string "fs" "$fs"
|
|
json_add_string "mount_options" "$opts"
|
|
json_add_string "dump" "$dump"
|
|
json_add_string "pass" "$pass"
|
|
json_close_object
|
|
done
|
|
IFS="$old_ifs"
|
|
|
|
json_close_array
|
|
json_dump
|
|
}
|
|
|
|
action_eject_device() {
|
|
read -r input
|
|
local dev=$(echo "$input" | jsonfilter -e '@.device' 2>/dev/null | sed 's|^/dev/||')
|
|
|
|
if [ -z "$dev" ]; then
|
|
echo '{"code": 1, "error": "Device required"}'
|
|
return
|
|
fi
|
|
|
|
if echo "$dev" | grep -q "^md"; then
|
|
if which mdadm >/dev/null 2>&1; then
|
|
local members=$(awk -v d="$dev" '$0 ~ d {gsub(/[[\]:]/,""); for(i=5;i<=NF;i++) sub(/\[[0-9]+\]/, "", $i); for(i=5;i<=NF;i++) printf "/dev/%s ", $i}' /proc/mdstat 2>/dev/null)
|
|
mdadm --stop "/dev/$dev" >/dev/null 2>&1
|
|
mdadm --remove "/dev/$dev" >/dev/null 2>&1
|
|
for mbr in $members; do
|
|
mdadm --zero-superblock "$mbr" >/dev/null 2>&1
|
|
done
|
|
if which uci >/dev/null 2>&1 && [ -f /etc/config/mdadm ]; then
|
|
uci -q delete mdadm
|
|
uci commit mdadm
|
|
mdadm -D -s 2>/dev/null | while read -r line; do
|
|
local dev_item=$(echo "$line" | awk '{print $2}')
|
|
local uuid_item=$(echo "$line" | grep -o 'UUID=[^ ]*' | cut -d= -f2)
|
|
if [ -n "$dev_item" ] && [ -n "$uuid_item" ]; then
|
|
local s=$(uci add mdadm array)
|
|
uci set mdadm.$s.device="$dev_item"
|
|
uci set mdadm.$s.uuid="$uuid_item"
|
|
fi
|
|
done
|
|
uci commit mdadm
|
|
fi
|
|
echo '{"code": 0, "message": "RAID stopped and removed"}'
|
|
return
|
|
fi
|
|
fi
|
|
|
|
if [ -e "/sys/block/$dev/device/delete" ]; then
|
|
echo 1 > "/sys/block/$dev/device/delete"
|
|
echo '{"code": 0, "message": "Device ejected"}'
|
|
else
|
|
echo '{"code": 1, "error": "Cannot eject this device"}'
|
|
fi
|
|
}
|
|
|
|
action_rescan_disks() {
|
|
for host in /sys/class/scsi_host/host*/scan; do
|
|
[ -e "$host" ] && echo "- - -" > "$host" 2>/dev/null
|
|
done
|
|
which mdadm >/dev/null 2>&1 && mdadm --assemble --scan >/dev/null 2>&1
|
|
echo '{"code": 0, "message": "Scan completed"}'
|
|
}
|
|
|
|
action_list_raid_devices() {
|
|
json_init
|
|
json_add_object "raid_devices"
|
|
|
|
if [ -f /proc/mdstat ]; then
|
|
local old_ifs="$IFS"
|
|
IFS='
|
|
'
|
|
local r_lines=$(awk '/^md[0-9]+ :/ {
|
|
md=$1;
|
|
active=$3;
|
|
level=$4;
|
|
members="";
|
|
for(i=5;i<=NF;i++) {
|
|
sub(/\[[0-9]+\]/, "", $i);
|
|
members=(members ? members ", " : "") "/dev/" $i;
|
|
}
|
|
print md "|" active "|" level "|" members;
|
|
}' /proc/mdstat 2>/dev/null)
|
|
for line in $r_lines; do
|
|
IFS="|"
|
|
set -- $line
|
|
local md="$1"
|
|
local act="$2"
|
|
local lvl="$3"
|
|
local mbrs="$4"
|
|
IFS="$old_ifs"
|
|
[ -n "$md" ] || continue
|
|
|
|
local sz=$(cat "/sys/class/block/$md/size" 2>/dev/null || echo 0)
|
|
local ss=$(cat "/sys/class/block/$md/queue/logical_block_size" 2>/dev/null || echo 512)
|
|
local size_bytes=$(awk -v s="$sz" -v b="$ss" 'BEGIN { printf "%.0f", s*b }')
|
|
local st="ACTIVE"
|
|
if which mdadm >/dev/null 2>&1; then
|
|
local state=$(mdadm --detail "/dev/$md" 2>/dev/null | grep -i "^[[:space:]]*State :" | awk -F': ' '{print $2}')
|
|
[ -n "$state" ] && st="$state"
|
|
fi
|
|
|
|
json_add_object "$md"
|
|
json_add_string "name" "$md"
|
|
json_add_string "path" "/dev/$md"
|
|
json_add_string "level" "$lvl"
|
|
json_add_string "active" "$(echo $act | tr 'a-z' 'A-Z')"
|
|
json_add_string "status" "$st"
|
|
json_add_string "members_str" "$mbrs"
|
|
json_add_int "size" "$size_bytes"
|
|
json_add_string "size_formated" "$(format_bytes $size_bytes)"
|
|
json_close_object
|
|
done
|
|
IFS="$old_ifs"
|
|
fi
|
|
|
|
json_close_object
|
|
json_dump
|
|
}
|
|
|
|
action_create_raid() {
|
|
read -r input
|
|
local name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
|
local level=$(echo "$input" | jsonfilter -e '@.level' 2>/dev/null)
|
|
local members=$(echo "$input" | jsonfilter -e '@.members[@]' 2>/dev/null)
|
|
|
|
if [ -z "$name" ]; then
|
|
for n in $(seq 0 127); do
|
|
if [ ! -e "/dev/md$n" ]; then
|
|
name="/dev/md$n"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
case "$name" in
|
|
/dev/*) ;;
|
|
*) name="/dev/$name" ;;
|
|
esac
|
|
|
|
local count=$(echo "$members" | wc -w)
|
|
if [ "$count" -lt 2 ]; then
|
|
echo '{"code": 1, "error": "At least 2 members required"}'
|
|
return
|
|
fi
|
|
|
|
local out=$(mdadm --create "$name" --run --assume-clean --homehost=any --level="$level" --raid-devices="$count" $members 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
if which uci >/dev/null 2>&1 && [ -f /etc/config/mdadm ]; then
|
|
uci -q delete mdadm
|
|
uci commit mdadm
|
|
mdadm -D -s 2>/dev/null | while read -r line; do
|
|
local dev_item=$(echo "$line" | awk '{print $2}')
|
|
local uuid_item=$(echo "$line" | grep -o 'UUID=[^ ]*' | cut -d= -f2)
|
|
if [ -n "$dev_item" ] && [ -n "$uuid_item" ]; then
|
|
local s=$(uci add mdadm array)
|
|
uci set mdadm.$s.device="$dev_item"
|
|
uci set mdadm.$s.uuid="$uuid_item"
|
|
fi
|
|
done
|
|
uci commit mdadm
|
|
/etc/init.d/mdadm enable 2>/dev/null
|
|
fi
|
|
echo '{"code": 0, "message": "RAID created successfully"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_list_btrfs_devices() {
|
|
json_init
|
|
json_add_object "btrfs_devices"
|
|
|
|
if which btrfs >/dev/null 2>&1; then
|
|
local old_ifs="$IFS"
|
|
IFS='
|
|
'
|
|
local b_lines=$(btrfs filesystem show -d --raw 2>/dev/null | awk '
|
|
/^Label:/ {
|
|
lbl=$2; gsub(/'\''/, "", lbl);
|
|
uuid=$4;
|
|
cur_uuid=uuid;
|
|
labels[cur_uuid]=lbl;
|
|
}
|
|
/Total devices/ {
|
|
used[cur_uuid]=$NF;
|
|
}
|
|
/devid/ {
|
|
sz=$4;
|
|
path=$NF;
|
|
sizes[cur_uuid]+=sz;
|
|
members[cur_uuid]=(members[cur_uuid] ? members[cur_uuid] ", " : "") path;
|
|
}
|
|
END {
|
|
for (u in labels) {
|
|
print u "|" labels[u] "|" (sizes[u]+0) "|" (used[u]+0) "|" members[u];
|
|
}
|
|
}')
|
|
for line in $b_lines; do
|
|
IFS="|"
|
|
set -- $line
|
|
local uuid="$1"
|
|
local lbl="$2"
|
|
local sz="$3"
|
|
local used="$4"
|
|
local mbrs="$5"
|
|
IFS="$old_ifs"
|
|
[ -n "$uuid" ] || continue
|
|
json_add_object "$uuid"
|
|
json_add_string "uuid" "$uuid"
|
|
json_add_string "label" "$lbl"
|
|
json_add_int "size" "$sz"
|
|
json_add_string "size_formated" "$(format_bytes $sz)"
|
|
json_add_int "used" "$used"
|
|
json_add_string "used_formated" "$(format_bytes $used)"
|
|
json_add_string "members" "$mbrs"
|
|
json_close_object
|
|
done
|
|
IFS="$old_ifs"
|
|
fi
|
|
|
|
json_close_object
|
|
json_dump
|
|
}
|
|
|
|
action_get_btrfs_info() {
|
|
read -r input
|
|
local uuid=$(echo "$input" | jsonfilter -e '@.uuid' 2>/dev/null)
|
|
if [ -z "$uuid" ]; then
|
|
echo '{"error": "UUID required"}'
|
|
return
|
|
fi
|
|
|
|
local mnt="/tmp/.btrfs_tmp"
|
|
mkdir -p "$mnt"
|
|
umount "$mnt" >/dev/null 2>&1
|
|
mount -t btrfs -o subvol=/ "UUID=$uuid" "$mnt" >/dev/null 2>&1
|
|
|
|
json_init
|
|
if [ -d "$mnt" ]; then
|
|
local show=$(btrfs filesystem show --raw "$mnt" 2>/dev/null)
|
|
local usage_out=$(btrfs filesystem usage -b "$mnt" 2>/dev/null)
|
|
local label=$(echo "$show" | grep "^Label:" | head -n1 | awk '{print $2}' | sed "s/'//g")
|
|
local used=$(echo "$usage_out" | grep -i "^[[:space:]]*Used:" | head -n1 | awk '{print $2}')
|
|
local free=$(echo "$usage_out" | grep -i "^[[:space:]]*Free (estimated):" | head -n1 | awk '{print $3}')
|
|
local d_raid=$(echo "$usage_out" | grep -i "^Data," | head -n1 | awk '{print $2}')
|
|
local m_raid=$(echo "$usage_out" | grep -i "^Metadata," | head -n1 | awk '{print $2}')
|
|
local mbrs=$(echo "$show" | grep "path" | awk '{print $NF}' | tr '\n' ',' | sed 's/,$//;s/,/, /g')
|
|
|
|
local total=$(awk -v u="${used:-0}" -v f="${free:-0}" 'BEGIN { printf "%.0f", u+f }')
|
|
local pct="0%"
|
|
[ "$total" -gt 0 ] && pct="$(awk -v u="$used" -v t="$total" 'BEGIN {printf "%.2f%%", (u/t)*100}')"
|
|
|
|
json_add_string "uuid" "$uuid"
|
|
json_add_string "label" "${label:--}"
|
|
json_add_string "data_raid_level" "${d_raid:--}"
|
|
json_add_string "metadata_raid_level" "${m_raid:--}"
|
|
json_add_int "size" "$total"
|
|
json_add_string "size_formated" "$(format_bytes $total)"
|
|
json_add_int "used" "${used:-0}"
|
|
json_add_string "used_formated" "$(format_bytes ${used:-0})"
|
|
json_add_int "free" "${free:-0}"
|
|
json_add_string "free_formated" "$(format_bytes ${free:-0})"
|
|
json_add_string "usage" "$pct"
|
|
json_add_string "members" "$mbrs"
|
|
fi
|
|
json_dump
|
|
}
|
|
|
|
action_get_btrfs_subvolumes() {
|
|
read -r input
|
|
local uuid=$(echo "$input" | jsonfilter -e '@.uuid' 2>/dev/null)
|
|
local mnt="/tmp/.btrfs_tmp"
|
|
mkdir -p "$mnt"
|
|
umount "$mnt" >/dev/null 2>&1
|
|
mount -t btrfs -o subvol=/ "UUID=$uuid" "$mnt" >/dev/null 2>&1
|
|
|
|
json_init
|
|
json_add_array "subvolumes"
|
|
|
|
if which btrfs >/dev/null 2>&1 && [ -d "$mnt" ]; then
|
|
local def_id=$(btrfs subvolume get-default "$mnt" 2>/dev/null | awk '{print $2}')
|
|
|
|
local root_id="5"
|
|
json_add_object
|
|
json_add_string "id" "$root_id"
|
|
json_add_string "path" "/"
|
|
json_add_string "top_level" "0"
|
|
json_add_boolean "default_subvolume" $( [ "$def_id" = "$root_id" ] && echo 1 || echo 0 )
|
|
json_close_object
|
|
|
|
local old_ifs="$IFS"
|
|
IFS='
|
|
'
|
|
local sv_lines=$(btrfs subvolume list -gcu "$mnt" 2>/dev/null)
|
|
for line in $sv_lines; do
|
|
IFS="$old_ifs"
|
|
local id=$(echo "$line" | awk '{print $2}')
|
|
local top=$(echo "$line" | awk '{print $8}')
|
|
local sv_uuid=$(echo "$line" | awk '{print $10}')
|
|
local p=$(echo "$line" | awk '{print $12}')
|
|
|
|
json_add_object
|
|
json_add_string "id" "$id"
|
|
json_add_string "path" "/$p"
|
|
json_add_string "top_level" "$top"
|
|
json_add_string "uuid" "$sv_uuid"
|
|
json_add_boolean "default_subvolume" $( [ "$def_id" = "$id" ] && echo 1 || echo 0 )
|
|
json_close_object
|
|
done
|
|
IFS="$old_ifs"
|
|
fi
|
|
|
|
json_close_array
|
|
json_dump
|
|
}
|
|
|
|
action_create_btrfs() {
|
|
read -r input
|
|
local label=$(echo "$input" | jsonfilter -e '@.label' 2>/dev/null)
|
|
local level=$(echo "$input" | jsonfilter -e '@.level' 2>/dev/null || echo "single")
|
|
local members=$(echo "$input" | jsonfilter -e '@.members[@]' 2>/dev/null)
|
|
|
|
local count=$(echo "$members" | wc -w)
|
|
if [ "$count" -lt 1 ]; then
|
|
echo '{"code": 1, "error": "Members required"}'
|
|
return
|
|
fi
|
|
|
|
local opt=""
|
|
[ -n "$label" ] && opt="-L $label"
|
|
local out=$(mkfs.btrfs -f $opt -d "$level" $members 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_btrfs_subvol_create() {
|
|
read -r input
|
|
local uuid=$(echo "$input" | jsonfilter -e '@.uuid' 2>/dev/null)
|
|
local path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null)
|
|
local mnt="/tmp/.btrfs_tmp"
|
|
mount -t btrfs -o subvol=/ "UUID=$uuid" "$mnt" >/dev/null 2>&1
|
|
|
|
local out=$(btrfs subvolume create "$mnt$path" 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_btrfs_subvol_delete() {
|
|
read -r input
|
|
local uuid=$(echo "$input" | jsonfilter -e '@.uuid' 2>/dev/null)
|
|
local path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null)
|
|
local mnt="/tmp/.btrfs_tmp"
|
|
mount -t btrfs -o subvol=/ "UUID=$uuid" "$mnt" >/dev/null 2>&1
|
|
|
|
local out=$(btrfs subvolume delete "$mnt$path" 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_btrfs_subvol_set_default() {
|
|
read -r input
|
|
local uuid=$(echo "$input" | jsonfilter -e '@.uuid' 2>/dev/null)
|
|
local path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null)
|
|
local mnt="/tmp/.btrfs_tmp"
|
|
mount -t btrfs -o subvol=/ "UUID=$uuid" "$mnt" >/dev/null 2>&1
|
|
|
|
local out=$(btrfs subvolume set-default "$mnt$path" 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_btrfs_set_label() {
|
|
read -r input
|
|
local uuid=$(echo "$input" | jsonfilter -e '@.uuid' 2>/dev/null)
|
|
local label=$(echo "$input" | jsonfilter -e '@.label' 2>/dev/null)
|
|
local mnt="/tmp/.btrfs_tmp"
|
|
mount -t btrfs -o subvol=/ "UUID=$uuid" "$mnt" >/dev/null 2>&1
|
|
|
|
local out=$(btrfs filesystem label "$mnt" "$label" 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
action_btrfs_snapshot_create() {
|
|
read -r input
|
|
local uuid=$(echo "$input" | jsonfilter -e '@.uuid' 2>/dev/null)
|
|
local src=$(echo "$input" | jsonfilter -e '@.source' 2>/dev/null)
|
|
local dst=$(echo "$input" | jsonfilter -e '@.dest' 2>/dev/null)
|
|
local ro=$(echo "$input" | jsonfilter -e '@.readonly' 2>/dev/null)
|
|
local mnt="/tmp/.btrfs_tmp"
|
|
mount -t btrfs -o subvol=/ "UUID=$uuid" "$mnt" >/dev/null 2>&1
|
|
|
|
[ -z "$dst" ] && dst="/.snapshot${src}/$(date +%Y%m%d%H%M%S)"
|
|
mkdir -p "$(dirname "$mnt$dst")"
|
|
|
|
local opt=""
|
|
[ "$ro" = "1" ] || [ "$ro" = "true" ] && opt="-r"
|
|
local out=$(btrfs subvolume snapshot $opt "$mnt$src" "$mnt$dst" 2>&1)
|
|
local res=$?
|
|
if [ $res -eq 0 ]; then
|
|
echo '{"code": 0, "message": "OK"}'
|
|
else
|
|
json_init
|
|
json_add_int "code" "$res"
|
|
json_add_string "error" "$out"
|
|
json_dump
|
|
fi
|
|
}
|
|
|
|
# Main entry point
|
|
case "$1" in
|
|
list)
|
|
echo '{
|
|
"list_devices": {},
|
|
"get_disk_info": {"device": "string"},
|
|
"get_smart_attr": {"device": "string"},
|
|
"get_format_cmd": {},
|
|
"format_partition": {"partition": "string", "filesystem": "string", "force": "int"},
|
|
"mk_partition_table": {"device": "string", "table": "string"},
|
|
"create_partition": {"device": "string", "start_sec": "string", "end_sec": "string", "type": "string"},
|
|
"remove_partition": {"device": "string", "number": "int"},
|
|
"mount": {"device": "string", "point": "string", "fs": "string", "options": "string"},
|
|
"umount": {"point": "string"},
|
|
"get_mount_points": {},
|
|
"eject_device": {"device": "string"},
|
|
"rescan_disks": {},
|
|
"list_raid_devices": {},
|
|
"create_raid": {"name": "string", "level": "string", "members": "array"},
|
|
"list_btrfs_devices": {},
|
|
"get_btrfs_info": {"uuid": "string"},
|
|
"get_btrfs_subvolumes": {"uuid": "string"},
|
|
"create_btrfs": {"label": "string", "level": "string", "members": "array"},
|
|
"btrfs_subvol_create": {"uuid": "string", "path": "string"},
|
|
"btrfs_subvol_delete": {"uuid": "string", "path": "string"},
|
|
"btrfs_subvol_set_default": {"uuid": "string", "path": "string"},
|
|
"btrfs_set_label": {"uuid": "string", "label": "string"},
|
|
"btrfs_snapshot_create": {"uuid": "string", "source": "string", "dest": "string", "readonly": "boolean"}
|
|
}'
|
|
;;
|
|
call)
|
|
case "$2" in
|
|
list_devices) action_list_devices ;;
|
|
get_disk_info) action_get_disk_info ;;
|
|
get_smart_attr) action_get_smart_attr ;;
|
|
get_format_cmd) action_get_format_cmd ;;
|
|
format_partition) action_format_partition ;;
|
|
mk_partition_table) action_mk_partition_table ;;
|
|
create_partition) action_create_partition ;;
|
|
remove_partition) action_remove_partition ;;
|
|
mount) action_mount ;;
|
|
umount) action_umount ;;
|
|
get_mount_points) action_get_mount_points ;;
|
|
eject_device) action_eject_device ;;
|
|
rescan_disks) action_rescan_disks ;;
|
|
list_raid_devices) action_list_raid_devices ;;
|
|
create_raid) action_create_raid ;;
|
|
list_btrfs_devices) action_list_btrfs_devices ;;
|
|
get_btrfs_info) action_get_btrfs_info ;;
|
|
get_btrfs_subvolumes) action_get_btrfs_subvolumes ;;
|
|
create_btrfs) action_create_btrfs ;;
|
|
btrfs_subvol_create) action_btrfs_subvol_create ;;
|
|
btrfs_subvol_delete) action_btrfs_subvol_delete ;;
|
|
btrfs_subvol_set_default) action_btrfs_subvol_set_default ;;
|
|
btrfs_set_label) action_btrfs_set_label ;;
|
|
btrfs_snapshot_create) action_btrfs_snapshot_create ;;
|
|
*) echo '{"error": "Method not found"}' ;;
|
|
esac
|
|
;;
|
|
esac
|