wgctl/commands/subnet.command.sh
Nuno Duque Nunes 8bb1de4976 init feature
2026-05-19 15:26:31 +00:00

272 lines
No EOL
8.2 KiB
Bash

#!/usr/bin/env bash
# subnet.command.sh — manage the subnet map (subnets.json)
#
# Subcommands:
# wgctl subnet list
# wgctl subnet show --name <name>
# wgctl subnet add --name <name> --subnet <cidr> [--type <type>]
# [--tunnel-mode split|full] [--desc <desc>]
# [--group <parent>]
# wgctl subnet rm --name <name>
# wgctl subnet rename --name <old> --new-name <new>
# ============================================
# Lifecycle
# ============================================
function cmd::subnet::on_load() {
flag::register --name
flag::register --subnet
flag::register --type
flag::register --tunnel-mode
flag::register --desc
flag::register --group
flag::register --new-name
}
# ============================================
# Help
# ============================================
function cmd::subnet::help() {
cat <<EOF
Usage: wgctl subnet <subcommand> [options]
Manage the subnet map.
Subcommands:
list List all configured subnets
show --name <name> Show details for a subnet
add --name <name> Add a new subnet entry
--subnet <cidr>
[--type <type>]
[--tunnel-mode split|full]
[--desc <desc>]
[--group <parent>]
rm --name <name> Remove a subnet (refused if in use)
rename --name <old> Rename a subnet (refused if in use)
--new-name <new>
Examples:
wgctl subnet list
wgctl subnet show --name guests
wgctl subnet add --name iot-cctv --subnet 10.1.211.0/24 --type iot
wgctl subnet add --name desktop --subnet 10.1.101.0/24 --group guests
wgctl subnet rm --name iot-cctv
wgctl subnet rename --name iot-cctv --new-name cctv
EOF
}
# ============================================
# Run
# ============================================
function cmd::subnet::run() {
local subcmd="${1:-list}"
shift || true
case "$subcmd" in
list) cmd::subnet::_list "$@" ;;
show) cmd::subnet::_show "$@" ;;
add) cmd::subnet::_add "$@" ;;
rm) cmd::subnet::_rm "$@" ;;
rename) cmd::subnet::_rename "$@" ;;
--help) cmd::subnet::help ;;
*)
log::error "Unknown subcommand '${subcmd}'. Available: list, show, add, rm, rename"
return 1
;;
esac
}
# ============================================
# Subcommands
# ============================================
function cmd::subnet::_list() {
local data
data=$(subnet::list_data)
if [[ -z "$data" ]]; then
log::info "No subnets defined."
return 0
fi
ui::subnet::header
local prev_group=""
while IFS='|' read -r display_name subnet type_key tunnel_mode desc is_group group_parent; do
cmd::subnet::_maybe_group_separator "$is_group" "$group_parent" "$prev_group"
prev_group=$(cmd::subnet::_update_prev_group "$is_group" "$group_parent" "$prev_group")
ui::subnet::row "$display_name" "$subnet" "$type_key" "$tunnel_mode" "$desc" "$is_group"
done <<< "$data"
}
function cmd::subnet::_maybe_group_separator() {
local is_group="${1:-}" group_parent="${2:-}" prev_group="${3:-}"
if [[ "$is_group" == "true" && "$group_parent" != "$prev_group" && -n "$prev_group" ]]; then
ui::subnet::group_separator
elif [[ "$is_group" == "false" && -n "$prev_group" ]]; then
ui::subnet::group_separator
fi
}
function cmd::subnet::_update_prev_group() {
local is_group="${1:-}" group_parent="${2:-}" prev_group="${3:-}"
if [[ "$is_group" == "true" ]]; then
echo "$group_parent"
else
echo ""
fi
}
function cmd::subnet::_show() {
local name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--help) cmd::subnet::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
subnet::require_exists "$name" || return 1
local data
data=$(subnet::show_data "$name")
local is_group="false"
while IFS='|' read -r key val rest; do
case "$key" in
name)
ui::subnet::detail "$val" "$is_group"
;;
is_group)
is_group="$val"
ui::subnet::detail_field "Type" "$( [[ $val == true ]] && echo "group" || echo "scalar" )"
[[ "$val" == "true" ]] && ui::subnet::child_header
;;
subnet) ui::subnet::detail_field "Subnet" "$val" ;;
type) ui::subnet::detail_field "Device type" "$val" ;;
tunnel_mode) ui::subnet::detail_field "Tunnel" "$val" ;;
desc) ui::subnet::detail_field "Description" "$val" ;;
child)
local c_type="$val"
local c_subnet c_tunnel c_desc
c_subnet=$(echo "$rest" | cut -d'|' -f1)
c_tunnel=$(echo "$rest" | cut -d'|' -f2)
c_desc=$(echo "$rest" | cut -d'|' -f3)
ui::subnet::child_row "$c_type" "$c_subnet" "$c_tunnel" "$c_desc"
;;
esac
done <<< "$data"
local peers_using
peers_using=$(subnet::peers_using "$name")
ui::subnet::peers_in_use "$peers_using"
}
function cmd::subnet::_add() {
local name="" cidr="" type_key="" tunnel_mode="split" desc="" group_parent=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--subnet) cidr="$2"; shift 2 ;;
--type) type_key="$2"; shift 2 ;;
--tunnel-mode) tunnel_mode="$2"; shift 2 ;;
--desc) desc="$2"; shift 2 ;;
--group) group_parent="$2"; shift 2 ;;
--help) cmd::subnet::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
[[ -z "$cidr" ]] && { log::error "Missing required flag: --subnet"; return 1; }
cmd::subnet::_validate_tunnel_mode "$tunnel_mode" || return 1
cmd::subnet::_validate_cidr "$cidr" || return 1
json::subnet_add "$(ctx::subnets)" "$name" "$cidr" \
"${type_key:-$name}" "$tunnel_mode" "$desc" "$group_parent"
log::ok "Subnet '${name}' added (${cidr})"
}
function cmd::subnet::_rm() {
local name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--help) cmd::subnet::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
subnet::require_exists "$name" || return 1
local peers_using
peers_using=$(subnet::peers_using "$name")
if [[ -n "$peers_using" ]]; then
log::error "Cannot remove subnet '${name}' — in use by: ${peers_using//,/, }"
log::error "Migrate or remove those peers first."
return 1
fi
json::subnet_remove "$(ctx::subnets)" "$name" ""
log::ok "Subnet '${name}' removed"
}
function cmd::subnet::_rename() {
local name="" new_name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--new-name) new_name="$2"; shift 2 ;;
--help) cmd::subnet::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
[[ -z "$new_name" ]] && { log::error "Missing required flag: --new-name"; return 1; }
subnet::require_exists "$name" || return 1
local peers_using
peers_using=$(subnet::peers_using "$name")
if [[ -n "$peers_using" ]]; then
log::error "Cannot rename subnet '${name}' — configs already distributed to: ${peers_using//,/, }"
log::error "Client configs reference the subnet CIDR which cannot change after distribution."
return 1
fi
json::subnet_rename "$(ctx::subnets)" "$name" "$new_name" ""
log::ok "Subnet '${name}' renamed to '${new_name}'"
}
# ============================================
# Validation Helpers
# ============================================
function cmd::subnet::_validate_tunnel_mode() {
local mode="${1:-}"
case "$mode" in
split|full) return 0 ;;
*)
log::error "Invalid --tunnel-mode '${mode}'. Use: split, full"
return 1
;;
esac
}
function cmd::subnet::_validate_cidr() {
local cidr="${1:-}"
if ! echo "$cidr" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$'; then
log::error "Invalid CIDR format: '${cidr}'"
return 1
fi
}