Fix PATH for apt/git/nmcli in systemd service environment
systemd runs services with a minimal PATH that may not include /usr/bin or /usr/sbin. Add _safe_env() helpers that ensure standard paths are present, and expand apt-cache discovery to check common locations directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9c402e3726
commit
0ec9726db4
3 changed files with 52 additions and 6 deletions
|
|
@ -46,6 +46,17 @@ def _require_nmcli():
|
||||||
raise HTTPException(503, "NetworkManager (nmcli) not available on this system")
|
raise HTTPException(503, "NetworkManager (nmcli) not available on this system")
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_env():
|
||||||
|
"""Build environment with full PATH for nmcli commands."""
|
||||||
|
env = {**os.environ, "LC_ALL": "C"}
|
||||||
|
path = env.get("PATH", "")
|
||||||
|
for p in ("/usr/bin", "/usr/sbin", "/bin", "/sbin"):
|
||||||
|
if p not in path:
|
||||||
|
path = p + ":" + path
|
||||||
|
env["PATH"] = path
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
async def _nmcli(*args: str, timeout: float = 30) -> str:
|
async def _nmcli(*args: str, timeout: float = 30) -> str:
|
||||||
"""Run nmcli with C locale, return stdout. Raise HTTPException on failure."""
|
"""Run nmcli with C locale, return stdout. Raise HTTPException on failure."""
|
||||||
_require_nmcli()
|
_require_nmcli()
|
||||||
|
|
@ -54,7 +65,7 @@ async def _nmcli(*args: str, timeout: float = 30) -> str:
|
||||||
*cmd,
|
*cmd,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
env={**os.environ, "LC_ALL": "C"},
|
env=_safe_env(),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,19 @@ router = APIRouter(prefix="/api/packages", tags=["packages"])
|
||||||
# Guard
|
# Guard
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_HAS_APT = bool(shutil.which("apt-cache"))
|
def _find_apt():
|
||||||
|
"""Find apt-cache binary, checking common paths if PATH is restricted."""
|
||||||
|
found = shutil.which("apt-cache")
|
||||||
|
if found:
|
||||||
|
return True
|
||||||
|
# systemd services may have a minimal PATH — check directly
|
||||||
|
for p in ("/usr/bin/apt-cache", "/usr/sbin/apt-cache"):
|
||||||
|
if os.path.isfile(p) and os.access(p, os.X_OK):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
_HAS_APT = _find_apt()
|
||||||
|
|
||||||
|
|
||||||
def _require_apt():
|
def _require_apt():
|
||||||
|
|
@ -38,6 +50,18 @@ def _validate_name(name: str) -> str:
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_env():
|
||||||
|
"""Build environment with full PATH for apt/dpkg commands."""
|
||||||
|
env = {**os.environ, "LC_ALL": "C", "DEBIAN_FRONTEND": "noninteractive"}
|
||||||
|
# Ensure standard paths are included (systemd may use minimal PATH)
|
||||||
|
path = env.get("PATH", "")
|
||||||
|
for p in ("/usr/bin", "/usr/sbin", "/bin", "/sbin"):
|
||||||
|
if p not in path:
|
||||||
|
path = p + ":" + path
|
||||||
|
env["PATH"] = path
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
async def _apt(*args: str, timeout: float = 30) -> str:
|
async def _apt(*args: str, timeout: float = 30) -> str:
|
||||||
"""Run an apt/dpkg command and return stdout."""
|
"""Run an apt/dpkg command and return stdout."""
|
||||||
_require_apt()
|
_require_apt()
|
||||||
|
|
@ -46,7 +70,7 @@ async def _apt(*args: str, timeout: float = 30) -> str:
|
||||||
*cmd,
|
*cmd,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
env={**os.environ, "LC_ALL": "C", "DEBIAN_FRONTEND": "noninteractive"},
|
env=_safe_env(),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||||
|
|
@ -68,7 +92,7 @@ async def _apt_nofail(*args: str, timeout: float = 30) -> tuple[int, str, str]:
|
||||||
*cmd,
|
*cmd,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
env={**os.environ, "LC_ALL": "C", "DEBIAN_FRONTEND": "noninteractive"},
|
env=_safe_env(),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,17 @@ def _require_git():
|
||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _safe_env():
|
||||||
|
"""Build environment with full PATH for git commands."""
|
||||||
|
env = {**os.environ, "LC_ALL": "C"}
|
||||||
|
path = env.get("PATH", "")
|
||||||
|
for p in ("/usr/bin", "/usr/sbin", "/bin", "/sbin"):
|
||||||
|
if p not in path:
|
||||||
|
path = p + ":" + path
|
||||||
|
env["PATH"] = path
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
async def _git(*args: str, timeout: float = 30) -> str:
|
async def _git(*args: str, timeout: float = 30) -> str:
|
||||||
"""Run a git command in the Atlus install directory."""
|
"""Run a git command in the Atlus install directory."""
|
||||||
_require_git()
|
_require_git()
|
||||||
|
|
@ -38,7 +49,7 @@ async def _git(*args: str, timeout: float = 30) -> str:
|
||||||
*cmd,
|
*cmd,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
env={**os.environ, "LC_ALL": "C"},
|
env=_safe_env(),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||||
|
|
@ -60,7 +71,7 @@ async def _git_nofail(*args: str, timeout: float = 30) -> tuple[int, str]:
|
||||||
*cmd,
|
*cmd,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
env={**os.environ, "LC_ALL": "C"},
|
env=_safe_env(),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue