Compare commits
3 commits
a7b05547f5
...
ddd705aa87
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddd705aa87 | ||
|
|
00d6be0766 | ||
|
|
8f3360c631 |
9 changed files with 1076 additions and 2 deletions
304
commands/export.command.sh
Normal file
304
commands/export.command.sh
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
#!/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
|
||||||
|
}
|
||||||
288
commands/import.command.sh
Normal file
288
commands/import.command.sh
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# commands/import.command.sh
|
||||||
|
|
||||||
|
function cmd::import::on_load() {
|
||||||
|
flag::register --file
|
||||||
|
flag::register --peer
|
||||||
|
flag::register --dry-run
|
||||||
|
flag::register --force
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::import::help() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: wgctl import --file <file> [options]
|
||||||
|
|
||||||
|
Import a wgctl JSON export bundle.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--file <path> Path to export bundle (required)
|
||||||
|
--peer <name> Import only this peer from a full backup
|
||||||
|
--dry-run Show what would be imported without making changes
|
||||||
|
--force Overwrite existing data
|
||||||
|
|
||||||
|
Export types handled:
|
||||||
|
peer Single peer bundle
|
||||||
|
peer_conf Peer conf only
|
||||||
|
peer_meta Peer meta only
|
||||||
|
identity Identity bundle
|
||||||
|
full Full backup
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
wgctl import --file backup.json
|
||||||
|
wgctl import --file backup.json --peer phone-nuno
|
||||||
|
wgctl import --file phone-nuno.json --dry-run
|
||||||
|
wgctl import --file backup.json --force
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::import::run() {
|
||||||
|
local file="" peer="" dry_run=false force=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--file) file="$2"; shift 2 ;;
|
||||||
|
--peer) peer="$2"; shift 2 ;;
|
||||||
|
--dry-run) dry_run=true; shift ;;
|
||||||
|
--force) force=true; shift ;;
|
||||||
|
--help) cmd::import::help; return ;;
|
||||||
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$file" ]] && log::error "Missing required flag: --file" && return 1
|
||||||
|
[[ ! -f "$file" ]] && log::error "File not found: ${file}" && return 1
|
||||||
|
|
||||||
|
# Read export metadata via Python
|
||||||
|
local export_type export_version
|
||||||
|
export_type=$(json::import_get_field \
|
||||||
|
"$file" "export_type" 2>/dev/null)
|
||||||
|
export_version=$(json::import_get_field \
|
||||||
|
"$file" "wgctl_version" 2>/dev/null)
|
||||||
|
|
||||||
|
[[ -z "$export_type" ]] && \
|
||||||
|
log::error "Invalid export file: ${file}" && return 1
|
||||||
|
|
||||||
|
# Version check
|
||||||
|
local current_version
|
||||||
|
current_version=$(wgctl::version 2>/dev/null || echo "unknown")
|
||||||
|
if [[ "$export_version" != "$current_version" ]]; then
|
||||||
|
log::wg_warning "Export version (${export_version}) differs from current (${current_version})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log::section "wgctl Import"
|
||||||
|
printf " File: %s\n" "$file"
|
||||||
|
printf " Type: %s\n" "$export_type"
|
||||||
|
$dry_run && printf " Mode: \033[2mdry run\033[0m\n"
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
case "$export_type" in
|
||||||
|
peer) cmd::import::_peer "$file" "$dry_run" "$force" ;;
|
||||||
|
peer_conf) cmd::import::_peer_conf "$file" "$dry_run" "$force" ;;
|
||||||
|
peer_meta) cmd::import::_peer_meta "$file" "$dry_run" "$force" ;;
|
||||||
|
identity) cmd::import::_identity "$file" "$dry_run" "$force" ;;
|
||||||
|
full)
|
||||||
|
if [[ -n "$peer" ]]; then
|
||||||
|
cmd::import::_peer_from_full "$file" "$peer" "$dry_run" "$force"
|
||||||
|
else
|
||||||
|
cmd::import::_full "$file" "$dry_run" "$force"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log::error "Unknown export type: ${export_type}"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Helpers
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function cmd::import::_print_results() {
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
case "$line" in
|
||||||
|
error:*) log::error "${line#error:}" ;;
|
||||||
|
skip:*) printf " \033[2mskip: %s (already exists)\033[0m\n" "${line#skip:}" ;;
|
||||||
|
peer:*) printf " ✓ peer: %s\n" "${line#peer:}" ;;
|
||||||
|
group:*) printf " ✓ group: %s\n" "${line#group:}" ;;
|
||||||
|
*) printf " ✓ %s\n" "$line" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Peer import
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function cmd::import::_peer() {
|
||||||
|
local file="${1:-}" dry_run="${2:-false}" force="${3:-false}"
|
||||||
|
|
||||||
|
local name
|
||||||
|
name=$(json::import_get_field "$file" "data" "name" 2>/dev/null)
|
||||||
|
[[ -z "$name" ]] && log::error "Could not read peer name from export" && return 1
|
||||||
|
|
||||||
|
$dry_run && \
|
||||||
|
printf " \033[2m[dry-run]\033[0m Would import peer '%s'\n" "$name" && return 0
|
||||||
|
|
||||||
|
local err
|
||||||
|
err=$(json::import_peer \
|
||||||
|
"$file" "data" "$name" \
|
||||||
|
"$(ctx::clients)" "$(ctx::meta)" \
|
||||||
|
"$(ctx::groups)" "$(ctx::blocks)" \
|
||||||
|
"$($force && echo true || echo false)" \
|
||||||
|
2>&1 >/dev/null) || { log::error "$err"; return 1; }
|
||||||
|
|
||||||
|
json::import_peer \
|
||||||
|
"$file" "data" "$name" \
|
||||||
|
"$(ctx::clients)" "$(ctx::meta)" \
|
||||||
|
"$(ctx::groups)" "$(ctx::blocks)" \
|
||||||
|
"$($force && echo true || echo false)" \
|
||||||
|
2>/dev/null | cmd::import::_print_results
|
||||||
|
|
||||||
|
log::wg_success "Imported peer '${name}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Peer conf only
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function cmd::import::_peer_conf() {
|
||||||
|
local file="${1:-}" dry_run="${2:-false}" force="${3:-false}"
|
||||||
|
|
||||||
|
local name
|
||||||
|
name=$(json::import_get_field "$file" "data" "name" 2>/dev/null)
|
||||||
|
local conf_file="$(ctx::clients)/${name}.conf"
|
||||||
|
|
||||||
|
if [[ -f "$conf_file" ]] && ! $force; then
|
||||||
|
log::error "Peer conf '${name}' already exists. Use --force to overwrite."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
$dry_run && \
|
||||||
|
printf " \033[2m[dry-run]\033[0m Would import conf for '%s'\n" "$name" && return 0
|
||||||
|
|
||||||
|
python3 -c "
|
||||||
|
import json, base64
|
||||||
|
d = json.load(open('${file}'))
|
||||||
|
conf = base64.b64decode(d['data']['conf']).decode()
|
||||||
|
open('${conf_file}', 'w').write(conf)
|
||||||
|
" 2>/dev/null || { log::error "Failed to write conf"; return 1; }
|
||||||
|
|
||||||
|
printf " ✓ conf\n"
|
||||||
|
log::wg_success "Imported conf for '${name}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Peer meta only
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function cmd::import::_peer_meta() {
|
||||||
|
local file="${1:-}" dry_run="${2:-false}" force="${3:-false}"
|
||||||
|
|
||||||
|
local name
|
||||||
|
name=$(json::import_get_field "$file" "data" "name" 2>/dev/null)
|
||||||
|
|
||||||
|
[[ ! -f "$(ctx::clients)/${name}.conf" ]] && \
|
||||||
|
log::error "Peer '${name}' does not exist — import the peer conf first" && return 1
|
||||||
|
|
||||||
|
$dry_run && \
|
||||||
|
printf " \033[2m[dry-run]\033[0m Would import meta for '%s'\n" "$name" && return 0
|
||||||
|
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
d = json.load(open('${file}'))
|
||||||
|
meta = d['data']['meta']
|
||||||
|
open('$(ctx::meta)/${name}.meta', 'w').write(json.dumps(meta, indent=2))
|
||||||
|
" 2>/dev/null || { log::error "Failed to write meta"; return 1; }
|
||||||
|
|
||||||
|
printf " ✓ meta\n"
|
||||||
|
log::wg_success "Imported meta for '${name}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Identity import
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function cmd::import::_identity() {
|
||||||
|
local file="${1:-}" dry_run="${2:-false}" force="${3:-false}"
|
||||||
|
|
||||||
|
local name
|
||||||
|
name=$(json::import_get_field "$file" "data" "name" 2>/dev/null)
|
||||||
|
|
||||||
|
$dry_run && \
|
||||||
|
printf " \033[2m[dry-run]\033[0m Would import identity '%s'\n" "$name" && return 0
|
||||||
|
|
||||||
|
local err
|
||||||
|
err=$(json::import_identity \
|
||||||
|
"$file" "$name" \
|
||||||
|
"$(ctx::identities)" "$(ctx::clients)" \
|
||||||
|
"$($force && echo true || echo false)" \
|
||||||
|
2>&1 >/dev/null) || { log::error "$err"; return 1; }
|
||||||
|
|
||||||
|
json::import_identity \
|
||||||
|
"$file" "$name" \
|
||||||
|
"$(ctx::identities)" "$(ctx::clients)" \
|
||||||
|
"$($force && echo true || echo false)" \
|
||||||
|
2>/dev/null | cmd::import::_print_results
|
||||||
|
|
||||||
|
log::wg_success "Imported identity '${name}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Single peer from full backup
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function cmd::import::_peer_from_full() {
|
||||||
|
local file="${1:-}" name="${2:-}" dry_run="${3:-false}" force="${4:-false}"
|
||||||
|
|
||||||
|
$dry_run && \
|
||||||
|
printf " \033[2m[dry-run]\033[0m Would import peer '%s' from backup\n" "$name" && return 0
|
||||||
|
|
||||||
|
local err
|
||||||
|
err=$(json::import_peer \
|
||||||
|
"$file" "peers" "$name" \
|
||||||
|
"$(ctx::clients)" "$(ctx::meta)" \
|
||||||
|
"$(ctx::groups)" "$(ctx::blocks)" \
|
||||||
|
"$($force && echo true || echo false)" \
|
||||||
|
2>&1 >/dev/null) || { log::error "$err"; return 1; }
|
||||||
|
|
||||||
|
json::import_peer \
|
||||||
|
"$file" "peers" "$name" \
|
||||||
|
"$(ctx::clients)" "$(ctx::meta)" \
|
||||||
|
"$(ctx::groups)" "$(ctx::blocks)" \
|
||||||
|
"$($force && echo true || echo false)" \
|
||||||
|
2>/dev/null | cmd::import::_print_results
|
||||||
|
|
||||||
|
log::wg_success "Imported peer '${name}' from backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Full backup import
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function cmd::import::_full() {
|
||||||
|
local file="${1:-}" dry_run="${2:-false}" force="${3:-false}"
|
||||||
|
|
||||||
|
local peer_count
|
||||||
|
peer_count=$(json::import_get_field \
|
||||||
|
"$file" "data" "peers" 2>/dev/null | python3 -c \
|
||||||
|
"import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "?")
|
||||||
|
|
||||||
|
printf " Importing full backup (%s peers)...\n\n" "$peer_count"
|
||||||
|
|
||||||
|
$dry_run && \
|
||||||
|
log::wg_warning "Dry run — no changes would be made" && return 0
|
||||||
|
|
||||||
|
json::import_full \
|
||||||
|
"$file" \
|
||||||
|
"$(ctx::clients)" "$(ctx::meta)" \
|
||||||
|
"$(ctx::rules)" "$(ctx::identities)" \
|
||||||
|
"$(ctx::groups)" "$(ctx::blocks)" \
|
||||||
|
"$(ctx::policies)" "$(ctx::subnets)" \
|
||||||
|
"$(ctx::net)" "$(ctx::hosts)" \
|
||||||
|
"$($force && echo true || echo false)" \
|
||||||
|
2>/dev/null | cmd::import::_print_results
|
||||||
|
|
||||||
|
log::wg_success "Import complete"
|
||||||
|
}
|
||||||
|
|
@ -138,6 +138,8 @@ function cmd::test::run_all_integration_sections() {
|
||||||
cmd::test::section_peer_cmd
|
cmd::test::section_peer_cmd
|
||||||
cmd::test::section_group_purge
|
cmd::test::section_group_purge
|
||||||
cmd::test::section_logs_clean
|
cmd::test::section_logs_clean
|
||||||
|
cmd::test::section_display
|
||||||
|
cmd::test::section_export
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_list() {
|
function cmd::test::section_list() {
|
||||||
|
|
@ -174,6 +176,8 @@ function cmd::test::section_config() {
|
||||||
cmd::test::run_cmd "config --name phone-nuno" "PrivateKey" config --name phone-nuno
|
cmd::test::run_cmd "config --name phone-nuno" "PrivateKey" config --name phone-nuno
|
||||||
cmd::test::run_cmd "config --name nuno --type phone" "PrivateKey" config --name nuno --type phone
|
cmd::test::run_cmd "config --name nuno --type phone" "PrivateKey" config --name nuno --type phone
|
||||||
cmd::test::run_cmd "qr --name phone-nuno" "" qr --name phone-nuno
|
cmd::test::run_cmd "qr --name phone-nuno" "" qr --name phone-nuno
|
||||||
|
cmd::test::run_cmd "config migrate --dry-run" "" config migrate --dry-run
|
||||||
|
cmd::test::run_cmd_fails "config missing --name" config
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_rules() {
|
function cmd::test::section_rules() {
|
||||||
|
|
@ -367,3 +371,56 @@ function cmd::test::section_logs_clean() {
|
||||||
"keepalive" \
|
"keepalive" \
|
||||||
logs clean --force
|
logs clean --force
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cmd::test::section_export() {
|
||||||
|
test::section "Export"
|
||||||
|
|
||||||
|
# Single peer export
|
||||||
|
cmd::test::run_cmd "export --peer" '"export_type":"peer"' export --peer phone-nuno
|
||||||
|
cmd::test::run_cmd "export --peer has name" '"name":"phone-nuno"' export --peer phone-nuno
|
||||||
|
cmd::test::run_cmd "export --peer has conf" '"conf":' export --peer phone-nuno
|
||||||
|
cmd::test::run_cmd "export --peer has identity" '"identity":"nuno"' export --peer phone-nuno
|
||||||
|
cmd::test::run_cmd "export --peer has groups" '"groups":' export --peer phone-nuno
|
||||||
|
cmd::test::run_cmd "export --peer has blocks" '"blocks":' export --peer phone-nuno
|
||||||
|
cmd::test::run_cmd "export --peer conf-only" '"export_type":"peer_conf"' export --peer phone-nuno --conf-only
|
||||||
|
cmd::test::run_cmd "export --peer meta-only" '"export_type":"peer_meta"' export --peer phone-nuno --meta-only
|
||||||
|
cmd::test::run_cmd_fails "export missing flag" export
|
||||||
|
# Identity export
|
||||||
|
cmd::test::run_cmd "export --identity" '"export_type":"identity"' export --identity nuno
|
||||||
|
|
||||||
|
# Full backup
|
||||||
|
cmd::test::run_cmd "export --all" '"export_type"' export --all
|
||||||
|
cmd::test::run_cmd "export --all is full" '"full"' export --all
|
||||||
|
cmd::test::run_cmd "export --all has peers" '"peers"' export --all
|
||||||
|
cmd::test::run_cmd "export --all has rules" '"rules"' export --all
|
||||||
|
cmd::test::run_cmd "export --all has identities" '"identities"' export --all
|
||||||
|
cmd::test::run_cmd "export --all has config" '"config"' export --all
|
||||||
|
cmd::test::run_cmd "export --all no-config" '"peers"' export --all --no-config
|
||||||
|
# --no-config should NOT have config section
|
||||||
|
local no_config_out
|
||||||
|
no_config_out=$(wgctl export --all --no-config 2>/dev/null)
|
||||||
|
if echo "$no_config_out" | grep -qF '"config":'; then
|
||||||
|
test::fail "export --all --no-config should not have config section"
|
||||||
|
else
|
||||||
|
test::pass "export --all --no-config has no config section"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export to file
|
||||||
|
local tmp_file="/tmp/wgctl_test_export_$$.json"
|
||||||
|
cmd::test::run_cmd "export --peer --out file" "Exported to" export --peer phone-nuno --out "$tmp_file"
|
||||||
|
[[ -f "$tmp_file" ]] && test::pass "export --out file exists" \
|
||||||
|
|| test::fail "export --out file not created"
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
|
||||||
|
# Version in export
|
||||||
|
cmd::test::run_cmd "export has version" '"wgctl_version":' export --peer phone-nuno
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::test::section_display() {
|
||||||
|
test::section "Display config"
|
||||||
|
|
||||||
|
cmd::test::run_cmd "display config exists" "" \
|
||||||
|
config show --name phone-nuno # just check wgctl works, not display directly
|
||||||
|
|
||||||
|
# Test style via unit tests (see unit section)
|
||||||
|
}
|
||||||
|
|
@ -49,6 +49,8 @@ function cmd::test::run_all_unit_sections() {
|
||||||
cmd::test::unit_parse_since
|
cmd::test::unit_parse_since
|
||||||
cmd::test::unit_group_status
|
cmd::test::unit_group_status
|
||||||
cmd::test::unit_json_output
|
cmd::test::unit_json_output
|
||||||
|
cmd::test::unit_display
|
||||||
|
cmd::test::unit_version
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::unit_subnet() {
|
function cmd::test::unit_subnet() {
|
||||||
|
|
@ -225,6 +227,8 @@ function cmd::test::unit_group_status() {
|
||||||
function cmd::test::unit_json_output() {
|
function cmd::test::unit_json_output() {
|
||||||
test::section "Unit: JSON output"
|
test::section "Unit: JSON output"
|
||||||
|
|
||||||
|
command::_load_mixins 2>/dev/null || true
|
||||||
|
|
||||||
# json::envelope produces valid structure
|
# json::envelope produces valid structure
|
||||||
local result
|
local result
|
||||||
result=$(echo '{"peers":[]}' | json::envelope "list" "0")
|
result=$(echo '{"peers":[]}' | json::envelope "list" "0")
|
||||||
|
|
@ -235,8 +239,11 @@ function cmd::test::unit_json_output() {
|
||||||
|
|
||||||
# command::mixin registration
|
# command::mixin registration
|
||||||
load_command list
|
load_command list
|
||||||
cmd::test::assert_true "json_output mixin registered" "declare -f command::mixin::json_output::register >/dev/null 2>&1"
|
cmd::test::assert_true "json_output mixin registered" \
|
||||||
cmd::test::assert_true "command::json accessor exists" "declare -f command::json >/dev/null 2>&1"
|
declare -f command::mixin::json_output::register >/dev/null 2>&1
|
||||||
|
|
||||||
|
cmd::test::assert_true "command::json accessor exists" \
|
||||||
|
declare -f command::json >/dev/null 2>&1
|
||||||
|
|
||||||
# json::error_envelope
|
# json::error_envelope
|
||||||
local err_result
|
local err_result
|
||||||
|
|
@ -252,3 +259,35 @@ function cmd::test::unit_json_output() {
|
||||||
_COMMAND_JSON=false
|
_COMMAND_JSON=false
|
||||||
cmd::test::assert_false "command::json false" "command::json"
|
cmd::test::assert_false "command::json false" "command::json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cmd::test::unit_display() {
|
||||||
|
test::section "Unit: display config"
|
||||||
|
|
||||||
|
load_module display
|
||||||
|
|
||||||
|
# Default style is compact
|
||||||
|
cmd::test::assert "display::style peer_list default" \
|
||||||
|
"$(display::style "peer_list")" "compact"
|
||||||
|
|
||||||
|
# is_compact returns true for compact
|
||||||
|
display::is_compact "peer_list" && \
|
||||||
|
test::pass "display::is_compact peer_list" || \
|
||||||
|
test::fail "display::is_compact peer_list"
|
||||||
|
|
||||||
|
# is_table returns false for compact
|
||||||
|
display::is_table "peer_list" && \
|
||||||
|
test::fail "display::is_table should be false for compact" || \
|
||||||
|
test::pass "display::is_table returns false for compact"
|
||||||
|
|
||||||
|
# Unknown view defaults to compact
|
||||||
|
cmd::test::assert "display::style unknown view" \
|
||||||
|
"$(display::style "nonexistent_view")" "compact"
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::test::unit_version() {
|
||||||
|
test::section "Unit: wgctl version"
|
||||||
|
local ver
|
||||||
|
ver=$(wgctl::version 2>/dev/null)
|
||||||
|
[[ -n "$ver" ]] && test::pass "wgctl::version returns value: $ver" \
|
||||||
|
|| test::fail "wgctl::version returns empty"
|
||||||
|
}
|
||||||
|
|
@ -161,3 +161,8 @@ function json::peer_transfer_delta() {
|
||||||
python3 "$JSON_HELPER" peer_transfer_delta "$@" </dev/null
|
python3 "$JSON_HELPER" peer_transfer_delta "$@" </dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Importer
|
||||||
|
function json::import_peer() { python3 "$JSON_HELPER" import_peer "$@" </dev/null; }
|
||||||
|
function json::import_identity() { python3 "$JSON_HELPER" import_identity "$@" </dev/null; }
|
||||||
|
function json::import_full() { python3 "$JSON_HELPER" import_full "$@" </dev/null; }
|
||||||
|
function json::import_get_field() { python3 "$JSON_HELPER" import_get_field "$@" </dev/null; }
|
||||||
|
|
@ -1627,6 +1627,180 @@ def display_load(file):
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
def _export_peer_data(name, clients_dir, meta_dir, identities_dir, groups_dir, blocks_dir):
|
||||||
|
"""Build peer data dict for export."""
|
||||||
|
import glob, base64, os
|
||||||
|
|
||||||
|
conf_path = os.path.join(clients_dir, f"{name}.conf")
|
||||||
|
if not os.path.exists(conf_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(conf_path, 'rb') as f:
|
||||||
|
conf_b64 = base64.b64encode(f.read()).decode()
|
||||||
|
|
||||||
|
# Public key
|
||||||
|
key_path = os.path.join(clients_dir, f"{name}_public.key")
|
||||||
|
public_key = open(key_path).read().strip() if os.path.exists(key_path) else ''
|
||||||
|
|
||||||
|
# IP from conf
|
||||||
|
ip = ''
|
||||||
|
with open(conf_path) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith('Address'):
|
||||||
|
ip = line.split('=')[1].strip().split('/')[0]
|
||||||
|
break
|
||||||
|
|
||||||
|
# Meta
|
||||||
|
meta = {}
|
||||||
|
meta_path = os.path.join(meta_dir, f"{name}.meta")
|
||||||
|
if os.path.exists(meta_path):
|
||||||
|
try:
|
||||||
|
with open(meta_path) as f:
|
||||||
|
meta = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
direct_rule = meta.get('rule', '')
|
||||||
|
|
||||||
|
# Identity + type — scan identity files once for both
|
||||||
|
identity = ''
|
||||||
|
peer_type = meta.get('type', '')
|
||||||
|
for id_file in sorted(glob.glob(os.path.join(identities_dir, '*.identity'))):
|
||||||
|
try:
|
||||||
|
with open(id_file) as f:
|
||||||
|
id_data = json.load(f)
|
||||||
|
if name in id_data.get('peers', []):
|
||||||
|
identity = id_data.get('name', '')
|
||||||
|
if not peer_type:
|
||||||
|
peer_type = id_data.get('devices', {}).get(name, {}).get('type', '')
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Groups
|
||||||
|
peer_groups = []
|
||||||
|
for grp_file in sorted(glob.glob(os.path.join(groups_dir, '*.group'))):
|
||||||
|
try:
|
||||||
|
with open(grp_file) as f:
|
||||||
|
g = json.load(f)
|
||||||
|
if name in g.get('peers', []):
|
||||||
|
peer_groups.append(g.get('name', ''))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Blocks
|
||||||
|
blocks = {'is_blocked': False, 'block_file': None}
|
||||||
|
block_path = os.path.join(blocks_dir, f"{name}.block")
|
||||||
|
if os.path.exists(block_path):
|
||||||
|
with open(block_path, 'rb') as f:
|
||||||
|
blocks = {
|
||||||
|
'is_blocked': True,
|
||||||
|
'block_file': base64.b64encode(f.read()).decode()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'ip': ip,
|
||||||
|
'type': peer_type,
|
||||||
|
'public_key': public_key,
|
||||||
|
'conf': conf_b64,
|
||||||
|
'meta': meta,
|
||||||
|
'identity': identity,
|
||||||
|
'groups': peer_groups,
|
||||||
|
'direct_rule': direct_rule,
|
||||||
|
'blocks': blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def export_full(clients_dir, meta_dir, rules_dir, identities_dir,
|
||||||
|
groups_dir, blocks_dir, config_file,
|
||||||
|
policies_file, subnets_file, net_file, hosts_file,
|
||||||
|
no_config, no_peers, version):
|
||||||
|
"""Build full wgctl export as JSON."""
|
||||||
|
import glob, os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
# Config
|
||||||
|
if no_config != 'true' and os.path.exists(config_file):
|
||||||
|
try:
|
||||||
|
with open(config_file) as f:
|
||||||
|
data['config'] = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
data['config'] = {}
|
||||||
|
|
||||||
|
# Peers
|
||||||
|
if no_peers != 'true':
|
||||||
|
peers = []
|
||||||
|
for conf_path in sorted(glob.glob(f"{clients_dir}/*.conf")):
|
||||||
|
name = os.path.basename(conf_path).replace('.conf', '')
|
||||||
|
try:
|
||||||
|
peer = _export_peer_data(
|
||||||
|
name, clients_dir, meta_dir, identities_dir,
|
||||||
|
groups_dir, blocks_dir)
|
||||||
|
if peer:
|
||||||
|
peers.append(peer)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
data['peers'] = peers
|
||||||
|
|
||||||
|
# Rules
|
||||||
|
rules = []
|
||||||
|
for rule_file in sorted(
|
||||||
|
glob.glob(f"{rules_dir}/*.rule") +
|
||||||
|
glob.glob(f"{rules_dir}/base/*.rule")
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
with open(rule_file) as f:
|
||||||
|
rules.append(json.load(f))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
data['rules'] = rules
|
||||||
|
|
||||||
|
# Identities
|
||||||
|
identities = []
|
||||||
|
for id_file in sorted(glob.glob(f"{identities_dir}/*.identity")):
|
||||||
|
try:
|
||||||
|
with open(id_file) as f:
|
||||||
|
identities.append(json.load(f))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
data['identities'] = identities
|
||||||
|
|
||||||
|
# Groups
|
||||||
|
groups = []
|
||||||
|
for grp_file in sorted(glob.glob(f"{groups_dir}/*.group")):
|
||||||
|
try:
|
||||||
|
with open(grp_file) as f:
|
||||||
|
groups.append(json.load(f))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
data['groups'] = groups
|
||||||
|
|
||||||
|
# Flat JSON files
|
||||||
|
for key, path in [
|
||||||
|
('policies', policies_file),
|
||||||
|
('subnets', subnets_file),
|
||||||
|
('services', net_file),
|
||||||
|
('hosts', hosts_file),
|
||||||
|
]:
|
||||||
|
if os.path.exists(path):
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
data[key] = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ts = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
result = {
|
||||||
|
'wgctl_version': version,
|
||||||
|
'export_type': 'full',
|
||||||
|
'exported_at': ts,
|
||||||
|
'data': data,
|
||||||
|
}
|
||||||
|
print(json.dumps(result))
|
||||||
|
|
||||||
# ======================================================
|
# ======================================================
|
||||||
|
|
||||||
def _net_read(file):
|
def _net_read(file):
|
||||||
|
|
@ -1998,6 +2172,25 @@ commands = {
|
||||||
'peer_history_lookup': lambda args: peer_history_lookup(args[0], args[1]),
|
'peer_history_lookup': lambda args: peer_history_lookup(args[0], args[1]),
|
||||||
'config_load': lambda args: config_load(args[0]),
|
'config_load': lambda args: config_load(args[0]),
|
||||||
'display_load': lambda args: display_load(args[0]),
|
'display_load': lambda args: display_load(args[0]),
|
||||||
|
'export_full': lambda args: export_full(
|
||||||
|
args[0], args[1], args[2], args[3], args[4], args[5],
|
||||||
|
args[6], args[7], args[8], args[9], args[10],
|
||||||
|
args[11] if len(args) > 11 else 'false',
|
||||||
|
args[12] if len(args) > 12 else 'false',
|
||||||
|
args[13] if len(args) > 13 else 'unknown'),
|
||||||
|
|
||||||
|
# Import
|
||||||
|
'import_peer': lambda args: __import__('lib.importer', fromlist=['import_peer']).import_peer(
|
||||||
|
args[0], args[1], args[2], args[3], args[4],
|
||||||
|
args[5], args[6], args[7] if len(args) > 7 else 'false'),
|
||||||
|
'import_identity': lambda args: __import__('lib.importer', fromlist=['import_identity']).import_identity(
|
||||||
|
args[0], args[1], args[2], args[3],
|
||||||
|
args[4] if len(args) > 4 else 'false'),
|
||||||
|
'import_full': lambda args: __import__('lib.importer', fromlist=['import_full']).import_full(
|
||||||
|
args[0], args[1], args[2], args[3], args[4], args[5],
|
||||||
|
args[6], args[7], args[8], args[9], args[10],
|
||||||
|
args[11] if len(args) > 11 else 'false'),
|
||||||
|
'import_get_field': lambda args: __import__('lib.importer', fromlist=['import_get_field']).import_get_field(args[0], *args[1:]),
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
BIN
core/lib/__pycache__/importer.cpython-311.pyc
Normal file
BIN
core/lib/__pycache__/importer.cpython-311.pyc
Normal file
Binary file not shown.
184
core/lib/importer.py
Normal file
184
core/lib/importer.py
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
|
|
||||||
|
def import_peer(file, data_key, name, clients_dir, meta_dir,
|
||||||
|
groups_dir, blocks_dir, force):
|
||||||
|
"""
|
||||||
|
Import a single peer from an export bundle.
|
||||||
|
data_key: 'data' for peer export, 'peers' for full backup
|
||||||
|
Returns: list of imported items as strings
|
||||||
|
"""
|
||||||
|
import base64, os
|
||||||
|
|
||||||
|
d = json.load(open(file))
|
||||||
|
|
||||||
|
if data_key == 'data':
|
||||||
|
peer = d['data']
|
||||||
|
else:
|
||||||
|
# Find peer in full backup peers array
|
||||||
|
peers = d['data'].get('peers', [])
|
||||||
|
peer = next((p for p in peers if p['name'] == name), None)
|
||||||
|
if not peer:
|
||||||
|
print(f"error: peer '{name}' not found in backup", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
imported = []
|
||||||
|
|
||||||
|
# conf
|
||||||
|
conf_path = os.path.join(clients_dir, f"{name}.conf")
|
||||||
|
if os.path.exists(conf_path) and force != 'true':
|
||||||
|
print(f"error: peer '{name}' already exists, use --force to overwrite",
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
os.makedirs(clients_dir, exist_ok=True)
|
||||||
|
conf = base64.b64decode(peer['conf']).decode()
|
||||||
|
open(conf_path, 'w').write(conf)
|
||||||
|
imported.append('conf')
|
||||||
|
|
||||||
|
# meta
|
||||||
|
meta = peer.get('meta', {})
|
||||||
|
if meta:
|
||||||
|
os.makedirs(meta_dir, exist_ok=True)
|
||||||
|
open(os.path.join(meta_dir, f"{name}.meta"), 'w').write(
|
||||||
|
json.dumps(meta, indent=2))
|
||||||
|
imported.append('meta')
|
||||||
|
|
||||||
|
# groups
|
||||||
|
for grp in peer.get('groups', []):
|
||||||
|
grp_file = os.path.join(groups_dir, f"{grp}.group")
|
||||||
|
if os.path.exists(grp_file):
|
||||||
|
try:
|
||||||
|
g = json.load(open(grp_file))
|
||||||
|
if name not in g.get('peers', []):
|
||||||
|
g.setdefault('peers', []).append(name)
|
||||||
|
open(grp_file, 'w').write(json.dumps(g, indent=2))
|
||||||
|
imported.append(f"group:{grp}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# blocks
|
||||||
|
blocks = peer.get('blocks', {})
|
||||||
|
if blocks.get('is_blocked') and blocks.get('block_file'):
|
||||||
|
os.makedirs(blocks_dir, exist_ok=True)
|
||||||
|
block_data = base64.b64decode(blocks['block_file'])
|
||||||
|
open(os.path.join(blocks_dir, f"{name}.block"), 'wb').write(block_data)
|
||||||
|
imported.append('block')
|
||||||
|
|
||||||
|
print('\n'.join(imported))
|
||||||
|
|
||||||
|
|
||||||
|
def import_identity(file, name, identities_dir, clients_dir, force):
|
||||||
|
"""Import an identity from an export bundle."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
d = json.load(open(file))
|
||||||
|
id_data = d['data'].get('identity', d['data'])
|
||||||
|
|
||||||
|
# Check all referenced peers exist
|
||||||
|
peers = id_data.get('peers', [])
|
||||||
|
missing = [p for p in peers
|
||||||
|
if not os.path.exists(os.path.join(clients_dir, f"{p}.conf"))]
|
||||||
|
if missing:
|
||||||
|
print(f"error: missing peers: {' '.join(missing)}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
id_file = os.path.join(identities_dir, f"{name}.identity")
|
||||||
|
if os.path.exists(id_file) and force != 'true':
|
||||||
|
print(f"error: identity '{name}' already exists, use --force to overwrite",
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
os.makedirs(identities_dir, exist_ok=True)
|
||||||
|
open(id_file, 'w').write(json.dumps(id_data, indent=2))
|
||||||
|
print('identity')
|
||||||
|
|
||||||
|
|
||||||
|
def import_full(file, clients_dir, meta_dir, rules_dir, identities_dir,
|
||||||
|
groups_dir, blocks_dir, policies_file, subnets_file,
|
||||||
|
net_file, hosts_file, force):
|
||||||
|
"""Import a full backup bundle."""
|
||||||
|
import base64, os, glob
|
||||||
|
|
||||||
|
d = json.load(open(file))
|
||||||
|
data = d['data']
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Peers
|
||||||
|
for peer in data.get('peers', []):
|
||||||
|
name = peer.get('name', '')
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
conf_path = os.path.join(clients_dir, f"{name}.conf")
|
||||||
|
if os.path.exists(conf_path) and force != 'true':
|
||||||
|
results.append(f"skip:{name}")
|
||||||
|
continue
|
||||||
|
os.makedirs(clients_dir, exist_ok=True)
|
||||||
|
conf = base64.b64decode(peer['conf']).decode()
|
||||||
|
open(conf_path, 'w').write(conf)
|
||||||
|
|
||||||
|
meta = peer.get('meta', {})
|
||||||
|
if meta:
|
||||||
|
os.makedirs(meta_dir, exist_ok=True)
|
||||||
|
open(os.path.join(meta_dir, f"{name}.meta"), 'w').write(
|
||||||
|
json.dumps(meta, indent=2))
|
||||||
|
|
||||||
|
blocks = peer.get('blocks', {})
|
||||||
|
if blocks.get('is_blocked') and blocks.get('block_file'):
|
||||||
|
os.makedirs(blocks_dir, exist_ok=True)
|
||||||
|
block_data = base64.b64decode(blocks['block_file'])
|
||||||
|
open(os.path.join(blocks_dir, f"{name}.block"), 'wb').write(block_data)
|
||||||
|
|
||||||
|
results.append(f"peer:{name}")
|
||||||
|
except Exception as e:
|
||||||
|
results.append(f"error:{name}:{e}")
|
||||||
|
|
||||||
|
# Rules
|
||||||
|
os.makedirs(rules_dir, exist_ok=True)
|
||||||
|
for rule in data.get('rules', []):
|
||||||
|
name = rule.get('name', '')
|
||||||
|
if name:
|
||||||
|
open(os.path.join(rules_dir, f"{name}.rule"), 'w').write(
|
||||||
|
json.dumps(rule, indent=2))
|
||||||
|
results.append('rules')
|
||||||
|
|
||||||
|
# Identities
|
||||||
|
os.makedirs(identities_dir, exist_ok=True)
|
||||||
|
for identity in data.get('identities', []):
|
||||||
|
name = identity.get('name', '')
|
||||||
|
if name:
|
||||||
|
open(os.path.join(identities_dir, f"{name}.identity"), 'w').write(
|
||||||
|
json.dumps(identity, indent=2))
|
||||||
|
results.append('identities')
|
||||||
|
|
||||||
|
# Groups
|
||||||
|
os.makedirs(groups_dir, exist_ok=True)
|
||||||
|
for grp in data.get('groups', []):
|
||||||
|
name = grp.get('name', '')
|
||||||
|
if name:
|
||||||
|
open(os.path.join(groups_dir, f"{name}.group"), 'w').write(
|
||||||
|
json.dumps(grp, indent=2))
|
||||||
|
results.append('groups')
|
||||||
|
|
||||||
|
# Flat JSON files
|
||||||
|
for key, path in [('policies', policies_file), ('subnets', subnets_file),
|
||||||
|
('services', net_file), ('hosts', hosts_file)]:
|
||||||
|
section = data.get(key)
|
||||||
|
if section is not None:
|
||||||
|
open(path, 'w').write(json.dumps(section, indent=2))
|
||||||
|
results.append(key)
|
||||||
|
|
||||||
|
print('\n'.join(results))
|
||||||
|
|
||||||
|
|
||||||
|
def import_get_field(file, *keys):
|
||||||
|
"""Get a field from export JSON. Keys are dot-separated path."""
|
||||||
|
d = json.load(open(file))
|
||||||
|
val = d
|
||||||
|
for k in keys:
|
||||||
|
val = val.get(k, '')
|
||||||
|
if not val:
|
||||||
|
break
|
||||||
|
print(val if val else '')
|
||||||
4
wgctl
4
wgctl
|
|
@ -5,6 +5,10 @@ source "$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)/core.sh"
|
||||||
|
|
||||||
LOG_LEVEL=DEBUG
|
LOG_LEVEL=DEBUG
|
||||||
|
|
||||||
|
WGCTL_VERSION="0.7.1"
|
||||||
|
|
||||||
|
function wgctl::version() { echo "$WGCTL_VERSION"; }
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Modules
|
# Modules
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue