"""Fixed model job. No paths, packages, URLs or commands are accepted from clients.""" import hashlib import http.client import json import os import shutil import socket import subprocess import sys import tempfile import time import uuid import zipfile from pathlib import Path, PurePosixPath SHARE = Path("/usr/share/mission-core-node/realsense") ROOT = Path("/var/lib/mission-core-node-drivers") REPORT = ROOT / "preparation.json" STEPS = [ ("platform", "Проверка совместимости системы"), ("payload", "Проверка встроенного драйвера"), ("runtime", "Развёртывание драйвера"), ("access", "Настройка доступа к камере"), ("service", "Запуск службы камеры"), ] def publish(value): ROOT.mkdir(mode=0o755, exist_ok=True) if ROOT.is_symlink() or ROOT.stat().st_uid != 0 or ROOT.stat().st_mode & 0o022: raise RuntimeError("Небезопасный каталог драйверов") ROOT.chmod(0o755) handle, name = tempfile.mkstemp(prefix=".preparation-", dir=ROOT) tmp = Path(name) with os.fdopen(handle, "w") as f: os.fchmod(f.fileno(), 0o644) json.dump(value, f, ensure_ascii=False) f.flush() os.fsync(f.fileno()) tmp.replace(REPORT) def run(*args): result = subprocess.run(args, capture_output=True, timeout=90) if result.returncode: raise RuntimeError("Системный этап не завершён. Повторите подготовку устройства.") def safe_members(archive): for info in archive.infolist(): path = PurePosixPath(info.filename) if ( path.is_absolute() or ".." in path.parts or (info.external_attr >> 16) & 0o170000 == 0o120000 ): raise RuntimeError("Недопустимое содержимое драйверного пакета") if ".data" in info.filename or info.filename.endswith(".pth"): raise RuntimeError("Пакет требует неподдерживаемый способ установки") yield info def configure_imu_namespace(): # Isolated Python mode deliberately omits the script directory. This fixed, # root-owned module is the same allowlist used by the udev grant. import importlib.util spec = importlib.util.spec_from_file_location( "realsense_iio_access", Path(__file__).with_name("realsense_iio_access.py") ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) paths = [] for root in Path("/sys/bus/iio/devices").glob("iio:device*"): resolved = root.resolve() try: module.validate(str(resolved)[4:]) except ValueError: continue paths.extend(str(p) for p in module.allowed_attributes(resolved)) # No subtree write grant. Only concrete allowlisted IIO attribute files # belonging to detected D455s are admitted to the service mount namespace. if any(" " in p or "\n" in p or "%" in p for p in paths): raise RuntimeError("Неподдерживаемый путь IMU") content = ( "[Service]\nReadWritePaths=\n" + "".join("ReadWritePaths=-" + p + "\n" for p in sorted(paths)) ).encode() folder = Path("/etc/systemd/system/mission-core-realsense.service.d") folder.mkdir(exist_ok=True, mode=0o755) destination = folder / "70-imu-access.conf" fingerprint = ROOT / "imu-config.sha256" previous = fingerprint.read_text() if fingerprint.exists() else None if destination.is_symlink() or fingerprint.is_symlink(): raise RuntimeError("Конфликт настроек доступа IMU") if destination.exists(): old = destination.read_bytes() if old == content: return False if hashlib.sha256(old).hexdigest() != previous: raise RuntimeError("Настройки IMU изменены в системе. Чужой файл сохранён.") # Read-only preflight; every entry point shares the board's capture owner. sock = Path("/run/mission-core-sensors/driver.sock") if sock.exists(): client = http.client.HTTPConnection("driver", timeout=5) client.sock = socket.socket(socket.AF_UNIX) client.sock.settimeout(5) try: client.sock.connect(str(sock)) client.request("GET", "/prepare-safe") response = client.getresponse() if response.status != 200 or not json.loads(response.read(1024)).get("safe"): raise RuntimeError("Остановите захват всех камер перед подготовкой драйвера.") finally: client.close() destination.write_bytes(content) destination.chmod(0o644) fingerprint.write_text(hashlib.sha256(content).hexdigest()) fingerprint.chmod(0o644) return True def prepare(): manifest = json.loads((SHARE / "bundle.json").read_text()) revision = manifest["revision"] if not revision.isalnum(): raise RuntimeError("Некорректная версия драйвера") target = ROOT / revision if target.is_symlink() or (ROOT / "active.path").is_symlink(): raise RuntimeError("Конфликт установленного драйвера") imu_changed = False state = { "schema": "missioncore.node.device-preparation/v1", "model_id": "realsense.d455", "revision": revision, "run_id": str(uuid.uuid4()), "started_at": time.time(), "state": "running", "steps": [{"id": k, "label": v, "state": "pending"} for k, v in STEPS], } publish(state) try: for step in state["steps"]: step["state"] = "running" state["updated_at"] = time.time() publish(state) if step["id"] == "platform": release = dict( line.split("=", 1) for line in Path("/etc/os-release").read_text().splitlines() if "=" in line ) if ( (release.get("ID", "").strip('"'), release.get("VERSION_ID", "").strip('"')) != ("ubuntu", "24.04") or os.uname().machine != "x86_64" or sys.version_info[:2] != (3, 12) ): raise RuntimeError("Встроенный драйвер несовместим с этой системой") elif step["id"] == "payload": for entry in manifest["wheels"]: path = SHARE / entry["name"] if ( path.name != entry["name"] or path.is_symlink() or hashlib.sha256(path.read_bytes()).hexdigest() != entry["sha256"] ): raise RuntimeError( "Контрольная сумма драйвера не совпала. Переустановите пакет Node." ) elif step["id"] == "runtime": if not target.exists(): stage = ROOT / (revision + ".partial") if stage.exists(): shutil.rmtree(stage) stage.mkdir(mode=0o755) for entry in manifest["wheels"]: with zipfile.ZipFile(SHARE / entry["name"]) as archive: archive.extractall(stage, members=safe_members(archive)) for path in stage.rglob("*"): path.chmod(0o755 if path.is_dir() else 0o644) stage.rename(target) # Wheel RECORD content is checked again against the bundled wheel; # a previous successful report never substitutes for integrity. for entry in manifest["wheels"]: with zipfile.ZipFile(SHARE / entry["name"]) as archive: for info in safe_members(archive): if not info.is_dir() and ( target / info.filename ).read_bytes() != archive.read(info): raise RuntimeError( "Установленный драйвер изменён. " "Нужна переустановка пакета драйвера." ) (ROOT / "active.path").write_text(str(target)) (ROOT / "active.path").chmod(0o644) elif step["id"] == "access": imu_changed = configure_imu_namespace() source = SHARE / "70-mission-core-realsense.rules" dest = Path("/etc/udev/rules.d/70-mission-core-realsense.rules") if dest.is_symlink() or ( dest.exists() and dest.read_bytes() != source.read_bytes() and hashlib.sha256(dest.read_bytes()).hexdigest() != "782eba7935400e688a7eaea53fe50d358a046eb6b2c99187a787cc03b0301449" ): raise RuntimeError( "Правила доступа к камере изменены в системе. Чужая конфигурация сохранена." ) dest.write_bytes(source.read_bytes()) dest.chmod(0o644) run("/usr/bin/udevadm", "control", "--reload-rules") # Restrict trigger to the admitted product; no unrelated USB reset. run( "/usr/bin/udevadm", "trigger", "--action=change", "--subsystem-match=usb", "--attr-match=idVendor=8086", "--attr-match=idProduct=0b5c", ) run( "/usr/bin/udevadm", "trigger", "--action=change", "--subsystem-match=video4linux", ) run("/usr/bin/udevadm", "trigger", "--action=change", "--subsystem-match=hidraw") run("/usr/bin/udevadm", "trigger", "--action=change", "--subsystem-match=iio") run("/usr/bin/udevadm", "settle", "--timeout=10") elif step["id"] == "service": run("/usr/bin/systemctl", "enable", "mission-core-realsense.service") # The fixed job refuses running capture before changing its namespace. run("/usr/bin/systemctl", "daemon-reload") run( "/usr/bin/systemctl", "restart" if imu_changed else "start", "mission-core-realsense.service", ) run("/usr/bin/systemctl", "is-active", "--quiet", "mission-core-realsense.service") step["state"] = "complete" publish(state) state["state"] = "complete" except ( OSError, ValueError, RuntimeError, subprocess.SubprocessError, zipfile.BadZipFile, ) as error: step["state"] = "error" step["message"] = ( str(error)[:300] if isinstance(error, RuntimeError) else "Не удалось подготовить драйвер. Повторите действие." ) state["state"] = "error" for item in state["steps"]: if item["state"] == "pending": item["state"] = "blocked" state["updated_at"] = time.time() publish(state) return state["state"] == "complete" if __name__ == "__main__": os.umask(0o022) if os.geteuid() != 0 or len(sys.argv) != 1: sys.exit(1) sys.exit(0 if prepare() else 1)