236 lines
6.8 KiB
Python
Executable File
236 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""
|
|
mass_update.py - Mass update uTun servers via SSH
|
|
Usage: python3 mass_update.py [fast|full] [-c config] [-j jobs] (default: fast)
|
|
|
|
Config format (JSON):
|
|
{
|
|
"servers": [
|
|
{"host": "10.0.0.1"},
|
|
{"host": "10.0.0.2", "port": 2222, "user": "admin", "ssh_key": "keys/server2_rsa", "workdir": "/opt/utun"}
|
|
]
|
|
}
|
|
|
|
Default config: tools/utun_servers.json (in script directory)
|
|
Fallback: ~/.utun_servers.json
|
|
SSH keys: tools/keys/ (in script directory)
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from pathlib import Path
|
|
|
|
SSH_TIMEOUT = 300
|
|
|
|
|
|
def get_script_dir():
|
|
"""Get script directory (tools/)."""
|
|
return Path(__file__).parent.resolve()
|
|
|
|
|
|
def get_default_config():
|
|
"""Get config path: tools/utun_servers.json or fallback to home."""
|
|
script_dir = get_script_dir()
|
|
config_in_tools = script_dir / "utun_servers.json"
|
|
if config_in_tools.exists():
|
|
return config_in_tools
|
|
config_in_home = Path.home() / ".utun_servers.json"
|
|
if config_in_home.exists():
|
|
return config_in_home
|
|
return config_in_tools
|
|
|
|
|
|
def resolve_ssh_key(ssh_key, script_dir):
|
|
"""Resolve SSH key path."""
|
|
if not ssh_key:
|
|
return None
|
|
key_path = Path(ssh_key)
|
|
if key_path.is_absolute():
|
|
return str(key_path)
|
|
keys_dir = script_dir / "keys"
|
|
return str(keys_dir / ssh_key)
|
|
|
|
|
|
def load_servers(config_path, script_dir):
|
|
"""Load server list from JSON config."""
|
|
with open(config_path, 'r') as f:
|
|
data = json.load(f)
|
|
servers = data.get("servers", [])
|
|
for server in servers:
|
|
if "ssh_key" in server:
|
|
server["_resolved_key"] = resolve_ssh_key(server["ssh_key"], script_dir)
|
|
return servers
|
|
|
|
|
|
def update_server(server, command, logs_dir):
|
|
"""Run update on a single server via SSH. Returns result dict with logs_dir for log writing."""
|
|
host = server["host"]
|
|
port = server.get("port", 22)
|
|
workdir = server.get("workdir", "~/utun")
|
|
user = server.get("user", "root")
|
|
resolved_key = server.get("_resolved_key")
|
|
|
|
ssh_cmd = ["ssh"]
|
|
if resolved_key and Path(resolved_key).exists():
|
|
ssh_cmd.extend(["-i", resolved_key])
|
|
ssh_cmd.extend([
|
|
"-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 write_log({
|
|
"host": host,
|
|
"returncode": result.returncode,
|
|
"stdout": result.stdout,
|
|
"stderr": result.stderr
|
|
}, logs_dir)
|
|
except subprocess.TimeoutExpired:
|
|
return write_log({
|
|
"host": host,
|
|
"returncode": -1,
|
|
"stdout": "",
|
|
"stderr": f"Timeout after {SSH_TIMEOUT}s"
|
|
}, logs_dir)
|
|
except Exception as e:
|
|
return write_log({
|
|
"host": host,
|
|
"returncode": -1,
|
|
"stdout": "",
|
|
"stderr": str(e)
|
|
}, logs_dir)
|
|
|
|
|
|
def write_log(result, logs_dir):
|
|
"""Write session log to file. Returns result dict."""
|
|
host = result["host"]
|
|
rc = result["returncode"]
|
|
log_path = logs_dir / f"{host}.log"
|
|
try:
|
|
with open(log_path, 'w') as f:
|
|
f.write(f"Host: {host} | Exit code: {rc}\n")
|
|
f.write("=" * 60 + "\n")
|
|
if result["stdout"]:
|
|
f.write(result["stdout"])
|
|
if not result["stdout"].endswith("\n"):
|
|
f.write("\n")
|
|
if result["stderr"]:
|
|
f.write("STDERR:\n")
|
|
f.write(result["stderr"])
|
|
if not result["stderr"].endswith("\n"):
|
|
f.write("\n")
|
|
result["_log_file"] = str(log_path)
|
|
except Exception as e:
|
|
result["_log_file"] = f"ERROR writing log: {e}"
|
|
return result
|
|
|
|
|
|
def main():
|
|
script_dir = get_script_dir()
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Mass update uTun servers via SSH"
|
|
)
|
|
parser.add_argument(
|
|
"command",
|
|
nargs='?',
|
|
default="fast",
|
|
choices=["fast", "full"],
|
|
help="Update mode: fast (make) or full (autotools rebuild) (default: fast)"
|
|
)
|
|
parser.add_argument(
|
|
"-c", "--config",
|
|
help=f"Servers config file (default: auto from script dir)"
|
|
)
|
|
parser.add_argument(
|
|
"-j", "--jobs",
|
|
type=int,
|
|
default=10,
|
|
help="Parallel SSH jobs (default: 10)"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.config:
|
|
config_path = Path(args.config)
|
|
else:
|
|
config_path = get_default_config()
|
|
|
|
if not config_path.exists():
|
|
print(f"ERROR: Config not found: {config_path}", file=sys.stderr)
|
|
print("\nCreate config in script directory:", file=sys.stderr)
|
|
print(f" {script_dir / 'utun_servers.json'}", file=sys.stderr)
|
|
print("\nOr use -c to specify custom config.", file=sys.stderr)
|
|
print("\nFormat:", file=sys.stderr)
|
|
example = {
|
|
"servers": [
|
|
{"host": "10.0.0.1"},
|
|
{"host": "10.0.0.2", "port": 2222, "user": "admin", "ssh_key": "keys/server2_rsa"}
|
|
]
|
|
}
|
|
print(json.dumps(example, indent=2), file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
servers = load_servers(config_path, script_dir)
|
|
if not servers:
|
|
print("ERROR: No servers found in config", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
logs_dir = Path("logs")
|
|
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
print(f"Updating {len(servers)} servers in '{args.command}' mode...")
|
|
print(f"Parallel jobs: {args.jobs}")
|
|
print(f"Config: {config_path}")
|
|
print(f"Logs: {logs_dir.resolve()}")
|
|
print()
|
|
|
|
success = 0
|
|
failed = 0
|
|
|
|
with ThreadPoolExecutor(max_workers=args.jobs) as executor:
|
|
futures = {
|
|
executor.submit(update_server, s, args.command, logs_dir): s
|
|
for s in servers
|
|
}
|
|
|
|
for future in as_completed(futures):
|
|
result = future.result()
|
|
host = result["host"]
|
|
rc = result["returncode"]
|
|
|
|
if rc == 0:
|
|
success += 1
|
|
else:
|
|
failed += 1
|
|
print(f"{'='*60}")
|
|
print(f"Host: {host} | Exit code: {rc} | Log: {result.get('_log_file', 'N/A')}")
|
|
print(f"{'='*60}")
|
|
if result["stdout"]:
|
|
print(result["stdout"])
|
|
if result["stderr"]:
|
|
print(result["stderr"], file=sys.stderr, end="")
|
|
|
|
print(f"{'='*60}")
|
|
print(f"Done: {success} succeeded, {failed} failed")
|
|
|
|
sys.exit(0 if failed == 0 else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|