"""Shared Nexus files sudoers template and remote install helpers.""" from __future__ import annotations import asyncio import shlex import textwrap from server.domain.models import Server from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command, ssh_pool from server.infrastructure.database.crypto import decrypt_value from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback NEXUS_FILES_SUDOERS_PATH = "/etc/sudoers.d/nexus-files" def build_nexus_files_sudoers(ssh_user: str) -> str: """Return sudoers.d fragment for file manager + rsync push elevation.""" user = (ssh_user or "deploy").strip() or "deploy" return textwrap.dedent(f"""\ # nexus-files: configured by Nexus (file manager + rsync push) {user} ALL=(root) NOPASSWD: \\ /bin/ls, /usr/bin/ls, \\ /bin/cat, /usr/bin/cat, \\ /usr/bin/tee, /bin/tee, \\ /bin/mkdir, /usr/bin/mkdir, \\ /bin/cp, /usr/bin/cp, \\ /bin/mv, /usr/bin/mv, \\ /bin/rm, /usr/bin/rm, \\ /bin/chmod, /usr/bin/chmod, \\ /bin/chown, /usr/bin/chown, \\ /bin/bash, /usr/bin/bash, \\ /usr/bin/rsync, /bin/rsync """) async def probe_rsync_sudo(server: Server, *, timeout: int = 15) -> bool: """True when ``sudo -n rsync`` works on the target.""" ssh_user = (server.username or "root").strip() or "root" if ssh_user == "root": result = await exec_ssh_command(server, "rsync --version", timeout=timeout) return result.get("exit_code") == 0 result = await exec_ssh_command(server, "sudo -n rsync --version", timeout=timeout) return result.get("exit_code") == 0 async def _write_sudoers_with_password( server: Server, content: str, sudoers_path: str, plain_password: str, ) -> dict: write_cmd = ( f"printf %s {shlex.quote(content)}" f" | sudo -S tee {shlex.quote(sudoers_path)} > /dev/null" f" && sudo chmod 440 {shlex.quote(sudoers_path)}" f" && sudo visudo -cf {shlex.quote(sudoers_path)}" ) conn = None try: conn = await ssh_pool.acquire(server) result = await asyncio.wait_for( conn.run(write_cmd, input=plain_password + "\n", timeout=20), timeout=25, ) exit_code = result.exit_status if result.exit_status is not None else -1 stderr_out = (result.stderr or "").strip() stdout_out = (result.stdout or "").strip() if exit_code != 0: detail = stderr_out or stdout_out or f"exit {exit_code}" return {"ok": False, "action": "password_sudo_failed", "detail": detail[:500]} return {"ok": True, "action": "patched_password_sudo"} finally: if conn is not None: await ssh_pool.release(server.id) async def install_nexus_files_sudoers(server: Server, *, dry_run: bool = False) -> dict: """Ensure ``/etc/sudoers.d/nexus-files`` includes rsync (and file ops whitelist). Returns ``{ok, action, server_id, server_name, ssh_user, detail?}``. """ ssh_user = (server.username or "root").strip() or "root" base = { "server_id": server.id, "server_name": server.name, "ssh_user": ssh_user, } if ssh_user == "root": return {**base, "ok": True, "action": "skip_root"} if await probe_rsync_sudo(server): return {**base, "ok": True, "action": "already_ok"} if dry_run: return {**base, "ok": True, "action": "would_patch"} content = build_nexus_files_sudoers(ssh_user) sudoers_path = NEXUS_FILES_SUDOERS_PATH if server.password: plain = decrypt_value(server.password) outcome = await _write_sudoers_with_password(server, content, sudoers_path, plain) if not outcome["ok"]: return {**base, **outcome} if await probe_rsync_sudo(server): return {**base, **outcome} return { **base, "ok": False, "action": "written_but_rsync_probe_failed", "detail": "sudoers 已写入但 sudo -n rsync 仍失败", } write_inner = ( f"printf %s {shlex.quote(content)}" f" | tee {shlex.quote(sudoers_path)} > /dev/null" f" && chmod 440 {shlex.quote(sudoers_path)}" f" && visudo -cf {shlex.quote(sudoers_path)}" ) result = await exec_ssh_command_with_fallback(server, write_inner, timeout=30) if result.get("exit_code") != 0: detail = (result.get("stderr") or result.get("stdout") or "")[:500] return { **base, "ok": False, "action": "nopasswd_write_failed", "detail": detail or "无法写入 sudoers(需存储 SSH 密码或已配置 bash 免密 sudo)", } if await probe_rsync_sudo(server): return {**base, "ok": True, "action": "patched_nopasswd"} return { **base, "ok": False, "action": "written_but_rsync_probe_failed", "detail": "sudoers 已写入但 sudo -n rsync 仍失败,请检查 visudo 与白名单路径", }