Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m27s
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
160 lines
5.2 KiB
Python
160 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Local backup capacity checks; no deletion, provider contact or app changes.
|
|
|
|
Candidate limits need operating admission. Estimates reserve complete pipeline
|
|
scratch, while RLIMIT_FSIZE bounds individual newly written files. This is not
|
|
a filesystem quota or a reservation against unrelated writers.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import resource
|
|
import shutil
|
|
import stat
|
|
from pathlib import Path
|
|
|
|
FILE_LIMIT = 1024**3
|
|
STATE_BUDGET = 8 * 1024**3
|
|
MINIMUM_FREE = 8 * 1024**3
|
|
CONTROL_MARGIN = 16 * 1024**2
|
|
MINIMUM_INODES = 1024
|
|
|
|
|
|
class Refused(ValueError):
|
|
pass
|
|
|
|
|
|
def active_file_limit():
|
|
return resource.getrlimit(resource.RLIMIT_FSIZE)[0]
|
|
|
|
|
|
def enforce_file_limit(maximum=FILE_LIMIT):
|
|
soft, hard = resource.getrlimit(resource.RLIMIT_FSIZE)
|
|
effective = min(
|
|
[maximum] + [value for value in [soft, hard] if value != resource.RLIM_INFINITY]
|
|
)
|
|
if effective <= 0:
|
|
raise Refused("positive_file_limit_required")
|
|
resource.setrlimit(resource.RLIMIT_FSIZE, (effective, effective))
|
|
return effective
|
|
|
|
|
|
def disk_free(path):
|
|
return shutil.disk_usage(path).free
|
|
|
|
|
|
def free_inodes(path):
|
|
return os.statvfs(path).f_favail
|
|
|
|
|
|
def state_usage(root):
|
|
info = root.lstat()
|
|
if (
|
|
not stat.S_ISDIR(info.st_mode)
|
|
or info.st_uid != os.geteuid()
|
|
or info.st_mode & 0o077
|
|
or root.resolve() != root.absolute()
|
|
):
|
|
raise Refused("private_state_directory_required")
|
|
|
|
def onerror(error):
|
|
raise error
|
|
|
|
used = max(info.st_size, info.st_blocks * 512)
|
|
for directory, directories, files in os.walk(root, followlinks=False, onerror=onerror):
|
|
for name in directories + files:
|
|
item = (Path(directory) / name).lstat()
|
|
if (
|
|
not (stat.S_ISREG(item.st_mode) or stat.S_ISDIR(item.st_mode))
|
|
or item.st_uid != os.geteuid()
|
|
or item.st_mode & 0o077
|
|
or (stat.S_ISREG(item.st_mode) and item.st_nlink != 1)
|
|
):
|
|
raise Refused("unsafe_state_entry")
|
|
used += max(item.st_size, item.st_blocks * 512)
|
|
return used
|
|
|
|
|
|
def reserve_plan(
|
|
state,
|
|
*,
|
|
phase="capture",
|
|
require_file_limit=True,
|
|
maximum_file=FILE_LIMIT,
|
|
state_budget=STATE_BUDGET,
|
|
minimum_free=MINIMUM_FREE,
|
|
):
|
|
if phase not in {"capture", "resume", "mirror"} or any(
|
|
type(v) is not int or v <= 0 for v in [maximum_file, state_budget, minimum_free]
|
|
):
|
|
raise Refused("explicit_positive_capacity_profile_required")
|
|
# New capture: dump, content, tar, ciphertext, readback, retained local copy,
|
|
# and temporary rewritten inventory. Sealed retry needs three new large
|
|
# files; mirror needs only its fresh ciphertext readback.
|
|
slots = {"capture": 7, "resume": 3, "mirror": 1}[phase]
|
|
state_reserve = slots * maximum_file + CONTROL_MARGIN
|
|
# The existing command wrapper uses one anonymous output sink outside state.
|
|
disk_reserve = state_reserve + maximum_file
|
|
used, available, inodes = state_usage(state), disk_free(state), free_inodes(state)
|
|
limit = active_file_limit()
|
|
bounded = limit != resource.RLIM_INFINITY and 0 < limit <= maximum_file
|
|
reasons = []
|
|
if used + state_reserve > state_budget:
|
|
reasons.append("local_retention_budget_required")
|
|
if available < minimum_free + disk_reserve:
|
|
reasons.append("disk_headroom_required")
|
|
if inodes < MINIMUM_INODES:
|
|
reasons.append("inode_headroom_required")
|
|
if require_file_limit and not bounded:
|
|
reasons.append("bounded_file_executor_required")
|
|
return {
|
|
"schema": "coulomb.backup-capacity.v1",
|
|
"phase": phase,
|
|
"status": "refused" if reasons else ("ready" if bounded else "reviewed_unbounded_executor"),
|
|
"reasons": reasons,
|
|
"state_bytes": used,
|
|
"state_budget_bytes": state_budget,
|
|
"state_reserve_bytes": state_reserve,
|
|
"disk_reserve_bytes": disk_reserve,
|
|
"free_bytes": available,
|
|
"minimum_free_bytes": minimum_free,
|
|
"free_inodes": inodes,
|
|
"minimum_free_inodes": MINIMUM_INODES,
|
|
"maximum_new_file_bytes": maximum_file,
|
|
"active_file_limit_bytes": limit,
|
|
"bounded_file_executor": bounded,
|
|
"state_modified": False,
|
|
"retained_data_deleted": False,
|
|
"provider_contacted": False,
|
|
"filesystem_blocks_reserved": False,
|
|
"limits_owner_admitted": False,
|
|
}
|
|
|
|
|
|
def require_capacity(state, *, phase="capture"):
|
|
report = reserve_plan(state, phase=phase)
|
|
if report["status"] != "ready":
|
|
raise Refused("backup_capacity_review_required")
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--state-dir", type=Path, required=True)
|
|
parser.add_argument("--phase", choices=["capture", "resume", "mirror"], default="capture")
|
|
args = parser.parse_args()
|
|
print(
|
|
json.dumps(
|
|
reserve_plan(args.state_dir, phase=args.phase, require_file_limit=False), sort_keys=True
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except (ValueError, OSError):
|
|
raise SystemExit("Capacity review refused; inspect private state locally.")
|