mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-28 23:57:58 +09:00
Hardening plan §8. A stdlib-only, import-safe, unit-tested Python package that
ports the stabilized bash install logic into testable orchestration. Per the
plan's migration path this is ADDITIVE — install.sh stays canonical and this is
NOT wired into it yet; it is the validated foundation a future thin-bootstrap
install.sh can call.
backend/serverkit_installer/:
- _proc.py — sudo-aware subprocess helpers, mirroring app/utils/system.py
WITHOUT importing app (the bootstrap runs before the venv
exists, so it must not pull in Flask).
- distro.py — OS family mapping (a faithful port of install.sh's
os_family_from, rhel-before-fedora) + container/WSL/systemd
predicates (mirroring scripts/lib/env.sh).
- deps.py — loads scripts/deps/manifest.yaml (its primary consumer) and
resolves base/python/node/docker/pkg-manager per family.
- firewall.py — detect + pure open/close command builders (port of
scripts/lib/firewall.sh).
- service.py — init detect + pure start/stop/enable/reload builders (port of
scripts/lib/init.sh).
- main.py — composes a READ-ONLY install "plan" + `python -m
serverkit_installer plan|detect` CLI. No host mutation.
Tests: backend/tests/test_serverkit_installer.py — 46 pure-logic cases
(family mapping vs os_family_from, real-manifest resolution for all six
families, firewall/service command builders, env predicates, composed plan).
46 passed; py_compile + pyflakes clean. The CLI proves the manifest is genuinely
consumed (Debian -> apt + python3.11 + base packages), not dead config.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
64 lines
2.1 KiB
Python
64 lines
2.1 KiB
Python
"""Subprocess + privilege helpers for the installer bootstrap.
|
|
|
|
Stdlib-only on purpose: this code can run *before* the backend venv exists, so
|
|
it must not import ``app.utils.system`` (which imports Flask). It mirrors that
|
|
module's ``needs_sudo``/``run_privileged`` semantics so the two stay
|
|
conceptually unified — a command list is wrapped with ``sudo`` only when the
|
|
process isn't already root, isn't on Windows, and ``sudo`` is available.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from typing import List, Sequence
|
|
|
|
|
|
def needs_sudo() -> bool:
|
|
"""Return True if commands should be prefixed with ``sudo``."""
|
|
if os.name == "nt":
|
|
return False
|
|
geteuid = getattr(os, "geteuid", None)
|
|
if geteuid is not None and geteuid() == 0:
|
|
return False
|
|
if not shutil.which("sudo"):
|
|
return False
|
|
return True
|
|
|
|
|
|
def with_privilege(cmd: Sequence[str]) -> List[str]:
|
|
"""Return *cmd* with ``sudo`` prepended when necessary."""
|
|
parts = list(cmd)
|
|
if parts and needs_sudo() and parts[0] != "sudo":
|
|
return ["sudo"] + parts
|
|
return parts
|
|
|
|
|
|
def run(cmd: Sequence[str], *, dry_run: bool = False, check: bool = False,
|
|
capture: bool = True, **kwargs) -> subprocess.CompletedProcess:
|
|
"""Run *cmd* (a list). Under ``dry_run`` nothing executes — a synthetic,
|
|
successful CompletedProcess is returned so callers can treat both paths
|
|
uniformly.
|
|
"""
|
|
parts = list(cmd)
|
|
if dry_run:
|
|
return subprocess.CompletedProcess(parts, 0, stdout="", stderr="")
|
|
kwargs.setdefault("text", True)
|
|
if capture:
|
|
kwargs.setdefault("stdout", subprocess.PIPE)
|
|
kwargs.setdefault("stderr", subprocess.PIPE)
|
|
return subprocess.run(parts, check=check, **kwargs)
|
|
|
|
|
|
def command_ok(cmd: Sequence[str]) -> bool:
|
|
"""Run *cmd* quietly and return True iff it exits 0. Never raises."""
|
|
try:
|
|
result = subprocess.run(
|
|
list(cmd),
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
return result.returncode == 0
|
|
except (OSError, ValueError):
|
|
return False
|