fix(watch): psutil SSH 安装 apt 走 sudo
非 root 用户 apt-get 未加 sudo 导致 Permission denied;补充 run_root 与中文错误提示。
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
# 审计 — psutil 安装 apt sudo 修复
|
||||||
|
|
||||||
|
**日期**:2026-06-12
|
||||||
|
**Changelog**: `docs/changelog/2026-06-12-psutil-install-sudo-fix.md`
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
| 文件 |
|
||||||
|
|------|
|
||||||
|
| `server/utils/psutil_install.py` |
|
||||||
|
| `server/application/server_batch_common.py` |
|
||||||
|
| `tests/test_psutil_install.py` |
|
||||||
|
|
||||||
|
## Step 3 / Closure / DoD
|
||||||
|
|
||||||
|
- H1 PASS — 行为修复,无新 API
|
||||||
|
- DoD: pytest test_psutil_install 5 passed
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Changelog — psutil SSH 安装 apt sudo 修复
|
||||||
|
|
||||||
|
**日期**:2026-06-12
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
修复非 root SSH 用户点击「安装 psutil(SSH)」时 `apt-get` 无 sudo 导致 Permission denied。
|
||||||
|
|
||||||
|
## 变更
|
||||||
|
|
||||||
|
- `psutil_install` 脚本增加 `run_root()`:`apt/dnf/yum` 与系统 pip 在非 root 时走 `sudo`
|
||||||
|
- `sudo_wrap` sudoers 补充 `python3` / `pip3` 路径
|
||||||
|
- 安装失败时返回中文提示(apt lock / 需免密 sudo)
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/pytest tests/test_psutil_install.py -q
|
||||||
|
```
|
||||||
@@ -45,6 +45,8 @@ def sudo_wrap(cmd: str, ssh_user: str) -> str:
|
|||||||
f"{ssh_user} ALL=(ALL) NOPASSWD: "
|
f"{ssh_user} ALL=(ALL) NOPASSWD: "
|
||||||
"/usr/bin/systemctl, "
|
"/usr/bin/systemctl, "
|
||||||
"/usr/bin/apt-get, /usr/bin/yum, /usr/bin/dnf, /usr/bin/apk, "
|
"/usr/bin/apt-get, /usr/bin/yum, /usr/bin/dnf, /usr/bin/apk, "
|
||||||
|
"/usr/bin/python3, /usr/bin/python3.10, /usr/bin/python3.11, /usr/bin/python3.12, "
|
||||||
|
"/usr/bin/pip3, "
|
||||||
"/usr/bin/rm, /usr/bin/mkdir, /usr/bin/cp, /usr/bin/tee, "
|
"/usr/bin/rm, /usr/bin/mkdir, /usr/bin/cp, /usr/bin/tee, "
|
||||||
"/usr/bin/curl, /usr/bin/fuser, /usr/bin/kill, /usr/bin/pkill, "
|
"/usr/bin/curl, /usr/bin/fuser, /usr/bin/kill, /usr/bin/pkill, "
|
||||||
f"{shlex.quote('/opt/nexus-agent/.venv/bin/pip')}*"
|
f"{shlex.quote('/opt/nexus-agent/.venv/bin/pip')}*"
|
||||||
|
|||||||
@@ -2,13 +2,22 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from server.application.server_batch_common import sudo_wrap
|
from server.application.server_batch_common import sudo_wrap
|
||||||
from server.domain.models import Server
|
from server.domain.models import Server
|
||||||
from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
|
from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
|
||||||
|
|
||||||
# Try --user pip, then distro packages, then system pip. Verify with same python3 as watch probe.
|
# pip --user first (no root); package managers and system pip via run_root (sudo when non-root).
|
||||||
PSUTIL_INSTALL_SHELL = r"""
|
PSUTIL_INSTALL_SHELL = r"""
|
||||||
set -e
|
run_root() {
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
sudo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
verify_psutil() {
|
verify_psutil() {
|
||||||
for py in python3 python3.12 python3.11 python3.10; do
|
for py in python3 python3.12 python3.11 python3.10; do
|
||||||
if command -v "$py" >/dev/null 2>&1 && "$py" -c "import psutil" 2>/dev/null; then
|
if command -v "$py" >/dev/null 2>&1 && "$py" -c "import psutil" 2>/dev/null; then
|
||||||
@@ -18,7 +27,8 @@ verify_psutil() {
|
|||||||
done
|
done
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
verify_psutil || true
|
|
||||||
|
verify_psutil && exit 0
|
||||||
|
|
||||||
for py in python3 python3.12 python3.11 python3.10; do
|
for py in python3 python3.12 python3.11 python3.10; do
|
||||||
command -v "$py" >/dev/null 2>&1 || continue
|
command -v "$py" >/dev/null 2>&1 || continue
|
||||||
@@ -28,19 +38,25 @@ for py in python3 python3.12 python3.11 python3.10; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-psutil \
|
if run_root apt-get update -qq \
|
||||||
&& verify_psutil && exit 0
|
&& run_root env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-psutil; then
|
||||||
|
verify_psutil && exit 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if command -v dnf >/dev/null 2>&1; then
|
if command -v dnf >/dev/null 2>&1; then
|
||||||
dnf install -y -q python3-psutil && verify_psutil && exit 0
|
if run_root dnf install -y -q python3-psutil; then
|
||||||
|
verify_psutil && exit 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if command -v yum >/dev/null 2>&1; then
|
if command -v yum >/dev/null 2>&1; then
|
||||||
yum install -y -q python3-psutil && verify_psutil && exit 0
|
if run_root yum install -y -q python3-psutil; then
|
||||||
|
verify_psutil && exit 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for py in python3 python3.12 python3.11 python3.10; do
|
for py in python3 python3.12 python3.11 python3.10; do
|
||||||
command -v "$py" >/dev/null 2>&1 || continue
|
command -v "$py" >/dev/null 2>&1 || continue
|
||||||
if "$py" -m pip install -q psutil 2>/dev/null; then
|
if run_root "$py" -m pip install -q psutil 2>/dev/null; then
|
||||||
verify_psutil && exit 0
|
verify_psutil && exit 0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -49,13 +65,40 @@ echo "ERROR: psutil install failed — pip/apt unavailable or permission denied"
|
|||||||
exit 1
|
exit 1
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_APT_PERM_RE = re.compile(
|
||||||
|
r"lock file|permission denied|unable to lock|not allowed to execute",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _psutil_install_error_zh(stderr: str, stdout: str) -> str:
|
||||||
|
text = f"{stderr}\n{stdout}"
|
||||||
|
if _APT_PERM_RE.search(text):
|
||||||
|
return (
|
||||||
|
"安装 psutil 需要 root 或免密 sudo(apt/pip)。"
|
||||||
|
"请改用 root SSH,或在子机配置免密 sudo 后重试;也可手动执行:"
|
||||||
|
"sudo apt-get install -y python3-psutil"
|
||||||
|
)
|
||||||
|
if "sudo requires a password" in text.lower():
|
||||||
|
return "sudo 需要密码,请配置免密 sudo 或使用 root SSH 登录"
|
||||||
|
line = ""
|
||||||
|
for chunk in (stderr, stdout):
|
||||||
|
for raw in reversed(chunk.splitlines()):
|
||||||
|
raw = raw.strip()
|
||||||
|
if raw and not raw.startswith("W:"):
|
||||||
|
line = raw[:300]
|
||||||
|
break
|
||||||
|
if line:
|
||||||
|
break
|
||||||
|
return line or "psutil 安装失败"
|
||||||
|
|
||||||
|
|
||||||
def build_psutil_install_cmd(*, ssh_user: str) -> str:
|
def build_psutil_install_cmd(*, ssh_user: str) -> str:
|
||||||
return sudo_wrap(PSUTIL_INSTALL_SHELL.strip(), ssh_user)
|
return sudo_wrap(PSUTIL_INSTALL_SHELL.strip(), ssh_user)
|
||||||
|
|
||||||
|
|
||||||
async def install_psutil_via_ssh(server: Server, *, timeout: int = 90) -> dict[str, str | int]:
|
async def install_psutil_via_ssh(server: Server, *, timeout: int = 120) -> dict[str, str | int | bool]:
|
||||||
"""Run remote install; returns {exit_code, stdout, stderr, already_installed?}."""
|
"""Run remote install; returns success, message, stdout/stderr."""
|
||||||
ssh_user = (server.username or "root").strip()
|
ssh_user = (server.username or "root").strip()
|
||||||
cmd = build_psutil_install_cmd(ssh_user=ssh_user)
|
cmd = build_psutil_install_cmd(ssh_user=ssh_user)
|
||||||
result = await exec_ssh_command(server, cmd, timeout=timeout)
|
result = await exec_ssh_command(server, cmd, timeout=timeout)
|
||||||
@@ -71,11 +114,10 @@ async def install_psutil_via_ssh(server: Server, *, timeout: int = 90) -> dict[s
|
|||||||
"stderr": stderr[:500],
|
"stderr": stderr[:500],
|
||||||
"message": "psutil 已就绪",
|
"message": "psutil 已就绪",
|
||||||
}
|
}
|
||||||
detail = stderr or stdout or f"exit {exit_code}"
|
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"exit_code": exit_code,
|
"exit_code": exit_code,
|
||||||
"stdout": stdout[:2000],
|
"stdout": stdout[:2000],
|
||||||
"stderr": stderr[:500],
|
"stderr": stderr[:500],
|
||||||
"message": detail[:300],
|
"message": _psutil_install_error_zh(stderr, stdout),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,17 @@ from server.utils.psutil_install import build_psutil_install_cmd, install_psutil
|
|||||||
|
|
||||||
def test_build_psutil_install_cmd_root():
|
def test_build_psutil_install_cmd_root():
|
||||||
cmd = build_psutil_install_cmd(ssh_user="root")
|
cmd = build_psutil_install_cmd(ssh_user="root")
|
||||||
|
assert "run_root" in cmd
|
||||||
assert "pip install --user -q psutil" in cmd
|
assert "pip install --user -q psutil" in cmd
|
||||||
assert "python3-psutil" in cmd
|
assert "python3-psutil" in cmd
|
||||||
assert "verify_psutil" in cmd
|
assert "run_root apt-get" in cmd
|
||||||
|
|
||||||
|
|
||||||
def test_build_psutil_install_cmd_non_root_wraps_sudo():
|
def test_build_psutil_install_cmd_non_root_wraps_sudo():
|
||||||
cmd = build_psutil_install_cmd(ssh_user="deploy")
|
cmd = build_psutil_install_cmd(ssh_user="deploy")
|
||||||
assert "sudoers.d/nexus-agent" in cmd
|
assert "sudoers.d/nexus-agent" in cmd
|
||||||
assert "pip install --user -q psutil" in cmd
|
assert "run_root apt-get" in cmd
|
||||||
|
assert 'sudo "$@"' in cmd
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -43,4 +45,14 @@ async def test_install_psutil_via_ssh_failure():
|
|||||||
):
|
):
|
||||||
result = await install_psutil_via_ssh(server)
|
result = await install_psutil_via_ssh(server)
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "permission denied" in result["message"]
|
assert result["message"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_psutil_install_error_zh_apt_lock():
|
||||||
|
from server.utils.psutil_install import _psutil_install_error_zh
|
||||||
|
|
||||||
|
msg = _psutil_install_error_zh(
|
||||||
|
"E: Could not open lock file /var/lib/apt/lists/lock - open (13: Permission denied)",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
assert "免密 sudo" in msg or "root" in msg
|
||||||
|
|||||||
Reference in New Issue
Block a user