Files
NODEDC_MISSION_CORE/apps/node-agent/packaging/realsense_prepare.py
T

195 lines
8.2 KiB
Python

"""Fixed model job. No paths, packages, URLs or commands are accepted from clients."""
import hashlib
import json
import os
import shutil
import subprocess
import sys
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:
raise RuntimeError("Небезопасный каталог драйверов")
ROOT.chmod(0o755)
tmp = ROOT / ".preparation.tmp"
with tmp.open("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 prepare():
manifest = json.loads((SHARE / "bundle.json").read_text())
revision = manifest["revision"]
if not revision.isalnum():
raise RuntimeError("Некорректная версия драйвера")
target = ROOT / revision
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":
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()
):
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", "settle", "--timeout=10")
elif step["id"] == "service":
run("/usr/bin/systemctl", "enable", "mission-core-realsense.service")
# Never restart a running acquisition on repeated preparation.
run("/usr/bin/systemctl", "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)