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 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.api.auth import get_current_user
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
|
||||||
router = APIRouter(prefix="/soft", tags=["soft"])
|
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)
|
# Per distro: apt = Debian/Ubuntu, rpm = RHEL/Fedora/Alma/Rocky, apk = Alpine (best-effort)
|
||||||
SOFTWARE_LIST: list[dict[str, str]] = [
|
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:
|
def _detect_package_manager() -> PmKind:
|
||||||
if shutil.which("apt-get"):
|
if _resolve_command("apt-get"):
|
||||||
return "apt"
|
return "apt"
|
||||||
if shutil.which("dnf"):
|
if _resolve_command("dnf"):
|
||||||
return "dnf"
|
return "dnf"
|
||||||
if shutil.which("yum"):
|
if _resolve_command("yum"):
|
||||||
return "yum"
|
return "yum"
|
||||||
if shutil.which("apk"):
|
if _resolve_command("microdnf"):
|
||||||
|
return "microdnf"
|
||||||
|
if _resolve_command("apk"):
|
||||||
return "apk"
|
return "apk"
|
||||||
return "none"
|
return "none"
|
||||||
|
|
||||||
@@ -150,7 +166,7 @@ def _detect_package_manager() -> PmKind:
|
|||||||
def _package_name(entry: dict[str, str], pm: PmKind) -> str:
|
def _package_name(entry: dict[str, str], pm: PmKind) -> str:
|
||||||
if pm == "apt":
|
if pm == "apt":
|
||||||
return entry["apt"]
|
return entry["apt"]
|
||||||
if pm in ("dnf", "yum"):
|
if pm in ("dnf", "yum", "microdnf"):
|
||||||
return entry["rpm"]
|
return entry["rpm"]
|
||||||
if pm == "apk":
|
if pm == "apk":
|
||||||
return entry.get("apk") or entry["apt"]
|
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]:
|
def _apt_env() -> dict[str, str]:
|
||||||
env = os.environ.copy()
|
env = environment_with_system_path()
|
||||||
env.setdefault("DEBIAN_FRONTEND", "noninteractive")
|
env.setdefault("DEBIAN_FRONTEND", "noninteractive")
|
||||||
env.setdefault("APT_LISTCHANGES_FRONTEND", "none")
|
env.setdefault("APT_LISTCHANGES_FRONTEND", "none")
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
def _run_shell(script: str, timeout: int, env: dict[str, str] | None = None) -> None:
|
def _run_shell(script: str, timeout: int, env: dict[str, str] | None = None) -> None:
|
||||||
|
run_env = environment_with_system_path(env)
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
script,
|
script,
|
||||||
shell=True,
|
shell=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
env=env if env is not None else os.environ.copy(),
|
env=run_env,
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
return
|
return
|
||||||
@@ -192,11 +209,13 @@ def _check_installed_apt(pkg: str) -> tuple[bool, str]:
|
|||||||
|
|
||||||
def _check_installed_rpm(pkg: str) -> tuple[bool, str]:
|
def _check_installed_rpm(pkg: str) -> tuple[bool, str]:
|
||||||
try:
|
try:
|
||||||
|
rpm_bin = _resolve_command("rpm") or "rpm"
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["rpm", "-q", "--queryformat", "%{EVR}", pkg],
|
[rpm_bin, "-q", "--queryformat", "%{EVR}", pkg],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=5,
|
timeout=5,
|
||||||
|
env=environment_with_system_path(),
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
return False, ""
|
return False, ""
|
||||||
@@ -208,11 +227,13 @@ def _check_installed_rpm(pkg: str) -> tuple[bool, str]:
|
|||||||
|
|
||||||
def _check_installed_apk(pkg: str) -> tuple[bool, str]:
|
def _check_installed_apk(pkg: str) -> tuple[bool, str]:
|
||||||
try:
|
try:
|
||||||
|
apk_bin = _resolve_command("apk") or "apk"
|
||||||
r = subprocess.run(
|
r = subprocess.run(
|
||||||
["apk", "info", "-e", pkg],
|
[apk_bin, "info", "-e", pkg],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=5,
|
timeout=5,
|
||||||
|
env=environment_with_system_path(),
|
||||||
)
|
)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
return False, ""
|
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]:
|
def _check_installed(pm: PmKind, pkg: str) -> tuple[bool, str]:
|
||||||
if pm == "apt":
|
if pm == "apt":
|
||||||
return _check_installed_apt(pkg)
|
return _check_installed_apt(pkg)
|
||||||
if pm in ("dnf", "yum"):
|
if pm in ("dnf", "yum", "microdnf"):
|
||||||
return _check_installed_rpm(pkg)
|
return _check_installed_rpm(pkg)
|
||||||
if pm == "apk":
|
if pm == "apk":
|
||||||
return _check_installed_apk(pkg)
|
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]:
|
def _install_script(pm: PmKind, pkg: str) -> tuple[str, int, dict[str, str] | None]:
|
||||||
q = shlex.quote(pkg)
|
q = shlex.quote(pkg)
|
||||||
if pm == "apt":
|
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":
|
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":
|
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":
|
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(
|
raise HTTPException(
|
||||||
status_code=501,
|
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]:
|
def _uninstall_script(pm: PmKind, pkg: str) -> tuple[str, int, dict[str, str] | None]:
|
||||||
q = shlex.quote(pkg)
|
q = shlex.quote(pkg)
|
||||||
if pm == "apt":
|
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":
|
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":
|
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":
|
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(
|
raise HTTPException(
|
||||||
status_code=501,
|
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./\-]*$")
|
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:
|
def md5(strings: str | bytes) -> str:
|
||||||
"""Generate MD5 hash"""
|
"""Generate MD5 hash"""
|
||||||
@@ -78,6 +96,7 @@ async def exec_shell(
|
|||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
|
env=environment_with_system_path(),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
stdout, stderr = await asyncio.wait_for(
|
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,
|
capture_output=True,
|
||||||
timeout=timeout or 300,
|
timeout=timeout or 300,
|
||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
|
env=environment_with_system_path(),
|
||||||
)
|
)
|
||||||
out = result.stdout.decode("utf-8", errors="replace") if result.stdout else ""
|
out = result.stdout.decode("utf-8", errors="replace") if result.stdout else ""
|
||||||
err = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
|
err = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function SoftPage() {
|
|||||||
|
|
||||||
<div className="mb-4 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 text-amber-800 dark:text-amber-200 text-sm">
|
<div className="mb-4 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 text-amber-800 dark:text-amber-200 text-sm">
|
||||||
Installs use your server package manager ({detectedPm || '…loading…'}). Panel must run as root
|
Installs use your server package manager ({detectedPm || '…loading…'}). Panel must run as root
|
||||||
(or equivalent). Supported: Debian/Ubuntu (apt), RHEL/Fedora/Alma/Rocky (dnf/yum), Alpine (apk).
|
(or equivalent). Supported: apt, dnf/yum/microdnf, apk.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
|||||||
@@ -288,7 +288,8 @@ $REDIS_WANTS
|
|||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
User=root
|
||||||
WorkingDirectory=$INSTALL_PATH/backend
|
WorkingDirectory=$INSTALL_PATH/backend
|
||||||
Environment="PATH=$INSTALL_PATH/backend/venv/bin:\$PATH"
|
# Include system paths: systemd does not expand \$PATH reliably; venv-only PATH breaks dnf/apt from the API.
|
||||||
|
Environment="PATH=$INSTALL_PATH/backend/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
Environment=PYTHONUNBUFFERED=1
|
Environment=PYTHONUNBUFFERED=1
|
||||||
ExecStart=$INSTALL_PATH/backend/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port $BACKEND_PORT
|
ExecStart=$INSTALL_PATH/backend/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port $BACKEND_PORT
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|||||||
Reference in New Issue
Block a user