Files
Nexus/scripts/batch_patch_files_sudoers.py
T
Nexus Agent f11ad35708
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / e2e (push) Blocked by required conditions
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
chore(scripts): 推送提权验证脚本与温胜夜生产验证报告
2026-06-11 23:28:59 +08:00

158 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Batch patch /etc/sudoers.d/nexus-files on sub-servers (add rsync for push elevation).
Usage:
python scripts/batch_patch_files_sudoers.py --name 温胜夜
python scripts/batch_patch_files_sudoers.py --non-root-only --limit 50
python scripts/batch_patch_files_sudoers.py --ids 12,34 --dry-run
Requires .env with DATABASE_URL. SSH creds from servers table.
Run from dev machine or on central host: bash scripts/batch_patch_files_sudoers.sh
"""
from __future__ import annotations
import argparse
import asyncio
import os
import sys
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
def _load_dotenv() -> None:
env_path = ROOT / ".env"
if not env_path.is_file():
return
for line in env_path.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key, value = key.strip(), value.strip().strip("\"'")
if key.startswith("NEXUS_") and key[6:] not in os.environ:
os.environ[key[6:]] = value
elif key and key not in os.environ:
os.environ[key] = value
async def _fetch_servers(
session: Any,
*,
server_id: int | None,
server_ids: list[int] | None,
name_like: str | None,
non_root_only: bool,
limit: int,
) -> list[Any]:
from sqlalchemy import or_, select
from server.domain.models import Server
q = select(Server).order_by(Server.id)
if server_id is not None:
q = q.where(Server.id == server_id)
elif server_ids:
q = q.where(Server.id.in_(server_ids))
if name_like:
q = q.where(or_(Server.name.like(f"%{name_like}%"), Server.domain.like(f"%{name_like}%")))
if non_root_only:
q = q.where(Server.username.isnot(None)).where(Server.username != "root")
result = await session.execute(q)
servers = list(result.scalars().all())
if limit > 0:
servers = servers[:limit]
return servers
async def run_patch(args: argparse.Namespace) -> list[dict[str, Any]]:
from server.application.services.files_sudoers_service import install_nexus_files_sudoers
from server.config import settings
from server.infrastructure.database.session import AsyncSessionLocal, init_db
from server.infrastructure.redis.client import close_redis, init_redis
await init_db()
await init_redis(settings.REDIS_URL)
ids: list[int] | None = None
if args.ids:
ids = [int(x.strip()) for x in args.ids.split(",") if x.strip()]
async with AsyncSessionLocal() as session:
servers = await _fetch_servers(
session,
server_id=args.id,
server_ids=ids,
name_like=args.name,
non_root_only=args.non_root_only,
limit=args.limit,
)
if not servers:
print("No servers matched.", file=sys.stderr)
return []
sem = asyncio.Semaphore(max(1, args.concurrency))
results: list[dict[str, Any]] = []
async def _one(server: Any) -> None:
async with sem:
try:
row = await install_nexus_files_sudoers(server, dry_run=args.dry_run)
except Exception as exc:
row = {
"server_id": server.id,
"server_name": server.name,
"ssh_user": server.username,
"ok": False,
"action": "error",
"detail": str(exc)[:500],
}
results.append(row)
status = "OK" if row.get("ok") else "FAIL"
print(
f"[{status}] #{row.get('server_id')} {row.get('server_name')} "
f"({row.get('ssh_user')}) → {row.get('action')}"
+ (f" — {row.get('detail')}" if row.get("detail") else ""),
flush=True,
)
await asyncio.gather(*[_one(s) for s in servers])
from server.infrastructure.redis.client import close_redis
await close_redis()
ok = sum(1 for r in results if r.get("ok"))
fail = len(results) - ok
print(f"\nDone: {ok} ok / {fail} fail / {len(results)} total")
return results
def main() -> int:
_load_dotenv()
parser = argparse.ArgumentParser(description="Batch patch nexus-files sudoers (rsync for push)")
parser.add_argument("--id", type=int, help="Single server id")
parser.add_argument("--ids", type=str, help="Comma-separated server ids")
parser.add_argument("--name", type=str, help="Filter name/domain LIKE")
parser.add_argument("--non-root-only", action="store_true", help="Skip username=root")
parser.add_argument("--limit", type=int, default=0, help="Max servers (0=all)")
parser.add_argument("--concurrency", type=int, default=5)
parser.add_argument("--dry-run", action="store_true", help="Report only, no SSH writes")
args = parser.parse_args()
if not args.id and not args.ids and not args.name and not args.non_root_only:
parser.error("Specify --id, --ids, --name, and/or --non-root-only")
results = asyncio.run(run_patch(args))
return 0 if results and all(r.get("ok") for r in results) else (1 if results else 2)
if __name__ == "__main__":
raise SystemExit(main())