nuno revised this gist 1 month ago. Go to revision
1 file changed, 340 insertions
subnet.command.sh(file created)
| @@ -0,0 +1,340 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # subnet.command.sh — manage the subnet map (subnets.json) | |
| 3 | + | # | |
| 4 | + | # Subcommands: | |
| 5 | + | # wgctl subnet list | |
| 6 | + | # wgctl subnet show --name <name> | |
| 7 | + | # wgctl subnet add --name <name> --subnet <cidr> [--type <type>] | |
| 8 | + | # [--tunnel-mode split|full] [--desc <desc>] | |
| 9 | + | # [--group <parent>] | |
| 10 | + | # wgctl subnet rm --name <name> | |
| 11 | + | # wgctl subnet rename --name <old> --new-name <new> | |
| 12 | + | ||
| 13 | + | # ============================================ | |
| 14 | + | # Lifecycle | |
| 15 | + | # ============================================ | |
| 16 | + | ||
| 17 | + | function cmd::subnet::on_load() { | |
| 18 | + | flag::register --name | |
| 19 | + | flag::register --subnet | |
| 20 | + | flag::register --type | |
| 21 | + | flag::register --tunnel-mode | |
| 22 | + | flag::register --desc | |
| 23 | + | flag::register --group | |
| 24 | + | flag::register --new-name | |
| 25 | + | ||
| 26 | + | command::mixin json_output | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | # ============================================ | |
| 30 | + | # Help | |
| 31 | + | # ============================================ | |
| 32 | + | ||
| 33 | + | function cmd::subnet::help() { | |
| 34 | + | cat <<EOF | |
| 35 | + | Usage: wgctl subnet <subcommand> [options] | |
| 36 | + | ||
| 37 | + | Manage the subnet map. | |
| 38 | + | ||
| 39 | + | Subcommands: | |
| 40 | + | list List all configured subnets | |
| 41 | + | show --name <name> Show details for a subnet | |
| 42 | + | add --name <name> Add a new subnet entry | |
| 43 | + | --subnet <cidr> | |
| 44 | + | [--type <type>] | |
| 45 | + | [--tunnel-mode split|full] | |
| 46 | + | [--desc <desc>] | |
| 47 | + | [--group <parent>] | |
| 48 | + | rm --name <name> Remove a subnet (refused if in use) | |
| 49 | + | rename --name <old> Rename a subnet (refused if in use) | |
| 50 | + | --new-name <new> | |
| 51 | + | ||
| 52 | + | Examples: | |
| 53 | + | wgctl subnet list | |
| 54 | + | wgctl subnet show --name guests | |
| 55 | + | wgctl subnet add --name iot-cctv --subnet 10.1.211.0/24 --type iot | |
| 56 | + | wgctl subnet add --name desktop --subnet 10.1.101.0/24 --group guests | |
| 57 | + | wgctl subnet rm --name iot-cctv | |
| 58 | + | wgctl subnet rename --name iot-cctv --new-name cctv | |
| 59 | + | EOF | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | # ============================================ | |
| 63 | + | # Run | |
| 64 | + | # ============================================ | |
| 65 | + | ||
| 66 | + | function cmd::subnet::run() { | |
| 67 | + | local subcmd="${1:-list}" | |
| 68 | + | shift || true | |
| 69 | + | ||
| 70 | + | if command::json; then | |
| 71 | + | cmd::subnet::_output_json | |
| 72 | + | return 0 | |
| 73 | + | fi | |
| 74 | + | ||
| 75 | + | case "$subcmd" in | |
| 76 | + | list) cmd::subnet::_list "$@" ;; | |
| 77 | + | show) cmd::subnet::_show "$@" ;; | |
| 78 | + | add) cmd::subnet::_add "$@" ;; | |
| 79 | + | rm) cmd::subnet::_rm "$@" ;; | |
| 80 | + | rename) cmd::subnet::_rename "$@" ;; | |
| 81 | + | --help) cmd::subnet::help ;; | |
| 82 | + | *) | |
| 83 | + | log::error "Unknown subcommand '${subcmd}'. Available: list, show, add, rm, rename" | |
| 84 | + | return 1 | |
| 85 | + | ;; | |
| 86 | + | esac | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | # ============================================ | |
| 90 | + | # Subcommands | |
| 91 | + | # ============================================ | |
| 92 | + | ||
| 93 | + | function cmd::subnet::_list() { | |
| 94 | + | local data | |
| 95 | + | data=$(subnet::list_data | ui::sort_rows 1) | |
| 96 | + | ||
| 97 | + | if [[ -z "$data" ]]; then | |
| 98 | + | log::info "No subnets defined." | |
| 99 | + | return 0 | |
| 100 | + | fi | |
| 101 | + | ||
| 102 | + | if display::is_table "subnet_list"; then | |
| 103 | + | cmd::subnet::_render_table "$data" | |
| 104 | + | return 0 | |
| 105 | + | fi | |
| 106 | + | ||
| 107 | + | echo "" | |
| 108 | + | local prev_group="" | |
| 109 | + | while IFS='|' read -r display_name subnet type_key tunnel_mode desc is_group group_parent; do | |
| 110 | + | if [[ "$is_group" == "true" ]]; then | |
| 111 | + | # Print group parent header when we encounter first child | |
| 112 | + | if [[ "$group_parent" != "$prev_group" ]]; then | |
| 113 | + | [[ -n "$prev_group" ]] && ui::subnet::group_separator | |
| 114 | + | ui::subnet::row_group_parent "$group_parent" | |
| 115 | + | prev_group="$group_parent" | |
| 116 | + | fi | |
| 117 | + | ui::subnet::row_group_child "$type_key" "$subnet" "$tunnel_mode" | |
| 118 | + | else | |
| 119 | + | # Scalar entry | |
| 120 | + | [[ -n "$prev_group" ]] && ui::subnet::group_separator | |
| 121 | + | prev_group="" | |
| 122 | + | ui::subnet::row_scalar "$display_name" "$subnet" "$tunnel_mode" | |
| 123 | + | fi | |
| 124 | + | done <<< "$data" | |
| 125 | + | echo "" | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | function cmd::subnet::_render_table() { | |
| 129 | + | local data="${1:-}" | |
| 130 | + | [[ -z "$data" ]] && return 0 | |
| 131 | + | ||
| 132 | + | ui::subnet::list_header_table | |
| 133 | + | while IFS='|' read -r type cidr display_name tunnel desc is_group group_parent; do | |
| 134 | + | [[ -z "$type" ]] && continue | |
| 135 | + | ui::subnet::list_row_table "$type" "$cidr" "$tunnel" "$desc" | |
| 136 | + | done <<< "$data" | |
| 137 | + | printf "\n" | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | function cmd::subnet::_maybe_group_separator() { | |
| 141 | + | local is_group="${1:-}" group_parent="${2:-}" prev_group="${3:-}" | |
| 142 | + | if [[ "$is_group" == "true" && "$group_parent" != "$prev_group" && -n "$prev_group" ]]; then | |
| 143 | + | ui::subnet::group_separator | |
| 144 | + | elif [[ "$is_group" == "false" && -n "$prev_group" ]]; then | |
| 145 | + | ui::subnet::group_separator | |
| 146 | + | fi | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | function cmd::subnet::_update_prev_group() { | |
| 150 | + | local is_group="${1:-}" group_parent="${2:-}" prev_group="${3:-}" | |
| 151 | + | if [[ "$is_group" == "true" ]]; then | |
| 152 | + | echo "$group_parent" | |
| 153 | + | else | |
| 154 | + | echo "" | |
| 155 | + | fi | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | function cmd::subnet::_show() { | |
| 159 | + | local name="" | |
| 160 | + | while [[ $# -gt 0 ]]; do | |
| 161 | + | case "$1" in | |
| 162 | + | --name) name="$2"; shift 2 ;; | |
| 163 | + | --help) cmd::subnet::help; return ;; | |
| 164 | + | *) log::error "Unknown flag: $1"; return 1 ;; | |
| 165 | + | esac | |
| 166 | + | done | |
| 167 | + | ||
| 168 | + | [[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; } | |
| 169 | + | subnet::require_exists "$name" || return 1 | |
| 170 | + | ||
| 171 | + | local data | |
| 172 | + | data=$(subnet::show_data "$name") | |
| 173 | + | ||
| 174 | + | local is_group="false" | |
| 175 | + | local show_name="" show_subnet="" show_tunnel="" show_desc="" | |
| 176 | + | ||
| 177 | + | while IFS='|' read -r key val rest; do | |
| 178 | + | case "$key" in | |
| 179 | + | name) show_name="$val" ;; | |
| 180 | + | is_group) is_group="$val" ;; | |
| 181 | + | subnet) show_subnet="$val" ;; | |
| 182 | + | tunnel_mode) show_tunnel="$val" ;; | |
| 183 | + | desc) show_desc="$val" ;; | |
| 184 | + | esac | |
| 185 | + | done <<< "$data" | |
| 186 | + | ||
| 187 | + | if [[ "$is_group" == "true" ]]; then | |
| 188 | + | # Group display | |
| 189 | + | ui::subnet::show_group "$show_name" | |
| 190 | + | ||
| 191 | + | while IFS='|' read -r key val rest; do | |
| 192 | + | [[ "$key" != "child" ]] && continue | |
| 193 | + | local c_type="$val" | |
| 194 | + | local c_subnet c_tunnel c_desc | |
| 195 | + | c_subnet=$(echo "$rest" | cut -d'|' -f1) | |
| 196 | + | c_tunnel=$(echo "$rest" | cut -d'|' -f2) | |
| 197 | + | c_desc=$(echo "$rest" | cut -d'|' -f3) | |
| 198 | + | ui::subnet::show_child_row "$c_type" "$c_subnet" "$c_tunnel" "$c_desc" | |
| 199 | + | done <<< "$data" | |
| 200 | + | ||
| 201 | + | local peers_using | |
| 202 | + | peers_using=$(subnet::peers_using "$name") | |
| 203 | + | ui::subnet::show_peers_annotated "$peers_using" "$(ctx::subnets)" | |
| 204 | + | else | |
| 205 | + | # Scalar display | |
| 206 | + | ui::subnet::show_scalar "$show_name" "$show_subnet" "$show_tunnel" "$show_desc" | |
| 207 | + | ||
| 208 | + | local peers_using | |
| 209 | + | peers_using=$(subnet::peers_using "$name") | |
| 210 | + | ui::subnet::show_peers "$peers_using" | |
| 211 | + | fi | |
| 212 | + | ||
| 213 | + | echo "" | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | function cmd::subnet::_add() { | |
| 217 | + | local name="" cidr="" type_key="" tunnel_mode="split" desc="" group_parent="" | |
| 218 | + | ||
| 219 | + | while [[ $# -gt 0 ]]; do | |
| 220 | + | case "$1" in | |
| 221 | + | --name) name="$2"; shift 2 ;; | |
| 222 | + | --subnet) cidr="$2"; shift 2 ;; | |
| 223 | + | --type) type_key="$2"; shift 2 ;; | |
| 224 | + | --tunnel-mode) tunnel_mode="$2"; shift 2 ;; | |
| 225 | + | --desc) desc="$2"; shift 2 ;; | |
| 226 | + | --group) group_parent="$2"; shift 2 ;; | |
| 227 | + | --help) cmd::subnet::help; return ;; | |
| 228 | + | *) log::error "Unknown flag: $1"; return 1 ;; | |
| 229 | + | esac | |
| 230 | + | done | |
| 231 | + | ||
| 232 | + | [[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; } | |
| 233 | + | [[ -z "$cidr" ]] && { log::error "Missing required flag: --subnet"; return 1; } | |
| 234 | + | ||
| 235 | + | cmd::subnet::_validate_tunnel_mode "$tunnel_mode" || return 1 | |
| 236 | + | cmd::subnet::_validate_cidr "$cidr" || return 1 | |
| 237 | + | ||
| 238 | + | json::subnet_add "$(ctx::subnets)" "$name" "$cidr" \ | |
| 239 | + | "${type_key:-$name}" "$tunnel_mode" "$desc" "$group_parent" | |
| 240 | + | ||
| 241 | + | log::ok "Subnet '${name}' added (${cidr})" | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | function cmd::subnet::_rm() { | |
| 245 | + | local name="" | |
| 246 | + | while [[ $# -gt 0 ]]; do | |
| 247 | + | case "$1" in | |
| 248 | + | --name) name="$2"; shift 2 ;; | |
| 249 | + | --help) cmd::subnet::help; return ;; | |
| 250 | + | *) log::error "Unknown flag: $1"; return 1 ;; | |
| 251 | + | esac | |
| 252 | + | done | |
| 253 | + | ||
| 254 | + | [[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; } | |
| 255 | + | subnet::require_exists "$name" || return 1 | |
| 256 | + | ||
| 257 | + | local peers_using | |
| 258 | + | peers_using=$(subnet::peers_using "$name") | |
| 259 | + | ||
| 260 | + | if [[ -n "$peers_using" ]]; then | |
| 261 | + | log::error "Cannot remove subnet '${name}' — in use by: ${peers_using//,/, }" | |
| 262 | + | log::error "Migrate or remove those peers first." | |
| 263 | + | return 1 | |
| 264 | + | fi | |
| 265 | + | ||
| 266 | + | json::subnet_remove "$(ctx::subnets)" "$name" "" | |
| 267 | + | log::ok "Subnet '${name}' removed" | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | function cmd::subnet::_rename() { | |
| 271 | + | local name="" new_name="" | |
| 272 | + | while [[ $# -gt 0 ]]; do | |
| 273 | + | case "$1" in | |
| 274 | + | --name) name="$2"; shift 2 ;; | |
| 275 | + | --new-name) new_name="$2"; shift 2 ;; | |
| 276 | + | --help) cmd::subnet::help; return ;; | |
| 277 | + | *) log::error "Unknown flag: $1"; return 1 ;; | |
| 278 | + | esac | |
| 279 | + | done | |
| 280 | + | ||
| 281 | + | [[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; } | |
| 282 | + | [[ -z "$new_name" ]] && { log::error "Missing required flag: --new-name"; return 1; } | |
| 283 | + | subnet::require_exists "$name" || return 1 | |
| 284 | + | ||
| 285 | + | local peers_using | |
| 286 | + | peers_using=$(subnet::peers_using "$name") | |
| 287 | + | if [[ -n "$peers_using" ]]; then | |
| 288 | + | log::error "Cannot rename subnet '${name}' — configs already distributed to: ${peers_using//,/, }" | |
| 289 | + | log::error "Client configs reference the subnet CIDR which cannot change after distribution." | |
| 290 | + | return 1 | |
| 291 | + | fi | |
| 292 | + | ||
| 293 | + | json::subnet_rename "$(ctx::subnets)" "$name" "$new_name" "" | |
| 294 | + | log::ok "Subnet '${name}' renamed to '${new_name}'" | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | # ============================================ | |
| 298 | + | # Validation Helpers | |
| 299 | + | # ============================================ | |
| 300 | + | ||
| 301 | + | function cmd::subnet::_validate_tunnel_mode() { | |
| 302 | + | local mode="${1:-}" | |
| 303 | + | case "$mode" in | |
| 304 | + | split|full) return 0 ;; | |
| 305 | + | *) | |
| 306 | + | log::error "Invalid --tunnel-mode '${mode}'. Use: split, full" | |
| 307 | + | return 1 | |
| 308 | + | ;; | |
| 309 | + | esac | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | function cmd::subnet::_validate_cidr() { | |
| 313 | + | local cidr="${1:-}" | |
| 314 | + | if ! echo "$cidr" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$'; then | |
| 315 | + | log::error "Invalid CIDR format: '${cidr}'" | |
| 316 | + | return 1 | |
| 317 | + | fi | |
| 318 | + | } | |
| 319 | + | ||
| 320 | + | function cmd::subnet::_output_json() { | |
| 321 | + | local data | |
| 322 | + | data=$(subnet::list_data 2>/dev/null) | |
| 323 | + | ||
| 324 | + | local -a subnets=() | |
| 325 | + | while IFS='|' read -r type cidr display_name tunnel_mode desc is_group group_parent; do | |
| 326 | + | [[ -z "$type" ]] && continue | |
| 327 | + | ||
| 328 | + | local is_group_json="false" | |
| 329 | + | [[ "$is_group" == "true" ]] && is_group_json="true" | |
| 330 | + | ||
| 331 | + | subnets+=("$(printf '{"type":"%s","cidr":"%s","display_name":"%s","tunnel_mode":"%s","desc":"%s","is_group":%s,"group_parent":"%s"}' \ | |
| 332 | + | "$type" "$cidr" "$display_name" "$tunnel_mode" \ | |
| 333 | + | "$desc" "$is_group_json" "${group_parent:-}")") | |
| 334 | + | done <<< "$data" | |
| 335 | + | ||
| 336 | + | local count=${#subnets[@]} | |
| 337 | + | local array | |
| 338 | + | array=$(printf '%s\n' "${subnets[@]:-}" | paste -sd ',' -) | |
| 339 | + | printf '{"subnets":[%s]}' "${array:-}" | json::envelope "subnet list" "$count" | |
| 340 | + | } | |
Newer
Older