feat(perception): integrate calibrated operator pipeline
Add calibrated K1 projection, recorded and near-live perception qualification, unified Rerun operator layers, bounded replay admission, audited viewer controls, worker experiments, and lab evidence.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# Mission Core CVAT D-only profile
|
||||
|
||||
This profile installs the annotation control plane in a dedicated WSL2
|
||||
distribution named `MissionCore-CVAT`. Its VHDX, Docker image store, CVAT
|
||||
source, persistent volumes, reports, and imported datasets live below
|
||||
`D:\NDC_MISSIONCORE`. It does not use the Docker Desktop image store that backs
|
||||
the Triton, Frigate, and Ollama containers.
|
||||
|
||||
Pinned inputs:
|
||||
|
||||
- Ubuntu 24.04.4 WSL AMD64 image, SHA-256
|
||||
`9b2f7730dc68227dd04a9f3e5eab86ad85caf556b8606ad94f1f29ff5c4fd3f5`
|
||||
- CVAT `v2.70.0`
|
||||
- D free-space floor: `360 GiB`
|
||||
- WSL VHD logical ceiling: `32 GB` (sparse allocation)
|
||||
|
||||
The official CVAT Compose topology is retained. The override only replaces its
|
||||
named volumes with explicit bind-backed directories inside the D-hosted WSL
|
||||
VHD. Images are pulled serially and the D free-space floor is checked before and
|
||||
after every image.
|
||||
|
||||
Run from the Windows host:
|
||||
|
||||
```powershell
|
||||
wsl -d MissionCore-CVAT -u root -- bash /mnt/d/NDC_MISSIONCORE/workspace/mission-core-compute/cvat/provision_cvat_wsl.sh
|
||||
wsl -d MissionCore-CVAT -u root -- bash /mnt/d/NDC_MISSIONCORE/workspace/mission-core-compute/cvat/ensure_cvat_admin.sh
|
||||
wsl -d MissionCore-CVAT -u root -- bash /mnt/d/NDC_MISSIONCORE/workspace/mission-core-compute/cvat/import_e2_workspace.sh
|
||||
```
|
||||
|
||||
After a Windows reboot, start the existing deployment without pulling images or
|
||||
re-provisioning it:
|
||||
|
||||
```powershell
|
||||
& D:\NDC_MISSIONCORE\workspace\mission-core-compute\cvat\Start-Cvat.ps1
|
||||
```
|
||||
|
||||
The launcher checks the `360 GiB` D-drive floor, starts the pinned Compose
|
||||
topology in the D-hosted WSL distribution, waits for the API, and keeps the WSL
|
||||
runtime alive. It writes startup logs only below
|
||||
`D:\NDC_MISSIONCORE\runtime\annotation\cvat\logs`.
|
||||
|
||||
From the Mission Core repository on the Mac, start CVAT if needed, discover the
|
||||
current WSL address, and open the SSH tunnel without a hard-coded IP:
|
||||
|
||||
```bash
|
||||
bash experiments/perception/worker/cvat/open_cvat_tunnel.sh
|
||||
```
|
||||
|
||||
Keep that terminal open and use `http://localhost:18080`. The same SSH session
|
||||
keeps the WSL runtime alive. Pass `--background` when detached runtime and
|
||||
tunnel sessions are preferred; rerun the helper after a Mac or Windows reboot.
|
||||
|
||||
The generated administrator password is stored only in
|
||||
`D:\NDC_MISSIONCORE\secrets\cvat\admin.env`; it is not printed by the script or
|
||||
committed to Git.
|
||||
|
||||
Traefik binds inside the dedicated WSL environment to `127.0.0.1:8080` and
|
||||
`127.0.0.1:8090`. It also publishes container port `8080` as WSL-internal port
|
||||
`18080` so the existing Windows SSH service can forward it without relying on
|
||||
Windows-to-WSL localhost forwarding. Remote review still uses the exact SSH
|
||||
host alias and a local forward; this profile does not add routes, Windows port
|
||||
proxies, DNS changes, or firewall rules.
|
||||
@@ -0,0 +1,58 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$TimeoutSeconds = 180
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Distro = 'MissionCore-CVAT'
|
||||
$Root = 'D:\NDC_MISSIONCORE'
|
||||
$RuntimeRoot = Join-Path $Root 'runtime\annotation\cvat'
|
||||
$LogRoot = Join-Path $RuntimeRoot 'logs'
|
||||
$LinuxStartScript = '/mnt/d/NDC_MISSIONCORE/workspace/mission-core-compute/cvat/start_cvat_runtime.sh'
|
||||
$FreeGiBFloor = 360
|
||||
|
||||
$drive = Get-PSDrive -Name D
|
||||
$freeGiB = [math]::Floor($drive.Free / 1GB)
|
||||
if ($freeGiB -lt $FreeGiBFloor) {
|
||||
throw "D: free-space floor crossed ($freeGiB GiB free; $FreeGiBFloor GiB required)"
|
||||
}
|
||||
|
||||
try {
|
||||
$about = Invoke-RestMethod -Uri 'http://localhost:8080/api/server/about' -TimeoutSec 3
|
||||
if ($about.version -eq '2.70.0') {
|
||||
Write-Output "CVAT already ready version=$($about.version) free_gib=$freeGiB"
|
||||
exit 0
|
||||
}
|
||||
} catch {
|
||||
# A stopped WSL distribution is the normal condition after a Windows reboot.
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $LogRoot -Force | Out-Null
|
||||
$timestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')
|
||||
$stdoutPath = Join-Path $LogRoot "start-$timestamp.stdout.log"
|
||||
$stderrPath = Join-Path $LogRoot "start-$timestamp.stderr.log"
|
||||
$arguments = @(
|
||||
'-d', $Distro,
|
||||
'-u', 'root',
|
||||
'--', 'bash', $LinuxStartScript, '--keepalive'
|
||||
)
|
||||
|
||||
Start-Process -FilePath 'wsl.exe' -ArgumentList $arguments -WindowStyle Hidden `
|
||||
-RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
do {
|
||||
Start-Sleep -Seconds 2
|
||||
try {
|
||||
$about = Invoke-RestMethod -Uri 'http://localhost:8080/api/server/about' -TimeoutSec 3
|
||||
if ($about.version -eq '2.70.0') {
|
||||
Write-Output "CVAT ready version=$($about.version) free_gib=$freeGiB"
|
||||
Write-Output "startup_log=$stdoutPath"
|
||||
exit 0
|
||||
}
|
||||
} catch {
|
||||
# Continue until the complete CVAT Compose topology is ready.
|
||||
}
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
|
||||
throw "CVAT did not become ready within $TimeoutSeconds seconds; inspect $stderrPath"
|
||||
@@ -0,0 +1,50 @@
|
||||
services:
|
||||
traefik:
|
||||
ports: !override
|
||||
- 127.0.0.1:8080:8080
|
||||
- 127.0.0.1:8090:8090
|
||||
- 0.0.0.0:18080:8080
|
||||
|
||||
volumes:
|
||||
cvat_db:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: /srv/mission-core-cvat/volumes/cvat_db
|
||||
cvat_data:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: /srv/mission-core-cvat/volumes/cvat_data
|
||||
cvat_keys:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: /srv/mission-core-cvat/volumes/cvat_keys
|
||||
cvat_logs:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: /srv/mission-core-cvat/volumes/cvat_logs
|
||||
cvat_inmem_db:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: /srv/mission-core-cvat/volumes/cvat_inmem_db
|
||||
cvat_events_db:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: /srv/mission-core-cvat/volumes/cvat_events_db
|
||||
cvat_cache_db:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: /srv/mission-core-cvat/volumes/cvat_cache_db
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
readonly SECRET_ROOT="/mnt/d/NDC_MISSIONCORE/secrets/cvat"
|
||||
readonly SECRET_FILE="${SECRET_ROOT}/admin.env"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "ensure_cvat_admin.sh must run as root" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
install -d -m 0700 "${SECRET_ROOT}"
|
||||
if [[ ! -f "${SECRET_FILE}" ]]; then
|
||||
umask 077
|
||||
password="$(openssl rand -hex 24)"
|
||||
{
|
||||
printf 'CVAT_ADMIN_USERNAME=missioncore\n'
|
||||
printf 'CVAT_ADMIN_EMAIL=missioncore@local.invalid\n'
|
||||
printf 'CVAT_ADMIN_PASSWORD=%s\n' "${password}"
|
||||
} >"${SECRET_FILE}"
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "${SECRET_FILE}"
|
||||
set +a
|
||||
|
||||
docker exec \
|
||||
-e "CVAT_ADMIN_USERNAME=${CVAT_ADMIN_USERNAME}" \
|
||||
-e "CVAT_ADMIN_EMAIL=${CVAT_ADMIN_EMAIL}" \
|
||||
-e "CVAT_ADMIN_PASSWORD=${CVAT_ADMIN_PASSWORD}" \
|
||||
cvat_server \
|
||||
python3 /home/django/manage.py shell -c \
|
||||
'import os; from django.contrib.auth import get_user_model; User = get_user_model(); user, _ = User.objects.get_or_create(username=os.environ["CVAT_ADMIN_USERNAME"]); user.email = os.environ["CVAT_ADMIN_EMAIL"]; user.is_staff = True; user.is_superuser = True; user.set_password(os.environ["CVAT_ADMIN_PASSWORD"]); user.save()'
|
||||
|
||||
printf 'admin_ready=true\nusername=%s\nsecret_file=%s\n' \
|
||||
"${CVAT_ADMIN_USERNAME}" "${SECRET_FILE}"
|
||||
@@ -0,0 +1,284 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from collections import Counter
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from cvat_sdk import make_client, models
|
||||
from cvat_sdk.api_client.exceptions import ServiceException
|
||||
from cvat_sdk.core.proxies.tasks import ResourceType
|
||||
|
||||
WORKSPACE_ID = (
|
||||
"annotation-workspace-"
|
||||
"9a950d1c37d56dc12cc285b13c5addd7795285879cbcb1fbb2d5811c3c69821a"
|
||||
)
|
||||
EVALUATION_PACK_ID = (
|
||||
"evaluation-pack-"
|
||||
"7a983bba75d46c7c260252cb2d461e1384dcb92cda9e164397e841e6ebb37789"
|
||||
)
|
||||
EXPECTED_FRAME_COUNT = 64
|
||||
EXPECTED_INSTANCE_COUNT = 775
|
||||
BACKGROUND_LABEL = {
|
||||
"name": "background",
|
||||
"color": "#000000",
|
||||
"attributes": [],
|
||||
}
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _label_specs(raw_labels: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
labels = [
|
||||
{
|
||||
"name": item["name"],
|
||||
"color": item["color"],
|
||||
"attributes": [],
|
||||
}
|
||||
for item in raw_labels
|
||||
]
|
||||
if not any(label["name"] == BACKGROUND_LABEL["name"] for label in labels):
|
||||
labels.insert(0, dict(BACKGROUND_LABEL))
|
||||
return labels
|
||||
|
||||
|
||||
def _annotation_counts(task: Any) -> dict[str, int]:
|
||||
annotations = task.get_annotations()
|
||||
return {
|
||||
"shape_count": len(annotations.shapes),
|
||||
"tag_count": len(annotations.tags),
|
||||
"track_count": len(annotations.tracks),
|
||||
}
|
||||
|
||||
|
||||
def _list_tasks_when_ready(client: Any, *, attempts: int = 60) -> list[Any]:
|
||||
for attempt in range(1, attempts + 1):
|
||||
try:
|
||||
return list(client.tasks.list())
|
||||
except ServiceException as error:
|
||||
if error.status not in {500, 502, 503} or attempt == attempts:
|
||||
raise
|
||||
time.sleep(2)
|
||||
raise AssertionError("unreachable")
|
||||
|
||||
|
||||
def _task_record(task: Any, *, disposition: str, expected_labels: list[str]) -> dict[str, Any]:
|
||||
task.fetch()
|
||||
task_labels = list(task.get_labels())
|
||||
actual_labels = sorted(label.name for label in task_labels)
|
||||
if task.size != EXPECTED_FRAME_COUNT:
|
||||
raise RuntimeError(
|
||||
f"Task {task.id} has {task.size} frames, expected {EXPECTED_FRAME_COUNT}"
|
||||
)
|
||||
if actual_labels != sorted(expected_labels):
|
||||
raise RuntimeError(
|
||||
f"Task {task.id} labels differ: actual={actual_labels!r} "
|
||||
f"expected={sorted(expected_labels)!r}"
|
||||
)
|
||||
annotation_counts = _annotation_counts(task)
|
||||
label_names_by_id = {label.id: label.name for label in task_labels}
|
||||
annotations = task.get_annotations()
|
||||
shape_counts_by_label = dict(
|
||||
sorted(
|
||||
Counter(
|
||||
label_names_by_id.get(shape.label_id, f"unknown:{shape.label_id}")
|
||||
for shape in annotations.shapes
|
||||
).items()
|
||||
)
|
||||
)
|
||||
return {
|
||||
"id": task.id,
|
||||
"name": task.name,
|
||||
"disposition": disposition,
|
||||
"frame_count": task.size,
|
||||
"label_names": actual_labels,
|
||||
**annotation_counts,
|
||||
"shape_counts_by_label": shape_counts_by_label,
|
||||
"url_path": f"/tasks/{task.id}",
|
||||
}
|
||||
|
||||
|
||||
def _ensure_task(
|
||||
client: Any,
|
||||
*,
|
||||
name: str,
|
||||
labels: list[dict[str, Any]],
|
||||
images_path: Path,
|
||||
annotation_path: Path,
|
||||
annotation_format: str,
|
||||
expected_shape_count: int | None,
|
||||
) -> dict[str, Any]:
|
||||
matches = [task for task in _list_tasks_when_ready(client) if task.name == name]
|
||||
if len(matches) > 1:
|
||||
raise RuntimeError(f"Multiple CVAT tasks have the reserved name {name!r}")
|
||||
|
||||
expected_labels = [label["name"] for label in labels]
|
||||
if matches:
|
||||
task = matches[0]
|
||||
task.fetch()
|
||||
actual_labels = sorted(label.name for label in task.get_labels())
|
||||
expected_without_background = sorted(
|
||||
label for label in expected_labels if label != BACKGROUND_LABEL["name"]
|
||||
)
|
||||
disposition = "reused"
|
||||
if (
|
||||
actual_labels == expected_without_background
|
||||
and BACKGROUND_LABEL["name"] in expected_labels
|
||||
):
|
||||
task.update(
|
||||
models.PatchedTaskWriteRequest(
|
||||
labels=[models.PatchedLabelRequest(**BACKGROUND_LABEL)]
|
||||
)
|
||||
)
|
||||
disposition = "reused-and-background-label-added"
|
||||
|
||||
record = _task_record(
|
||||
task,
|
||||
disposition=disposition,
|
||||
expected_labels=expected_labels,
|
||||
)
|
||||
if record["shape_count"] == 0:
|
||||
task.import_annotations(annotation_format, annotation_path)
|
||||
record = _task_record(
|
||||
task,
|
||||
disposition=f"{disposition}-and-annotations-imported",
|
||||
expected_labels=expected_labels,
|
||||
)
|
||||
if expected_shape_count is not None and record["shape_count"] != expected_shape_count:
|
||||
raise RuntimeError(
|
||||
f"Task {task.id} has {record['shape_count']} shapes, "
|
||||
f"expected {expected_shape_count}"
|
||||
)
|
||||
return record
|
||||
|
||||
task = client.tasks.create_from_data(
|
||||
spec=models.TaskWriteRequest(
|
||||
name=name,
|
||||
labels=labels,
|
||||
segment_size=EXPECTED_FRAME_COUNT,
|
||||
overlap=0,
|
||||
),
|
||||
resources=[images_path],
|
||||
resource_type=ResourceType.LOCAL,
|
||||
data_params={
|
||||
"image_quality": 100,
|
||||
"sorting_method": "lexicographical",
|
||||
"use_cache": False,
|
||||
},
|
||||
annotation_path=annotation_path,
|
||||
annotation_format=annotation_format,
|
||||
status_check_period=2,
|
||||
)
|
||||
record = _task_record(task, disposition="created", expected_labels=expected_labels)
|
||||
if expected_shape_count is not None and record["shape_count"] != expected_shape_count:
|
||||
raise RuntimeError(
|
||||
f"Task {task.id} has {record['shape_count']} shapes, "
|
||||
f"expected {expected_shape_count}"
|
||||
)
|
||||
return record
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--server", required=True)
|
||||
parser.add_argument("--username", required=True)
|
||||
parser.add_argument("--password-env", required=True)
|
||||
parser.add_argument("--workspace", type=Path, required=True)
|
||||
parser.add_argument("--report", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
password = os.environ.get(args.password_env)
|
||||
if not password:
|
||||
raise RuntimeError(f"Password environment variable {args.password_env!r} is empty")
|
||||
|
||||
workspace = args.workspace.resolve()
|
||||
if workspace.name != WORKSPACE_ID:
|
||||
raise RuntimeError(f"Unexpected annotation workspace: {workspace}")
|
||||
|
||||
manifest_path = workspace / "manifest.json"
|
||||
labels_path = workspace / "cvat" / "labels.json"
|
||||
images_path = workspace / "cvat" / "images.zip"
|
||||
instance_path = workspace / "cvat" / "instance-coco.zip"
|
||||
semantic_path = workspace / "cvat" / "semantic-mask.zip"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
labels = json.loads(labels_path.read_text(encoding="utf-8"))
|
||||
|
||||
if manifest["ground_truth"] is not False:
|
||||
raise RuntimeError("LAB E2 import must remain an unreviewed model draft")
|
||||
if manifest["identity"]["frame_count"] != EXPECTED_FRAME_COUNT:
|
||||
raise RuntimeError("Unexpected LAB E2 frame count")
|
||||
if manifest["identity"]["draft_instance_count"] != EXPECTED_INSTANCE_COUNT:
|
||||
raise RuntimeError("Unexpected LAB E2 instance count")
|
||||
|
||||
task_specs = [
|
||||
{
|
||||
"name": "LAB E2 | K1 | instance prelabels | pack 7a983bba",
|
||||
"labels": _label_specs(labels["instance_task"]),
|
||||
"annotation_path": instance_path,
|
||||
"annotation_format": "COCO 1.0",
|
||||
"expected_shape_count": EXPECTED_INSTANCE_COUNT,
|
||||
},
|
||||
{
|
||||
"name": "LAB E2 | K1 | dense semantic prelabels | pack 7a983bba",
|
||||
"labels": _label_specs(labels["semantic_task"]),
|
||||
"annotation_path": semantic_path,
|
||||
"annotation_format": "Segmentation mask 1.1",
|
||||
"expected_shape_count": None,
|
||||
},
|
||||
]
|
||||
|
||||
with make_client(args.server, credentials=(args.username, password)) as client:
|
||||
client.check_server_version(fail_if_unsupported=True)
|
||||
task_records = [
|
||||
_ensure_task(
|
||||
client,
|
||||
name=task_spec["name"],
|
||||
labels=task_spec["labels"],
|
||||
images_path=images_path,
|
||||
annotation_path=task_spec["annotation_path"],
|
||||
annotation_format=task_spec["annotation_format"],
|
||||
expected_shape_count=task_spec["expected_shape_count"],
|
||||
)
|
||||
for task_spec in task_specs
|
||||
]
|
||||
|
||||
report = {
|
||||
"schema_version": "missioncore.lab-e2-cvat-import/v1",
|
||||
"created_at_utc": datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"server": args.server,
|
||||
"cvat_version": "v2.70.0",
|
||||
"workspace_id": WORKSPACE_ID,
|
||||
"evaluation_pack_id": EVALUATION_PACK_ID,
|
||||
"workspace_manifest_sha256": _sha256(manifest_path),
|
||||
"ground_truth": False,
|
||||
"inputs": {
|
||||
"images_zip_sha256": _sha256(images_path),
|
||||
"instance_coco_zip_sha256": _sha256(instance_path),
|
||||
"semantic_mask_zip_sha256": _sha256(semantic_path),
|
||||
},
|
||||
"tasks": task_records,
|
||||
"next_gate": (
|
||||
"two-pass human review and reviewed export; "
|
||||
"do not treat drafts as accuracy evidence"
|
||||
),
|
||||
}
|
||||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.report.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
readonly FREE_GIB_FLOOR="${FREE_GIB_FLOOR:-360}"
|
||||
readonly WINDOWS_ROOT="/mnt/d/NDC_MISSIONCORE"
|
||||
readonly COMPUTE_ROOT="${WINDOWS_ROOT}/workspace/mission-core-compute"
|
||||
readonly TOOL_ROOT="/srv/mission-core-cvat/tools/e2-import"
|
||||
readonly SECRET_FILE="${WINDOWS_ROOT}/secrets/cvat/admin.env"
|
||||
readonly WORKSPACE_ROOT="${WINDOWS_ROOT}/runtime/annotation/imports/LAB-E2/annotation-workspace-9a950d1c37d56dc12cc285b13c5addd7795285879cbcb1fbb2d5811c3c69821a"
|
||||
readonly REPORT_ROOT="${WINDOWS_ROOT}/runtime/annotation/cvat/reports"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "import_e2_workspace.sh must run as root" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
free_gib() {
|
||||
df -BG --output=avail "${WINDOWS_ROOT}" | tail -n 1 | tr -dc '0-9'
|
||||
}
|
||||
|
||||
guard_disk() {
|
||||
local stage="$1"
|
||||
local free
|
||||
free="$(free_gib)"
|
||||
printf 'disk_guard stage=%s free_gib=%s floor_gib=%s\n' \
|
||||
"${stage}" "${free}" "${FREE_GIB_FLOOR}"
|
||||
if (( free < FREE_GIB_FLOOR )); then
|
||||
echo "D: free-space floor crossed; refusing further writes" >&2
|
||||
exit 3
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_cvat() {
|
||||
local attempt http_code
|
||||
for attempt in $(seq 1 60); do
|
||||
http_code="$(
|
||||
curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||
"http://localhost:8080/api/server/about" || true
|
||||
)"
|
||||
if [[ "${http_code}" == "200" ]]; then
|
||||
http_code="$(
|
||||
curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||
"http://localhost:8080/api/tasks" || true
|
||||
)"
|
||||
if [[ "${http_code}" == "200" || "${http_code}" == "401" || "${http_code}" == "403" ]]; then
|
||||
printf 'cvat_ready attempt=%s tasks_http=%s\n' "${attempt}" "${http_code}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "CVAT API did not become ready within 120 seconds" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
test -f "${SECRET_FILE}"
|
||||
test -f "${WORKSPACE_ROOT}/manifest.json"
|
||||
test -f "${COMPUTE_ROOT}/cvat/import_e2_workspace.py"
|
||||
guard_disk preflight
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y python3-venv
|
||||
install -d -m 0750 "${TOOL_ROOT}" "${REPORT_ROOT}"
|
||||
|
||||
if [[ ! -x "${TOOL_ROOT}/venv/bin/python" ]]; then
|
||||
python3 -m venv "${TOOL_ROOT}/venv"
|
||||
"${TOOL_ROOT}/venv/bin/pip" install --disable-pip-version-check \
|
||||
"cvat-sdk==2.70.0"
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "${SECRET_FILE}"
|
||||
set +a
|
||||
|
||||
wait_for_cvat
|
||||
report="${REPORT_ROOT}/lab-e2-cvat-import-$(date -u +%Y%m%dT%H%M%SZ).json"
|
||||
"${TOOL_ROOT}/venv/bin/python" "${COMPUTE_ROOT}/cvat/import_e2_workspace.py" \
|
||||
--server "http://localhost:8080" \
|
||||
--username "${CVAT_ADMIN_USERNAME}" \
|
||||
--password-env CVAT_ADMIN_PASSWORD \
|
||||
--workspace "${WORKSPACE_ROOT}" \
|
||||
--report "${report}"
|
||||
|
||||
guard_disk imported
|
||||
printf 'import_report=%s\n' "${report}"
|
||||
@@ -0,0 +1,179 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
from cvat_sdk import make_client
|
||||
from import_e2_workspace import _ensure_task, _label_specs, _sha256
|
||||
|
||||
WORKSPACE_SCHEMA = "missioncore.lab-e3-cvat-review-workspace/v1"
|
||||
IDENTITY_SCHEMA = "missioncore.lab-e3-cvat-review-identity/v1"
|
||||
EXPECTED_PACK_ID = (
|
||||
"evaluation-pack-"
|
||||
"7a983bba75d46c7c260252cb2d461e1384dcb92cda9e164397e841e6ebb37789"
|
||||
)
|
||||
EXPECTED_RESULT_ID = (
|
||||
"e3-segmentation-"
|
||||
"01bd497c44c2b940add145ec784d3418010327bce0baddd4420b0925317e8a16"
|
||||
)
|
||||
EXPECTED_FRAMES = 64
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _valid_sha256(value: object) -> bool:
|
||||
return (
|
||||
isinstance(value, str)
|
||||
and len(value) == 64
|
||||
and all(character in "0123456789abcdef" for character in value)
|
||||
)
|
||||
|
||||
|
||||
def _read_object(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeError(f"JSON root is not an object: {path}")
|
||||
return value
|
||||
|
||||
|
||||
def _safe_artifact(root: Path, encoded: object) -> Path:
|
||||
if not isinstance(encoded, str):
|
||||
raise RuntimeError("artifact path is not a string")
|
||||
relative = PurePosixPath(encoded)
|
||||
if relative.is_absolute() or ".." in relative.parts or not relative.parts:
|
||||
raise RuntimeError("artifact path is unsafe")
|
||||
path = root.joinpath(*relative.parts).resolve(strict=True)
|
||||
if not path.is_file() or not path.is_relative_to(root):
|
||||
raise RuntimeError("artifact path escaped its root")
|
||||
return path
|
||||
|
||||
|
||||
def _workspace(root: Path, images_zip: Path) -> tuple[dict[str, Any], dict[str, Path]]:
|
||||
manifest_path = root / "manifest.json"
|
||||
manifest = _read_object(manifest_path)
|
||||
identity = manifest.get("identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != WORKSPACE_SCHEMA
|
||||
or manifest.get("ground_truth") is not False
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("schema_version") != IDENTITY_SCHEMA
|
||||
or not _valid_sha256(identity_sha256)
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or manifest.get("workspace_id") != f"e3-cvat-review-{identity_sha256}"
|
||||
or root.name != manifest.get("workspace_id")
|
||||
or identity.get("evaluation_pack_id") != EXPECTED_PACK_ID
|
||||
or identity.get("e3_result_id") != EXPECTED_RESULT_ID
|
||||
or identity.get("frame_count") != EXPECTED_FRAMES
|
||||
or _sha256(images_zip) != identity.get("images_zip_sha256")
|
||||
):
|
||||
raise RuntimeError("LAB E3 CVAT review workspace is incompatible")
|
||||
artifacts: dict[str, Path] = {}
|
||||
descriptors = manifest.get("artifacts")
|
||||
if not isinstance(descriptors, list):
|
||||
raise RuntimeError("LAB E3 CVAT artifact list is invalid")
|
||||
for descriptor in descriptors:
|
||||
if not isinstance(descriptor, dict):
|
||||
raise RuntimeError("LAB E3 CVAT artifact descriptor is invalid")
|
||||
path = _safe_artifact(root, descriptor.get("path"))
|
||||
if (
|
||||
path.stat().st_size != descriptor.get("bytes")
|
||||
or not _valid_sha256(descriptor.get("sha256"))
|
||||
or _sha256(path) != descriptor["sha256"]
|
||||
):
|
||||
raise RuntimeError(f"LAB E3 CVAT artifact changed: {path.name}")
|
||||
artifacts[path.name] = path
|
||||
if set(artifacts) != {
|
||||
"labels.json",
|
||||
"control-fisheye-mask.zip",
|
||||
"challenger-kb4-cubemap5.zip",
|
||||
}:
|
||||
raise RuntimeError("LAB E3 CVAT artifact set changed")
|
||||
return manifest, artifacts
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--server", required=True)
|
||||
parser.add_argument("--username", required=True)
|
||||
parser.add_argument("--password-env", required=True)
|
||||
parser.add_argument("--workspace", type=Path, required=True)
|
||||
parser.add_argument("--images-zip", type=Path, required=True)
|
||||
parser.add_argument("--report", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
password = os.environ.get(args.password_env)
|
||||
if not password:
|
||||
raise RuntimeError(f"Password environment variable {args.password_env!r} is empty")
|
||||
workspace = args.workspace.resolve(strict=True)
|
||||
images_zip = args.images_zip.resolve(strict=True)
|
||||
manifest, artifacts = _workspace(workspace, images_zip)
|
||||
labels_document = _read_object(artifacts["labels.json"])
|
||||
semantic_labels = labels_document.get("semantic_task")
|
||||
if not isinstance(semantic_labels, list):
|
||||
raise RuntimeError("semantic labels are absent")
|
||||
labels = _label_specs(semantic_labels)
|
||||
task_specs = (
|
||||
{
|
||||
"name": "LAB E3 | K1 | EoMT fisheye control | pack 7a983bba",
|
||||
"annotation": artifacts["control-fisheye-mask.zip"],
|
||||
"role": "control",
|
||||
},
|
||||
{
|
||||
"name": "LAB E3 | K1 | EoMT KB4 cubemap5 challenger | pack 7a983bba",
|
||||
"annotation": artifacts["challenger-kb4-cubemap5.zip"],
|
||||
"role": "challenger",
|
||||
},
|
||||
)
|
||||
with make_client(args.server, credentials=(args.username, password)) as client:
|
||||
client.check_server_version(fail_if_unsupported=True)
|
||||
tasks = []
|
||||
for spec in task_specs:
|
||||
record = _ensure_task(
|
||||
client,
|
||||
name=spec["name"],
|
||||
labels=labels,
|
||||
images_path=images_zip,
|
||||
annotation_path=spec["annotation"],
|
||||
annotation_format="Segmentation mask 1.1",
|
||||
expected_shape_count=None,
|
||||
)
|
||||
record["role"] = spec["role"]
|
||||
tasks.append(record)
|
||||
|
||||
report = {
|
||||
"schema_version": "missioncore.lab-e3-cvat-import/v1",
|
||||
"created_at_utc": datetime.now(UTC)
|
||||
.isoformat(timespec="milliseconds")
|
||||
.replace("+00:00", "Z"),
|
||||
"server": args.server,
|
||||
"cvat_version": "v2.70.0",
|
||||
"workspace_id": manifest["workspace_id"],
|
||||
"workspace_manifest_sha256": _sha256(workspace / "manifest.json"),
|
||||
"evaluation_pack_id": EXPECTED_PACK_ID,
|
||||
"e3_result_id": EXPECTED_RESULT_ID,
|
||||
"ground_truth": False,
|
||||
"priority_image_ids": manifest["identity"]["priority_image_ids"],
|
||||
"tasks": tasks,
|
||||
"next_gate": "two-pass human review and reviewed export",
|
||||
}
|
||||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.report.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
readonly FREE_GIB_FLOOR="${FREE_GIB_FLOOR:-360}"
|
||||
readonly WINDOWS_ROOT="/mnt/d/NDC_MISSIONCORE"
|
||||
readonly COMPUTE_ROOT="${WINDOWS_ROOT}/workspace/mission-core-compute"
|
||||
readonly TOOL_ROOT="/srv/mission-core-cvat/tools/e2-import"
|
||||
readonly SECRET_FILE="${WINDOWS_ROOT}/secrets/cvat/admin.env"
|
||||
readonly WORKSPACE_ID="e3-cvat-review-ca599521e345446ca8e9af5a9013062099278e7317f83ff89740c6b092ddc52f"
|
||||
readonly WORKSPACE_ROOT="${WINDOWS_ROOT}/runtime/annotation/imports/LAB-E3/${WORKSPACE_ID}"
|
||||
readonly E2_WORKSPACE_ID="annotation-workspace-9a950d1c37d56dc12cc285b13c5addd7795285879cbcb1fbb2d5811c3c69821a"
|
||||
readonly IMAGES_ZIP="${WINDOWS_ROOT}/runtime/annotation/imports/LAB-E2/${E2_WORKSPACE_ID}/cvat/images.zip"
|
||||
readonly REPORT_ROOT="${WINDOWS_ROOT}/runtime/annotation/cvat/reports"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "import_e3_review.sh must run as root" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
free_gib() {
|
||||
df -BG --output=avail "${WINDOWS_ROOT}" | tail -n 1 | tr -dc '0-9'
|
||||
}
|
||||
|
||||
guard_disk() {
|
||||
local stage="$1"
|
||||
local free
|
||||
free="$(free_gib)"
|
||||
printf 'disk_guard stage=%s free_gib=%s floor_gib=%s\n' \
|
||||
"${stage}" "${free}" "${FREE_GIB_FLOOR}"
|
||||
if (( free < FREE_GIB_FLOOR )); then
|
||||
echo "D: free-space floor crossed; refusing further writes" >&2
|
||||
exit 3
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_cvat() {
|
||||
local attempt http_code
|
||||
for attempt in $(seq 1 60); do
|
||||
http_code="$(
|
||||
curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||
"http://localhost:8080/api/server/about" || true
|
||||
)"
|
||||
if [[ "${http_code}" == "200" ]]; then
|
||||
printf 'cvat_ready attempt=%s\n' "${attempt}"
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "CVAT API did not become ready within 120 seconds" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
test -x "${TOOL_ROOT}/venv/bin/python"
|
||||
test -f "${SECRET_FILE}"
|
||||
test -f "${WORKSPACE_ROOT}/manifest.json"
|
||||
test -f "${IMAGES_ZIP}"
|
||||
test -f "${COMPUTE_ROOT}/cvat/import_e2_workspace.py"
|
||||
test -f "${COMPUTE_ROOT}/cvat/import_e3_review.py"
|
||||
guard_disk preflight
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "${SECRET_FILE}"
|
||||
set +a
|
||||
|
||||
wait_for_cvat
|
||||
report="${REPORT_ROOT}/lab-e3-cvat-import-$(date -u +%Y%m%dT%H%M%SZ).json"
|
||||
"${TOOL_ROOT}/venv/bin/python" "${COMPUTE_ROOT}/cvat/import_e3_review.py" \
|
||||
--server "http://localhost:8080" \
|
||||
--username "${CVAT_ADMIN_USERNAME}" \
|
||||
--password-env CVAT_ADMIN_PASSWORD \
|
||||
--workspace "${WORKSPACE_ROOT}" \
|
||||
--images-zip "${IMAGES_ZIP}" \
|
||||
--report "${report}"
|
||||
|
||||
guard_disk imported
|
||||
printf 'import_report=%s\n' "${report}"
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly SSH_ALIAS="mission-gpu"
|
||||
readonly LOCAL_PORT="${CVAT_LOCAL_PORT:-18080}"
|
||||
readonly REMOTE_WSL_PORT="18080"
|
||||
readonly LINUX_START_SCRIPT='/mnt/d/NDC_MISSIONCORE/workspace/mission-core-compute/cvat/start_cvat_runtime.sh'
|
||||
|
||||
if curl --max-time 2 --fail --silent \
|
||||
"http://localhost:${LOCAL_PORT}/api/server/about" >/dev/null 2>&1; then
|
||||
printf 'cvat_tunnel_ready url=http://localhost:%s\n' "${LOCAL_PORT}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v lsof >/dev/null \
|
||||
&& lsof -nP -iTCP:"${LOCAL_PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
|
||||
echo "Local port ${LOCAL_PORT} is already occupied by a non-responsive process" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
runtime_command="wsl.exe -d MissionCore-CVAT -u root -- bash ${LINUX_START_SCRIPT} --keepalive"
|
||||
background=false
|
||||
if [[ "${1:-}" == "--background" ]]; then
|
||||
background=true
|
||||
ssh -f "${SSH_ALIAS}" "${runtime_command}"
|
||||
else
|
||||
ssh "${SSH_ALIAS}" "${runtime_command}" &
|
||||
runtime_ssh_pid=$!
|
||||
cleanup() {
|
||||
if [[ -n "${tunnel_ssh_pid:-}" ]]; then
|
||||
kill "${tunnel_ssh_pid}" 2>/dev/null || true
|
||||
fi
|
||||
kill "${runtime_ssh_pid}" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
fi
|
||||
|
||||
for attempt in $(seq 1 30); do
|
||||
cvat_ip="$(
|
||||
ssh "${SSH_ALIAS}" \
|
||||
"wsl.exe -d MissionCore-CVAT -u root -- hostname -I" \
|
||||
| tr -d '\r' | awk '{print $1}'
|
||||
)"
|
||||
if [[ "${cvat_ip}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [[ ! "${cvat_ip:-}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "MissionCore-CVAT did not return a valid WSL address" >&2
|
||||
if [[ "${background}" == false ]]; then
|
||||
kill "${runtime_ssh_pid}" 2>/dev/null || true
|
||||
fi
|
||||
exit 3
|
||||
fi
|
||||
|
||||
forward=(
|
||||
-N
|
||||
-L "${LOCAL_PORT}:${cvat_ip}:${REMOTE_WSL_PORT}"
|
||||
-o ExitOnForwardFailure=yes
|
||||
"${SSH_ALIAS}"
|
||||
)
|
||||
|
||||
if [[ "${background}" == true ]]; then
|
||||
ssh -f "${forward[@]}"
|
||||
else
|
||||
ssh "${forward[@]}" &
|
||||
tunnel_ssh_pid=$!
|
||||
fi
|
||||
|
||||
for attempt in $(seq 1 60); do
|
||||
if curl --max-time 2 --fail --silent \
|
||||
"http://localhost:${LOCAL_PORT}/api/server/about" >/dev/null 2>&1; then
|
||||
printf 'cvat_tunnel_ready url=http://localhost:%s wsl_ip=%s\n' \
|
||||
"${LOCAL_PORT}" "${cvat_ip}"
|
||||
if [[ "${background}" == true ]]; then
|
||||
exit 0
|
||||
fi
|
||||
wait "${tunnel_ssh_pid}"
|
||||
tunnel_status=$?
|
||||
kill "${runtime_ssh_pid}" 2>/dev/null || true
|
||||
exit "${tunnel_status}"
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "CVAT tunnel did not become ready within 120 seconds" >&2
|
||||
if [[ "${background}" == false ]]; then
|
||||
kill "${tunnel_ssh_pid}" "${runtime_ssh_pid}" 2>/dev/null || true
|
||||
fi
|
||||
exit 4
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Do not inherit Windows executables into this D-only runtime. In particular,
|
||||
# Docker Desktop's docker.exe belongs to a different image store.
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
readonly CVAT_TAG="${CVAT_TAG:-v2.70.0}"
|
||||
readonly FREE_GIB_FLOOR="${FREE_GIB_FLOOR:-360}"
|
||||
readonly WINDOWS_ROOT="/mnt/d/NDC_MISSIONCORE"
|
||||
readonly COMPUTE_ROOT="${WINDOWS_ROOT}/workspace/mission-core-compute"
|
||||
readonly RUNTIME_ROOT="${WINDOWS_ROOT}/runtime/annotation/cvat"
|
||||
readonly CVAT_ROOT="${RUNTIME_ROOT}/source/cvat-${CVAT_TAG}"
|
||||
readonly STATE_ROOT="/srv/mission-core-cvat"
|
||||
readonly OVERRIDE_FILE="${COMPUTE_ROOT}/cvat/docker-compose.override.yml"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "provision_cvat_wsl.sh must run as root" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
free_gib() {
|
||||
df -BG --output=avail "${WINDOWS_ROOT}" | tail -n 1 | tr -dc '0-9'
|
||||
}
|
||||
|
||||
guard_disk() {
|
||||
local stage="$1"
|
||||
local free
|
||||
free="$(free_gib)"
|
||||
printf 'disk_guard stage=%s free_gib=%s floor_gib=%s\n' \
|
||||
"${stage}" "${free}" "${FREE_GIB_FLOOR}"
|
||||
if (( free < FREE_GIB_FLOOR )); then
|
||||
echo "D: free-space floor crossed; refusing further writes" >&2
|
||||
exit 3
|
||||
fi
|
||||
}
|
||||
|
||||
guard_disk preflight
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl git
|
||||
|
||||
if ! dpkg-query -W -f='${Status}' docker-ce 2>/dev/null \
|
||||
| grep -qx 'install ok installed'; then
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||
-o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
arch="$(dpkg --print-architecture)"
|
||||
printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu %s stable\n' \
|
||||
"${arch}" "${VERSION_CODENAME}" >/etc/apt/sources.list.d/docker.list
|
||||
apt-get update
|
||||
apt-get install -y \
|
||||
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
fi
|
||||
|
||||
systemctl enable --now docker
|
||||
docker version
|
||||
docker compose version
|
||||
guard_disk docker-engine
|
||||
|
||||
install -d -m 0750 \
|
||||
"${STATE_ROOT}/volumes/cvat_db" \
|
||||
"${STATE_ROOT}/volumes/cvat_data" \
|
||||
"${STATE_ROOT}/volumes/cvat_keys" \
|
||||
"${STATE_ROOT}/volumes/cvat_logs" \
|
||||
"${STATE_ROOT}/volumes/cvat_inmem_db" \
|
||||
"${STATE_ROOT}/volumes/cvat_events_db" \
|
||||
"${STATE_ROOT}/volumes/cvat_cache_db" \
|
||||
"${RUNTIME_ROOT}/source" \
|
||||
"${RUNTIME_ROOT}/reports"
|
||||
|
||||
if [[ ! -d "${CVAT_ROOT}/.git" ]]; then
|
||||
git clone --depth 1 --branch "${CVAT_TAG}" \
|
||||
https://github.com/cvat-ai/cvat.git "${CVAT_ROOT}"
|
||||
fi
|
||||
|
||||
test "$(git -C "${CVAT_ROOT}" describe --tags --exact-match)" = "${CVAT_TAG}"
|
||||
test -f "${OVERRIDE_FILE}"
|
||||
guard_disk cvat-source
|
||||
|
||||
export CVAT_VERSION="${CVAT_TAG}"
|
||||
export CVAT_HOST="localhost"
|
||||
export CVAT_HTTP_PORT="8080"
|
||||
export COMPOSE_PROJECT_NAME="missioncore-cvat"
|
||||
|
||||
compose=(
|
||||
docker compose
|
||||
--project-directory "${CVAT_ROOT}"
|
||||
-f "${CVAT_ROOT}/docker-compose.yml"
|
||||
-f "${OVERRIDE_FILE}"
|
||||
)
|
||||
|
||||
"${compose[@]}" config --quiet
|
||||
mapfile -t images < <("${compose[@]}" config --images | sort -u)
|
||||
for image in "${images[@]}"; do
|
||||
guard_disk "before-pull:${image}"
|
||||
docker pull "${image}"
|
||||
guard_disk "after-pull:${image}"
|
||||
done
|
||||
|
||||
"${compose[@]}" up -d --no-build
|
||||
guard_disk cvat-started
|
||||
|
||||
deadline=$((SECONDS + 600))
|
||||
until curl --fail --silent --show-error http://localhost:8080/api/server/about \
|
||||
>/dev/null; do
|
||||
if (( SECONDS >= deadline )); then
|
||||
"${compose[@]}" ps
|
||||
echo "CVAT did not become ready within 600 seconds" >&2
|
||||
exit 4
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
report="${RUNTIME_ROOT}/reports/deployment-$(date -u +%Y%m%dT%H%M%SZ).txt"
|
||||
{
|
||||
printf 'cvat_tag=%s\n' "${CVAT_TAG}"
|
||||
printf 'cvat_commit=%s\n' "$(git -C "${CVAT_ROOT}" rev-parse HEAD)"
|
||||
printf 'docker_version=%s\n' "$(docker version --format '{{.Server.Version}}')"
|
||||
printf 'compose_version=%s\n' "$(docker compose version --short)"
|
||||
printf 'free_gib=%s\n' "$(free_gib)"
|
||||
printf 'generated_at_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
docker image inspect "${images[@]}" \
|
||||
--format 'image={{index .RepoTags 0}} id={{.Id}} size={{.Size}}'
|
||||
"${compose[@]}" ps
|
||||
} | tee "${report}"
|
||||
|
||||
printf 'CVAT ready at http://localhost:8080\nreport=%s\n' "${report}"
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
readonly FREE_GIB_FLOOR="${FREE_GIB_FLOOR:-360}"
|
||||
readonly WINDOWS_ROOT="/mnt/d/NDC_MISSIONCORE"
|
||||
readonly COMPUTE_ROOT="${WINDOWS_ROOT}/workspace/mission-core-compute"
|
||||
readonly CVAT_ROOT="${WINDOWS_ROOT}/runtime/annotation/cvat/source/cvat-v2.70.0"
|
||||
readonly OVERRIDE_FILE="${COMPUTE_ROOT}/cvat/docker-compose.override.yml"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "start_cvat_runtime.sh must run as root" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
free_gib="$(df -BG --output=avail "${WINDOWS_ROOT}" | tail -n 1 | tr -dc '0-9')"
|
||||
printf 'disk_guard stage=start free_gib=%s floor_gib=%s\n' \
|
||||
"${free_gib}" "${FREE_GIB_FLOOR}"
|
||||
if (( free_gib < FREE_GIB_FLOOR )); then
|
||||
echo "D: free-space floor crossed; refusing to start CVAT" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
test -f "${CVAT_ROOT}/docker-compose.yml"
|
||||
test -f "${OVERRIDE_FILE}"
|
||||
systemctl start docker
|
||||
|
||||
export CVAT_VERSION="v2.70.0"
|
||||
export CVAT_HOST="localhost"
|
||||
export CVAT_HTTP_PORT="8080"
|
||||
export COMPOSE_PROJECT_NAME="missioncore-cvat"
|
||||
|
||||
compose=(
|
||||
docker compose
|
||||
--project-directory "${CVAT_ROOT}"
|
||||
-f "${CVAT_ROOT}/docker-compose.yml"
|
||||
-f "${OVERRIDE_FILE}"
|
||||
)
|
||||
"${compose[@]}" up -d --no-build
|
||||
|
||||
deadline=$((SECONDS + 180))
|
||||
while true; do
|
||||
about_http="$(
|
||||
curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||
http://localhost:8080/api/server/about || true
|
||||
)"
|
||||
tasks_http="$(
|
||||
curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||
http://localhost:8080/api/tasks || true
|
||||
)"
|
||||
if [[ "${about_http}" == "200" ]] \
|
||||
&& [[ "${tasks_http}" == "200" || "${tasks_http}" == "401" || "${tasks_http}" == "403" ]]; then
|
||||
break
|
||||
fi
|
||||
if (( SECONDS >= deadline )); then
|
||||
"${compose[@]}" ps
|
||||
echo "CVAT did not become ready within 180 seconds" >&2
|
||||
exit 4
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
printf 'cvat_ready about_http=%s tasks_http=%s free_gib=%s\n' \
|
||||
"${about_http}" "${tasks_http}" "${free_gib}"
|
||||
|
||||
if [[ "${1:-}" == "--keepalive" ]]; then
|
||||
exec sleep infinity
|
||||
fi
|
||||
Reference in New Issue
Block a user