mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-27 23:28:03 +09:00
- Harness: bootstrap find/awk in distro-compatibility.py (AL2023, Tumbleweed, SLES BCI minimal images lack them; the source-level suites legitimately use both) - Python gate 3.11-3.14: Arch/Manjaro rolling ship only python 3.14 with no versioned fallback packages (same trap as Debian 13/#99); align scripts/update.sh, which had drifted to a 3.11-3.12 gate - Gentoo: add emerge as a supported installer package-manager path (install.sh, scripts/lib/pkg.sh, distro.py family, deps manifest); provision via slotted dev-lang/python:3.13 -> 3.12 - Tests: move out-of-range stubs above the new ceiling, shadow all locate_python candidate names (real python3.14/3.13 in Manjaro/ Gentoo images were winning over fixtures), cover the 7th manager - Catalog: Gentoo is no longer a documented expected failure Verified: test_lib 32/32, test_install 124/124, test_update 76/76, pytest test_serverkit_installer 46/46, and all 6 previously failing images now pass both quick and provision container probes (12/12).
156 lines
4.9 KiB
Python
156 lines
4.9 KiB
Python
"""OS / distro + environment detection.
|
|
|
|
The family mapping is a faithful port of install.sh's ``os_family_from`` (kept
|
|
in lockstep so bash and Python agree on what "supported distro" means), and the
|
|
container/WSL/systemd predicates mirror scripts/lib/env.sh.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
from dataclasses import dataclass
|
|
|
|
FAMILIES = ("debian", "rhel", "fedora", "suse", "arch", "alpine", "gentoo")
|
|
|
|
# Exact ID → family map (matches install.sh's case arms).
|
|
_ID_FAMILY = {
|
|
"ubuntu": "debian", "linuxmint": "debian", "pop": "debian",
|
|
"raspbian": "debian", "elementary": "debian", "zorin": "debian",
|
|
"debian": "debian", "devuan": "debian",
|
|
"fedora": "fedora", "nobara": "fedora",
|
|
"rocky": "rhel", "almalinux": "rhel", "rhel": "rhel", "centos": "rhel",
|
|
"ol": "rhel", "oracle": "rhel", "eurolinux": "rhel",
|
|
"sles": "suse", "sled": "suse", "suse": "suse", "sle-micro": "suse",
|
|
"arch": "arch", "manjaro": "arch", "endeavouros": "arch", "cachyos": "arch",
|
|
"alpine": "alpine",
|
|
"gentoo": "gentoo", "funtoo": "gentoo",
|
|
}
|
|
|
|
# ID_LIKE substrings → family, in priority order. rhel before fedora so RHEL
|
|
# clones (ID_LIKE="rhel centos fedora") pick the RHEL family, not fedora.
|
|
_LIKE_FAMILY = (
|
|
("debian", "debian"), ("ubuntu", "debian"),
|
|
("rhel", "rhel"), ("centos", "rhel"),
|
|
("fedora", "fedora"),
|
|
("suse", "suse"),
|
|
("arch", "arch"),
|
|
("alpine", "alpine"),
|
|
("gentoo", "gentoo"),
|
|
)
|
|
|
|
|
|
def family_from(distro_id: str, id_like: str = "") -> str:
|
|
"""Map an os-release ID (+ ID_LIKE fallback) to a ServerKit family.
|
|
|
|
Returns one of FAMILIES or ``"unknown"``.
|
|
"""
|
|
did = (distro_id or "").strip().lower()
|
|
if did in _ID_FAMILY:
|
|
return _ID_FAMILY[did]
|
|
if did.startswith("opensuse"):
|
|
return "suse"
|
|
like = (id_like or "").lower()
|
|
for needle, family in _LIKE_FAMILY:
|
|
if needle in like:
|
|
return family
|
|
return "unknown"
|
|
|
|
|
|
def parse_os_release(path: str = "/etc/os-release") -> dict:
|
|
"""Parse a shell-style os-release file into a dict. Missing file → {}."""
|
|
out: dict = {}
|
|
try:
|
|
with open(path, encoding="utf-8") as handle:
|
|
for raw in handle:
|
|
line = raw.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
out[key.strip()] = value.strip().strip('"').strip("'")
|
|
except OSError:
|
|
pass
|
|
return out
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Distro:
|
|
id: str
|
|
id_like: str
|
|
pretty_name: str
|
|
family: str
|
|
|
|
@property
|
|
def supported(self) -> bool:
|
|
return self.family in FAMILIES
|
|
|
|
|
|
def detect(os_release_path: str = "/etc/os-release") -> Distro:
|
|
"""Detect the running distro from an os-release file."""
|
|
data = parse_os_release(os_release_path)
|
|
return Distro(
|
|
id=data.get("ID", ""),
|
|
id_like=data.get("ID_LIKE", ""),
|
|
pretty_name=data.get("PRETTY_NAME", ""),
|
|
family=family_from(data.get("ID", ""), data.get("ID_LIKE", "")),
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Environment predicates (mirror scripts/lib/env.sh). The keyword overrides
|
|
# exist for unit testing against fixtures.
|
|
# ---------------------------------------------------------------------------
|
|
_CONTAINER_CGROUP_MARKERS = ("docker", "lxc", "containerd", "kubepods", "podman")
|
|
|
|
|
|
def is_container(
|
|
*,
|
|
force: "bool | None" = None,
|
|
dockerenv: str = "/.dockerenv",
|
|
containerenv: str = "/run/.containerenv",
|
|
cgroup_file: str = "/proc/1/cgroup",
|
|
container_env: "str | None" = None,
|
|
) -> bool:
|
|
"""True when running inside a container."""
|
|
if force is not None:
|
|
return force
|
|
if os.path.exists(dockerenv):
|
|
return True
|
|
if os.path.exists(containerenv):
|
|
return True
|
|
env_val = os.environ.get("container") if container_env is None else container_env
|
|
if env_val:
|
|
return True
|
|
try:
|
|
with open(cgroup_file, encoding="utf-8", errors="ignore") as handle:
|
|
content = handle.read()
|
|
return any(marker in content for marker in _CONTAINER_CGROUP_MARKERS)
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def is_wsl(
|
|
*,
|
|
force: "bool | None" = None,
|
|
osrelease_file: str = "/proc/sys/kernel/osrelease",
|
|
) -> bool:
|
|
"""True under Windows Subsystem for Linux."""
|
|
if force is not None:
|
|
return force
|
|
try:
|
|
with open(osrelease_file, encoding="utf-8", errors="ignore") as handle:
|
|
content = handle.read().lower()
|
|
return "microsoft" in content or "wsl" in content
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def has_systemd(
|
|
*,
|
|
force: "bool | None" = None,
|
|
systemd_dir: str = "/run/systemd/system",
|
|
) -> bool:
|
|
"""True when systemd is usable as the init/service manager."""
|
|
if force is not None:
|
|
return force
|
|
return bool(shutil.which("systemctl")) and os.path.isdir(systemd_dir)
|