#!/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 < [options] Import a wgctl JSON export bundle. Options: --file Path to export bundle (required) --peer 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" }