#!/usr/bin/env bash # subnet.command.sh — manage the subnet map (subnets.json) # # Subcommands: # wgctl subnet list # wgctl subnet show --name # wgctl subnet add --name --subnet [--type ] # [--tunnel-mode split|full] [--desc ] # [--group ] # wgctl subnet rm --name # wgctl subnet rename --name --new-name # ============================================ # 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 < [options] Manage the subnet map. Subcommands: list List all configured subnets show --name Show details for a subnet add --name Add a new subnet entry --subnet [--type ] [--tunnel-mode split|full] [--desc ] [--group ] rm --name Remove a subnet (refused if in use) rename --name Rename a subnet (refused if in use) --new-name 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 echo "" local prev_group="" while IFS='|' read -r display_name subnet type_key tunnel_mode desc is_group group_parent; do if [[ "$is_group" == "true" ]]; then # Print group parent header when we encounter first child if [[ "$group_parent" != "$prev_group" ]]; then [[ -n "$prev_group" ]] && ui::subnet::group_separator ui::subnet::row_group_parent "$group_parent" prev_group="$group_parent" fi ui::subnet::row_group_child "$type_key" "$subnet" "$tunnel_mode" else # Scalar entry [[ -n "$prev_group" ]] && ui::subnet::group_separator prev_group="" ui::subnet::row_scalar "$display_name" "$subnet" "$tunnel_mode" fi done <<< "$data" echo "" } 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" local show_name="" show_subnet="" show_tunnel="" show_desc="" while IFS='|' read -r key val rest; do case "$key" in name) show_name="$val" ;; is_group) is_group="$val" ;; subnet) show_subnet="$val" ;; tunnel_mode) show_tunnel="$val" ;; desc) show_desc="$val" ;; esac done <<< "$data" if [[ "$is_group" == "true" ]]; then # Group display ui::subnet::show_group "$show_name" while IFS='|' read -r key val rest; do [[ "$key" != "child" ]] && continue 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::show_child_row "$c_type" "$c_subnet" "$c_tunnel" "$c_desc" done <<< "$data" local peers_using peers_using=$(subnet::peers_using "$name") ui::subnet::show_peers_annotated "$peers_using" "$(ctx::subnets)" else # Scalar display ui::subnet::show_scalar "$show_name" "$show_subnet" "$show_tunnel" "$show_desc" local peers_using peers_using=$(subnet::peers_using "$name") ui::subnet::show_peers "$peers_using" fi echo "" } 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 }