Files
guko/guko.py

87 lines
3.4 KiB
Python
Raw Permalink Normal View History

2026-06-23 00:44:35 +00:00
#!/usr/bin/env python3
import argparse, os, subprocess, sys, shlex, json
from pathlib import Path
from auth import resolve_ssh, ssh_args, ssh_display
ROOT = Path(__file__).resolve().parent
INV = Path(os.environ.get('GUKO_INV') or os.environ.get('VPSPILOT_INV') or ROOT / 'servers.json')
def load_inventory():
if not INV.exists():
return {'servers': []}
return json.loads(INV.read_text() or '{}')
def ssh_env_for(s, inv):
cfg = resolve_ssh(s, inv)
if cfg.get('auth') == 'password' and cfg.get('password'):
env = os.environ.copy()
env['SSHPASS'] = str(cfg['password'])
return env
return None
def health_cmd():
return r'''set -e
printf 'host='; hostname
printf 'uptime='; uptime -p || true
printf 'load='; awk '{print $1,$2,$3}' /proc/loadavg
printf 'mem='; free -h | awk '/Mem:/ {print $3 "/" $2}'
printf 'disk='; df -h / | awk 'NR==2 {print $3 "/" $2 " (" $5 ")"}'
printf 'kernel='; uname -r
printf 'ssh='; ss -ltnp 2>/dev/null | grep -E ':(22|53580) ' || true
systemctl is-active nezha-agent >/dev/null 2>&1 && echo 'nezha=active' || echo 'nezha=inactive-or-missing'
'''
def main():
p = argparse.ArgumentParser(prog='guko', description='GUKO server manager')
sub = p.add_subparsers(dest='cmd', required=True)
sub.add_parser('list')
sub.add_parser('health')
runp = sub.add_parser('run'); runp.add_argument('name'); runp.add_argument('command', nargs=argparse.REMAINDER)
args = p.parse_args()
inv = load_inventory()
servers = inv.get('servers') or []
if args.cmd == 'list':
if not servers:
print('no servers in servers.json')
for s in servers:
extra = f" nezha_id={s.get('nezha_id')}" if s.get('nezha_id') is not None else ''
try:
endpoint = ssh_display(s, inv)
except Exception:
endpoint = '<no-host>'
print(f"{s.get('name','?')} {endpoint} {s.get('role','')}{extra}")
elif args.cmd == 'health':
if not servers:
print('no servers in servers.json')
return
for s in servers:
if not s.get('host') and not (s.get('ssh') or {}).get('host'):
print(f"\n== {s.get('name','?')} (<no-host>) ==")
print('skipped: no ssh host in inventory')
continue
print(f"\n== {s.get('name',s.get('host'))} ({ssh_display(s, inv)}) ==")
r = subprocess.run(ssh_args(s, health_cmd(), inv=inv), text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30, env=ssh_env_for(s, inv))
print(r.stdout.rstrip())
elif args.cmd == 'run':
target = next((s for s in servers if s.get('name') == args.name or s.get('host') == args.name or str(s.get('nezha_id')) == args.name), None)
if not target:
sys.exit(f'not found: {args.name}')
if not target.get('host') and not (target.get('ssh') or {}).get('host'):
sys.exit(f"no ssh host for: {args.name}")
if not args.command:
sys.exit('missing command')
# Support both styles:
# guko.py run host 'systemctl status nginx --no-pager'
# guko.py run host systemctl status nginx --no-pager
remote = args.command[0] if len(args.command) == 1 else ' '.join(shlex.quote(x) for x in args.command)
subprocess.run(ssh_args(target, remote, inv=inv), check=False, env=ssh_env_for(target, inv))
if __name__ == '__main__':
main()