diff --git a/tools/mass_update.py b/tools/mass_update.py new file mode 100755 index 00000000..b49d72cc --- /dev/null +++ b/tools/mass_update.py @@ -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() diff --git a/tools/utun_update.sh b/tools/utun_update.sh new file mode 100755 index 00000000..530fedd3 --- /dev/null +++ b/tools/utun_update.sh @@ -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