84b388fae2
文件推送与文件管理对齐:非 root 默认 auto_sudo 时使用 sudo -n rsync; 新增批量 sudoers 补丁脚本以补全 rsync 白名单。
141 lines
5.0 KiB
Python
141 lines
5.0 KiB
Python
"""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 与白名单路径",
|
||
}
|