export.command.sh
· 8.9 KiB · Bash
Eredeti
#!/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::block_history)" \
"$(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
}
| 1 | #!/usr/bin/env bash |
| 2 | # commands/export.command.sh |
| 3 | |
| 4 | function 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 | |
| 16 | function cmd::export::help() { |
| 17 | cat <<EOF |
| 18 | Usage: wgctl export [options] |
| 19 | |
| 20 | Export wgctl data as a portable JSON bundle. |
| 21 | |
| 22 | Options: |
| 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 | |
| 33 | Examples: |
| 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 |
| 39 | EOF |
| 40 | } |
| 41 | |
| 42 | function 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 | |
| 106 | function 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 | |
| 189 | function 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 | |
| 206 | function 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) |
| 231 | function 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 | |
| 287 | function 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 | |
| 296 | function cmd::export::_compact_json() { |
| 297 | local file="$1" |
| 298 | python3 -c " |
| 299 | import json, sys |
| 300 | try: |
| 301 | print(json.dumps(json.load(open('${file}')))) |
| 302 | except Exception as e: |
| 303 | print('{}', file=sys.stderr) |
| 304 | " 2>/dev/null |
| 305 | } |