feat(k1): stabilize LAB bridge and isolate onboard device integration
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
// Only WLAN discovery. No host association/profile modification authority.
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (subject.user === "mission-core-k1" && action.id === "org.freedesktop.NetworkManager.wifi.scan") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import sys
|
||||
from build_deb import build, VERSION, BRAND_SHA256
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DG_COMMIT = "999864e5b0a81555823cfa1ea6e8cf8a417c37f1"
|
||||
DG_COMMIT = "1bdfc6c24072d38cc1068086ea271c444f2524ad"
|
||||
|
||||
|
||||
def guideline_sources():
|
||||
@@ -32,6 +32,9 @@ def provenance():
|
||||
"base_commit": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip(),
|
||||
"design_guideline_commit": DG_COMMIT,
|
||||
"design_guideline_files": guideline_sources(),
|
||||
"shared_sensor_ui_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "packages/sensor-ui/src").rglob("*")) if p.is_file()},
|
||||
"k1_frontend_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "plugins/xgrids-k1/frontend/src").rglob("*")) if p.is_file()},
|
||||
"k1_runtime_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "src/k1link").rglob("*")) if p.is_file() and p.suffix in (".py", ".json")},
|
||||
"toolchain": json.loads((ROOT / "toolchain.json").read_text()), "files": files}
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import tarfile
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
VERSION = "0.6.11"
|
||||
VERSION = "0.7.1"
|
||||
BRAND_SHA256 = "8bfee8ca9f98e0db48d98aae3af4b32493b8593e18b064a0239d513d824182af"
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ Architecture: amd64
|
||||
Maintainer: NODE.DC local build <noreply@example.invalid>
|
||||
Section: admin
|
||||
Priority: optional
|
||||
Depends: adduser, systemd, python3, python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme
|
||||
Depends: adduser, systemd, python3, python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme, bluez, network-manager, iproute2, ffmpeg
|
||||
Description: Mission Core onboard computer configuration
|
||||
Local graphical setup, host inventory, SSH access and persistent node identity.
|
||||
""".encode()
|
||||
@@ -107,6 +107,29 @@ Description: Mission Core onboard computer configuration
|
||||
files.append(("usr/share/mission-core-node/realsense/" + item["name"], data, 0o644))
|
||||
for path in (ROOT / "sensors").glob("*.py"):
|
||||
files.append(("usr/lib/mission-core-node/sensors/" + path.name, path.read_bytes(), 0o644))
|
||||
for name in ("k1_prepare.py", "k1_bootstrap.py"):
|
||||
files.append(("usr/lib/mission-core-node/" + name, (p / name).read_bytes(), 0o644))
|
||||
files.append(("usr/lib/mission-core-node/install-k1-credential", (p / "install-k1-credential").read_bytes(), 0o755))
|
||||
files.append(("usr/lib/systemd/system/mission-core-k1.service", (p / "mission-core-k1.service").read_bytes(), 0o644))
|
||||
files.append(("usr/share/polkit-1/rules.d/50-mission-core-k1.rules", (p / "50-mission-core-k1.rules").read_bytes(), 0o644))
|
||||
k1_bundle = json.loads((p / "k1-bundle.json").read_text())
|
||||
files.append(("usr/share/mission-core-node/k1/bundle.json", (p / "k1-bundle.json").read_bytes(), 0o644))
|
||||
for item in k1_bundle["wheels"]:
|
||||
data = (ROOT / "build/k1-wheels" / item["name"]).read_bytes()
|
||||
if hashlib.sha256(data).hexdigest() != item["sha256"]:
|
||||
raise ValueError("K1 bundle hash mismatch")
|
||||
files.append(("usr/share/mission-core-node/k1/" + item["name"], data, 0o644))
|
||||
repository = ROOT.parents[1]
|
||||
# Reuse the admitted plugin runtime and the transport-neutral renderer.
|
||||
# No separate Core web service is started on the board.
|
||||
for path in (repository / "src/k1link").rglob("*"):
|
||||
if path.is_file() and path.suffix in (".py", ".json"):
|
||||
files.append(("usr/lib/mission-core-node/k1/src/k1link/" + str(path.relative_to(repository / "src/k1link")), path.read_bytes(), 0o644))
|
||||
for relative in ("plugins/xgrids-k1/profile_loader.py", "plugins/xgrids-k1/plugin.manifest.json",
|
||||
"config/observatory-equipment-models.json",
|
||||
"config/observatory-recorded-capture-profiles.json",
|
||||
"plugins/xgrids-k1/profiles/fw-3.0.2/local-network.v2.json"):
|
||||
files.append(("usr/lib/mission-core-node/k1/" + relative, (repository / relative).read_bytes(), 0o644))
|
||||
sdk = ROOT.parents[1] / "packages/plugin-sdk/python/missioncore_plugin_sdk"
|
||||
for path in sdk.rglob("*.py"):
|
||||
files.append(("usr/lib/mission-core-node/sdk/missioncore_plugin_sdk/" + str(path.relative_to(sdk)), path.read_bytes(), 0o644))
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
"""Engineering build input, never run on an operator board. Exact PyPI hashes only."""
|
||||
|
||||
import hashlib
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.request import urlopen
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
manifest = json.loads((root / "packaging/realsense-bundle.json").read_text())
|
||||
output = root / "build/realsense-wheels"
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--model", choices=("realsense", "k1"), default="realsense")
|
||||
model = parser.parse_args().model
|
||||
manifest = json.loads((root / f"packaging/{model}-bundle.json").read_text())
|
||||
output = root / f"build/{model}-wheels"
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
for item in manifest["wheels"]:
|
||||
target = output / item["name"]
|
||||
@@ -23,8 +28,24 @@ for item in manifest["wheels"]:
|
||||
)
|
||||
if not source["url"].startswith("https://files.pythonhosted.org/"):
|
||||
raise ValueError("Unexpected package origin")
|
||||
with urlopen(source["url"], timeout=120) as response:
|
||||
data = response.read(item["bytes"] + 1)
|
||||
partial = target.with_suffix(target.suffix + ".partial")
|
||||
for attempt in range(4):
|
||||
offset = partial.stat().st_size if partial.exists() else 0
|
||||
request = Request(source["url"], headers={"Range": f"bytes={offset}-"} if offset else {})
|
||||
try:
|
||||
with urlopen(request, timeout=60) as response:
|
||||
if offset and response.status != 206:
|
||||
raise RuntimeError("Package server did not honor resume range")
|
||||
with partial.open("ab" if offset else "wb") as stream:
|
||||
while chunk := response.read(1024 * 1024):
|
||||
stream.write(chunk)
|
||||
break
|
||||
except (OSError, TimeoutError):
|
||||
if attempt == 3:
|
||||
raise
|
||||
time.sleep(2)
|
||||
data = partial.read_bytes()
|
||||
if len(data) != item["bytes"] or hashlib.sha256(data).hexdigest() != item["sha256"]:
|
||||
raise ValueError("Driver checksum mismatch")
|
||||
target.write_bytes(data)
|
||||
partial.replace(target)
|
||||
print(json.dumps({"model": model, "wheel": item["name"], "bytes": len(data)}), flush=True)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/python3 -I
|
||||
"""Administrator-only import of the exact application key from protected stdin."""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
if os.geteuid() != 0 or len(sys.argv) != 1:
|
||||
raise SystemExit("Root stdin import required")
|
||||
secret = bytearray(sys.stdin.buffer.read(1025).strip())
|
||||
try:
|
||||
if len(secret) != 36 or any(v < 33 or v > 126 for v in secret):
|
||||
raise SystemExit("Credential does not match the reviewed K1 profile")
|
||||
root = Path("/etc/credstore.encrypted")
|
||||
root.mkdir(mode=0o700, exist_ok=True)
|
||||
if root.is_symlink() or root.stat().st_uid != 0 or root.stat().st_mode & 0o022:
|
||||
raise SystemExit("Unsafe credential store")
|
||||
path = root / "k1-application"
|
||||
if path.is_symlink() or path.exists():
|
||||
raise SystemExit("K1 credential already installed; explicit rotation required")
|
||||
with tempfile.TemporaryDirectory(prefix=".k1-", dir=root) as directory:
|
||||
staged = Path(directory) / "encrypted"
|
||||
completed = subprocess.run(
|
||||
["/usr/bin/systemd-creds", "encrypt", "--name=k1-application", "--with-key=host", "-", str(staged)],
|
||||
input=secret, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30,
|
||||
)
|
||||
if completed.returncode:
|
||||
raise SystemExit("K1 credential import failed")
|
||||
staged.chmod(0o600)
|
||||
with staged.open("rb") as stream:
|
||||
os.fsync(stream.fileno())
|
||||
# Atomic publication without overwriting a concurrently installed key.
|
||||
os.link(staged, path)
|
||||
finally:
|
||||
secret[:] = b"\0" * len(secret)
|
||||
@@ -0,0 +1,265 @@
|
||||
{
|
||||
"schema": "missioncore.node.driver-bundle/v1",
|
||||
"model_id": "xgrids.k1",
|
||||
"revision": "c7ed0bba39f757afdac176a8",
|
||||
"python": "3.12",
|
||||
"platform": "linux-amd64",
|
||||
"lock_sha256": "551c8ccdc44bc3724328dd1e316c81d20e63d2e4dcd97148377d6cacef479246",
|
||||
"wheels": [
|
||||
{
|
||||
"name": "aioice-0.10.2-py3-none-any.whl",
|
||||
"sha256": "14911c15ab12d096dd14d372ebb4aecbb7420b52c9b76fdfcf54375dec17fcbf",
|
||||
"bytes": 24875
|
||||
},
|
||||
{
|
||||
"name": "aiortc-1.14.0-py3-none-any.whl",
|
||||
"sha256": "4b244d7e482f4e1f67e685b3468269628eca1ec91fa5b329ab517738cfca086e",
|
||||
"bytes": 93183
|
||||
},
|
||||
{
|
||||
"name": "annotated_doc-0.0.4-py3-none-any.whl",
|
||||
"sha256": "571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320",
|
||||
"bytes": 5303
|
||||
},
|
||||
{
|
||||
"name": "annotated_types-0.7.0-py3-none-any.whl",
|
||||
"sha256": "1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53",
|
||||
"bytes": 13643
|
||||
},
|
||||
{
|
||||
"name": "anyio-4.14.2-py3-none-any.whl",
|
||||
"sha256": "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494",
|
||||
"bytes": 125813
|
||||
},
|
||||
{
|
||||
"name": "attrs-26.1.0-py3-none-any.whl",
|
||||
"sha256": "c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309",
|
||||
"bytes": 67548
|
||||
},
|
||||
{
|
||||
"name": "av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl",
|
||||
"sha256": "7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2",
|
||||
"bytes": 41174337
|
||||
},
|
||||
{
|
||||
"name": "bleak-3.0.2-py3-none-any.whl",
|
||||
"sha256": "39092feb9e83f1df5ad2f88e837723c7211c982ce9e9cda6235104bc2ebe0d0d",
|
||||
"bytes": 146490
|
||||
},
|
||||
{
|
||||
"name": "certifi-2026.7.22-py3-none-any.whl",
|
||||
"sha256": "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775",
|
||||
"bytes": 136983
|
||||
},
|
||||
{
|
||||
"name": "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
|
||||
"sha256": "c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf",
|
||||
"bytes": 221822
|
||||
},
|
||||
{
|
||||
"name": "click-8.4.2-py3-none-any.whl",
|
||||
"sha256": "e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76",
|
||||
"bytes": 119243
|
||||
},
|
||||
{
|
||||
"name": "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl",
|
||||
"sha256": "42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b",
|
||||
"bytes": 4459756
|
||||
},
|
||||
{
|
||||
"name": "dbus_fast-5.0.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
|
||||
"sha256": "4ffcf16034f71a801bd2108aeffb6337d104c9459e8b1a218d16a917c8a2d2e9",
|
||||
"bytes": 852687
|
||||
},
|
||||
{
|
||||
"name": "dnspython-2.8.0-py3-none-any.whl",
|
||||
"sha256": "01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af",
|
||||
"bytes": 331094
|
||||
},
|
||||
{
|
||||
"name": "fastapi-0.139.0-py3-none-any.whl",
|
||||
"sha256": "cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189",
|
||||
"bytes": 130339
|
||||
},
|
||||
{
|
||||
"name": "foxglove_sdk-0.25.3-cp310-abi3-manylinux_2_28_x86_64.whl",
|
||||
"sha256": "bcc894b88188d8169973cfbb1370300f671760adea9d6e9447e5a03b2289527d",
|
||||
"bytes": 19220466
|
||||
},
|
||||
{
|
||||
"name": "google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl",
|
||||
"sha256": "14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411",
|
||||
"bytes": 33364
|
||||
},
|
||||
{
|
||||
"name": "h11-0.16.0-py3-none-any.whl",
|
||||
"sha256": "63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86",
|
||||
"bytes": 37515
|
||||
},
|
||||
{
|
||||
"name": "httpcore-1.0.9-py3-none-any.whl",
|
||||
"sha256": "2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55",
|
||||
"bytes": 78784
|
||||
},
|
||||
{
|
||||
"name": "httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",
|
||||
"sha256": "b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2",
|
||||
"bytes": 523851
|
||||
},
|
||||
{
|
||||
"name": "httpx-0.28.1-py3-none-any.whl",
|
||||
"sha256": "d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad",
|
||||
"bytes": 73517
|
||||
},
|
||||
{
|
||||
"name": "idna-3.18-py3-none-any.whl",
|
||||
"sha256": "7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2",
|
||||
"bytes": 65455
|
||||
},
|
||||
{
|
||||
"name": "ifaddr-0.2.0-py3-none-any.whl",
|
||||
"sha256": "085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748",
|
||||
"bytes": 12314
|
||||
},
|
||||
{
|
||||
"name": "lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
|
||||
"sha256": "24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e",
|
||||
"bytes": 1368249
|
||||
},
|
||||
{
|
||||
"name": "markdown_it_py-4.2.0-py3-none-any.whl",
|
||||
"sha256": "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a",
|
||||
"bytes": 91687
|
||||
},
|
||||
{
|
||||
"name": "mdurl-0.1.2-py3-none-any.whl",
|
||||
"sha256": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
|
||||
"bytes": 9979
|
||||
},
|
||||
{
|
||||
"name": "numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
|
||||
"sha256": "59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca",
|
||||
"bytes": 16672469
|
||||
},
|
||||
{
|
||||
"name": "paho_mqtt-2.1.0-py3-none-any.whl",
|
||||
"sha256": "6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee",
|
||||
"bytes": 67219
|
||||
},
|
||||
{
|
||||
"name": "pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
|
||||
"sha256": "78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91",
|
||||
"bytes": 6940830
|
||||
},
|
||||
{
|
||||
"name": "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl",
|
||||
"sha256": "076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9",
|
||||
"bytes": 155560
|
||||
},
|
||||
{
|
||||
"name": "pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl",
|
||||
"sha256": "5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778",
|
||||
"bytes": 50088993
|
||||
},
|
||||
{
|
||||
"name": "pycparser-3.0-py3-none-any.whl",
|
||||
"sha256": "b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992",
|
||||
"bytes": 48172
|
||||
},
|
||||
{
|
||||
"name": "pydantic-2.13.4-py3-none-any.whl",
|
||||
"sha256": "45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba",
|
||||
"bytes": 472262
|
||||
},
|
||||
{
|
||||
"name": "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||
"sha256": "926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce",
|
||||
"bytes": 2094516
|
||||
},
|
||||
{
|
||||
"name": "pyee-14.0.0-py3-none-any.whl",
|
||||
"sha256": "3ac2d3229a9677f7de2c33d7f52fe25b638a46b19c413fea2edc8c6d0a644e4d",
|
||||
"bytes": 15553
|
||||
},
|
||||
{
|
||||
"name": "pygments-2.20.0-py3-none-any.whl",
|
||||
"sha256": "81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176",
|
||||
"bytes": 1231151
|
||||
},
|
||||
{
|
||||
"name": "pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl",
|
||||
"sha256": "293c9f2ac21a2bd689c477603a1aa235d85cf252160e6715f0101e42a43cbedc",
|
||||
"bytes": 2434534
|
||||
},
|
||||
{
|
||||
"name": "pyopenssl-26.2.0-py3-none-any.whl",
|
||||
"sha256": "4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70",
|
||||
"bytes": 55823
|
||||
},
|
||||
{
|
||||
"name": "python_dotenv-1.2.2-py3-none-any.whl",
|
||||
"sha256": "1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a",
|
||||
"bytes": 22101
|
||||
},
|
||||
{
|
||||
"name": "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
|
||||
"sha256": "ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc",
|
||||
"bytes": 807870
|
||||
},
|
||||
{
|
||||
"name": "rerun_sdk-0.36.3-cp310-abi3-manylinux_2_28_x86_64.whl",
|
||||
"sha256": "287059b7154bf3881f5b32035f5772d0556d55a0a894650fb74a2605fb39afbe",
|
||||
"bytes": 163018185
|
||||
},
|
||||
{
|
||||
"name": "rich-14.3.4-py3-none-any.whl",
|
||||
"sha256": "07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952",
|
||||
"bytes": 310480
|
||||
},
|
||||
{
|
||||
"name": "shellingham-1.5.4-py2.py3-none-any.whl",
|
||||
"sha256": "7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686",
|
||||
"bytes": 9755
|
||||
},
|
||||
{
|
||||
"name": "starlette-1.3.1-py3-none-any.whl",
|
||||
"sha256": "c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6",
|
||||
"bytes": 73632
|
||||
},
|
||||
{
|
||||
"name": "typer-0.26.8-py3-none-any.whl",
|
||||
"sha256": "3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c",
|
||||
"bytes": 122564
|
||||
},
|
||||
{
|
||||
"name": "typing_extensions-4.16.0-py3-none-any.whl",
|
||||
"sha256": "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8",
|
||||
"bytes": 45571
|
||||
},
|
||||
{
|
||||
"name": "typing_inspection-0.4.2-py3-none-any.whl",
|
||||
"sha256": "4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7",
|
||||
"bytes": 14611
|
||||
},
|
||||
{
|
||||
"name": "uvicorn-0.51.0-py3-none-any.whl",
|
||||
"sha256": "5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b",
|
||||
"bytes": 73219
|
||||
},
|
||||
{
|
||||
"name": "uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
|
||||
"sha256": "7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4",
|
||||
"bytes": 4426307
|
||||
},
|
||||
{
|
||||
"name": "watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||
"sha256": "e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5",
|
||||
"bytes": 456398
|
||||
},
|
||||
{
|
||||
"name": "websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl",
|
||||
"sha256": "35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a",
|
||||
"bytes": 187345
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Fixed root-owned import path; no user site, environment path or import hooks."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
root = Path("/var/lib/mission-core-k1-runtime")
|
||||
reference = root / "active.path"
|
||||
runtime = Path(reference.read_text().strip())
|
||||
if (
|
||||
reference.is_symlink()
|
||||
or runtime.is_symlink()
|
||||
or runtime.parent != root
|
||||
or not runtime.name.isalnum()
|
||||
or runtime.stat().st_uid != 0
|
||||
or runtime.stat().st_mode & 0o022
|
||||
):
|
||||
raise RuntimeError("Unsafe K1 runtime")
|
||||
sys.path[:0] = [
|
||||
str(runtime), str(runtime / "rerun_sdk"),
|
||||
"/usr/lib/mission-core-node/k1/src", "/usr/lib/mission-core-node/sdk",
|
||||
]
|
||||
os.environ["MISSIONCORE_DATA_DIR"] = "/var/lib/mission-core-k1"
|
||||
from k1link.device_plugins.xgrids_k1.node_bridge import main
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Install only the bundled, hash-pinned Ubuntu K1 runtime; no network I/O."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
SHARE = Path("/usr/share/mission-core-node/k1")
|
||||
ROOT = Path("/var/lib/mission-core-k1-runtime")
|
||||
|
||||
|
||||
def 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
|
||||
or ".data" in path.parts
|
||||
):
|
||||
raise RuntimeError("Unsafe K1 runtime archive")
|
||||
# Rerun's pinned wheel declares this one static package directory.
|
||||
# We do not execute .pth files; bootstrap adds the exact directory.
|
||||
if info.filename.endswith(".pth") and not (
|
||||
info.filename == "rerun_sdk.pth" and archive.read(info) == b"rerun_sdk\n"
|
||||
):
|
||||
raise RuntimeError("Unreviewed K1 Python path hook")
|
||||
# Distribution script/data relocation must be handled deliberately,
|
||||
# never interpreted as an install hook by the operator's Python.
|
||||
if any(part.endswith(".data") for part in path.parts):
|
||||
raise RuntimeError("K1 wheel requires unsupported relocation")
|
||||
yield info
|
||||
|
||||
|
||||
def prepare():
|
||||
if os.geteuid() != 0 or os.uname().machine != "x86_64" or sys.version_info[:2] != (3, 12):
|
||||
raise RuntimeError("K1 runtime requires privileged Ubuntu amd64 Python 3.12 installation")
|
||||
release = Path("/etc/os-release").read_text()
|
||||
if "ID=ubuntu" not in release or 'VERSION_ID="24.04"' not in release:
|
||||
raise RuntimeError("K1 runtime requires Ubuntu 24.04")
|
||||
manifest = json.loads((SHARE / "bundle.json").read_text())
|
||||
revision = manifest["revision"]
|
||||
if not revision.isalnum():
|
||||
raise RuntimeError("Invalid K1 runtime revision")
|
||||
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("Unsafe K1 runtime root")
|
||||
target = ROOT / revision
|
||||
if target.is_symlink() or (ROOT / "active.path").is_symlink():
|
||||
raise RuntimeError("Unsafe K1 runtime reference")
|
||||
for item in manifest["wheels"]:
|
||||
path = SHARE / item["name"]
|
||||
if (
|
||||
path.name != item["name"]
|
||||
or path.is_symlink()
|
||||
or hashlib.sha256(path.read_bytes()).hexdigest() != item["sha256"]
|
||||
):
|
||||
raise RuntimeError("K1 runtime checksum mismatch")
|
||||
if not target.exists():
|
||||
stage = Path(tempfile.mkdtemp(prefix=".k1-", dir=ROOT))
|
||||
try:
|
||||
for item in manifest["wheels"]:
|
||||
with zipfile.ZipFile(SHARE / item["name"]) as archive:
|
||||
archive.extractall(stage, members=members(archive))
|
||||
for path in stage.rglob("*"):
|
||||
path.chmod(0o755 if path.is_dir() else 0o644)
|
||||
stage.chmod(0o755)
|
||||
stage.rename(target)
|
||||
finally:
|
||||
if stage.exists():
|
||||
shutil.rmtree(stage)
|
||||
for item in manifest["wheels"]:
|
||||
with zipfile.ZipFile(SHARE / item["name"]) as archive:
|
||||
for info in members(archive):
|
||||
path = target / info.filename
|
||||
if path.is_symlink() or (
|
||||
not info.is_dir() and path.read_bytes() != archive.read(info)
|
||||
):
|
||||
raise RuntimeError("Installed K1 runtime differs from bundled wheel")
|
||||
fd, name = tempfile.mkstemp(prefix=".active-", dir=ROOT)
|
||||
with os.fdopen(fd, "w") as stream:
|
||||
os.fchmod(stream.fileno(), 0o644)
|
||||
stream.write(str(target))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(name, ROOT / "active.path")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
prepare()
|
||||
@@ -0,0 +1,38 @@
|
||||
[Unit]
|
||||
Description=Mission Core Node K1 Bridge and acquisition
|
||||
After=bluetooth.service NetworkManager.service
|
||||
Wants=bluetooth.service NetworkManager.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=mission-core-k1
|
||||
Group=mission-core-node
|
||||
SupplementaryGroups=bluetooth
|
||||
ExecStart=/usr/bin/python3 -I /usr/lib/mission-core-node/k1_bootstrap.py
|
||||
StateDirectory=mission-core-k1
|
||||
StateDirectoryMode=0700
|
||||
RuntimeDirectory=mission-core-k1
|
||||
RuntimeDirectoryMode=0750
|
||||
LoadCredentialEncrypted=k1-application
|
||||
UMask=0007
|
||||
Environment=OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
||||
CapabilityBoundingSet=
|
||||
LockPersonality=yes
|
||||
TasksMax=128
|
||||
MemoryMax=2G
|
||||
LimitNOFILE=2048
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -8,6 +8,10 @@ case "$1" in
|
||||
if ! getent passwd mission-core-sensors >/dev/null; then
|
||||
adduser --system --group --home /var/lib/mission-core-sensors --no-create-home --disabled-login mission-core-sensors
|
||||
fi
|
||||
if ! getent passwd mission-core-k1 >/dev/null; then
|
||||
adduser --system --home /var/lib/mission-core-k1 --no-create-home --disabled-login --ingroup mission-core-node mission-core-k1
|
||||
fi
|
||||
/usr/bin/python3 -I /usr/lib/mission-core-node/k1_prepare.py
|
||||
# Only bootstrap required to open the GUI. Operational configuration is a
|
||||
# versioned job started by «Настройка окружения → Сконфигурировать».
|
||||
if [ -d /run/systemd/system ]; then
|
||||
@@ -15,6 +19,8 @@ case "$1" in
|
||||
systemctl enable mission-core-node.service
|
||||
systemctl restart mission-core-node.service
|
||||
systemctl try-restart mission-core-realsense.service
|
||||
systemctl enable mission-core-k1.service
|
||||
systemctl restart mission-core-k1.service
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -8,6 +8,12 @@ if [ "$1" = install ] || [ "$1" = upgrade ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ -S /run/mission-core-k1/driver.sock ]; then
|
||||
if ! /usr/bin/python3 -I -c 'import http.client,json,socket; c=http.client.HTTPConnection("k1",timeout=5); c.sock=socket.socket(socket.AF_UNIX); c.sock.settimeout(5); c.sock.connect("/run/mission-core-k1/driver.sock"); c.request("GET","/prepare-safe"); r=c.getresponse(); assert r.status==200 and json.load(r).get("safe") is True'; then
|
||||
echo "Mission Core Node: завершите подключение или запись K1 перед обновлением." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
mc_node_device_job=$(systemctl show --property=ActiveState --value mission-core-node-realsense-prepare.service 2>/dev/null || true)
|
||||
case "$mc_node_device_job" in
|
||||
active|activating) echo "Mission Core Node: дождитесь завершения подготовки устройства." >&2; exit 1 ;;
|
||||
@@ -20,6 +26,11 @@ if [ "$1" = install ] || [ "$1" = upgrade ]; then
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
# End the admitted idle worker before dpkg replaces its Python modules.
|
||||
# New UI commands now fail unavailable instead of racing the package copy.
|
||||
if [ -f /usr/lib/systemd/system/mission-core-k1.service ]; then
|
||||
systemctl stop mission-core-k1.service
|
||||
fi
|
||||
fi
|
||||
. /etc/os-release
|
||||
if [ "${ID:-}" != ubuntu ] || [ "${VERSION_ID:-}" != 24.04 ]; then
|
||||
|
||||
@@ -7,6 +7,12 @@ if [ -d /run/systemd/system ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ -S /run/mission-core-k1/driver.sock ]; then
|
||||
if ! /usr/bin/python3 -I -c 'import http.client,json,socket; c=http.client.HTTPConnection("k1",timeout=5); c.sock=socket.socket(socket.AF_UNIX); c.sock.settimeout(5); c.sock.connect("/run/mission-core-k1/driver.sock"); c.request("GET","/prepare-safe"); r=c.getresponse(); assert r.status==200 and json.load(r).get("safe") is True'; then
|
||||
echo "Mission Core Node: завершите подключение или запись K1 перед обновлением." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
mc_node_device_job=$(systemctl show --property=ActiveState --value mission-core-node-realsense-prepare.service 2>/dev/null || true)
|
||||
case "$mc_node_device_job" in
|
||||
active|activating) echo "Mission Core Node: дождитесь завершения подготовки устройства." >&2; exit 1 ;;
|
||||
@@ -40,6 +46,8 @@ case "$1" in
|
||||
/usr/sbin/sshd -t
|
||||
systemctl try-reload-or-restart ssh.service
|
||||
fi
|
||||
systemctl stop mission-core-k1.service
|
||||
systemctl disable mission-core-k1.service || true
|
||||
systemctl stop mission-core-realsense.service
|
||||
systemctl disable mission-core-realsense.service || true
|
||||
systemctl stop mission-core-node.service
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Resolve Linux wheel inputs from the frozen monorepo lock, without downloading."""
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from packaging.markers import default_environment
|
||||
from packaging.requirements import Requirement
|
||||
from packaging.tags import compatible_tags, cpython_tags
|
||||
from packaging.utils import canonicalize_name, parse_wheel_filename
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPOSITORY = ROOT.parents[1]
|
||||
|
||||
|
||||
def resolve():
|
||||
requirements = ROOT / "build/k1-requirements.txt"
|
||||
requirements.parent.mkdir(exist_ok=True)
|
||||
subprocess.run(["uv", "export", "--frozen", "--extra", "node-device-media", "--no-dev",
|
||||
"--no-emit-project", "--no-emit-package", "missioncore-plugin-sdk", "--no-hashes",
|
||||
"--output-file", str(requirements)], cwd=REPOSITORY, check=True, stdout=subprocess.DEVNULL)
|
||||
environment = default_environment()
|
||||
environment.update(sys_platform="linux", platform_system="Linux", platform_machine="x86_64",
|
||||
python_version="3.12", python_full_version="3.12.3", implementation_name="cpython",
|
||||
platform_python_implementation="CPython")
|
||||
platforms = [f"manylinux_2_{n}_x86_64" for n in range(39, 16, -1)] + ["manylinux2014_x86_64", "linux_x86_64"]
|
||||
tags = list(cpython_tags((3, 12), platforms=platforms)) + list(compatible_tags((3, 12), interpreter="cp312", platforms=platforms))
|
||||
ranks = {tag: i for i, tag in enumerate(tags)}
|
||||
lock_data = (REPOSITORY / "uv.lock").read_bytes()
|
||||
lock = tomllib.loads(lock_data.decode())
|
||||
items = []
|
||||
for line in requirements.read_text().splitlines():
|
||||
if not line or line.lstrip().startswith("#"):
|
||||
continue
|
||||
requirement = Requirement(line)
|
||||
if requirement.marker and not requirement.marker.evaluate(environment):
|
||||
continue
|
||||
name = canonicalize_name(requirement.name)
|
||||
package = next(v for v in lock["package"] if canonicalize_name(v["name"]) == name and v["version"] in requirement.specifier)
|
||||
candidates = []
|
||||
for wheel in package.get("wheels", []):
|
||||
filename = wheel["url"].split("/")[-1]
|
||||
_, _, _, wheel_tags = parse_wheel_filename(filename)
|
||||
matches = [ranks[tag] for tag in wheel_tags if tag in ranks]
|
||||
if matches:
|
||||
candidates.append((min(matches), filename, wheel))
|
||||
if not candidates:
|
||||
raise RuntimeError("No reviewed Linux wheel: " + name)
|
||||
_, filename, wheel = min(candidates)
|
||||
items.append({"name": filename, "sha256": wheel["hash"].removeprefix("sha256:"), "bytes": wheel["size"]})
|
||||
revision = hashlib.sha256(json.dumps(items, sort_keys=True).encode()).hexdigest()[:24]
|
||||
manifest = {"schema": "missioncore.node.driver-bundle/v1", "model_id": "xgrids.k1", "revision": revision,
|
||||
"python": "3.12", "platform": "linux-amd64", "lock_sha256": hashlib.sha256(lock_data).hexdigest(), "wheels": items}
|
||||
(ROOT / "packaging/k1-bundle.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
print(json.dumps({"wheels": len(items), "bytes": sum(v["bytes"] for v in items), "revision": revision}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
resolve()
|
||||
Reference in New Issue
Block a user