Files
Nexus/scripts/import_servers_from_xls.py
T

211 lines
7.0 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Import servers from Excel (.xls) via Nexus API (name + IP columns).
Usage:
NEXUS_TEST_BASE=https://api.synaglobal.vip \\
NEXUS_TEST_ADMIN_USER=admin \\
NEXUS_TEST_ADMIN_PASSWORD='***' \\
python scripts/import_servers_from_xls.py /path/to/servers.xls
Options:
--dry-run Parse only, do not call API
--batch Use /add-by-ips-batch (requires server with name-tab support)
--chunk N Batch chunk size (default 200)
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
def _load_test_credentials() -> tuple[str, str, str]:
sys.path.insert(0, str(ROOT / "tests"))
import test_api # noqa: E402
test_api._load_credentials_from_env_file()
base = os.environ.get("NEXUS_TEST_BASE", "http://127.0.0.1:8600").rstrip("/")
user = os.environ.get("NEXUS_TEST_ADMIN_USER", "admin")
password = os.environ.get("NEXUS_TEST_ADMIN_PASSWORD", "admin")
return base, user, password
def _api_json(method: str, url: str, token: str | None = None, body: dict | None = None, timeout: int = 300):
data = None
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
if body is not None:
data = json.dumps(body).encode()
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read()
return resp.status, json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
raw = e.read()
try:
payload = json.loads(raw) if raw else {"detail": e.reason}
except json.JSONDecodeError:
payload = {"detail": raw.decode(errors="replace")}
return e.code, payload
def _login(base: str, user: str, password: str) -> str:
code, data = _api_json("POST", f"{base}/api/auth/login", body={"username": user, "password": password})
if code != 200 or not data.get("access_token"):
detail = data.get("detail", data)
raise SystemExit(f"登录失败 ({code}): {detail}")
return data["access_token"]
def _read_xls(path: Path) -> list[tuple[str, str]]:
try:
import xlrd
except ImportError as e:
raise SystemExit("需要 xlrd: pip install xlrd") from e
wb = xlrd.open_workbook(str(path))
sh = wb.sheet_by_index(0)
rows: list[tuple[str, str]] = []
for r in range(sh.nrows):
if sh.ncols < 2:
continue
name = str(sh.cell_value(r, 0)).strip()
ip = str(sh.cell_value(r, 1)).strip()
if not ip:
continue
rows.append((name or ip, ip))
return rows
def _dedupe_rows(rows: list[tuple[str, str]]) -> tuple[list[tuple[str, str]], int]:
seen: set[str] = set()
out: list[tuple[str, str]] = []
removed = 0
for name, ip in rows:
key = ip.casefold()
if key in seen:
removed += 1
continue
seen.add(key)
out.append((name, ip))
return out, removed
def _import_single(base: str, token: str, rows: list[tuple[str, str]], port: int, agent_port: int) -> dict:
stats = {"created": 0, "pending": 0, "skipped": 0, "errors": 0}
total = len(rows)
for i, (name, ip) in enumerate(rows, 1):
code, data = _api_json(
"POST",
f"{base}/api/servers/add-by-ip",
token=token,
body={
"domain": ip,
"name": name,
"port": port,
"agent_port": agent_port,
},
timeout=300,
)
if code == 200 and data.get("success"):
stats["created"] += 1
status = "created"
elif code == 409:
stats["skipped"] += 1
status = "skipped"
elif code == 200 and not data.get("success"):
stats["pending"] += 1
status = "pending"
else:
stats["errors"] += 1
status = f"error:{code}"
print(f"[{i}/{total}] {status} {name} ({ip})", flush=True)
return stats
def _import_batch(
base: str,
token: str,
rows: list[tuple[str, str]],
port: int,
agent_port: int,
chunk: int,
) -> dict:
stats = {"created": 0, "pending": 0, "skipped": 0, "errors": 0}
for start in range(0, len(rows), chunk):
part = rows[start : start + chunk]
text = "\n".join(f"{name}\t{ip}" for name, ip in part)
code, data = _api_json(
"POST",
f"{base}/api/servers/add-by-ips-batch",
token=token,
body={"text": text, "port": port, "agent_port": agent_port},
timeout=3600,
)
if code != 200:
stats["errors"] += len(part)
print(f"batch {start // chunk + 1} failed ({code}): {data.get('detail', data)}", flush=True)
continue
stats["created"] += int(data.get("created", 0))
stats["pending"] += int(data.get("pending", 0))
stats["skipped"] += int(data.get("skipped", 0))
print(
f"batch {start // chunk + 1}: created={data.get('created')} "
f"pending={data.get('pending')} skipped={data.get('skipped')}",
flush=True,
)
return stats
def main() -> None:
parser = argparse.ArgumentParser(description="Import servers from Excel via Nexus API")
parser.add_argument("xls", type=Path, help="Excel file (.xls), column A=name, B=IP")
parser.add_argument("--dry-run", action="store_true", help="Parse only")
parser.add_argument("--batch", action="store_true", help="Use batch API instead of single add-by-ip")
parser.add_argument("--chunk", type=int, default=200, help="Batch chunk size")
parser.add_argument("--port", type=int, default=22)
parser.add_argument("--agent-port", type=int, default=8601)
args = parser.parse_args()
if not args.xls.is_file():
raise SystemExit(f"文件不存在: {args.xls}")
rows = _read_xls(args.xls)
rows, dup_removed = _dedupe_rows(rows)
print(f"读取 {args.xls.name}: {len(rows)} 台(去重忽略 {dup_removed} 行重复 IP")
if args.dry_run:
for name, ip in rows[:5]:
print(f" {name}\t{ip}")
if len(rows) > 5:
print(f" ... 共 {len(rows)} 行")
return
base, user, password = _load_test_credentials()
print(f"目标 API: {base} 用户: {user}")
token = _login(base, user, password)
started = time.time()
if args.batch:
stats = _import_batch(base, token, rows, args.port, args.agent_port, args.chunk)
else:
stats = _import_single(base, token, rows, args.port, args.agent_port)
elapsed = int(time.time() - started)
print(
f"完成 ({elapsed}s): 成功 {stats['created']},待连接 {stats['pending']}"
f"跳过 {stats['skipped']},错误 {stats['errors']}"
)
if __name__ == "__main__":
main()