# services/cassandra/migration-jobs/artifact-copy-veles-to-cassandra-job.yaml # Suspended by default. Run only after scaling the Veles artifact exporter to 1. apiVersion: batch/v1 kind: Job metadata: name: cassandra-artifact-copy-from-veles-2 namespace: cassandra spec: suspend: true backoffLimit: 0 activeDeadlineSeconds: 43200 ttlSecondsAfterFinished: 86400 template: spec: serviceAccountName: cassandra-artifact-migration automountServiceAccountToken: false restartPolicy: Never nodeSelector: cassandra.bstein.dev/node-pool: oceanus tolerations: - key: veles.bstein.dev/simulation operator: Equal value: "true" effect: NoSchedule containers: - name: copy-artifacts image: python:3.12-alpine imagePullPolicy: IfNotPresent env: - name: SOURCE_URL value: http://veles-artifact-export-for-cassandra.veles.svc.cluster.local:8765 - name: DEST_ROOT value: /dest - name: ALLOW_NONEMPTY_DEST value: "0" - name: RESET_DESTINATION value: "1" command: ["python", "-c"] args: - | from __future__ import annotations from pathlib import Path import hashlib import json import os import shutil import tarfile import time import urllib.request SOURCE_URL = os.environ["SOURCE_URL"].rstrip("/") DEST = Path(os.environ.get("DEST_ROOT", "/dest")).resolve() ALLOW_NONEMPTY = os.environ.get("ALLOW_NONEMPTY_DEST", "") in {"1", "true", "yes"} RESET_DESTINATION = os.environ.get("RESET_DESTINATION", "") in {"1", "true", "yes"} EXCLUDE_TOP = {"lost+found", ".cassandra-artifact-migration"} REPORT_DIR = DEST / ".cassandra-artifact-migration" def visible_entries() -> list[Path]: return [entry for entry in DEST.iterdir() if entry.name not in EXCLUDE_TOP] def assert_safe_destination() -> None: DEST.mkdir(parents=True, exist_ok=True) entries = visible_entries() if entries and RESET_DESTINATION: for entry in entries: if entry.is_dir(): shutil.rmtree(entry) else: entry.unlink() entries = visible_entries() if entries and not ALLOW_NONEMPTY: names = ", ".join(entry.name for entry in entries[:10]) raise SystemExit(f"destination is not empty; refusing to merge into existing data: {names}") REPORT_DIR.mkdir(parents=True, exist_ok=True) def fetch_manifest() -> dict[str, object]: with urllib.request.urlopen(f"{SOURCE_URL}/manifest.json", timeout=120) as response: return json.loads(response.read().decode()) def wait_for_manifest() -> dict[str, object]: last_error = "" for _attempt in range(90): try: return fetch_manifest() except Exception as exc: last_error = str(exc) time.sleep(5) raise SystemExit(f"source artifact exporter did not become ready: {last_error}") def safe_extract() -> None: root_text = str(DEST) with urllib.request.urlopen(f"{SOURCE_URL}/archive.tar", timeout=600) as response: with tarfile.open(fileobj=response, mode="r|*") as archive: for member in archive: target = (DEST / member.name).resolve() if target != DEST and not str(target).startswith(root_text + os.sep): raise SystemExit(f"unsafe archive path: {member.name}") archive.extract(member, path=DEST) def digest_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def destination_manifest() -> dict[str, object]: files = [] total_bytes = 0 for path in sorted(DEST.rglob("*")): if not path.is_file(): continue rel = path.relative_to(DEST).as_posix() if rel.split("/", 1)[0] in EXCLUDE_TOP: continue size = path.stat().st_size total_bytes += size files.append({"path": rel, "size": size, "sha256": digest_file(path)}) manifest_text = "\n".join( f"{item['sha256']} {item['size']} {item['path']}" for item in files ) return { "schema": "cassandra.artifact_manifest.v1", "source": "cassandra-artifacts", "file_count": len(files), "total_bytes": total_bytes, "manifest_sha256": hashlib.sha256(manifest_text.encode()).hexdigest(), "files": files, } def comparable(manifest: dict[str, object]) -> dict[str, object]: return { "file_count": manifest.get("file_count"), "total_bytes": manifest.get("total_bytes"), "manifest_sha256": manifest.get("manifest_sha256"), "files": manifest.get("files"), } assert_safe_destination() source = wait_for_manifest() (REPORT_DIR / "source_manifest.json").write_text(json.dumps(source, indent=2, sort_keys=True)) safe_extract() dest = destination_manifest() (REPORT_DIR / "destination_manifest.json").write_text(json.dumps(dest, indent=2, sort_keys=True)) summary = { "source_file_count": source.get("file_count"), "destination_file_count": dest.get("file_count"), "source_total_bytes": source.get("total_bytes"), "destination_total_bytes": dest.get("total_bytes"), "source_manifest_sha256": source.get("manifest_sha256"), "destination_manifest_sha256": dest.get("manifest_sha256"), "verified": comparable(source) == comparable(dest), } (REPORT_DIR / "copy_summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True)) if not summary["verified"]: raise SystemExit(f"artifact copy verification failed: {summary}") print(json.dumps(summary, sort_keys=True), flush=True) resources: requests: cpu: 500m memory: 512Mi limits: cpu: "2" memory: 2Gi volumeMounts: - name: artifacts mountPath: /dest volumes: - name: artifacts persistentVolumeClaim: claimName: cassandra-artifacts