"""Versioned, idempotent VESC profile; no packages downloaded and no motor I/O.""" import fcntl import hashlib import json import os from pathlib import Path import platform import subprocess import sys import time import uuid sys.path.insert(0, str(Path(__file__).resolve().parent)) from runtime.serial import discover from runtime.service import atomic STATE = Path("/var/lib/mission-core-node-profiles/vesc") def prepare(): if os.geteuid() != 0 or sys.argv[1:]: raise RuntimeError("Fixed system profile only") release = platform.freedesktop_os_release() if (release.get("ID"), release.get("VERSION_ID"), platform.machine()) != ("ubuntu", "24.04", "x86_64"): raise RuntimeError("Ubuntu 24.04 amd64 required") STATE.mkdir(mode=0o755, parents=True, exist_ok=True) for directory in (STATE.parent, STATE): info = directory.lstat() if directory.is_symlink() or info.st_uid != 0 or info.st_mode & 0o022: raise RuntimeError("Untrusted profile state") lock = os.open(STATE / "prepare.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) report = {"schema": "missioncore.node.device-preparation/v1", "model_id": "vesc.controller", "version": "0.7.4", "run_id": uuid.uuid4().hex, "started_at": time.time(), "monotonic_started": time.monotonic(), "state": "running", "steps": []} def publish(): atomic(STATE / "preparation.json", report) os.chmod(STATE / "preparation.json", 0o644) def run(name, label, args): step = {"id": name, "label": label, "state": "running"} report["steps"].append(step) publish() result = subprocess.run(args, capture_output=True, timeout=45, env={"PATH": "/usr/sbin:/usr/bin:/sbin:/bin", "LANG": "C.UTF-8"}) step["state"] = "complete" if result.returncode == 0 else "error" publish() if result.returncode: raise RuntimeError("Не завершён этап: " + label) try: import pwd try: pwd.getpwnam("mission-core-vesc") except KeyError: run("account", "Подготовка доступа", ["/usr/sbin/adduser", "--system", "--group", "--home", "/var/lib/mission-core-vesc", "--no-create-home", "--disabled-login", "mission-core-vesc"]) run("native", "Проверка VESC Tool", ["/usr/sbin/runuser", "-u", "mission-core-vesc", "--", "/usr/bin/python3", "-I", "/usr/lib/mission-core-vesc/native_check.py"]) source = Path("/usr/share/mission-core-node/profiles/vesc/70-mission-core-vesc.rules") rules = source.read_bytes() report["udev_sha256"] = hashlib.sha256(rules).hexdigest() target = Path("/etc/udev/rules.d/70-mission-core-vesc.rules") if target.is_symlink(): raise RuntimeError("Untrusted udev destination") target.write_bytes(rules) target.chmod(0o644) run("rules", "Настройка USB-доступа", ["/usr/bin/udevadm", "control", "--reload-rules"]) for i, device in enumerate(discover()): run("usb" + str(i), "Применение USB-доступа", ["/usr/bin/udevadm", "trigger", "--action=change", "/sys/class/tty/" + device.tty]) run("settle", "Проверка USB-доступа", ["/usr/bin/udevadm", "settle", "--timeout=10"]) run("enable", "Подготовка службы", ["/usr/bin/systemctl", "enable", "mission-core-vesc.service"]) run("runtime", "Запуск чтения контроллеров", ["/usr/bin/systemctl", "restart", "mission-core-vesc.service"]) report["state"] = "complete" except (OSError, RuntimeError, subprocess.SubprocessError) as error: report.update(state="error", message=str(error)[:300]) finally: report["duration_seconds"] = time.monotonic() - report["monotonic_started"] publish() os.close(lock) return report["state"] == "complete" if __name__ == "__main__": sys.exit(0 if prepare() else 1)