538 lines
20 KiB
Python
Executable File
538 lines
20 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Safely and idempotently remove one verified recorder ``ready`` episode.
|
|
|
|
This helper intentionally has no command-line option for either root directory.
|
|
It can only move a direct child of the fixed recorder ``ready`` directory into
|
|
the fixed ``deleting`` staging directory and remove that tombstone. The caller
|
|
must provide both the episode id and the SHA-256 of the exact manifest that was
|
|
verified and durably published on the PS workstation.
|
|
|
|
Before recursive removal, an authorization ledger is fsynced in the fixed
|
|
``delete_ledger`` sibling directory. It survives partial tombstone deletion and
|
|
is removed only after the tree cleanup itself is durable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import fcntl
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import stat
|
|
import sys
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
BASE_ROOT = Path("/home/nvidia/tg3_data_collection")
|
|
READY_ROOT = BASE_ROOT / "ready"
|
|
DELETING_ROOT = BASE_ROOT / "deleting"
|
|
LEDGER_ROOT = BASE_ROOT / "delete_ledger"
|
|
SAFE_EPISODE_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$")
|
|
SAFE_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
MAX_MANIFEST_BYTES = 16 * 1024 * 1024
|
|
MAX_LEDGER_BYTES = 64 * 1024
|
|
LEDGER_SCHEMA_VERSION = 1
|
|
|
|
|
|
class DeleteSafetyError(RuntimeError):
|
|
"""The requested deletion did not satisfy the fixed safety contract."""
|
|
|
|
|
|
class DeleteCollisionError(DeleteSafetyError):
|
|
"""A conflicting ready/deleting episode exists and was left untouched."""
|
|
|
|
|
|
def safe_episode_name(value: str) -> bool:
|
|
return bool(SAFE_EPISODE_NAME.fullmatch(value)) and value not in (".", "..")
|
|
|
|
|
|
def _open_directory(path: Path) -> int:
|
|
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
try:
|
|
descriptor = os.open(path, flags)
|
|
except OSError as exc:
|
|
raise DeleteSafetyError(f"unsafe or missing directory: {path}: {exc}") from exc
|
|
try:
|
|
metadata = os.fstat(descriptor)
|
|
if not stat.S_ISDIR(metadata.st_mode):
|
|
raise DeleteSafetyError(f"not a directory: {path}")
|
|
return descriptor
|
|
except Exception:
|
|
os.close(descriptor)
|
|
raise
|
|
|
|
|
|
def _open_child_directory(parent_fd: int, name: str) -> int:
|
|
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
try:
|
|
descriptor = os.open(name, flags, dir_fd=parent_fd)
|
|
except OSError as exc:
|
|
raise DeleteSafetyError(f"unsafe or missing episode directory: {name}: {exc}") from exc
|
|
try:
|
|
if not stat.S_ISDIR(os.fstat(descriptor).st_mode):
|
|
raise DeleteSafetyError(f"episode is not a directory: {name}")
|
|
return descriptor
|
|
except Exception:
|
|
os.close(descriptor)
|
|
raise
|
|
|
|
|
|
def _read_regular_file(parent_fd: int, name: str, *, max_bytes: int) -> bytes:
|
|
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
|
|
try:
|
|
descriptor = os.open(name, flags, dir_fd=parent_fd)
|
|
except OSError as exc:
|
|
raise DeleteSafetyError(f"missing or unsafe regular file {name!r}: {exc}") from exc
|
|
try:
|
|
metadata = os.fstat(descriptor)
|
|
if not stat.S_ISREG(metadata.st_mode):
|
|
raise DeleteSafetyError(f"not a regular file: {name}")
|
|
if metadata.st_size > max_bytes:
|
|
raise DeleteSafetyError(f"file is too large: {name}")
|
|
chunks: list[bytes] = []
|
|
remaining = max_bytes + 1
|
|
while remaining:
|
|
block = os.read(descriptor, min(1024 * 1024, remaining))
|
|
if not block:
|
|
break
|
|
chunks.append(block)
|
|
remaining -= len(block)
|
|
payload = b"".join(chunks)
|
|
if len(payload) > max_bytes:
|
|
raise DeleteSafetyError(f"file is too large: {name}")
|
|
return payload
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def _validate_tree_entries(directory_fd: int) -> None:
|
|
"""Require the quarantined recorder tree to contain only dirs/files."""
|
|
|
|
for child in os.listdir(directory_fd):
|
|
if child in (".", "..") or "/" in child or "\x00" in child:
|
|
raise DeleteSafetyError(f"unsafe directory entry: {child!r}")
|
|
metadata = os.stat(child, dir_fd=directory_fd, follow_symlinks=False)
|
|
if stat.S_ISDIR(metadata.st_mode):
|
|
child_fd = _open_child_directory(directory_fd, child)
|
|
try:
|
|
_validate_tree_entries(child_fd)
|
|
finally:
|
|
os.close(child_fd)
|
|
elif not stat.S_ISREG(metadata.st_mode):
|
|
raise DeleteSafetyError(f"episode contains a symlink/special entry: {child}")
|
|
|
|
|
|
def _ledger_name(episode: str, manifest_sha256: str) -> str:
|
|
return f"{episode}.{manifest_sha256}.authorized.json"
|
|
|
|
|
|
def _ledger_payload(
|
|
episode: str, manifest_sha256: str, tombstone: str
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"schema_version": LEDGER_SCHEMA_VERSION,
|
|
"state": "AUTHORIZED_DELETE",
|
|
"episode_id": episode,
|
|
"manifest_sha256": manifest_sha256,
|
|
"tombstone": tombstone,
|
|
}
|
|
|
|
|
|
def _validate_ledger(
|
|
ledger_fd: int,
|
|
ledger_name: str,
|
|
*,
|
|
episode: str,
|
|
manifest_sha256: str,
|
|
tombstone: str,
|
|
) -> None:
|
|
raw = _read_regular_file(ledger_fd, ledger_name, max_bytes=MAX_LEDGER_BYTES)
|
|
try:
|
|
payload: Any = json.loads(raw.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise DeleteSafetyError(f"invalid deletion authorization ledger: {exc}") from exc
|
|
if payload != _ledger_payload(episode, manifest_sha256, tombstone):
|
|
raise DeleteSafetyError("deletion authorization ledger identity mismatch")
|
|
|
|
|
|
def _write_ledger(
|
|
ledger_fd: int,
|
|
ledger_name: str,
|
|
*,
|
|
episode: str,
|
|
manifest_sha256: str,
|
|
tombstone: str,
|
|
) -> None:
|
|
payload = (
|
|
json.dumps(
|
|
_ledger_payload(episode, manifest_sha256, tombstone),
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
)
|
|
+ "\n"
|
|
).encode("utf-8")
|
|
temporary = f".{ledger_name}.{uuid.uuid4().hex}.tmp"
|
|
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
|
|
descriptor = os.open(temporary, flags, 0o600, dir_fd=ledger_fd)
|
|
try:
|
|
view = memoryview(payload)
|
|
while view:
|
|
written = os.write(descriptor, view)
|
|
if written <= 0:
|
|
raise OSError("short write while creating deletion ledger")
|
|
view = view[written:]
|
|
os.fsync(descriptor)
|
|
finally:
|
|
os.close(descriptor)
|
|
try:
|
|
# A hard-link publish is atomic and refuses to replace any existing
|
|
# sidecar. The helper flock serializes legitimate writers as well.
|
|
os.link(
|
|
temporary,
|
|
ledger_name,
|
|
src_dir_fd=ledger_fd,
|
|
dst_dir_fd=ledger_fd,
|
|
follow_symlinks=False,
|
|
)
|
|
os.fsync(ledger_fd)
|
|
finally:
|
|
try:
|
|
os.unlink(temporary, dir_fd=ledger_fd)
|
|
os.fsync(ledger_fd)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
|
|
def _validate_episode(
|
|
parent_fd: int,
|
|
directory_name: str,
|
|
*,
|
|
episode: str,
|
|
expected_manifest_sha256: str,
|
|
) -> None:
|
|
episode_fd = _open_child_directory(parent_fd, directory_name)
|
|
try:
|
|
# READY must be a real regular file. Its contents are deliberately not
|
|
# trusted as a deletion credential; the exact manifest digest is.
|
|
_read_regular_file(episode_fd, "READY", max_bytes=4096)
|
|
raw_manifest = _read_regular_file(
|
|
episode_fd, "manifest.json", max_bytes=MAX_MANIFEST_BYTES
|
|
)
|
|
actual_digest = hashlib.sha256(raw_manifest).hexdigest()
|
|
if actual_digest != expected_manifest_sha256:
|
|
raise DeleteSafetyError(
|
|
"manifest SHA-256 mismatch: "
|
|
f"expected {expected_manifest_sha256}, got {actual_digest}"
|
|
)
|
|
try:
|
|
manifest: Any = json.loads(raw_manifest.decode("utf-8"))
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise DeleteSafetyError(f"invalid manifest JSON: {exc}") from exc
|
|
if not isinstance(manifest, dict):
|
|
raise DeleteSafetyError("manifest root is not an object")
|
|
if manifest.get("state") != "complete":
|
|
raise DeleteSafetyError("manifest state is not complete")
|
|
if manifest.get("episode_id") != episode:
|
|
raise DeleteSafetyError("manifest episode_id does not match target")
|
|
_validate_tree_entries(episode_fd)
|
|
finally:
|
|
os.close(episode_fd)
|
|
|
|
|
|
def _remove_tree_at(parent_fd: int, name: str) -> None:
|
|
"""Remove one child tree without following any symbolic link."""
|
|
|
|
directory_fd = _open_child_directory(parent_fd, name)
|
|
try:
|
|
for child in os.listdir(directory_fd):
|
|
if child in (".", "..") or "/" in child or "\x00" in child:
|
|
raise DeleteSafetyError(f"unsafe directory entry: {child!r}")
|
|
metadata = os.stat(child, dir_fd=directory_fd, follow_symlinks=False)
|
|
if stat.S_ISDIR(metadata.st_mode):
|
|
_remove_tree_at(directory_fd, child)
|
|
else:
|
|
# Symlinks and all non-directory entries are unlinked, never
|
|
# followed. Recorder output is expected to contain only files.
|
|
os.unlink(child, dir_fd=directory_fd)
|
|
os.fsync(directory_fd)
|
|
finally:
|
|
os.close(directory_fd)
|
|
os.rmdir(name, dir_fd=parent_fd)
|
|
|
|
|
|
def _child_kind(parent_fd: int, name: str) -> str:
|
|
try:
|
|
metadata = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
|
|
except FileNotFoundError:
|
|
return "absent"
|
|
if stat.S_ISDIR(metadata.st_mode):
|
|
return "directory"
|
|
if stat.S_ISLNK(metadata.st_mode):
|
|
return "symlink"
|
|
if stat.S_ISREG(metadata.st_mode):
|
|
return "regular"
|
|
return "other"
|
|
|
|
|
|
def delete_ready_episode(
|
|
episode: str,
|
|
manifest_sha256: str,
|
|
*,
|
|
ready_root: Path = READY_ROOT,
|
|
deleting_root: Path = DELETING_ROOT,
|
|
ledger_root: Path | None = None,
|
|
) -> dict[str, str]:
|
|
"""Compare, atomically quarantine, and remove exactly one ready episode.
|
|
|
|
Root arguments are injectable only for unit tests. The CLI below always
|
|
uses the immutable production constants, including a ledger directory that
|
|
is outside the recursively removed ``deleting`` tree.
|
|
"""
|
|
|
|
if not safe_episode_name(episode):
|
|
raise DeleteSafetyError(f"unsafe episode id: {episode!r}")
|
|
if not SAFE_SHA256.fullmatch(manifest_sha256):
|
|
raise DeleteSafetyError("manifest SHA-256 must be 64 lowercase hex characters")
|
|
|
|
ready_root = Path(ready_root)
|
|
deleting_root = Path(deleting_root)
|
|
ledger_root = Path(ledger_root or (deleting_root.parent / LEDGER_ROOT.name))
|
|
if not (
|
|
ready_root.parent == deleting_root.parent == ledger_root.parent
|
|
):
|
|
raise DeleteSafetyError("ready, deleting, and ledger roots must be siblings")
|
|
base_root = ready_root.parent
|
|
|
|
# Refuse a symlinked/wrong parent even in tests. resolve(strict=True) also
|
|
# proves that all existing parent components resolve to this exact path.
|
|
try:
|
|
if base_root.resolve(strict=True) != base_root.absolute():
|
|
raise DeleteSafetyError("base root must not contain symlinks")
|
|
if ready_root.resolve(strict=True) != ready_root.absolute():
|
|
raise DeleteSafetyError("ready root must not contain symlinks")
|
|
except FileNotFoundError as exc:
|
|
raise DeleteSafetyError(f"fixed recorder root is missing: {exc}") from exc
|
|
|
|
base_fd = _open_directory(base_root)
|
|
ready_fd = _open_directory(ready_root)
|
|
deleting_fd: int | None = None
|
|
ledger_fd: int | None = None
|
|
lock_fd: int | None = None
|
|
try:
|
|
base_stat = os.fstat(base_fd)
|
|
ready_stat = os.fstat(ready_fd)
|
|
if base_stat.st_dev != ready_stat.st_dev:
|
|
raise DeleteSafetyError("ready root is not on the recorder filesystem")
|
|
|
|
deleting_kind = _child_kind(base_fd, deleting_root.name)
|
|
if deleting_kind == "absent":
|
|
os.mkdir(deleting_root.name, mode=0o750, dir_fd=base_fd)
|
|
os.fsync(base_fd)
|
|
elif deleting_kind != "directory":
|
|
raise DeleteSafetyError("deleting root is not a real directory")
|
|
deleting_fd = _open_directory(deleting_root)
|
|
if os.fstat(deleting_fd).st_dev != ready_stat.st_dev:
|
|
raise DeleteSafetyError("deleting root is not on the ready filesystem")
|
|
|
|
ledger_kind = _child_kind(base_fd, ledger_root.name)
|
|
if ledger_kind == "absent":
|
|
os.mkdir(ledger_root.name, mode=0o750, dir_fd=base_fd)
|
|
os.fsync(base_fd)
|
|
elif ledger_kind != "directory":
|
|
raise DeleteSafetyError("delete ledger root is not a real directory")
|
|
ledger_fd = _open_directory(ledger_root)
|
|
if os.fstat(ledger_fd).st_dev != ready_stat.st_dev:
|
|
raise DeleteSafetyError("delete ledger is not on the ready filesystem")
|
|
|
|
lock_flags = os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0)
|
|
lock_fd = os.open(".delete_ready.lock", lock_flags, 0o600, dir_fd=deleting_fd)
|
|
if not stat.S_ISREG(os.fstat(lock_fd).st_mode):
|
|
raise DeleteSafetyError("deletion lock is not a regular file")
|
|
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
|
|
|
tombstone = f"{episode}.{manifest_sha256}.deleting"
|
|
authorization = _ledger_name(episode, manifest_sha256)
|
|
ready_kind = _child_kind(ready_fd, episode)
|
|
tombstone_kind = _child_kind(deleting_fd, tombstone)
|
|
authorization_kind = _child_kind(ledger_fd, authorization)
|
|
|
|
# Any other tombstone for this episode is a manifest-identity collision.
|
|
prefix = f"{episode}."
|
|
collisions = [
|
|
name
|
|
for name in os.listdir(deleting_fd)
|
|
if name.startswith(prefix)
|
|
and name.endswith(".deleting")
|
|
and name != tombstone
|
|
]
|
|
if collisions:
|
|
raise DeleteCollisionError(
|
|
f"conflicting deleting tombstone(s) exist: {sorted(collisions)!r}"
|
|
)
|
|
ledger_collisions = [
|
|
name
|
|
for name in os.listdir(ledger_fd)
|
|
if name.startswith(prefix)
|
|
and name.endswith(".authorized.json")
|
|
and name != authorization
|
|
]
|
|
if ledger_collisions:
|
|
raise DeleteCollisionError(
|
|
"conflicting deletion authorization(s) exist: "
|
|
f"{sorted(ledger_collisions)!r}"
|
|
)
|
|
if ready_kind not in ("absent", "directory"):
|
|
raise DeleteSafetyError(f"ready target is unsafe: {ready_kind}")
|
|
if tombstone_kind not in ("absent", "directory"):
|
|
raise DeleteSafetyError(f"deleting target is unsafe: {tombstone_kind}")
|
|
if authorization_kind not in ("absent", "regular"):
|
|
raise DeleteSafetyError(
|
|
f"deletion authorization is unsafe: {authorization_kind}"
|
|
)
|
|
if ready_kind == "directory" and tombstone_kind == "directory":
|
|
raise DeleteCollisionError("ready target and deleting tombstone both exist")
|
|
if ready_kind == "directory" and authorization_kind == "regular":
|
|
raise DeleteCollisionError(
|
|
"ready target exists alongside a stale deletion authorization"
|
|
)
|
|
|
|
if ready_kind == "directory":
|
|
_validate_episode(
|
|
ready_fd,
|
|
episode,
|
|
episode=episode,
|
|
expected_manifest_sha256=manifest_sha256,
|
|
)
|
|
os.rename(
|
|
episode,
|
|
tombstone,
|
|
src_dir_fd=ready_fd,
|
|
dst_dir_fd=deleting_fd,
|
|
)
|
|
os.fsync(ready_fd)
|
|
os.fsync(deleting_fd)
|
|
# Re-validate after the rename. If the source name was exchanged
|
|
# between the first comparison and rename, the unexpected tree is
|
|
# quarantined but is never unlinked.
|
|
_validate_episode(
|
|
deleting_fd,
|
|
tombstone,
|
|
episode=episode,
|
|
expected_manifest_sha256=manifest_sha256,
|
|
)
|
|
_write_ledger(
|
|
ledger_fd,
|
|
authorization,
|
|
episode=episode,
|
|
manifest_sha256=manifest_sha256,
|
|
tombstone=tombstone,
|
|
)
|
|
authorization_kind = "regular"
|
|
result_state = "deleted"
|
|
elif tombstone_kind == "directory":
|
|
if authorization_kind == "regular":
|
|
# The durable out-of-tree ledger remains usable even if an
|
|
# interrupted recursive removal already removed READY and the
|
|
# manifest from inside the tombstone.
|
|
_validate_ledger(
|
|
ledger_fd,
|
|
authorization,
|
|
episode=episode,
|
|
manifest_sha256=manifest_sha256,
|
|
tombstone=tombstone,
|
|
)
|
|
else:
|
|
# A crash between rename and ledger publication is recoverable
|
|
# only while the tombstone still proves its original identity.
|
|
_validate_episode(
|
|
deleting_fd,
|
|
tombstone,
|
|
episode=episode,
|
|
expected_manifest_sha256=manifest_sha256,
|
|
)
|
|
_write_ledger(
|
|
ledger_fd,
|
|
authorization,
|
|
episode=episode,
|
|
manifest_sha256=manifest_sha256,
|
|
tombstone=tombstone,
|
|
)
|
|
authorization_kind = "regular"
|
|
result_state = "resumed_delete"
|
|
else:
|
|
if authorization_kind == "regular":
|
|
# The tree was fully removed and only the final ledger cleanup
|
|
# (or its ACK) was interrupted.
|
|
_validate_ledger(
|
|
ledger_fd,
|
|
authorization,
|
|
episode=episode,
|
|
manifest_sha256=manifest_sha256,
|
|
tombstone=tombstone,
|
|
)
|
|
os.unlink(authorization, dir_fd=ledger_fd)
|
|
os.fsync(ledger_fd)
|
|
return {
|
|
"state": "already_absent",
|
|
"episode_id": episode,
|
|
"manifest_sha256": manifest_sha256,
|
|
}
|
|
|
|
_remove_tree_at(deleting_fd, tombstone)
|
|
os.fsync(deleting_fd)
|
|
# Delete authorization is deliberately the final artifact removed.
|
|
# If this unlink/ACK is lost, the next invocation validates it and
|
|
# safely returns already_absent.
|
|
_validate_ledger(
|
|
ledger_fd,
|
|
authorization,
|
|
episode=episode,
|
|
manifest_sha256=manifest_sha256,
|
|
tombstone=tombstone,
|
|
)
|
|
os.unlink(authorization, dir_fd=ledger_fd)
|
|
os.fsync(ledger_fd)
|
|
return {
|
|
"state": result_state,
|
|
"episode_id": episode,
|
|
"manifest_sha256": manifest_sha256,
|
|
}
|
|
finally:
|
|
if lock_fd is not None:
|
|
os.close(lock_fd)
|
|
if deleting_fd is not None:
|
|
os.close(deleting_fd)
|
|
if ledger_fd is not None:
|
|
os.close(ledger_fd)
|
|
os.close(ready_fd)
|
|
os.close(base_fd)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("episode_id")
|
|
parser.add_argument("manifest_sha256")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
try:
|
|
result = delete_ready_episode(args.episode_id, args.manifest_sha256)
|
|
except DeleteCollisionError as exc:
|
|
print(json.dumps({"state": "collision", "error": str(exc)}))
|
|
return 3
|
|
except (DeleteSafetyError, OSError) as exc:
|
|
print(json.dumps({"state": "refused", "error": str(exc)}))
|
|
return 2
|
|
print(json.dumps(result, sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|