💋 Sync 2026-10-04 04:16:09

This commit is contained in:
github-actions[bot] committed 2026-10-04 04:16:09 +08:00
1 parent f6b2df73ec
commit f62992fd80
745 files changed
+212416 -1173

No files matched your search

@@ -0,0 +1,77 @@
#!/bin/sh
# Aggressive rebuild — matches source netwizard behaviour. Called only if user opts in.
MODE="$1"; LAN_IP="$2"; PPPOE_USER="$3"; PPPOE_PASS="$4"; WAN_IFACE="$5"
[ -z "$MODE" ] && MODE="dhcp"
[ -z "$LAN_IP" ] && LAN_IP="192.168.10.1"
BACKUP_DIR="/etc/config.bak.$(date +%s)"
mkdir -p "$BACKUP_DIR"
for f in network firewall dhcp wireless system wizard netwizard; do
[ -f /etc/config/$f ] && cp /etc/config/$f "$BACKUP_DIR/"
done
echo "Backup saved to $BACKUP_DIR"
if [ -z "$WAN_IFACE" ]; then
WAN_IFACE=$(uci -q get network.wan.device)
[ -z "$WAN_IFACE" ] && WAN_IFACE=$(ls /sys/class/net/ 2>/dev/null | grep -E '^(eth[01]|wan)' | head -n1)
[ -z "$WAN_IFACE" ] && WAN_IFACE="eth1"
fi
uci -q delete network.wan 2>/dev/null
uci -q delete network.wan6 2>/dev/null
uci -q delete network.lan6 2>/dev/null
WAN_IDX=$(uci show firewall | sed -n "s/^firewall\.@zone\[\([0-9]*\)\]\.name='wan'$/\1/p")
[ -n "$WAN_IDX" ] && uci -q delete firewall.@zone[$WAN_IDX] 2>/dev/null
if [ "$MODE" = "siderouter" ]; then
LAN_IDX=$(uci show firewall | sed -n "s/^firewall\.@zone\[\([0-9]*\)\]\.name='lan'$/\1/p")
[ -n "$LAN_IDX" ] && uci -q delete firewall.@zone[$LAN_IDX].masq 2>/dev/null
uci set network.lan=interface
uci set network.lan.proto='static'
uci set network.lan.ipaddr="$LAN_IP"
uci set network.lan.netmask='255.255.255.0'
else
uci set network.wan=interface
uci set network.wan.device="$WAN_IFACE"
if [ "$MODE" = "pppoe" ]; then
uci set network.wan.proto='pppoe'
[ -n "$PPPOE_USER" ] && uci set network.wan.username="$PPPOE_USER"
[ -n "$PPPOE_PASS" ] && uci set network.wan.password="$PPPOE_PASS"
else
uci set network.wan.proto='dhcp'
fi
uci set network.wan6=interface
uci set network.wan6.device="@wan"
uci set network.wan6.proto='dhcpv6'
uci add firewall zone >/dev/null
uci set firewall.@zone[-1].name='wan'
uci set firewall.@zone[-1].input='REJECT'
uci set firewall.@zone[-1].output='ACCEPT'
uci set firewall.@zone[-1].forward='REJECT'
uci set firewall.@zone[-1].masq='1'
uci set firewall.@zone[-1].mtu_fix='1'
uci add_list firewall.@zone[-1].network='wan'
uci add_list firewall.@zone[-1].network='wan6'
uci add firewall forwarding >/dev/null
uci set firewall.@forwarding[-1].src='lan'
uci set firewall.@forwarding[-1].dest='wan'
uci set network.lan.ipaddr="$LAN_IP"
uci set network.lan.netmask='255.255.255.0'
fi
uci -q commit network
uci -q commit firewall
uci -q commit dhcp
/etc/init.d/network restart >/dev/null 2>&1 &
sleep 2
/etc/init.d/firewall reload >/dev/null 2>&1 &
/etc/init.d/dnsmasq reload >/dev/null 2>&1 &
echo "Aggressive apply complete. Mode: $MODE, WAN: $WAN_IFACE, LAN: $LAN_IP"
echo "Backup: $BACKUP_DIR"
@@ -0,0 +1,45 @@
#!/bin/sh
# Regenerate nginx shortcut server blocks + dnsmasq domain entries from /etc/config/wizard shortcuts.
# Called after the user adds/removes a shortcut in the UI.
clean() {
local sec
for sec in $(uci -q show nginx 2>/dev/null | sed -n "s/^nginx\.\(_sc_[^=.]*\)=server$/\1/p"); do
uci -q delete nginx."$sec"
done
for sec in $(uci -q show dhcp 2>/dev/null | sed -n "s/^dhcp\.\(_sc_[^=.]*\)=domain$/\1/p"); do
uci -q delete dhcp."$sec"
done
}
process_one() {
local shortcut to_url lanaddr cfg="$1"
lanaddr="$(uci -q get network.lan.ipaddr | tr ' ' '\n' | head -n1)"
lanaddr="${lanaddr%%/*}"
[ -z "$lanaddr" ] && lanaddr="10.0.0.1"
config_get shortcut "$cfg" shortcut
config_get to_url "$cfg" to_url
[ -z "$shortcut" ] || [ -z "$to_url" ] && return 0
if [ -f /etc/nginx/uci.conf ]; then
uci -q set nginx._sc_$shortcut=server
uci -q set nginx._sc_$shortcut.server_name="$shortcut"
uci -q add_list nginx._sc_$shortcut.listen="80"
uci -q set nginx._sc_$shortcut.return="302 $to_url"
uci -q set dhcp._sc_$shortcut=domain
uci -q set dhcp._sc_$shortcut.name="$shortcut"
uci -q set dhcp._sc_$shortcut.ip="$lanaddr"
fi
}
clean
config_load wizard
config_foreach process_one shortcuts
uci -q commit nginx 2>/dev/null
uci -q commit dhcp 2>/dev/null
[ -x /etc/init.d/nginx ] && /etc/init.d/nginx reload >/dev/null 2>&1
[ -x /etc/init.d/dnsmasq ] && /etc/init.d/dnsmasq reload >/dev/null 2>&1
echo "Shortcuts applied."
@@ -0,0 +1,12 @@
#!/bin/sh
BACKUP_DIR="/etc/config.bak.$(date +%s)"
mkdir -p "$BACKUP_DIR"
COUNT=0
for f in network firewall dhcp wireless system wizard netwizard taskplan; do
if [ -f /etc/config/$f ]; then
cp /etc/config/$f "$BACKUP_DIR/"
COUNT=$((COUNT + 1))
fi
done
echo "Backed up $COUNT files to $BACKUP_DIR"
ls -1 "$BACKUP_DIR"
@@ -0,0 +1,2 @@
#!/bin/sh
ls -1d /etc/config.bak.* 2>/dev/null | sort -r
@@ -0,0 +1,23 @@
#!/bin/sh
DIR="$1"
[ -z "$DIR" ] && { echo "Usage: restore.sh <backup-dir>"; exit 1; }
[ ! -d "$DIR" ] && { echo "Directory not found: $DIR"; exit 1; }
SAFETY="/etc/config.bak.pre-restore.$(date +%s)"
mkdir -p "$SAFETY"
for f in "$DIR"/*; do
[ -f "$f" ] || continue
base=$(basename "$f")
[ -f "/etc/config/$base" ] && cp "/etc/config/$base" "$SAFETY/"
cp "$f" "/etc/config/$base"
done
for c in network firewall dhcp wireless wizard netwizard taskplan; do
uci -q commit $c 2>/dev/null
done
/etc/init.d/network reload >/dev/null 2>&1 &
/etc/init.d/firewall reload >/dev/null 2>&1 &
echo "Restored from $DIR"
echo "Previous state saved to $SAFETY"
@@ -0,0 +1,82 @@
#!/bin/sh
# ubus RPC module for Quick Actions Failover
. /usr/share/libubox/jshn.sh
list_methods() {
cat <<'EOF'
{
"status": {},
"interfaces": {},
"action": { "action": "string" }
}
EOF
}
method_status() {
cat >/dev/null
local en lock
en=$(uci -q get quickactions.failover.enabled); [ -z "$en" ] && en=1
[ -f /tmp/quickactions_wan_down.lock ] && lock=1 || lock=0
json_init
json_add_int "enabled" "$en"
json_add_int "on_backup" "$lock"
json_add_string "primary" "$(uci -q get quickactions.failover.primary)"
json_add_string "backups" "$(uci -q get quickactions.failover.backups)"
json_add_string "check_ips" "$(uci -q get quickactions.failover.check_ips)"
json_add_string "interval" "$(uci -q get quickactions.failover.check_interval)"
json_add_string "last_log" "$(logread 2>/dev/null | grep -E 'quickactions-(failover|hotplug)' | tail -1)"
json_dump
}
method_interfaces() {
cat >/dev/null
local list
list=$(
{
uci show network 2>/dev/null | grep '=interface$' | cut -d. -f2 | cut -d= -f1
ls -1 /sys/class/net/ 2>/dev/null | grep -E '^(pppoe-|eth|usb)'
} | sort -u | tr '\n' ' '
)
json_init
json_add_string "interfaces" "$list"
json_dump
}
method_action() {
local payload action out
payload=$(cat)
json_load "$payload"
json_get_var action "action"
case "$action" in
on|off|status|test)
out=$(/usr/bin/quickactions-failover "$action" 2>&1)
;;
*)
out="unknown action: [$action]"
;;
esac
json_init
json_add_string "output" "$out"
json_dump
}
call_method() {
local method="$1"
case "$method" in
action) method_action ;;
status) method_status ;;
interfaces) method_interfaces ;;
*) cat >/dev/null
json_init
json_add_string "error" "unknown_method"
json_dump ;;
esac
}
case "$1" in
list) list_methods ;;
call) call_method "$2" ;;
*) echo '{"error":"usage: '$0' {list|call <method>}"}' ;;
esac
@@ -0,0 +1,338 @@
#!/bin/sh
# ubus RPC module for Quick Actions Scripts
. /usr/share/libubox/jshn.sh
SCRIPTS_DIR=/etc/quickactions/scripts
mkdir -p "$SCRIPTS_DIR"
list_methods() {
cat <<'EOF'
{
"list": {},
"read": { "sid": "string" },
"write": { "sid": "string", "name": "string", "category": "string", "content": "string", "args": "string", "timeout": "string", "enabled": "string" },
"delete": { "sid": "string" },
"run": { "sid": "string" },
"chmod": { "sid": "string", "mode": "string" },
"install_preset": { "preset": "string" },
"detail": { "sid": "string" }
}
EOF
}
method_list() {
cat >/dev/null
json_init
json_add_array "scripts"
# No pipe into while → no subshell → json_add works
for sid in $(uci show quickactions 2>/dev/null | grep '=script$' | cut -d. -f2 | cut -d= -f1); do
name=$(uci -q get "quickactions.$sid.name")
category=$(uci -q get "quickactions.$sid.category")
path=$(uci -q get "quickactions.$sid.path")
[ -z "$path" ] && path="$SCRIPTS_DIR/$sid.sh"
enabled=$(uci -q get "quickactions.$sid.enabled")
[ -z "$enabled" ] && enabled="1"
sz=0
[ -f "$path" ] && sz=$(wc -c < "$path" 2>/dev/null)
[ -z "$sz" ] && sz=0
json_add_object
json_add_string "sid" "$sid"
json_add_string "name" "$name"
json_add_string "category" "$category"
json_add_string "path" "$path"
json_add_string "enabled" "$enabled"
json_add_int "size" "$sz"
json_close_object
done
json_close_array
json_dump
}
method_read() {
local sid=""
json_load "$(cat)"
json_get_var sid "sid"
[ -z "$sid" ] && { json_init; json_add_string "error" "no sid"; json_dump; return; }
local path
path=$(uci -q get "quickactions.$sid.path")
[ -z "$path" ] && path="$SCRIPTS_DIR/$sid.sh"
json_init
if [ -f "$path" ]; then
json_add_string "content" "$(cat "$path")"
else
json_add_string "content" ""
fi
json_dump
}
method_write() {
local payload sid name category content args timeout enabled path
payload=$(cat)
json_load "$payload"
json_get_var sid "sid"
json_get_var name "name"
json_get_var category "category"
json_get_var content "content"
json_get_var args "args"
json_get_var timeout "timeout"
json_get_var enabled "enabled"
[ -z "$sid" ] && sid="script_$(date +%s)"
[ -z "$name" ] && name="$sid"
[ -z "$category" ] && category="custom"
[ -z "$timeout" ] && timeout="60"
[ -z "$enabled" ] && enabled="1"
path=$(uci -q get "quickactions.$sid.path")
[ -z "$path" ] && path="$SCRIPTS_DIR/$sid.sh"
mkdir -p "$(dirname "$path")"
printf '%s' "$content" > "$path"
chmod 755 "$path" 2>/dev/null
uci -q set "quickactions.$sid=script"
uci -q set "quickactions.$sid.name=$name"
uci -q set "quickactions.$sid.category=$category"
uci -q set "quickactions.$sid.path=$path"
uci -q set "quickactions.$sid.enabled=$enabled"
uci -q set "quickactions.$sid.timeout=$timeout"
[ -n "$args" ] && uci -q set "quickactions.$sid.args=$args"
uci -q commit quickactions
json_init
json_add_string "sid" "$sid"
json_add_boolean "ok" 1
json_dump
}
method_delete() {
local sid=""
json_load "$(cat)"
json_get_var sid "sid"
[ -z "$sid" ] && { json_init; json_add_boolean "ok" 0; json_dump; return; }
local path
path=$(uci -q get "quickactions.$sid.path")
[ -z "$path" ] && path="$SCRIPTS_DIR/$sid.sh"
[ -f "$path" ] && rm -f "$path"
uci -q delete "quickactions.$sid"
uci -q commit quickactions
json_init
json_add_boolean "ok" 1
json_dump
}
method_run() {
local sid=""
json_load "$(cat)"
json_get_var sid "sid"
[ -z "$sid" ] && { json_init; json_add_string "error" "no sid"; json_dump; return; }
local path timeout args
path=$(uci -q get "quickactions.$sid.path")
[ -z "$path" ] && path="$SCRIPTS_DIR/$sid.sh"
timeout=$(uci -q get "quickactions.$sid.timeout")
[ -z "$timeout" ] && timeout=60
args=$(uci -q get "quickactions.$sid.args")
if [ ! -f "$path" ]; then
json_init
json_add_string "output" "Script not found: $path"
json_add_int "exit_code" 127
json_dump
return
fi
[ -x "$path" ] || chmod 755 "$path" 2>/dev/null
local start_sec end_sec out rc
start_sec=$(date +%s 2>/dev/null)
out=$(cd / && timeout "$timeout" "$path" $args 2>&1)
rc=$?
end_sec=$(date +%s 2>/dev/null)
json_init
json_add_string "output" "$out"
json_add_int "exit_code" "$rc"
json_add_int "duration" "$((end_sec - start_sec))"
json_dump
}
method_chmod() {
local payload sid mode
payload=$(cat)
json_load "$payload"
json_get_var sid "sid"
json_get_var mode "mode"
[ -z "$mode" ] && mode="755"
local path
path=$(uci -q get "quickactions.$sid.path")
[ -z "$path" ] && path="$SCRIPTS_DIR/$sid.sh"
[ -f "$path" ] && chmod "$mode" "$path" 2>/dev/null
json_init
json_add_boolean "ok" 1
json_dump
}
method_detail() {
local sid=""
json_load "$(cat)"
json_get_var sid "sid"
[ -z "$sid" ] && { json_init; json_add_string "error" "no sid"; json_dump; return; }
local path
path=$(uci -q get "quickactions.$sid.path")
[ -z "$path" ] && path="$SCRIPTS_DIR/$sid.sh"
json_init
json_add_string "sid" "$sid"
json_add_string "name" "$(uci -q get quickactions.$sid.name)"
json_add_string "category" "$(uci -q get quickactions.$sid.category)"
json_add_string "path" "$path"
json_add_string "enabled" "$(uci -q get quickactions.$sid.enabled)"
json_add_string "timeout" "$(uci -q get quickactions.$sid.timeout)"
json_add_string "args" "$(uci -q get quickactions.$sid.args)"
json_add_string "description" "$(uci -q get quickactions.$sid.description)"
if [ -f "$path" ]; then
json_add_string "content" "$(cat "$path")"
json_add_int "size" "$(wc -c < "$path")"
json_add_string "permissions" "$(ls -l "$path" | awk '{print $1}')"
else
json_add_string "content" ""
json_add_int "size" 0
json_add_string "permissions" ""
fi
json_dump
}
method_install_preset() {
local preset=""
json_load "$(cat)"
json_get_var preset "preset"
local sid="failover-lite"
local path="$SCRIPTS_DIR/$sid.sh"
mkdir -p "$SCRIPTS_DIR"
if [ "$preset" = "failover" ] || [ -z "$preset" ]; then
cat > "$path" << 'FO_EOF'
#!/bin/sh
# Failover-lite — MWAN3 substitute for low-RAM routers.
. /lib/functions.sh
config_load quickactions
cfg() { config_get v failover "$1" "$2"; echo "$v"; }
ENABLED=$(cfg enabled 1); [ "$ENABLED" = "0" ] && exit 0
PRIMARY=$(cfg primary ""); [ -z "$PRIMARY" ] && exit 0
CHECK_IPS=$(cfg check_ips "1.1.1.1 8.8.8.8")
PING_TIMEOUT=$(cfg ping_timeout 5)
FAIL_AT=$(cfg fail_threshold 2)
REC_AT=$(cfg recover_threshold 1)
LOCK=/tmp/qa-failover-preset.lock
FAIL_FILE=/tmp/qa-failover-preset.fails
OK_FILE=/tmp/qa-failover-preset.oks
phys_iface() {
local log="$1" dev
[ -e "/sys/class/net/pppoe-$log" ] && { echo "pppoe-$log"; return; }
case "$log" in *pppoe*) [ -e "/sys/class/net/$log" ] && { echo "$log"; return; } ;; esac
dev=$(uci -q get network.$log.device)
[ -n "$dev" ] && [ -e "/sys/class/net/$dev" ] && { echo "$dev"; return; }
echo "$log"
}
ping_ok() {
local iface="$1" ip
for ip in $CHECK_IPS; do
ping -I "$iface" -c 1 -W "$PING_TIMEOUT" "$ip" >/dev/null 2>&1 && return 0
done
return 1
}
flush_ct() {
[ -e /proc/net/nf_conntrack ] && echo f > /proc/net/nf_conntrack
[ -e /proc/net/ip_conntrack ] && echo f > /proc/net/ip_conntrack
return 0
}
DEV=$(phys_iface "$PRIMARY")
if [ -f "$LOCK" ]; then
OK_N=0; [ -f "$OK_FILE" ] && OK_N=$(cat "$OK_FILE")
if ping_ok "$DEV"; then
OK_N=$((OK_N + 1))
if [ "$OK_N" -ge "$REC_AT" ]; then
rm -f "$LOCK" "$FAIL_FILE" "$OK_FILE"
flush_ct
logger -t qa-failover-preset "PRIMARY $PRIMARY recovered"
else
echo "$OK_N" > "$OK_FILE"
fi
else
echo 0 > "$OK_FILE"
fi
exit 0
fi
if ping_ok "$DEV"; then
rm -f "$FAIL_FILE"
exit 0
fi
FAIL_N=0; [ -f "$FAIL_FILE" ] && FAIL_N=$(cat "$FAIL_FILE")
FAIL_N=$((FAIL_N + 1))
echo "$FAIL_N" > "$FAIL_FILE"
if [ "$FAIL_N" -ge "$FAIL_AT" ]; then
touch "$LOCK"; rm -f "$FAIL_FILE"; flush_ct
logger -t qa-failover-preset "PRIMARY $PRIMARY DOWN after $FAIL_AT failures"
fi
exit 0
FO_EOF
fi
chmod 755 "$path"
uci -q set "quickactions.$sid=script"
uci -q set "quickactions.$sid.name=Failover-lite"
uci -q set "quickactions.$sid.category=failover"
uci -q set "quickactions.$sid.path=$path"
uci -q set "quickactions.$sid.enabled=0"
uci -q set "quickactions.$sid.timeout=30"
uci -q set "quickactions.$sid.description=MWAN3 substitute: ping test + conntrack flush"
uci -q commit quickactions
json_init
json_add_string "sid" "$sid"
json_add_string "path" "$path"
json_add_boolean "ok" 1
json_dump
}
call_method() {
local method="$1"
case "$method" in
list) method_list ;;
read) method_read ;;
write) method_write ;;
delete) method_delete ;;
run) method_run ;;
chmod) method_chmod ;;
detail) method_detail ;;
install_preset) method_install_preset ;;
*) cat >/dev/null
json_init
json_add_string "error" "unknown_method"
json_dump ;;
esac
}
case "$1" in
list) list_methods ;;
call) call_method "$2" ;;
*) echo '{"error":"usage"}' ;;
esac
@@ -0,0 +1,117 @@
#!/bin/sh
# ubus RPC module for Quick Actions Task Plan
. /usr/share/libubox/jshn.sh
LOG_FILE="/etc/quickactions-taskplan/taskplan.log"
CONFIG_FILE="/etc/config/taskplan"
CRON_FILE="/etc/crontabs/root"
INIT_SCRIPT="/etc/init.d/quickactions-taskplan"
list_methods() {
cat <<'EOF'
{
"status": {},
"list": {},
"task_list": {},
"start": {},
"stop": {},
"restart": {},
"reload": {}
}
EOF
}
method_status() {
local running=0 cron_tasks=0 scheduled=0 startup=0 log_content=""
pgrep -f quickactions-taskplan >/dev/null 2>&1 && running=1
[ -f "$CRON_FILE" ] && cron_tasks=$(grep -c "quickactions-taskplanhandler" "$CRON_FILE" 2>/dev/null || echo 0)
[ -f "$CONFIG_FILE" ] && scheduled=$(grep -c "stime" "$CONFIG_FILE" 2>/dev/null || echo 0)
[ -f "$CONFIG_FILE" ] && startup=$(grep -c "ltime" "$CONFIG_FILE" 2>/dev/null || echo 0)
[ -f "$LOG_FILE" ] && log_content=$(tail -n 50 "$LOG_FILE" 2>/dev/null)
json_init
json_add_int "running" $running
json_add_int "cron_tasks" $cron_tasks
json_add_int "scheduled_tasks" $scheduled
json_add_int "startup_tasks" $startup
json_add_string "log" "$log_content"
json_dump
}
method_list() {
local ssum=0 lsum=0
[ -f "$CONFIG_FILE" ] && ssum=$(grep -c "stime" "$CONFIG_FILE" 2>/dev/null || echo 0)
[ -f "$CONFIG_FILE" ] && lsum=$(grep -c "ltime" "$CONFIG_FILE" 2>/dev/null || echo 0)
json_init
json_add_int "scheduled" $ssum
json_add_int "startup" $lsum
json_dump
}
method_task_list() {
json_init
json_add_array "tasks"
if [ -f "$CONFIG_FILE" ]; then
local index=0
while true; do
local section=$(uci -q get taskplan.@stime[$index] 2>/dev/null)
[ -z "$section" ] && break
json_add_object
json_add_string "type" "scheduled"
json_add_int "index" $index
json_add_int "enable" "$(uci -q get taskplan.@stime[$index].enable || echo 0)"
json_add_string "remarks" "$(uci -q get taskplan.@stime[$index].remarks || echo "")"
json_add_string "stype" "$(uci -q get taskplan.@stime[$index].stype || echo "")"
json_add_string "schedule" "$(uci -q get taskplan.@stime[$index].minute) $(uci -q get taskplan.@stime[$index].hour) $(uci -q get taskplan.@stime[$index].week) $(uci -q get taskplan.@stime[$index].month)"
json_close_object
index=$((index + 1))
done
index=0
while true; do
local section=$(uci -q get taskplan.@ltime[$index] 2>/dev/null)
[ -z "$section" ] && break
json_add_object
json_add_string "type" "startup"
json_add_int "index" $index
json_add_int "enable" "$(uci -q get taskplan.@ltime[$index].enable || echo 0)"
json_add_string "remarks" "$(uci -q get taskplan.@ltime[$index].remarks || echo "")"
json_add_string "stype" "$(uci -q get taskplan.@ltime[$index].stype || echo "")"
json_add_int "delay" "$(uci -q get taskplan.@ltime[$index].delay || echo 10)"
json_close_object
index=$((index + 1))
done
fi
json_close_array
json_dump
}
service_action() {
local action="$1"
[ -x "$INIT_SCRIPT" ] && "$INIT_SCRIPT" "$action" >/dev/null 2>&1
local ret=$?
json_init
json_add_int "result" $ret
[ $ret -eq 0 ] && json_add_string "message" "Service $action ok"
json_dump
}
call_method() {
local method="$1"
cat >/dev/null
case "$method" in
status) method_status ;;
list) method_list ;;
task_list) method_task_list ;;
start) service_action start ;;
stop) service_action stop ;;
restart) service_action restart ;;
reload) service_action reload ;;
*) json_init; json_add_string "error" "unknown_method"; json_dump ;;
esac
}
case "$1" in
list) list_methods ;;
call) call_method "$2" ;;
*) echo '{"error":"usage: '$0' {list|call <method>}"}' ;;
esac
@@ -0,0 +1,640 @@
#!/bin/sh
# ubus RPC module for Quick Actions VLAN tab.
# Params arrive on stdin as JSON. No positional args.
. /usr/share/libubox/jshn.sh
VLAN_DIR=/etc/quickactions/vlan
TPL_DIR=$VLAN_DIR/templates
GEN_DIR=$VLAN_DIR/generated
mkdir -p "$VLAN_DIR" "$TPL_DIR" "$GEN_DIR"
list_methods() {
cat <<'EOF'
{
"detect": {},
"preview": { "plan": "string" },
"apply": { "plan": "string", "safe": "string" },
"snapshot": { "note": "string" },
"list_snapshots": {},
"restore": { "path": "string" },
"confirm": {},
"generate_cli": { "plan": "string", "target": "string" },
"save_template": { "name": "string", "target": "string", "plan": "string", "notes": "string" },
"list_templates": {},
"load_template": { "name": "string" },
"delete_template": { "name": "string" },
"save_generated": { "name": "string", "target": "string", "content": "string" },
"list_generated": {},
"delete_generated": { "name": "string" },
"lldp_neighbors": {},
"snmp_query": { "ip": "string", "community": "string" },
"bridge_vlan_show": {},
"kernel_ifaces": {},
"iface_stats": { "ifaces": "string" },
"ping_test": { "iface": "string", "ip": "string" }
}
EOF
}
# read stdin and populate named vars (var=json_key pairs)
_load() {
local payload
payload=$(cat)
json_load "$payload"
while [ "$#" -ge 2 ]; do
eval "$1=\"\$(json_get_var __v '$2'; echo \$__v)\""
shift 2
done
}
detect() {
json_init
local mode="none"
uci show network 2>/dev/null | grep -q "\.type='bridge'" && mode="dsa"
uci show network 2>/dev/null | grep -q "^network\.@switch\[0\]" && mode="swconfig"
json_add_string "mode" "$mode"
json_add_array "bridges"
for s in $(uci show network 2>/dev/null | grep "=device$" | cut -d. -f2 | cut -d= -f1); do
t=$(uci -q get "network.$s.type")
[ "$t" != "bridge" ] && continue
name=$(uci -q get "network.$s.name")
[ -z "$name" ] && name="$s"
json_add_object
json_add_string "sid" "$s"
json_add_string "name" "$name"
vf=$(uci -q get "network.$s.vlan_filtering")
json_add_string "vlan_filtering" "${vf:-0}"
json_close_object
done
json_close_array
json_add_array "bridge_vlans"
for s in $(uci show network 2>/dev/null | grep "=bridge-vlan$" | cut -d. -f2 | cut -d= -f1); do
json_add_object
json_add_string "sid" "$s"
json_add_string "device" "$(uci -q get network.$s.device)"
json_add_string "vlan" "$(uci -q get network.$s.vlan)"
json_add_string "ports" "$(uci -q get network.$s.ports | tr '\n' ' ')"
json_close_object
done
json_close_array
json_add_array "interfaces"
for s in $(uci show network 2>/dev/null | grep "=interface$" | cut -d. -f2 | cut -d= -f1); do
json_add_object
json_add_string "sid" "$s"
json_add_string "device" "$(uci -q get network.$s.device)"
json_add_string "proto" "$(uci -q get network.$s.proto)"
json_add_string "ipaddr" "$(uci -q get network.$s.ipaddr)"
json_add_string "netmask" "$(uci -q get network.$s.netmask)"
json_close_object
done
json_close_array
json_add_array "dhcp"
for s in $(uci show dhcp 2>/dev/null | grep "=dhcp$" | cut -d. -f2 | cut -d= -f1); do
json_add_object
json_add_string "sid" "$s"
json_add_string "interface" "$(uci -q get dhcp.$s.interface)"
json_add_string "start" "$(uci -q get dhcp.$s.start)"
json_add_string "limit" "$(uci -q get dhcp.$s.limit)"
json_close_object
done
json_close_array
json_add_array "zones"
local i=0
while true; do
local name
name=$(uci -q get "firewall.@zone[$i].name")
[ -z "$name" ] && break
json_add_object
json_add_string "name" "$name"
json_add_string "network" "$(uci -q get firewall.@zone[$i].network | tr '\n' ' ')"
json_add_string "input" "$(uci -q get firewall.@zone[$i].input)"
json_add_string "output" "$(uci -q get firewall.@zone[$i].output)"
json_add_string "forward" "$(uci -q get firewall.@zone[$i].forward)"
json_close_object
i=$((i + 1))
done
json_close_array
json_add_array "wifi"
for s in $(uci show wireless 2>/dev/null | grep "=wifi-iface$" | cut -d. -f2 | cut -d= -f1); do
json_add_object
json_add_string "sid" "$s"
json_add_string "device" "$(uci -q get wireless.$s.device)"
json_add_string "mode" "$(uci -q get wireless.$s.mode)"
json_add_string "ssid" "$(uci -q get wireless.$s.ssid)"
json_add_string "network" "$(uci -q get wireless.$s.network)"
json_close_object
done
json_close_array
json_add_array "easymesh"
for iface in /sys/class/net/bat0.*; do
[ -e "$iface" ] || continue
json_add_string "" "$(basename "$iface")"
done
json_close_array
json_add_string "kernel" "$(uname -r)"
json_add_string "mem_mb" "$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null)"
json_add_string "hostname" "$(cat /proc/sys/kernel/hostname 2>/dev/null)"
json_dump
}
kernel_ifaces() {
json_init
json_add_array "ifaces"
for d in /sys/class/net/*; do
[ -e "$d" ] || continue
local n
n=$(basename "$d")
case "$n" in
lo|sit*|ip6tnl*|gre*|gretap*|erspan*|ifb*|veth*) continue ;;
esac
local up
up="0"; [ "$(cat "$d/operstate" 2>/dev/null)" = "up" ] && up="1"
json_add_object
json_add_string "name" "$n"
json_add_int "up" "$up"
json_close_object
done
json_close_array
json_dump
}
preview() {
local plan
_load plan plan
[ -z "$plan" ] && { json_init; json_add_string "error" "no plan"; json_dump; return; }
echo "$plan" > "$VLAN_DIR/last-plan.json"
local out
out=$(
python3 - <<'PYPREVIEW' 2>&1
import json
plan = json.load(open("/etc/quickactions/vlan/last-plan.json"))
lines = []
for v in plan.get("vlans", []):
lines.append("network.%s=bridge-vlan device=%s vlan=%s" % (
v.get("sid","vlan"), v.get("bridge",""), v.get("vlan","")))
for i in plan.get("interfaces", []):
lines.append("network.%s=interface device=%s proto=%s ip=%s" % (
i.get("name",""), i.get("device",""), i.get("proto",""), i.get("ipaddr","")))
print("\n".join(lines))
PYPREVIEW
)
json_init
json_add_string "diff" "$out"
json_dump
}
apply() {
local plan safe
_load plan plan safe safe
[ -z "$plan" ] && { json_init; json_add_string "error" "no plan"; json_dump; return; }
[ -z "$safe" ] && safe="0"
local ts
ts=$(date +%Y%m%d-%H%M%S)
cp /etc/config/network "$VLAN_DIR/network.bak.$ts" 2>/dev/null
echo "auto before apply" > "$VLAN_DIR/network.bak.$ts.note"
echo "$plan" > "$VLAN_DIR/last-plan.json"
if ! command -v python3 >/dev/null 2>&1; then
json_init
json_add_string "error" "python3 required for VLAN apply"
json_dump
return
fi
python3 - <<'PYAPPLY'
import json, subprocess
plan = json.load(open("/etc/quickactions/vlan/last-plan.json"))
def uci(*args):
try:
subprocess.run(["uci"] + list(args), check=True, capture_output=True)
except subprocess.CalledProcessError:
return False
return True
for v in plan.get("vlans", []):
name = v.get("sid") or ("vlan_%s_%s" % (v.get("bridge","br-lan"), v.get("vlan","0")))
uci("set", "network.%s=bridge-vlan" % name)
uci("set", "network.%s.device=%s" % (name, v.get("bridge","")))
uci("set", "network.%s.vlan=%s" % (name, v.get("vlan","")))
for p in v.get("tagged", []):
uci("add_list", "network.%s.ports=%s:t" % (name, p))
for p in v.get("untagged", []):
uci("add_list", "network.%s.ports=%s:u*" % (name, p))
for i in plan.get("interfaces", []):
n = i.get("name","")
if not n: continue
uci("set", "network.%s=interface" % n)
uci("set", "network.%s.device=%s" % (n, i.get("device","")))
uci("set", "network.%s.proto=%s" % (n, i.get("proto","static")))
if i.get("ipaddr"): uci("set", "network.%s.ipaddr=%s" % (n, i["ipaddr"]))
if i.get("netmask"): uci("set", "network.%s.netmask=%s" % (n, i["netmask"]))
for d in plan.get("dhcp", []):
n = d.get("name","")
if not n: continue
uci("set", "dhcp.%s=dhcp" % n)
uci("set", "dhcp.%s.interface=%s" % (n, d.get("interface","")))
uci("set", "dhcp.%s.start=%s" % (n, d.get("start","100")))
uci("set", "dhcp.%s.limit=%s" % (n, d.get("limit","150")))
uci("set", "dhcp.%s.leasetime=%s" % (n, d.get("leasetime","12h")))
for z in plan.get("zones", []):
n = z.get("name","")
if not n: continue
exists = subprocess.run(["uci","-q","get","firewall.%s" % n], capture_output=True).returncode == 0
if not exists:
uci("set", "firewall.%s=zone" % n)
uci("set", "firewall.%s.name=%s" % (n, n))
for net in z.get("network", []):
uci("add_list", "firewall.%s.network=%s" % (n, net))
uci("set", "firewall.%s.input=%s" % (n, z.get("input","REJECT")))
uci("set", "firewall.%s.output=%s" % (n, z.get("output","ACCEPT")))
uci("set", "firewall.%s.forward=%s" % (n, z.get("forward","REJECT")))
subprocess.run(["uci","commit","network"])
subprocess.run(["uci","commit","dhcp"])
subprocess.run(["uci","commit","firewall"])
PYAPPLY
if [ "$safe" = "1" ]; then
local now deadline
now=$(date +%s)
deadline=$((now + 90))
echo "$deadline" > "$VLAN_DIR/pending_rollback"
( sleep 90
if [ -f "$VLAN_DIR/pending_rollback" ]; then
dl=$(cat "$VLAN_DIR/pending_rollback" 2>/dev/null)
if [ "$dl" = "$deadline" ]; then
cp "$VLAN_DIR/network.bak.$ts" /etc/config/network
uci commit network
/etc/init.d/network reload
rm -f "$VLAN_DIR/pending_rollback"
fi
fi ) >/dev/null 2>&1 &
fi
/etc/init.d/network reload >/dev/null 2>&1
sleep 2
json_init
json_add_boolean "ok" 1
json_add_string "snapshot" "$VLAN_DIR/network.bak.$ts"
[ "$safe" = "1" ] && json_add_int "rollback_in" 90
json_dump
}
snapshot() {
local note
_load note note
[ -z "$note" ] && note="manual"
local ts path
ts=$(date +%Y%m%d-%H%M%S)
path="$VLAN_DIR/network.bak.$ts"
cp /etc/config/network "$path" 2>/dev/null || { json_init; json_add_string "error" "cp failed"; json_dump; return; }
echo "$note" > "$path.note" 2>/dev/null
ls -1t $VLAN_DIR/network.bak.* 2>/dev/null | grep -v '\.note$' | tail -n +21 | while read -r old; do
rm -f "$old" "$old.note"
done
json_init
json_add_string "path" "$path"
json_add_string "ts" "$ts"
json_dump
}
list_snapshots() {
json_init
json_add_array "snapshots"
for f in $(ls -1t $VLAN_DIR/network.bak.* 2>/dev/null | grep -v '\.note$'); do
[ -f "$f" ] || continue
local sz note
sz=$(wc -c < "$f" 2>/dev/null)
note=""
[ -f "$f.note" ] && note=$(cat "$f.note" 2>/dev/null)
json_add_object
json_add_string "path" "$f"
json_add_string "size" "${sz:-0}"
json_add_string "note" "$note"
json_close_object
done
json_close_array
json_add_string "pending" "$(cat $VLAN_DIR/pending_rollback 2>/dev/null)"
json_dump
}
restore() {
local path
_load path path
[ -z "$path" ] || [ ! -f "$path" ] && { json_init; json_add_string "error" "bad path"; json_dump; return; }
cp "$path" /etc/config/network
uci commit network
/etc/init.d/network reload >/dev/null 2>&1
json_init
json_add_boolean "ok" 1
json_dump
}
confirm() {
rm -f "$VLAN_DIR/pending_rollback"
json_init
json_add_boolean "ok" 1
json_dump
}
generate_cli() {
local plan target
_load plan plan target target
[ -z "$target" ] && target="generic"
echo "$plan" > "$VLAN_DIR/last-plan.json"
local out
out=$(
python3 - <<PYGEN
import json
plan = json.load(open("/etc/quickactions/vlan/last-plan.json"))
target = "$target"
out = []
if target == "mikrotik":
bridge = plan["vlans"][0].get("bridge","bridge1") if plan.get("vlans") else "bridge1"
pvid = {}
for v in plan.get("vlans", []):
for p in v.get("untagged", []):
pvid[p] = v.get("vlan")
out.append("/interface bridge")
out.append("add name=%s vlan-filtering=yes" % bridge)
out.append("/interface bridge port")
for p, vid in pvid.items():
out.append("add bridge=%s interface=%s pvid=%s" % (bridge, p, vid))
out.append("/interface bridge vlan")
for v in plan.get("vlans", []):
t = ",".join(v.get("tagged",[]))
u = ",".join(v.get("untagged",[]))
parts = ["bridge=%s" % bridge, "vlan-ids=%s" % v.get("vlan","")]
if t: parts.append("tagged=%s" % t)
if u: parts.append("untagged=%s" % u)
out.append("add " + " ".join(parts))
elif target == "cisco":
for v in plan.get("vlans", []):
out.append("vlan %s" % v.get("vlan",""))
out.append(" name VLAN_%s" % v.get("vlan",""))
out.append("!")
trunks = set()
for v in plan.get("vlans", []):
for p in v.get("tagged", []):
trunks.add(p)
for p in sorted(trunks):
out.append("interface %s" % p)
out.append(" switchport trunk encapsulation dot1q")
out.append(" switchport mode trunk")
allowed = []
for v in plan.get("vlans", []):
if p in v.get("tagged", []):
allowed.append(str(v.get("vlan")))
out.append(" switchport trunk allowed vlan %s" % ",".join(allowed))
out.append("!")
for v in plan.get("vlans", []):
for p in v.get("untagged", []):
out.append("interface %s" % p)
out.append(" switchport mode access")
out.append(" switchport access vlan %s" % v.get("vlan",""))
out.append("!")
elif target == "edgeos":
for v in plan.get("vlans", []):
vid = v.get("vlan","")
for p in v.get("tagged", []):
out.append("set interfaces ethernet %s vif %s description VLAN_%s" % (p, vid, vid))
for p in v.get("untagged", []):
out.append("set interfaces ethernet %s description VLAN_%s" % (p, vid))
out.append("set interfaces ethernet %s pvid %s" % (p, vid))
else:
out.append("VLAN Ports (T=tagged, U=untagged)")
for v in plan.get("vlans", []):
plist = []
for p in v.get("tagged", []): plist.append("%s T" % p)
for p in v.get("untagged", []): plist.append("%s U" % p)
out.append("%-5s %s" % (v.get("vlan",""), ", ".join(plist)))
print("\n".join(out))
PYGEN
)
json_init
json_add_string "cli" "$out"
json_add_string "target" "$target"
json_dump
}
save_template() {
local name target plan notes
_load name name target target plan plan notes notes
[ -z "$name" ] && { json_init; json_add_string "error" "name required"; json_dump; return; }
local fname
fname=$(echo "$name" | tr -c 'a-zA-Z0-9._-' '_')
printf '%s\n' "$plan" > "$TPL_DIR/$fname.json"
printf '{"target":"%s","notes":"%s"}\n' "$target" "$notes" > "$TPL_DIR/$fname.meta"
json_init
json_add_string "name" "$fname"
json_dump
}
list_templates() {
json_init
json_add_array "templates"
for f in $TPL_DIR/*.json; do
[ -f "$f" ] || continue
local b meta
b=$(basename "$f" .json)
meta="{}"
[ -f "$TPL_DIR/$b.meta" ] && meta=$(cat "$TPL_DIR/$b.meta")
json_add_object
json_add_string "name" "$b"
json_add_string "meta" "$meta"
json_close_object
done
json_close_array
json_dump
}
load_template() {
local name
_load name name
local fname
fname=$(echo "$name" | tr -c 'a-zA-Z0-9._-' '_')
if [ ! -f "$TPL_DIR/$fname.json" ]; then
json_init; json_add_string "error" "not found"; json_dump; return
fi
json_init
json_add_string "plan" "$(cat "$TPL_DIR/$fname.json")"
[ -f "$TPL_DIR/$fname.meta" ] && json_add_string "meta" "$(cat "$TPL_DIR/$fname.meta")"
json_dump
}
delete_template() {
local name
_load name name
local fname
fname=$(echo "$name" | tr -c 'a-zA-Z0-9._-' '_')
rm -f "$TPL_DIR/$fname.json" "$TPL_DIR/$fname.meta"
json_init; json_add_boolean "ok" 1; json_dump
}
save_generated() {
local name target content
_load name name target target content content
[ -z "$name" ] && name="gen-$(date +%s)"
local fname
fname=$(echo "$name" | tr -c 'a-zA-Z0-9._-' '_')
printf '%s\n' "$content" > "$GEN_DIR/$fname.txt"
echo "$target" > "$GEN_DIR/$fname.target"
ls -1t $GEN_DIR/*.txt 2>/dev/null | tail -n +51 | while read -r old; do
rm -f "$old" "$old.target"
done
json_init; json_add_string "name" "$fname"; json_dump
}
list_generated() {
json_init
json_add_array "generated"
for f in $GEN_DIR/*.txt; do
[ -f "$f" ] || continue
local b tgt
b=$(basename "$f" .txt)
tgt=""
[ -f "$GEN_DIR/$b.target" ] && tgt=$(cat "$GEN_DIR/$b.target")
json_add_object
json_add_string "name" "$b"
json_add_string "target" "$tgt"
json_close_object
done
json_close_array
json_dump
}
delete_generated() {
local name
_load name name
local fname
fname=$(echo "$name" | tr -c 'a-zA-Z0-9._-' '_')
rm -f "$GEN_DIR/$fname.txt" "$GEN_DIR/$fname.target"
json_init; json_add_boolean "ok" 1; json_dump
}
lldp_neighbors() {
if ! command -v lldpcli >/dev/null 2>&1; then
json_init; json_add_string "error" "lldpcli not installed"; json_dump; return
fi
local out
out=$(lldpcli show neighbors 2>&1)
json_init
json_add_string "output" "$out"
json_dump
}
snmp_query() {
local ip community
_load ip ip community community
[ -z "$community" ] && community="public"
if ! command -v snmpwalk >/dev/null 2>&1; then
json_init; json_add_string "error" "snmpwalk not installed"; json_dump; return
fi
local out
out=$(snmpwalk -v2c -c "$community" -t 3 -r 1 "$ip" 1.3.6.1.2.1.17.7.1.4.3.1.2 2>&1)
json_init
json_add_string "ip" "$ip"
json_add_string "output" "$out"
json_dump
}
bridge_vlan_show() {
local out
out=$(bridge vlan show 2>&1)
json_init
json_add_string "output" "$out"
json_dump
}
iface_stats() {
local ifaces
_load ifaces ifaces
json_init
json_add_array "stats"
for i in $ifaces; do
[ -d "/sys/class/net/$i" ] || continue
local rx tx
rx=$(cat /sys/class/net/$i/statistics/rx_bytes 2>/dev/null)
tx=$(cat /sys/class/net/$i/statistics/tx_bytes 2>/dev/null)
json_add_object
json_add_string "iface" "$i"
json_add_int "rx" "${rx:-0}"
json_add_int "tx" "${tx:-0}"
json_close_object
done
json_close_array
json_dump
}
ping_test() {
local iface ip
_load iface iface ip ip
[ -z "$iface" ] && { json_init; json_add_string "error" "missing iface"; json_dump; return; }
[ -z "$ip" ] && { json_init; json_add_string "error" "missing ip"; json_dump; return; }
local out rc
out=$(ping -I "$iface" -c 3 -W 3 "$ip" 2>&1)
rc=$?
json_init
json_add_string "output" "$out"
json_add_int "exit_code" "$rc"
json_dump
}
call_method() {
local method="$1"
case "$method" in
detect) detect ;;
preview) preview ;;
apply) apply ;;
snapshot) snapshot ;;
list_snapshots) list_snapshots ;;
restore) restore ;;
confirm) confirm ;;
generate_cli) generate_cli ;;
save_template) save_template ;;
list_templates) list_templates ;;
load_template) load_template ;;
delete_template) delete_template ;;
save_generated) save_generated ;;
list_generated) list_generated ;;
delete_generated) delete_generated ;;
lldp_neighbors) lldp_neighbors ;;
snmp_query) snmp_query ;;
bridge_vlan_show) bridge_vlan_show ;;
kernel_ifaces) kernel_ifaces ;;
iface_stats) iface_stats ;;
ping_test) ping_test ;;
*) cat >/dev/null
json_init
json_add_string "error" "unknown_method"
json_dump ;;
esac
}
case "$1" in
list) list_methods ;;
call) call_method "$2" ;;
*) echo '{"error":"usage"}' ;;
esac