Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
301 lines
13 KiB
Python
301 lines
13 KiB
Python
"""Verified source-bundle installation primitives; no CLI, capture or timer start.
|
|
|
|
The supervised caller must hold backup/snapshot locks and verify disabled units
|
|
and the admitted route/profile. This module confines file writes and preserves
|
|
preimages; it does not grant that caller authority or activate a schedule.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
import stat
|
|
import tarfile
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
CONSUMER = (
|
|
"paired-backup.py", "snapshot-content-and-db.py", "mirror-paired-backup.py",
|
|
"check-paired-backup-health.py", "backup_capacity.py", "mirror-platform-runtime.json",
|
|
"backup_alerts.py", "notify-backup-alert.py", "observe-backup-alerts.py",
|
|
)
|
|
PLATFORM = (
|
|
"application_recovery_identities.py", "application_recovery_mirror.py",
|
|
"application_recovery_resources.py", "primary_application_archive.py",
|
|
)
|
|
UNITS = (
|
|
"coulomb-social-backup-freshness.service", "coulomb-social-paired-backup.service",
|
|
"coulomb-social-backup-freshness.timer", "coulomb-social-paired-backup.timer",
|
|
"coulomb-social-backup-alert@.service",
|
|
)
|
|
TARGETS = {**{"consumer/" + n: "/opt/coulomb-social-backup/" + n for n in CONSUMER},
|
|
**{"platform/" + n: "/opt/railiance-platform/scripts/" + n for n in PLATFORM},
|
|
**{"units/" + n: "/etc/systemd/system/" + n for n in UNITS},
|
|
"configuration/alerting.env": "/etc/coulomb-social-backup/alerting.env",
|
|
"evidence/backup-primary.lock": None,
|
|
"evidence/alertmanager-config.pending.json": None}
|
|
|
|
|
|
class Refused(ValueError):
|
|
pass
|
|
|
|
|
|
def sha(data):
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def read_bundle(data, archive_sha256, manifest_sha256):
|
|
if len(data) > 2 * 1024 * 1024 or sha(data) != archive_sha256:
|
|
raise Refused("archive_digest_or_size")
|
|
with tarfile.open(fileobj=io.BytesIO(data)) as archive:
|
|
members = archive.getmembers()
|
|
names = [m.name for m in members]
|
|
if (len(set(names)) != len(names) or set(names) != set(TARGETS) | {"manifest.json"}
|
|
or any(not m.isfile() or not 0 < m.size <= 512 * 1024 for m in members)):
|
|
raise Refused("unexpected_archive_member")
|
|
contents = {m.name: archive.extractfile(m).read() for m in members}
|
|
if sha(contents["manifest.json"]) != manifest_sha256:
|
|
raise Refused("manifest_digest")
|
|
manifest = json.loads(contents.pop("manifest.json"))
|
|
if manifest.get("schema") != "coulomb.backup-operating-source-bundle.v1":
|
|
raise Refused("manifest_schema")
|
|
files = manifest["files"]
|
|
if len(files) != len(TARGETS) or {f["member"] for f in files} != set(TARGETS):
|
|
raise Refused("manifest_members")
|
|
for entry in files:
|
|
name = entry["member"]
|
|
if entry["target"] != TARGETS[name]:
|
|
raise Refused("target_outside_reviewed_paths")
|
|
if entry["target"] and entry["mode"] != ("0644" if name.startswith("units/") else "0600"):
|
|
raise Refused("unexpected_target_mode")
|
|
if entry["sha256"] != sha(contents[name]) or entry["bytes"] != len(contents[name]):
|
|
raise Refused("member_digest_or_size")
|
|
return manifest, contents
|
|
|
|
|
|
def path_for(root, target):
|
|
if target not in TARGETS.values() or target is None:
|
|
raise Refused("target_outside_reviewed_paths")
|
|
return root / target.lstrip("/")
|
|
|
|
|
|
def trusted_parents(root, path):
|
|
current = path.parent
|
|
while True:
|
|
if current.exists() or current.is_symlink():
|
|
info = current.lstat()
|
|
if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid()
|
|
or info.st_mode & 0o022):
|
|
raise Refused("unsafe_target_parent")
|
|
if current == root:
|
|
break
|
|
if current == current.parent:
|
|
raise Refused("target_not_under_root")
|
|
current = current.parent
|
|
|
|
|
|
def fingerprint(path):
|
|
if not path.exists() and not path.is_symlink():
|
|
return {"exists": False}
|
|
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
|
|
with os.fdopen(fd, "rb") as stream:
|
|
info = os.fstat(stream.fileno())
|
|
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.geteuid() or info.st_nlink != 1:
|
|
raise Refused("unsafe_target_file")
|
|
data = stream.read(512 * 1024 + 1)
|
|
after = os.fstat(stream.fileno())
|
|
if len(data) > 512 * 1024 or (info.st_size, info.st_mtime_ns) != (
|
|
after.st_size, after.st_mtime_ns):
|
|
raise Refused("target_size_or_read_race")
|
|
return {"exists": True, "sha256": sha(data), "mode": stat.S_IMODE(info.st_mode),
|
|
"uid": info.st_uid, "gid": info.st_gid, "inode": info.st_ino,
|
|
"device": info.st_dev, "mtime_ns": info.st_mtime_ns}
|
|
|
|
|
|
def observe(root=Path("/")):
|
|
result = {}
|
|
for target in TARGETS.values():
|
|
if target is not None:
|
|
path = path_for(root, target)
|
|
trusted_parents(root, path)
|
|
result[target] = fingerprint(path)
|
|
return result
|
|
|
|
|
|
def sync_directory(path):
|
|
fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY)
|
|
try:
|
|
os.fsync(fd)
|
|
finally:
|
|
os.close(fd)
|
|
|
|
|
|
def journal(directory, record):
|
|
fd, name = tempfile.mkstemp(prefix=".journal-", dir=directory)
|
|
with os.fdopen(fd, "w") as stream:
|
|
json.dump(record, stream, indent=2)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
os.replace(name, directory / "receipt.json")
|
|
sync_directory(directory)
|
|
|
|
|
|
def stage(path, data, mode, gid=None):
|
|
fd, name = tempfile.mkstemp(prefix=".coulomb-install-", dir=path.parent)
|
|
try:
|
|
with os.fdopen(fd, "wb") as stream:
|
|
stream.write(data)
|
|
if gid is not None:
|
|
os.fchown(stream.fileno(), os.geteuid(), gid)
|
|
os.fchmod(stream.fileno(), mode)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
except BaseException:
|
|
Path(name).unlink(missing_ok=True)
|
|
raise
|
|
return Path(name)
|
|
|
|
|
|
def restore(root, directory, record, reload_manager):
|
|
"""Explicit rollback: refuse interference before undoing any installed file."""
|
|
if len({c["target"] for c in record["changes"]}) != len(record["changes"]):
|
|
raise Refused("duplicate_rollback_target")
|
|
for item in record["changes"]:
|
|
path = path_for(root, item["target"])
|
|
trusted_parents(root, path)
|
|
preimage = item["preimage"]
|
|
if not preimage.endswith(".before") or not preimage.removesuffix(".before").isdecimal():
|
|
raise Refused("invalid_preimage_path")
|
|
for key in ("temporary", "rollback_temporary"):
|
|
if key in item:
|
|
temporary = Path(item[key])
|
|
if (temporary.parent != path.parent
|
|
or not temporary.name.startswith(".coulomb-install-")):
|
|
raise Refused("staging_outside_reviewed_parent")
|
|
current = fingerprint(path_for(root, item["target"]))
|
|
if current not in (item["before"], item["staged"], item.get("restored")):
|
|
raise Refused("rollback_target_changed_requires_review")
|
|
if item["before"]["exists"]:
|
|
if sha((directory / item["preimage"]).read_bytes()) != item["before"]["sha256"]:
|
|
raise Refused("rollback_preimage_differs")
|
|
# Persist expected restoration identities before any replacement/unlink, so
|
|
# an interrupted rollback can distinguish its own writes from interference.
|
|
for item in record["changes"]:
|
|
path = path_for(root, item["target"])
|
|
if fingerprint(path) != item["staged"] or item.get("restored") is not None:
|
|
continue
|
|
before = item["before"]
|
|
if before["exists"]:
|
|
temporary = stage(path, (directory / item["preimage"]).read_bytes(),
|
|
before["mode"], before["gid"])
|
|
item.update(rollback_temporary=str(temporary), restored=fingerprint(temporary))
|
|
else:
|
|
item["restored"] = {"exists": False}
|
|
record["status"] = "rollback_prepared"
|
|
journal(directory, record)
|
|
for item in reversed(record["changes"]):
|
|
path = path_for(root, item["target"])
|
|
if fingerprint(path) == item["staged"]:
|
|
before = item["before"]
|
|
if before["exists"]:
|
|
temporary = Path(item["rollback_temporary"])
|
|
if fingerprint(temporary) != item["restored"]:
|
|
raise Refused("rollback_staging_changed_requires_review")
|
|
os.replace(temporary, path)
|
|
else:
|
|
path.unlink()
|
|
sync_directory(path.parent)
|
|
temporary = Path(item["temporary"])
|
|
if temporary.exists():
|
|
if fingerprint(temporary) != item["staged"]:
|
|
raise Refused("rollback_staging_changed_requires_review")
|
|
temporary.unlink()
|
|
reload_manager()
|
|
for item in record["changes"]:
|
|
now, before = fingerprint(path_for(root, item["target"])), item["before"]
|
|
keys = ("exists", "sha256", "mode", "uid", "gid") if before["exists"] else ("exists",)
|
|
if any(now.get(k) != before.get(k) for k in keys):
|
|
raise Refused("rollback_readback_failed")
|
|
record["status"] = "prior_files_restored_verified"
|
|
journal(directory, record)
|
|
return record
|
|
|
|
|
|
def install(manifest, contents, expected_before, directory, *, root, reload_manager,
|
|
verify_inactive):
|
|
"""Caller supplies admitted bytes, exact native preimage and locked execution context.
|
|
|
|
Shared existing platform helpers must already match their pinned content.
|
|
Unchanged files keep their inode/mode; changed existing Python files retain
|
|
their owner's execute bit. All installs finish with inactive-unit verification.
|
|
"""
|
|
if observe(root) != expected_before:
|
|
raise Refused("installed_source_drift")
|
|
verify_inactive()
|
|
for entry in manifest["files"]:
|
|
if entry["member"].startswith("platform/"):
|
|
before = expected_before[entry["target"]]
|
|
if before["exists"] and before["sha256"] != entry["sha256"]:
|
|
raise Refused("shared_platform_source_drift")
|
|
directory.mkdir(mode=0o700, parents=True, exist_ok=False)
|
|
record = {"schema": "coulomb.backup-source-installation.v1", "status": "staging",
|
|
"changes": [], "timers_activated": False, "capture_executed": False}
|
|
journal(directory, record)
|
|
try:
|
|
for entry in manifest["files"]:
|
|
target = entry["target"]
|
|
if target is None:
|
|
continue
|
|
before = expected_before[target]
|
|
if before["exists"] and before["sha256"] == entry["sha256"]:
|
|
continue
|
|
path = path_for(root, target)
|
|
trusted_parents(root, path)
|
|
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
preimage = str(len(record["changes"])) + ".before"
|
|
if before["exists"]:
|
|
data = path.read_bytes()
|
|
if sha(data) != before["sha256"]:
|
|
raise Refused("preimage_changed")
|
|
fd = os.open(directory / preimage, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
with os.fdopen(fd, "wb") as stream:
|
|
stream.write(data)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
mode = int(entry["mode"], 8)
|
|
if target.endswith(".py") and before["exists"]:
|
|
mode |= before["mode"] & 0o100
|
|
temporary = stage(path, contents[entry["member"]], mode,
|
|
before["gid"] if before["exists"] else None)
|
|
record["changes"].append({
|
|
"target": target, "before": before, "preimage": preimage,
|
|
"temporary": str(temporary), "staged": fingerprint(temporary)})
|
|
journal(directory, record)
|
|
if observe(root) != expected_before:
|
|
raise Refused("source_changed_during_staging")
|
|
record["status"] = "installing"
|
|
journal(directory, record)
|
|
for item in record["changes"]:
|
|
if fingerprint(path_for(root, item["target"])) != item["before"]:
|
|
raise Refused("source_changed_before_replace")
|
|
os.replace(item["temporary"], path_for(root, item["target"]))
|
|
sync_directory(path_for(root, item["target"]).parent)
|
|
reload_manager()
|
|
verify_inactive()
|
|
for item in record["changes"]:
|
|
if fingerprint(path_for(root, item["target"])) != item["staged"]:
|
|
raise Refused("installed_readback_differs")
|
|
expected_after = dict(expected_before)
|
|
expected_after.update({item["target"]: item["staged"] for item in record["changes"]})
|
|
if observe(root) != expected_after:
|
|
raise Refused("preserved_source_changed")
|
|
record["status"] = "sources_installed_units_inactive_verified"
|
|
record["verified_targets"] = len(expected_after)
|
|
journal(directory, record)
|
|
except Exception as error:
|
|
record.update(status="installation_requires_review", failure_type=type(error).__name__)
|
|
journal(directory, record)
|
|
raise
|
|
return record
|