Browse Source

Add remote update scripts for mass deployment

- 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
Evgeny 5 months ago
parent
commit
1c501f9690
  1. 163
      tools/mass_update.py
  2. 121
      tools/utun_update.sh

163
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()

121
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
Loading…
Cancel
Save