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

272 lines
12 KiB
Python

"""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)