Files
bf-infra/tools/oci2docker.py
Christoph Emsenhuber 60360fc3cc TP16.3 §2/§3 durch: cloudflared auf ap1/ap2/ap3, bf-public healthy mit 12 Connectoren
Merge von claude/nervous-williams-acf000 (d5b3ce3) plus Ergebnis der
Vor-Ort-Session vom 2026-08-31.

Ergebnis
- je ein cloudflared 2026.6.1 auf ap1/ap2/ap3, bf-public healthy/12
  (vorher 0 Connectoren), Flash je AP 54-55 MiB frei, WLAN zurueck.

Drei Runbook-Fehler aufgedeckt und korrigiert (§2.2, §3.3, §3.5, §8)
- Flash-Budget war Faktor 2,5 zu optimistisch: cloudflared entpackt
  69,5 MiB (gemessen mit docker export), nicht 28 MB -- das offizielle
  Image passt auf keinen der drei APs. Ersatz: eigenes FROM-scratch-Image
  mit 35,3 MiB aus dem statisch gelinkten Binary, Binary-SHA e93824f6
  gepinnt, Neubau byte-identisch. tools/build-cfd-image.py.
- docker save schreibt seit Docker 25 OCI statt docker-v1 -> RouterOS
  meldet "no config found in manifest". tools/oci2docker.py als Konverter.
- /container/envs nimmt list=, nicht name= (auch MikroMCPs
  manage_container_env ist dadurch fuer dieses Feld unbrauchbar).

Weitere Funde
- Der device-mode-Bestaetigungs-Boot installiert keine Pakete; erst
  Paket-Reboot, dann device-mode.
- "A - AVAILABLE" in /system package print ist ein Download-Katalog,
  kein Paket auf dem Geraet (Gegenprobe am Core: 2,9 MB frei, listet
  10,1 MB wifi-qcom als verfuegbar).
- MikroMCPs export_config exportiert unvollstaendig (301 B statt 6,2 KiB).

Offen und entscheidungsbeduerftig
- Compliance 128/135: §3.4 verlangt Direkt-Writes fuer ip address/nat,
  die unter GC-Kontrolle stehen -> sechs neue extra-Drifts, zwei je AP.
  Inhaltlich richtig, gehoeren ins Soll -- kein Re-Baseline, kein Rueckbau.
- Zwei Nautobot-Tokens sind in dieser Session ins Transkript geraten
  (unzureichende sed-Maskierung) -> Rotation noetig, TP30. Merkregel in
  CLAUDE.md ergaenzt.

Merge-Konflikte aufgeloest
- Nummernkonflikt: beide Branches hatten ein TP37. main behaelt TP37
  (Nautobot als Netzwerkdoku, User-Okay 08-30), OOB-Mgmt wird TP38.
  Braucht noch Christophs Okay.
- Drei CF-Rollback-Snapshots hatten gleichen Namen bei verschiedenem
  Inhalt -> aelterer Stand als *-vor-hostname-schnitt.json erhalten,
  keiner verworfen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 16:15:13 +02:00

70 lines
2.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""OCI-Archiv (docker save ab Docker 25 mit containerd-Store) -> klassisches
docker-archive v1, wie RouterOS /container es erwartet."""
import gzip, hashlib, json, os, shutil, sys, tarfile, tempfile
src, dst = sys.argv[1], sys.argv[2]
work = tempfile.mkdtemp(prefix="oci2docker.")
ext = os.path.join(work, "in")
out = os.path.join(work, "out")
os.makedirs(ext); os.makedirs(out)
with tarfile.open(src) as t:
t.extractall(ext)
man = json.load(open(os.path.join(ext, "manifest.json")))
if len(man) != 1:
sys.exit(f"erwarte genau 1 Image im Archiv, gefunden: {len(man)}")
entry = man[0]
# --- Config ---
cfg_src = os.path.join(ext, entry["Config"])
cfg_raw = open(cfg_src, "rb").read()
cfg_sha = hashlib.sha256(cfg_raw).hexdigest()
cfg_name = f"{cfg_sha}.json"
open(os.path.join(out, cfg_name), "wb").write(cfg_raw)
cfg = json.loads(cfg_raw)
# --- Layer: entpacken, falls gzip ---
layers_out = []
for i, lp in enumerate(entry["Layers"]):
blob = os.path.join(ext, lp)
with open(blob, "rb") as f:
magic = f.read(2)
d = os.path.join(out, f"layer{i:02d}")
os.makedirs(d)
target = os.path.join(d, "layer.tar")
if magic == b"\x1f\x8b":
with gzip.open(blob, "rb") as fi, open(target, "wb") as fo:
shutil.copyfileobj(fi, fo, 1024 * 1024)
else:
shutil.copyfile(blob, target)
open(os.path.join(d, "VERSION"), "w").write("1.0")
open(os.path.join(d, "json"), "w").write(json.dumps({"id": f"layer{i:02d}"}))
layers_out.append(f"layer{i:02d}/layer.tar")
# --- diff_ids gegenpruefen: sie muessen die UNKOMPRIMIERTEN Digests sein ---
diff_ids = cfg.get("rootfs", {}).get("diff_ids", [])
if len(diff_ids) != len(layers_out):
sys.exit(f"diff_ids ({len(diff_ids)}) != Layer ({len(layers_out)})")
for want, rel in zip(diff_ids, layers_out):
h = hashlib.sha256()
with open(os.path.join(out, rel), "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
got = "sha256:" + h.hexdigest()
if got != want:
sys.exit(f"Layer-Digest weicht ab:\n erwartet {want}\n erhalten {got}")
json.dump([{"Config": cfg_name,
"RepoTags": entry.get("RepoTags") or [],
"Layers": layers_out}],
open(os.path.join(out, "manifest.json"), "w"))
with tarfile.open(dst, "w") as t:
for n in sorted(os.listdir(out)):
t.add(os.path.join(out, n), arcname=n)
shutil.rmtree(work)
print(f"ok: {dst} ({os.path.getsize(dst)} Bytes, {len(layers_out)} Layer, diff_ids gepruegt)")