|
|
|
@ -7,30 +7,64 @@ Usage: python3 mass_update.py [fast|full] [-c config] [-j jobs] |
|
|
|
Config format (JSON): |
|
|
|
Config format (JSON): |
|
|
|
{ |
|
|
|
{ |
|
|
|
"servers": [ |
|
|
|
"servers": [ |
|
|
|
{"host": "10.0.0.1", "workdir": "/opt/utun"}, |
|
|
|
{"host": "10.0.0.1"}, |
|
|
|
{"host": "10.0.0.2", "port": 2222, "user": "admin", "workdir": "/root/utun"} |
|
|
|
{"host": "10.0.0.2", "port": 2222, "user": "admin", "ssh_key": "keys/server2_rsa", "workdir": "/opt/utun"} |
|
|
|
] |
|
|
|
] |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Default config: ~/.utun_servers.json |
|
|
|
Default config: tools/utun_servers.json (in script directory) |
|
|
|
|
|
|
|
Fallback: ~/.utun_servers.json |
|
|
|
|
|
|
|
SSH keys: tools/keys/ (in script directory) |
|
|
|
""" |
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
import argparse |
|
|
|
import argparse |
|
|
|
import json |
|
|
|
import json |
|
|
|
|
|
|
|
import os |
|
|
|
import subprocess |
|
|
|
import subprocess |
|
|
|
import sys |
|
|
|
import sys |
|
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed |
|
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed |
|
|
|
from pathlib import Path |
|
|
|
from pathlib import Path |
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_CONFIG = Path.home() / ".utun_servers.json" |
|
|
|
SSH_TIMEOUT = 300 |
|
|
|
SSH_TIMEOUT = 300 # 5 minutes |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_servers(config_path): |
|
|
|
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.""" |
|
|
|
"""Load server list from JSON config.""" |
|
|
|
with open(config_path, 'r') as f: |
|
|
|
with open(config_path, 'r') as f: |
|
|
|
data = json.load(f) |
|
|
|
data = json.load(f) |
|
|
|
return data.get("servers", []) |
|
|
|
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): |
|
|
|
def update_server(server, command): |
|
|
|
@ -39,16 +73,19 @@ def update_server(server, command): |
|
|
|
port = server.get("port", 22) |
|
|
|
port = server.get("port", 22) |
|
|
|
workdir = server.get("workdir", "~/utun") |
|
|
|
workdir = server.get("workdir", "~/utun") |
|
|
|
user = server.get("user", "root") |
|
|
|
user = server.get("user", "root") |
|
|
|
|
|
|
|
resolved_key = server.get("_resolved_key") |
|
|
|
|
|
|
|
|
|
|
|
ssh_cmd = [ |
|
|
|
ssh_cmd = ["ssh"] |
|
|
|
"ssh", |
|
|
|
if resolved_key and Path(resolved_key).exists(): |
|
|
|
|
|
|
|
ssh_cmd.extend(["-i", resolved_key]) |
|
|
|
|
|
|
|
ssh_cmd.extend([ |
|
|
|
"-p", str(port), |
|
|
|
"-p", str(port), |
|
|
|
"-o", "StrictHostKeyChecking=no", |
|
|
|
"-o", "StrictHostKeyChecking=no", |
|
|
|
"-o", "ConnectTimeout=10", |
|
|
|
"-o", "ConnectTimeout=10", |
|
|
|
"-o", "BatchMode=yes", |
|
|
|
"-o", "BatchMode=yes", |
|
|
|
f"{user}@{host}", |
|
|
|
f"{user}@{host}", |
|
|
|
f"cd {workdir} && ./tools/utun_update.sh {command}" |
|
|
|
f"cd {workdir} && ./tools/utun_update.sh {command}" |
|
|
|
] |
|
|
|
]) |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
|
try: |
|
|
|
result = subprocess.run( |
|
|
|
result = subprocess.run( |
|
|
|
@ -80,6 +117,8 @@ def update_server(server, command): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(): |
|
|
|
def main(): |
|
|
|
|
|
|
|
script_dir = get_script_dir() |
|
|
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser( |
|
|
|
parser = argparse.ArgumentParser( |
|
|
|
description="Mass update uTun servers via SSH" |
|
|
|
description="Mass update uTun servers via SSH" |
|
|
|
) |
|
|
|
) |
|
|
|
@ -90,8 +129,7 @@ def main(): |
|
|
|
) |
|
|
|
) |
|
|
|
parser.add_argument( |
|
|
|
parser.add_argument( |
|
|
|
"-c", "--config", |
|
|
|
"-c", "--config", |
|
|
|
default=str(DEFAULT_CONFIG), |
|
|
|
help=f"Servers config file (default: auto from script dir)" |
|
|
|
help=f"Servers config file (default: {DEFAULT_CONFIG})" |
|
|
|
|
|
|
|
) |
|
|
|
) |
|
|
|
parser.add_argument( |
|
|
|
parser.add_argument( |
|
|
|
"-j", "--jobs", |
|
|
|
"-j", "--jobs", |
|
|
|
@ -102,20 +140,27 @@ def main(): |
|
|
|
|
|
|
|
|
|
|
|
args = parser.parse_args() |
|
|
|
args = parser.parse_args() |
|
|
|
|
|
|
|
|
|
|
|
config_path = Path(args.config) |
|
|
|
if args.config: |
|
|
|
|
|
|
|
config_path = Path(args.config) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
config_path = get_default_config() |
|
|
|
|
|
|
|
|
|
|
|
if not config_path.exists(): |
|
|
|
if not config_path.exists(): |
|
|
|
print(f"ERROR: Config not found: {config_path}", file=sys.stderr) |
|
|
|
print(f"ERROR: Config not found: {config_path}", file=sys.stderr) |
|
|
|
print("\nCreate it with format:", 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 = { |
|
|
|
example = { |
|
|
|
"servers": [ |
|
|
|
"servers": [ |
|
|
|
{"host": "10.0.0.1", "workdir": "/opt/utun"}, |
|
|
|
{"host": "10.0.0.1"}, |
|
|
|
{"host": "10.0.0.2", "port": 2222, "user": "admin"} |
|
|
|
{"host": "10.0.0.2", "port": 2222, "user": "admin", "ssh_key": "keys/server2_rsa"} |
|
|
|
] |
|
|
|
] |
|
|
|
} |
|
|
|
} |
|
|
|
print(json.dumps(example, indent=2), file=sys.stderr) |
|
|
|
print(json.dumps(example, indent=2), file=sys.stderr) |
|
|
|
sys.exit(1) |
|
|
|
sys.exit(1) |
|
|
|
|
|
|
|
|
|
|
|
servers = load_servers(config_path) |
|
|
|
servers = load_servers(config_path, script_dir) |
|
|
|
if not servers: |
|
|
|
if not servers: |
|
|
|
print("ERROR: No servers found in config", file=sys.stderr) |
|
|
|
print("ERROR: No servers found in config", file=sys.stderr) |
|
|
|
sys.exit(1) |
|
|
|
sys.exit(1) |
|
|
|
|