最後活躍 1 month ago

修訂 a7597076ebf8483495216eeec42cfa775d39c9ee

export.command.sh 原始檔案
1#!/usr/bin/env bash
2# commands/export.command.sh
3
4function cmd::export::on_load() {
5 flag::register --peer
6 flag::register --identity
7 flag::register --all
8 flag::register --out
9 flag::register --conf-only
10 flag::register --meta-only
11 flag::register --no-config
12 flag::register --no-peers
13 flag::register --force
14}
15
16function cmd::export::help() {
17 cat <<EOF
18Usage: wgctl export [options]
19
20Export wgctl data as a portable JSON bundle.
21
22Options:
23 --peer <name> Export a single peer (conf, meta, groups, identity, blocks)
24 --identity <name> Export an identity
25 --all Full backup (all peers, rules, identities, groups, etc.)
26 --out <file> Write to file instead of stdout
27 --conf-only Export peer conf only (with --peer)
28 --meta-only Export peer meta only (with --peer)
29 --no-config Skip wgctl.json (with --all)
30 --no-peers Skip peer confs (with --all)
31 --force Overwrite existing output file
32
33Examples:
34 wgctl export --peer phone-nuno
35 wgctl export --peer phone-nuno --out phone-nuno.json
36 wgctl export --identity nuno --out nuno.json
37 wgctl export --all --out backup.json
38 wgctl export --all --no-config --out data-only.json
39EOF
40}
41
42function cmd::export::run() {
43 local peer="" identity="" all=false out=""
44 local conf_only=false meta_only=false
45 local no_config=false no_peers=false force=false
46
47 while [[ $# -gt 0 ]]; do
48 case "$1" in
49 --peer) peer="$2"; shift 2 ;;
50 --identity) identity="$2"; shift 2 ;;
51 --all) all=true; shift ;;
52 --out) out="$2"; shift 2 ;;
53 --conf-only) conf_only=true; shift ;;
54 --meta-only) meta_only=true; shift ;;
55 --no-config) no_config=true; shift ;;
56 --no-peers) no_peers=true; shift ;;
57 --force) force=true; shift ;;
58 --help) cmd::export::help; return ;;
59 *) log::error "Unknown flag: $1"; return 1 ;;
60 esac
61 done
62
63 # Validate
64 local mode_count=0
65 [[ -n "$peer" ]] && (( mode_count++ )) || true
66 [[ -n "$identity" ]] && (( mode_count++ )) || true
67 $all && (( mode_count++ )) || true
68
69 if [[ "$mode_count" -eq 0 ]]; then
70 log::error "Specify --peer, --identity, or --all"
71 cmd::export::help
72 return 1
73 fi
74 if [[ "$mode_count" -gt 1 ]]; then
75 log::error "Only one of --peer, --identity, --all can be used at a time"
76 return 1
77 fi
78
79 # Check output file
80 if [[ -n "$out" && -f "$out" && ! $force ]]; then
81 log::error "Output file already exists: ${out} (use --force to overwrite)"
82 return 1
83 fi
84
85 local json=""
86 if [[ -n "$peer" ]]; then
87 json=$(cmd::export::_peer "$peer" "$conf_only" "$meta_only") || return 1
88 elif [[ -n "$identity" ]]; then
89 json=$(cmd::export::_identity "$identity") || return 1
90 elif $all; then
91 json=$(cmd::export::_full "$no_config" "$no_peers") || return 1
92 fi
93
94 if [[ -n "$out" ]]; then
95 echo "$json" > "$out"
96 log::wg_success "Exported to ${out}"
97 else
98 echo "$json"
99 fi
100}
101
102# ======================================================
103# Peer export
104# ======================================================
105
106function cmd::export::_peer() {
107 local name="${1:-}" conf_only="${2:-false}" meta_only="${3:-false}"
108
109 peers::require_exists "$name" || return 1
110
111 local conf_file
112 conf_file="$(ctx::clients)/${name}.conf"
113 [[ ! -f "$conf_file" ]] && log::error "Client conf not found: ${conf_file}" && return 1
114
115 local conf_b64
116 conf_b64=$(base64 -w 0 < "$conf_file" 2>/dev/null || base64 < "$conf_file")
117
118 if $conf_only; then
119 cmd::export::_envelope "peer_conf" \
120 "$(printf '{"name":"%s","conf":"%s"}' "$name" "$conf_b64")"
121 return 0
122 fi
123
124 # Meta
125 local meta_file meta_json="{}"
126 meta_file="$(ctx::meta)/${name}.meta"
127 [[ -f "$meta_file" ]] && meta_json=$(cat "$meta_file")
128
129 if $meta_only; then
130 cmd::export::_envelope "peer_meta" \
131 "$(printf '{"name":"%s","meta":%s}' "$name" "$meta_json")"
132 return 0
133 fi
134
135 # Public key
136 local public_key=""
137 local key_file
138 key_file="$(ctx::clients)/${name}_public.key"
139 [[ -f "$key_file" ]] && public_key=$(cat "$key_file")
140
141 # IP
142 local ip
143 ip=$(peers::get_ip "$name")
144
145 # Type
146 local peer_type
147 peer_type=$(peers::get_type "$name" 2>/dev/null || echo "")
148
149 # Direct rule
150 local direct_rule
151 direct_rule=$(peers::get_meta "$name" "rule" 2>/dev/null || echo "")
152
153 # Identity
154 local identity
155 identity=$(peers::get_identity "$name" 2>/dev/null || echo "")
156
157 # Groups
158 local -a group_list=()
159 while IFS= read -r g; do
160 [[ -n "$g" ]] && group_list+=("\"$g\"")
161 done < <(json::peer_groups "$(ctx::groups)" "$name" 2>/dev/null)
162 local groups_json="[]"
163 [[ ${#group_list[@]} -gt 0 ]] && \
164 groups_json="[$(printf '%s,' "${group_list[@]}" | sed 's/,$//')]"
165
166 # Blocks
167 local block_file is_blocked="false" block_json="null"
168 block_file="$(ctx::blocks)/${name}.block"
169 if [[ -f "$block_file" ]]; then
170 is_blocked="true"
171 block_json=$(base64 -w 0 < "$block_file" 2>/dev/null || base64 < "$block_file")
172 block_json="\"${block_json}\""
173 fi
174
175 local peer_data
176 peer_data=$(printf \
177 '{"name":"%s","ip":"%s","type":"%s","public_key":"%s","conf":"%s","meta":%s,"identity":"%s","groups":%s,"direct_rule":"%s","blocks":{"is_blocked":%s,"block_file":%s}}' \
178 "$name" "$ip" "$peer_type" "$public_key" "$conf_b64" \
179 "$meta_json" "$identity" "$groups_json" "$direct_rule" \
180 "$is_blocked" "$block_json")
181
182 cmd::export::_envelope "peer" "$peer_data"
183}
184
185# ======================================================
186# Identity export
187# ======================================================
188
189function cmd::export::_identity() {
190 local name="${1:-}"
191 identity::require_exists "$name" || return 1
192
193 local id_file
194 id_file="$(ctx::identities)/${name}.identity"
195 local id_json
196 id_json=$(cat "$id_file")
197
198 cmd::export::_envelope "identity" \
199 "$(printf '{"name":"%s","identity":%s}' "$name" "$id_json")"
200}
201
202# ======================================================
203# Full backup
204# ======================================================
205
206function cmd::export::_full() {
207 local no_config="${1:-false}" no_peers="${2:-false}"
208 local version
209 version=$(wgctl::version 2>/dev/null || echo "unknown")
210
211 python3 "$(ctx::json_helper)" export_full \
212 "$(ctx::clients)" \
213 "$(ctx::meta)" \
214 "$(ctx::rules)" \
215 "$(ctx::identities)" \
216 "$(ctx::groups)" \
217 "$(ctx::blocks)" \
218 "$(ctx::block_history)" \
219 "$(ctx::config_file)" \
220 "$(ctx::policies)" \
221 "$(ctx::subnets)" \
222 "$(ctx::net)" \
223 "$(ctx::hosts)" \
224 "$no_config" \
225 "$no_peers" \
226 "$version" \
227 2>/dev/null
228}
229
230# Helper — peer data without envelope (used by full backup)
231function cmd::export::_peer_data() {
232 local name="${1:-}"
233 local conf_file
234 conf_file="$(ctx::clients)/${name}.conf"
235 [[ ! -f "$conf_file" ]] && return 0
236
237 local conf_b64
238 conf_b64=$(base64 -w 0 < "$conf_file" 2>/dev/null || base64 < "$conf_file")
239
240 local meta_file meta_json="{}"
241 meta_file="$(ctx::meta)/${name}.meta"
242 [[ -f "$meta_file" ]] && meta_json=$(cat "$meta_file")
243
244 local public_key=""
245 local key_file
246 key_file="$(ctx::clients)/${name}_public.key"
247 [[ -f "$key_file" ]] && public_key=$(cat "$key_file")
248
249 local ip
250 ip=$(peers::get_ip "$name")
251
252 local peer_type
253 peer_type=$(peers::get_type "$name" 2>/dev/null || echo "")
254
255 local direct_rule
256 direct_rule=$(peers::get_meta "$name" "rule" 2>/dev/null || echo "")
257
258 local identity
259 identity=$(peers::get_identity "$name" 2>/dev/null || echo "")
260
261 local -a group_list=()
262 while IFS= read -r g; do
263 [[ -n "$g" ]] && group_list+=("\"$g\"")
264 done < <(json::peer_groups "$(ctx::groups)" "$name" 2>/dev/null)
265 local groups_json="[]"
266 [[ ${#group_list[@]} -gt 0 ]] && \
267 groups_json="[$(printf '%s,' "${group_list[@]}" | sed 's/,$//')]"
268
269 local block_file is_blocked="false" block_json="null"
270 block_file="$(ctx::blocks)/${name}.block"
271 if [[ -f "$block_file" ]]; then
272 is_blocked="true"
273 block_json="\"$(base64 -w 0 < "$block_file" 2>/dev/null || base64 < "$block_file")\""
274 fi
275
276 printf \
277 '{"name":"%s","ip":"%s","type":"%s","public_key":"%s","conf":"%s","meta":%s,"identity":"%s","groups":%s,"direct_rule":"%s","blocks":{"is_blocked":%s,"block_file":%s}}' \
278 "$name" "$ip" "$peer_type" "$public_key" "$conf_b64" \
279 "$meta_json" "$identity" "$groups_json" "$direct_rule" \
280 "$is_blocked" "$block_json"
281}
282
283# ======================================================
284# Envelope helper
285# ======================================================
286
287function cmd::export::_envelope() {
288 local export_type="${1:-}" data="${2:-}"
289 local version ts
290 version=$(wgctl::version 2>/dev/null || echo "unknown")
291 ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
292 printf '{"wgctl_version":"%s","export_type":"%s","exported_at":"%s","data":%s}\n' \
293 "$version" "$export_type" "$ts" "$data"
294}
295
296function cmd::export::_compact_json() {
297 local file="$1"
298 python3 -c "
299import json, sys
300try:
301 print(json.dumps(json.load(open('${file}'))))
302except Exception as e:
303 print('{}', file=sys.stderr)
304" 2>/dev/null
305}