Browse Source
- utun_update.sh: Shell script for updating single instance * fast mode: git pull + make clean + make * full mode: complete rebuild with autotools * Auto-detects project root from script location * Handles PID-based process stop/start * Supports Linux and BSD (nproc/sysctl) * Returns proper exit codes (0-4) - mass_update.py: Python script for parallel SSH updates * Parallel execution via ThreadPoolExecutor * Configurable via JSON (~/.utun_servers.json) * Supports custom port/user per server * Timeout and error handling * Summary output per server Tools for remote mass deployment of uTun updates via SSH.congestion
2 changed files with 284 additions and 0 deletions
@ -0,0 +1,163 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
|
||||||
|
""" |
||||||
|
mass_update.py - Mass update uTun servers via SSH |
||||||
|
Usage: python3 mass_update.py [fast|full] [-c config] [-j jobs] |
||||||
|
|
||||||
|
Config format (JSON): |
||||||
|
{ |
||||||
|
"servers": [ |
||||||
|
{"host": "10.0.0.1", "workdir": "/opt/utun"}, |
||||||
|
{"host": "10.0.0.2", "port": 2222, "user": "admin", "workdir": "/root/utun"} |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
Default config: ~/.utun_servers.json |
||||||
|
""" |
||||||
|
|
||||||
|
import argparse |
||||||
|
import json |
||||||
|
import subprocess |
||||||
|
import sys |
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed |
||||||
|
from pathlib import Path |
||||||
|
|
||||||
|
DEFAULT_CONFIG = Path.home() / ".utun_servers.json" |
||||||
|
SSH_TIMEOUT = 300 # 5 minutes |
||||||
|
|
||||||
|
|
||||||
|
def load_servers(config_path): |
||||||
|
"""Load server list from JSON config.""" |
||||||
|
with open(config_path, 'r') as f: |
||||||
|
data = json.load(f) |
||||||
|
return data.get("servers", []) |
||||||
|
|
||||||
|
|
||||||
|
def update_server(server, command): |
||||||
|
"""Run update on a single server via SSH.""" |
||||||
|
host = server["host"] |
||||||
|
port = server.get("port", 22) |
||||||
|
workdir = server.get("workdir", "~/utun") |
||||||
|
user = server.get("user", "root") |
||||||
|
|
||||||
|
ssh_cmd = [ |
||||||
|
"ssh", |
||||||
|
"-p", str(port), |
||||||
|
"-o", "StrictHostKeyChecking=no", |
||||||
|
"-o", "ConnectTimeout=10", |
||||||
|
"-o", "BatchMode=yes", |
||||||
|
f"{user}@{host}", |
||||||
|
f"cd {workdir} && ./tools/utun_update.sh {command}" |
||||||
|
] |
||||||
|
|
||||||
|
try: |
||||||
|
result = subprocess.run( |
||||||
|
ssh_cmd, |
||||||
|
capture_output=True, |
||||||
|
text=True, |
||||||
|
timeout=SSH_TIMEOUT |
||||||
|
) |
||||||
|
return { |
||||||
|
"host": host, |
||||||
|
"returncode": result.returncode, |
||||||
|
"stdout": result.stdout, |
||||||
|
"stderr": result.stderr |
||||||
|
} |
||||||
|
except subprocess.TimeoutExpired: |
||||||
|
return { |
||||||
|
"host": host, |
||||||
|
"returncode": -1, |
||||||
|
"stdout": "", |
||||||
|
"stderr": f"Timeout after {SSH_TIMEOUT}s" |
||||||
|
} |
||||||
|
except Exception as e: |
||||||
|
return { |
||||||
|
"host": host, |
||||||
|
"returncode": -1, |
||||||
|
"stdout": "", |
||||||
|
"stderr": str(e) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
def main(): |
||||||
|
parser = argparse.ArgumentParser( |
||||||
|
description="Mass update uTun servers via SSH" |
||||||
|
) |
||||||
|
parser.add_argument( |
||||||
|
"command", |
||||||
|
choices=["fast", "full"], |
||||||
|
help="Update mode: fast (make) or full (autotools rebuild)" |
||||||
|
) |
||||||
|
parser.add_argument( |
||||||
|
"-c", "--config", |
||||||
|
default=str(DEFAULT_CONFIG), |
||||||
|
help=f"Servers config file (default: {DEFAULT_CONFIG})" |
||||||
|
) |
||||||
|
parser.add_argument( |
||||||
|
"-j", "--jobs", |
||||||
|
type=int, |
||||||
|
default=10, |
||||||
|
help="Parallel SSH jobs (default: 10)" |
||||||
|
) |
||||||
|
|
||||||
|
args = parser.parse_args() |
||||||
|
|
||||||
|
config_path = Path(args.config) |
||||||
|
if not config_path.exists(): |
||||||
|
print(f"ERROR: Config not found: {config_path}", file=sys.stderr) |
||||||
|
print("\nCreate it with format:", file=sys.stderr) |
||||||
|
example = { |
||||||
|
"servers": [ |
||||||
|
{"host": "10.0.0.1", "workdir": "/opt/utun"}, |
||||||
|
{"host": "10.0.0.2", "port": 2222, "user": "admin"} |
||||||
|
] |
||||||
|
} |
||||||
|
print(json.dumps(example, indent=2), file=sys.stderr) |
||||||
|
sys.exit(1) |
||||||
|
|
||||||
|
servers = load_servers(config_path) |
||||||
|
if not servers: |
||||||
|
print("ERROR: No servers found in config", file=sys.stderr) |
||||||
|
sys.exit(1) |
||||||
|
|
||||||
|
print(f"Updating {len(servers)} servers in '{args.command}' mode...") |
||||||
|
print(f"Parallel jobs: {args.jobs}") |
||||||
|
print(f"Config: {config_path}") |
||||||
|
print() |
||||||
|
|
||||||
|
success = 0 |
||||||
|
failed = 0 |
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=args.jobs) as executor: |
||||||
|
futures = { |
||||||
|
executor.submit(update_server, s, args.command): s |
||||||
|
for s in servers |
||||||
|
} |
||||||
|
|
||||||
|
for future in as_completed(futures): |
||||||
|
result = future.result() |
||||||
|
host = result["host"] |
||||||
|
rc = result["returncode"] |
||||||
|
|
||||||
|
print(f"{'='*60}") |
||||||
|
print(f"Host: {host} | Exit code: {rc}") |
||||||
|
print(f"{'='*60}") |
||||||
|
|
||||||
|
if result["stdout"]: |
||||||
|
print(result["stdout"]) |
||||||
|
if result["stderr"]: |
||||||
|
print(result["stderr"], file=sys.stderr, end="") |
||||||
|
|
||||||
|
if rc == 0: |
||||||
|
success += 1 |
||||||
|
else: |
||||||
|
failed += 1 |
||||||
|
|
||||||
|
print(f"{'='*60}") |
||||||
|
print(f"Done: {success} succeeded, {failed} failed") |
||||||
|
|
||||||
|
sys.exit(0 if failed == 0 else 1) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() |
||||||
@ -0,0 +1,121 @@ |
|||||||
|
#!/bin/sh |
||||||
|
set -e |
||||||
|
|
||||||
|
# utun_update.sh - Remote uTun update script |
||||||
|
# Usage: ./tools/utun_update.sh [fast|full] [workdir] |
||||||
|
# fast: git pull + make clean + make |
||||||
|
# full: git pull + make distclean + autoreconf + configure + make |
||||||
|
# Returns: |
||||||
|
# 0 - success |
||||||
|
# 1 - invalid arguments or not a git repo |
||||||
|
# 2 - git pull failed |
||||||
|
# 3 - build failed |
||||||
|
# 4 - start failed |
||||||
|
|
||||||
|
# Determine project root from script location |
||||||
|
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) |
||||||
|
PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) |
||||||
|
|
||||||
|
# Parse arguments |
||||||
|
MODE="${1:-fast}" |
||||||
|
WORKDIR="${2:-$PROJECT_ROOT}" |
||||||
|
|
||||||
|
# Validate mode |
||||||
|
if [ "$MODE" != "fast" ] && [ "$MODE" != "full" ]; then |
||||||
|
echo "ERROR: Invalid mode '$MODE'. Use 'fast' or 'full'" >&2 |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
# Change to workdir |
||||||
|
cd "$WORKDIR" || { echo "ERROR: Cannot cd to $WORKDIR" >&2; exit 1; } |
||||||
|
|
||||||
|
# Validate git repository |
||||||
|
if [ ! -d ".git" ]; then |
||||||
|
echo "ERROR: Not a git repository: $WORKDIR" >&2 |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
PIDFILE="/var/run/utun.pid" |
||||||
|
UTUN_BIN="./src/utun" |
||||||
|
|
||||||
|
echo "=== uTun Update ===" |
||||||
|
echo "Mode: $MODE" |
||||||
|
echo "Workdir: $(pwd)" |
||||||
|
|
||||||
|
# Stop utun if running via pidfile |
||||||
|
if [ -f "$PIDFILE" ]; then |
||||||
|
PID=$(cat "$PIDFILE") |
||||||
|
if kill -0 "$PID" 2>/dev/null; then |
||||||
|
echo "Stopping utun (PID $PID)..." |
||||||
|
kill -TERM "$PID" 2>/dev/null || true |
||||||
|
# Wait graceful shutdown (up to 10s) |
||||||
|
for i in 1 2 3 4 5 6 7 8 9 10; do |
||||||
|
if ! kill -0 "$PID" 2>/dev/null; then |
||||||
|
break |
||||||
|
fi |
||||||
|
sleep 1 |
||||||
|
done |
||||||
|
# Force kill if still running |
||||||
|
if kill -0 "$PID" 2>/dev/null; then |
||||||
|
echo "Force killing utun (PID $PID)..." |
||||||
|
kill -9 "$PID" 2>/dev/null || true |
||||||
|
sleep 1 |
||||||
|
fi |
||||||
|
fi |
||||||
|
rm -f "$PIDFILE" |
||||||
|
fi |
||||||
|
|
||||||
|
# Git pull |
||||||
|
echo "Pulling latest changes..." |
||||||
|
if ! git pull; then |
||||||
|
echo "ERROR: git pull failed" >&2 |
||||||
|
exit 2 |
||||||
|
fi |
||||||
|
|
||||||
|
# Detect number of CPUs (Linux or BSD) |
||||||
|
if command -v nproc >/dev/null 2>&1; then |
||||||
|
JOBS=$(nproc) |
||||||
|
else |
||||||
|
JOBS=$(sysctl -n hw.ncpu 2>/dev/null || echo 1) |
||||||
|
fi |
||||||
|
|
||||||
|
# Build |
||||||
|
if [ "$MODE" = "full" ]; then |
||||||
|
echo "Full rebuild (autotools, clean, configure)..." |
||||||
|
make distclean 2>/dev/null || true |
||||||
|
autoreconf -fi 2>&1 | tee autoreconf.log |
||||||
|
./configure --prefix=/usr/local 2>&1 | tee configure.log |
||||||
|
make clean |
||||||
|
else |
||||||
|
echo "Fast rebuild (make clean && make)..." |
||||||
|
make clean |
||||||
|
fi |
||||||
|
|
||||||
|
echo "Building with $JOBS parallel jobs..." |
||||||
|
if ! make -j"$JOBS" 2>&1 | tee build.log; then |
||||||
|
echo "ERROR: Build failed" >&2 |
||||||
|
exit 3 |
||||||
|
fi |
||||||
|
|
||||||
|
# Verify binary exists |
||||||
|
if [ ! -x "$UTUN_BIN" ]; then |
||||||
|
echo "ERROR: Binary not found or not executable: $UTUN_BIN" >&2 |
||||||
|
exit 3 |
||||||
|
fi |
||||||
|
|
||||||
|
echo "Build successful: $(ls -la $UTUN_BIN)" |
||||||
|
|
||||||
|
# Start utun |
||||||
|
echo "Starting utun..." |
||||||
|
nohup "$UTUN_BIN" > utun.log 2> utun_err.log & |
||||||
|
NEW_PID=$! |
||||||
|
sleep 2 |
||||||
|
|
||||||
|
# Verify process is running |
||||||
|
if kill -0 "$NEW_PID" 2>/dev/null; then |
||||||
|
echo "uTun started successfully (PID $NEW_PID)" |
||||||
|
exit 0 |
||||||
|
else |
||||||
|
echo "ERROR: Failed to start utun (PID $NEW_PID died)" >&2 |
||||||
|
exit 4 |
||||||
|
fi |
||||||
Loading…
Reference in new issue