Naposledy aktivní 1 month ago

Revize fd959b1dbdfd2671abbec1522eedffc44ff5082f

subnet.command.sh Raw
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
17function 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
33function cmd::subnet::help() {
34 cat <<EOF
35Usage: wgctl subnet <subcommand> [options]
36
37Manage the subnet map.
38
39Subcommands:
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
52Examples:
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
59EOF
60}
61
62# ============================================
63# Run
64# ============================================
65
66function 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
93function 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
128function 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
140function 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
149function 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
158function 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
216function 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
244function 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
270function 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
301function 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
312function 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
320function 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}