new changes
This commit is contained in:
Binary file not shown.
@@ -10,13 +10,13 @@ from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.core.utils import exec_shell_sync
|
||||
from app.core.utils import environment_with_system_path, exec_shell_sync
|
||||
from app.api.auth import get_current_user
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/soft", tags=["soft"])
|
||||
|
||||
PmKind = Literal["apt", "dnf", "yum", "apk", "none"]
|
||||
PmKind = Literal["apt", "dnf", "yum", "microdnf", "apk", "none"]
|
||||
|
||||
# Per distro: apt = Debian/Ubuntu, rpm = RHEL/Fedora/Alma/Rocky, apk = Alpine (best-effort)
|
||||
SOFTWARE_LIST: list[dict[str, str]] = [
|
||||
@@ -135,14 +135,30 @@ SOFTWARE_LIST: list[dict[str, str]] = [
|
||||
]
|
||||
|
||||
|
||||
def _resolve_command(name: str) -> str | None:
|
||||
"""Locate an executable; systemd often provides a venv-only PATH, so scan standard dirs too."""
|
||||
std = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
merged = f"{os.environ.get('PATH', '').strip()}:{std}".strip(":")
|
||||
found = shutil.which(name, path=merged)
|
||||
if found:
|
||||
return found
|
||||
for d in ("/usr/bin", "/usr/sbin", "/bin", "/sbin", "/usr/local/bin", "/usr/local/sbin"):
|
||||
cand = os.path.join(d, name)
|
||||
if os.path.isfile(cand) and os.access(cand, os.X_OK):
|
||||
return cand
|
||||
return None
|
||||
|
||||
|
||||
def _detect_package_manager() -> PmKind:
|
||||
if shutil.which("apt-get"):
|
||||
if _resolve_command("apt-get"):
|
||||
return "apt"
|
||||
if shutil.which("dnf"):
|
||||
if _resolve_command("dnf"):
|
||||
return "dnf"
|
||||
if shutil.which("yum"):
|
||||
if _resolve_command("yum"):
|
||||
return "yum"
|
||||
if shutil.which("apk"):
|
||||
if _resolve_command("microdnf"):
|
||||
return "microdnf"
|
||||
if _resolve_command("apk"):
|
||||
return "apk"
|
||||
return "none"
|
||||
|
||||
@@ -150,7 +166,7 @@ def _detect_package_manager() -> PmKind:
|
||||
def _package_name(entry: dict[str, str], pm: PmKind) -> str:
|
||||
if pm == "apt":
|
||||
return entry["apt"]
|
||||
if pm in ("dnf", "yum"):
|
||||
if pm in ("dnf", "yum", "microdnf"):
|
||||
return entry["rpm"]
|
||||
if pm == "apk":
|
||||
return entry.get("apk") or entry["apt"]
|
||||
@@ -158,20 +174,21 @@ def _package_name(entry: dict[str, str], pm: PmKind) -> str:
|
||||
|
||||
|
||||
def _apt_env() -> dict[str, str]:
|
||||
env = os.environ.copy()
|
||||
env = environment_with_system_path()
|
||||
env.setdefault("DEBIAN_FRONTEND", "noninteractive")
|
||||
env.setdefault("APT_LISTCHANGES_FRONTEND", "none")
|
||||
return env
|
||||
|
||||
|
||||
def _run_shell(script: str, timeout: int, env: dict[str, str] | None = None) -> None:
|
||||
run_env = environment_with_system_path(env)
|
||||
result = subprocess.run(
|
||||
script,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
env=env if env is not None else os.environ.copy(),
|
||||
env=run_env,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return
|
||||
@@ -192,11 +209,13 @@ def _check_installed_apt(pkg: str) -> tuple[bool, str]:
|
||||
|
||||
def _check_installed_rpm(pkg: str) -> tuple[bool, str]:
|
||||
try:
|
||||
rpm_bin = _resolve_command("rpm") or "rpm"
|
||||
result = subprocess.run(
|
||||
["rpm", "-q", "--queryformat", "%{EVR}", pkg],
|
||||
[rpm_bin, "-q", "--queryformat", "%{EVR}", pkg],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
env=environment_with_system_path(),
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return False, ""
|
||||
@@ -208,11 +227,13 @@ def _check_installed_rpm(pkg: str) -> tuple[bool, str]:
|
||||
|
||||
def _check_installed_apk(pkg: str) -> tuple[bool, str]:
|
||||
try:
|
||||
apk_bin = _resolve_command("apk") or "apk"
|
||||
r = subprocess.run(
|
||||
["apk", "info", "-e", pkg],
|
||||
[apk_bin, "info", "-e", pkg],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
env=environment_with_system_path(),
|
||||
)
|
||||
if r.returncode != 0:
|
||||
return False, ""
|
||||
@@ -228,7 +249,7 @@ def _check_installed_apk(pkg: str) -> tuple[bool, str]:
|
||||
def _check_installed(pm: PmKind, pkg: str) -> tuple[bool, str]:
|
||||
if pm == "apt":
|
||||
return _check_installed_apt(pkg)
|
||||
if pm in ("dnf", "yum"):
|
||||
if pm in ("dnf", "yum", "microdnf"):
|
||||
return _check_installed_rpm(pkg)
|
||||
if pm == "apk":
|
||||
return _check_installed_apk(pkg)
|
||||
@@ -238,32 +259,60 @@ def _check_installed(pm: PmKind, pkg: str) -> tuple[bool, str]:
|
||||
def _install_script(pm: PmKind, pkg: str) -> tuple[str, int, dict[str, str] | None]:
|
||||
q = shlex.quote(pkg)
|
||||
if pm == "apt":
|
||||
return f"apt-get update -qq && apt-get install -y {q}", 600, _apt_env()
|
||||
ag = _resolve_command("apt-get")
|
||||
if not ag:
|
||||
raise HTTPException(status_code=501, detail="apt-get not found on this system.")
|
||||
exe = shlex.quote(ag)
|
||||
return f"{exe} update -qq && {exe} install -y {q}", 600, _apt_env()
|
||||
if pm == "dnf":
|
||||
return f"dnf install -y {q}", 600, None
|
||||
exe = shlex.quote(x) if (x := _resolve_command("dnf")) else None
|
||||
if exe:
|
||||
return f"{exe} install -y {q}", 600, None
|
||||
if pm == "yum":
|
||||
return f"yum install -y {q}", 600, None
|
||||
exe = shlex.quote(x) if (x := _resolve_command("yum")) else None
|
||||
if exe:
|
||||
return f"{exe} install -y {q}", 600, None
|
||||
if pm == "microdnf":
|
||||
exe = shlex.quote(x) if (x := _resolve_command("microdnf")) else None
|
||||
if exe:
|
||||
return f"{exe} install -y {q}", 600, None
|
||||
if pm == "apk":
|
||||
return f"apk update && apk add {q}", 600, None
|
||||
exe = shlex.quote(x) if (x := _resolve_command("apk")) else None
|
||||
if exe:
|
||||
return f"{exe} update && {exe} add {q}", 600, None
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="No supported package manager found (need apt-get, dnf, yum, or apk).",
|
||||
detail="No supported package manager found (need apt-get, dnf, yum, microdnf, or apk).",
|
||||
)
|
||||
|
||||
|
||||
def _uninstall_script(pm: PmKind, pkg: str) -> tuple[str, int, dict[str, str] | None]:
|
||||
q = shlex.quote(pkg)
|
||||
if pm == "apt":
|
||||
return f"apt-get remove -y {q}", 180, _apt_env()
|
||||
ag = _resolve_command("apt-get")
|
||||
if ag:
|
||||
exe = shlex.quote(ag)
|
||||
return f"{exe} remove -y {q}", 180, _apt_env()
|
||||
raise HTTPException(status_code=501, detail="apt-get not found on this system.")
|
||||
if pm == "dnf":
|
||||
return f"dnf remove -y {q}", 180, None
|
||||
exe = shlex.quote(x) if (x := _resolve_command("dnf")) else None
|
||||
if exe:
|
||||
return f"{exe} remove -y {q}", 180, None
|
||||
if pm == "yum":
|
||||
return f"yum remove -y {q}", 180, None
|
||||
exe = shlex.quote(x) if (x := _resolve_command("yum")) else None
|
||||
if exe:
|
||||
return f"{exe} remove -y {q}", 180, None
|
||||
if pm == "microdnf":
|
||||
exe = shlex.quote(x) if (x := _resolve_command("microdnf")) else None
|
||||
if exe:
|
||||
return f"{exe} remove -y {q}", 180, None
|
||||
if pm == "apk":
|
||||
return f"apk del {q}", 120, None
|
||||
exe = shlex.quote(x) if (x := _resolve_command("apk")) else None
|
||||
if exe:
|
||||
return f"{exe} del {q}", 120, None
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="No supported package manager found (need apt-get, dnf, yum, or apk).",
|
||||
detail="No supported package manager found (need apt-get, dnf, yum, microdnf, or apk).",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -9,6 +9,24 @@ from typing import Tuple, Optional
|
||||
|
||||
regex_safe_path = re.compile(r"^[\w\s./\-]*$")
|
||||
|
||||
# systemd often sets PATH to venv-only; subprocess shells then miss /usr/bin (dnf, apt-get, …).
|
||||
_SYSTEM_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
|
||||
def ensure_system_path(env: dict[str, str]) -> None:
|
||||
"""Append standard locations to PATH if /usr/bin is missing."""
|
||||
cur = (env.get("PATH") or "").strip()
|
||||
if "/usr/bin" in cur:
|
||||
return
|
||||
env["PATH"] = f"{cur}:{_SYSTEM_PATH}" if cur else _SYSTEM_PATH
|
||||
|
||||
|
||||
def environment_with_system_path(base: Optional[dict[str, str]] = None) -> dict[str, str]:
|
||||
"""Copy of process env (or base) with PATH guaranteed to include system bin dirs."""
|
||||
env = dict(base) if base is not None else os.environ.copy()
|
||||
ensure_system_path(env)
|
||||
return env
|
||||
|
||||
|
||||
def md5(strings: str | bytes) -> str:
|
||||
"""Generate MD5 hash"""
|
||||
@@ -78,6 +96,7 @@ async def exec_shell(
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=cwd,
|
||||
env=environment_with_system_path(),
|
||||
)
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
@@ -102,6 +121,7 @@ def exec_shell_sync(cmd: str, timeout: Optional[float] = None, cwd: Optional[str
|
||||
capture_output=True,
|
||||
timeout=timeout or 300,
|
||||
cwd=cwd,
|
||||
env=environment_with_system_path(),
|
||||
)
|
||||
out = result.stdout.decode("utf-8", errors="replace") if result.stdout else ""
|
||||
err = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
|
||||
|
||||
Reference in New Issue
Block a user