tools: add ppf-status cluster overview
This commit is contained in:
@@ -42,6 +42,7 @@ tools/
|
||||
ppf-logs view container logs
|
||||
ppf-service manage containers (status/start/stop/restart)
|
||||
ppf-db database operations (stats/purge-proxies/vacuum)
|
||||
ppf-status cluster overview (containers, workers, queue)
|
||||
playbooks/
|
||||
deploy.yml ansible playbook (sync, compose, restart)
|
||||
inventory.ini hosts with WireGuard IPs + SSH key
|
||||
@@ -107,6 +108,13 @@ ppf-db purge-proxies # stop odin, delete all proxies, restart
|
||||
ppf-db vacuum # reclaim disk space
|
||||
```
|
||||
|
||||
### Cluster Status
|
||||
|
||||
```bash
|
||||
ppf-status # full overview: containers, DB, workers, queue
|
||||
ppf-status --json # raw JSON from odin API
|
||||
```
|
||||
|
||||
### Direct Ansible (for operations not covered by tools)
|
||||
|
||||
Use the toolkit inventory for ad-hoc commands over WireGuard:
|
||||
|
||||
@@ -227,6 +227,7 @@ ppf-deploy --check # dry run with diff
|
||||
ppf-logs [node] # view container logs (-f to follow)
|
||||
ppf-service <cmd> [nodes...] # status / start / stop / restart
|
||||
ppf-db <cmd> # stats / purge-proxies / vacuum
|
||||
ppf-status # cluster overview (containers, workers, queue)
|
||||
```
|
||||
|
||||
See `--help` on each tool.
|
||||
|
||||
246
tools/ppf-status
Executable file
246
tools/ppf-status
Executable file
@@ -0,0 +1,246 @@
|
||||
#!/bin/bash
|
||||
# ppf-status -- PPF cluster overview
|
||||
#
|
||||
# Usage:
|
||||
# ppf-status [options]
|
||||
|
||||
set -eu
|
||||
|
||||
# Resolve to real path (handles symlinks from ~/.local/bin/)
|
||||
SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$SCRIPT_PATH")")"
|
||||
# shellcheck disable=SC1091
|
||||
. "$SCRIPT_DIR/lib/ppf-common.sh"
|
||||
|
||||
ODIN_URL="http://127.0.0.1:8081"
|
||||
PROXY_DB="/home/podman/ppf/data/proxies.sqlite"
|
||||
URL_DB="/home/podman/ppf/data/websites.sqlite"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Usage
|
||||
# ---------------------------------------------------------------------------
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: ppf-status [options]
|
||||
|
||||
Show PPF cluster overview.
|
||||
|
||||
Options:
|
||||
--json raw JSON from API
|
||||
--help show this help
|
||||
--version show version
|
||||
|
||||
Displays:
|
||||
- Container health per node
|
||||
- Worker stats (tested, working, rate, active)
|
||||
- Odin manager stats (verification, queue)
|
||||
- Database counts (proxies, URLs)
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parse args
|
||||
# ---------------------------------------------------------------------------
|
||||
RAW_JSON=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--help|-h) usage ;;
|
||||
--version|-V) echo "ppf-status $PPF_TOOLS_VERSION"; exit 0 ;;
|
||||
--json) RAW_JSON=1 ;;
|
||||
-*) die "Unknown option: $1" ;;
|
||||
*) die "Unknown argument: $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fetch API data from odin (run on odin via curl to localhost)
|
||||
# ---------------------------------------------------------------------------
|
||||
api_json=$(ansible_cmd "$MASTER" -m raw -a \
|
||||
"curl -sf --max-time 5 ${ODIN_URL}/api/workers 2>/dev/null || echo '{}'" \
|
||||
2>/dev/null | sed 's/Shared connection.*closed\.\?//; /^\s*$/d; /^odin/d; /CHANGED/d; /SUCCESS/d')
|
||||
|
||||
if [ "$RAW_JSON" -eq 1 ]; then
|
||||
echo "$api_json"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if we got valid data
|
||||
if ! echo "$api_json" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
|
||||
die "Failed to fetch API data from odin"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Container health
|
||||
# ---------------------------------------------------------------------------
|
||||
section "Containers"
|
||||
|
||||
for host in $ALL_HOSTS; do
|
||||
output=$(compose_cmd "$host" "ps" 2>/dev/null) || true
|
||||
if echo "$output" | grep -qi "up\|running"; then
|
||||
log_ok "$host"
|
||||
elif echo "$output" | grep -qi "exit"; then
|
||||
log_err "$host (exited)"
|
||||
else
|
||||
log_warn "$host (unknown)"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Database summary (quick counts from odin)
|
||||
# ---------------------------------------------------------------------------
|
||||
section "Database"
|
||||
|
||||
proxy_count=$(ansible_cmd "$MASTER" -m raw -a \
|
||||
"sudo -u podman sqlite3 '$PROXY_DB' 'SELECT COUNT(*) FROM proxylist;'" 2>/dev/null \
|
||||
| sed 's/Shared connection.*//; /^\s*$/d; /^odin/d; /CHANGED/d; /SUCCESS/d' || echo '?')
|
||||
working_count=$(ansible_cmd "$MASTER" -m raw -a \
|
||||
"sudo -u podman sqlite3 '$PROXY_DB' 'SELECT COUNT(*) FROM proxylist WHERE failed=0 AND proto IS NOT NULL;'" 2>/dev/null \
|
||||
| sed 's/Shared connection.*//; /^\s*$/d; /^odin/d; /CHANGED/d; /SUCCESS/d' || echo '?')
|
||||
url_count=$(ansible_cmd "$MASTER" -m raw -a \
|
||||
"sudo -u podman sqlite3 '$URL_DB' 'SELECT COUNT(*) FROM uris;'" 2>/dev/null \
|
||||
| sed 's/Shared connection.*//; /^\s*$/d; /^odin/d; /CHANGED/d; /SUCCESS/d' || echo '?')
|
||||
|
||||
log_info "Proxies: ${proxy_count} total, ${working_count} working"
|
||||
log_info "URLs: ${url_count}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parse and display via Python for clean formatting
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "$api_json" | python3 -c "
|
||||
import sys, json
|
||||
|
||||
NO_COLOR = __import__('os').environ.get('NO_COLOR', '')
|
||||
|
||||
# Colors
|
||||
if not NO_COLOR and sys.stdout.isatty():
|
||||
RST = '\033[0m'
|
||||
DIM = '\033[2m'
|
||||
BOLD = '\033[1m'
|
||||
RED = '\033[38;5;167m'
|
||||
GREEN = '\033[38;5;114m'
|
||||
YELLOW = '\033[38;5;180m'
|
||||
BLUE = '\033[38;5;110m'
|
||||
CYAN = '\033[38;5;116m'
|
||||
else:
|
||||
RST = DIM = BOLD = RED = GREEN = YELLOW = BLUE = CYAN = ''
|
||||
|
||||
def ok(s): return GREEN + s + RST
|
||||
def err(s): return RED + s + RST
|
||||
def warn(s): return YELLOW + s + RST
|
||||
def dim(s): return DIM + s + RST
|
||||
def bold(s): return BOLD + CYAN + s + RST
|
||||
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
except:
|
||||
sys.exit(0)
|
||||
|
||||
workers = data.get('workers', [])
|
||||
summary = data.get('summary', {})
|
||||
queue = data.get('queue', {})
|
||||
manager = data.get('manager', {})
|
||||
|
||||
# Workers table
|
||||
print()
|
||||
print(bold(' Workers'))
|
||||
if workers:
|
||||
# Header
|
||||
print(dim(' %-12s %7s %9s %9s %7s %6s %s' % (
|
||||
'NAME', 'TESTED', 'WORKING', 'FAILED', 'RATE', 'ACT', 'STATUS')))
|
||||
for w in sorted(workers, key=lambda x: x.get('name', '')):
|
||||
name = w.get('name', w.get('ip', '?'))
|
||||
tested = w.get('proxies_tested', 0)
|
||||
working = w.get('proxies_working', 0)
|
||||
failed = w.get('proxies_failed', 0)
|
||||
rate = w.get('success_rate', 0)
|
||||
active = w.get('active', False)
|
||||
threads = w.get('threads', 0)
|
||||
|
||||
# Format numbers compactly
|
||||
def fmt(n):
|
||||
if n >= 1000000: return '%.1fM' % (n / 1000000)
|
||||
if n >= 1000: return '%.1fk' % (n / 1000)
|
||||
return str(n)
|
||||
|
||||
act_str = ok('yes') if active else err('no')
|
||||
if rate >= 30:
|
||||
rate_str = ok('%.1f%%' % rate)
|
||||
elif rate >= 10:
|
||||
rate_str = warn('%.1f%%' % rate)
|
||||
else:
|
||||
rate_str = err('%.1f%%' % rate)
|
||||
|
||||
age = w.get('age', 0)
|
||||
if age > 300 and not active:
|
||||
status = err('stale (%dm)' % (age // 60))
|
||||
elif active:
|
||||
status = ok('testing')
|
||||
else:
|
||||
status = dim('idle')
|
||||
|
||||
print(' %-12s %7s %9s %9s %7s %6s %s' % (
|
||||
name, fmt(tested), fmt(working), fmt(failed),
|
||||
rate_str, act_str, status))
|
||||
|
||||
# Summary line
|
||||
total_t = summary.get('total_tested', 0)
|
||||
total_w = summary.get('total_working', 0)
|
||||
total_f = summary.get('total_failed', 0)
|
||||
overall = summary.get('overall_success_rate', 0)
|
||||
active_count = data.get('active', 0)
|
||||
total_count = data.get('total', 0)
|
||||
print(dim(' %-12s %7s %9s %9s %7s %6s' % (
|
||||
'TOTAL',
|
||||
fmt(total_t) if total_t else '-',
|
||||
fmt(total_w) if total_w else '-',
|
||||
fmt(total_f) if total_f else '-',
|
||||
'%.1f%%' % overall,
|
||||
'%d/%d' % (active_count, total_count))))
|
||||
else:
|
||||
print(err(' no workers connected'))
|
||||
|
||||
# Manager (odin verification)
|
||||
if manager:
|
||||
print()
|
||||
print(bold(' Odin Verification'))
|
||||
m_rate = manager.get('success_rate', 0)
|
||||
m_tested = manager.get('tested', 0)
|
||||
m_passed = manager.get('passed', 0)
|
||||
m_threads = manager.get('threads', 0)
|
||||
m_speed = manager.get('rate', 0)
|
||||
m_queue = manager.get('queue_size', 0)
|
||||
m_uptime = manager.get('uptime', 0)
|
||||
|
||||
def fmt_time(s):
|
||||
if s >= 3600: return '%dh%dm' % (s // 3600, (s % 3600) // 60)
|
||||
if s >= 60: return '%dm%ds' % (s // 60, s % 60)
|
||||
return '%ds' % s
|
||||
|
||||
if m_rate >= 30:
|
||||
rate_str = ok('%.1f%%' % m_rate)
|
||||
elif m_rate >= 10:
|
||||
rate_str = warn('%.1f%%' % m_rate)
|
||||
else:
|
||||
rate_str = err('%.1f%%' % m_rate)
|
||||
|
||||
print(' threads: %d rate: %.2f/s uptime: %s' % (m_threads, m_speed, fmt_time(m_uptime)))
|
||||
print(' tested: %s passed: %s success: %s' % (fmt(m_tested), fmt(m_passed), rate_str))
|
||||
print(' queue: %d jobs' % m_queue)
|
||||
|
||||
# Queue
|
||||
if queue:
|
||||
print()
|
||||
print(bold(' Proxy Queue'))
|
||||
print(' total: %d due: %d pending: %d claimed: %d' % (
|
||||
queue.get('total', 0), queue.get('due', 0),
|
||||
queue.get('pending', 0), queue.get('claimed', 0)))
|
||||
sess_tested = queue.get('session_tested', 0)
|
||||
sess_pct = queue.get('session_pct', 0)
|
||||
if sess_tested:
|
||||
print(' session: %s tested (%.1f%%)' % (fmt(sess_tested), sess_pct))
|
||||
|
||||
print()
|
||||
"
|
||||
Reference in New Issue
Block a user