- export --peer: single peer export with conf/meta/identity/groups/blocks - export --identity: identity export - export --all: full backup with all sections - export --conf-only, --meta-only: selective peer export - export --no-config, --no-peers: selective full export - export --out: write to file - json_helper: export_full(), _export_peer_data() Python functions - base64 encoding for conf and block files - valid JSON output via Python for full backup
304 lines
No EOL
8.9 KiB
Bash
304 lines
No EOL
8.9 KiB
Bash
#!/usr/bin/env bash
|
|
# commands/export.command.sh
|
|
|
|
function cmd::export::on_load() {
|
|
flag::register --peer
|
|
flag::register --identity
|
|
flag::register --all
|
|
flag::register --out
|
|
flag::register --conf-only
|
|
flag::register --meta-only
|
|
flag::register --no-config
|
|
flag::register --no-peers
|
|
flag::register --force
|
|
}
|
|
|
|
function cmd::export::help() {
|
|
cat <<EOF
|
|
Usage: wgctl export [options]
|
|
|
|
Export wgctl data as a portable JSON bundle.
|
|
|
|
Options:
|
|
--peer <name> Export a single peer (conf, meta, groups, identity, blocks)
|
|
--identity <name> Export an identity
|
|
--all Full backup (all peers, rules, identities, groups, etc.)
|
|
--out <file> Write to file instead of stdout
|
|
--conf-only Export peer conf only (with --peer)
|
|
--meta-only Export peer meta only (with --peer)
|
|
--no-config Skip wgctl.json (with --all)
|
|
--no-peers Skip peer confs (with --all)
|
|
--force Overwrite existing output file
|
|
|
|
Examples:
|
|
wgctl export --peer phone-nuno
|
|
wgctl export --peer phone-nuno --out phone-nuno.json
|
|
wgctl export --identity nuno --out nuno.json
|
|
wgctl export --all --out backup.json
|
|
wgctl export --all --no-config --out data-only.json
|
|
EOF
|
|
}
|
|
|
|
function cmd::export::run() {
|
|
local peer="" identity="" all=false out=""
|
|
local conf_only=false meta_only=false
|
|
local no_config=false no_peers=false force=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--peer) peer="$2"; shift 2 ;;
|
|
--identity) identity="$2"; shift 2 ;;
|
|
--all) all=true; shift ;;
|
|
--out) out="$2"; shift 2 ;;
|
|
--conf-only) conf_only=true; shift ;;
|
|
--meta-only) meta_only=true; shift ;;
|
|
--no-config) no_config=true; shift ;;
|
|
--no-peers) no_peers=true; shift ;;
|
|
--force) force=true; shift ;;
|
|
--help) cmd::export::help; return ;;
|
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
|
esac
|
|
done
|
|
|
|
# Validate
|
|
local mode_count=0
|
|
[[ -n "$peer" ]] && (( mode_count++ )) || true
|
|
[[ -n "$identity" ]] && (( mode_count++ )) || true
|
|
$all && (( mode_count++ )) || true
|
|
|
|
if [[ "$mode_count" -eq 0 ]]; then
|
|
log::error "Specify --peer, --identity, or --all"
|
|
cmd::export::help
|
|
return 1
|
|
fi
|
|
if [[ "$mode_count" -gt 1 ]]; then
|
|
log::error "Only one of --peer, --identity, --all can be used at a time"
|
|
return 1
|
|
fi
|
|
|
|
# Check output file
|
|
if [[ -n "$out" && -f "$out" && ! $force ]]; then
|
|
log::error "Output file already exists: ${out} (use --force to overwrite)"
|
|
return 1
|
|
fi
|
|
|
|
local json=""
|
|
if [[ -n "$peer" ]]; then
|
|
json=$(cmd::export::_peer "$peer" "$conf_only" "$meta_only") || return 1
|
|
elif [[ -n "$identity" ]]; then
|
|
json=$(cmd::export::_identity "$identity") || return 1
|
|
elif $all; then
|
|
json=$(cmd::export::_full "$no_config" "$no_peers") || return 1
|
|
fi
|
|
|
|
if [[ -n "$out" ]]; then
|
|
echo "$json" > "$out"
|
|
log::wg_success "Exported to ${out}"
|
|
else
|
|
echo "$json"
|
|
fi
|
|
}
|
|
|
|
# ======================================================
|
|
# Peer export
|
|
# ======================================================
|
|
|
|
function cmd::export::_peer() {
|
|
local name="${1:-}" conf_only="${2:-false}" meta_only="${3:-false}"
|
|
|
|
peers::require_exists "$name" || return 1
|
|
|
|
local conf_file
|
|
conf_file="$(ctx::clients)/${name}.conf"
|
|
[[ ! -f "$conf_file" ]] && log::error "Client conf not found: ${conf_file}" && return 1
|
|
|
|
local conf_b64
|
|
conf_b64=$(base64 -w 0 < "$conf_file" 2>/dev/null || base64 < "$conf_file")
|
|
|
|
if $conf_only; then
|
|
cmd::export::_envelope "peer_conf" \
|
|
"$(printf '{"name":"%s","conf":"%s"}' "$name" "$conf_b64")"
|
|
return 0
|
|
fi
|
|
|
|
# Meta
|
|
local meta_file meta_json="{}"
|
|
meta_file="$(ctx::meta)/${name}.meta"
|
|
[[ -f "$meta_file" ]] && meta_json=$(cat "$meta_file")
|
|
|
|
if $meta_only; then
|
|
cmd::export::_envelope "peer_meta" \
|
|
"$(printf '{"name":"%s","meta":%s}' "$name" "$meta_json")"
|
|
return 0
|
|
fi
|
|
|
|
# Public key
|
|
local public_key=""
|
|
local key_file
|
|
key_file="$(ctx::clients)/${name}_public.key"
|
|
[[ -f "$key_file" ]] && public_key=$(cat "$key_file")
|
|
|
|
# IP
|
|
local ip
|
|
ip=$(peers::get_ip "$name")
|
|
|
|
# Type
|
|
local peer_type
|
|
peer_type=$(peers::get_type "$name" 2>/dev/null || echo "")
|
|
|
|
# Direct rule
|
|
local direct_rule
|
|
direct_rule=$(peers::get_meta "$name" "rule" 2>/dev/null || echo "")
|
|
|
|
# Identity
|
|
local identity
|
|
identity=$(peers::get_identity "$name" 2>/dev/null || echo "")
|
|
|
|
# Groups
|
|
local -a group_list=()
|
|
while IFS= read -r g; do
|
|
[[ -n "$g" ]] && group_list+=("\"$g\"")
|
|
done < <(json::peer_groups "$(ctx::groups)" "$name" 2>/dev/null)
|
|
local groups_json="[]"
|
|
[[ ${#group_list[@]} -gt 0 ]] && \
|
|
groups_json="[$(printf '%s,' "${group_list[@]}" | sed 's/,$//')]"
|
|
|
|
# Blocks
|
|
local block_file is_blocked="false" block_json="null"
|
|
block_file="$(ctx::blocks)/${name}.block"
|
|
if [[ -f "$block_file" ]]; then
|
|
is_blocked="true"
|
|
block_json=$(base64 -w 0 < "$block_file" 2>/dev/null || base64 < "$block_file")
|
|
block_json="\"${block_json}\""
|
|
fi
|
|
|
|
local peer_data
|
|
peer_data=$(printf \
|
|
'{"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}}' \
|
|
"$name" "$ip" "$peer_type" "$public_key" "$conf_b64" \
|
|
"$meta_json" "$identity" "$groups_json" "$direct_rule" \
|
|
"$is_blocked" "$block_json")
|
|
|
|
cmd::export::_envelope "peer" "$peer_data"
|
|
}
|
|
|
|
# ======================================================
|
|
# Identity export
|
|
# ======================================================
|
|
|
|
function cmd::export::_identity() {
|
|
local name="${1:-}"
|
|
identity::require_exists "$name" || return 1
|
|
|
|
local id_file
|
|
id_file="$(ctx::identities)/${name}.identity"
|
|
local id_json
|
|
id_json=$(cat "$id_file")
|
|
|
|
cmd::export::_envelope "identity" \
|
|
"$(printf '{"name":"%s","identity":%s}' "$name" "$id_json")"
|
|
}
|
|
|
|
# ======================================================
|
|
# Full backup
|
|
# ======================================================
|
|
|
|
function cmd::export::_full() {
|
|
local no_config="${1:-false}" no_peers="${2:-false}"
|
|
local version
|
|
version=$(wgctl::version 2>/dev/null || echo "unknown")
|
|
|
|
python3 "$(ctx::json_helper)" export_full \
|
|
"$(ctx::clients)" \
|
|
"$(ctx::meta)" \
|
|
"$(ctx::rules)" \
|
|
"$(ctx::identities)" \
|
|
"$(ctx::groups)" \
|
|
"$(ctx::blocks)" \
|
|
"$(ctx::config_file)" \
|
|
"$(ctx::policies)" \
|
|
"$(ctx::subnets)" \
|
|
"$(ctx::net)" \
|
|
"$(ctx::hosts)" \
|
|
"$no_config" \
|
|
"$no_peers" \
|
|
"$version" \
|
|
2>/dev/null
|
|
}
|
|
|
|
# Helper — peer data without envelope (used by full backup)
|
|
function cmd::export::_peer_data() {
|
|
local name="${1:-}"
|
|
local conf_file
|
|
conf_file="$(ctx::clients)/${name}.conf"
|
|
[[ ! -f "$conf_file" ]] && return 0
|
|
|
|
local conf_b64
|
|
conf_b64=$(base64 -w 0 < "$conf_file" 2>/dev/null || base64 < "$conf_file")
|
|
|
|
local meta_file meta_json="{}"
|
|
meta_file="$(ctx::meta)/${name}.meta"
|
|
[[ -f "$meta_file" ]] && meta_json=$(cat "$meta_file")
|
|
|
|
local public_key=""
|
|
local key_file
|
|
key_file="$(ctx::clients)/${name}_public.key"
|
|
[[ -f "$key_file" ]] && public_key=$(cat "$key_file")
|
|
|
|
local ip
|
|
ip=$(peers::get_ip "$name")
|
|
|
|
local peer_type
|
|
peer_type=$(peers::get_type "$name" 2>/dev/null || echo "")
|
|
|
|
local direct_rule
|
|
direct_rule=$(peers::get_meta "$name" "rule" 2>/dev/null || echo "")
|
|
|
|
local identity
|
|
identity=$(peers::get_identity "$name" 2>/dev/null || echo "")
|
|
|
|
local -a group_list=()
|
|
while IFS= read -r g; do
|
|
[[ -n "$g" ]] && group_list+=("\"$g\"")
|
|
done < <(json::peer_groups "$(ctx::groups)" "$name" 2>/dev/null)
|
|
local groups_json="[]"
|
|
[[ ${#group_list[@]} -gt 0 ]] && \
|
|
groups_json="[$(printf '%s,' "${group_list[@]}" | sed 's/,$//')]"
|
|
|
|
local block_file is_blocked="false" block_json="null"
|
|
block_file="$(ctx::blocks)/${name}.block"
|
|
if [[ -f "$block_file" ]]; then
|
|
is_blocked="true"
|
|
block_json="\"$(base64 -w 0 < "$block_file" 2>/dev/null || base64 < "$block_file")\""
|
|
fi
|
|
|
|
printf \
|
|
'{"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}}' \
|
|
"$name" "$ip" "$peer_type" "$public_key" "$conf_b64" \
|
|
"$meta_json" "$identity" "$groups_json" "$direct_rule" \
|
|
"$is_blocked" "$block_json"
|
|
}
|
|
|
|
# ======================================================
|
|
# Envelope helper
|
|
# ======================================================
|
|
|
|
function cmd::export::_envelope() {
|
|
local export_type="${1:-}" data="${2:-}"
|
|
local version ts
|
|
version=$(wgctl::version 2>/dev/null || echo "unknown")
|
|
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
printf '{"wgctl_version":"%s","export_type":"%s","exported_at":"%s","data":%s}\n' \
|
|
"$version" "$export_type" "$ts" "$data"
|
|
}
|
|
|
|
function cmd::export::_compact_json() {
|
|
local file="$1"
|
|
python3 -c "
|
|
import json, sys
|
|
try:
|
|
print(json.dumps(json.load(open('${file}'))))
|
|
except Exception as e:
|
|
print('{}', file=sys.stderr)
|
|
" 2>/dev/null
|
|
} |