Merge feature/json-output: --json output, command mixin system (v0.6.0)
This commit is contained in:
commit
1fa40c1e25
19 changed files with 698 additions and 8 deletions
|
|
@ -14,6 +14,8 @@ function cmd::activity::on_load() {
|
|||
flag::register --hours
|
||||
flag::register --type
|
||||
flag::register --dropped
|
||||
|
||||
command::mixin json_output
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -70,6 +72,11 @@ function cmd::activity::run() {
|
|||
esac
|
||||
done
|
||||
|
||||
if command::json; then
|
||||
cmd::activity::_output_json "$hours"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Resolve peer name if type provided
|
||||
if [[ -n "$filter_peer" && -n "$filter_type" ]]; then
|
||||
filter_peer=$(peers::resolve_and_require "$filter_peer" "$filter_type") || return 1
|
||||
|
|
@ -188,3 +195,51 @@ function cmd::activity::run() {
|
|||
echo ""
|
||||
}
|
||||
|
||||
function cmd::activity::_output_json() {
|
||||
local hours="${1:-24}"
|
||||
local data
|
||||
data=$(json::activity_aggregate \
|
||||
"$(ctx::fw_events_log)" "$(ctx::events_log)" \
|
||||
"$(config::interface)" "$(ctx::net)" \
|
||||
"$(ctx::clients)" "$(ctx::meta)" \
|
||||
"$hours" "" "" 2>/dev/null)
|
||||
|
||||
local -a peers=()
|
||||
local current_peer="" current_services=""
|
||||
local -a current_svc_list=()
|
||||
|
||||
while IFS='|' read -r record_type rest; do
|
||||
case "$record_type" in
|
||||
peer)
|
||||
# Flush previous peer
|
||||
if [[ -n "$current_peer" ]]; then
|
||||
local svc_array
|
||||
svc_array=$(printf '%s\n' "${current_svc_list[@]:-}" | paste -sd ',' -)
|
||||
peers+=("${current_peer},\"services\":[${svc_array:-}]}")
|
||||
current_svc_list=()
|
||||
fi
|
||||
local name rx tx drops
|
||||
IFS='|' read -r name rx tx drops <<< "$rest"
|
||||
current_peer=$(printf '{"name":"%s","rx":%s,"tx":%s,"drops":%s' \
|
||||
"$name" "$rx" "$tx" "$drops")
|
||||
;;
|
||||
service)
|
||||
local peer dest count
|
||||
IFS='|' read -r peer dest count <<< "$rest"
|
||||
current_svc_list+=("$(printf '{"dest":"%s","drops":%s}' "$dest" "$count")")
|
||||
;;
|
||||
esac
|
||||
done <<< "$data"
|
||||
|
||||
# Flush last peer
|
||||
if [[ -n "$current_peer" ]]; then
|
||||
local svc_array
|
||||
svc_array=$(printf '%s\n' "${current_svc_list[@]:-}" | paste -sd ',' -)
|
||||
peers+=("${current_peer},\"services\":[${svc_array:-}]}")
|
||||
fi
|
||||
|
||||
local count=${#peers[@]}
|
||||
local array
|
||||
array=$(printf '%s\n' "${peers[@]:-}" | paste -sd ',' -)
|
||||
printf '{"peers":[%s]}' "${array:-}" | json::envelope "activity" "$count"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ function cmd::group::on_load() {
|
|||
flag::register --force
|
||||
flag::register --all
|
||||
flag::register --dry-run
|
||||
command::mixin json_output
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -83,6 +84,11 @@ function cmd::group::run() {
|
|||
local subcmd="${1:-help}"
|
||||
shift || true
|
||||
|
||||
if command::json; then
|
||||
cmd::group::_output_json
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$subcmd" in
|
||||
list|ls) cmd::group::list "$@" ;;
|
||||
show) cmd::group::show "$@" ;;
|
||||
|
|
@ -908,4 +914,23 @@ function cmd::group::purge_stale() {
|
|||
log::wg_success "${action} ${total_removed} stale peer(s)..."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function cmd::group::_output_json() {
|
||||
local groups_dir
|
||||
groups_dir="$(ctx::groups)"
|
||||
local data
|
||||
data=$(json::group_list_data "$groups_dir" "$(ctx::blocks)" "$(ctx::clients)" 2>/dev/null)
|
||||
|
||||
local -a groups=()
|
||||
while IFS='|' read -r name desc peer_count blocked_count; do
|
||||
[[ -z "$name" ]] && continue
|
||||
groups+=("$(printf '{"name":"%s","desc":"%s","peer_count":%s,"blocked_count":%s}' \
|
||||
"$name" "$desc" "$peer_count" "$blocked_count")")
|
||||
done <<< "$data"
|
||||
|
||||
local count=${#groups[@]}
|
||||
local array
|
||||
array=$(printf '%s\n' "${groups[@]:-}" | paste -sd ',' -)
|
||||
printf '{"groups":[%s]}' "${array:-}" | json::envelope "group list" "$count"
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ function cmd::hosts::on_load() {
|
|||
flag::register --tag
|
||||
flag::register --tags
|
||||
flag::register --force
|
||||
|
||||
command::mixin json_output
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -71,6 +73,12 @@ EOF
|
|||
function cmd::hosts::run() {
|
||||
local subcmd="${1:-list}"
|
||||
shift || true
|
||||
|
||||
if command::json; then
|
||||
cmd::hosts::_output_json
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$subcmd" in
|
||||
list) cmd::hosts::list "$@" ;;
|
||||
show) cmd::hosts::show "$@" ;;
|
||||
|
|
@ -288,4 +296,30 @@ function cmd::hosts::rm() {
|
|||
|
||||
json::hosts_remove "$(ctx::hosts)" "$entry_type" "$key"
|
||||
log::wg_success "Removed ${entry_type}: ${key}"
|
||||
}
|
||||
|
||||
function cmd::hosts::_output_json() {
|
||||
local data
|
||||
data=$(json::hosts_list "$(ctx::hosts)" 2>/dev/null)
|
||||
|
||||
local -a hosts=()
|
||||
while IFS='|' read -r type ip name desc tags; do
|
||||
[[ -z "$type" ]] && continue
|
||||
|
||||
local tags_json="[]"
|
||||
if [[ -n "$tags" ]]; then
|
||||
local tags_array
|
||||
tags_array=$(echo "$tags" | tr ',' '\n' | \
|
||||
while IFS= read -r t; do [[ -n "$t" ]] && printf '"%s",' "$t"; done | sed 's/,$//')
|
||||
tags_json="[${tags_array}]"
|
||||
fi
|
||||
|
||||
hosts+=("$(printf '{"type":"%s","ip":"%s","name":"%s","desc":"%s","tags":%s}' \
|
||||
"$type" "$ip" "$name" "$desc" "$tags_json")")
|
||||
done <<< "$data"
|
||||
|
||||
local count=${#hosts[@]}
|
||||
local array
|
||||
array=$(printf '%s\n' "${hosts[@]:-}" | paste -sd ',' -)
|
||||
printf '{"hosts":[%s]}' "${array:-}" | json::envelope "hosts list" "$count"
|
||||
}
|
||||
|
|
@ -20,9 +20,7 @@ function cmd::identity::on_load() {
|
|||
flag::register --peer
|
||||
flag::register --dry-run
|
||||
flag::register --force
|
||||
# rule subcommand flags
|
||||
flag::register --rule
|
||||
# options subcommand flags
|
||||
flag::register --policy
|
||||
flag::register --set-strict-rule
|
||||
flag::register --unset-strict-rule
|
||||
|
|
@ -31,6 +29,8 @@ function cmd::identity::on_load() {
|
|||
flag::register --field
|
||||
flag::register --value
|
||||
flag::register --migrate
|
||||
|
||||
command::mixin json_output
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -78,6 +78,11 @@ function cmd::identity::run() {
|
|||
local subcmd="${1:-list}"
|
||||
shift || true
|
||||
|
||||
if command::json && [[ "$subcmd" == "list" ]]; then
|
||||
cmd::identity::_output_json
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$subcmd" in
|
||||
list) cmd::identity::_list "$@" ;;
|
||||
show) cmd::identity::_show "$@" ;;
|
||||
|
|
@ -565,4 +570,41 @@ function cmd::identity::_options() {
|
|||
if ! $changed; then
|
||||
cmd::identity::_rule_show --name "$name"
|
||||
fi
|
||||
}
|
||||
}
|
||||
|
||||
function cmd::identity::_output_json() {
|
||||
local data
|
||||
data=$(identity::list_data 2>/dev/null)
|
||||
|
||||
local -a identities=()
|
||||
while IFS='|' read -r name peer_count types rules policy; do
|
||||
[[ -z "$name" ]] && continue
|
||||
|
||||
# Build rules array
|
||||
local rules_json="[]"
|
||||
if [[ -n "$rules" ]]; then
|
||||
local rules_array
|
||||
rules_array=$(echo "$rules" | tr ',' '\n' | \
|
||||
while IFS= read -r r; do [[ -n "$r" ]] && printf '"%s",' "$r"; done | sed 's/,$//')
|
||||
rules_json="[${rules_array}]"
|
||||
fi
|
||||
|
||||
# Build types array (was comma-separated string)
|
||||
local types_json="[]"
|
||||
if [[ -n "$types" ]]; then
|
||||
local types_array
|
||||
types_array=$(echo "$types" | tr ',' '\n' | \
|
||||
while IFS= read -r t; do [[ -n "$t" ]] && printf '"%s",' "$t"; done | sed 's/,$//')
|
||||
types_json="[${types_array}]"
|
||||
fi
|
||||
|
||||
identities+=("$(printf '{"name":"%s","peer_count":%s,"types":%s,"rules":%s,"policy":"%s"}' \
|
||||
"$name" "$peer_count" "$types_json" "$rules_json" "$policy")")
|
||||
done <<< "$data"
|
||||
|
||||
local count=${#identities[@]}
|
||||
local array
|
||||
array=$(printf '%s\n' "${identities[@]:-}" | paste -sd ',' -)
|
||||
printf '{"identities":[%s]}' "${array:-}" | json::envelope "identity list" "$count"
|
||||
}
|
||||
|
||||
|
|
@ -5,6 +5,8 @@ function cmd::inspect::on_load() {
|
|||
flag::register --type
|
||||
flag::register --config
|
||||
flag::register --qr
|
||||
|
||||
command::mixin json_output
|
||||
}
|
||||
|
||||
function cmd::inspect::help() {
|
||||
|
|
@ -329,6 +331,11 @@ function cmd::inspect::run() {
|
|||
|
||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
|
||||
if command::json; then
|
||||
cmd::inspect::_output_json "$name"
|
||||
return 0
|
||||
fi
|
||||
|
||||
load_command list
|
||||
|
||||
log::section "Inspect: ${name}"
|
||||
|
|
@ -351,4 +358,71 @@ function cmd::inspect::run() {
|
|||
fi
|
||||
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# JSON (API consumption)
|
||||
# ============================================
|
||||
|
||||
function cmd::inspect::_output_json() {
|
||||
local name="${1:-}"
|
||||
|
||||
local ip type rule allowed_ips public_key is_blocked status
|
||||
ip=$(peers::get_ip "$name")
|
||||
type=$(peers::get_type "$name")
|
||||
rule=$(peers::get_meta "$name" "rule")
|
||||
allowed_ips=$(grep "^AllowedIPs" "$(ctx::clients)/${name}.conf" 2>/dev/null | \
|
||||
awk '{print $3}' | tr -d ',')
|
||||
public_key=$(keys::public "$name" 2>/dev/null || echo "")
|
||||
peers::is_blocked "$name" && is_blocked="true" || is_blocked="false"
|
||||
|
||||
# Handshake status
|
||||
local handshake_ts=0
|
||||
handshake_ts=$(wg show "$(config::interface)" latest-handshakes 2>/dev/null | \
|
||||
grep "$public_key" | awk '{print $2}') || handshake_ts=0
|
||||
|
||||
local last_ts
|
||||
last_ts=$(peers::get_meta "$name" "last_ts" 2>/dev/null || echo "")
|
||||
|
||||
local conn_state
|
||||
conn_state=$(peers::connection_state "$is_blocked" "false" \
|
||||
"${handshake_ts:-0}" "${last_ts:-}" | cut -d'|' -f1)
|
||||
|
||||
# Groups
|
||||
local groups_json="[]"
|
||||
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)
|
||||
[[ ${#group_list[@]} -gt 0 ]] && \
|
||||
groups_json="[$(printf '%s,' "${group_list[@]}" | sed 's/,$//')]"
|
||||
|
||||
# Identity
|
||||
local identity
|
||||
identity=$(peers::get_identity "$name" 2>/dev/null || echo "")
|
||||
|
||||
# Rule extends
|
||||
local rule_extends="[]"
|
||||
if [[ -n "$rule" ]]; then
|
||||
local rule_file
|
||||
rule_file=$(json::find_rule_file "$(ctx::rules)" "$rule" 2>/dev/null)
|
||||
if [[ -n "$rule_file" ]]; then
|
||||
local -a extends=()
|
||||
while IFS= read -r ext; do
|
||||
[[ -n "$ext" ]] && extends+=("\"$ext\"")
|
||||
done < <(json::get "$rule_file" "extends" 2>/dev/null)
|
||||
[[ ${#extends[@]} -gt 0 ]] && \
|
||||
rule_extends="[$(printf '%s,' "${extends[@]}" | sed 's/,$//')]"
|
||||
fi
|
||||
fi
|
||||
|
||||
local data
|
||||
data=$(printf '{"name":"%s","ip":"%s","type":"%s","rule":"%s","rule_extends":%s,"allowed_ips":"%s","public_key":"%s","is_blocked":%s,"status":"%s","identity":"%s","groups":%s}' \
|
||||
"$name" "$ip" "$type" \
|
||||
"${rule:-}" "$rule_extends" \
|
||||
"${allowed_ips:-}" "$public_key" \
|
||||
"$is_blocked" "$conn_state" \
|
||||
"${identity:-}" "$groups_json")
|
||||
|
||||
printf '%s' "$data" | json::envelope "inspect" "1"
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ function cmd::list::on_load() {
|
|||
flag::register --allowed
|
||||
flag::register --detailed
|
||||
flag::register --name
|
||||
command::mixin json_output
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -209,6 +210,11 @@ function cmd::list::run() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
if command::json; then
|
||||
cmd::list::_output_json "$collected_rows"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if $detailed; then
|
||||
cmd::list::_render_detailed "$collected_rows"
|
||||
cmd::list::_render_summary_from_rows "$collected_rows"
|
||||
|
|
@ -662,4 +668,32 @@ function cmd::list::_build_group_summary() {
|
|||
function cmd::list::_show_client_safe() {
|
||||
local name="$1"
|
||||
cmd::list::show_client "$name" || true
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# JSON (API consumption)
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_output_json() {
|
||||
local rows="${1:-}"
|
||||
local -a peers=()
|
||||
|
||||
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||
[[ -z "$name" ]] && continue
|
||||
|
||||
# Escape strings for JSON
|
||||
local peer_json
|
||||
peer_json=$(printf '{"name":"%s","ip":"%s","type":"%s","rule":"%s","group":"%s","status":"%s","last_seen":"%s","is_blocked":%s,"is_restricted":%s}' \
|
||||
"$name" "$ip" "$type" \
|
||||
"${rule}" "${group}" \
|
||||
"$status" "$last_seen" \
|
||||
"$is_blocked" "$is_restricted")
|
||||
peers+=("$peer_json")
|
||||
done <<< "$rows"
|
||||
|
||||
local count=${#peers[@]}
|
||||
local array
|
||||
# Join array with commas
|
||||
array=$(printf '%s\n' "${peers[@]}" | paste -sd ',' -)
|
||||
printf '{"peers":[%s]}' "$array" | json::envelope "list" "$count"
|
||||
}
|
||||
34
commands/mixins/MIXIN_TEMPLATE.mixin.sh.template
Normal file
34
commands/mixins/MIXIN_TEMPLATE.mixin.sh.template
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/mixins/<name>.mixin.sh
|
||||
# Template for creating a new mixin
|
||||
#
|
||||
# 1. Copy this file to core/mixins/ (framework) or commands/mixins/ (wgctl-specific)
|
||||
# 2. Replace <name> with your mixin name (e.g. peer_filter, time_filter)
|
||||
# 3. Replace <FLAG> with your flag (e.g. --name, --since)
|
||||
# 4. Add state variable and accessor function
|
||||
# 5. In on_load: command::mixin <name>
|
||||
|
||||
# State variable
|
||||
_COMMAND_<NAME>=false # or "" for string values
|
||||
|
||||
# Called when command::mixin <name> is used in on_load
|
||||
function command::mixin::<name>::register() {
|
||||
flag::register --<flag>
|
||||
# Add more flag::register calls if needed
|
||||
}
|
||||
|
||||
# Called before each command invocation to reset state
|
||||
function command::mixin::<name>::reset() {
|
||||
_COMMAND_<NAME>=false
|
||||
}
|
||||
|
||||
# Called for each arg — return 0 if consumed, 1 if not
|
||||
function command::mixin::<name>::process() {
|
||||
case "$1" in
|
||||
--<flag>) _COMMAND_<NAME>=true; return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# Public accessor — used by commands
|
||||
function command::<name>() { [[ "${_COMMAND_<NAME>:-false}" == "true" ]]; }
|
||||
|
|
@ -8,6 +8,8 @@ function cmd::net::on_load() {
|
|||
flag::register --tag
|
||||
flag::register --detailed
|
||||
flag::register --force
|
||||
|
||||
command::mixin json_output
|
||||
}
|
||||
|
||||
function cmd::net::help() {
|
||||
|
|
@ -58,6 +60,12 @@ EOF
|
|||
function cmd::net::run() {
|
||||
local subcmd="${1:-list}"
|
||||
shift || true
|
||||
|
||||
if command::json; then
|
||||
cmd::net::_output_json
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$subcmd" in
|
||||
list) cmd::net::list "$@" ;;
|
||||
show) cmd::net::show "$@" ;;
|
||||
|
|
@ -304,4 +312,31 @@ function cmd::net::rm() {
|
|||
json::net_remove "$(ctx::net)" "$name"
|
||||
log::wg_success "Removed: ${name}"
|
||||
return 0
|
||||
}
|
||||
|
||||
function cmd::net::_output_json() {
|
||||
local net_file
|
||||
net_file="$(ctx::net)"
|
||||
local data
|
||||
data=$(json::net_list "$net_file" 2>/dev/null)
|
||||
|
||||
local -a services=()
|
||||
while IFS='|' read -r name ip desc tags port_count; do
|
||||
[[ -z "$name" ]] && continue
|
||||
# Build tags array
|
||||
local tags_json="[]"
|
||||
if [[ -n "$tags" ]]; then
|
||||
local tags_array
|
||||
tags_array=$(echo "$tags" | tr ',' '\n' | \
|
||||
while IFS= read -r t; do [[ -n "$t" ]] && printf '"%s",' "$t"; done | sed 's/,$//')
|
||||
tags_json="[${tags_array}]"
|
||||
fi
|
||||
services+=("$(printf '{"name":"%s","ip":"%s","desc":"%s","tags":%s,"port_count":%s}' \
|
||||
"$name" "$ip" "$desc" "$tags_json" "$port_count")")
|
||||
done <<< "$data"
|
||||
|
||||
local count=${#services[@]}
|
||||
local array
|
||||
array=$(printf '%s\n' "${services[@]:-}" | paste -sd ',' -)
|
||||
printf '{"services":[%s]}' "${array:-}" | json::envelope "net list" "$count"
|
||||
}
|
||||
|
|
@ -27,6 +27,8 @@ function cmd::policy::on_load() {
|
|||
flag::register --desc
|
||||
flag::register --field
|
||||
flag::register --value
|
||||
|
||||
command::mixin json_output
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -79,6 +81,11 @@ function cmd::policy::run() {
|
|||
local subcmd="${1:-list}"
|
||||
shift || true
|
||||
|
||||
if command::json; then
|
||||
cmd::policy::_output_json
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$subcmd" in
|
||||
list) cmd::policy::_list "$@" ;;
|
||||
show) cmd::policy::_show "$@" ;;
|
||||
|
|
@ -244,4 +251,28 @@ function cmd::policy::_set() {
|
|||
|
||||
json::policy_set_field "$(ctx::policies)" "$name" "$field" "$value"
|
||||
log::ok "Policy '${name}': ${field} = ${value}"
|
||||
}
|
||||
|
||||
function cmd::policy::_output_json() {
|
||||
local data
|
||||
data=$(policy::list_data 2>/dev/null)
|
||||
|
||||
local -a policies=()
|
||||
while IFS='|' read -r name tunnel_mode default_rule strict_rule auto_apply desc; do
|
||||
[[ -z "$name" ]] && continue
|
||||
|
||||
local strict_json="false"
|
||||
[[ "$strict_rule" == "true" ]] && strict_json="true"
|
||||
local auto_json="true"
|
||||
[[ "$auto_apply" == "false" ]] && auto_json="false"
|
||||
|
||||
policies+=("$(printf '{"name":"%s","tunnel_mode":"%s","default_rule":"%s","strict_rule":%s,"auto_apply":%s,"desc":"%s"}' \
|
||||
"$name" "$tunnel_mode" "${default_rule:-}" \
|
||||
"$strict_json" "$auto_json" "$desc")")
|
||||
done <<< "$data"
|
||||
|
||||
local count=${#policies[@]}
|
||||
local array
|
||||
array=$(printf '%s\n' "${policies[@]:-}" | paste -sd ',' -)
|
||||
printf '{"policies":[%s]}' "${array:-}" | json::envelope "policy list" "$count"
|
||||
}
|
||||
|
|
@ -31,6 +31,8 @@ function cmd::rule::on_load() {
|
|||
flag::register --force
|
||||
flag::register --type
|
||||
flag::register --all
|
||||
|
||||
command::mixin json_output
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -116,6 +118,11 @@ function cmd::rule::run() {
|
|||
local subcmd="${1:-help}"
|
||||
shift || true
|
||||
|
||||
if command::json; then
|
||||
cmd::rule::_output_json
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$subcmd" in
|
||||
list|ls) cmd::rule::list "$@" ;;
|
||||
show|inspect) cmd::rule::show "$@" ;;
|
||||
|
|
@ -671,4 +678,38 @@ function cmd::rule::reapply() {
|
|||
rule::require_exists "$name" || return 1
|
||||
rule::reapply_all "$name"
|
||||
log::wg_success "Rule '${name}' reapplied"
|
||||
}
|
||||
|
||||
function cmd::rule::_output_json() {
|
||||
local rules_dir
|
||||
rules_dir="$(ctx::rules)"
|
||||
local data
|
||||
data=$(json::rule_list_data "$rules_dir" "$(ctx::meta)" 2>/dev/null)
|
||||
|
||||
local -a rules=()
|
||||
while IFS='|' read -r name desc n_allows n_blocks peer_count extends is_base group; do
|
||||
[[ -z "$name" ]] && continue
|
||||
|
||||
# Build extends array
|
||||
local extends_json="[]"
|
||||
if [[ -n "$extends" ]]; then
|
||||
local ext_array
|
||||
ext_array=$(echo "$extends" | tr ',' '\n' | \
|
||||
while IFS= read -r e; do [[ -n "$e" ]] && printf '"%s",' "$e"; done | sed 's/,$//')
|
||||
extends_json="[${ext_array}]"
|
||||
fi
|
||||
|
||||
# Convert Python bool to JSON bool
|
||||
local is_base_json="false"
|
||||
[[ "$is_base" == "True" ]] && is_base_json="true"
|
||||
|
||||
rules+=("$(printf '{"name":"%s","desc":"%s","allows":%s,"blocks":%s,"peer_count":%s,"extends":%s,"is_base":%s,"group":"%s"}' \
|
||||
"$name" "$desc" "$n_allows" "$n_blocks" "$peer_count" \
|
||||
"$extends_json" "$is_base_json" "$group")")
|
||||
done <<< "$data"
|
||||
|
||||
local count=${#rules[@]}
|
||||
local array
|
||||
array=$(printf '%s\n' "${rules[@]:-}" | paste -sd ',' -)
|
||||
printf '{"rules":[%s]}' "${array:-}" | json::envelope "rule list" "$count"
|
||||
}
|
||||
|
|
@ -22,6 +22,8 @@ function cmd::subnet::on_load() {
|
|||
flag::register --desc
|
||||
flag::register --group
|
||||
flag::register --new-name
|
||||
|
||||
command::mixin json_output
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -65,6 +67,11 @@ function cmd::subnet::run() {
|
|||
local subcmd="${1:-list}"
|
||||
shift || true
|
||||
|
||||
if command::json; then
|
||||
cmd::subnet::_output_json
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$subcmd" in
|
||||
list) cmd::subnet::_list "$@" ;;
|
||||
show) cmd::subnet::_show "$@" ;;
|
||||
|
|
@ -291,4 +298,26 @@ function cmd::subnet::_validate_cidr() {
|
|||
log::error "Invalid CIDR format: '${cidr}'"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
function cmd::subnet::_output_json() {
|
||||
local data
|
||||
data=$(subnet::list_data 2>/dev/null)
|
||||
|
||||
local -a subnets=()
|
||||
while IFS='|' read -r type cidr display_name tunnel_mode desc is_group group_parent; do
|
||||
[[ -z "$type" ]] && continue
|
||||
|
||||
local is_group_json="false"
|
||||
[[ "$is_group" == "true" ]] && is_group_json="true"
|
||||
|
||||
subnets+=("$(printf '{"type":"%s","cidr":"%s","display_name":"%s","tunnel_mode":"%s","desc":"%s","is_group":%s,"group_parent":"%s"}' \
|
||||
"$type" "$cidr" "$display_name" "$tunnel_mode" \
|
||||
"$desc" "$is_group_json" "${group_parent:-}")")
|
||||
done <<< "$data"
|
||||
|
||||
local count=${#subnets[@]}
|
||||
local array
|
||||
array=$(printf '%s\n' "${subnets[@]:-}" | paste -sd ',' -)
|
||||
printf '{"subnets":[%s]}' "${array:-}" | json::envelope "subnet list" "$count"
|
||||
}
|
||||
|
|
@ -132,8 +132,11 @@ function cmd::test::run_all_integration_sections() {
|
|||
cmd::test::section_net
|
||||
cmd::test::section_subnet
|
||||
cmd::test::section_identity
|
||||
cmd::test::section_activity
|
||||
cmd::test::section_hosts
|
||||
cmd::test::section_peer_cmd
|
||||
cmd::test::section_group_purge
|
||||
cmd::test::section_logs_clean
|
||||
}
|
||||
|
||||
function cmd::test::section_list() {
|
||||
|
|
@ -145,6 +148,12 @@ function cmd::test::section_list() {
|
|||
cmd::test::run_cmd "list --type phone" "phone" list --type phone
|
||||
cmd::test::run_cmd "list --detailed" "rule:" list --detailed
|
||||
cmd::test::run_cmd "list --name phone-nuno" "phone-nuno" list --name phone-nuno
|
||||
cmd::test::run_cmd "list --json" '"ok":true' list --json
|
||||
cmd::test::run_cmd "list --json has peers" '"peers":' list --json
|
||||
cmd::test::run_cmd "list --json has meta" '"meta":' list --json
|
||||
cmd::test::run_cmd "list --json peer name" '"name":' list --json
|
||||
cmd::test::run_cmd "list --json peer ip" '"ip":' list --json
|
||||
cmd::test::run_cmd "list --json peer status" '"status":' list --json
|
||||
}
|
||||
|
||||
function cmd::test::section_inspect() {
|
||||
|
|
@ -153,6 +162,10 @@ function cmd::test::section_inspect() {
|
|||
cmd::test::run_cmd "inspect --name nuno --type phone" "IP:" inspect --name nuno --type phone
|
||||
cmd::test::run_cmd "inspect --name phone-nuno --config" "PrivateKey" inspect --name phone-nuno --config
|
||||
cmd::test::run_cmd_fails "inspect nonexistent" inspect --name nonexistent-peer
|
||||
cmd::test::run_cmd "inspect --json" '"ok":true' inspect --name phone-nuno --json
|
||||
cmd::test::run_cmd "inspect --json rule" '"rule":' inspect --name phone-nuno --json
|
||||
cmd::test::run_cmd "inspect --json identity" '"identity":' inspect --name phone-nuno --json
|
||||
cmd::test::run_cmd "inspect --json groups" '"groups":' inspect --name phone-nuno --json
|
||||
}
|
||||
|
||||
function cmd::test::section_config() {
|
||||
|
|
@ -168,6 +181,7 @@ function cmd::test::section_rules() {
|
|||
cmd::test::run_cmd "rule show --name guest" "Description" rule show --name guest
|
||||
cmd::test::run_cmd "rule show --name user" "Description" rule show --name user
|
||||
cmd::test::run_cmd "rule show --name admin" "Description" rule show --name admin
|
||||
cmd::test::run_cmd "rule list --json" '"rules":' rule list --json
|
||||
cmd::test::run_cmd_fails "rule show nonexistent" rule show --name nonexistent
|
||||
}
|
||||
|
||||
|
|
@ -175,6 +189,7 @@ function cmd::test::section_groups() {
|
|||
test::section "Groups"
|
||||
cmd::test::run_cmd "group list" "Groups" group list
|
||||
cmd::test::run_cmd "group show --name family" "Peers:" group show --name family
|
||||
cmd::test::run_cmd "group list --json" '"groups":' group list --json
|
||||
cmd::test::run_cmd_fails "group show nonexistent" group show --name nonexistent
|
||||
}
|
||||
|
||||
|
|
@ -196,6 +211,10 @@ function cmd::test::section_logs() {
|
|||
cmd::test::run_cmd "logs --fw --since 2099-01-01" "No logs" logs --fw --since "2099-01-01"
|
||||
cmd::test::run_cmd "logs --wg --event attempt" "" logs --wg --event attempt
|
||||
cmd::test::run_cmd "logs --detailed" "" logs --detailed
|
||||
cmd::test::run_cmd "logs --resolved" "" logs --resolved
|
||||
cmd::test::run_cmd "logs --ascending" "" logs --ascending
|
||||
cmd::test::run_cmd "logs --descending" "" logs --descending
|
||||
cmd::test::run_cmd "logs --wg --ascending" "" logs --wg --ascending
|
||||
}
|
||||
|
||||
function cmd::test::section_fw() {
|
||||
|
|
@ -222,8 +241,9 @@ function cmd::test::section_net() {
|
|||
cmd::test::run_cmd "net add port again" "Added" net add --name test-svc:web --port 9999:tcp
|
||||
cmd::test::run_cmd "net rm all ports" "Removed" net rm --name test-svc:ports --force
|
||||
cmd::test::run_cmd "net rm service" "Removed" net rm --name test-svc --force
|
||||
cmd::test::run_cmd_fails "net show nonexistent" net show --name nonexistent-svc
|
||||
cmd::test::run_cmd_fails "net add port no service" net add --name nonexistent:web --port 80:tcp
|
||||
cmd::test::run_cmd "net list --json" '"services":' net list --json
|
||||
cmd::test::run_cmd_fails "net show nonexistent" net show --name nonexistent-svc
|
||||
cmd::test::run_cmd_fails "net add port no service" net add --name nonexistent:web --port 80:tcp
|
||||
}
|
||||
|
||||
function cmd::test::section_subnet() {
|
||||
|
|
@ -255,9 +275,14 @@ function cmd::test::section_identity() {
|
|||
cmd::test::run_cmd "identity list" "" identity list
|
||||
cmd::test::run_cmd "identity migrate --dry-run" "Dry run" identity migrate --dry-run
|
||||
cmd::test::run_cmd "identity show nuno" "nuno" identity show --name nuno
|
||||
cmd::test::run_cmd "identity list --json" '"identities":' identity list --json
|
||||
cmd::test::run_cmd_fails "identity show nonexistent" identity show --name nonexistent
|
||||
}
|
||||
|
||||
function cmd::test::section_activity() {
|
||||
cmd::test::run_cmd "activity --json" '"peers":' activity --json
|
||||
}
|
||||
|
||||
function cmd::test::section_hosts() {
|
||||
test::section "Hosts"
|
||||
|
||||
|
|
@ -299,4 +324,25 @@ function cmd::test::section_peer_cmd() {
|
|||
"$WGCTL_BINARY" peer update-tunnel --name phone-testunit --mode split > /dev/null 2>&1 || true
|
||||
"$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
|
||||
function cmd::test::section_group_purge() {
|
||||
test::section "Group: purge-stale"
|
||||
|
||||
# dry-run should not modify anything
|
||||
cmd::test::run_cmd "purge-stale --all --dry-run" \
|
||||
"[dry-run]" \
|
||||
group purge-stale --all --dry-run --force
|
||||
|
||||
# single group dry-run
|
||||
cmd::test::run_cmd "purge-stale --name family --dry-run" \
|
||||
"[dry-run]" \
|
||||
group purge-stale --name family --dry-run --force
|
||||
}
|
||||
|
||||
function cmd::test::section_logs_clean() {
|
||||
test::section "Logs: clean"
|
||||
|
||||
cmd::test::run_cmd "logs clean --force" \
|
||||
"keepalive" \
|
||||
logs clean --force
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ function cmd::test::run_all_unit_sections() {
|
|||
cmd::test::unit_config
|
||||
cmd::test::unit_parse_since
|
||||
cmd::test::unit_group_status
|
||||
cmd::test::unit_json_output
|
||||
}
|
||||
|
||||
function cmd::test::unit_subnet() {
|
||||
|
|
@ -219,4 +220,30 @@ function cmd::test::unit_group_status() {
|
|||
|
||||
IFS='|' read -r color str <<< "$(ui::group::status 4 2)"
|
||||
cmd::test::assert "group status partial str" "$str" "partial (2/4)"
|
||||
}
|
||||
|
||||
function cmd::test::unit_json_output() {
|
||||
test::section "Unit: JSON output"
|
||||
|
||||
# json::envelope produces valid structure
|
||||
local result
|
||||
result=$(echo '{"peers":[]}' | json::envelope "list" "0")
|
||||
cmd::test::assert "envelope ok field" "$(echo "$result" | grep -o '"ok":true')" '"ok":true'
|
||||
cmd::test::assert "envelope command field" "$(echo "$result" | grep -o '"command":"list"')" '"command":"list"'
|
||||
cmd::test::assert "envelope meta field" "$(echo "$result" | grep -o '"meta":')" '"meta":'
|
||||
cmd::test::assert "envelope count field" "$(echo "$result" | grep -o '"count":0')" '"count":0'
|
||||
|
||||
# json::error_envelope
|
||||
local err_result
|
||||
err_result=$(json::error_envelope "inspect" "Peer not found")
|
||||
cmd::test::assert "error envelope ok false" \
|
||||
"$(echo "$err_result" | grep -o '"ok":false')" '"ok":false'
|
||||
cmd::test::assert "error envelope error field" \
|
||||
"$(echo "$err_result" | grep -o '"error":')" '"error":'
|
||||
|
||||
# command::json mixin accessor
|
||||
_COMMAND_JSON=true
|
||||
cmd::test::assert_true "command::json true" "command::json"
|
||||
_COMMAND_JSON=false
|
||||
cmd::test::assert_false "command::json false" "command::json"
|
||||
}
|
||||
5
core.sh
5
core.sh
|
|
@ -11,9 +11,12 @@ source "${WGCTL_DIR}/core/context.sh"
|
|||
source "${WGCTL_DIR}/core/utils.sh"
|
||||
source "${WGCTL_DIR}/core/module.sh"
|
||||
source "${WGCTL_DIR}/core/command.sh"
|
||||
source "${WGCTL_DIR}/core/command_mixins.sh"
|
||||
source "${WGCTL_DIR}/core/flag.sh"
|
||||
source "${WGCTL_DIR}/core/json.sh"
|
||||
source "${WGCTL_DIR}/core/ui.sh"
|
||||
source "${WGCTL_DIR}/core/color.sh"
|
||||
source "${WGCTL_DIR}/core/fmt.sh"
|
||||
source "${WGCTL_DIR}/core/test/test.sh"
|
||||
source "${WGCTL_DIR}/core/test/test.sh"
|
||||
|
||||
command::_load_mixins
|
||||
|
|
|
|||
|
|
@ -39,10 +39,17 @@ function command::exists() { command::has_function "$1" run; }
|
|||
function command::run() {
|
||||
local cmd="$1"
|
||||
shift
|
||||
|
||||
command::_reset_mixin_state # reset values only, keep _ACTIVE_MIXINS
|
||||
|
||||
local -a args=("$@")
|
||||
command::_preprocess_flags args
|
||||
|
||||
local fn
|
||||
fn=$(command::fn "$cmd" run)
|
||||
core::call_function "$fn" "$@"
|
||||
core::call_function "$fn" ${args[@]+"${args[@]}"}
|
||||
}
|
||||
|
||||
|
||||
function core::call_function() {
|
||||
local fn="$1"
|
||||
|
|
|
|||
111
core/command_mixins.sh
Normal file
111
core/command_mixins.sh
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/command_mixins.sh
|
||||
# Mixin infrastructure — loads mixin files and provides command::mixin
|
||||
|
||||
# ============================================
|
||||
# Active mixin tracking (per-process)
|
||||
# ============================================
|
||||
|
||||
declare -a _ACTIVE_MIXINS=()
|
||||
|
||||
# ============================================
|
||||
# Auto-load all mixin files
|
||||
# ============================================
|
||||
|
||||
function command::_load_mixins() {
|
||||
local mixin_file
|
||||
local -a mixin_paths=(
|
||||
"${WGCTL_DIR}/core/mixins/"*.mixin.sh
|
||||
"${WGCTL_DIR}/commands/mixins/"*.mixin.sh
|
||||
)
|
||||
for mixin_file in "${mixin_paths[@]:-}"; do
|
||||
[[ -f "$mixin_file" ]] && source "$mixin_file"
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# command::mixin <name>
|
||||
# Called from cmd::<name>::on_load to opt into a mixin
|
||||
# ============================================
|
||||
|
||||
function command::mixin() {
|
||||
local name="${1:-}"
|
||||
[[ -z "$name" ]] && log::error "command::mixin: missing name" && return 1
|
||||
|
||||
local register_fn="command::mixin::${name}::register"
|
||||
if declare -f "$register_fn" >/dev/null 2>&1; then
|
||||
# Track for reset/process — avoid duplicates
|
||||
local m already=false
|
||||
for m in "${_ACTIVE_MIXINS[@]:-}"; do
|
||||
[[ "$m" == "$name" ]] && already=true && break
|
||||
done
|
||||
$already || _ACTIVE_MIXINS+=("$name")
|
||||
"$register_fn"
|
||||
else
|
||||
log::error "Unknown mixin: ${name} (no command::mixin::${name}::register found)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# command::_reset_mixins
|
||||
# Called by command::run before each invocation
|
||||
# ============================================
|
||||
|
||||
function command::_reset_mixin_state() {
|
||||
# Reset values but keep _ACTIVE_MIXINS populated
|
||||
local m
|
||||
for m in "${_ACTIVE_MIXINS[@]:-}"; do
|
||||
local reset_fn="command::mixin::${m}::reset"
|
||||
declare -f "$reset_fn" >/dev/null 2>&1 && "$reset_fn"
|
||||
done
|
||||
}
|
||||
|
||||
function command::_reset_mixins() {
|
||||
_ACTIVE_MIXINS=()
|
||||
local reset_fn mixin_file
|
||||
# Reset all known mixins regardless of active state
|
||||
for mixin_file in \
|
||||
"${WGCTL_DIR}/core/mixins/"*.mixin.sh \
|
||||
"${WGCTL_DIR}/commands/mixins/"*.mixin.sh; do
|
||||
[[ -f "$mixin_file" ]] || continue
|
||||
local mixin_name
|
||||
mixin_name=$(basename "$mixin_file" .mixin.sh)
|
||||
reset_fn="command::mixin::${mixin_name}::reset"
|
||||
declare -f "$reset_fn" >/dev/null 2>&1 && "$reset_fn"
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# command::_preprocess_flags <args_nameref>
|
||||
# Called by command::run — strips mixin flags from args
|
||||
# ============================================
|
||||
|
||||
function command::_preprocess_flags() {
|
||||
local -n _args_ref="$1"
|
||||
local -a _filtered=()
|
||||
|
||||
if [[ ${#_args_ref[@]} -eq 0 ]]; then
|
||||
return 0 # nothing to process
|
||||
fi
|
||||
|
||||
for _arg in "${_args_ref[@]}"; do
|
||||
local _consumed=false
|
||||
for _mixin in "${_ACTIVE_MIXINS[@]:-}"; do
|
||||
local _process_fn="command::mixin::${_mixin}::process"
|
||||
if declare -f "$_process_fn" >/dev/null 2>&1; then
|
||||
if "$_process_fn" "$_arg"; then
|
||||
_consumed=true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
$_consumed || _filtered+=("$_arg")
|
||||
done
|
||||
|
||||
if [[ ${#_filtered[@]} -gt 0 ]]; then
|
||||
_args_ref=("${_filtered[@]}")
|
||||
else
|
||||
_args_ref=()
|
||||
fi
|
||||
}
|
||||
20
core/json.sh
20
core/json.sh
|
|
@ -124,6 +124,26 @@ function json::peer_history_lookup() { python3 "$JSON_HELPER" peer_history_lo
|
|||
function json::clean_handshakes() { python3 "$JSON_HELPER" clean_handshakes "$@" </dev/null; }
|
||||
function json::batch_resolve() { python3 "$JSON_HELPER" batch_resolve "$(ctx::hosts)" "$(ctx::net)" "$@" </dev/null; }
|
||||
|
||||
# JSON Envelopes
|
||||
function json::envelope() {
|
||||
local command="${1:-}" count="${2:-0}"
|
||||
# Reads JSON array from stdin, wraps in envelope
|
||||
local data
|
||||
data=$(cat)
|
||||
local ts
|
||||
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
printf '{"ok":true,"command":"%s","data":%s,"meta":{"count":%s,"generated_at":"%s"}}\n' \
|
||||
"$command" "$data" "$count" "$ts"
|
||||
}
|
||||
|
||||
function json::error_envelope() {
|
||||
local command="${1:-}" error="${2:-}"
|
||||
local ts
|
||||
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
printf '{"ok":false,"command":"%s","error":"%s","meta":{"generated_at":"%s"}}\n' \
|
||||
"$command" "$error" "$ts"
|
||||
}
|
||||
|
||||
function json::peer_transfer() {
|
||||
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
||||
ACTIVITY_TOTAL_MED="$(config::activity_total_med)" \
|
||||
|
|
|
|||
21
core/mixins/json_output.mixin.sh
Normal file
21
core/mixins/json_output.mixin.sh
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/mixins/json_output.mixin.sh
|
||||
# Adds --json flag support to any command
|
||||
|
||||
_COMMAND_JSON=false
|
||||
|
||||
function command::mixin::json_output::register() {
|
||||
flag::register --json
|
||||
}
|
||||
|
||||
function command::mixin::json_output::reset() {
|
||||
_COMMAND_JSON=false
|
||||
}
|
||||
|
||||
function command::mixin::json_output::process() {
|
||||
[[ "$1" == "--json" ]] && _COMMAND_JSON=true && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# Public accessor
|
||||
function command::json() { [[ "${_COMMAND_JSON:-false}" == "true" ]]; }
|
||||
21
core/mixins/no_color.mixin.sh
Normal file
21
core/mixins/no_color.mixin.sh
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/mixins/no_color.mixin.sh
|
||||
# Adds --no-color flag support to any command
|
||||
|
||||
_COMMAND_NO_COLOR=false
|
||||
|
||||
function command::mixin::no_color::register() {
|
||||
flag::register --no-color
|
||||
}
|
||||
|
||||
function command::mixin::no_color::reset() {
|
||||
_COMMAND_NO_COLOR=false
|
||||
}
|
||||
|
||||
function command::mixin::no_color::process() {
|
||||
[[ "$1" == "--no-color" ]] && _COMMAND_NO_COLOR=true && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# Public accessor
|
||||
function command::no_color() { [[ "${_COMMAND_NO_COLOR:-false}" == "true" ]]; }
|
||||
Loading…
Add table
Reference in a new issue